Skip to content

Commit ad819dc

Browse files
authored
Implement 'withoutEscapingSlashes' option and clean up string writing (#1885)
1 parent df28bc3 commit ad819dc

File tree

2 files changed

+31
-73
lines changed

2 files changed

+31
-73
lines changed

Sources/NewCodable/JSON/JSONWriter.swift

Lines changed: 24 additions & 73 deletions
Original file line numberDiff line numberDiff line change
@@ -24,15 +24,22 @@ internal struct JSONWriter: ~Copyable, ~Escapable {
2424

2525
private var indent = 0
2626
private let pretty: Bool
27-
private let withoutEscapingSlashes: Bool
27+
private let escapeTable: StaticString
28+
private let escapeLengths: [UInt8]
2829

2930
var data: GrowableEncodingBytes
3031

3132
@_lifetime(immortal)
3233
internal init(pretty: Bool, withoutEscapingSlashes: Bool) {
3334
self.data = .init()
3435
self.pretty = pretty
35-
self.withoutEscapingSlashes = withoutEscapingSlashes
36+
if withoutEscapingSlashes {
37+
self.escapeTable = Self.escapeTable
38+
self.escapeLengths = Self.escapeLens
39+
} else {
40+
self.escapeTable = Self.escapeTableWithEscapedForwardSlash
41+
self.escapeLengths = Self.escapeLensWithEscapedForwardSlash
42+
}
3643
}
3744

3845
@inline(__always)
@@ -235,14 +242,14 @@ internal struct JSONWriter: ~Copyable, ~Escapable {
235242
@inline(__always)
236243
func escapeLength(for byte: UInt8) -> Int {
237244
let byteInt = Int(byte)
238-
let escapeLen = Self.escapeLensWithEscapedForwardSlash[byteInt]
245+
let escapeLen = self.escapeLengths[byteInt]
239246
return Int(escapeLen)
240247
}
241248

242249
@inline(__always)
243250
@_lifetime(self: copy self)
244251
mutating func writeEscape(byte: UInt8, length: Int) {
245-
var ptr = Self.escapeTableWithEscapedForwardSlash.utf8Start
252+
var ptr = self.escapeTable.utf8Start
246253
ptr += 8 * Int(byte)
247254
self.write(pointer: ptr, count: length)
248255
}
@@ -253,13 +260,6 @@ internal struct JSONWriter: ~Copyable, ~Escapable {
253260
return
254261
}
255262

256-
@inline(__always)
257-
func appendAccumulatedBytes(from mark: UnsafePointer<UInt8>, to cursor: UnsafePointer<UInt8>) {
258-
if cursor > mark {
259-
write(pointer: mark, count: cursor-mark)
260-
}
261-
}
262-
263263
@inline(__always)
264264
func appendAccumulatedBytes(in span: Span<UInt8>, from mark: Int, to cursor: Int) {
265265
if cursor > mark {
@@ -275,7 +275,7 @@ internal struct JSONWriter: ~Copyable, ~Escapable {
275275
var shift = 0
276276
while shift < T.bitWidth {
277277
result |= 0x01 << shift
278-
shift += UInt8.bitWidth
278+
shift &+= UInt8.bitWidth
279279
}
280280
return result
281281
}
@@ -320,70 +320,36 @@ internal struct JSONWriter: ~Copyable, ~Escapable {
320320
return result != 0
321321
}
322322

323-
@inline(__always)
324-
func writeNonEscapingCharacters_SIMD(cursor: inout UnsafePointer<UInt8>, end: UnsafePointer<UInt8>) {
325-
let start = cursor
326-
if (end - cursor) > MemoryLayout<SIMD16<UInt8>>.size * 2 { // Only do SIMD if we have at least two vectors worth.
327-
let spaces = SIMD16<UInt8>(repeating: ._space)
328-
let quotes = SIMD16<UInt8>(repeating: ._quote)
329-
let slash = SIMD16<UInt8>(repeating: ._slash)
330-
let backslash = SIMD16<UInt8>(repeating: ._backslash)
331-
332-
let packedDistance = MemoryLayout<SIMD16<UInt8>>.size
333-
while (end - cursor) > packedDistance {
334-
let input = UnsafeRawPointer(cursor).loadUnaligned(as: SIMD16<UInt8>.self)
335-
let hasControlChars = input .< spaces
336-
let hasQuotes = input .== quotes
337-
let hasSlashes = input .== slash
338-
let hasBackslashes = input .== backslash
339-
let result = hasControlChars .| hasQuotes .| hasSlashes .| hasBackslashes
340-
if any(result) == false {
341-
cursor += packedDistance
342-
continue
343-
} else {
344-
let bitcast = unsafeBitCast(result, to: UInt128.self)
345-
let zeroBytes = bitcast.trailingZeroBitCount / 8
346-
cursor += zeroBytes
347-
break
348-
}
349-
}
350-
}
351-
let len = cursor - start
352-
if len > 0 {
353-
self.write(pointer: start, count: len)
354-
}
355-
}
356-
357323
@inline(__always)
358324
func writeNonEscapingCharacters_SIMD(in span: Span<UInt8>, from cursor: inout Int) {
359325
let start = cursor
360326
let end = span.count
361-
if (end - cursor) > MemoryLayout<SIMD16<UInt8>>.size * 2 { // Only do SIMD if we have at least two vectors worth.
327+
if (end &- cursor) > MemoryLayout<SIMD16<UInt8>>.size * 2 { // Only do SIMD if we have at least two vectors worth.
362328
let spaces = SIMD16<UInt8>(repeating: ._space)
363329
let quotes = SIMD16<UInt8>(repeating: ._quote)
364330
let slash = SIMD16<UInt8>(repeating: ._slash)
365331
let backslash = SIMD16<UInt8>(repeating: ._backslash)
366332

367333
let packedDistance = MemoryLayout<SIMD16<UInt8>>.size
368-
while (end - cursor) > packedDistance {
334+
while (end &- cursor) > packedDistance {
369335
let input = span.bytes.unsafeLoadUnaligned(fromUncheckedByteOffset: cursor, as: SIMD16<UInt8>.self)
370336
let hasControlChars = input .< spaces
371337
let hasQuotes = input .== quotes
372338
let hasSlashes = input .== slash
373339
let hasBackslashes = input .== backslash
374340
let result = hasControlChars .| hasQuotes .| hasSlashes .| hasBackslashes
375341
if any(result) == false {
376-
cursor += packedDistance
342+
cursor &+= packedDistance
377343
continue
378344
} else {
379345
let bitcast = unsafeBitCast(result, to: UInt128.self)
380346
let zeroBytes = bitcast.trailingZeroBitCount / 8
381-
cursor += zeroBytes
347+
cursor &+= zeroBytes
382348
break
383349
}
384350
}
385351
}
386-
let len = cursor - start
352+
let len = cursor &- start
387353
if len > 0 {
388354
appendAccumulatedBytes(in: span, from: start, to: cursor)
389355
}
@@ -393,54 +359,39 @@ internal struct JSONWriter: ~Copyable, ~Escapable {
393359
func skipNonEscapingCharacters_SWAR(in span: Span<UInt8>, at cursor: inout Int) -> Bool {
394360
do {
395361
let packedDistance = MemoryLayout<UInt64>.size
396-
if (span.count - cursor) >= packedDistance {
362+
if (span.count &- cursor) >= packedDistance {
397363
let integer = span.bytes.unsafeLoadUnaligned(fromUncheckedByteOffset: cursor, as: UInt64.self)
398364
let bytesBeforeEscape = bytesBeforeEscape(input: integer)
399-
cursor += bytesBeforeEscape
365+
cursor &+= bytesBeforeEscape
400366
return bytesBeforeEscape == MemoryLayout<UInt64>.size
401367
}
402368
}
403369

404370
do {
405371
let packedDistance = MemoryLayout<UInt32>.size
406-
if (span.count - cursor) >= packedDistance {
372+
if (span.count &- cursor) >= packedDistance {
407373
let integer = span.bytes.unsafeLoadUnaligned(fromUncheckedByteOffset: cursor, as: UInt32.self)
408374
let bytesBeforeEscape = bytesBeforeEscape(input: integer)
409-
cursor += bytesBeforeEscape
375+
cursor &+= bytesBeforeEscape
410376
return bytesBeforeEscape == MemoryLayout<UInt32>.size
411377
}
412378
}
413379

414380
return false
415381
}
416-
417-
@inline(__always)
418-
func processByte(at cursor: inout UnsafePointer<UInt8>, mark: inout UnsafePointer<UInt8>) {
419-
let byte = cursor.pointee
420-
let escapeLen = escapeLength(for: byte)
421-
if escapeLen > 0 {
422-
appendAccumulatedBytes(from: cursor, to: mark)
423-
writeEscape(byte: byte, length: escapeLen)
424-
cursor += 1
425-
mark = cursor
426-
} else {
427-
// Accumulate byte
428-
cursor += 1
429-
}
430-
}
431-
382+
432383
@inline(__always)
433384
func processByte(in span: Span<UInt8>, at cursor: inout Int, mark: inout Int) {
434385
let byte = span[unchecked: cursor]
435386
let escapeLen = escapeLength(for: byte)
436387
if escapeLen > 0 {
437388
appendAccumulatedBytes(in: span, from: mark, to: cursor)
438389
writeEscape(byte: byte, length: escapeLen)
439-
cursor += 1
390+
cursor &+= 1
440391
mark = cursor
441392
} else {
442393
// Accumulate byte
443-
cursor += 1
394+
cursor &+= 1
444395
}
445396
}
446397

Tests/NewCodableTests/JSONEncodingDecodingTests.swift

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -994,6 +994,13 @@ struct JSONEncodingDecodingTests {
994994
""".data(using: .utf8)!)
995995
}
996996

997+
@Test func jsonNonEscapedForwardSlashes() {
998+
_testRoundTrip(of: ["/":1], expectedJSON:
999+
"""
1000+
{"/":1}
1001+
""".data(using: .utf8)!, encoderOptions: .init(withoutEscapingSlashes: true))
1002+
}
1003+
9971004
@Test func jsonUnicodeCharacters() {
9981005
// UTF8:
9991006
// E9 96 86 E5 B4 AC EB B0 BA EB 80 AB E9 A2 92

0 commit comments

Comments
 (0)