Skip to content

Commit 95a76cc

Browse files
authored
Merge pull request #52 from orlandos-nl/jo/key-lookup-optimizations
Optimize object key lookup through reducing String initialization
2 parents 4de5273 + 3e2ea94 commit 95a76cc

File tree

10 files changed

+533
-120
lines changed

10 files changed

+533
-120
lines changed

Package.swift

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,12 @@ let package = Package(
1414
.library(
1515
name: "IkigaJSON",
1616
targets: ["IkigaJSON"]
17-
)
17+
),
18+
// Embedded Swift compatible library - no Foundation or NIO dependencies
19+
.library(
20+
name: "IkigaJSONCore",
21+
targets: ["_JSONCore"]
22+
),
1823
],
1924
dependencies: [
2025
// Dependencies declare other packages that this package depends on.
@@ -24,7 +29,11 @@ let package = Package(
2429
// Targets are the basic building blocks of a package. A target can define a module or a test suite.
2530
// Targets can depend on other targets in this package, and on products in packages which this package depends on.
2631
.target(
27-
name: "_JSONCore"
32+
name: "_JSONCore",
33+
swiftSettings: [
34+
// Enable strict concurrency for Embedded Swift compatibility
35+
.enableExperimentalFeature("StrictConcurrency"),
36+
]
2837
),
2938
.target(
3039
name: "_NIOJSON",

Sources/_JSONCore/Errors.swift

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -114,13 +114,11 @@ public enum JSONParserError: Error, Sendable {
114114
case unexpectedToken(line: Int, column: Int, token: UInt8, reason: Reason)
115115
}
116116

117-
public struct TypeConversionError<F: FixedWidthInteger & Sendable>: Error {
118-
let from: F
119-
let to: Any.Type
117+
public struct TypeConversionError<F: FixedWidthInteger & Sendable, T>: Error, Sendable {
118+
public let from: F
120119

121-
public init(from: F, to: Any.Type) {
120+
public init(from: F, to: T.Type) {
122121
self.from = from
123-
self.to = to
124122
}
125123
}
126124

Sources/_JSONCore/Parser/JSONParser+Parsing.swift

Lines changed: 63 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,7 @@ extension JSONTokenizer {
9999
}
100100

101101
try skipWhitespace() // needed because of the comma
102-
try scanStringLiteral()
102+
try scanObjectKey()
103103
try skipWhitespace()
104104

105105
guard nextByte() == .colon else {
@@ -280,4 +280,66 @@ extension JSONTokenizer {
280280

281281
throw JSONParserError.missingData(line: line, column: column)
282282
}
283+
284+
/// Scans an object key (string) and computes its hash for fast lookup
285+
@_optimize(speed)
286+
@inlinable
287+
mutating func scanObjectKey() throws(JSONParserError) {
288+
if currentByte != .quote {
289+
throw JSONParserError.unexpectedToken(line: line, column: column, token: currentByte, reason: .expectedObjectKey)
290+
}
291+
292+
var currentIndex: Int = 1
293+
var didEscape = false
294+
defer { advance(currentIndex) }
295+
296+
while currentIndex < count {
297+
defer { currentIndex = currentIndex &+ 1 }
298+
299+
let byte = self[currentIndex]
300+
301+
if byte == .quote {
302+
var escaped = false
303+
304+
if didEscape {
305+
var backwardsOffset = currentIndex &- 1
306+
307+
escapeLoop: while backwardsOffset >= 1 {
308+
defer { backwardsOffset = backwardsOffset &- 1 }
309+
310+
if self[backwardsOffset] == .backslash {
311+
escaped = !escaped
312+
} else {
313+
break escapeLoop
314+
}
315+
}
316+
}
317+
318+
if !escaped {
319+
let start = JSONSourcePosition(byteIndex: currentOffset)
320+
let stringToken = JSONToken.String(
321+
start: start,
322+
byteLength: currentIndex &+ 1,
323+
usesEscaping: didEscape
324+
)
325+
326+
// Compute hash of key bytes (excluding quotes)
327+
// Key content starts at offset 1 (after opening quote)
328+
// and ends just before `currentIndex` (which points to the closing quote).
329+
var hash: UInt32 = 2166136261 // FNV offset basis
330+
for i in 1..<currentIndex {
331+
hash ^= UInt32(self[i])
332+
hash &*= 16777619 // FNV prime
333+
}
334+
335+
destination.objectKeyFound(stringToken, hash: hash)
336+
return
337+
}
338+
} else if byte == .backslash {
339+
didEscape = true
340+
}
341+
}
342+
343+
throw JSONParserError.missingData(line: line, column: column)
344+
}
283345
}

Sources/_JSONCore/Parser/JSONTokenizerDestination.swift

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,17 @@ public protocol JSONTokenizerDestination {
1313
mutating func nullFound(_ null: JSONToken.Null)
1414
mutating func stringFound(_ string: JSONToken.String)
1515
mutating func numberFound(_ number: JSONToken.Number)
16+
17+
/// Called when an object key is found, with a pre-computed hash of the key bytes.
18+
/// Default implementation calls `stringFound()` for backward compatibility.
19+
mutating func objectKeyFound(_ string: JSONToken.String, hash: UInt32)
20+
}
21+
22+
extension JSONTokenizerDestination {
23+
@inlinable
24+
public mutating func objectKeyFound(_ string: JSONToken.String, hash: UInt32) {
25+
stringFound(string)
26+
}
1627
}
1728

1829
public enum JSONToken: Sendable, Hashable {

0 commit comments

Comments
 (0)