json-as 1.4.0 → 1.6.0

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 (108) hide show
  1. package/CHANGELOG.md +54 -29
  2. package/README.md +126 -33
  3. package/assembly/custom/chars.ts +39 -78
  4. package/assembly/deserialize/error.ts +21 -0
  5. package/assembly/deserialize/index/arbitrary.ts +50 -11
  6. package/assembly/deserialize/index/array.ts +2 -1
  7. package/assembly/deserialize/index/bool.ts +1 -1
  8. package/assembly/deserialize/index/float.ts +2 -4
  9. package/assembly/deserialize/index/integer.ts +2 -4
  10. package/assembly/deserialize/index/object.ts +9 -1
  11. package/assembly/deserialize/index/string.ts +39 -7
  12. package/assembly/deserialize/index/unsigned.ts +2 -4
  13. package/assembly/deserialize/index.ts +0 -1
  14. package/assembly/deserialize/naive/array/arbitrary.ts +3 -2
  15. package/assembly/deserialize/naive/array/array.ts +1 -0
  16. package/assembly/deserialize/naive/array/bool.ts +21 -11
  17. package/assembly/deserialize/naive/array/box.ts +13 -33
  18. package/assembly/deserialize/naive/array/float.ts +23 -13
  19. package/assembly/deserialize/naive/array/integer.ts +34 -22
  20. package/assembly/deserialize/naive/array/map.ts +1 -1
  21. package/assembly/deserialize/naive/array/object.ts +2 -1
  22. package/assembly/deserialize/naive/array/string.ts +29 -16
  23. package/assembly/deserialize/naive/array/struct.ts +19 -1
  24. package/assembly/deserialize/naive/bool.ts +18 -10
  25. package/assembly/deserialize/naive/date.ts +1 -2
  26. package/assembly/deserialize/naive/float.ts +3 -9
  27. package/assembly/deserialize/naive/integer.ts +1 -2
  28. package/assembly/deserialize/naive/map.ts +342 -43
  29. package/assembly/deserialize/naive/object.ts +250 -55
  30. package/assembly/deserialize/naive/raw.ts +10 -2
  31. package/assembly/deserialize/naive/set.ts +2 -5
  32. package/assembly/deserialize/naive/staticarray.ts +4 -4
  33. package/assembly/deserialize/naive/string.ts +46 -60
  34. package/assembly/deserialize/naive/unsigned.ts +1 -2
  35. package/assembly/deserialize/parseMode.ts +216 -0
  36. package/assembly/deserialize/simd/array/integer.ts +8 -10
  37. package/assembly/deserialize/simd/float.ts +38 -34
  38. package/assembly/deserialize/simd/integer.ts +2 -7
  39. package/assembly/deserialize/simd/string.ts +84 -39
  40. package/assembly/deserialize/string-validation.ts +34 -0
  41. package/assembly/deserialize/swar/array/arbitrary.ts +1 -2
  42. package/assembly/deserialize/swar/array/array.ts +64 -15
  43. package/assembly/deserialize/swar/array/bool.ts +15 -5
  44. package/assembly/deserialize/swar/array/box.ts +1 -2
  45. package/assembly/deserialize/swar/array/float.ts +399 -30
  46. package/assembly/deserialize/swar/array/generic.ts +13 -4
  47. package/assembly/deserialize/swar/array/integer.ts +29 -78
  48. package/assembly/deserialize/swar/array/map.ts +1 -2
  49. package/assembly/deserialize/swar/array/object.ts +12 -4
  50. package/assembly/deserialize/swar/array/raw.ts +1 -2
  51. package/assembly/deserialize/swar/array/shared.ts +7 -14
  52. package/assembly/deserialize/swar/array/string.ts +37 -13
  53. package/assembly/deserialize/swar/array/struct.ts +109 -21
  54. package/assembly/deserialize/swar/array.ts +1 -3
  55. package/assembly/deserialize/swar/float.ts +23 -21
  56. package/assembly/deserialize/swar/integer.ts +2 -7
  57. package/assembly/deserialize/swar/string.ts +81 -124
  58. package/assembly/deserialize/swar/typedarray.ts +4 -4
  59. package/assembly/index.d.ts +35 -24
  60. package/assembly/index.ts +1775 -281
  61. package/assembly/serialize/index/arbitrary.ts +70 -4
  62. package/assembly/serialize/index/bool.ts +1 -1
  63. package/assembly/serialize/index/float.ts +1 -5
  64. package/assembly/serialize/index/integer.ts +1 -1
  65. package/assembly/serialize/index/jsonarray.ts +51 -0
  66. package/assembly/serialize/index/object.ts +42 -3
  67. package/assembly/serialize/index/string.ts +1 -2
  68. package/assembly/serialize/index/typedarray.ts +1 -2
  69. package/assembly/serialize/index.ts +1 -0
  70. package/assembly/serialize/naive/array.ts +87 -34
  71. package/assembly/serialize/naive/bool.ts +0 -1
  72. package/assembly/serialize/naive/float.ts +15 -29
  73. package/assembly/serialize/naive/integer.ts +1 -5
  74. package/assembly/serialize/naive/map.ts +37 -0
  75. package/assembly/serialize/naive/raw.ts +1 -2
  76. package/assembly/serialize/naive/set.ts +0 -5
  77. package/assembly/serialize/naive/staticarray.ts +0 -6
  78. package/assembly/serialize/naive/string.ts +1 -10
  79. package/assembly/serialize/naive/typedarray.ts +0 -7
  80. package/assembly/serialize/simd/string.ts +1 -3
  81. package/assembly/serialize/swar/string.ts +1 -2
  82. package/assembly/util/atoi-fast.ts +4 -14
  83. package/assembly/util/bytes.ts +1 -2
  84. package/assembly/util/eisel-lemire.ts +179 -0
  85. package/assembly/util/idofd.ts +1 -2
  86. package/assembly/util/isSpace.ts +1 -2
  87. package/assembly/util/itoa-fast.ts +6 -9
  88. package/assembly/util/nextPowerOf2.ts +1 -2
  89. package/assembly/util/parsefloat-fast.ts +3 -5
  90. package/assembly/util/prettyWhitespaceSimd.ts +69 -0
  91. package/assembly/util/ptrToStr.ts +1 -2
  92. package/assembly/util/scanValueEndSimd.ts +83 -22
  93. package/assembly/util/scanValueEndSwar.ts +67 -25
  94. package/assembly/util/scientific.ts +5 -8
  95. package/assembly/util/snp.ts +1 -2
  96. package/assembly/util/swar-int.ts +5 -10
  97. package/assembly/util/swar.ts +2 -4
  98. package/assembly/util/validateJson.ts +338 -0
  99. package/lib/as-bs.ts +51 -51
  100. package/package.json +25 -8
  101. package/transform/lib/index.d.ts +6 -0
  102. package/transform/lib/index.js +874 -171
  103. package/transform/lib/types.d.ts +3 -1
  104. package/transform/lib/types.js +6 -0
  105. package/assembly/deserialize/index/struct.ts +0 -1
  106. package/assembly/deserialize/naive/struct.ts +0 -21
  107. package/assembly/util/dragonbox-cache.ts +0 -445
  108. package/assembly/util/dragonbox.ts +0 -652
@@ -12,13 +12,14 @@
12
12
  //
13
13
  // Inspired by Daniel Lemire, "Number parsing at a gigabyte per second" (2021)
14
14
  // and the simdjson `fast_float` implementation. The integer-part loop stays
15
- // scalar most JSON float payloads have 1-3 digit integer parts, so a parse4
15
+ // scalar - most JSON float payloads have 1-3 digit integer parts, so a parse4
16
16
  // stride there pays the wasted-validate cost on every call without saving
17
17
  // enough scalar iterations.
18
18
 
19
19
  import { ptrToStr } from "../../util/ptrToStr";
20
20
  import { parse4Digits_PairMul } from "../../util/swar-int";
21
21
  import { scientific } from "../../util/scientific";
22
+ import { eiselLemire22 } from "../../util/eisel-lemire";
22
23
 
23
24
  export const POW10_F64_POS: usize = memory.data<f64>([
24
25
  1, 1e1, 1e2, 1e3, 1e4, 1e5, 1e6, 1e7, 1e8, 1e9, 1e10, 1e11, 1e12, 1e13, 1e14,
@@ -34,14 +35,11 @@ const ASCII_DOT: u16 = 46;
34
35
  const ASCII_ZERO: u16 = 48;
35
36
  const ASCII_E_UP: u16 = 69;
36
37
  const ASCII_E_LO: u16 = 101;
37
-
38
- // @ts-ignore: inline
39
- @inline export function loadPow10(exp: u32): f64 {
38
+ export function loadPow10(exp: u32): f64 {
40
39
  return load<f64>(POW10_F64_POS + ((<usize>exp) << 3));
41
40
  }
42
41
 
43
- // @ts-ignore: inline
44
- @inline function fallback<T>(srcStart: usize, srcEnd: usize): T {
42
+ function fallback<T>(srcStart: usize, srcEnd: usize): T {
45
43
  const s = ptrToStr(srcStart, srcEnd);
46
44
  // @ts-ignore
47
45
  const type: T = 0;
@@ -50,9 +48,7 @@ const ASCII_E_LO: u16 = 101;
50
48
  // @ts-ignore
51
49
  return <T>(<f32>f32.parse(s));
52
50
  }
53
-
54
- // @ts-ignore: inline
55
- @inline function fallbackField<T extends number>(
51
+ function fallbackField<T extends number>(
56
52
  origStart: usize,
57
53
  end: usize,
58
54
  fieldPtr: usize,
@@ -77,37 +73,37 @@ export function deserializeFloat_SWAR<T>(srcStart: usize, srcEnd: usize): T {
77
73
  // Integer part: scalar. Most JSON integers are 1-3 digits, so parse4 would
78
74
  // waste a validate per call.
79
75
  let mantissa: u64 = 0;
80
- let intDigits: i32 = 0;
76
+ const intStart = p;
81
77
  while (p < srcEnd) {
82
78
  const d = <u32>load<u16>(p) - ASCII_ZERO;
83
79
  if (d > 9) break;
84
80
  mantissa = mantissa * 10 + <u64>d;
85
- intDigits++;
86
81
  p += 2;
87
82
  }
83
+ const intDigits = <i32>((p - intStart) >> 1);
88
84
 
89
85
  // Fractional part: parse4 stride (8 bytes / 4 digits) → scalar tail.
90
86
  // parse8 was tried and benchmarked even/worse; the saved mantissa mul
91
87
  // didn't outweigh the extra load and combined-validation latency, and the
92
88
  // dependency chain is the same length either way.
93
- let fracDigits: i32 = 0;
89
+ let fracStart = p;
94
90
  if (p < srcEnd && load<u16>(p) == ASCII_DOT) {
95
91
  p += 2;
92
+ fracStart = p;
96
93
  while (p + 6 < srcEnd) {
97
94
  const parsed = parse4Digits_PairMul(load<u64>(p));
98
95
  if (parsed == U32.MAX_VALUE) break;
99
96
  mantissa = mantissa * 10_000 + <u64>parsed;
100
- fracDigits += 4;
101
97
  p += 8;
102
98
  }
103
99
  while (p < srcEnd) {
104
100
  const d = <u32>load<u16>(p) - ASCII_ZERO;
105
101
  if (d > 9) break;
106
102
  mantissa = mantissa * 10 + <u64>d;
107
- fracDigits++;
108
103
  p += 2;
109
104
  }
110
105
  }
106
+ const fracDigits = <i32>((p - fracStart) >> 1);
111
107
 
112
108
  const mantDigits = intDigits + fracDigits;
113
109
  if (mantDigits == 0) return fallback<T>(origStart, srcEnd);
@@ -163,7 +159,10 @@ export function deserializeFloat_SWAR<T>(srcStart: usize, srcEnd: usize): T {
163
159
  result /= loadPow10(<u32>-exponent);
164
160
  }
165
161
  } else if (mantDigits <= 19) {
166
- result = scientific(mantissa, exponent);
162
+ result =
163
+ exponent >= -22 && exponent <= 22
164
+ ? eiselLemire22(mantissa, exponent)
165
+ : scientific(mantissa, exponent);
167
166
  } else {
168
167
  return fallback<T>(origStart, srcEnd);
169
168
  }
@@ -193,33 +192,33 @@ export function deserializeFloatField_SWAR<T extends number>(
193
192
  }
194
193
 
195
194
  let mantissa: u64 = 0;
196
- let intDigits: i32 = 0;
195
+ const intStart = p;
197
196
  while (p < srcEnd) {
198
197
  const d = <u32>load<u16>(p) - ASCII_ZERO;
199
198
  if (d > 9) break;
200
199
  mantissa = mantissa * 10 + <u64>d;
201
- intDigits++;
202
200
  p += 2;
203
201
  }
202
+ const intDigits = <i32>((p - intStart) >> 1);
204
203
 
205
- let fracDigits: i32 = 0;
204
+ let fracStart = p;
206
205
  if (p < srcEnd && load<u16>(p) == ASCII_DOT) {
207
206
  p += 2;
207
+ fracStart = p;
208
208
  while (p + 6 < srcEnd) {
209
209
  const parsed = parse4Digits_PairMul(load<u64>(p));
210
210
  if (parsed == U32.MAX_VALUE) break;
211
211
  mantissa = mantissa * 10_000 + <u64>parsed;
212
- fracDigits += 4;
213
212
  p += 8;
214
213
  }
215
214
  while (p < srcEnd) {
216
215
  const d = <u32>load<u16>(p) - ASCII_ZERO;
217
216
  if (d > 9) break;
218
217
  mantissa = mantissa * 10 + <u64>d;
219
- fracDigits++;
220
218
  p += 2;
221
219
  }
222
220
  }
221
+ const fracDigits = <i32>((p - fracStart) >> 1);
223
222
 
224
223
  const mantDigits = intDigits + fracDigits;
225
224
  if (mantDigits == 0) unreachable();
@@ -282,7 +281,10 @@ export function deserializeFloatField_SWAR<T extends number>(
282
281
  result /= loadPow10(<u32>-exponent);
283
282
  }
284
283
  } else if (mantDigits <= 19) {
285
- result = scientific(mantissa, exponent);
284
+ result =
285
+ exponent >= -22 && exponent <= 22
286
+ ? eiselLemire22(mantissa, exponent)
287
+ : scientific(mantissa, exponent);
286
288
  } else {
287
289
  fallbackField<T>(origStart, p, fieldPtr);
288
290
  return p;
@@ -40,11 +40,7 @@ const ASCII_ZERO: u16 = 48;
40
40
  * @param value The `u64` accumulator, interpreted as a two's-complement
41
41
  * signed integer for narrower types.
42
42
  */
43
- // @ts-expect-error: @inline is a valid decorator
44
- @inline function storeSignedToField<T extends number>(
45
- dstPtr: usize,
46
- value: u64,
47
- ): void {
43
+ function storeSignedToField<T extends number>(dstPtr: usize, value: u64): void {
48
44
  if (sizeof<T>() == 1) {
49
45
  store<i8>(dstPtr, <i8>value);
50
46
  } else if (sizeof<T>() == 2) {
@@ -63,8 +59,7 @@ const ASCII_ZERO: u16 = 48;
63
59
  * @param dstPtr Destination pointer (already includes any field offset).
64
60
  * @param value The `u64` accumulator.
65
61
  */
66
- // @ts-expect-error: @inline is a valid decorator
67
- @inline function storeUnsignedToField<T extends number>(
62
+ function storeUnsignedToField<T extends number>(
68
63
  dstPtr: usize,
69
64
  value: u64,
70
65
  ): void {
@@ -4,6 +4,8 @@ import { __heap_base } from "memory";
4
4
  import { BACK_SLASH, QUOTE } from "../../custom/chars";
5
5
  import { DESERIALIZE_ESCAPE_TABLE } from "../../globals/tables";
6
6
  import { hex4_to_u16_swar } from "../../util/swar";
7
+ import { markProductionParseError } from "../error";
8
+ import { isValidStringEscape } from "../string-validation";
7
9
 
8
10
  // Overflow Pattern for Unicode Escapes (READ)
9
11
  // \u0001 0 \u00|01__ + 4
@@ -41,11 +43,7 @@ import { hex4_to_u16_swar } from "../../util/swar";
41
43
  * @param dst buffer to write to
42
44
  * @returns number of bytes written
43
45
  */
44
- // @ts-expect-error: @inline is a valid decorator
45
- @inline function copyStringFromSource(
46
- srcStart: usize,
47
- byteLength: usize,
48
- ): string {
46
+ function copyStringFromSource(srcStart: usize, byteLength: usize): string {
49
47
  if (byteLength == 0) return changetype<string>("");
50
48
  const out = __new(byteLength, idof<string>());
51
49
  memory.copy(out, srcStart, byteLength);
@@ -75,7 +73,7 @@ function deserializeEscapedString_SWAR(
75
73
  }
76
74
 
77
75
  let srcStart = escapeStart;
78
- const srcEnd8 = srcEnd >= 8 ? srcEnd - 8 : 0;
76
+ const srcEnd8 = srcEnd - 8;
79
77
 
80
78
  while (srcStart <= srcEnd8) {
81
79
  const block = load<u64>(srcStart);
@@ -111,6 +109,11 @@ function deserializeEscapedString_SWAR(
111
109
  const srcIdx = srcStart + laneIdx;
112
110
  if ((load<u32>(srcIdx) & 0xffff) !== 0x5c) continue; // false positive
113
111
  bs.offset += laneIdx;
112
+ if (!isValidStringEscape(srcIdx, srcEnd)) {
113
+ bs.offset = bs.buffer + outStart;
114
+ markProductionParseError();
115
+ return changetype<string>(0);
116
+ }
114
117
  const code = load<u16>(srcIdx, 2);
115
118
  if (code !== 0x75) {
116
119
  store<u16>(bs.offset, load<u16>(DESERIALIZE_ESCAPE_TABLE + code));
@@ -138,6 +141,11 @@ function deserializeEscapedString_SWAR(
138
141
  srcStart += 2;
139
142
  continue;
140
143
  }
144
+ if (!isValidStringEscape(srcStart, srcEnd)) {
145
+ bs.offset = bs.buffer + outStart;
146
+ markProductionParseError();
147
+ return changetype<string>(0);
148
+ }
141
149
  const code = load<u16>(srcStart, 2);
142
150
  if (code !== 0x75) {
143
151
  store<u16>(bs.offset, load<u16>(DESERIALIZE_ESCAPE_TABLE + code));
@@ -245,7 +253,7 @@ function writeStringToField(
245
253
  // the next unread src pointer.
246
254
  //
247
255
  // HYBRID strategy (validated against the prior run-copy scanner across escape
248
- // densities see __benches__/custom/swar-string-deser-hybrid-h2h: +17–70%):
256
+ // densities - see __benches__/custom/swar-string-deser-hybrid-h2h: +17–70%):
249
257
  // * Escape-bearing block: one optimistic whole-block u64 store copies the
250
258
  // plain prefix for free, then the (scalar-confirmed) escape is decoded.
251
259
  // * Clean block: stream the first one, then if the clean run continues switch
@@ -260,16 +268,25 @@ function deserializeEscapedStringField_SWAR(
260
268
  ): usize {
261
269
  const prefixLen = <u32>(escapeStart - payloadStart);
262
270
  bs.offset = bs.buffer;
263
- bs.ensureSize(<u32>(srcEnd - payloadStart) + 8); // +8 slack for u64 overcopy
271
+ // `srcEnd` may be the end of a containing map/document when a generated
272
+ // parser is cursor-driven. Reserving that entire remainder for one escaped
273
+ // field retained multi-megabyte scratch buffers. Grow with the decoded
274
+ // string instead; escapes only shrink relative to the source.
275
+ bs.ensureSize(prefixLen + 1024);
264
276
  if (prefixLen != 0) {
265
277
  memory.copy(bs.buffer, payloadStart, prefixLen);
266
278
  bs.offset += prefixLen;
267
279
  }
268
280
 
269
281
  let srcStart = escapeStart;
270
- const srcEnd8 = srcEnd >= 8 ? srcEnd - 8 : 0;
282
+ const srcEnd8 = srcEnd - 8;
283
+ let scratchEnd = bs.buffer + bs.bufferSize - 8;
271
284
 
272
285
  while (srcStart <= srcEnd8) {
286
+ if (bs.offset > scratchEnd) {
287
+ bs.ensureSize(1024);
288
+ scratchEnd = bs.buffer + bs.bufferSize - 8;
289
+ }
273
290
  const block = load<u64>(srcStart);
274
291
  let mask = backslash_or_quote_mask(block);
275
292
  if (mask == 0) {
@@ -289,6 +306,10 @@ function deserializeEscapedStringField_SWAR(
289
306
  srcStart += 8;
290
307
  }
291
308
  const runLen = <u32>(srcStart - runStart);
309
+ if (bs.offset + runLen > scratchEnd) {
310
+ bs.ensureSize(runLen + 1024);
311
+ scratchEnd = bs.buffer + bs.bufferSize - 8;
312
+ }
292
313
  memory.copy(bs.offset, runStart, runLen);
293
314
  bs.offset += runLen;
294
315
  }
@@ -314,6 +335,11 @@ function deserializeEscapedStringField_SWAR(
314
335
  bs.offset = bs.buffer;
315
336
  return srcIdx + 2;
316
337
  }
338
+ if (!isValidStringEscape(srcIdx, srcEnd)) {
339
+ bs.offset = bs.buffer;
340
+ markProductionParseError();
341
+ return 0;
342
+ }
317
343
  const code = load<u16>(srcIdx, 2);
318
344
  if (code !== 0x75) {
319
345
  store<u16>(bs.offset, load<u16>(DESERIALIZE_ESCAPE_TABLE + code));
@@ -334,6 +360,7 @@ function deserializeEscapedStringField_SWAR(
334
360
  }
335
361
 
336
362
  // scalar tail (< 8 bytes remaining)
363
+ bs.ensureSize(<u32>(srcEnd - srcStart) + 2);
337
364
  while (srcStart < srcEnd) {
338
365
  const char = load<u16>(srcStart);
339
366
  if (char == QUOTE) {
@@ -347,6 +374,11 @@ function deserializeEscapedStringField_SWAR(
347
374
  srcStart += 2;
348
375
  continue;
349
376
  }
377
+ if (!isValidStringEscape(srcStart, srcEnd)) {
378
+ bs.offset = bs.buffer;
379
+ markProductionParseError();
380
+ return 0;
381
+ }
350
382
  const code = load<u16>(srcStart, 2);
351
383
  if (code !== 0x75) {
352
384
  store<u16>(bs.offset, load<u16>(DESERIALIZE_ESCAPE_TABLE + code));
@@ -360,122 +392,37 @@ function deserializeEscapedStringField_SWAR(
360
392
  }
361
393
 
362
394
  bs.offset = bs.buffer;
363
- abort("Unterminated string literal");
364
- return srcStart;
395
+ markProductionParseError();
396
+ return 0;
365
397
  }
366
398
 
367
- function deserializeEscapedStringContinuation_SWAR_MergedTuned(
368
- lastPtr: usize,
399
+ export function deserializeStringField_SWAR<T extends string | null>(
369
400
  srcStart: usize,
370
401
  srcEnd: usize,
371
- dstFieldPtr: usize,
402
+ dstObj: usize,
403
+ dstOffset: usize = 0,
372
404
  ): usize {
373
- const srcEnd8 = srcEnd - 8;
374
-
375
- while (srcStart <= srcEnd8) {
376
- const blockStart = srcStart;
377
- let mask = backslash_or_quote_mask(load<u64>(srcStart));
378
- if (mask === 0) {
379
- srcStart += 8;
380
- continue;
381
- }
382
-
383
- do {
384
- const laneIdx = usize(ctz(mask) >> 3);
385
- mask &= mask - 1;
386
- const srcIdx = srcStart + laneIdx;
387
- const char = load<u16>(srcIdx);
388
- if (char == QUOTE) {
389
- const runLen = <u32>(srcIdx - lastPtr);
390
- if (runLen != 0) {
391
- memory.copy(bs.offset, lastPtr, runLen);
392
- bs.offset += runLen;
393
- }
394
- writeStringToField(
395
- dstFieldPtr,
396
- bs.buffer,
397
- <u32>(bs.offset - bs.buffer),
398
- );
399
- bs.offset = bs.buffer;
400
- return srcIdx + 2;
401
- }
402
- if (char != BACK_SLASH) continue;
403
-
404
- const runLen = <u32>(srcIdx - lastPtr);
405
- if (runLen != 0) {
406
- memory.copy(bs.offset, lastPtr, runLen);
407
- bs.offset += runLen;
408
- }
409
-
410
- const chunk = load<u32>(srcIdx);
411
- const code = <u16>(chunk >> 16);
412
- if (code !== 0x75) {
413
- store<u16>(bs.offset, load<u16>(DESERIALIZE_ESCAPE_TABLE + code));
414
- bs.offset += 2;
415
- lastPtr = srcIdx + 4;
416
- } else {
417
- store<u16>(bs.offset, hex4_to_u16_swar(load<u64>(srcIdx, 4)));
418
- bs.offset += 2;
419
- lastPtr = srcIdx + 12;
420
- }
421
- srcStart = lastPtr;
422
- break;
423
- } while (mask !== 0);
424
-
425
- if (srcStart == blockStart) srcStart += 8;
426
- }
427
-
428
- while (srcStart < srcEnd) {
429
- const tailChar = load<u16>(srcStart);
430
- if (tailChar == QUOTE) {
431
- const runLen = <u32>(srcStart - lastPtr);
432
- if (runLen != 0) {
433
- memory.copy(bs.offset, lastPtr, runLen);
434
- bs.offset += runLen;
435
- }
436
- writeStringToField(dstFieldPtr, bs.buffer, <u32>(bs.offset - bs.buffer));
437
- bs.offset = bs.buffer;
438
- return srcStart + 2;
439
- }
440
- if (tailChar != BACK_SLASH) {
441
- srcStart += 2;
442
- continue;
443
- }
444
-
445
- const runLen = <u32>(srcStart - lastPtr);
446
- if (runLen != 0) {
447
- memory.copy(bs.offset, lastPtr, runLen);
448
- bs.offset += runLen;
449
- }
450
- const tailCode = load<u16>(srcStart, 2);
451
- if (tailCode !== 0x75) {
452
- store<u16>(bs.offset, load<u16>(DESERIALIZE_ESCAPE_TABLE + tailCode));
453
- bs.offset += 2;
454
- srcStart += 4;
455
- } else {
456
- store<u16>(bs.offset, hex4_to_u16_swar(load<u64>(srcStart, 4)));
457
- bs.offset += 2;
458
- srcStart += 12;
459
- }
460
- lastPtr = srcStart;
405
+ if (srcStart + 2 > srcEnd || load<u16>(srcStart) != QUOTE) {
406
+ markProductionParseError();
407
+ return 0;
461
408
  }
462
-
463
- bs.offset = bs.buffer;
464
- return srcStart;
409
+ return deserializeStringFieldTrusted_SWAR(
410
+ srcStart + 2,
411
+ srcEnd,
412
+ dstObj,
413
+ dstOffset,
414
+ );
465
415
  }
466
416
 
467
- export function deserializeStringField_SWAR<T extends string | null>(
468
- srcStart: usize,
417
+ export function deserializeStringFieldTrusted_SWAR(
418
+ payloadStart: usize,
469
419
  srcEnd: usize,
470
420
  dstObj: usize,
471
421
  dstOffset: usize = 0,
472
422
  ): usize {
473
423
  const dstFieldPtr = dstObj + dstOffset;
474
- if (srcStart + 2 > srcEnd || load<u16>(srcStart) != QUOTE)
475
- abort("Expected leading quote");
476
-
477
- const payloadStart = srcStart + 2;
478
- srcStart = payloadStart;
424
+ let srcStart = payloadStart;
425
+ let pendingMask: u64 = 0;
479
426
 
480
427
  // Wide pre-scan: skip 16 bytes per iter while both halves are clean. The
481
428
  // common case (plain ASCII payloads, no escape) hits this loop exclusively
@@ -483,16 +430,29 @@ export function deserializeStringField_SWAR<T extends string | null>(
483
430
  if (srcEnd >= 16) {
484
431
  const srcEnd16 = srcEnd - 16;
485
432
  while (srcStart <= srcEnd16) {
486
- const m0 = backslash_or_quote_mask(load<u64>(srcStart));
487
- const m1 = backslash_or_quote_mask(load<u64>(srcStart, 8));
488
- if ((m0 | m1) != 0) break;
433
+ // Test the first word before loading the second: short values and keys
434
+ // close (or escape) within the first 8 bytes, so this skips the second
435
+ // load on the common case while still skipping 16 bytes when both clean.
436
+ const firstMask = backslash_or_quote_mask(load<u64>(srcStart));
437
+ if (firstMask != 0) {
438
+ pendingMask = firstMask;
439
+ break;
440
+ }
441
+ const secondMask = backslash_or_quote_mask(load<u64>(srcStart, 8));
442
+ if (secondMask != 0) {
443
+ srcStart += 8;
444
+ pendingMask = secondMask;
445
+ break;
446
+ }
489
447
  srcStart += 16;
490
448
  }
491
449
  }
492
450
 
493
451
  const srcEnd8 = srcEnd - 8;
494
452
  while (srcStart <= srcEnd8) {
495
- let mask = backslash_or_quote_mask(load<u64>(srcStart));
453
+ let mask = pendingMask;
454
+ if (mask == 0) mask = backslash_or_quote_mask(load<u64>(srcStart));
455
+ pendingMask = 0;
496
456
  if (mask === 0) {
497
457
  srcStart += 8;
498
458
  continue;
@@ -544,8 +504,8 @@ export function deserializeStringField_SWAR<T extends string | null>(
544
504
  srcStart += 2;
545
505
  }
546
506
 
547
- abort("Unterminated string literal");
548
- return srcStart;
507
+ markProductionParseError();
508
+ return 0;
549
509
  }
550
510
 
551
511
  /**
@@ -555,8 +515,7 @@ export function deserializeStringField_SWAR<T extends string | null>(
555
515
  * so callers must confirm the hit scalarly.
556
516
  * Each matching lane sets itself to 0x80.
557
517
  */
558
- // @ts-expect-error: @inline is a valid decorator
559
- @inline function backslash_or_quote_mask(block: u64): u64 {
518
+ function backslash_or_quote_mask(block: u64): u64 {
560
519
  const b = block ^ 0x005c_005c_005c_005c;
561
520
  const q = block ^ 0x0022_0022_0022_0022;
562
521
  return (
@@ -575,8 +534,7 @@ export function deserializeStringField_SWAR<T extends string | null>(
575
534
  *
576
535
  * Each matching lane sets itself to 0x80.
577
536
  */
578
- // @ts-expect-error: @inline is a valid decorator
579
- @inline function backslash_mask(block: u64): u64 {
537
+ function backslash_mask(block: u64): u64 {
580
538
  const b = block ^ 0x005c_005c_005c_005c;
581
539
  const backslash_mask =
582
540
  (b - 0x0001_0001_0001_0001) & ~b & 0x0080_0080_0080_0080;
@@ -597,8 +555,7 @@ export function deserializeStringField_SWAR<T extends string | null>(
597
555
  * WARNING: The low byte of a code unit *may* be a backslash, thus triggering false positives!
598
556
  * This is useful for a hot path where it is possible to detect the false positive scalarly.
599
557
  */
600
- // @ts-expect-error: @inline is a valid decorator
601
- @inline function backslash_mask_unsafe(block: u64): u64 {
558
+ function backslash_mask_unsafe(block: u64): u64 {
602
559
  const b = block ^ 0x005c_005c_005c_005c;
603
560
  const backslash_mask =
604
561
  (b - 0x0001_0001_0001_0001) & ~b & 0x0080_0080_0080_0080;
@@ -6,10 +6,10 @@
6
6
  //
7
7
  // - **No count pass.** TypedArrays have a fixed length at construction,
8
8
  // so the natural approach is to count first then allocate. We tried
9
- // that with a SWAR comma counter it cut the per-element cost but
9
+ // that with a SWAR comma counter - it cut the per-element cost but
10
10
  // kept us ~30% below the top-level `f64[]` path because the count
11
11
  // scan still touched the whole input twice. Instead we allocate
12
- // worst-case (`(srcEnd - srcStart) >> 2 + 1` elements each
12
+ // worst-case (`(srcEnd - srcStart) >> 2 + 1` elements - each
13
13
  // element needs >= "D," = 2 UTF-16 chars = 4 bytes) and `__renew`
14
14
  // the underlying buffer down to the exact byte count after parsing.
15
15
  // The over-allocation peaks at ~2-3× the final size for typical
@@ -46,7 +46,7 @@ import {
46
46
  * Worst-case element count: each element occupies >= 1 digit + 1
47
47
  * delimiter = 2 UTF-16 chars = 4 bytes. So `(srcEnd - srcStart) >> 2`
48
48
  * upper-bounds the count. Allocating to worst-case lets us skip a
49
- * full count pass over the input at the cost of an over-allocated
49
+ * full count pass over the input - at the cost of an over-allocated
50
50
  * underlying buffer that we trim via `__renew` once we know the
51
51
  * actual element count.
52
52
  *
@@ -125,7 +125,7 @@ export function deserializeTypedArray_SWAR<T extends ArrayLike<number>>(
125
125
  // directly: `__renew` the buffer to the actual byte length and
126
126
  // update the view's `byteLength` and `dataStart`. AS's TypedArray
127
127
  // structure has `buffer`, `dataStart` (= buffer), `byteLength`
128
- // (capacity in bytes) in that order same layout as ArrayBufferView.
128
+ // (capacity in bytes) in that order - same layout as ArrayBufferView.
129
129
  const actualCount = i32(<usize>(writePtr - dataStart) / elementSize);
130
130
  if (actualCount != maxElements) {
131
131
  const actualBytes = <usize>actualCount * elementSize;
@@ -8,12 +8,12 @@ declare class JSONConfig {
8
8
  * access; untouched fields pass their original bytes straight through on
9
9
  * serialize.
10
10
  *
11
- * - `"none"` *(default)* every field is parsed eagerly, up-front.
12
- * - `"auto"` the transform defers fields whose estimated parse cost is high
11
+ * - `"none"` *(default)* - every field is parsed eagerly, up-front.
12
+ * - `"auto"` - the transform defers fields whose estimated parse cost is high
13
13
  * (nested structs, arrays, maps, long strings) and keeps cheap fields
14
14
  * (primitives, enums, `Date`) eager. Use `@eager` to force a field back to
15
15
  * eager, or `@lazy` to force one on.
16
- * - `"all"` every field is deferred. Best for proxy / filter / forward
16
+ * - `"all"` - every field is deferred. Best for proxy / filter / forward
17
17
  * workloads over large payloads; note it generates a getter and a serialize
18
18
  * branch per field, so module size grows with very wide schemas.
19
19
  *
@@ -50,9 +50,9 @@ declare class JSONConfig {
50
50
  * JSON.parse<Vec3>('{"x":1,"y":2,"z":3}');
51
51
  * ```
52
52
  */
53
- // @ts-ignore: type
54
53
  declare function json(config?: JSONConfig): Function;
55
-
54
+ // @ts-expect-error: type
55
+ declare function json(..._): void;
56
56
  /**
57
57
  * Alias for {@link json}. `@serializable` and `@json` are interchangeable.
58
58
  */
@@ -96,7 +96,7 @@ declare function omit(..._): void;
96
96
  * The field is still parsed normally; the condition only affects serialization.
97
97
  *
98
98
  * @param condition - A predicate that receives the **instance** and returns
99
- * `true` to omit the field `(self: T) => boolean` or a string expression
99
+ * `true` to omit the field - `(self: T) => boolean` - or a string expression
100
100
  * evaluated in the instance's scope (reference fields via `this`).
101
101
  *
102
102
  * @example
@@ -124,13 +124,31 @@ declare function omitif(condition: string | ((self: any) => boolean)): Function;
124
124
  * ```
125
125
  */
126
126
  // @ts-ignore: type
127
- declare function omitnull(..._): Function;
127
+ declare function omitnull(..._): void;
128
+
129
+ /**
130
+ * Field decorator that marks a property as optional for deserialization: the
131
+ * key may be absent from (or appear anywhere in) the input, and the field keeps
132
+ * its default. Unlike `@omitnull`/`@omitif` it does NOT omit the field on
133
+ * serialize and has no nullability requirement - it only opts the field into
134
+ * the order-tolerant fast path.
135
+ *
136
+ * @example
137
+ * ```ts
138
+ * @json
139
+ * class Tweet {
140
+ * @optional retweeted_status: Retweet | null = null; // key may be absent
141
+ * }
142
+ * ```
143
+ */
144
+ // @ts-ignore: type
145
+ declare function optional(..._): void;
128
146
 
129
147
  /**
130
148
  * Field decorator that defers parsing of a property until it is first read
131
149
  * (on-demand / lazy parsing). The raw JSON slice is stored at parse time and
132
150
  * materialized into the field's type on first access, then cached; an untouched
133
- * field round-trips by copying its original bytes never re-parsed or
151
+ * field round-trips by copying its original bytes - never re-parsed or
134
152
  * re-serialized.
135
153
  *
136
154
  * Equivalent to the `JSON.Lazy<T>` type-wrapper form. Pays off for fields you
@@ -170,7 +188,7 @@ declare function eager(..._): void;
170
188
  * the generated serialization. The method receives the instance and must return
171
189
  * a **valid JSON string**. Pair with {@link deserializer}.
172
190
  *
173
- * @param shape - Optional JSON value shape the output conforms to one of
191
+ * @param shape - Optional JSON value shape the output conforms to - one of
174
192
  * `"any"` (default), `"string"`, `"number"`, `"object"`, `"array"`,
175
193
  * `"boolean"`, or `"null"`.
176
194
  *
@@ -212,11 +230,11 @@ declare function serializer(
212
230
  /**
213
231
  * Method decorator marking a member as the class's custom deserializer,
214
232
  * replacing the generated deserialization. The method receives the raw JSON
215
- * string and must return a **new** instance never assume an existing
233
+ * string and must return a **new** instance - never assume an existing
216
234
  * destination is reused. Pair with {@link serializer} (see it for a full,
217
235
  * round-tripping example).
218
236
  *
219
- * @param shape - Optional JSON value shape the input conforms to one of
237
+ * @param shape - Optional JSON value shape the input conforms to - one of
220
238
  * `"any"` (default), `"string"`, `"number"`, `"object"`, `"array"`,
221
239
  * `"boolean"`, or `"null"`.
222
240
  *
@@ -235,19 +253,6 @@ declare function deserializer(
235
253
  shape?: "any" | "string" | "number" | "object" | "array" | "boolean" | "null",
236
254
  ): any;
237
255
 
238
- /**
239
- * Parsing/serialization strategy selected at build time via the `JSON_MODE`
240
- * environment variable and exposed as {@link JSON_MODE}.
241
- */
242
- declare const enum JSONMode {
243
- /** Scalar/word-at-a-time (SWAR) scanning. The default; no extra flags. */
244
- SWAR = 0,
245
- /** 128-bit SIMD scanning. Fastest on larger payloads; needs `--enable simd`. */
246
- SIMD = 1,
247
- /** Straightforward byte-at-a-time scanning. Smallest code, slowest. */
248
- NAIVE = 2,
249
- }
250
-
251
256
  /**
252
257
  * The active {@link JSONMode}, injected by the transform from the `JSON_MODE`
253
258
  * build-time environment variable (default `SWAR`). Set it on the `asc`
@@ -262,6 +267,12 @@ declare const enum JSONMode {
262
267
  */
263
268
  declare const JSON_MODE: JSONMode;
264
269
 
270
+ /**
271
+ * Whether strict RFC 8259 validation is enabled. Injected by the transform from
272
+ * the `JSON_STRICT` build-time environment variable (default `false`).
273
+ */
274
+ declare const JSON_STRICT: bool;
275
+
265
276
  /**
266
277
  * Whether the string cache is enabled (default off). Injected from the
267
278
  * `JSON_CACHE` build-time environment variable. When on, repeated strings are