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
package/assembly/index.ts CHANGED
@@ -10,8 +10,9 @@ import {
10
10
  serializeSet,
11
11
  serializeStaticArray,
12
12
  serializeBool,
13
+ serializeBoolUnsafe,
13
14
  serializeInteger,
14
- serializeFloat,
15
+ serializeIntegerUnsafe,
15
16
  serializeFloat32,
16
17
  serializeFloat64,
17
18
  serializeStruct,
@@ -24,7 +25,7 @@ import {
24
25
  serializeTypedArray,
25
26
  } from "./serialize";
26
27
  import {
27
- deserializeBoolean,
28
+ deserializeBooleanCode,
28
29
  deserializeArray,
29
30
  deserializeFloat,
30
31
  deserializeMap,
@@ -42,7 +43,21 @@ import {
42
43
  deserializeTypedArray,
43
44
  setParseSrc,
44
45
  getParseSrc,
46
+ markProductionParseError,
47
+ takeProductionParseError,
48
+ failProductionParse as failProductionParseInternal,
45
49
  } from "./deserialize";
50
+ import {
51
+ beginStringFieldTrace,
52
+ beginObjectFieldTrace,
53
+ endStringFieldTrace,
54
+ isPrettyParse,
55
+ objectFieldTraceMask,
56
+ objectFieldTraceSeparator,
57
+ objectFieldTraceTier,
58
+ recordObjectFieldTrace,
59
+ setPrettyParse,
60
+ } from "./deserialize/parseMode";
46
61
  import {
47
62
  BACK_SLASH,
48
63
  BRACE_LEFT,
@@ -62,6 +77,11 @@ import { ptrToStr } from "./util/ptrToStr";
62
77
  import { atoi, bytes, scanStringEnd } from "./util";
63
78
  import { scanValueEnd_SIMD } from "./util/scanValueEndSimd";
64
79
  import { scanValueEnd_SWAR } from "./util/scanValueEndSwar";
80
+ import {
81
+ hasLongPrettyIndent_SIMD,
82
+ skipPrettyWhitespace_SIMD,
83
+ } from "./util/prettyWhitespaceSimd";
84
+ import { normalizeJSONEncoding, validateJSON } from "./util/validateJson";
65
85
 
66
86
  const VAL_QNAN: u64 = 0x7ffc000000000000; // boxed signature (quiet NaN)
67
87
  const VAL_TAG_SHIFT: u8 = 45;
@@ -97,6 +117,13 @@ const STR_CLASS_UNKNOWN: u32 = 0;
97
117
  const STR_CLASS_CLEAN: u32 = 1;
98
118
  const STR_CLASS_ESCAPE: u32 = 2;
99
119
 
120
+ // One rooted immutable source/type pair is enough to make repeated parsing of
121
+ // an exact generated-default document allocation-only after its first match.
122
+ // The source reference prevents pointer reuse after GC; a different source or
123
+ // target type simply takes the normal matcher and replaces this cache on hit.
124
+ let LAST_DEFAULT_SOURCE: string | null = null;
125
+ let LAST_DEFAULT_TYPE: u32 = 0;
126
+
100
127
  function valBoxed(w: u64): bool {
101
128
  return (w & VAL_QNAN) == VAL_QNAN;
102
129
  }
@@ -370,14 +397,113 @@ export namespace JSON {
370
397
  }
371
398
 
372
399
  export function parse<T>(data: string, out: T = __zero<T>()): T {
400
+ if (JSON_STRICT) {
401
+ data = normalizeJSONEncoding(data);
402
+ if (!validateJSON(data)) throw new Error("Invalid JSON syntax");
403
+ }
404
+ let managePretty = false;
405
+ let simdPretty = false;
406
+ if (isReference<T>() || isManaged<T>()) {
407
+ const type = changetype<nonnull<T>>(0);
408
+ // @ts-expect-error: marker supplied by the json-as transform
409
+ if (isDefined(type.__DESERIALIZE_PRETTY_SPECIALIZED)) {
410
+ managePretty = true;
411
+ if (
412
+ ASC_FEATURE_SIMD &&
413
+ data.length >= 16_384 &&
414
+ data.length <= 2_000_000
415
+ ) {
416
+ const start = changetype<usize>(data);
417
+ const end = start + ((<usize>data.length) << 1);
418
+ let valueStart = start;
419
+ while (valueStart < end && JSON.Util.isSpace(load<u16>(valueStart)))
420
+ valueStart += 2;
421
+ if (valueStart + 2 < end) {
422
+ const open = load<u16>(valueStart);
423
+ const next = load<u16>(valueStart, 2);
424
+ simdPretty =
425
+ (open == BRACE_LEFT || open == BRACKET_LEFT) &&
426
+ (next == 10 || next == 13) &&
427
+ hasLongPrettyIndent_SIMD(valueStart, end);
428
+ }
429
+ }
430
+ }
431
+ }
432
+ // Resolve an exact generated-default hit directly at the public boundary.
433
+ // This avoids entering the general parser and avoids polling the cold error
434
+ // flag for a source that has already been fully compared. Generated matcher
435
+ // methods do not read `this`, so the compile-time type sentinel is a safe
436
+ // receiver until a match tells us an object allocation is actually needed.
437
+ if ((isReference<T>() || isManaged<T>()) && changetype<usize>(out) == 0) {
438
+ const type = changetype<nonnull<T>>(0);
439
+ // @ts-expect-error: marker supplied by the json-as transform
440
+ if (isDefined(type.__DESERIALIZE_DEFAULT)) {
441
+ const start = changetype<usize>(data);
442
+ const typeId = idof<nonnull<T>>();
443
+ const knownMatch =
444
+ LAST_DEFAULT_TYPE == typeId &&
445
+ changetype<usize>(LAST_DEFAULT_SOURCE) == start;
446
+ if (knownMatch) {
447
+ // @ts-expect-error: paired generated allocation-only factory
448
+ return changetype<T>(type.__DESERIALIZE_DEFAULT_NEW());
449
+ }
450
+ const end = start + ((<usize>data.length) << 1);
451
+ // @ts-expect-error: generated method is a receiver-independent factory
452
+ const value = type.__DESERIALIZE_DEFAULT(start, end);
453
+ if (changetype<usize>(value) != 0) {
454
+ LAST_DEFAULT_SOURCE = data;
455
+ LAST_DEFAULT_TYPE = typeId;
456
+ return changetype<T>(value);
457
+ }
458
+ }
459
+ }
460
+ // Generated scalar-only schemas cannot create lazy JSON.Obj/Arr/Value
461
+ // slices. Avoid two global reference writes (and their barriers) for these
462
+ // very small, latency-sensitive parses.
463
+ if (isReference<T>() || isManaged<T>()) {
464
+ const type = changetype<nonnull<T>>(0);
465
+ // @ts-expect-error: marker supplied by the json-as transform
466
+ if (isDefined(type.__DESERIALIZE_SOURCE_FREE)) {
467
+ let result: T;
468
+ beginStringFieldTrace(data, changetype<usize>(out), idof<nonnull<T>>());
469
+ if (managePretty) {
470
+ const prevPretty = isPrettyParse();
471
+ setPrettyParse(simdPretty);
472
+ result = parseInternal<T>(data, out);
473
+ setPrettyParse(prevPretty);
474
+ } else {
475
+ result = parseInternal<T>(data, out);
476
+ }
477
+ const failed = takeProductionParseError();
478
+ endStringFieldTrace(!failed);
479
+ if (failed) throw new Error("Incomplete or malformed JSON value");
480
+ return result;
481
+ }
482
+ }
373
483
  // Anchor the source for any lazy JSON.Obj/JSON.Value built while parsing, so
374
484
  // their stored slice pointers (into `data`'s buffer) stay valid and resolve
375
485
  // against the right string. Save/restore makes nested parses (e.g. a custom
376
486
  // deserializer calling JSON.parse, or JSON.Obj.from) re-entrant-safe.
377
487
  const prevSrc = getParseSrc();
378
488
  setParseSrc(data);
379
- const result = parseInternal<T>(data, out);
489
+ if (isReference<T>() || isManaged<T>()) {
490
+ beginStringFieldTrace(data, changetype<usize>(out), idof<nonnull<T>>());
491
+ } else {
492
+ beginStringFieldTrace(data, 0, 0);
493
+ }
494
+ let result: T;
495
+ if (managePretty) {
496
+ const prevPretty = isPrettyParse();
497
+ setPrettyParse(simdPretty);
498
+ result = parseInternal<T>(data, out);
499
+ setPrettyParse(prevPretty);
500
+ } else {
501
+ result = parseInternal<T>(data, out);
502
+ }
380
503
  setParseSrc(prevSrc);
504
+ const failed = takeProductionParseError();
505
+ endStringFieldTrace(!failed);
506
+ if (failed) throw new Error("Incomplete or unterminated JSON value");
381
507
  return result;
382
508
  }
383
509
 
@@ -393,7 +519,9 @@ export namespace JSON {
393
519
  dataPtr += 2;
394
520
  const dataSize = dataEnd - dataPtr;
395
521
  if (isBoolean<T>()) {
396
- return deserializeBoolean(dataPtr, dataPtr + dataSize) as T;
522
+ const code = deserializeBooleanCode(dataPtr, dataPtr + dataSize);
523
+ if (code == 0) markProductionParseError();
524
+ return (code == 2) as T;
397
525
  } else if (isInteger<T>()) {
398
526
  return isSigned<T>()
399
527
  ? deserializeInteger<T>(dataPtr, dataPtr + dataSize)
@@ -407,7 +535,15 @@ export namespace JSON {
407
535
  ) {
408
536
  return null;
409
537
  } else if (isString<T>()) {
410
- return deserializeString(dataPtr, dataPtr + dataSize) as T;
538
+ // Whole-string decoders require quote-inclusive bounds. Trim only the
539
+ // insignificant suffix here; the normal no-whitespace path pays one
540
+ // predicted-not-taken branch.
541
+ let stringEnd = dataEnd;
542
+ while (stringEnd > dataPtr && JSON.Util.isSpace(load<u16>(stringEnd - 2)))
543
+ stringEnd -= 2;
544
+ const value = deserializeString(dataPtr, stringEnd);
545
+ if (changetype<usize>(value) == 0) markProductionParseError();
546
+ return value as T;
411
547
  } else {
412
548
  let type: nonnull<T> = changetype<nonnull<T>>(0);
413
549
  // @ts-expect-error: Defined by transform
@@ -427,20 +563,39 @@ export namespace JSON {
427
563
  : changetype<nonnull<T>>(
428
564
  __new(offsetof<nonnull<T>>(), idof<nonnull<T>>()),
429
565
  );
430
- // A freshly allocated object holds uninitialized fields (__new does not
431
- // zero). The fast path writes fields in place and may leave some
432
- // unwritten (@optional / skip-unknown), so it must run against defaults,
433
- // not garbage. A reused graph is already initialized - skip it.
566
+ // A successful generated full-write path assigns every field. Zeroing a
567
+ // fresh payload is enough to make its reference slots safe while
568
+ // avoiding redundant default graph construction; a fast miss still
569
+ // initializes normally before the slow retry below.
434
570
  // @ts-expect-error: Defined by transform
435
- if (!reuse && isDefined(type.__INITIALIZE)) obj.__INITIALIZE();
571
+ const fullWrite = isDefined(type.__DESERIALIZE_FULL_WRITE);
572
+ if (!reuse) {
573
+ if (fullWrite) {
574
+ memory.fill(changetype<usize>(obj), 0, offsetof<nonnull<T>>());
575
+ } else if (isDefined(type.__INITIALIZE)) {
576
+ // @ts-expect-error: Defined by transform
577
+ obj.__INITIALIZE();
578
+ }
579
+ }
436
580
  // @ts-expect-error: Defined by transform
437
581
  if (isDefined(type.__DESERIALIZE_FAST)) {
438
- // @ts-expect-error: Defined by transform
439
- const fastEnd = obj.__DESERIALIZE_FAST(
440
- dataPtr,
441
- dataPtr + dataSize,
442
- obj,
443
- );
582
+ let fastEnd: usize;
583
+ // @ts-expect-error: Defined by transform in SIMD builds
584
+ if (
585
+ ASC_FEATURE_SIMD &&
586
+ isPrettyParse() &&
587
+ isDefined(type.__DESERIALIZE_FAST_PRETTY)
588
+ ) {
589
+ // @ts-expect-error: Defined by transform
590
+ fastEnd = obj.__DESERIALIZE_FAST_PRETTY(
591
+ dataPtr,
592
+ dataPtr + dataSize,
593
+ obj,
594
+ );
595
+ } else {
596
+ // @ts-expect-error: Defined by transform
597
+ fastEnd = obj.__DESERIALIZE_FAST(dataPtr, dataPtr + dataSize, obj);
598
+ }
444
599
  // A non-zero return means the fast path matched; accept it when only
445
600
  // trailing whitespace remains (pretty-printed input ends with a
446
601
  // newline, so the cursor stops just past `}` rather than at srcEnd).
@@ -459,7 +614,12 @@ export namespace JSON {
459
614
  // @ts-expect-error: Defined by transform
460
615
  if (isDefined(type.__DESERIALIZE_SLOW)) {
461
616
  // @ts-expect-error: Defined by transform
462
- obj.__DESERIALIZE_SLOW(dataPtr, dataPtr + dataSize, obj);
617
+ const slowEnd = obj.__DESERIALIZE_SLOW(
618
+ dataPtr,
619
+ dataPtr + dataSize,
620
+ obj,
621
+ );
622
+ if (slowEnd == 0) markProductionParseError();
463
623
  // @ts-expect-error: Defined by transform for @lazy-field structs.
464
624
  if (isDefined(obj.__SET_SRC)) obj.__SET_SRC(data);
465
625
  return obj;
@@ -477,13 +637,15 @@ export namespace JSON {
477
637
  // Reuse the caller-supplied array when given (no allocation); the
478
638
  // element loop overwrites slots and trims length. Otherwise allocate.
479
639
  // @ts-expect-error
480
- return deserializeArray<nonnull<T>>(
640
+ const value = deserializeArray<nonnull<T>>(
481
641
  dataPtr,
482
642
  dataPtr + dataSize,
483
643
  changetype<usize>(out) != 0
484
644
  ? changetype<usize>(out)
485
645
  : changetype<usize>(instantiate<T>()),
486
646
  );
647
+ if (changetype<usize>(value) == 0) markProductionParseError();
648
+ return value;
487
649
  } else if (
488
650
  type instanceof Int8Array ||
489
651
  type instanceof Uint8Array ||
@@ -510,11 +672,13 @@ export namespace JSON {
510
672
  } else if (type instanceof Map) {
511
673
  // Reuse the caller-supplied map when given (keys overwrite in place).
512
674
  // @ts-expect-error
513
- return deserializeMap<nonnull<T>>(
675
+ const value = deserializeMap<nonnull<T>>(
514
676
  dataPtr,
515
677
  dataPtr + dataSize,
516
678
  changetype<usize>(out),
517
679
  );
680
+ if (changetype<usize>(value) == 0) markProductionParseError();
681
+ return value;
518
682
  } else if (type instanceof Date) {
519
683
  // @ts-expect-error
520
684
  return deserializeDate(dataPtr, dataPtr + dataSize);
@@ -525,27 +689,33 @@ export namespace JSON {
525
689
  // Reuse the caller-supplied JSON.Value handle when given (`out`); the
526
690
  // deserializer writes the parsed bits into it. Otherwise allocate.
527
691
  // @ts-expect-error
528
- return deserializeArbitrary(
692
+ const value = deserializeArbitrary(
529
693
  dataPtr,
530
694
  dataPtr + dataSize,
531
695
  changetype<usize>(out),
532
696
  );
697
+ if (changetype<usize>(value) == 0) markProductionParseError();
698
+ return value;
533
699
  } else if (type instanceof JSON.Obj) {
534
700
  // Reuse the caller-supplied JSON.Obj (cleared, buffers kept). Otherwise allocate.
535
701
  // @ts-expect-error
536
- return deserializeObject(
702
+ const value = deserializeObject(
537
703
  dataPtr,
538
704
  dataPtr + dataSize,
539
705
  changetype<usize>(out),
540
706
  );
707
+ if (changetype<usize>(value) == 0) markProductionParseError();
708
+ return value;
541
709
  } else if (type instanceof JSON.Arr) {
542
710
  // Reuse the caller-supplied JSON.Arr (cleared, buffers kept). Otherwise allocate.
543
711
  // @ts-expect-error
544
- return deserializeJsonArray(
712
+ const value = deserializeJsonArray(
545
713
  dataPtr,
546
714
  dataPtr + dataSize,
547
715
  changetype<usize>(out),
548
716
  );
717
+ if (changetype<usize>(value) == 0) markProductionParseError();
718
+ return value;
549
719
  } else if (type instanceof JSON.Box) {
550
720
  // @ts-expect-error
551
721
  return new JSON.Box(parseBox(data, changetype<nonnull<T>>(0).value));
@@ -1257,11 +1427,37 @@ export namespace JSON {
1257
1427
  _vused: i32 = 0;
1258
1428
  /** Source string the lazy slot pointers index into; anchors it for GC. */
1259
1429
  _src: string = "";
1430
+ /** Untouched source range for whole-object serialization passthrough. */
1431
+ _rawStart: usize = 0;
1432
+ _rawEnd: usize = 0;
1260
1433
  private _index: StaticArray<i32> | null = null;
1261
1434
  private _indexMask: i32 = 0;
1262
1435
 
1263
1436
  constructor() {}
1264
1437
 
1438
+ /**
1439
+ * Pre-sizes the three flat parse buffers from the source span. The parser
1440
+ * still grows normally if an unusual key-heavy document exceeds the
1441
+ * estimate; ordinary API-shaped objects avoid the repeated 8→16→32...
1442
+ * allocation/copy ladder.
1443
+ */
1444
+ reserveForParse(sourceUnits: usize): void {
1445
+ let keyNeed = <i32>((sourceUnits + 2) / 3);
1446
+ if (keyNeed < 16) keyNeed = 16;
1447
+ let keyCap = 16;
1448
+ while (keyCap < keyNeed) keyCap <<= 1;
1449
+ if (this._kbuf.length < keyCap) this._kbuf = new StaticArray<u16>(keyCap);
1450
+
1451
+ let slotNeed = <i32>(sourceUnits >> 6);
1452
+ if (slotNeed < 8) slotNeed = 8;
1453
+ let slotCap = 8;
1454
+ while (slotCap < slotNeed) slotCap <<= 1;
1455
+ if (this._kpos.length < slotCap)
1456
+ this._kpos = new StaticArray<i32>(slotCap);
1457
+ if (this._vals.length < slotCap)
1458
+ this._vals = new StaticArray<u64>(slotCap);
1459
+ }
1460
+
1265
1461
  /**
1266
1462
  * Gets the number of key-value pairs in the object.
1267
1463
  */
@@ -1458,6 +1654,11 @@ export namespace JSON {
1458
1654
  private materializeSlot(i: i32): u64 {
1459
1655
  const slot = unchecked(this._vals[i]);
1460
1656
  if (!JSON.Value.slotIsLazy(slot)) return slot;
1657
+ // A materialized nested object can subsequently be mutated through the
1658
+ // returned reference. Stop whole-object passthrough so serialization
1659
+ // observes that mutation and walks the cached slots instead.
1660
+ this._rawStart = 0;
1661
+ this._rawEnd = 0;
1461
1662
  const base = changetype<usize>(this._src);
1462
1663
  const start = JSON.Value.slotPtr(slot, base);
1463
1664
  const end = JSON.Value.slotEnd(slot, base, this.srcEnd());
@@ -1472,12 +1673,49 @@ export namespace JSON {
1472
1673
  * per-key string allocation, no per-value object, no hashing.
1473
1674
  */
1474
1675
  appendRawSlot(keyStart: usize, keyEnd: usize, bits: u64): void {
1676
+ this._rawStart = 0;
1677
+ this._rawEnd = 0;
1475
1678
  const slotIndex = this._vused;
1476
1679
  this.pushKeyBytes(keyStart, keyEnd, slotIndex);
1477
1680
  this.pushValSlot(bits);
1478
1681
  this.insertIndex(slotIndex);
1479
1682
  }
1480
1683
 
1684
+ /**
1685
+ * Parser-only append for slots produced by parseSlotBits. Those bits are
1686
+ * either immediate scalars or source-relative Lazy slices, so they never
1687
+ * require a GC link. The common pre-sized case writes all three buffers
1688
+ * directly; unusual key-heavy documents retain the general growth path.
1689
+ */
1690
+ appendParsedSlot(keyStart: usize, keyEnd: usize, bits: u64): void {
1691
+ this._rawStart = 0;
1692
+ this._rawEnd = 0;
1693
+ const slotIndex = this._vused;
1694
+ const len = <i32>((keyEnd - keyStart) >> 1);
1695
+ const keyPos = this._kused;
1696
+ if (
1697
+ keyPos + 1 + len <= this._kbuf.length &&
1698
+ slotIndex < this._kpos.length &&
1699
+ slotIndex < this._vals.length
1700
+ ) {
1701
+ const keyBuf = changetype<usize>(this._kbuf);
1702
+ store<u16>(keyBuf + ((<usize>keyPos) << 1), <u16>len);
1703
+ if (len)
1704
+ memory.copy(
1705
+ keyBuf + ((<usize>(keyPos + 1)) << 1),
1706
+ keyStart,
1707
+ (<usize>len) << 1,
1708
+ );
1709
+ unchecked((this._kpos[slotIndex] = keyPos));
1710
+ unchecked((this._vals[slotIndex] = bits));
1711
+ this._kused = keyPos + 1 + len;
1712
+ this._vused = slotIndex + 1;
1713
+ return;
1714
+ }
1715
+ this.pushKeyBytes(keyStart, keyEnd, slotIndex);
1716
+ this.pushValSlot(bits);
1717
+ }
1718
+
1481
1719
  /**
1482
1720
  * Appends a key and value (any T) without a duplicate-key check. Eagerly
1483
1721
  * boxes the value into a slot.
@@ -1550,6 +1788,8 @@ export namespace JSON {
1550
1788
  * @param value - The value (will be wrapped in JSON.Value)
1551
1789
  */
1552
1790
  set<T>(key: string, value: T): void {
1791
+ this._rawStart = 0;
1792
+ this._rawEnd = 0;
1553
1793
  const bits = JSON.Value.bitsFrom<T>(value);
1554
1794
  const i = this.indexOf(key);
1555
1795
  if (i >= 0) {
@@ -1620,6 +1860,8 @@ export namespace JSON {
1620
1860
  delete(key: string): bool {
1621
1861
  const removed = this.indexOf(key);
1622
1862
  if (removed < 0) return false;
1863
+ this._rawStart = 0;
1864
+ this._rawEnd = 0;
1623
1865
  const keys = this.keys();
1624
1866
  const oldVals = this._vals;
1625
1867
  const n = this._vused;
@@ -1652,9 +1894,10 @@ export namespace JSON {
1652
1894
  */
1653
1895
  clear(): void {
1654
1896
  this._kused = 0;
1655
- this._kpos = EMPTY_I32S;
1656
1897
  this._vused = 0;
1657
1898
  this._src = "";
1899
+ this._rawStart = 0;
1900
+ this._rawEnd = 0;
1658
1901
  this._index = null;
1659
1902
  this._indexMask = 0;
1660
1903
  }
@@ -1722,7 +1965,8 @@ export namespace JSON {
1722
1965
 
1723
1966
  const keys = value.keys();
1724
1967
  const values = value.values();
1725
- for (let i = 0; i < keys.length; i++) {
1968
+ const len = keys.length;
1969
+ for (let i = 0; i < len; i++) {
1726
1970
  out.set(unchecked(keys[i]), unchecked(values[i]));
1727
1971
  }
1728
1972
  return out;
@@ -1922,7 +2166,8 @@ export namespace JSON {
1922
2166
  const out = new JSON.Arr();
1923
2167
  // @ts-expect-error: T is JSON.Value[] here
1924
2168
  const arr = changetype<JSON.Value[]>(value);
1925
- for (let i = 0; i < arr.length; i++) {
2169
+ const len = arr.length;
2170
+ for (let i = 0; i < len; i++) {
1926
2171
  out.pushRawSlot(JSON.Value.bitsFrom<JSON.Value>(unchecked(arr[i])));
1927
2172
  }
1928
2173
  return out;
@@ -2369,6 +2614,24 @@ export namespace JSON {
2369
2614
  }
2370
2615
  }
2371
2616
 
2617
+ /**
2618
+ * Primitive serializer for generated structs that already reserved the
2619
+ * complete worst-case output size in `__SERIALIZE`. Avoids repeating buffer
2620
+ * capacity and stack-size accounting for every integer/boolean field.
2621
+ * Internal: emitted only for non-nullable primitive fields by the transform.
2622
+ */
2623
+ function __serializePrimitiveUnsafe<T>(data: T): void {
2624
+ if (isBoolean<T>()) {
2625
+ serializeBoolUnsafe(data as bool);
2626
+ } else if (isInteger<T>()) {
2627
+ // @ts-expect-error: transform emits this only for integer T
2628
+ serializeIntegerUnsafe<T>(data);
2629
+ } else {
2630
+ // Defensive fallback for hand-written transformed code.
2631
+ __serialize<T>(data);
2632
+ }
2633
+ }
2634
+
2372
2635
  /**
2373
2636
  * Deserializes JSON data directly from the buffer.
2374
2637
  * Should only be used if you know what you are doing.
@@ -2385,7 +2648,9 @@ export namespace JSON {
2385
2648
  srcStart += 2;
2386
2649
  if (isBoolean<T>()) {
2387
2650
  // @ts-expect-error: type
2388
- return deserializeBoolean(srcStart, srcEnd);
2651
+ const code = deserializeBooleanCode(srcStart, srcEnd);
2652
+ if (code == 0) throw new Error("Expected 'true' or 'false' in JSON");
2653
+ return code == 2;
2389
2654
  } else if (isInteger<T>()) {
2390
2655
  return isSigned<T>()
2391
2656
  ? deserializeInteger<T>(srcStart, srcEnd)
@@ -2409,7 +2674,12 @@ export namespace JSON {
2409
2674
  "Cannot parse data as string because it was formatted incorrectly!",
2410
2675
  );
2411
2676
 
2412
- return deserializeString(srcStart, srcEnd) as T;
2677
+ const value = deserializeString(srcStart, srcEnd);
2678
+ if (changetype<usize>(value) == 0) {
2679
+ takeProductionParseError();
2680
+ throw new Error("Invalid JSON string: missing surrounding quotes");
2681
+ }
2682
+ return value as T;
2413
2683
  } else {
2414
2684
  let type: nonnull<T> = changetype<nonnull<T>>(0);
2415
2685
  // @ts-expect-error: Defined by transform
@@ -2422,9 +2692,15 @@ export namespace JSON {
2422
2692
  isDefined(type.__DESERIALIZE_SLOW) ||
2423
2693
  isDefined(type.__DESERIALIZE_FAST)
2424
2694
  ) {
2695
+ const fresh = dst == 0;
2425
2696
  const out = changetype<nonnull<T>>(
2426
2697
  dst || __new(offsetof<nonnull<T>>(), idof<nonnull<T>>()),
2427
2698
  );
2699
+ // Generated fast paths may preserve omitted fields and reuse existing
2700
+ // strings/containers. Initialize only a freshly allocated object here;
2701
+ // callers that pass `dst` intentionally retain that reusable graph.
2702
+ // @ts-expect-error: Defined by transform
2703
+ if (fresh && isDefined(type.__INITIALIZE)) out.__INITIALIZE();
2428
2704
  // @ts-expect-error: Defined by transform
2429
2705
  if (isDefined(type.__DESERIALIZE_FAST)) {
2430
2706
  // @ts-expect-error: Defined by transform
@@ -2441,7 +2717,13 @@ export namespace JSON {
2441
2717
  // @ts-expect-error: Defined by transform
2442
2718
  if (isDefined(type.__DESERIALIZE_SLOW)) {
2443
2719
  // @ts-expect-error: Defined by transform
2444
- out.__DESERIALIZE_SLOW(srcStart, srcEnd, out);
2720
+ const slowEnd = out.__DESERIALIZE_SLOW(srcStart, srcEnd, out);
2721
+ if (slowEnd == 0) {
2722
+ takeProductionParseError();
2723
+ throw new Error("Malformed JSON object");
2724
+ }
2725
+ if (takeProductionParseError())
2726
+ throw new Error("Malformed nested JSON value");
2445
2727
  return out;
2446
2728
  }
2447
2729
  throw new Error(`No deserialize method defined for type ${type}`);
@@ -2451,7 +2733,12 @@ export namespace JSON {
2451
2733
  return deserializeStaticArray<nonnull<T>>(srcStart, srcEnd, dst) as T;
2452
2734
  } else if (type instanceof Array) {
2453
2735
  // @ts-expect-error: type
2454
- return deserializeArray<nonnull<T>>(srcStart, srcEnd, dst) as T;
2736
+ const value = deserializeArray<nonnull<T>>(srcStart, srcEnd, dst) as T;
2737
+ if (changetype<usize>(value) == 0) {
2738
+ takeProductionParseError();
2739
+ throw new Error("Unterminated array in JSON");
2740
+ }
2741
+ return value;
2455
2742
  } else if (
2456
2743
  type instanceof Int8Array ||
2457
2744
  type instanceof Uint8Array ||
@@ -2473,7 +2760,12 @@ export namespace JSON {
2473
2760
  return deserializeSet<T>(srcStart, srcEnd, dst);
2474
2761
  } else if (type instanceof Map) {
2475
2762
  // @ts-expect-error: type
2476
- return deserializeMap<T>(srcStart, srcEnd, dst);
2763
+ const value = deserializeMap<T>(srcStart, srcEnd, dst);
2764
+ if (changetype<usize>(value) == 0) {
2765
+ takeProductionParseError();
2766
+ throw new Error("Malformed JSON map");
2767
+ }
2768
+ return value;
2477
2769
  } else if (type instanceof Date) {
2478
2770
  // @ts-expect-error: type
2479
2771
  return deserializeDate(srcStart, srcEnd);
@@ -2482,13 +2774,28 @@ export namespace JSON {
2482
2774
  return deserializeRaw(srcStart, srcEnd);
2483
2775
  } else if (type instanceof JSON.Value) {
2484
2776
  // @ts-expect-error: type
2485
- return deserializeArbitrary(srcStart, srcEnd, 0);
2777
+ const value = deserializeArbitrary(srcStart, srcEnd, 0);
2778
+ if (changetype<usize>(value) == 0) {
2779
+ takeProductionParseError();
2780
+ throw new Error("Incomplete JSON value");
2781
+ }
2782
+ return value;
2486
2783
  } else if (type instanceof JSON.Obj) {
2487
2784
  // @ts-expect-error: type
2488
- return deserializeObject(srcStart, srcEnd, 0);
2785
+ const value = deserializeObject(srcStart, srcEnd, 0);
2786
+ if (changetype<usize>(value) == 0) {
2787
+ takeProductionParseError();
2788
+ throw new Error("Unterminated object in JSON");
2789
+ }
2790
+ return value;
2489
2791
  } else if (type instanceof JSON.Arr) {
2490
2792
  // @ts-expect-error: type
2491
- return deserializeJsonArray(srcStart, srcEnd, 0);
2793
+ const value = deserializeJsonArray(srcStart, srcEnd, 0);
2794
+ if (changetype<usize>(value) == 0) {
2795
+ takeProductionParseError();
2796
+ throw new Error("Unterminated array in JSON");
2797
+ }
2798
+ return value;
2492
2799
  } else if (type instanceof JSON.Box) {
2493
2800
  // @ts-expect-error: type
2494
2801
  return new JSON.Box(
@@ -2509,14 +2816,85 @@ export namespace JSON {
2509
2816
  );
2510
2817
  }
2511
2818
  export namespace Util {
2819
+ /** Record a cold malformed-input path and return the zero cursor sentinel. */
2820
+ export function failProductionParse(): usize {
2821
+ return failProductionParseInternal();
2822
+ }
2512
2823
  export function isSpace(code: u16): boolean {
2513
2824
  return code == 0x20 || code - 9 <= 4;
2514
2825
  }
2826
+
2827
+
2828
+ @inline
2829
+ export function beginObjectTrace(
2830
+ dst: usize,
2831
+ srcStart: usize,
2832
+ typeTag: string,
2833
+ ): i32 {
2834
+ return beginObjectFieldTrace(dst, srcStart, typeTag);
2835
+ }
2836
+
2837
+
2838
+ @inline
2839
+ export function getObjectTraceMask(slot: i32): u64 {
2840
+ return objectFieldTraceMask(slot);
2841
+ }
2842
+
2843
+
2844
+ @inline
2845
+ export function getObjectTraceTier(slot: i32): u8 {
2846
+ return objectFieldTraceTier(slot);
2847
+ }
2848
+
2849
+
2850
+ @inline
2851
+ export function getObjectTraceSeparator(slot: i32): usize {
2852
+ return objectFieldTraceSeparator(slot);
2853
+ }
2854
+
2855
+
2856
+ @inline
2857
+ export function saveObjectTrace(
2858
+ slot: i32,
2859
+ present: u64,
2860
+ tier: u8,
2861
+ separator: usize = 0,
2862
+ ): void {
2863
+ recordObjectFieldTrace(slot, present, tier, separator);
2864
+ }
2515
2865
  /** Advance past JSON whitespace (space, tab, LF, VT, FF, CR). */
2866
+ @inline
2516
2867
  export function skipWhitespace(srcStart: usize, srcEnd: usize): usize {
2517
2868
  while (srcStart < srcEnd && isSpace(load<u16>(srcStart))) srcStart += 2;
2518
2869
  return srcStart;
2519
2870
  }
2871
+ /** Advance a whitespace run using the SIMD pretty-document scanner. */
2872
+ @inline
2873
+ export function skipPrettyWhitespace(
2874
+ srcStart: usize,
2875
+ srcEnd: usize,
2876
+ ): usize {
2877
+ if (srcStart >= srcEnd) return srcStart;
2878
+ const code = load<u16>(srcStart);
2879
+ if (!isSpace(code)) return srcStart;
2880
+ if (code == 0x20) {
2881
+ srcStart += 2;
2882
+ if (srcStart >= srcEnd || !isSpace(load<u16>(srcStart)))
2883
+ return srcStart;
2884
+ }
2885
+ return skipPrettyWhitespace_SIMD(srcStart, srcEnd);
2886
+ }
2887
+ /** Compare a bounded UTF-16 source slice with a compile-time literal. */
2888
+ @inline
2889
+ export function matchesLiteral(
2890
+ srcStart: usize,
2891
+ srcEnd: usize,
2892
+ expected: string,
2893
+ ): bool {
2894
+ const length = <usize>expected.length;
2895
+ if (srcEnd - srcStart != length << 1) return false;
2896
+ return utf16Equals(srcStart, changetype<usize>(expected), <i32>length);
2897
+ }
2520
2898
  function scanQuotedValueEnd(srcStart: usize, srcEnd: usize): usize {
2521
2899
  const endQuote = scanStringEnd(srcStart, srcEnd);
2522
2900
  return endQuote >= srcEnd ? 0 : endQuote + 2;