json-as 1.5.0 → 1.6.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (66) hide show
  1. package/CHANGELOG.md +10 -1
  2. package/README.md +42 -0
  3. package/assembly/deserialize/error.ts +21 -0
  4. package/assembly/deserialize/index/arbitrary.ts +29 -8
  5. package/assembly/deserialize/index/array.ts +2 -1
  6. package/assembly/deserialize/index/bool.ts +1 -1
  7. package/assembly/deserialize/index/object.ts +3 -0
  8. package/assembly/deserialize/index/string.ts +37 -0
  9. package/assembly/deserialize/index.ts +0 -1
  10. package/assembly/deserialize/naive/array/arbitrary.ts +3 -2
  11. package/assembly/deserialize/naive/array/array.ts +1 -0
  12. package/assembly/deserialize/naive/array/bool.ts +21 -11
  13. package/assembly/deserialize/naive/array/box.ts +13 -33
  14. package/assembly/deserialize/naive/array/float.ts +23 -13
  15. package/assembly/deserialize/naive/array/integer.ts +33 -21
  16. package/assembly/deserialize/naive/array/object.ts +1 -0
  17. package/assembly/deserialize/naive/array/string.ts +29 -16
  18. package/assembly/deserialize/naive/bool.ts +17 -5
  19. package/assembly/deserialize/naive/float.ts +1 -2
  20. package/assembly/deserialize/naive/map.ts +339 -39
  21. package/assembly/deserialize/naive/object.ts +122 -65
  22. package/assembly/deserialize/naive/raw.ts +13 -1
  23. package/assembly/deserialize/naive/set.ts +0 -1
  24. package/assembly/deserialize/naive/staticarray.ts +3 -2
  25. package/assembly/deserialize/naive/string.ts +41 -52
  26. package/assembly/deserialize/parseMode.ts +216 -0
  27. package/assembly/deserialize/simd/array/integer.ts +6 -3
  28. package/assembly/deserialize/simd/float.ts +35 -29
  29. package/assembly/deserialize/simd/string.ts +79 -17
  30. package/assembly/deserialize/string-validation.ts +34 -0
  31. package/assembly/deserialize/swar/array/array.ts +63 -13
  32. package/assembly/deserialize/swar/array/bool.ts +14 -3
  33. package/assembly/deserialize/swar/array/float.ts +394 -13
  34. package/assembly/deserialize/swar/array/generic.ts +12 -2
  35. package/assembly/deserialize/swar/array/integer.ts +22 -62
  36. package/assembly/deserialize/swar/array/object.ts +11 -2
  37. package/assembly/deserialize/swar/array/shared.ts +1 -1
  38. package/assembly/deserialize/swar/array/string.ts +34 -5
  39. package/assembly/deserialize/swar/array/struct.ts +109 -15
  40. package/assembly/deserialize/swar/float.ts +19 -12
  41. package/assembly/deserialize/swar/string.ts +70 -111
  42. package/assembly/index.d.ts +6 -0
  43. package/assembly/index.ts +411 -33
  44. package/assembly/serialize/index/bool.ts +1 -1
  45. package/assembly/serialize/index/float.ts +1 -5
  46. package/assembly/serialize/index/integer.ts +1 -1
  47. package/assembly/serialize/index/object.ts +17 -0
  48. package/assembly/serialize/naive/array.ts +64 -0
  49. package/assembly/serialize/naive/float.ts +0 -5
  50. package/assembly/serialize/naive/map.ts +37 -0
  51. package/assembly/serialize/naive/set.ts +0 -1
  52. package/assembly/serialize/naive/staticarray.ts +0 -1
  53. package/assembly/serialize/naive/string.ts +0 -6
  54. package/assembly/serialize/naive/typedarray.ts +0 -1
  55. package/assembly/util/eisel-lemire.ts +179 -0
  56. package/assembly/util/prettyWhitespaceSimd.ts +69 -0
  57. package/assembly/util/scanValueEndSimd.ts +59 -36
  58. package/assembly/util/validateJson.ts +338 -0
  59. package/lib/as-bs.ts +28 -6
  60. package/package.json +17 -7
  61. package/transform/lib/index.d.ts +6 -0
  62. package/transform/lib/index.js +773 -112
  63. package/transform/lib/types.d.ts +1 -0
  64. package/transform/lib/types.js +3 -0
  65. package/assembly/deserialize/index/struct.ts +0 -1
  66. package/assembly/deserialize/naive/struct.ts +0 -21
@@ -0,0 +1,216 @@
1
+ let SIMD_PRETTY_PARSE = false;
2
+ const ENABLE_EXACT_SOURCE_TRACES = true;
3
+
4
+ // Steady-state SIMD string-field trace. JSON source strings and parsed field
5
+ // strings are immutable at the language level, so the tuple
6
+ // (source position, destination slot, current string reference) proves that a
7
+ // field still contains the bytes produced by the prior parse. Any mutation or
8
+ // graph replacement changes at least one tuple component and takes the normal
9
+ // decoder, which refreshes that trace slot.
10
+ let STRING_TRACE_DEPTH = 0;
11
+ let STRING_TRACE_ACTIVE = false;
12
+ let STRING_TRACE_COMPLETE = false;
13
+ let STRING_TRACE_SOURCE: string | null = null;
14
+ let STRING_TRACE_OUT: usize = 0;
15
+ let STRING_TRACE_TYPE: u32 = 0;
16
+ let STRING_TRACE_INDEX = 0;
17
+ let STRING_TRACE_COUNT = 0;
18
+ let STRING_TRACE_SLOT = 0;
19
+ const STRING_TRACE_FIELDS = new Array<usize>();
20
+ const STRING_TRACE_STARTS = new Array<usize>();
21
+ const STRING_TRACE_ENDS = new Array<usize>();
22
+ const STRING_TRACE_VALUES = new Array<string>();
23
+ let OBJECT_TRACE_INDEX = 0;
24
+ let OBJECT_TRACE_COUNT = 0;
25
+ const OBJECT_TRACE_FIELDS = new Array<usize>();
26
+ const OBJECT_TRACE_STARTS = new Array<usize>();
27
+ const OBJECT_TRACE_TYPES = new Array<string>();
28
+ const OBJECT_TRACE_MASKS = new Array<u64>();
29
+ const OBJECT_TRACE_TIERS = new Array<u8>();
30
+ const OBJECT_TRACE_SEPARATORS = new Array<usize>();
31
+
32
+
33
+ @inline
34
+ export function isPrettyParse(): bool {
35
+ return SIMD_PRETTY_PARSE;
36
+ }
37
+
38
+
39
+ @inline
40
+ export function setPrettyParse(value: bool): void {
41
+ SIMD_PRETTY_PARSE = value;
42
+ }
43
+
44
+ /** Enter one public parse call, suspending trace use in nested parses. */
45
+ @inline
46
+ export function beginStringFieldTrace(
47
+ source: string,
48
+ out: usize,
49
+ typeId: u32,
50
+ ): void {
51
+ if (!ENABLE_EXACT_SOURCE_TRACES) return;
52
+ STRING_TRACE_DEPTH++;
53
+ if (!ASC_FEATURE_SIMD || STRING_TRACE_DEPTH != 1 || out == 0) return;
54
+
55
+ const sourcePtr = changetype<usize>(source);
56
+ const sameRoot =
57
+ STRING_TRACE_COMPLETE &&
58
+ STRING_TRACE_OUT == out &&
59
+ STRING_TRACE_TYPE == typeId &&
60
+ changetype<usize>(STRING_TRACE_SOURCE) == sourcePtr;
61
+ if (!sameRoot) {
62
+ STRING_TRACE_SOURCE = source;
63
+ STRING_TRACE_OUT = out;
64
+ STRING_TRACE_TYPE = typeId;
65
+ STRING_TRACE_COUNT = 0;
66
+ STRING_TRACE_FIELDS.length = 0;
67
+ STRING_TRACE_STARTS.length = 0;
68
+ STRING_TRACE_ENDS.length = 0;
69
+ STRING_TRACE_VALUES.length = 0;
70
+ OBJECT_TRACE_COUNT = 0;
71
+ OBJECT_TRACE_FIELDS.length = 0;
72
+ OBJECT_TRACE_STARTS.length = 0;
73
+ OBJECT_TRACE_TYPES.length = 0;
74
+ OBJECT_TRACE_MASKS.length = 0;
75
+ OBJECT_TRACE_TIERS.length = 0;
76
+ OBJECT_TRACE_SEPARATORS.length = 0;
77
+ }
78
+ STRING_TRACE_INDEX = 0;
79
+ OBJECT_TRACE_INDEX = 0;
80
+ STRING_TRACE_COMPLETE = false;
81
+ STRING_TRACE_ACTIVE = true;
82
+ }
83
+
84
+ /** Leave the matching public parse call. */
85
+ @inline
86
+ export function endStringFieldTrace(success: bool): void {
87
+ if (!ENABLE_EXACT_SOURCE_TRACES) return;
88
+ if (STRING_TRACE_DEPTH == 1) {
89
+ STRING_TRACE_ACTIVE = false;
90
+ STRING_TRACE_COMPLETE = success;
91
+ }
92
+ STRING_TRACE_DEPTH--;
93
+ }
94
+
95
+ /** Return the cached cursor, or zero when this field must be decoded. */
96
+ @inline
97
+ export function probeStringFieldTrace(
98
+ dstFieldPtr: usize,
99
+ payloadStart: usize,
100
+ ): usize {
101
+ if (!ENABLE_EXACT_SOURCE_TRACES) return 0;
102
+ if (!STRING_TRACE_ACTIVE || STRING_TRACE_DEPTH != 1) return 0;
103
+ const slot = STRING_TRACE_INDEX++;
104
+ STRING_TRACE_SLOT = slot;
105
+ if (
106
+ slot < STRING_TRACE_COUNT &&
107
+ unchecked(STRING_TRACE_FIELDS[slot]) == dstFieldPtr &&
108
+ unchecked(STRING_TRACE_STARTS[slot]) == payloadStart &&
109
+ load<usize>(dstFieldPtr) ==
110
+ changetype<usize>(unchecked(STRING_TRACE_VALUES[slot]))
111
+ )
112
+ return unchecked(STRING_TRACE_ENDS[slot]);
113
+ return 0;
114
+ }
115
+
116
+ /** Record the successful decode corresponding to the most recent probe. */
117
+ @inline
118
+ export function recordStringFieldTrace(
119
+ dstFieldPtr: usize,
120
+ payloadStart: usize,
121
+ next: usize,
122
+ ): void {
123
+ if (!ENABLE_EXACT_SOURCE_TRACES) return;
124
+ if (!STRING_TRACE_ACTIVE || STRING_TRACE_DEPTH != 1) return;
125
+ const slot = STRING_TRACE_SLOT;
126
+ const value = load<string>(dstFieldPtr);
127
+ if (slot < STRING_TRACE_COUNT) {
128
+ unchecked((STRING_TRACE_FIELDS[slot] = dstFieldPtr));
129
+ unchecked((STRING_TRACE_STARTS[slot] = payloadStart));
130
+ unchecked((STRING_TRACE_ENDS[slot] = next));
131
+ unchecked((STRING_TRACE_VALUES[slot] = value));
132
+ } else {
133
+ STRING_TRACE_FIELDS.push(dstFieldPtr);
134
+ STRING_TRACE_STARTS.push(payloadStart);
135
+ STRING_TRACE_ENDS.push(next);
136
+ STRING_TRACE_VALUES.push(value);
137
+ STRING_TRACE_COUNT = slot + 1;
138
+ }
139
+ }
140
+
141
+ /**
142
+ * Reserve or find one optional-struct trace slot.
143
+ *
144
+ * Returns i32.MIN_VALUE when tracing is inactive, a non-negative slot on a
145
+ * miss, or `~slot` (negative) on a hit. Reserving at begin keeps nested object
146
+ * records well-ordered even though a parent only commits after its children.
147
+ */
148
+ @inline
149
+ export function beginObjectFieldTrace(
150
+ dst: usize,
151
+ srcStart: usize,
152
+ typeTag: string,
153
+ ): i32 {
154
+ if (!ENABLE_EXACT_SOURCE_TRACES) return i32.MIN_VALUE;
155
+ if (!STRING_TRACE_ACTIVE || STRING_TRACE_DEPTH != 1) return i32.MIN_VALUE;
156
+ const slot = OBJECT_TRACE_INDEX++;
157
+ if (
158
+ slot < OBJECT_TRACE_COUNT &&
159
+ unchecked(OBJECT_TRACE_FIELDS[slot]) == dst &&
160
+ unchecked(OBJECT_TRACE_STARTS[slot]) == srcStart &&
161
+ unchecked(OBJECT_TRACE_TIERS[slot]) != 0 &&
162
+ changetype<usize>(unchecked(OBJECT_TRACE_TYPES[slot])) ==
163
+ changetype<usize>(typeTag)
164
+ )
165
+ return ~slot;
166
+
167
+ if (slot < OBJECT_TRACE_COUNT) {
168
+ unchecked((OBJECT_TRACE_FIELDS[slot] = dst));
169
+ unchecked((OBJECT_TRACE_STARTS[slot] = srcStart));
170
+ unchecked((OBJECT_TRACE_TYPES[slot] = typeTag));
171
+ unchecked((OBJECT_TRACE_MASKS[slot] = 0));
172
+ unchecked((OBJECT_TRACE_TIERS[slot] = 0));
173
+ unchecked((OBJECT_TRACE_SEPARATORS[slot] = 0));
174
+ } else {
175
+ OBJECT_TRACE_FIELDS.push(dst);
176
+ OBJECT_TRACE_STARTS.push(srcStart);
177
+ OBJECT_TRACE_TYPES.push(typeTag);
178
+ OBJECT_TRACE_MASKS.push(0);
179
+ OBJECT_TRACE_TIERS.push(0);
180
+ OBJECT_TRACE_SEPARATORS.push(0);
181
+ OBJECT_TRACE_COUNT = slot + 1;
182
+ }
183
+ return slot;
184
+ }
185
+
186
+
187
+ @inline
188
+ export function objectFieldTraceMask(slot: i32): u64 {
189
+ return unchecked(OBJECT_TRACE_MASKS[slot]);
190
+ }
191
+
192
+
193
+ @inline
194
+ export function objectFieldTraceTier(slot: i32): u8 {
195
+ return unchecked(OBJECT_TRACE_TIERS[slot]);
196
+ }
197
+
198
+
199
+ @inline
200
+ export function objectFieldTraceSeparator(slot: i32): usize {
201
+ return unchecked(OBJECT_TRACE_SEPARATORS[slot]);
202
+ }
203
+
204
+
205
+ @inline
206
+ export function recordObjectFieldTrace(
207
+ slot: i32,
208
+ present: u64,
209
+ tier: u8,
210
+ separator: usize = 0,
211
+ ): void {
212
+ if (slot < 0 || slot >= OBJECT_TRACE_COUNT) return;
213
+ unchecked((OBJECT_TRACE_MASKS[slot] = present));
214
+ unchecked((OBJECT_TRACE_TIERS[slot] = tier));
215
+ unchecked((OBJECT_TRACE_SEPARATORS[slot] = separator));
216
+ }
@@ -397,7 +397,10 @@ export function deserializeIntegerArray_SIMD<T extends number[]>(
397
397
  dst || changetype<usize>(instantiate<T>()),
398
398
  );
399
399
  const originalSrcStart = srcStart;
400
- const reusableLength = out.length;
400
+ const reusableLength = load<i32>(
401
+ changetype<usize>(out),
402
+ offsetof<T>("length_"),
403
+ );
401
404
 
402
405
  if (reusableLength != 0) {
403
406
  const dataStart = out.dataStart;
@@ -455,7 +458,7 @@ export function deserializeIntegerArray_SIMD<T extends number[]>(
455
458
  continue;
456
459
  }
457
460
  if (code == BRACKET_RIGHT) {
458
- out.length = index;
461
+ if (index != reusableLength) out.length = index;
459
462
  return out;
460
463
  }
461
464
  break;
@@ -494,7 +497,7 @@ export function deserializeIntegerArray_SIMD<T extends number[]>(
494
497
  continue;
495
498
  }
496
499
  if (code == BRACKET_RIGHT) {
497
- out.length = index;
500
+ if (index != reusableLength) out.length = index;
498
501
  return out;
499
502
  }
500
503
  break;
@@ -1,7 +1,7 @@
1
- // SIMD float deserializers. Same Lemire-style fast path / `scientific()` /
2
- // `f*.parse` cascade as `swar/float.ts`, but the fractional digit loop uses
3
- // `parse8Digits_SIMD` (16 bytes / 8 digits) before falling through to
4
- // `parse4Digits_PairMul` (8 bytes / 4 digits) and finally the scalar tail.
1
+ // SIMD float deserializers. Same Eisel-Lemire / `scientific()` / `f*.parse`
2
+ // cascade as `swar/float.ts`, but the fractional digit loop starts with a
3
+ // `parse16Digits_SIMD` stride before falling through to `parse4Digits_PairMul`
4
+ // and finally the scalar tail.
5
5
  //
6
6
  // Output is bit-identical to `f64.parse` / `f32.parse` for every input - the
7
7
  // SIMD strides only change how the u64 mantissa is accumulated, not what it
@@ -14,6 +14,7 @@ import { ptrToStr } from "../../util/ptrToStr";
14
14
  import { parse4Digits_PairMul } from "../../util/swar-int";
15
15
  import { parse16Digits_SIMD } from "../../util/simd-int";
16
16
  import { scientific } from "../../util/scientific";
17
+ import { eiselLemire22 } from "../../util/eisel-lemire";
17
18
  import { loadPow10, MAX_EXACT_MANTISSA, MAX_EXACT_POW10 } from "../swar/float";
18
19
 
19
20
  const ASCII_PLUS: u16 = 43;
@@ -56,48 +57,47 @@ export function deserializeFloat_SIMD<T>(srcStart: usize, srcEnd: usize): T {
56
57
  }
57
58
 
58
59
  let mantissa: u64 = 0;
59
- let intDigits: i32 = 0;
60
+ const intStart = p;
60
61
  while (p < srcEnd) {
61
62
  const d = <u32>load<u16>(p) - ASCII_ZERO;
62
63
  if (d > 9) break;
63
64
  mantissa = mantissa * 10 + <u64>d;
64
- intDigits++;
65
65
  p += 2;
66
66
  }
67
+ const intDigits = <i32>((p - intStart) >> 1);
67
68
 
68
- // Fractional part: parse8 SIMD stride → parse4 SWAR stride → scalar tail.
69
- // `intDigits + fracDigits <= 11` gate keeps `mantissa * 10^8 + parsed8`
70
- // under u64 max even when the integer part is large.
71
- let fracDigits: i32 = 0;
69
+ // Fractional part: one parse16 SIMD stride → parse4 SWAR stride →
70
+ // scalar tail. The parse16 gate keeps the combined mantissa within u64.
71
+ let fracStart = p;
72
72
  if (p < srcEnd && load<u16>(p) == ASCII_DOT) {
73
73
  p += 2;
74
+ fracStart = p;
74
75
  // parse16 SIMD only fires on long fractions (>=16 digits ahead, with
75
76
  // <=3 integer digits to keep mantissa * 1e16 under u64 max). Rare in
76
77
  // typical JSON but a big win when it does fire (8 digits per stride in
77
78
  // SWAR's parse8 was benched even/worse than parse4, so we skip parse8
78
79
  // entirely and let parse4 handle the 4-15 char tail).
79
- while (p + 30 < srcEnd && intDigits + fracDigits <= 3) {
80
+ if (p + 30 < srcEnd && intDigits <= 3) {
80
81
  const parsed = parse16Digits_SIMD(p);
81
- if (parsed == U64.MAX_VALUE) break;
82
- mantissa = mantissa * 10_000_000_000_000_000 + parsed;
83
- fracDigits += 16;
84
- p += 32;
82
+ if (parsed != U64.MAX_VALUE) {
83
+ mantissa = mantissa * 10_000_000_000_000_000 + parsed;
84
+ p += 32;
85
+ }
85
86
  }
86
87
  while (p + 6 < srcEnd) {
87
88
  const parsed = parse4Digits_PairMul(load<u64>(p));
88
89
  if (parsed == U32.MAX_VALUE) break;
89
90
  mantissa = mantissa * 10_000 + <u64>parsed;
90
- fracDigits += 4;
91
91
  p += 8;
92
92
  }
93
93
  while (p < srcEnd) {
94
94
  const d = <u32>load<u16>(p) - ASCII_ZERO;
95
95
  if (d > 9) break;
96
96
  mantissa = mantissa * 10 + <u64>d;
97
- fracDigits++;
98
97
  p += 2;
99
98
  }
100
99
  }
100
+ const fracDigits = <i32>((p - fracStart) >> 1);
101
101
 
102
102
  const mantDigits = intDigits + fracDigits;
103
103
  if (mantDigits == 0) return fallback<T>(origStart, srcEnd);
@@ -148,7 +148,10 @@ export function deserializeFloat_SIMD<T>(srcStart: usize, srcEnd: usize): T {
148
148
  result /= loadPow10(<u32>-exponent);
149
149
  }
150
150
  } else if (mantDigits <= 19) {
151
- result = scientific(mantissa, exponent);
151
+ result =
152
+ exponent >= -22 && exponent <= 22
153
+ ? eiselLemire22(mantissa, exponent)
154
+ : scientific(mantissa, exponent);
152
155
  } else {
153
156
  return fallback<T>(origStart, srcEnd);
154
157
  }
@@ -178,45 +181,45 @@ export function deserializeFloatField_SIMD<T extends number>(
178
181
  }
179
182
 
180
183
  let mantissa: u64 = 0;
181
- let intDigits: i32 = 0;
184
+ const intStart = p;
182
185
  while (p < srcEnd) {
183
186
  const d = <u32>load<u16>(p) - ASCII_ZERO;
184
187
  if (d > 9) break;
185
188
  mantissa = mantissa * 10 + <u64>d;
186
- intDigits++;
187
189
  p += 2;
188
190
  }
191
+ const intDigits = <i32>((p - intStart) >> 1);
189
192
 
190
- let fracDigits: i32 = 0;
193
+ let fracStart = p;
191
194
  if (p < srcEnd && load<u16>(p) == ASCII_DOT) {
192
195
  p += 2;
196
+ fracStart = p;
193
197
  // parse16 SIMD only fires on long fractions (>=16 digits ahead, with
194
198
  // <=3 integer digits to keep mantissa * 1e16 under u64 max). Rare in
195
199
  // typical JSON but a big win when it does fire (8 digits per stride in
196
200
  // SWAR's parse8 was benched even/worse than parse4, so we skip parse8
197
201
  // entirely and let parse4 handle the 4-15 char tail).
198
- while (p + 30 < srcEnd && intDigits + fracDigits <= 3) {
202
+ if (p + 30 < srcEnd && intDigits <= 3) {
199
203
  const parsed = parse16Digits_SIMD(p);
200
- if (parsed == U64.MAX_VALUE) break;
201
- mantissa = mantissa * 10_000_000_000_000_000 + parsed;
202
- fracDigits += 16;
203
- p += 32;
204
+ if (parsed != U64.MAX_VALUE) {
205
+ mantissa = mantissa * 10_000_000_000_000_000 + parsed;
206
+ p += 32;
207
+ }
204
208
  }
205
209
  while (p + 6 < srcEnd) {
206
210
  const parsed = parse4Digits_PairMul(load<u64>(p));
207
211
  if (parsed == U32.MAX_VALUE) break;
208
212
  mantissa = mantissa * 10_000 + <u64>parsed;
209
- fracDigits += 4;
210
213
  p += 8;
211
214
  }
212
215
  while (p < srcEnd) {
213
216
  const d = <u32>load<u16>(p) - ASCII_ZERO;
214
217
  if (d > 9) break;
215
218
  mantissa = mantissa * 10 + <u64>d;
216
- fracDigits++;
217
219
  p += 2;
218
220
  }
219
221
  }
222
+ const fracDigits = <i32>((p - fracStart) >> 1);
220
223
 
221
224
  const mantDigits = intDigits + fracDigits;
222
225
  if (mantDigits == 0) unreachable();
@@ -279,7 +282,10 @@ export function deserializeFloatField_SIMD<T extends number>(
279
282
  result /= loadPow10(<u32>-exponent);
280
283
  }
281
284
  } else if (mantDigits <= 19) {
282
- result = scientific(mantissa, exponent);
285
+ result =
286
+ exponent >= -22 && exponent <= 22
287
+ ? eiselLemire22(mantissa, exponent)
288
+ : scientific(mantissa, exponent);
283
289
  } else {
284
290
  fallbackField<T>(origStart, p, fieldPtr);
285
291
  return p;
@@ -5,6 +5,9 @@ import { QUOTE } from "../../custom/chars";
5
5
  import { BACK_SLASH } from "../../custom/chars";
6
6
  import { DESERIALIZE_ESCAPE_TABLE } from "../../globals/tables";
7
7
  import { hex4_to_u16_swar } from "../../util/swar";
8
+ import { markProductionParseError } from "../error";
9
+ import { isValidStringEscape } from "../string-validation";
10
+ import { probeStringFieldTrace, recordStringFieldTrace } from "../parseMode";
8
11
 
9
12
  // @ts-expect-error: @lazy is a valid decorator
10
13
  @lazy const SPLAT_5C = i16x8.splat(0x5c); // \
@@ -116,7 +119,7 @@ function deserializeEscapedString_SIMD(
116
119
  }
117
120
 
118
121
  let srcStart = escapeStart;
119
- const srcEnd16 = srcEnd >= 16 ? srcEnd - 16 : 0;
122
+ const srcEnd16 = srcEnd - 16;
120
123
 
121
124
  while (srcStart <= srcEnd16) {
122
125
  const block = load<v128>(srcStart);
@@ -150,6 +153,11 @@ function deserializeEscapedString_SIMD(
150
153
  const laneIdx = usize(ctz(mask) << 1);
151
154
  bs.offset += laneIdx;
152
155
  const srcIdx = srcStart + laneIdx;
156
+ if (!isValidStringEscape(srcIdx, srcEnd)) {
157
+ bs.offset = bs.buffer + outStart;
158
+ markProductionParseError();
159
+ return changetype<string>(0);
160
+ }
153
161
  const code = load<u16>(srcIdx, 2);
154
162
  if (code !== 0x75) {
155
163
  store<u16>(bs.offset, load<u16>(DESERIALIZE_ESCAPE_TABLE + code));
@@ -171,6 +179,11 @@ function deserializeEscapedString_SIMD(
171
179
  srcStart += 2;
172
180
  continue;
173
181
  }
182
+ if (!isValidStringEscape(srcStart, srcEnd)) {
183
+ bs.offset = bs.buffer + outStart;
184
+ markProductionParseError();
185
+ return changetype<string>(0);
186
+ }
174
187
  const code = load<u16>(srcStart, 2);
175
188
  if (code !== 0x75) {
176
189
  store<u16>(bs.offset, load<u16>(DESERIALIZE_ESCAPE_TABLE + code));
@@ -262,16 +275,23 @@ function deserializeEscapedStringField_SIMD(
262
275
  ): usize {
263
276
  const prefixLen = <u32>(escapeStart - payloadStart);
264
277
  bs.offset = bs.buffer;
265
- bs.ensureSize(<u32>(srcEnd - payloadStart) + 16); // +16 slack for overcopy
278
+ // The containing parser may pass document-end bounds. Reserve according to
279
+ // decoded output rather than retaining the entire remaining document.
280
+ bs.ensureSize(prefixLen + 1024);
266
281
  if (prefixLen != 0) {
267
282
  memory.copy(bs.buffer, payloadStart, prefixLen);
268
283
  bs.offset += prefixLen;
269
284
  }
270
285
 
271
286
  let srcStart = escapeStart;
272
- const srcEnd16 = srcEnd >= 16 ? srcEnd - 16 : 0;
287
+ const srcEnd16 = srcEnd - 16;
288
+ let scratchEnd = bs.buffer + bs.bufferSize - 16;
273
289
 
274
290
  while (srcStart <= srcEnd16) {
291
+ if (bs.offset > scratchEnd) {
292
+ bs.ensureSize(1024);
293
+ scratchEnd = bs.buffer + bs.bufferSize - 16;
294
+ }
275
295
  const block = load<v128>(srcStart);
276
296
  const mask = i16x8.bitmask(
277
297
  v128.or(i16x8.eq(block, SPLAT_5C), i16x8.eq(block, SPLAT_22)),
@@ -302,6 +322,10 @@ function deserializeEscapedStringField_SIMD(
302
322
  srcStart += 16;
303
323
  }
304
324
  const runLen = <u32>(srcStart - runStart);
325
+ if (bs.offset + runLen > scratchEnd) {
326
+ bs.ensureSize(runLen + 1024);
327
+ scratchEnd = bs.buffer + bs.bufferSize - 16;
328
+ }
305
329
  memory.copy(bs.offset, runStart, runLen);
306
330
  bs.offset += runLen;
307
331
  }
@@ -325,6 +349,11 @@ function deserializeEscapedStringField_SIMD(
325
349
  return srcIdx + 2;
326
350
  }
327
351
 
352
+ if (!isValidStringEscape(srcIdx, srcEnd)) {
353
+ bs.offset = bs.buffer;
354
+ markProductionParseError();
355
+ return 0;
356
+ }
328
357
  const code = load<u16>(srcIdx, 2);
329
358
  if (code !== 0x75) {
330
359
  store<u16>(bs.offset, load<u16>(DESERIALIZE_ESCAPE_TABLE + code));
@@ -338,6 +367,7 @@ function deserializeEscapedStringField_SIMD(
338
367
  }
339
368
 
340
369
  // scalar tail (< 16 bytes remaining): emit chars directly.
370
+ bs.ensureSize(<u32>(srcEnd - srcStart) + 2);
341
371
  while (srcStart < srcEnd) {
342
372
  const char = load<u16>(srcStart);
343
373
  if (char == QUOTE) {
@@ -356,6 +386,11 @@ function deserializeEscapedStringField_SIMD(
356
386
  continue;
357
387
  }
358
388
 
389
+ if (!isValidStringEscape(srcStart, srcEnd)) {
390
+ bs.offset = bs.buffer;
391
+ markProductionParseError();
392
+ return 0;
393
+ }
359
394
  const code = load<u16>(srcStart, 2);
360
395
  if (code !== 0x75) {
361
396
  store<u16>(bs.offset, load<u16>(DESERIALIZE_ESCAPE_TABLE + code));
@@ -369,8 +404,8 @@ function deserializeEscapedStringField_SIMD(
369
404
  }
370
405
 
371
406
  bs.offset = bs.buffer;
372
- abort("Unterminated string literal");
373
- return srcStart;
407
+ markProductionParseError();
408
+ return 0;
374
409
  }
375
410
 
376
411
  export function deserializeStringField_SIMD<T extends string | null>(
@@ -379,13 +414,32 @@ export function deserializeStringField_SIMD<T extends string | null>(
379
414
  dstObj: usize,
380
415
  dstOffset: usize = 0,
381
416
  ): usize {
382
- const dstFieldPtr = dstObj + dstOffset;
383
- if (srcStart + 2 > srcEnd || load<u16>(srcStart) != QUOTE)
384
- abort("Expected leading quote");
417
+ if (srcStart + 2 > srcEnd || load<u16>(srcStart) != QUOTE) {
418
+ markProductionParseError();
419
+ return 0;
420
+ }
421
+ return deserializeStringFieldTrusted_SIMD(
422
+ srcStart + 2,
423
+ srcEnd,
424
+ dstObj,
425
+ dstOffset,
426
+ );
427
+ }
385
428
 
386
- const payloadStart = srcStart + 2;
387
- const srcEnd16 = srcEnd >= 16 ? srcEnd - 16 : 0;
388
- srcStart = payloadStart;
429
+ // Generated compact-object paths have already matched the key and validated
430
+ // the opening quote. Enter at the payload so that proof replaces (rather than
431
+ // duplicates) the public field decoder's framing load.
432
+ export function deserializeStringFieldTrusted_SIMD(
433
+ payloadStart: usize,
434
+ srcEnd: usize,
435
+ dstObj: usize,
436
+ dstOffset: usize = 0,
437
+ ): usize {
438
+ const dstFieldPtr = dstObj + dstOffset;
439
+ const cachedEnd = probeStringFieldTrace(dstFieldPtr, payloadStart);
440
+ if (cachedEnd != 0) return cachedEnd;
441
+ const srcEnd16 = srcEnd - 16;
442
+ let srcStart = payloadStart;
389
443
 
390
444
  while (srcStart <= srcEnd16) {
391
445
  const block = load<v128>(srcStart);
@@ -406,15 +460,19 @@ export function deserializeStringField_SIMD<T extends string | null>(
406
460
  payloadStart,
407
461
  <u32>(srcIdx - payloadStart),
408
462
  );
409
- return srcIdx + 2;
463
+ const next = srcIdx + 2;
464
+ recordStringFieldTrace(dstFieldPtr, payloadStart, next);
465
+ return next;
410
466
  }
411
467
  // backslash → vectorized escaped scan (no more SWAR fallback)
412
- return deserializeEscapedStringField_SIMD(
468
+ const next = deserializeEscapedStringField_SIMD(
413
469
  payloadStart,
414
470
  srcIdx,
415
471
  srcEnd,
416
472
  dstFieldPtr,
417
473
  );
474
+ if (next != 0) recordStringFieldTrace(dstFieldPtr, payloadStart, next);
475
+ return next;
418
476
  }
419
477
 
420
478
  while (srcStart < srcEnd) {
@@ -425,19 +483,23 @@ export function deserializeStringField_SIMD<T extends string | null>(
425
483
  payloadStart,
426
484
  <u32>(srcStart - payloadStart),
427
485
  );
428
- return srcStart + 2;
486
+ const next = srcStart + 2;
487
+ recordStringFieldTrace(dstFieldPtr, payloadStart, next);
488
+ return next;
429
489
  }
430
490
  if (char == BACK_SLASH) {
431
- return deserializeEscapedStringField_SIMD(
491
+ const next = deserializeEscapedStringField_SIMD(
432
492
  payloadStart,
433
493
  srcStart,
434
494
  srcEnd,
435
495
  dstFieldPtr,
436
496
  );
497
+ if (next != 0) recordStringFieldTrace(dstFieldPtr, payloadStart, next);
498
+ return next;
437
499
  }
438
500
  srcStart += 2;
439
501
  }
440
502
 
441
- abort("Unterminated string literal");
442
- return srcStart;
503
+ markProductionParseError();
504
+ return 0;
443
505
  }
@@ -0,0 +1,34 @@
1
+
2
+ @inline
3
+ function isHexDigit(c: u16): bool {
4
+ return (
5
+ (c >= 0x30 && c <= 0x39) ||
6
+ (c >= 0x41 && c <= 0x46) ||
7
+ (c >= 0x61 && c <= 0x66)
8
+ );
9
+ }
10
+
11
+ /** Validate one escape beginning at its backslash without scanning plain text. */
12
+ export function isValidStringEscape(srcStart: usize, srcEnd: usize): bool {
13
+ if (srcStart + 4 > srcEnd) return false;
14
+ const code = load<u16>(srcStart, 2);
15
+ if (code == 0x75) {
16
+ return (
17
+ srcStart + 12 <= srcEnd &&
18
+ isHexDigit(load<u16>(srcStart, 4)) &&
19
+ isHexDigit(load<u16>(srcStart, 6)) &&
20
+ isHexDigit(load<u16>(srcStart, 8)) &&
21
+ isHexDigit(load<u16>(srcStart, 10))
22
+ );
23
+ }
24
+ return (
25
+ code == 0x22 ||
26
+ code == 0x5c ||
27
+ code == 0x2f ||
28
+ code == 0x62 ||
29
+ code == 0x66 ||
30
+ code == 0x6e ||
31
+ code == 0x72 ||
32
+ code == 0x74
33
+ );
34
+ }