json-as 1.4.0 → 1.5.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 (86) hide show
  1. package/CHANGELOG.md +50 -29
  2. package/README.md +84 -33
  3. package/assembly/custom/chars.ts +39 -78
  4. package/assembly/deserialize/index/arbitrary.ts +26 -8
  5. package/assembly/deserialize/index/float.ts +2 -4
  6. package/assembly/deserialize/index/integer.ts +2 -4
  7. package/assembly/deserialize/index/object.ts +6 -1
  8. package/assembly/deserialize/index/string.ts +2 -7
  9. package/assembly/deserialize/index/unsigned.ts +2 -4
  10. package/assembly/deserialize/naive/array/integer.ts +1 -1
  11. package/assembly/deserialize/naive/array/map.ts +1 -1
  12. package/assembly/deserialize/naive/array/object.ts +1 -1
  13. package/assembly/deserialize/naive/array/struct.ts +19 -1
  14. package/assembly/deserialize/naive/bool.ts +1 -5
  15. package/assembly/deserialize/naive/date.ts +1 -2
  16. package/assembly/deserialize/naive/float.ts +2 -7
  17. package/assembly/deserialize/naive/integer.ts +1 -2
  18. package/assembly/deserialize/naive/map.ts +5 -6
  19. package/assembly/deserialize/naive/object.ts +151 -13
  20. package/assembly/deserialize/naive/raw.ts +1 -5
  21. package/assembly/deserialize/naive/set.ts +2 -4
  22. package/assembly/deserialize/naive/staticarray.ts +1 -2
  23. package/assembly/deserialize/naive/string.ts +6 -9
  24. package/assembly/deserialize/naive/unsigned.ts +1 -2
  25. package/assembly/deserialize/simd/array/integer.ts +2 -7
  26. package/assembly/deserialize/simd/float.ts +3 -5
  27. package/assembly/deserialize/simd/integer.ts +2 -7
  28. package/assembly/deserialize/simd/string.ts +5 -22
  29. package/assembly/deserialize/swar/array/arbitrary.ts +1 -2
  30. package/assembly/deserialize/swar/array/array.ts +1 -2
  31. package/assembly/deserialize/swar/array/bool.ts +1 -2
  32. package/assembly/deserialize/swar/array/box.ts +1 -2
  33. package/assembly/deserialize/swar/array/float.ts +6 -18
  34. package/assembly/deserialize/swar/array/generic.ts +1 -2
  35. package/assembly/deserialize/swar/array/integer.ts +7 -16
  36. package/assembly/deserialize/swar/array/map.ts +1 -2
  37. package/assembly/deserialize/swar/array/object.ts +1 -2
  38. package/assembly/deserialize/swar/array/raw.ts +1 -2
  39. package/assembly/deserialize/swar/array/shared.ts +6 -13
  40. package/assembly/deserialize/swar/array/string.ts +3 -8
  41. package/assembly/deserialize/swar/array/struct.ts +2 -8
  42. package/assembly/deserialize/swar/array.ts +1 -3
  43. package/assembly/deserialize/swar/float.ts +4 -9
  44. package/assembly/deserialize/swar/integer.ts +2 -7
  45. package/assembly/deserialize/swar/string.ts +13 -15
  46. package/assembly/deserialize/swar/typedarray.ts +4 -4
  47. package/assembly/index.d.ts +29 -24
  48. package/assembly/index.ts +1362 -246
  49. package/assembly/serialize/index/arbitrary.ts +70 -4
  50. package/assembly/serialize/index/jsonarray.ts +51 -0
  51. package/assembly/serialize/index/object.ts +25 -3
  52. package/assembly/serialize/index/string.ts +1 -2
  53. package/assembly/serialize/index/typedarray.ts +1 -2
  54. package/assembly/serialize/index.ts +1 -0
  55. package/assembly/serialize/naive/array.ts +23 -34
  56. package/assembly/serialize/naive/bool.ts +0 -1
  57. package/assembly/serialize/naive/float.ts +16 -25
  58. package/assembly/serialize/naive/integer.ts +1 -5
  59. package/assembly/serialize/naive/raw.ts +1 -2
  60. package/assembly/serialize/naive/set.ts +0 -4
  61. package/assembly/serialize/naive/staticarray.ts +0 -5
  62. package/assembly/serialize/naive/string.ts +2 -5
  63. package/assembly/serialize/naive/typedarray.ts +0 -6
  64. package/assembly/serialize/simd/string.ts +1 -3
  65. package/assembly/serialize/swar/string.ts +1 -2
  66. package/assembly/util/atoi-fast.ts +4 -14
  67. package/assembly/util/bytes.ts +1 -2
  68. package/assembly/util/idofd.ts +1 -2
  69. package/assembly/util/isSpace.ts +1 -2
  70. package/assembly/util/itoa-fast.ts +6 -9
  71. package/assembly/util/nextPowerOf2.ts +1 -2
  72. package/assembly/util/parsefloat-fast.ts +3 -5
  73. package/assembly/util/ptrToStr.ts +1 -2
  74. package/assembly/util/scanValueEndSimd.ts +54 -16
  75. package/assembly/util/scanValueEndSwar.ts +67 -25
  76. package/assembly/util/scientific.ts +5 -8
  77. package/assembly/util/snp.ts +1 -2
  78. package/assembly/util/swar-int.ts +5 -10
  79. package/assembly/util/swar.ts +2 -4
  80. package/lib/as-bs.ts +23 -45
  81. package/package.json +14 -7
  82. package/transform/lib/index.js +108 -64
  83. package/transform/lib/types.d.ts +2 -1
  84. package/transform/lib/types.js +3 -0
  85. package/assembly/util/dragonbox-cache.ts +0 -445
  86. package/assembly/util/dragonbox.ts +0 -652
package/assembly/index.ts CHANGED
@@ -16,6 +16,7 @@ import {
16
16
  serializeFloat64,
17
17
  serializeStruct,
18
18
  serializeObject,
19
+ serializeJsonArray,
19
20
  serializeRaw,
20
21
  serializeString,
21
22
  serializeArrayBufferUnsafe,
@@ -34,10 +35,13 @@ import {
34
35
  deserializeStaticArray,
35
36
  deserializeArbitrary,
36
37
  deserializeObject,
38
+ deserializeJsonArray,
37
39
  deserializeRaw,
38
40
  deserializeString,
39
41
  deserializeArrayBuffer,
40
42
  deserializeTypedArray,
43
+ setParseSrc,
44
+ getParseSrc,
41
45
  } from "./deserialize";
42
46
  import {
43
47
  BACK_SLASH,
@@ -53,81 +57,155 @@ import {
53
57
  FALSE_WORD_U64,
54
58
  } from "./custom/chars";
55
59
  import { itoa_buffered } from "util/number";
56
- import {
57
- dragonbox_f32_buffered,
58
- dragonbox_f64_buffered,
59
- } from "./util/dragonbox";
60
+ import { dtoa_buffered, ftoa_buffered } from "xjb-as";
60
61
  import { ptrToStr } from "./util/ptrToStr";
61
62
  import { atoi, bytes, scanStringEnd } from "./util";
62
63
  import { scanValueEnd_SIMD } from "./util/scanValueEndSimd";
63
64
  import { scanValueEnd_SWAR } from "./util/scanValueEndSwar";
64
65
 
65
- // --- NaN-boxing encoding for JSON.Value ----------------------------------
66
- // JSON.Value packs its type tag and payload into a single 8-byte word.
67
- // Real f64 values are stored as their raw IEEE-754 bits; every other type is
68
- // encoded inside a quiet-NaN ("boxed") word: a 5-bit tag (the JSON.Types id,
69
- // or Struct for @json classes) plus a 45-bit payload holding a 32-bit pointer
70
- // or a small scalar. 64-bit ints that don't fit the payload are spilled to a
71
- // heap StaticArray<u64> referenced by the payload pointer and flagged in the
72
- // sign bit. Only quiet-NaN doubles whose top two mantissa bits are both set
73
- // collide with the box signature; hardware NaN is 0x7FF8.. so finite/Inf/NaN
74
- // doubles all pass through untouched.
75
- //
76
- // The managed reference packed into the word is traced by JSON.Value.__visit;
77
- // AssemblyScript only wires that hook for library-declared classes, so the
78
- // json-as transform marks this source as a library source (see afterParse).
79
- // @ts-expect-error: Decorator valid here
80
- @inline const VAL_QNAN: u64 = 0x7ffc000000000000; // boxed signature (quiet NaN)
81
- // @ts-expect-error: Decorator valid here
82
- @inline const VAL_TAG_SHIFT: u8 = 45;
83
- // @ts-expect-error: Decorator valid here
84
- @inline const VAL_PAYLOAD_MASK: u64 = 0x00001fffffffffff; // low 45 bits
85
- // @ts-expect-error: Decorator valid here
86
- @inline const VAL_PTR_MASK: u64 = 0xffffffff; // wasm32 pointer
87
- // @ts-expect-error: Decorator valid here
88
- @inline const VAL_BOX64: u64 = 0x8000000000000000; // sign bit: 64-bit int spilled to heap
89
- // @ts-expect-error: Decorator valid here
90
- @inline const VAL_NULL: u64 = VAL_QNAN; // tag 0 (Null), payload 0
91
- // @ts-expect-error: Decorator valid here
92
- @inline const VAL_I64_LIMIT: i64 = 17592186044416; // 2^44 inline range is [-2^44, 2^44)
93
- // @ts-expect-error: Decorator valid here
94
- @inline const VAL_U64_LIMIT: u64 = 35184372088832; // 2^45 — inline range is [0, 2^45)
95
-
96
- // @ts-expect-error: Decorator valid here
97
- @inline function valBoxed(w: u64): bool {
66
+ const VAL_QNAN: u64 = 0x7ffc000000000000; // boxed signature (quiet NaN)
67
+ const VAL_TAG_SHIFT: u8 = 45;
68
+ const VAL_PAYLOAD_MASK: u64 = 0x00001fffffffffff; // low 45 bits
69
+ const VAL_PTR_MASK: u64 = 0xffffffff; // wasm32 pointer
70
+ const VAL_BOX64: u64 = 0x8000000000000000; // sign bit: 64-bit int spilled to heap
71
+ const VAL_NULL: u64 = VAL_QNAN; // tag 0 (Null), payload 0
72
+ const VAL_I64_LIMIT: i64 = 17592186044416; // 2^44 - inline range is [-2^44, 2^44)
73
+ const VAL_U64_LIMIT: u64 = 35184372088832; // 2^45 - inline range is [0, 2^45)
74
+
75
+ // Lazy value-slot payload layout (45-bit box payload), see JSON.Value.lazyBits.
76
+ // Compact form (bit 44 = 0): a source-relative start offset (23 bits) + the
77
+ // value's length (21 bits), both in UTF-16 units. Offset-heavy on purpose:
78
+ // object/array fields are usually small while the document can be large, so
79
+ // offset overflow (a field late in a big doc) is the realistic trigger, not a
80
+ // single giant field - so the offset field gets the wider range. bit 44 flags
81
+ // the absolute (scan-on-demand) fallback for a source/value past those ranges.
82
+ // bits [0..22] offset (<=~16MB src) · [23..43] length (<=~4MB val) · 44 abs
83
+ const LZ_OFF_BITS: u64 = 23;
84
+ const LZ_OFF_MASK: u64 = 0x7fffff; // (1 << 23) - 1
85
+ const LZ_LEN_MASK: u64 = 0x1fffff; // (1 << 21) - 1
86
+ const LZ_ABS_FLAG: u64 = 0x100000000000; // 1 << 44
87
+
88
+ // A materialized String box only uses the low 32 payload bits for its pointer,
89
+ // so two spare bits [32..33] cache the value's serialize-escape class. This lets
90
+ // re-serializing a dynamic string skip the per-char escape scan - a clean string
91
+ // emits via a single memcpy. `valPtr` masks bit 32+ off, so the pointer (and GC)
92
+ // are unaffected.
93
+ // 0 = unclassified · 1 = clean (no escaping -> memcpy) · 2 = needs escaping
94
+ const VAL_STR_CLASS_SHIFT: u64 = 32;
95
+ const VAL_STR_CLASS_MASK: u64 = 0x0000000300000000; // bits [32..33]
96
+ const STR_CLASS_UNKNOWN: u32 = 0;
97
+ const STR_CLASS_CLEAN: u32 = 1;
98
+ const STR_CLASS_ESCAPE: u32 = 2;
99
+
100
+ function valBoxed(w: u64): bool {
98
101
  return (w & VAL_QNAN) == VAL_QNAN;
99
102
  }
100
- // @ts-expect-error: Decorator valid here
101
- @inline function valTag(w: u64): u32 {
103
+ function valTag(w: u64): u32 {
102
104
  return <u32>((w >> VAL_TAG_SHIFT) & 0x1f);
103
105
  }
104
- // @ts-expect-error: Decorator valid here
105
- @inline function valPayload(w: u64): u64 {
106
+ function valPayload(w: u64): u64 {
106
107
  return w & VAL_PAYLOAD_MASK;
107
108
  }
108
- // @ts-expect-error: Decorator valid here
109
- @inline function valPtr(w: u64): usize {
109
+ function valPtr(w: u64): usize {
110
110
  return <usize>(w & VAL_PTR_MASK);
111
111
  }
112
- // @ts-expect-error: Decorator valid here
113
- @inline function valBox(tag: u32, payload: u64): u64 {
112
+ function valBox(tag: u32, payload: u64): u64 {
114
113
  return (
115
114
  VAL_QNAN | ((<u64>tag) << VAL_TAG_SHIFT) | (payload & VAL_PAYLOAD_MASK)
116
115
  );
117
116
  }
118
- // @ts-expect-error: Decorator valid here
119
- @inline function valIntTag<T>(): u32 {
117
+ function valLazy(w: u64): bool {
118
+ return valBoxed(w) && valTag(w) == JSON.Types.Lazy;
119
+ }
120
+ function valIntTag<T>(): u32 {
120
121
  if (sizeof<T>() == 1) return isSigned<T>() ? JSON.Types.I8 : JSON.Types.U8;
121
122
  if (sizeof<T>() == 2) return isSigned<T>() ? JSON.Types.I16 : JSON.Types.U16;
122
123
  if (sizeof<T>() == 4) return isSigned<T>() ? JSON.Types.I32 : JSON.Types.U32;
123
124
  return isSigned<T>() ? JSON.Types.I64 : JSON.Types.U64;
124
125
  }
125
126
 
127
+ function hashUtf16(ptr: usize, len: i32): u32 {
128
+ let h: u32 = 2166136261;
129
+ for (let i = 0; i < len; i++) {
130
+ h ^= <u32>load<u16>(ptr + ((<usize>i) << 1));
131
+ h *= 16777619;
132
+ }
133
+ h ^= <u32>len;
134
+ h *= 16777619;
135
+ return h;
136
+ }
137
+
138
+ // v128 path: compare 8 code units (16 bytes) per step, then a u64 step (4) and
139
+ // a scalar tail. Each load is bounded by `len`, so it never reads past either
140
+ // key. Only reachable (and thus only compiled) when the SIMD feature is on, so
141
+ // the intrinsics don't break the naive/swar builds.
142
+ function utf16Equals_SIMD(ptrA: usize, ptrB: usize, len: i32): bool {
143
+ let i = 0;
144
+ for (; i + 8 <= len; i += 8) {
145
+ const off = (<usize>i) << 1;
146
+ if (v128.any_true(v128.xor(v128.load(ptrA + off), v128.load(ptrB + off))))
147
+ return false;
148
+ }
149
+ for (; i + 4 <= len; i += 4) {
150
+ const off = (<usize>i) << 1;
151
+ if (load<u64>(ptrA + off) != load<u64>(ptrB + off)) return false;
152
+ }
153
+ for (; i < len; i++) {
154
+ const off = (<usize>i) << 1;
155
+ if (load<u16>(ptrA + off) != load<u16>(ptrB + off)) return false;
156
+ }
157
+ return true;
158
+ }
159
+
160
+ function utf16Equals(ptrA: usize, ptrB: usize, len: i32): bool {
161
+ if (ASC_FEATURE_SIMD) return utf16Equals_SIMD(ptrA, ptrB, len);
162
+ // Scalar: 4 code units (one u64) per step, scalar tail. Bounded by `len`.
163
+ let i = 0;
164
+ for (; i + 4 <= len; i += 4) {
165
+ const off = (<usize>i) << 1;
166
+ if (load<u64>(ptrA + off) != load<u64>(ptrB + off)) return false;
167
+ }
168
+ for (; i < len; i++) {
169
+ const off = (<usize>i) << 1;
170
+ if (load<u16>(ptrA + off) != load<u16>(ptrB + off)) return false;
171
+ }
172
+ return true;
173
+ }
174
+
126
175
  // Shared zero-length sentinel for JSON.Obj's key buffer, so an empty object
127
176
  // allocates no key storage until its first key is inserted. Never mutated.
128
177
  // @ts-expect-error: Decorator valid here
129
178
  @lazy const EMPTY_KEYS: StaticArray<u16> = new StaticArray<u16>(0);
130
179
 
180
+ // Shared zero-length sentinel for JSON.Obj's key-position/index buffers.
181
+ // Never mutated.
182
+ // @ts-expect-error: Decorator valid here
183
+ @lazy const EMPTY_I32S: StaticArray<i32> = new StaticArray<i32>(0);
184
+
185
+ // Shared zero-length sentinel for JSON.Obj's value-slot buffer, so an empty
186
+ // object allocates no slot storage until its first value. Never mutated.
187
+ // @ts-expect-error: Decorator valid here
188
+ @lazy const EMPTY_VALS: StaticArray<u64> = new StaticArray<u64>(0);
189
+
190
+ // A JSON.Obj with at most this many keys resolves lookups by linear scan and
191
+ // never allocates/hashes a key index. Most JSON objects are small and dynamic
192
+ // access touches only a few keys, so the O(n) index build a hash table needs is
193
+ // pure overhead below this size. Above it, the hash index amortizes.
194
+ const OBJ_LINEAR_MAX: i32 = 6;
195
+
196
+ // Deferred-value record for a lazy JSON.Value (see JSON.Types.Lazy). `lz` packs
197
+ // the unparsed source slice as (sliceStart << 32) | sliceEnd - the same encoding
198
+ // the transform uses for @lazy struct fields. `src` is the GC anchor that keeps
199
+ // the source string (and therefore the slice pointers, which index into its
200
+ // UTF-16 buffer) alive until the value is materialized. Managed so `src` is
201
+ // traced automatically; a JSON.Value's __visit traces the LazyRef itself.
202
+ // @ts-expect-error: decorators allowed here
203
+ @final
204
+ class LazyRef {
205
+ lz: u64 = 0;
206
+ src: string = "";
207
+ }
208
+
131
209
  export namespace JSON {
132
210
  /**
133
211
  * On-demand field marker. `JSON.Lazy<T>` is structurally just `T` (a no-op
@@ -139,15 +217,14 @@ export namespace JSON {
139
217
  export type Lazy<T> = T;
140
218
 
141
219
  /**
142
- * Whether a lazy slot's value is JSON null for `@omitnull` on lazy fields,
220
+ * Whether a lazy slot's value is JSON null - for `@omitnull` on lazy fields,
143
221
  * without forcing materialization. The slot encodes the state: `u64.MAX_VALUE`
144
222
  * = materialized (null iff the value pointer is 0), `0` = absent (null), any
145
223
  * other value = a not-yet-parsed slice range (null iff it is literally `null`).
146
224
  * @param valPtr pointer of the materialized value (0 when null)
147
225
  * @param lz the packed slot
148
226
  */
149
- // @ts-expect-error: inline
150
- @inline export function __lazyIsNull(valPtr: usize, lz: u64): bool {
227
+ export function __lazyIsNull(valPtr: usize, lz: u64): bool {
151
228
  if (lz == u64.MAX_VALUE) return valPtr == 0;
152
229
  if (lz == 0) return true;
153
230
  const hi = <usize>(lz >>> 32);
@@ -182,11 +259,7 @@ export namespace JSON {
182
259
  * @param data T
183
260
  * @returns string
184
261
  */
185
- // @ts-expect-error: inline
186
- @inline export function stringify<T>(
187
- data: T,
188
- out: string | null = null,
189
- ): string {
262
+ export function stringify<T>(data: T, out: string | null = null): string {
190
263
  if (isBoolean<T>()) {
191
264
  if (out) {
192
265
  if (<bool>data == true) {
@@ -226,13 +299,14 @@ export namespace JSON {
226
299
  return data.toString();
227
300
  } else if (isFloat<T>(data)) {
228
301
  out = out
229
- ? changetype<string>(__renew(changetype<usize>(out), 64))
230
- : changetype<string>(__new(64, idof<string>()));
302
+ ? changetype<string>(__renew(changetype<usize>(out), 128))
303
+ : changetype<string>(__new(128, idof<string>()));
304
+ const startPtr = changetype<usize>(out);
231
305
  const bytes =
232
306
  (sizeof<T>() == 4
233
- ? dragonbox_f32_buffered(changetype<usize>(out), <f32>data)
234
- : dragonbox_f64_buffered(changetype<usize>(out), <f64>data)) << 1;
235
- return changetype<string>(__renew(changetype<usize>(out), bytes));
307
+ ? ftoa_buffered(startPtr, <f32>data)
308
+ : dtoa_buffered(startPtr, <f64>data)) << 1;
309
+ return changetype<string>(__renew(startPtr, bytes));
236
310
  } else if (isNullable<T>() && changetype<usize>(data) == <usize>0) {
237
311
  if (out) {
238
312
  out = changetype<string>(__renew(changetype<usize>(out), 8));
@@ -289,21 +363,31 @@ export namespace JSON {
289
363
  // A type-correct "zero" for any T: null pointer for references, 0/false for
290
364
  // value types. `changetype<T>(0)` alone fails for bool/f64 (size mismatch),
291
365
  // so branch on isReference at compile time.
292
- // @ts-ignore: inline
293
- @inline function __zero<T>(): T {
366
+ function __zero<T>(): T {
294
367
  // @ts-ignore: compile-time intrinsic
295
368
  if (isReference<T>() || isManaged<T>()) return changetype<T>(0);
296
369
  return <T>0;
297
370
  }
298
371
 
299
- // @ts-expect-error: inline
300
- @inline export function parse<T>(data: string, out: T = __zero<T>()): T {
372
+ export function parse<T>(data: string, out: T = __zero<T>()): T {
373
+ // Anchor the source for any lazy JSON.Obj/JSON.Value built while parsing, so
374
+ // their stored slice pointers (into `data`'s buffer) stay valid and resolve
375
+ // against the right string. Save/restore makes nested parses (e.g. a custom
376
+ // deserializer calling JSON.parse, or JSON.Obj.from) re-entrant-safe.
377
+ const prevSrc = getParseSrc();
378
+ setParseSrc(data);
379
+ const result = parseInternal<T>(data, out);
380
+ setParseSrc(prevSrc);
381
+ return result;
382
+ }
383
+
384
+ function parseInternal<T>(data: string, out: T = __zero<T>()): T {
301
385
  let dataPtr = changetype<usize>(data);
302
386
  const dataEnd = dataPtr + bytes(data);
303
387
  // Entry point skips leading whitespace: every deserialize handler may then
304
388
  // assume srcStart points at the first non-whitespace char. Handlers must
305
389
  // NOT re-skip leading whitespace themselves. (Trailing whitespace is left
306
- // intact scalars stop at the value end, composites self-trim, and
390
+ // intact - scalars stop at the value end, composites self-trim, and
307
391
  // JSON.Raw intentionally preserves trailing bytes.)
308
392
  while (dataPtr < dataEnd && JSON.Util.isSpace(load<u16>(dataPtr)))
309
393
  dataPtr += 2;
@@ -337,11 +421,18 @@ export namespace JSON {
337
421
  isDefined(type.__DESERIALIZE_FAST)
338
422
  ) {
339
423
  // Reuse the caller-supplied `out` graph when given; otherwise allocate.
340
- const obj = changetype<usize>(out)
424
+ const reuse = changetype<usize>(out) != 0;
425
+ const obj = reuse
341
426
  ? changetype<nonnull<T>>(changetype<usize>(out))
342
427
  : changetype<nonnull<T>>(
343
428
  __new(offsetof<nonnull<T>>(), idof<nonnull<T>>()),
344
429
  );
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.
434
+ // @ts-expect-error: Defined by transform
435
+ if (!reuse && isDefined(type.__INITIALIZE)) obj.__INITIALIZE();
345
436
  // @ts-expect-error: Defined by transform
346
437
  if (isDefined(type.__DESERIALIZE_FAST)) {
347
438
  // @ts-expect-error: Defined by transform
@@ -358,7 +449,7 @@ export namespace JSON {
358
449
  JSON.Util.skipWhitespace(fastEnd, dataPtr + dataSize) ==
359
450
  dataPtr + dataSize
360
451
  ) {
361
- // @ts-expect-error: Defined by transform for @lazy-field structs
452
+ // @ts-expect-error: Defined by transform for @lazy-field structs -
362
453
  // pins the source so stored slice ranges stay valid.
363
454
  if (isDefined(obj.__SET_SRC)) obj.__SET_SRC(data);
364
455
  return obj;
@@ -383,11 +474,15 @@ export namespace JSON {
383
474
  0,
384
475
  );
385
476
  } else if (type instanceof Array) {
477
+ // Reuse the caller-supplied array when given (no allocation); the
478
+ // element loop overwrites slots and trims length. Otherwise allocate.
386
479
  // @ts-expect-error
387
480
  return deserializeArray<nonnull<T>>(
388
481
  dataPtr,
389
482
  dataPtr + dataSize,
390
- changetype<usize>(instantiate<T>()),
483
+ changetype<usize>(out) != 0
484
+ ? changetype<usize>(out)
485
+ : changetype<usize>(instantiate<T>()),
391
486
  );
392
487
  } else if (
393
488
  type instanceof Int8Array ||
@@ -413,8 +508,13 @@ export namespace JSON {
413
508
  // @ts-expect-error
414
509
  return deserializeSet<nonnull<T>>(dataPtr, dataPtr + dataSize, 0);
415
510
  } else if (type instanceof Map) {
511
+ // Reuse the caller-supplied map when given (keys overwrite in place).
416
512
  // @ts-expect-error
417
- return deserializeMap<nonnull<T>>(dataPtr, dataPtr + dataSize, 0);
513
+ return deserializeMap<nonnull<T>>(
514
+ dataPtr,
515
+ dataPtr + dataSize,
516
+ changetype<usize>(out),
517
+ );
418
518
  } else if (type instanceof Date) {
419
519
  // @ts-expect-error
420
520
  return deserializeDate(dataPtr, dataPtr + dataSize);
@@ -422,11 +522,30 @@ export namespace JSON {
422
522
  // @ts-expect-error: type
423
523
  return deserializeRaw(dataPtr, dataPtr + dataSize);
424
524
  } else if (type instanceof JSON.Value) {
525
+ // Reuse the caller-supplied JSON.Value handle when given (`out`); the
526
+ // deserializer writes the parsed bits into it. Otherwise allocate.
425
527
  // @ts-expect-error
426
- return deserializeArbitrary(dataPtr, dataPtr + dataSize, 0);
528
+ return deserializeArbitrary(
529
+ dataPtr,
530
+ dataPtr + dataSize,
531
+ changetype<usize>(out),
532
+ );
427
533
  } else if (type instanceof JSON.Obj) {
534
+ // Reuse the caller-supplied JSON.Obj (cleared, buffers kept). Otherwise allocate.
428
535
  // @ts-expect-error
429
- return deserializeObject(dataPtr, dataPtr + dataSize, 0);
536
+ return deserializeObject(
537
+ dataPtr,
538
+ dataPtr + dataSize,
539
+ changetype<usize>(out),
540
+ );
541
+ } else if (type instanceof JSON.Arr) {
542
+ // Reuse the caller-supplied JSON.Arr (cleared, buffers kept). Otherwise allocate.
543
+ // @ts-expect-error
544
+ return deserializeJsonArray(
545
+ dataPtr,
546
+ dataPtr + dataSize,
547
+ changetype<usize>(out),
548
+ );
430
549
  } else if (type instanceof JSON.Box) {
431
550
  // @ts-expect-error
432
551
  return new JSON.Box(parseBox(data, changetype<nonnull<T>>(0).value));
@@ -453,47 +572,33 @@ export namespace JSON {
453
572
  */
454
573
  export namespace Types {
455
574
  /** Represents a null value */
456
- // @ts-expect-error
457
- @inline export const Null: u16 = 0;
458
- // @ts-expect-error
459
- @inline export const Raw: u16 = 1;
460
- // @ts-expect-error
461
- @inline export const U8: u16 = 2;
462
- // @ts-expect-error
463
- @inline export const U16: u16 = 3;
464
- // @ts-expect-error
465
- @inline export const U32: u16 = 4;
466
- // @ts-expect-error
467
- @inline export const U64: u16 = 5;
468
- // @ts-expect-error
469
- @inline export const I8: u16 = 6;
470
- // @ts-expect-error
471
- @inline export const I16: u16 = 7;
472
- // @ts-expect-error
473
- @inline export const I32: u16 = 8;
474
- // @ts-expect-error
475
- @inline export const I64: u16 = 9;
476
- // @ts-expect-error
477
- @inline export const F32: u16 = 10;
478
- // @ts-expect-error
479
- @inline export const F64: u16 = 11;
480
- // @ts-expect-error
481
- @inline export const Bool: u16 = 12;
575
+ export const Null: u16 = 0;
576
+ export const Raw: u16 = 1;
577
+ export const U8: u16 = 2;
578
+ export const U16: u16 = 3;
579
+ export const U32: u16 = 4;
580
+ export const U64: u16 = 5;
581
+ export const I8: u16 = 6;
582
+ export const I16: u16 = 7;
583
+ export const I32: u16 = 8;
584
+ export const I64: u16 = 9;
585
+ export const F32: u16 = 10;
586
+ export const F64: u16 = 11;
587
+ export const Bool: u16 = 12;
482
588
  // Managed
483
- // @ts-expect-error
484
- @inline export const String: u16 = 13;
485
- // @ts-expect-error
486
- @inline export const Object: u16 = 14;
487
- // @ts-expect-error
488
- @inline export const Array: u16 = 15;
489
- // @ts-expect-error
490
- @inline export const Map: u16 = 16;
491
- // @ts-expect-error
492
- @inline export const Struct: u16 = 17;
493
- // @ts-expect-error
494
- @inline export const TypedArray: u16 = 18;
495
- // @ts-expect-error
496
- @inline export const ArrayBuffer: u16 = 19;
589
+ export const String: u16 = 13;
590
+ export const Object: u16 = 14;
591
+ export const Array: u16 = 15;
592
+ export const Map: u16 = 16;
593
+ export const Struct: u16 = 17;
594
+ export const TypedArray: u16 = 18;
595
+ export const ArrayBuffer: u16 = 19;
596
+ /**
597
+ * Internal: a not-yet-materialized value holding a raw source slice
598
+ * (see LazyRef). Never returned by `JSON.Value.type` - accessing the value
599
+ * materializes it first, so callers only ever observe the concrete type.
600
+ */
601
+ export const Lazy: u16 = 20;
497
602
  }
498
603
 
499
604
  /**
@@ -542,7 +647,7 @@ export namespace JSON {
542
647
  * @param data - A valid JSON string
543
648
  * @returns A new Raw instance
544
649
  */
545
- @inline static from(data: string): JSON.Raw {
650
+ static from(data: string): JSON.Raw {
546
651
  return new JSON.Raw(data);
547
652
  }
548
653
  }
@@ -566,8 +671,7 @@ export namespace JSON {
566
671
  * ```
567
672
  */
568
673
  // @ts-expect-error: decorators allowed here
569
- @final
570
- export class Value {
674
+ @final export class Value {
571
675
  /** Map of struct type IDs to their serialization function indices */
572
676
  @lazy static METHODS: Map<u32, u32> = new Map<u32, u32>();
573
677
 
@@ -584,6 +688,7 @@ export namespace JSON {
584
688
  * stored object's runtime header.
585
689
  */
586
690
  get type(): u16 {
691
+ this.materialize();
587
692
  const w = this.bits;
588
693
  if (!valBoxed(w)) return JSON.Types.F64;
589
694
  const tag = valTag(w);
@@ -598,7 +703,7 @@ export namespace JSON {
598
703
  * Creates an JSON.Value instance with no set value.
599
704
  * @returns An instance of JSON.Value.
600
705
  */
601
- @inline static empty(): JSON.Value {
706
+ static empty(): JSON.Value {
602
707
  const out = changetype<JSON.Value>(
603
708
  __new(offsetof<JSON.Value>(), idof<JSON.Value>()),
604
709
  );
@@ -611,7 +716,7 @@ export namespace JSON {
611
716
  * @param value - The value to be encapsulated.
612
717
  * @returns An instance of JSON.Value.
613
718
  */
614
- @inline static from<T>(value: T): JSON.Value {
719
+ static from<T>(value: T): JSON.Value {
615
720
  if (value instanceof JSON.Value) return value;
616
721
  const out = changetype<JSON.Value>(
617
722
  __new(offsetof<JSON.Value>(), idof<JSON.Value>()),
@@ -620,12 +725,248 @@ export namespace JSON {
620
725
  return out;
621
726
  }
622
727
 
728
+ /**
729
+ * Creates a lazy JSON.Value wrapping the unparsed source slice
730
+ * `[sliceStart, sliceEnd)` (UTF-16 byte pointers into `src`). The slice is
731
+ * parsed into a concrete value on first access (see `materialize`); until
732
+ * then it serializes by passing those bytes through verbatim. `src` anchors
733
+ * the source string so the slice pointers stay valid. Internal - produced by
734
+ * the dynamic deserializers, never called by user code.
735
+ */
736
+ static fromSlice(
737
+ sliceStart: usize,
738
+ sliceEnd: usize,
739
+ src: string,
740
+ ): JSON.Value {
741
+ const ref = new LazyRef();
742
+ ref.lz = ((<u64>sliceStart) << 32) | (<u64>(<u32>sliceEnd));
743
+ ref.src = src;
744
+ const out = changetype<JSON.Value>(
745
+ __new(offsetof<JSON.Value>(), idof<JSON.Value>()),
746
+ );
747
+ out.bits = valBox(JSON.Types.Lazy, <u64>changetype<usize>(ref));
748
+ return out;
749
+ }
750
+
751
+ /**
752
+ * Copy `src`'s NaN-boxed bits into the already-allocated value at `dst`,
753
+ * applying the itcms write barrier for any managed payload (mirrors
754
+ * `__visit`'s tracing rule: tags >= String carry a pointer, as do
755
+ * heap-spilled 64-bit ints). Lets `JSON.parse<JSON.Value>(data, out)` reuse
756
+ * the caller's handle without materializing `src`.
757
+ */
758
+ @unsafe static __adoptInto(dst: usize, src: JSON.Value): JSON.Value {
759
+ const target = changetype<JSON.Value>(dst);
760
+ const bits = src.bits;
761
+ target.bits = bits;
762
+ if (valBoxed(bits)) {
763
+ const tag = valTag(bits);
764
+ if (
765
+ tag >= JSON.Types.String ||
766
+ ((tag == JSON.Types.U64 || tag == JSON.Types.I64) &&
767
+ (bits & VAL_BOX64) != 0)
768
+ ) {
769
+ __link(dst, valPtr(bits), false);
770
+ }
771
+ }
772
+ return target;
773
+ }
774
+
775
+ /**
776
+ * Parses a deferred (lazy) value into a concrete one in place, replacing the
777
+ * boxed slice with the real boxed value (cached for subsequent reads). The
778
+ * deferred shapes are strings, objects and arrays; a string materializes to
779
+ * a `string`, while a composite's own nested deferred children stay lazy -
780
+ * one level is peeled per access. A no-op for already-materialized values.
781
+ * Never allocates during GC (not called from `__visit`).
782
+ */
783
+ private materialize(): void {
784
+ const w = this.bits;
785
+ if (!valLazy(w)) return;
786
+ const ref = changetype<LazyRef>(valPtr(w));
787
+ const lz = ref.lz;
788
+ this.bits = JSON.Value.parseSliceBits(
789
+ <usize>(lz >>> 32),
790
+ <usize>(<u32>lz),
791
+ ref.src,
792
+ );
793
+ }
794
+
795
+ /**
796
+ * Parses the raw slice `[start, end)` (the allocating shapes only: string,
797
+ * object, array) into a concrete value and returns its NaN-boxed bits. A
798
+ * composite's own nested string/composite children stay lazy - one level is
799
+ * peeled. Shared by `JSON.Value.materialize` (standalone lazy values) and
800
+ * `JSON.Obj`'s value-slot materialization.
801
+ */
802
+ static parseSliceBits(start: usize, end: usize, src: string): u64 {
803
+ const first = load<u16>(start);
804
+ if (first == 0x22 /* '"' */) {
805
+ // A string anchors no children, so no source pinning is needed.
806
+ return valBox(
807
+ JSON.Types.String,
808
+ <u64>changetype<usize>(deserializeString(start, end)),
809
+ );
810
+ }
811
+ // Pin the same source for the one level we peel so its children defer too.
812
+ const prev = getParseSrc();
813
+ setParseSrc(src);
814
+ let bits: u64;
815
+ if (first == 0x7b /* '{' */) {
816
+ bits = valBox(
817
+ JSON.Types.Object,
818
+ <u64>changetype<usize>(deserializeObject(start, end, 0)),
819
+ );
820
+ } else {
821
+ bits = valBox(
822
+ JSON.Types.Array,
823
+ <u64>changetype<usize>(deserializeJsonArray(start, end, 0)),
824
+ );
825
+ }
826
+ setParseSrc(prev);
827
+ return bits;
828
+ }
829
+
830
+ /**
831
+ * Internal: the packed slice `(start << 32) | end` if this value is still a
832
+ * deferred slice, else 0. Lets the serializer pass raw bytes through without
833
+ * forcing materialization. `start` is a non-zero pointer, so a real slice is
834
+ * never 0.
835
+ */
836
+ __lazySlice(): u64 {
837
+ const w = this.bits;
838
+ if (!valLazy(w)) return 0;
839
+ return changetype<LazyRef>(valPtr(w)).lz;
840
+ }
841
+
842
+ /**
843
+ * The cached serialize-escape class of a materialized String value: 0 = not
844
+ * yet classified, 1 = clean (no chars need escaping, so it serializes via a
845
+ * single memcpy), 2 = needs escaping. Stored in two spare payload bits so the
846
+ * scan is paid once and reused. Only meaningful for String-tagged values.
847
+ */
848
+ __strClass(): u32 {
849
+ return <u32>((this.bits >> VAL_STR_CLASS_SHIFT) & 3);
850
+ }
851
+ /** Records the serialize-escape class on this String value (see __strClass). */
852
+ __setStrClass(c: u32): void {
853
+ this.bits =
854
+ (this.bits & ~VAL_STR_CLASS_MASK) | ((<u64>c) << VAL_STR_CLASS_SHIFT);
855
+ }
856
+ /**
857
+ * Raw boxed bits. Lets the JSON.Obj/JSON.Arr serializers read back a class
858
+ * the serializer cached on a transient value (see `serializeArbitrary`) and
859
+ * persist it into their flat u64 slot, so re-serializing reuses it.
860
+ */
861
+ __bits(): u64 {
862
+ return this.bits;
863
+ }
864
+
865
+ // --- value-slot helpers (JSON.Obj stores values as flat NaN-boxed u64 ---
866
+ // slots instead of heap JSON.Value objects; these build/inspect/decode the
867
+ // raw bits without allocating, while keeping the box layout encapsulated).
868
+
869
+ /**
870
+ * Bits for a deferred slot. The 45-bit payload has two forms (bit 44 selects):
871
+ *
872
+ * compact (bit 44 = 0): the value's start *offset* and *length*, both in
873
+ * UTF-16 units relative to the source base - `(length << 22) | offset`.
874
+ * Gives the exact end with no scan for any value inside a source up to
875
+ * 2^22 units (8 MB) whose own length is also < 8 MB. This is the common
876
+ * case, and storing a relative offset (vs an absolute pointer) is also
877
+ * GC-relocation-safe.
878
+ *
879
+ * absolute (bit 44 = 1): the absolute start pointer in the low 32 bits;
880
+ * the end is scanned on demand. Fallback for a source or value past the
881
+ * 8 MB field range - rare, and correct (it scans from the value start;
882
+ * scanning a composite cannot safely resume mid-value).
883
+ */
884
+ static lazyBits(srcBase: usize, start: usize, end: usize): u64 {
885
+ const offset = <u64>((start - srcBase) >> 1);
886
+ const length = <u64>((end - start) >> 1);
887
+ let payload: u64;
888
+ if (offset <= LZ_OFF_MASK && length <= LZ_LEN_MASK) {
889
+ payload = (length << LZ_OFF_BITS) | offset;
890
+ } else {
891
+ payload = LZ_ABS_FLAG | ((<u64>start) & VAL_PTR_MASK);
892
+ }
893
+ return valBox(JSON.Types.Lazy, payload);
894
+ }
895
+ /** The value-end pointer of a lazy slot - from the packed length, or scanned. */
896
+ static slotEnd(w: u64, srcBase: usize, srcEnd: usize): usize {
897
+ const p = valPayload(w);
898
+ if (p & LZ_ABS_FLAG) {
899
+ return JSON.Util.scanValueEnd<JSON.Value>(
900
+ <usize>(p & VAL_PTR_MASK),
901
+ srcEnd,
902
+ );
903
+ }
904
+ const start = srcBase + ((<usize>(p & LZ_OFF_MASK)) << 1);
905
+ const length = <usize>((p >> LZ_OFF_BITS) & LZ_LEN_MASK);
906
+ return start + (length << 1);
907
+ }
908
+ /** Bits for a JSON null. */
909
+ static nullBits(): u64 {
910
+ return VAL_NULL;
911
+ }
912
+ /** Bits for a boolean. */
913
+ static boolBits(b: bool): u64 {
914
+ return valBox(JSON.Types.Bool, b ? 1 : 0);
915
+ }
916
+ /** Bits for an f64 (raw IEEE-754, NaN canonicalized off the box signature). */
917
+ static f64Bits(v: f64): u64 {
918
+ return isNaN(v) ? 0x7ff8000000000000 : reinterpret<u64>(v);
919
+ }
920
+ /** Whether a slot is still a deferred (start-pointer) slice. */
921
+ static slotIsLazy(w: u64): bool {
922
+ return valLazy(w);
923
+ }
924
+ /** The start pointer held by a lazy slot (compact offset or absolute). */
925
+ static slotPtr(w: u64, srcBase: usize): usize {
926
+ const p = valPayload(w);
927
+ if (p & LZ_ABS_FLAG) return <usize>(p & VAL_PTR_MASK);
928
+ return srcBase + ((<usize>(p & LZ_OFF_MASK)) << 1);
929
+ }
930
+ /** Wraps raw bits in a JSON.Value (eager scalar / materialized reference). */
931
+ static fromBits(w: u64): JSON.Value {
932
+ const out = changetype<JSON.Value>(
933
+ __new(offsetof<JSON.Value>(), idof<JSON.Value>()),
934
+ );
935
+ out.bits = w;
936
+ return out;
937
+ }
938
+ /** Concrete NaN-boxed bits for a value of type T (materializing if lazy). */
939
+ static bitsFrom<T>(value: T): u64 {
940
+ const v = JSON.Value.from<T>(value);
941
+ v.materialize();
942
+ return v.bits;
943
+ }
944
+ /** Decodes NaN-boxed bits into T (the body of the instance `get<T>`). */
945
+ static decodeBits<T>(w: u64): T {
946
+ if (isFloat<T>()) {
947
+ if (sizeof<T>() == 4) return <T>reinterpret<f32>(<u32>valPayload(w));
948
+ return <T>reinterpret<f64>(w);
949
+ } else if (isInteger<T>()) {
950
+ if (sizeof<T>() == 8) {
951
+ if (w & VAL_BOX64) return load<T>(valPtr(w));
952
+ if (isSigned<T>()) return <T>((<i64>(valPayload(w) << 19)) >> 19);
953
+ return <T>valPayload(w);
954
+ }
955
+ return <T>valPayload(w);
956
+ } else if (isBoolean<T>()) {
957
+ return <T>valPayload(w);
958
+ } else if (isReference<T>()) {
959
+ return changetype<T>(valPtr(w));
960
+ }
961
+ return unreachable();
962
+ }
963
+
623
964
  /**
624
965
  * Gets the type of a given value as a JSON.Types enum.
625
966
  * @param value - any
626
967
  * @returns JSON.Types
627
968
  */
628
- @inline getType<T>(value: T): JSON.Types {
969
+ getType<T>(value: T): JSON.Types {
629
970
  if (isNullable<T>() && changetype<usize>(value) === 0)
630
971
  return JSON.Types.Null;
631
972
  if (isBoolean<T>()) return JSON.Types.Bool;
@@ -672,13 +1013,14 @@ export namespace JSON {
672
1013
  if (value instanceof Map) return JSON.Types.Map;
673
1014
  if (value instanceof JSON.Raw) return JSON.Types.Raw;
674
1015
  if (value instanceof JSON.Obj) return JSON.Types.Object;
1016
+ if (value instanceof JSON.Arr) return JSON.Types.Array;
675
1017
  return JSON.Types.Null;
676
1018
  }
677
1019
  /**
678
1020
  * Sets the value of the JSON.Value instance.
679
1021
  * @param value - The value to be set.
680
1022
  */
681
- @inline set<T>(value: T): void {
1023
+ set<T>(value: T): void {
682
1024
  if (value instanceof JSON.Box) {
683
1025
  this.set(value.value);
684
1026
  } else if (isBoolean<T>()) {
@@ -739,9 +1081,16 @@ export namespace JSON {
739
1081
  this.bits = valBox(JSON.Types.Map, <u64>changetype<usize>(value));
740
1082
  } else if (value instanceof JSON.Obj) {
741
1083
  this.bits = valBox(JSON.Types.Object, <u64>changetype<usize>(value));
1084
+ } else if (value instanceof JSON.Arr) {
1085
+ this.bits = valBox(JSON.Types.Array, <u64>changetype<usize>(value));
742
1086
  // @ts-expect-error
743
1087
  } else if (isArray<T>() && idof<valueof<T>>() == idof<JSON.Value>()) {
744
- this.bits = valBox(JSON.Types.Array, <u64>changetype<usize>(value));
1088
+ // A JSON.Value[] is converted to the buffer-backed JSON.Arr form, so
1089
+ // the Array tag always boxes a JSON.Arr.
1090
+ this.bits = valBox(
1091
+ JSON.Types.Array,
1092
+ <u64>changetype<usize>(JSON.Arr.from<T>(value)),
1093
+ );
745
1094
  }
746
1095
  }
747
1096
 
@@ -774,24 +1123,9 @@ export namespace JSON {
774
1123
  * Gets the value of the JSON.Value instance.
775
1124
  * @returns The encapsulated value.
776
1125
  */
777
- @inline get<T>(): T {
778
- const w = this.bits;
779
- if (isFloat<T>()) {
780
- if (sizeof<T>() == 4) return <T>reinterpret<f32>(<u32>valPayload(w));
781
- return <T>reinterpret<f64>(w);
782
- } else if (isInteger<T>()) {
783
- if (sizeof<T>() == 8) {
784
- if (w & VAL_BOX64) return load<T>(valPtr(w));
785
- if (isSigned<T>()) return <T>((<i64>(valPayload(w) << 19)) >> 19);
786
- return <T>valPayload(w);
787
- }
788
- return <T>valPayload(w);
789
- } else if (isBoolean<T>()) {
790
- return <T>valPayload(w);
791
- } else if (isReference<T>()) {
792
- return changetype<T>(valPtr(w));
793
- }
794
- return unreachable();
1126
+ get<T>(): T {
1127
+ this.materialize();
1128
+ return JSON.Value.decodeBits<T>(this.bits);
795
1129
  }
796
1130
 
797
1131
  /**
@@ -799,7 +1133,7 @@ export namespace JSON {
799
1133
  * Alias for .get<T>()
800
1134
  * @returns The encapsulated value.
801
1135
  */
802
- @inline as<T>(): T {
1136
+ as<T>(): T {
803
1137
  return this.get<T>();
804
1138
  }
805
1139
 
@@ -808,7 +1142,8 @@ export namespace JSON {
808
1142
  * Alias for .get<T>()
809
1143
  * @returns The encapsulated value.
810
1144
  */
811
- @inline asBox<T>(): Box<T> | null {
1145
+ asBox<T>(): Box<T> | null {
1146
+ this.materialize();
812
1147
  if (this.type === JSON.Types.Null) return null;
813
1148
  return changetype<Box<T>>(JSON.Box.fromValue<T>(this));
814
1149
  }
@@ -818,6 +1153,7 @@ export namespace JSON {
818
1153
  * @returns The string representation of the JSON.Value.
819
1154
  */
820
1155
  toString(): string {
1156
+ this.materialize();
821
1157
  switch (this.type) {
822
1158
  case JSON.Types.Null:
823
1159
  return "null";
@@ -849,19 +1185,7 @@ export namespace JSON {
849
1185
  return this.get<JSON.Raw>().toString();
850
1186
  }
851
1187
  case JSON.Types.Array: {
852
- const arr = this.get<JSON.Value[]>();
853
- if (!arr.length) return "[]";
854
- let out = "[";
855
- const end = arr.length - 1;
856
- for (let i = 0; i < end; i++) {
857
- const element = unchecked(arr[i]);
858
- out += element.toString() + ",";
859
- }
860
-
861
- const element = unchecked(arr[end]);
862
- out += element.toString() + "]";
863
-
864
- return out.toString();
1188
+ return JSON.stringify(this.get<JSON.Arr>());
865
1189
  }
866
1190
  case JSON.Types.TypedArray:
867
1191
  case JSON.Types.ArrayBuffer: {
@@ -885,6 +1209,14 @@ export namespace JSON {
885
1209
  const w = this.bits;
886
1210
  if (!valBoxed(w)) return; // raw f64 holds no reference
887
1211
  const tag = valTag(w);
1212
+ // A deferred value carries a LazyRef pointer; tracing it keeps the LazyRef
1213
+ // (and, transitively, its `src` anchor) alive. Must precede the
1214
+ // `tag >= String` branch since Lazy(20) would otherwise fall into it.
1215
+ // Trace-only - never materialize here (no allocation during GC).
1216
+ if (tag == JSON.Types.Lazy) {
1217
+ __visit(valPtr(w), cookie);
1218
+ return;
1219
+ }
888
1220
  // String(13)..ArrayBuffer(19) and Struct all carry a managed pointer;
889
1221
  // Raw(1) is intentionally not traced (matches prior behavior).
890
1222
  if (tag >= JSON.Types.String) {
@@ -917,25 +1249,24 @@ export namespace JSON {
917
1249
  * console.log(JSON.stringify(obj)); // {"key":"value","count":42}
918
1250
  * ```
919
1251
  */
920
- export class Obj {
921
- // Keys are packed into one growable buffer, each prefixed by a u16 length
922
- // (UTF-16 code units), instead of allocating a heap string per key. Values
923
- // are a parallel array. A key -> position index is built lazily on the
924
- // first keyed access (never during parsing). Per-object allocation count
925
- // matches the previous Map-based storage, while deserialization avoids the
926
- // per-key string allocation and hashing entirely.
1252
+ @final export class Obj {
927
1253
  _kbuf: StaticArray<u16> = EMPTY_KEYS;
928
1254
  _kused: i32 = 0;
929
- _vals: JSON.Value[] = [];
930
- private _index: Map<string, i32> | null = null;
1255
+ _kpos: StaticArray<i32> = EMPTY_I32S;
1256
+ _vals: StaticArray<u64> = EMPTY_VALS;
1257
+ _vused: i32 = 0;
1258
+ /** Source string the lazy slot pointers index into; anchors it for GC. */
1259
+ _src: string = "";
1260
+ private _index: StaticArray<i32> | null = null;
1261
+ private _indexMask: i32 = 0;
931
1262
 
932
1263
  constructor() {}
933
1264
 
934
1265
  /**
935
1266
  * Gets the number of key-value pairs in the object.
936
1267
  */
937
- @inline get size(): i32 {
938
- return this._vals.length;
1268
+ get size(): i32 {
1269
+ return this._vused;
939
1270
  }
940
1271
 
941
1272
  /** Grows the key buffer to hold at least `need` code units. */
@@ -954,11 +1285,28 @@ export namespace JSON {
954
1285
  this._kbuf = nb;
955
1286
  }
956
1287
 
1288
+ /** Grows the key-position buffer to hold at least `need` entries. */
1289
+ private ensureKeyPosCap(need: i32): void {
1290
+ const cap = this._kpos.length;
1291
+ if (cap >= need) return;
1292
+ let n = cap ? cap : 8;
1293
+ while (n < need) n <<= 1;
1294
+ const nb = new StaticArray<i32>(n);
1295
+ if (this._vused)
1296
+ memory.copy(
1297
+ changetype<usize>(nb),
1298
+ changetype<usize>(this._kpos),
1299
+ (<usize>this._vused) << 2,
1300
+ );
1301
+ this._kpos = nb;
1302
+ }
1303
+
957
1304
  /** Appends a length-prefixed key (from a source memory range). */
958
- private pushKeyBytes(keyStart: usize, keyEnd: usize): void {
1305
+ private pushKeyBytes(keyStart: usize, keyEnd: usize, slotIndex: i32): void {
959
1306
  const len = <i32>((keyEnd - keyStart) >> 1);
960
1307
  const pos = this._kused;
961
1308
  this.ensureKeyCap(pos + 1 + len);
1309
+ this.ensureKeyPosCap(slotIndex + 1);
962
1310
  const buf = changetype<usize>(this._kbuf);
963
1311
  store<u16>(buf + ((<usize>pos) << 1), <u16>len);
964
1312
  if (len)
@@ -967,6 +1315,7 @@ export namespace JSON {
967
1315
  keyStart,
968
1316
  (<usize>len) << 1,
969
1317
  );
1318
+ unchecked((this._kpos[slotIndex] = pos));
970
1319
  this._kused = pos + 1 + len;
971
1320
  }
972
1321
 
@@ -982,38 +1331,215 @@ export namespace JSON {
982
1331
  return out;
983
1332
  }
984
1333
 
1334
+ /** Grows the value-slot buffer to hold at least `need` slots. */
1335
+ private ensureValCap(need: i32): void {
1336
+ const cap = this._vals.length;
1337
+ if (cap >= need) return;
1338
+ let n = cap ? cap : 8;
1339
+ while (n < need) n <<= 1;
1340
+ const nb = new StaticArray<u64>(n);
1341
+ if (this._vused)
1342
+ memory.copy(
1343
+ changetype<usize>(nb),
1344
+ changetype<usize>(this._vals),
1345
+ (<usize>this._vused) << 3,
1346
+ );
1347
+ this._vals = nb;
1348
+ }
1349
+
1350
+ /** Writes a slot and, if it carries a managed pointer, links it for the GC. */
1351
+ private storeSlot(i: i32, bits: u64): void {
1352
+ unchecked((this._vals[i] = bits));
1353
+ if (valBoxed(bits)) {
1354
+ const tag = valTag(bits);
1355
+ // Lazy slots hold an interior `_src` pointer (anchored by the _src
1356
+ // field), not an owned object - never link those.
1357
+ if (
1358
+ tag != JSON.Types.Lazy &&
1359
+ (tag >= JSON.Types.String ||
1360
+ ((tag == JSON.Types.U64 || tag == JSON.Types.I64) &&
1361
+ bits & VAL_BOX64))
1362
+ ) {
1363
+ __link(changetype<usize>(this), valPtr(bits), false);
1364
+ }
1365
+ }
1366
+ }
1367
+
1368
+ /** Appends a value slot (raw NaN-boxed bits). */
1369
+ private pushValSlot(bits: u64): void {
1370
+ const pos = this._vused;
1371
+ this.ensureValCap(pos + 1);
1372
+ this._vused = pos + 1;
1373
+ this.storeSlot(pos, bits);
1374
+ }
1375
+
1376
+ /** End pointer of the source buffer (upper bound for scanning a lazy slot). */
1377
+ private srcEnd(): usize {
1378
+ return changetype<usize>(this._src) + ((<usize>this._src.length) << 1);
1379
+ }
1380
+
1381
+ /** Compares a lookup key against the stored key bytes for slot `i`. */
1382
+ private keyEquals(i: i32, key: string): bool {
1383
+ const pos = unchecked(this._kpos[i]);
1384
+ const buf = changetype<usize>(this._kbuf) + ((<usize>pos) << 1);
1385
+ const len = <i32>load<u16>(buf);
1386
+ if (len != <i32>key.length) return false;
1387
+ return utf16Equals(changetype<usize>(key), buf + 2, len);
1388
+ }
1389
+
1390
+ /** Compares two stored key slots without materializing strings. */
1391
+ private slotEqualsSlot(a: i32, b: i32): bool {
1392
+ const posa = unchecked(this._kpos[a]);
1393
+ const bufA = changetype<usize>(this._kbuf) + ((<usize>posa) << 1);
1394
+ const lenA = <i32>load<u16>(bufA);
1395
+ const posb = unchecked(this._kpos[b]);
1396
+ const bufB = changetype<usize>(this._kbuf) + ((<usize>posb) << 1);
1397
+ const lenB = <i32>load<u16>(bufB);
1398
+ if (lenA != lenB) return false;
1399
+ return utf16Equals(bufA + 2, bufB + 2, lenA);
1400
+ }
1401
+
1402
+ /** Hashes the stored key for slot `i`. */
1403
+ private keyHashAt(i: i32): u32 {
1404
+ const pos = unchecked(this._kpos[i]);
1405
+ const buf = changetype<usize>(this._kbuf) + ((<usize>pos) << 1);
1406
+ const len = <i32>load<u16>(buf);
1407
+ return hashUtf16(buf + 2, len);
1408
+ }
1409
+
1410
+ /** Resolves a key to its slot index, or -1 if absent. */
1411
+ private indexOf(key: string): i32 {
1412
+ const n = this._vused;
1413
+ const keyLen = <i32>key.length;
1414
+ // Small objects: scan linearly and never build a hash index. The keys are
1415
+ // packed contiguously in _kbuf and the length prefix rejects mismatches
1416
+ // before any byte compare. Scan from the end so a duplicate key resolves
1417
+ // to its LAST occurrence - JSON last-value-wins, matching buildIndex()
1418
+ // (which overwrites the slot on a collision) for objects above the
1419
+ // threshold; a forward scan would return the first and disagree.
1420
+ if (n <= OBJ_LINEAR_MAX) {
1421
+ const kbuf = changetype<usize>(this._kbuf);
1422
+ const keyPtr = changetype<usize>(key);
1423
+ const kpos = this._kpos;
1424
+ for (let i = n - 1; i >= 0; i--) {
1425
+ const buf = kbuf + ((<usize>unchecked(kpos[i])) << 1);
1426
+ if (
1427
+ <i32>load<u16>(buf) == keyLen &&
1428
+ utf16Equals(keyPtr, buf + 2, keyLen)
1429
+ )
1430
+ return i;
1431
+ }
1432
+ return -1;
1433
+ }
1434
+ const idx = this.buildIndex();
1435
+ const mask = this._indexMask;
1436
+ if (mask == 0) {
1437
+ const entry = unchecked(idx[0]);
1438
+ return entry != 0 && this.keyEquals(entry - 1, key) ? entry - 1 : -1;
1439
+ }
1440
+ let slot = <i32>(
1441
+ (hashUtf16(changetype<usize>(key), <i32>key.length) & (<u32>mask))
1442
+ );
1443
+ const start = slot;
1444
+ while (true) {
1445
+ const entry = unchecked(idx[slot]);
1446
+ if (entry == 0) return -1;
1447
+ const i = entry - 1;
1448
+ if (this.keyEquals(i, key)) return i;
1449
+ slot = (slot + 1) & mask;
1450
+ // A correctly-maintained table stays below full load, so an empty slot
1451
+ // is found first. This wrap check is a safety net against spinning
1452
+ // forever should that invariant ever be violated.
1453
+ if (slot == start) return -1;
1454
+ }
1455
+ }
1456
+
1457
+ /** Parses a lazy slot in place, caching the concrete box, and returns it. */
1458
+ private materializeSlot(i: i32): u64 {
1459
+ const slot = unchecked(this._vals[i]);
1460
+ if (!JSON.Value.slotIsLazy(slot)) return slot;
1461
+ const base = changetype<usize>(this._src);
1462
+ const start = JSON.Value.slotPtr(slot, base);
1463
+ const end = JSON.Value.slotEnd(slot, base, this.srcEnd());
1464
+ const bits = JSON.Value.parseSliceBits(start, end, this._src);
1465
+ this.storeSlot(i, bits);
1466
+ return bits;
1467
+ }
1468
+
985
1469
  /**
986
- * Appends a key (from a source memory range) and value without a
987
- * duplicate-key check. Used by the deserializer no per-key string
988
- * allocation, no hashing.
1470
+ * Appends a key (from a source memory range) and a precomputed NaN-boxed
1471
+ * value slot without a duplicate-key check. Used by the deserializer - no
1472
+ * per-key string allocation, no per-value object, no hashing.
1473
+ */
1474
+ appendRawSlot(keyStart: usize, keyEnd: usize, bits: u64): void {
1475
+ const slotIndex = this._vused;
1476
+ this.pushKeyBytes(keyStart, keyEnd, slotIndex);
1477
+ this.pushValSlot(bits);
1478
+ this.insertIndex(slotIndex);
1479
+ }
1480
+
1481
+ /**
1482
+ * Appends a key and value (any T) without a duplicate-key check. Eagerly
1483
+ * boxes the value into a slot.
989
1484
  */
990
1485
  appendRaw<T>(keyStart: usize, keyEnd: usize, value: T): void {
991
- this.pushKeyBytes(keyStart, keyEnd);
992
- this._vals.push(JSON.Value.from<T>(value));
1486
+ this.appendRawSlot(keyStart, keyEnd, JSON.Value.bitsFrom<T>(value));
1487
+ }
1488
+
1489
+ /** Inserts a single slot into an already-built index. */
1490
+ private insertIndex(slotIndex: i32): void {
993
1491
  const idx = this._index;
994
- if (idx !== null) {
995
- const len = <i32>((keyEnd - keyStart) >> 1);
996
- const k = changetype<string>(__new((<usize>len) << 1, idof<string>()));
997
- if (len) memory.copy(changetype<usize>(k), keyStart, (<usize>len) << 1);
998
- idx.set(k, this._vals.length - 1);
1492
+ if (idx === null) return;
1493
+ const mask = this._indexMask;
1494
+ let slot = <i32>(this.keyHashAt(slotIndex) & (<u32>mask));
1495
+ const start = slot;
1496
+ while (unchecked(idx[slot]) != 0) {
1497
+ const entry = unchecked(idx[slot]) - 1;
1498
+ if (this.slotEqualsSlot(entry, slotIndex)) {
1499
+ unchecked((idx[slot] = slotIndex + 1));
1500
+ return;
1501
+ }
1502
+ slot = (slot + 1) & mask;
1503
+ if (slot == start) {
1504
+ this._index = null;
1505
+ return;
1506
+ }
999
1507
  }
1508
+ unchecked((idx[slot] = slotIndex + 1));
1509
+ // buildIndex() sizes the table at >2x the entry count (load < 0.5), but it
1510
+ // only runs lazily; appends since then go through here without resizing.
1511
+ // Once we cross half load, drop the index so the next access rebuilds it at
1512
+ // double capacity — this keeps an empty slot available for every probe.
1513
+ // Without it a small (e.g. cap-2) table fills and indexOf() spins forever.
1514
+ if ((slotIndex + 1) << 1 > mask + 1) this._index = null;
1000
1515
  }
1001
1516
 
1002
1517
  /** Builds (once) and returns the lazy key -> position index. */
1003
- private buildIndex(): Map<string, i32> {
1518
+ private buildIndex(): StaticArray<i32> {
1004
1519
  let idx = this._index;
1005
1520
  if (idx === null) {
1006
- idx = new Map<string, i32>();
1007
- const buf = changetype<usize>(this._kbuf);
1008
- const used = this._kused;
1009
- let pos = 0;
1010
- let i = 0;
1011
- while (pos < used) {
1012
- const len = <i32>load<u16>(buf + ((<usize>pos) << 1));
1013
- idx.set(this.makeKey(pos + 1, len), i++);
1014
- pos += 1 + len;
1521
+ const used = this._vused;
1522
+ let cap = 2;
1523
+ while (cap <= used << 1) cap <<= 1;
1524
+ idx = new StaticArray<i32>(cap);
1525
+ const mask = cap - 1;
1526
+ for (let i = 0; i < used; i++) {
1527
+ const keyPos = unchecked(this._kpos[i]);
1528
+ const buf = changetype<usize>(this._kbuf) + ((<usize>keyPos) << 1);
1529
+ const len = <i32>load<u16>(buf);
1530
+ let slot = <i32>(hashUtf16(buf + 2, len) & (<u32>mask));
1531
+ while (unchecked(idx[slot]) != 0) {
1532
+ const entry = unchecked(idx[slot]) - 1;
1533
+ if (this.slotEqualsSlot(entry, i)) {
1534
+ unchecked((idx[slot] = i + 1));
1535
+ break;
1536
+ }
1537
+ slot = (slot + 1) & mask;
1538
+ }
1539
+ if (unchecked(idx[slot]) == 0) unchecked((idx[slot] = i + 1));
1015
1540
  }
1016
1541
  this._index = idx;
1542
+ this._indexMask = mask;
1017
1543
  }
1018
1544
  return idx;
1019
1545
  }
@@ -1024,27 +1550,57 @@ export namespace JSON {
1024
1550
  * @param value - The value (will be wrapped in JSON.Value)
1025
1551
  */
1026
1552
  set<T>(key: string, value: T): void {
1027
- const idx = this.buildIndex();
1028
- if (idx.has(key)) {
1029
- unchecked((this._vals[idx.get(key)] = JSON.Value.from<T>(value)));
1553
+ const bits = JSON.Value.bitsFrom<T>(value);
1554
+ const i = this.indexOf(key);
1555
+ if (i >= 0) {
1556
+ this.storeSlot(i, bits);
1030
1557
  } else {
1558
+ const slotIndex = this._vused;
1031
1559
  this.pushKeyBytes(
1032
1560
  changetype<usize>(key),
1033
1561
  changetype<usize>(key) + ((<usize>key.length) << 1),
1562
+ slotIndex,
1034
1563
  );
1035
- this._vals.push(JSON.Value.from<T>(value));
1036
- idx.set(key, this._vals.length - 1);
1564
+ this.pushValSlot(bits);
1565
+ this.insertIndex(slotIndex);
1037
1566
  }
1038
1567
  }
1039
1568
 
1040
1569
  /**
1041
- * Gets a value by key.
1570
+ * Gets a value by key as a JSON.Value (dynamic access).
1042
1571
  * @param key - The key to look up
1043
1572
  * @returns The JSON.Value or null if not found
1044
1573
  */
1045
- @inline get(key: string): JSON.Value | null {
1046
- const idx = this.buildIndex();
1047
- return idx.has(key) ? unchecked(this._vals[idx.get(key)]) : null;
1574
+ get(key: string): JSON.Value | null {
1575
+ const i = this.indexOf(key);
1576
+ if (i < 0) return null;
1577
+ const slot = unchecked(this._vals[i]);
1578
+ if (JSON.Value.slotIsLazy(slot)) {
1579
+ // Hand back a self-contained lazy value (its own slice + anchor) so it
1580
+ // can materialize independently of this object.
1581
+ const base = changetype<usize>(this._src);
1582
+ const start = JSON.Value.slotPtr(slot, base);
1583
+ const end = JSON.Value.slotEnd(slot, base, this.srcEnd());
1584
+ return JSON.Value.fromSlice(start, end, this._src);
1585
+ }
1586
+ return JSON.Value.fromBits(slot);
1587
+ }
1588
+
1589
+ /**
1590
+ * Gets a value by key directly as `T`, with no intermediate JSON.Value
1591
+ * allocation. A deferred slot is parsed (and cached) on first access; an
1592
+ * absent key returns the type's default (null / 0 / false).
1593
+ *
1594
+ * Named `getAs` rather than `get<T>` because AssemblyScript has no method
1595
+ * overloading - `get(key)` (dynamic) and a typed `get` can't share a name.
1596
+ * @param key - The key to look up
1597
+ */
1598
+ getAs<T>(key: string): T {
1599
+ const i = this.indexOf(key);
1600
+ if (i < 0) return __zero<T>();
1601
+ let slot = unchecked(this._vals[i]);
1602
+ if (JSON.Value.slotIsLazy(slot)) slot = this.materializeSlot(i);
1603
+ return JSON.Value.decodeBits<T>(slot);
1048
1604
  }
1049
1605
 
1050
1606
  /**
@@ -1052,8 +1608,8 @@ export namespace JSON {
1052
1608
  * @param key - The key to check
1053
1609
  * @returns true if the key exists
1054
1610
  */
1055
- @inline has(key: string): bool {
1056
- return this.buildIndex().has(key);
1611
+ has(key: string): bool {
1612
+ return this.indexOf(key) >= 0;
1057
1613
  }
1058
1614
 
1059
1615
  /**
@@ -1062,34 +1618,53 @@ export namespace JSON {
1062
1618
  * @returns true if the key was found and deleted
1063
1619
  */
1064
1620
  delete(key: string): bool {
1065
- const idx = this.buildIndex();
1066
- if (!idx.has(key)) return false;
1067
- const removed = idx.get(key);
1621
+ const removed = this.indexOf(key);
1622
+ if (removed < 0) return false;
1068
1623
  const keys = this.keys();
1069
- const vals = this._vals;
1624
+ const oldVals = this._vals;
1625
+ const n = this._vused;
1070
1626
  this._kbuf = EMPTY_KEYS;
1071
1627
  this._kused = 0;
1072
- const newVals = new Array<JSON.Value>();
1073
- for (let j = 0; j < keys.length; j++) {
1628
+ this._kpos = EMPTY_I32S;
1629
+ this._vals = EMPTY_VALS;
1630
+ this._vused = 0;
1631
+ for (let j = 0; j < n; j++) {
1074
1632
  if (j == removed) continue;
1075
1633
  const k = unchecked(keys[j]);
1076
1634
  this.pushKeyBytes(
1077
1635
  changetype<usize>(k),
1078
1636
  changetype<usize>(k) + ((<usize>k.length) << 1),
1637
+ this._vused,
1079
1638
  );
1080
- newVals.push(unchecked(vals[j]));
1639
+ this.pushValSlot(unchecked(oldVals[j]));
1081
1640
  }
1082
- this._vals = newVals;
1083
1641
  this._index = null;
1642
+ this._indexMask = 0;
1084
1643
  return true;
1085
1644
  }
1086
1645
 
1646
+ /**
1647
+ * Removes all entries. Backing key/value buffer capacity is kept so a
1648
+ * subsequent parse or insert reuses it without re-allocating - this is what
1649
+ * makes `JSON.parse<JSON.Obj>(data, out)` allocation-light. `__visit` only
1650
+ * traces `[0, _vused)`, so resetting the used counters drops the old slots
1651
+ * from GC tracing safely.
1652
+ */
1653
+ clear(): void {
1654
+ this._kused = 0;
1655
+ this._kpos = EMPTY_I32S;
1656
+ this._vused = 0;
1657
+ this._src = "";
1658
+ this._index = null;
1659
+ this._indexMask = 0;
1660
+ }
1661
+
1087
1662
  /**
1088
1663
  * Gets all keys in the object.
1089
1664
  * @returns Array of string keys (in insertion order)
1090
1665
  */
1091
1666
  keys(): string[] {
1092
- const out = new Array<string>(this._vals.length);
1667
+ const out = new Array<string>(this._vused);
1093
1668
  const buf = changetype<usize>(this._kbuf);
1094
1669
  const used = this._kused;
1095
1670
  let pos = 0;
@@ -1106,15 +1681,29 @@ export namespace JSON {
1106
1681
  * Gets all values in the object.
1107
1682
  * @returns Array of JSON.Value instances (in insertion order)
1108
1683
  */
1109
- @inline values(): JSON.Value[] {
1110
- return this._vals.slice();
1684
+ values(): JSON.Value[] {
1685
+ const n = this._vused;
1686
+ const out = new Array<JSON.Value>(n);
1687
+ const base = changetype<usize>(this._src);
1688
+ const srcEnd = this.srcEnd();
1689
+ for (let i = 0; i < n; i++) {
1690
+ const slot = unchecked(this._vals[i]);
1691
+ if (JSON.Value.slotIsLazy(slot)) {
1692
+ const start = JSON.Value.slotPtr(slot, base);
1693
+ const end = JSON.Value.slotEnd(slot, base, srcEnd);
1694
+ unchecked((out[i] = JSON.Value.fromSlice(start, end, this._src)));
1695
+ } else {
1696
+ unchecked((out[i] = JSON.Value.fromBits(slot)));
1697
+ }
1698
+ }
1699
+ return out;
1111
1700
  }
1112
1701
 
1113
1702
  /**
1114
1703
  * Serializes the object to a JSON string.
1115
1704
  * @returns JSON string representation
1116
1705
  */
1117
- @inline toString(): string {
1706
+ toString(): string {
1118
1707
  return JSON.stringify(this);
1119
1708
  }
1120
1709
 
@@ -1147,6 +1736,537 @@ export namespace JSON {
1147
1736
  }
1148
1737
  return parsed.get<JSON.Obj>();
1149
1738
  }
1739
+
1740
+
1741
+ @unsafe private __visit(cookie: u32): void {
1742
+ __visit(changetype<usize>(this._kbuf), cookie);
1743
+ __visit(changetype<usize>(this._kpos), cookie);
1744
+ __visit(changetype<usize>(this._vals), cookie);
1745
+ __visit(changetype<usize>(this._src), cookie);
1746
+ __visit(changetype<usize>(this._index), cookie); // null-safe in rt
1747
+ const vals = this._vals;
1748
+ const n = this._vused;
1749
+ for (let i = 0; i < n; i++) {
1750
+ const w = unchecked(vals[i]);
1751
+ if (!valBoxed(w)) continue; // raw f64, no reference
1752
+ const tag = valTag(w);
1753
+ if (tag == JSON.Types.Lazy) continue; // interior _src pointer
1754
+ if (tag >= JSON.Types.String) {
1755
+ __visit(valPtr(w), cookie);
1756
+ } else if (
1757
+ (tag == JSON.Types.U64 || tag == JSON.Types.I64) &&
1758
+ w & VAL_BOX64
1759
+ ) {
1760
+ __visit(valPtr(w), cookie); // heap-spilled 64-bit int
1761
+ }
1762
+ }
1763
+ }
1764
+ }
1765
+
1766
+ /**
1767
+ * Dynamic JSON array with JSON.Value elements, backed by the same flat
1768
+ * NaN-boxed u64 slot buffer as JSON.Obj (no per-element JSON.Value objects).
1769
+ * Deferred string/array/object elements are parsed on first access; untouched
1770
+ * elements serialize straight from their source bytes.
1771
+ *
1772
+ * Index with `arr.at(i)` (returns a JSON.Value) or read typed via `getAs<T>(i)`.
1773
+ *
1774
+ * @example
1775
+ * ```typescript
1776
+ * const arr = JSON.parse<JSON.Arr>('[1,"two",[3,4]]');
1777
+ * arr.at(0).get<f64>(); // 1
1778
+ * arr.getAs<string>(1); // "two"
1779
+ * arr.getAs<JSON.Arr>(2).at(0); // 3
1780
+ * ```
1781
+ */
1782
+ @final export class Arr {
1783
+ // Same flat slot model as JSON.Obj, without the key buffer. See JSON.Obj for
1784
+ // the slot encoding, the custom `__visit`, and the `__link` write barrier.
1785
+ _vals: StaticArray<u64> = EMPTY_VALS;
1786
+ _vused: i32 = 0;
1787
+ /** Source string the lazy slot pointers index into; anchors it for GC. */
1788
+ _src: string = "";
1789
+
1790
+ constructor() {}
1791
+
1792
+ /** Number of elements. */
1793
+ get length(): i32 {
1794
+ return this._vused;
1795
+ }
1796
+
1797
+ /**
1798
+ * Removes all elements, keeping the value-slot buffer capacity so a
1799
+ * subsequent parse or push reuses it - powers `JSON.parse<JSON.Arr>(data,
1800
+ * out)`. `__visit` only traces `[0, _vused)`, so this is GC-safe.
1801
+ */
1802
+ clear(): void {
1803
+ this._vused = 0;
1804
+ this._src = "";
1805
+ }
1806
+
1807
+ /** Grows the value-slot buffer to hold at least `need` slots. */
1808
+ private ensureValCap(need: i32): void {
1809
+ const cap = this._vals.length;
1810
+ if (cap >= need) return;
1811
+ let n = cap ? cap : 8;
1812
+ while (n < need) n <<= 1;
1813
+ const nb = new StaticArray<u64>(n);
1814
+ if (this._vused)
1815
+ memory.copy(
1816
+ changetype<usize>(nb),
1817
+ changetype<usize>(this._vals),
1818
+ (<usize>this._vused) << 3,
1819
+ );
1820
+ this._vals = nb;
1821
+ }
1822
+
1823
+ /** Writes a slot and, if it carries a managed pointer, links it for the GC. */
1824
+ private storeSlot(i: i32, bits: u64): void {
1825
+ unchecked((this._vals[i] = bits));
1826
+ if (valBoxed(bits)) {
1827
+ const tag = valTag(bits);
1828
+ if (
1829
+ tag != JSON.Types.Lazy &&
1830
+ (tag >= JSON.Types.String ||
1831
+ ((tag == JSON.Types.U64 || tag == JSON.Types.I64) &&
1832
+ bits & VAL_BOX64))
1833
+ ) {
1834
+ __link(changetype<usize>(this), valPtr(bits), false);
1835
+ }
1836
+ }
1837
+ }
1838
+
1839
+ /** Appends a value slot (raw NaN-boxed bits). Deserializer entry point. */
1840
+ pushRawSlot(bits: u64): void {
1841
+ const pos = this._vused;
1842
+ this.ensureValCap(pos + 1);
1843
+ this._vused = pos + 1;
1844
+ this.storeSlot(pos, bits);
1845
+ }
1846
+
1847
+ /** End pointer of the source buffer (upper bound for scanning a lazy slot). */
1848
+ private srcEnd(): usize {
1849
+ return changetype<usize>(this._src) + ((<usize>this._src.length) << 1);
1850
+ }
1851
+
1852
+ /** Parses a lazy slot in place, caching the concrete box, and returns it. */
1853
+ private materializeSlot(i: i32): u64 {
1854
+ const slot = unchecked(this._vals[i]);
1855
+ if (!JSON.Value.slotIsLazy(slot)) return slot;
1856
+ const base = changetype<usize>(this._src);
1857
+ const start = JSON.Value.slotPtr(slot, base);
1858
+ const end = JSON.Value.slotEnd(slot, base, this.srcEnd());
1859
+ const bits = JSON.Value.parseSliceBits(start, end, this._src);
1860
+ this.storeSlot(i, bits);
1861
+ return bits;
1862
+ }
1863
+
1864
+ /** Element access as a JSON.Value: `arr.at(i)`. */
1865
+ at(index: i32): JSON.Value {
1866
+ if (<u32>index >= <u32>this._vused) throw new Error("Index out of range");
1867
+ const slot = unchecked(this._vals[index]);
1868
+ if (JSON.Value.slotIsLazy(slot)) {
1869
+ const base = changetype<usize>(this._src);
1870
+ const start = JSON.Value.slotPtr(slot, base);
1871
+ const end = JSON.Value.slotEnd(slot, base, this.srcEnd());
1872
+ return JSON.Value.fromSlice(start, end, this._src);
1873
+ }
1874
+ return JSON.Value.fromBits(slot);
1875
+ }
1876
+
1877
+ /**
1878
+ * Reads element `i` directly as `T`, with no intermediate JSON.Value
1879
+ * allocation. A deferred slot is parsed (and cached) on first access.
1880
+ * @param i - element index
1881
+ */
1882
+ getAs<T>(i: i32): T {
1883
+ let slot = unchecked(this._vals[i]);
1884
+ if (JSON.Value.slotIsLazy(slot)) slot = this.materializeSlot(i);
1885
+ return JSON.Value.decodeBits<T>(slot);
1886
+ }
1887
+
1888
+ /** Appends a value (any T), eagerly boxed into a slot. */
1889
+ push<T>(value: T): void {
1890
+ this.pushRawSlot(JSON.Value.bitsFrom<T>(value));
1891
+ }
1892
+
1893
+ /** Overwrites element `i`. */
1894
+ set<T>(i: i32, value: T): void {
1895
+ this.storeSlot(i, JSON.Value.bitsFrom<T>(value));
1896
+ }
1897
+
1898
+ /**
1899
+ * Bounds-checked element access via `arr[i]` - returns a `JSON.Value`
1900
+ * (allocating), mirroring `at(i)`. For an allocation-free typed read use
1901
+ * `getAs<T>(i)`.
1902
+ */
1903
+ @operator("[]") private __get(index: i32): JSON.Value {
1904
+ return this.at(index);
1905
+ }
1906
+
1907
+ /** Element assignment via `arr[i] = value` (any `JSON.Value`). */
1908
+ @operator("[]=") private __set(index: i32, value: JSON.Value): void {
1909
+ this.set<JSON.Value>(index, value);
1910
+ }
1911
+
1912
+ /** Serializes the array to a JSON string. */
1913
+ toString(): string {
1914
+ return JSON.stringify(this);
1915
+ }
1916
+
1917
+ /** Creates a JSON.Arr from a JSON.Value[] (or returns an existing one). */
1918
+ static from<T>(value: T): JSON.Arr {
1919
+ if (value instanceof JSON.Arr) return value;
1920
+ // @ts-expect-error: handled by the isArray guard below
1921
+ if (isArray<T>() && idof<valueof<T>>() == idof<JSON.Value>()) {
1922
+ const out = new JSON.Arr();
1923
+ // @ts-expect-error: T is JSON.Value[] here
1924
+ const arr = changetype<JSON.Value[]>(value);
1925
+ for (let i = 0; i < arr.length; i++) {
1926
+ out.pushRawSlot(JSON.Value.bitsFrom<JSON.Value>(unchecked(arr[i])));
1927
+ }
1928
+ return out;
1929
+ }
1930
+ throw new Error("JSON.Arr.from expects a JSON.Value[]");
1931
+ }
1932
+
1933
+ // ---- Array-like API: slot-optimized ports of the AssemblyScript stdlib ----
1934
+ // Slot-shuffling ops (reverse/fill/copyWithin/pop/shift/unshift/slice/splice)
1935
+ // move the raw u64 slots directly - no per-element JSON.Value - and keep any
1936
+ // managed pointers linked to the same owner. New single-source results share
1937
+ // `_src`, so copied deferred ranges stay lazy. Callbacks get a JSON.Value view
1938
+ // of each element via `at(i)`.
1939
+
1940
+ /** Concrete, source-independent bits for slot `i` (resolves a deferred slot
1941
+ * without caching it back into this array - used by cross-source ops). */
1942
+ private resolvedBits(i: i32): u64 {
1943
+ const slot = unchecked(this._vals[i]);
1944
+ if (!JSON.Value.slotIsLazy(slot)) return slot;
1945
+ const base = changetype<usize>(this._src);
1946
+ const start = JSON.Value.slotPtr(slot, base);
1947
+ const end = JSON.Value.slotEnd(slot, base, this.srcEnd());
1948
+ return JSON.Value.parseSliceBits(start, end, this._src);
1949
+ }
1950
+
1951
+ /** Appends `count` raw slots from `this[from..]` into a fresh `dst`, sharing
1952
+ * `_src` so copied deferred ranges still resolve. */
1953
+ private copyInto(dst: JSON.Arr, from: i32, count: i32): void {
1954
+ if (count <= 0) return;
1955
+ dst._src = this._src;
1956
+ const at = dst._vused;
1957
+ dst.ensureValCap(at + count);
1958
+ for (let k = 0; k < count; k++)
1959
+ dst.storeSlot(at + k, unchecked(this._vals[from + k]));
1960
+ dst._vused = at + count;
1961
+ }
1962
+
1963
+ /** Truncates (drops the tail) or extends (pads with `null`) the array. */
1964
+ set length(newLength: i32) {
1965
+ if (newLength < 0) throw new Error("Invalid array length");
1966
+ const used = this._vused;
1967
+ if (newLength <= used) {
1968
+ this._vused = newLength;
1969
+ return;
1970
+ }
1971
+ this.ensureValCap(newLength);
1972
+ for (let i = used; i < newLength; i++)
1973
+ unchecked((this._vals[i] = JSON.Value.nullBits()));
1974
+ this._vused = newLength;
1975
+ }
1976
+
1977
+ /** Removes and returns the last element. */
1978
+ pop(): JSON.Value {
1979
+ const n = this._vused;
1980
+ if (n == 0) throw new Error("pop from empty JSON.Arr");
1981
+ const v = this.at(n - 1);
1982
+ this._vused = n - 1;
1983
+ return v;
1984
+ }
1985
+
1986
+ /** Removes and returns the first element, shifting the rest down. */
1987
+ shift(): JSON.Value {
1988
+ const n = this._vused;
1989
+ if (n == 0) throw new Error("shift from empty JSON.Arr");
1990
+ const v = this.at(0);
1991
+ const base = changetype<usize>(this._vals);
1992
+ memory.copy(base, base + 8, (<usize>(n - 1)) << 3);
1993
+ this._vused = n - 1;
1994
+ return v;
1995
+ }
1996
+
1997
+ /** Prepends `value`, shifting existing elements up. Returns the new length. */
1998
+ unshift<T>(value: T): i32 {
1999
+ const n = this._vused;
2000
+ this.ensureValCap(n + 1);
2001
+ const base = changetype<usize>(this._vals);
2002
+ if (n) memory.copy(base + 8, base, (<usize>n) << 3);
2003
+ this._vused = n + 1;
2004
+ this.storeSlot(0, JSON.Value.bitsFrom<T>(value));
2005
+ return n + 1;
2006
+ }
2007
+
2008
+ /** Reverses the elements in place (slot swap). */
2009
+ reverse(): JSON.Arr {
2010
+ const vals = this._vals;
2011
+ let lo = 0;
2012
+ let hi = this._vused - 1;
2013
+ while (lo < hi) {
2014
+ const t = unchecked(vals[lo]);
2015
+ unchecked((vals[lo] = unchecked(vals[hi])));
2016
+ unchecked((vals[hi] = t));
2017
+ lo++;
2018
+ hi--;
2019
+ }
2020
+ return this;
2021
+ }
2022
+
2023
+ /** Fills `[start, end)` with `value`. */
2024
+ fill<T>(value: T, start: i32 = 0, end: i32 = i32.MAX_VALUE): JSON.Arr {
2025
+ const n = this._vused;
2026
+ let s = start < 0 ? max(n + start, 0) : min(start, n);
2027
+ const e = end < 0 ? max(n + end, 0) : min(end, n);
2028
+ const bits = JSON.Value.bitsFrom<T>(value);
2029
+ for (; s < e; s++) this.storeSlot(s, bits);
2030
+ return this;
2031
+ }
2032
+
2033
+ /** Copies the slot block `[start, end)` to `target`, in place. */
2034
+ copyWithin(target: i32, start: i32, end: i32 = i32.MAX_VALUE): JSON.Arr {
2035
+ const n = this._vused;
2036
+ const t = target < 0 ? max(n + target, 0) : min(target, n);
2037
+ const s = start < 0 ? max(n + start, 0) : min(start, n);
2038
+ const e = end < 0 ? max(n + end, 0) : min(end, n);
2039
+ const count = min(e - s, n - t);
2040
+ if (count > 0) {
2041
+ const base = changetype<usize>(this._vals);
2042
+ memory.copy(
2043
+ base + ((<usize>t) << 3),
2044
+ base + ((<usize>s) << 3),
2045
+ (<usize>count) << 3,
2046
+ );
2047
+ }
2048
+ return this;
2049
+ }
2050
+
2051
+ /** Returns a new JSON.Arr with the elements in `[start, end)` (lazy-preserving). */
2052
+ slice(start: i32 = 0, end: i32 = i32.MAX_VALUE): JSON.Arr {
2053
+ const n = this._vused;
2054
+ const s = start < 0 ? max(n + start, 0) : min(start, n);
2055
+ const e = end < 0 ? max(n + end, 0) : min(end, n);
2056
+ const out = new JSON.Arr();
2057
+ this.copyInto(out, s, e - s);
2058
+ return out;
2059
+ }
2060
+
2061
+ /** Removes `deleteCount` elements at `start`; returns them as a new JSON.Arr. */
2062
+ splice(start: i32, deleteCount: i32 = i32.MAX_VALUE): JSON.Arr {
2063
+ const n = this._vused;
2064
+ const s = start < 0 ? max(n + start, 0) : min(start, n);
2065
+ const d = max(min(deleteCount, n - s), 0);
2066
+ const removed = new JSON.Arr();
2067
+ this.copyInto(removed, s, d);
2068
+ const tail = n - (s + d);
2069
+ if (tail > 0) {
2070
+ const base = changetype<usize>(this._vals);
2071
+ memory.copy(
2072
+ base + ((<usize>s) << 3),
2073
+ base + ((<usize>(s + d)) << 3),
2074
+ (<usize>tail) << 3,
2075
+ );
2076
+ }
2077
+ this._vused = n - d;
2078
+ return removed;
2079
+ }
2080
+
2081
+ /** Returns a new JSON.Arr = this followed by `other` (resolves deferred slots
2082
+ * since the two sources can't share one `_src`). */
2083
+ concat(other: JSON.Arr): JSON.Arr {
2084
+ const out = new JSON.Arr();
2085
+ const a = this._vused;
2086
+ const b = other._vused;
2087
+ out.ensureValCap(a + b);
2088
+ for (let i = 0; i < a; i++) out.pushRawSlot(this.resolvedBits(i));
2089
+ for (let i = 0; i < b; i++) out.pushRawSlot(other.resolvedBits(i));
2090
+ return out;
2091
+ }
2092
+
2093
+ /** First index of `value` (typed compare via getAs<T>), or -1. */
2094
+ indexOf<T>(value: T, fromIndex: i32 = 0): i32 {
2095
+ const n = this._vused;
2096
+ let i = fromIndex < 0 ? max(n + fromIndex, 0) : fromIndex;
2097
+ for (; i < n; i++) if (this.getAs<T>(i) == value) return i;
2098
+ return -1;
2099
+ }
2100
+
2101
+ /** Last index of `value`, searching backwards, or -1. */
2102
+ lastIndexOf<T>(value: T, fromIndex: i32 = i32.MAX_VALUE): i32 {
2103
+ const n = this._vused;
2104
+ let i = fromIndex < 0 ? n + fromIndex : min(fromIndex, n - 1);
2105
+ for (; i >= 0; i--) if (this.getAs<T>(i) == value) return i;
2106
+ return -1;
2107
+ }
2108
+
2109
+ /** Whether `value` is present. */
2110
+ includes<T>(value: T, fromIndex: i32 = 0): bool {
2111
+ return this.indexOf<T>(value, fromIndex) >= 0;
2112
+ }
2113
+
2114
+ /** Calls `fn` for each element. */
2115
+ forEach(
2116
+ fn: (value: JSON.Value, index: i32, array: JSON.Arr) => void,
2117
+ ): void {
2118
+ for (let i = 0, n = this._vused; i < n; i++) fn(this.at(i), i, this);
2119
+ }
2120
+
2121
+ /** New JSON.Arr of `fn`'s results. */
2122
+ map(
2123
+ fn: (value: JSON.Value, index: i32, array: JSON.Arr) => JSON.Value,
2124
+ ): JSON.Arr {
2125
+ const n = this._vused;
2126
+ const out = new JSON.Arr();
2127
+ out.ensureValCap(n);
2128
+ for (let i = 0; i < n; i++) out.push<JSON.Value>(fn(this.at(i), i, this));
2129
+ return out;
2130
+ }
2131
+
2132
+ /** New JSON.Arr of elements passing `fn` (lazy-preserving). */
2133
+ filter(
2134
+ fn: (value: JSON.Value, index: i32, array: JSON.Arr) => bool,
2135
+ ): JSON.Arr {
2136
+ const out = new JSON.Arr();
2137
+ for (let i = 0, n = this._vused; i < n; i++)
2138
+ if (fn(this.at(i), i, this)) this.copyInto(out, i, 1);
2139
+ return out;
2140
+ }
2141
+
2142
+ /** First element passing `fn`, or `null`. */
2143
+ find(
2144
+ fn: (value: JSON.Value, index: i32, array: JSON.Arr) => bool,
2145
+ ): JSON.Value | null {
2146
+ for (let i = 0, n = this._vused; i < n; i++) {
2147
+ const v = this.at(i);
2148
+ if (fn(v, i, this)) return v;
2149
+ }
2150
+ return null;
2151
+ }
2152
+
2153
+ /** Index of the first element passing `fn`, or -1. */
2154
+ findIndex(
2155
+ fn: (value: JSON.Value, index: i32, array: JSON.Arr) => bool,
2156
+ ): i32 {
2157
+ for (let i = 0, n = this._vused; i < n; i++)
2158
+ if (fn(this.at(i), i, this)) return i;
2159
+ return -1;
2160
+ }
2161
+
2162
+ /** Last element passing `fn`, or `null`. */
2163
+ findLast(
2164
+ fn: (value: JSON.Value, index: i32, array: JSON.Arr) => bool,
2165
+ ): JSON.Value | null {
2166
+ for (let i = this._vused - 1; i >= 0; i--) {
2167
+ const v = this.at(i);
2168
+ if (fn(v, i, this)) return v;
2169
+ }
2170
+ return null;
2171
+ }
2172
+
2173
+ /** Index of the last element passing `fn`, or -1. */
2174
+ findLastIndex(
2175
+ fn: (value: JSON.Value, index: i32, array: JSON.Arr) => bool,
2176
+ ): i32 {
2177
+ for (let i = this._vused - 1; i >= 0; i--)
2178
+ if (fn(this.at(i), i, this)) return i;
2179
+ return -1;
2180
+ }
2181
+
2182
+ /** Whether every element passes `fn`. */
2183
+ every(fn: (value: JSON.Value, index: i32, array: JSON.Arr) => bool): bool {
2184
+ for (let i = 0, n = this._vused; i < n; i++)
2185
+ if (!fn(this.at(i), i, this)) return false;
2186
+ return true;
2187
+ }
2188
+
2189
+ /** Whether any element passes `fn`. */
2190
+ some(fn: (value: JSON.Value, index: i32, array: JSON.Arr) => bool): bool {
2191
+ for (let i = 0, n = this._vused; i < n; i++)
2192
+ if (fn(this.at(i), i, this)) return true;
2193
+ return false;
2194
+ }
2195
+
2196
+ /** Left fold. */
2197
+ reduce<U>(
2198
+ fn: (acc: U, value: JSON.Value, index: i32, array: JSON.Arr) => U,
2199
+ initialValue: U,
2200
+ ): U {
2201
+ let acc = initialValue;
2202
+ for (let i = 0, n = this._vused; i < n; i++)
2203
+ acc = fn(acc, this.at(i), i, this);
2204
+ return acc;
2205
+ }
2206
+
2207
+ /** Right fold. */
2208
+ reduceRight<U>(
2209
+ fn: (acc: U, value: JSON.Value, index: i32, array: JSON.Arr) => U,
2210
+ initialValue: U,
2211
+ ): U {
2212
+ let acc = initialValue;
2213
+ for (let i = this._vused - 1; i >= 0; i--)
2214
+ acc = fn(acc, this.at(i), i, this);
2215
+ return acc;
2216
+ }
2217
+
2218
+ /** Sorts in place by `comparator(a, b)` (materializes for comparison). */
2219
+ sort(comparator: (a: JSON.Value, b: JSON.Value) => i32): JSON.Arr {
2220
+ const n = this._vused;
2221
+ if (n < 2) return this;
2222
+ const view = new Array<JSON.Value>(n);
2223
+ for (let i = 0; i < n; i++) unchecked((view[i] = this.at(i)));
2224
+ view.sort(comparator);
2225
+ for (let i = 0; i < n; i++)
2226
+ this.storeSlot(i, JSON.Value.bitsFrom<JSON.Value>(unchecked(view[i])));
2227
+ return this;
2228
+ }
2229
+
2230
+ /** JS-parity element string: strings unquoted, null -> "", everything else
2231
+ * via ES-exact JSON (numbers drop a trailing `.0`). */
2232
+ private elemStr(i: i32): string {
2233
+ const v = this.at(i);
2234
+ const t = v.type;
2235
+ if (t == JSON.Types.String) return v.get<string>();
2236
+ if (t == JSON.Types.Null) return "";
2237
+ return JSON.stringify(v);
2238
+ }
2239
+
2240
+ /** Joins the elements with `separator` (JS `Array#join` semantics). */
2241
+ join(separator: string = ","): string {
2242
+ const n = this._vused;
2243
+ if (n == 0) return "";
2244
+ let out = this.elemStr(0);
2245
+ for (let i = 1; i < n; i++) out += separator + this.elemStr(i);
2246
+ return out;
2247
+ }
2248
+
2249
+ // See JSON.Obj.__visit - same custom GC visitor for the slot buffer.
2250
+ @unsafe private __visit(cookie: u32): void {
2251
+ __visit(changetype<usize>(this._vals), cookie);
2252
+ __visit(changetype<usize>(this._src), cookie);
2253
+ const vals = this._vals;
2254
+ const n = this._vused;
2255
+ for (let i = 0; i < n; i++) {
2256
+ const w = unchecked(vals[i]);
2257
+ if (!valBoxed(w)) continue;
2258
+ const tag = valTag(w);
2259
+ if (tag == JSON.Types.Lazy) continue;
2260
+ if (tag >= JSON.Types.String) {
2261
+ __visit(valPtr(w), cookie);
2262
+ } else if (
2263
+ (tag == JSON.Types.U64 || tag == JSON.Types.I64) &&
2264
+ w & VAL_BOX64
2265
+ ) {
2266
+ __visit(valPtr(w), cookie);
2267
+ }
2268
+ }
2269
+ }
1150
2270
  }
1151
2271
  /**
1152
2272
  * Box for primitive types
@@ -1161,7 +2281,7 @@ export namespace JSON {
1161
2281
  * @param value T
1162
2282
  * @returns this
1163
2283
  */
1164
- @inline set(value: T): Box<T> {
2284
+ set(value: T): Box<T> {
1165
2285
  this.value = value;
1166
2286
  return this;
1167
2287
  }
@@ -1176,7 +2296,7 @@ export namespace JSON {
1176
2296
  * @param from T
1177
2297
  * @returns Box<T> | null
1178
2298
  */
1179
- @inline static fromValue<T>(value: JSON.Value): Box<T> | null {
2299
+ static fromValue<T>(value: JSON.Value): Box<T> | null {
1180
2300
  if (!(value instanceof JSON.Value))
1181
2301
  throw new Error("value must be of type JSON.Value");
1182
2302
  if (value.type === JSON.Types.Null) return null;
@@ -1195,7 +2315,7 @@ export namespace JSON {
1195
2315
  * @param from T
1196
2316
  * @returns Box<T>
1197
2317
  */
1198
- @inline static from<T>(value: T): Box<T> {
2318
+ static from<T>(value: T): Box<T> {
1199
2319
  return new Box(value);
1200
2320
  }
1201
2321
  toString(): string {
@@ -1260,7 +2380,7 @@ export namespace JSON {
1260
2380
  function __deserialize<T>(srcStart: usize, srcEnd: usize, dst: usize = 0): T {
1261
2381
  // Skip leading whitespace once here so every handler below may assume
1262
2382
  // srcStart is at the first non-whitespace char. (Trailing whitespace is
1263
- // left intact composites self-trim and JSON.Raw preserves it.)
2383
+ // left intact - composites self-trim and JSON.Raw preserves it.)
1264
2384
  while (srcStart < srcEnd && JSON.Util.isSpace(load<u16>(srcStart)))
1265
2385
  srcStart += 2;
1266
2386
  if (isBoolean<T>()) {
@@ -1272,6 +2392,17 @@ export namespace JSON {
1272
2392
  : deserializeUnsigned<T>(srcStart, srcEnd);
1273
2393
  } else if (isFloat<T>()) {
1274
2394
  return deserializeFloat<T>(srcStart, srcEnd);
2395
+ } else if (
2396
+ isNullable<T>() &&
2397
+ srcEnd - srcStart == 8 &&
2398
+ load<u64>(srcStart) == NULL_WORD_U64
2399
+ ) {
2400
+ // A `null` literal must be matched before the string branch: a nullable
2401
+ // string (`string | null`) reports `isString<T>() == true`, so without
2402
+ // this `null` would be (mis)handled as a quoted string and abort. Mirrors
2403
+ // the same-ordered check in `parseInternal`. Reached by lazy-field
2404
+ // materialization, which routes every slot value through `__deserialize`.
2405
+ return null;
1275
2406
  } else if (isString<T>()) {
1276
2407
  if (srcEnd - srcStart < 4)
1277
2408
  throw new Error(
@@ -1279,12 +2410,6 @@ export namespace JSON {
1279
2410
  );
1280
2411
 
1281
2412
  return deserializeString(srcStart, srcEnd) as T;
1282
- } else if (
1283
- isNullable<T>() &&
1284
- srcEnd - srcStart == 8 &&
1285
- load<u64>(srcStart) == NULL_WORD_U64
1286
- ) {
1287
- return null;
1288
2413
  } else {
1289
2414
  let type: nonnull<T> = changetype<nonnull<T>>(0);
1290
2415
  // @ts-expect-error: Defined by transform
@@ -1323,10 +2448,10 @@ export namespace JSON {
1323
2448
  }
1324
2449
  if (type instanceof StaticArray) {
1325
2450
  // @ts-expect-error: type
1326
- return deserializeStaticArray<T>(srcStart, srcEnd, dst);
2451
+ return deserializeStaticArray<nonnull<T>>(srcStart, srcEnd, dst) as T;
1327
2452
  } else if (type instanceof Array) {
1328
2453
  // @ts-expect-error: type
1329
- return deserializeArray<T>(srcStart, srcEnd, dst);
2454
+ return deserializeArray<nonnull<T>>(srcStart, srcEnd, dst) as T;
1330
2455
  } else if (
1331
2456
  type instanceof Int8Array ||
1332
2457
  type instanceof Uint8Array ||
@@ -1361,6 +2486,9 @@ export namespace JSON {
1361
2486
  } else if (type instanceof JSON.Obj) {
1362
2487
  // @ts-expect-error: type
1363
2488
  return deserializeObject(srcStart, srcEnd, 0);
2489
+ } else if (type instanceof JSON.Arr) {
2490
+ // @ts-expect-error: type
2491
+ return deserializeJsonArray(srcStart, srcEnd, 0);
1364
2492
  } else if (type instanceof JSON.Box) {
1365
2493
  // @ts-expect-error: type
1366
2494
  return new JSON.Box(
@@ -1381,21 +2509,15 @@ export namespace JSON {
1381
2509
  );
1382
2510
  }
1383
2511
  export namespace Util {
1384
- // @ts-expect-error: decorator
1385
- @inline export function isSpace(code: u16): boolean {
2512
+ export function isSpace(code: u16): boolean {
1386
2513
  return code == 0x20 || code - 9 <= 4;
1387
2514
  }
1388
2515
  /** Advance past JSON whitespace (space, tab, LF, VT, FF, CR). */
1389
- // @ts-expect-error: decorator
1390
- @inline export function skipWhitespace(
1391
- srcStart: usize,
1392
- srcEnd: usize,
1393
- ): usize {
2516
+ export function skipWhitespace(srcStart: usize, srcEnd: usize): usize {
1394
2517
  while (srcStart < srcEnd && isSpace(load<u16>(srcStart))) srcStart += 2;
1395
2518
  return srcStart;
1396
2519
  }
1397
- // @ts-expect-error: decorator
1398
- @inline function scanQuotedValueEnd(srcStart: usize, srcEnd: usize): usize {
2520
+ function scanQuotedValueEnd(srcStart: usize, srcEnd: usize): usize {
1399
2521
  const endQuote = scanStringEnd(srcStart, srcEnd);
1400
2522
  return endQuote >= srcEnd ? 0 : endQuote + 2;
1401
2523
  }
@@ -1433,8 +2555,7 @@ export namespace JSON {
1433
2555
 
1434
2556
  return srcStart;
1435
2557
  }
1436
- // @ts-expect-error: decorator
1437
- @inline export function scanValueEnd<T = JSON.Value>(
2558
+ export function scanValueEnd<T = JSON.Value>(
1438
2559
  srcStart: usize,
1439
2560
  srcEnd: usize,
1440
2561
  ): usize {
@@ -1461,8 +2582,7 @@ export namespace JSON {
1461
2582
  return scanCompositeValueEnd(ptr, srcEnd);
1462
2583
  return scanScalarValueEnd(ptr, srcEnd);
1463
2584
  }
1464
- // @ts-expect-error: decorator
1465
- @inline export function ptrToStr(start: usize, end: usize): string {
2585
+ export function ptrToStr(start: usize, end: usize): string {
1466
2586
  const size = end - start;
1467
2587
  const out = __new(size, idof<string>());
1468
2588
  memory.copy(out, start, size);
@@ -1499,8 +2619,7 @@ export namespace JSON {
1499
2619
  * @param data - string
1500
2620
  * @returns - T
1501
2621
  */
1502
- // @ts-expect-error: inline
1503
- @inline export function parse<T>(data: string): T {
2622
+ export function parse<T>(data: string): T {
1504
2623
  bs.saveState();
1505
2624
  const result = JSON.parse<T>(data);
1506
2625
  bs.loadState();
@@ -1515,8 +2634,7 @@ export namespace JSON {
1515
2634
  * and `Date` fast paths are handled by the callers (which have buffer-reuse
1516
2635
  * optimizations); everything else routes here so the dispatch chain lives once.
1517
2636
  */
1518
- // @ts-expect-error: @inline is a valid decorator
1519
- @inline function serializeReference<T>(data: T): void {
2637
+ function serializeReference<T>(data: T): void {
1520
2638
  if (data instanceof Array) {
1521
2639
  // @ts-expect-error
1522
2640
  serializeArray(changetype<nonnull<T>>(data));
@@ -1563,6 +2681,8 @@ export namespace JSON {
1563
2681
  serializeArbitrary(data);
1564
2682
  } else if (data instanceof JSON.Obj) {
1565
2683
  serializeObject(data);
2684
+ } else if (data instanceof JSON.Arr) {
2685
+ serializeJsonArray(data);
1566
2686
  } else if (data instanceof JSON.Box) {
1567
2687
  JSON.__serialize(data.value);
1568
2688
  } else {
@@ -1580,12 +2700,11 @@ export enum JSONMode {
1580
2700
  NAIVE = 2,
1581
2701
  }
1582
2702
 
1583
- // @ts-expect-error: decorator
1584
- @inline function parseBox<T>(data: string, ty: T): T {
2703
+ function parseBox<T>(data: string, ty: T): T {
1585
2704
  return JSON.parse<T>(data);
1586
2705
  }
1587
- // @ts-expect-error: inline
1588
- @inline function deserializeBox<T>(
2706
+
2707
+ function deserializeBox<T>(
1589
2708
  srcStart: usize,
1590
2709
  srcEnd: usize,
1591
2710
  dst: usize,
@@ -1594,16 +2713,13 @@ export enum JSONMode {
1594
2713
  return JSON.__deserialize<T>(srcStart, srcEnd, dst);
1595
2714
  }
1596
2715
 
1597
- // @ts-expect-error: inline
1598
- @inline export function toRaw(data: string): JSON.Raw {
2716
+ export function toRaw(data: string): JSON.Raw {
1599
2717
  return new JSON.Raw(data);
1600
2718
  }
1601
- // @ts-expect-error: inline
1602
- @inline export function fromRaw(data: JSON.Raw): string {
2719
+ export function fromRaw(data: JSON.Raw): string {
1603
2720
  return data.data;
1604
2721
  }
1605
2722
 
1606
- // @ts-expect-error: inline
1607
- @inline export function toBox<T>(data: T): JSON.Box<T> {
2723
+ export function toBox<T>(data: T): JSON.Box<T> {
1608
2724
  return new JSON.Box<T>(data);
1609
2725
  }