json-as 1.3.8 → 1.4.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 (84) hide show
  1. package/CHANGELOG.md +24 -0
  2. package/README.md +49 -1
  3. package/assembly/deserialize/index/arbitrary.ts +2 -2
  4. package/assembly/deserialize/naive/array/arbitrary.ts +3 -136
  5. package/assembly/deserialize/naive/array/array.ts +30 -1
  6. package/assembly/deserialize/naive/array/integer.ts +1 -6
  7. package/assembly/deserialize/naive/array/map.ts +10 -14
  8. package/assembly/deserialize/naive/array/object.ts +10 -14
  9. package/assembly/deserialize/naive/float.ts +2 -4
  10. package/assembly/deserialize/naive/integer.ts +1 -2
  11. package/assembly/deserialize/naive/map.ts +40 -202
  12. package/assembly/deserialize/naive/object.ts +153 -174
  13. package/assembly/deserialize/naive/set.ts +1 -2
  14. package/assembly/deserialize/naive/staticarray.ts +1 -2
  15. package/assembly/deserialize/naive/string.ts +65 -18
  16. package/assembly/deserialize/naive/typedarray.ts +1 -2
  17. package/assembly/deserialize/naive/unsigned.ts +1 -2
  18. package/assembly/deserialize/simd/array/integer.ts +3 -6
  19. package/assembly/deserialize/simd/float.ts +2 -7
  20. package/assembly/deserialize/simd/integer.ts +4 -8
  21. package/assembly/deserialize/simd/string.ts +16 -21
  22. package/assembly/deserialize/swar/array/array.ts +1 -2
  23. package/assembly/deserialize/swar/array/bool.ts +1 -2
  24. package/assembly/deserialize/swar/array/float.ts +2 -3
  25. package/assembly/deserialize/swar/array/generic.ts +1 -2
  26. package/assembly/deserialize/swar/array/integer.ts +6 -11
  27. package/assembly/deserialize/swar/array/object.ts +1 -2
  28. package/assembly/deserialize/swar/array/shared.ts +3 -8
  29. package/assembly/deserialize/swar/array/string.ts +1 -2
  30. package/assembly/deserialize/swar/array/struct.ts +1 -1
  31. package/assembly/deserialize/swar/float.ts +3 -8
  32. package/assembly/deserialize/swar/integer.ts +4 -8
  33. package/assembly/deserialize/swar/string.ts +29 -41
  34. package/assembly/index.d.ts +248 -15
  35. package/assembly/index.ts +468 -146
  36. package/assembly/serialize/index/object.ts +18 -15
  37. package/assembly/serialize/naive/string.ts +9 -2
  38. package/assembly/serialize/swar/string.ts +1 -2
  39. package/assembly/util/atoi.ts +1 -2
  40. package/assembly/util/dragonbox.ts +0 -8
  41. package/assembly/util/itoa-fast.ts +3 -6
  42. package/assembly/util/parsefloat-fast.ts +1 -2
  43. package/assembly/util/scanValueEnd.ts +1 -2
  44. package/assembly/util/scanValueEndSimd.ts +160 -0
  45. package/assembly/util/scanValueEndSwar.ts +142 -0
  46. package/assembly/util/scientific.ts +3 -6
  47. package/assembly/util/simd-int.ts +4 -8
  48. package/assembly/util/snp.ts +1 -5
  49. package/assembly/util/stringScan.ts +2 -4
  50. package/assembly/util/swar-int.ts +3 -6
  51. package/lib/as-bs.ts +37 -0
  52. package/package.json +14 -4
  53. package/transform/lib/builder.d.ts +0 -1
  54. package/transform/lib/builder.js +0 -1
  55. package/transform/lib/index.d.ts +0 -1
  56. package/transform/lib/index.js +537 -290
  57. package/transform/lib/linkers/alias.d.ts +0 -1
  58. package/transform/lib/linkers/alias.js +0 -1
  59. package/transform/lib/linkers/custom.d.ts +0 -1
  60. package/transform/lib/linkers/custom.js +0 -1
  61. package/transform/lib/linkers/imports.d.ts +0 -1
  62. package/transform/lib/linkers/imports.js +0 -1
  63. package/transform/lib/types.d.ts +3 -2
  64. package/transform/lib/types.js +2 -1
  65. package/transform/lib/util.d.ts +0 -1
  66. package/transform/lib/util.js +0 -1
  67. package/transform/lib/visitor.d.ts +0 -1
  68. package/transform/lib/visitor.js +0 -1
  69. package/transform/lib/builder.d.ts.map +0 -1
  70. package/transform/lib/builder.js.map +0 -1
  71. package/transform/lib/index.d.ts.map +0 -1
  72. package/transform/lib/index.js.map +0 -1
  73. package/transform/lib/linkers/alias.d.ts.map +0 -1
  74. package/transform/lib/linkers/alias.js.map +0 -1
  75. package/transform/lib/linkers/custom.d.ts.map +0 -1
  76. package/transform/lib/linkers/custom.js.map +0 -1
  77. package/transform/lib/linkers/imports.d.ts.map +0 -1
  78. package/transform/lib/linkers/imports.js.map +0 -1
  79. package/transform/lib/types.d.ts.map +0 -1
  80. package/transform/lib/types.js.map +0 -1
  81. package/transform/lib/util.d.ts.map +0 -1
  82. package/transform/lib/util.js.map +0 -1
  83. package/transform/lib/visitor.d.ts.map +0 -1
  84. package/transform/lib/visitor.js.map +0 -1
package/assembly/index.ts CHANGED
@@ -40,6 +40,7 @@ import {
40
40
  deserializeTypedArray,
41
41
  } from "./deserialize";
42
42
  import {
43
+ BACK_SLASH,
43
44
  BRACE_LEFT,
44
45
  BRACE_RIGHT,
45
46
  BRACKET_LEFT,
@@ -58,14 +59,102 @@ import {
58
59
  } from "./util/dragonbox";
59
60
  import { ptrToStr } from "./util/ptrToStr";
60
61
  import { atoi, bytes, scanStringEnd } from "./util";
62
+ import { scanValueEnd_SIMD } from "./util/scanValueEndSimd";
63
+ import { scanValueEnd_SWAR } from "./util/scanValueEndSwar";
61
64
 
62
- /**
63
- * Offset of the 'storage' property in the JSON.Value class.
64
- */
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 {
98
+ return (w & VAL_QNAN) == VAL_QNAN;
99
+ }
100
+ // @ts-expect-error: Decorator valid here
101
+ @inline function valTag(w: u64): u32 {
102
+ return <u32>((w >> VAL_TAG_SHIFT) & 0x1f);
103
+ }
104
+ // @ts-expect-error: Decorator valid here
105
+ @inline function valPayload(w: u64): u64 {
106
+ return w & VAL_PAYLOAD_MASK;
107
+ }
108
+ // @ts-expect-error: Decorator valid here
109
+ @inline function valPtr(w: u64): usize {
110
+ return <usize>(w & VAL_PTR_MASK);
111
+ }
65
112
  // @ts-expect-error: Decorator valid here
66
- @inline const STORAGE = offsetof<JSON.Value>("storage");
113
+ @inline function valBox(tag: u32, payload: u64): u64 {
114
+ return (
115
+ VAL_QNAN | ((<u64>tag) << VAL_TAG_SHIFT) | (payload & VAL_PAYLOAD_MASK)
116
+ );
117
+ }
118
+ // @ts-expect-error: Decorator valid here
119
+ @inline function valIntTag<T>(): u32 {
120
+ if (sizeof<T>() == 1) return isSigned<T>() ? JSON.Types.I8 : JSON.Types.U8;
121
+ if (sizeof<T>() == 2) return isSigned<T>() ? JSON.Types.I16 : JSON.Types.U16;
122
+ if (sizeof<T>() == 4) return isSigned<T>() ? JSON.Types.I32 : JSON.Types.U32;
123
+ return isSigned<T>() ? JSON.Types.I64 : JSON.Types.U64;
124
+ }
125
+
126
+ // Shared zero-length sentinel for JSON.Obj's key buffer, so an empty object
127
+ // allocates no key storage until its first key is inserted. Never mutated.
128
+ // @ts-expect-error: Decorator valid here
129
+ @lazy const EMPTY_KEYS: StaticArray<u16> = new StaticArray<u16>(0);
67
130
 
68
131
  export namespace JSON {
132
+ /**
133
+ * On-demand field marker. `JSON.Lazy<T>` is structurally just `T` (a no-op
134
+ * type alias), so a field declared `JSON.Lazy<T>` is typed and accessed
135
+ * exactly like `T`. The transform detects the annotation and defers that
136
+ * field: its raw JSON slice is stored at parse time and parsed into `T` on
137
+ * first access (a generated get accessor).
138
+ */
139
+ export type Lazy<T> = T;
140
+
141
+ /**
142
+ * Whether a lazy slot's value is JSON null — for `@omitnull` on lazy fields,
143
+ * without forcing materialization. The slot encodes the state: `u64.MAX_VALUE`
144
+ * = materialized (null iff the value pointer is 0), `0` = absent (null), any
145
+ * other value = a not-yet-parsed slice range (null iff it is literally `null`).
146
+ * @param valPtr pointer of the materialized value (0 when null)
147
+ * @param lz the packed slot
148
+ */
149
+ // @ts-expect-error: inline
150
+ @inline export function __lazyIsNull(valPtr: usize, lz: u64): bool {
151
+ if (lz == u64.MAX_VALUE) return valPtr == 0;
152
+ if (lz == 0) return true;
153
+ const hi = <usize>(lz >>> 32);
154
+ // raw slice of length 4 (8 bytes) equal to the UTF-16 word "null"
155
+ return <usize>(<u32>lz) - hi == 8 && load<u64>(hi) == 0x006c006c0075006e;
156
+ }
157
+
69
158
  /**
70
159
  * Memory management utilities for the JSON serialization buffer.
71
160
  */
@@ -153,17 +242,17 @@ export namespace JSON {
153
242
  return NULL_WORD;
154
243
  } else if (isString<nonnull<T>>()) {
155
244
  serializeString(data as string);
156
- return bs.out<string>();
245
+ return out ? bs.outTo<string>(changetype<usize>(out)) : bs.out<string>();
157
246
  // @ts-expect-error: Defined by transform
158
247
  } else if (isDefined(data.__SERIALIZE_CUSTOM)) {
159
248
  // @ts-expect-error: Defined by transform
160
249
  data.__SERIALIZE_CUSTOM();
161
- return bs.out<string>();
250
+ return out ? bs.outTo<string>(changetype<usize>(out)) : bs.out<string>();
162
251
  // @ts-expect-error: Defined by transform
163
252
  } else if (isDefined(data.__SERIALIZE)) {
164
253
  // @ts-expect-error: Defined by transform
165
- inline.always(data.__SERIALIZE(changetype<usize>(data)));
166
- return bs.out<string>();
254
+ data.__SERIALIZE(changetype<usize>(data));
255
+ return out ? bs.outTo<string>(changetype<usize>(out)) : bs.out<string>();
167
256
  } else if (data instanceof Date) {
168
257
  out = out
169
258
  ? changetype<string>(__renew(changetype<usize>(out), 52))
@@ -179,7 +268,7 @@ export namespace JSON {
179
268
  return changetype<string>(out);
180
269
  } else {
181
270
  serializeReference<T>(data);
182
- return bs.out<string>();
271
+ return out ? bs.outTo<string>(changetype<usize>(out)) : bs.out<string>();
183
272
  }
184
273
  }
185
274
 
@@ -188,11 +277,27 @@ export namespace JSON {
188
277
  * ```js
189
278
  * JSON.parse<T>(data)
190
279
  * ```
280
+ * Pass an existing object as `out` to deserialize into it, reusing its
281
+ * allocations (symmetric with `stringify<T>(data, out)`). On the fast path the
282
+ * per-field reuse logic (nested structs reused as `dst`, strings `__renew`d in
283
+ * place when sizes match, arrays keeping capacity) makes a steady-state
284
+ * re-parse of the same shape allocate ~nothing after the first call.
191
285
  * @param data string
286
+ * @param out optional existing object to reuse (structs/composites only)
192
287
  * @returns T
193
288
  */
289
+ // A type-correct "zero" for any T: null pointer for references, 0/false for
290
+ // value types. `changetype<T>(0)` alone fails for bool/f64 (size mismatch),
291
+ // so branch on isReference at compile time.
292
+ // @ts-ignore: inline
293
+ @inline function __zero<T>(): T {
294
+ // @ts-ignore: compile-time intrinsic
295
+ if (isReference<T>() || isManaged<T>()) return changetype<T>(0);
296
+ return <T>0;
297
+ }
298
+
194
299
  // @ts-expect-error: inline
195
- @inline export function parse<T>(data: string): T {
300
+ @inline export function parse<T>(data: string, out: T = __zero<T>()): T {
196
301
  let dataPtr = changetype<usize>(data);
197
302
  const dataEnd = dataPtr + bytes(data);
198
303
  // Entry point skips leading whitespace: every deserialize handler may then
@@ -223,24 +328,27 @@ export namespace JSON {
223
328
  let type: nonnull<T> = changetype<nonnull<T>>(0);
224
329
  // @ts-expect-error: Defined by transform
225
330
  if (isDefined(type.__DESERIALIZE_CUSTOM)) {
226
- const out = changetype<nonnull<T>>(0);
331
+ const obj = changetype<nonnull<T>>(0);
227
332
  // @ts-expect-error
228
- return out.__DESERIALIZE_CUSTOM(data);
333
+ return obj.__DESERIALIZE_CUSTOM(data);
229
334
  // @ts-expect-error: Defined by transform
230
335
  } else if (
231
336
  isDefined(type.__DESERIALIZE_SLOW) ||
232
337
  isDefined(type.__DESERIALIZE_FAST)
233
338
  ) {
234
- const out = changetype<nonnull<T>>(
235
- __new(offsetof<nonnull<T>>(), idof<nonnull<T>>()),
236
- );
339
+ // Reuse the caller-supplied `out` graph when given; otherwise allocate.
340
+ const obj = changetype<usize>(out)
341
+ ? changetype<nonnull<T>>(changetype<usize>(out))
342
+ : changetype<nonnull<T>>(
343
+ __new(offsetof<nonnull<T>>(), idof<nonnull<T>>()),
344
+ );
237
345
  // @ts-expect-error: Defined by transform
238
346
  if (isDefined(type.__DESERIALIZE_FAST)) {
239
347
  // @ts-expect-error: Defined by transform
240
- const fastEnd = out.__DESERIALIZE_FAST(
348
+ const fastEnd = obj.__DESERIALIZE_FAST(
241
349
  dataPtr,
242
350
  dataPtr + dataSize,
243
- out,
351
+ obj,
244
352
  );
245
353
  // A non-zero return means the fast path matched; accept it when only
246
354
  // trailing whitespace remains (pretty-printed input ends with a
@@ -249,31 +357,37 @@ export namespace JSON {
249
357
  fastEnd != 0 &&
250
358
  JSON.Util.skipWhitespace(fastEnd, dataPtr + dataSize) ==
251
359
  dataPtr + dataSize
252
- )
253
- return out;
360
+ ) {
361
+ // @ts-expect-error: Defined by transform for @lazy-field structs —
362
+ // pins the source so stored slice ranges stay valid.
363
+ if (isDefined(obj.__SET_SRC)) obj.__SET_SRC(data);
364
+ return obj;
365
+ }
254
366
  }
255
- if (isDefined(type.__INITIALIZE)) out.__INITIALIZE();
367
+ if (isDefined(type.__INITIALIZE)) obj.__INITIALIZE();
256
368
  // @ts-expect-error: Defined by transform
257
369
  if (isDefined(type.__DESERIALIZE_SLOW)) {
258
370
  // @ts-expect-error: Defined by transform
259
- out.__DESERIALIZE_SLOW(dataPtr, dataPtr + dataSize, out);
260
- return out;
371
+ obj.__DESERIALIZE_SLOW(dataPtr, dataPtr + dataSize, obj);
372
+ // @ts-expect-error: Defined by transform for @lazy-field structs.
373
+ if (isDefined(obj.__SET_SRC)) obj.__SET_SRC(data);
374
+ return obj;
261
375
  }
262
376
  throw new Error(`No deserialize method defined for type ${type}`);
263
377
  }
264
378
  if (type instanceof StaticArray) {
265
379
  // @ts-expect-error
266
- return inline.always(
267
- deserializeStaticArray<nonnull<T>>(dataPtr, dataPtr + dataSize, 0),
380
+ return deserializeStaticArray<nonnull<T>>(
381
+ dataPtr,
382
+ dataPtr + dataSize,
383
+ 0,
268
384
  );
269
385
  } else if (type instanceof Array) {
270
386
  // @ts-expect-error
271
- return inline.always(
272
- deserializeArray<nonnull<T>>(
273
- dataPtr,
274
- dataPtr + dataSize,
275
- changetype<usize>(instantiate<T>()),
276
- ),
387
+ return deserializeArray<nonnull<T>>(
388
+ dataPtr,
389
+ dataPtr + dataSize,
390
+ changetype<usize>(instantiate<T>()),
277
391
  );
278
392
  } else if (
279
393
  type instanceof Int8Array ||
@@ -297,14 +411,10 @@ export namespace JSON {
297
411
  return deserializeArrayBuffer(dataPtr, dataPtr + dataSize, 0) as T;
298
412
  } else if (type instanceof Set) {
299
413
  // @ts-expect-error
300
- return inline.always(
301
- deserializeSet<nonnull<T>>(dataPtr, dataPtr + dataSize, 0),
302
- );
414
+ return deserializeSet<nonnull<T>>(dataPtr, dataPtr + dataSize, 0);
303
415
  } else if (type instanceof Map) {
304
416
  // @ts-expect-error
305
- return inline.always(
306
- deserializeMap<nonnull<T>>(dataPtr, dataPtr + dataSize, 0),
307
- );
417
+ return deserializeMap<nonnull<T>>(dataPtr, dataPtr + dataSize, 0);
308
418
  } else if (type instanceof Date) {
309
419
  // @ts-expect-error
310
420
  return deserializeDate(dataPtr, dataPtr + dataSize);
@@ -313,12 +423,10 @@ export namespace JSON {
313
423
  return deserializeRaw(dataPtr, dataPtr + dataSize);
314
424
  } else if (type instanceof JSON.Value) {
315
425
  // @ts-expect-error
316
- return inline.always(
317
- deserializeArbitrary(dataPtr, dataPtr + dataSize, 0),
318
- );
426
+ return deserializeArbitrary(dataPtr, dataPtr + dataSize, 0);
319
427
  } else if (type instanceof JSON.Obj) {
320
428
  // @ts-expect-error
321
- return inline.always(deserializeObject(dataPtr, dataPtr + dataSize, 0));
429
+ return deserializeObject(dataPtr, dataPtr + dataSize, 0);
322
430
  } else if (type instanceof JSON.Box) {
323
431
  // @ts-expect-error
324
432
  return new JSON.Box(parseBox(data, changetype<nonnull<T>>(0).value));
@@ -463,24 +571,39 @@ export namespace JSON {
463
571
  /** Map of struct type IDs to their serialization function indices */
464
572
  @lazy static METHODS: Map<u32, u32> = new Map<u32, u32>();
465
573
 
466
- /** The runtime type identifier (see JSON.Types) */
467
- public type: u16;
468
-
469
- /** Internal storage for the value (8 bytes, can hold any primitive or pointer) */
470
- private storage: u64;
574
+ /** NaN-boxed word holding both the type tag and the value (8 bytes). */
575
+ private bits: u64;
471
576
 
472
577
  private constructor() {
473
578
  unreachable();
474
579
  }
475
580
 
581
+ /**
582
+ * The runtime type identifier (see JSON.Types), decoded from the boxed word.
583
+ * Struct values report `idof<T>() + JSON.Types.Struct`, recovered from the
584
+ * stored object's runtime header.
585
+ */
586
+ get type(): u16 {
587
+ const w = this.bits;
588
+ if (!valBoxed(w)) return JSON.Types.F64;
589
+ const tag = valTag(w);
590
+ if (tag == JSON.Types.Struct) {
591
+ const rtId = changetype<OBJECT>(valPtr(w) - TOTAL_OVERHEAD).rtId;
592
+ return <u16>rtId + JSON.Types.Struct;
593
+ }
594
+ return <u16>tag;
595
+ }
596
+
476
597
  /**
477
598
  * Creates an JSON.Value instance with no set value.
478
599
  * @returns An instance of JSON.Value.
479
600
  */
480
601
  @inline static empty(): JSON.Value {
481
- return changetype<JSON.Value>(
602
+ const out = changetype<JSON.Value>(
482
603
  __new(offsetof<JSON.Value>(), idof<JSON.Value>()),
483
604
  );
605
+ out.bits = VAL_NULL;
606
+ return out;
484
607
  }
485
608
 
486
609
  /**
@@ -556,35 +679,37 @@ export namespace JSON {
556
679
  * @param value - The value to be set.
557
680
  */
558
681
  @inline set<T>(value: T): void {
559
- this.type = this.getType<T>(value);
560
-
561
- if (value instanceof JSON.Box) this.set(value.value);
562
- else if (isBoolean<T>())
563
- store<T>(changetype<usize>(this), value, STORAGE);
564
- else if (
565
- isInteger<T>() &&
566
- !isSigned<T>() &&
567
- changetype<usize>(value) == 0 &&
568
- nameof<T>() == "usize"
569
- )
570
- store<usize>(changetype<usize>(this), 0, STORAGE);
571
- else if (isInteger<T>() || isFloat<T>())
572
- store<T>(changetype<usize>(this), value, STORAGE);
573
- else if (isNullable<T>() && changetype<usize>(value) === 0)
574
- store<usize>(changetype<usize>(this), 0, STORAGE);
575
- else if (isString<T>()) store<T>(changetype<usize>(this), value, STORAGE);
576
- else if (value instanceof JSON.Raw)
577
- store<T>(changetype<usize>(this), value, STORAGE);
578
- // @ts-expect-error: supplied by transform
579
- else if (isDefined(value.__SERIALIZE) && isManaged<T>(value)) {
682
+ if (value instanceof JSON.Box) {
683
+ this.set(value.value);
684
+ } else if (isBoolean<T>()) {
685
+ this.bits = valBox(JSON.Types.Bool, value ? 1 : 0);
686
+ } else if (isInteger<T>() && nameof<T>() == "usize") {
687
+ // A `usize` of 0 is the null sentinel (see deserializeArbitrary);
688
+ // any other usize is an ordinary 32-bit unsigned integer.
689
+ this.bits = value ? valBox(valIntTag<T>(), <u64>value) : VAL_NULL;
690
+ } else if (isFloat<T>()) {
691
+ if (sizeof<T>() == 4) {
692
+ this.bits = valBox(JSON.Types.F32, <u64>reinterpret<u32>(<f32>value));
693
+ } else {
694
+ const f = <f64>value;
695
+ // Canonicalize NaN so it never collides with the box signature.
696
+ this.bits = isNaN(f) ? 0x7ff8000000000000 : reinterpret<u64>(f);
697
+ }
698
+ } else if (isInteger<T>()) {
699
+ if (sizeof<T>() == 8) this.setWide<T>(value);
700
+ else this.bits = valBox(valIntTag<T>(), <u64>value);
701
+ } else if (isNullable<T>() && changetype<usize>(value) === 0) {
702
+ this.bits = VAL_NULL;
703
+ } else if (isString<T>()) {
704
+ this.bits = valBox(JSON.Types.String, <u64>changetype<usize>(value));
705
+ } else if (value instanceof JSON.Raw) {
706
+ this.bits = valBox(JSON.Types.Raw, <u64>changetype<usize>(value));
707
+ // @ts-expect-error: supplied by transform
708
+ } else if (isDefined(value.__SERIALIZE) && isManaged<T>(value)) {
580
709
  // @ts-expect-error
581
710
  if (!JSON.Value.METHODS.has(idof<T>()))
582
711
  JSON.Value.METHODS.set(idof<T>(), value.__SERIALIZE.index);
583
- store<usize>(
584
- changetype<usize>(this),
585
- changetype<usize>(value),
586
- STORAGE,
587
- );
712
+ this.bits = valBox(JSON.Types.Struct, <u64>changetype<usize>(value));
588
713
  } else if (
589
714
  value instanceof Int8Array ||
590
715
  value instanceof Uint8Array ||
@@ -596,20 +721,52 @@ export namespace JSON {
596
721
  value instanceof Int64Array ||
597
722
  value instanceof Uint64Array ||
598
723
  value instanceof Float32Array ||
599
- value instanceof Float64Array ||
600
- value instanceof ArrayBuffer
601
- )
602
- store<T>(changetype<usize>(this), value, STORAGE);
603
- else if (value instanceof Map) {
724
+ value instanceof Float64Array
725
+ ) {
726
+ this.bits = valBox(
727
+ JSON.Types.TypedArray,
728
+ <u64>changetype<usize>(value),
729
+ );
730
+ } else if (value instanceof ArrayBuffer) {
731
+ this.bits = valBox(
732
+ JSON.Types.ArrayBuffer,
733
+ <u64>changetype<usize>(value),
734
+ );
735
+ } else if (value instanceof Map) {
604
736
  if (idof<T>() !== idof<Map<string, JSON.Value>>()) {
605
737
  abort("Maps must be of type Map<string, JSON.Value>!");
606
738
  }
607
- store<T>(changetype<usize>(this), value, STORAGE);
739
+ this.bits = valBox(JSON.Types.Map, <u64>changetype<usize>(value));
608
740
  } else if (value instanceof JSON.Obj) {
609
- store<T>(changetype<usize>(this), value, STORAGE);
741
+ this.bits = valBox(JSON.Types.Object, <u64>changetype<usize>(value));
610
742
  // @ts-expect-error
611
743
  } else if (isArray<T>() && idof<valueof<T>>() == idof<JSON.Value>()) {
612
- store<T>(changetype<usize>(this), value, STORAGE);
744
+ this.bits = valBox(JSON.Types.Array, <u64>changetype<usize>(value));
745
+ }
746
+ }
747
+
748
+ /** Encodes a 64-bit integer, spilling to the heap when it exceeds the payload. */
749
+ private setWide<T>(value: T): void {
750
+ if (isSigned<T>()) {
751
+ const v = <i64>value;
752
+ if (v >= -VAL_I64_LIMIT && v < VAL_I64_LIMIT) {
753
+ this.bits = valBox(JSON.Types.I64, <u64>v);
754
+ } else {
755
+ const box = new StaticArray<u64>(1);
756
+ unchecked((box[0] = <u64>v));
757
+ this.bits =
758
+ valBox(JSON.Types.I64, <u64>changetype<usize>(box)) | VAL_BOX64;
759
+ }
760
+ } else {
761
+ const v = <u64>value;
762
+ if (v < VAL_U64_LIMIT) {
763
+ this.bits = valBox(JSON.Types.U64, v);
764
+ } else {
765
+ const box = new StaticArray<u64>(1);
766
+ unchecked((box[0] = v));
767
+ this.bits =
768
+ valBox(JSON.Types.U64, <u64>changetype<usize>(box)) | VAL_BOX64;
769
+ }
613
770
  }
614
771
  }
615
772
 
@@ -618,7 +775,23 @@ export namespace JSON {
618
775
  * @returns The encapsulated value.
619
776
  */
620
777
  @inline get<T>(): T {
621
- return load<T>(changetype<usize>(this), STORAGE);
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();
622
795
  }
623
796
 
624
797
  /**
@@ -627,7 +800,7 @@ export namespace JSON {
627
800
  * @returns The encapsulated value.
628
801
  */
629
802
  @inline as<T>(): T {
630
- return load<T>(changetype<usize>(this), STORAGE);
803
+ return this.get<T>();
631
804
  }
632
805
 
633
806
  /**
@@ -709,8 +882,18 @@ export namespace JSON {
709
882
 
710
883
 
711
884
  @unsafe private __visit(cookie: u32): void {
712
- if (this.type >= JSON.Types.String) {
713
- __visit(load<usize>(changetype<usize>(this), STORAGE), cookie);
885
+ const w = this.bits;
886
+ if (!valBoxed(w)) return; // raw f64 holds no reference
887
+ const tag = valTag(w);
888
+ // String(13)..ArrayBuffer(19) and Struct all carry a managed pointer;
889
+ // Raw(1) is intentionally not traced (matches prior behavior).
890
+ if (tag >= JSON.Types.String) {
891
+ __visit(valPtr(w), cookie);
892
+ } else if (
893
+ (tag == JSON.Types.U64 || tag == JSON.Types.I64) &&
894
+ w & VAL_BOX64
895
+ ) {
896
+ __visit(valPtr(w), cookie); // heap-spilled 64-bit int
714
897
  }
715
898
  }
716
899
  }
@@ -735,8 +918,16 @@ export namespace JSON {
735
918
  * ```
736
919
  */
737
920
  export class Obj {
738
- /** Internal storage map */
739
- storage: Map<string, JSON.Value> = new Map<string, JSON.Value>();
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.
927
+ _kbuf: StaticArray<u16> = EMPTY_KEYS;
928
+ _kused: i32 = 0;
929
+ _vals: JSON.Value[] = [];
930
+ private _index: Map<string, i32> | null = null;
740
931
 
741
932
  constructor() {}
742
933
 
@@ -744,16 +935,106 @@ export namespace JSON {
744
935
  * Gets the number of key-value pairs in the object.
745
936
  */
746
937
  @inline get size(): i32 {
747
- return this.storage.size;
938
+ return this._vals.length;
939
+ }
940
+
941
+ /** Grows the key buffer to hold at least `need` code units. */
942
+ private ensureKeyCap(need: i32): void {
943
+ const cap = this._kbuf.length;
944
+ if (cap >= need) return;
945
+ let n = cap ? cap : 16;
946
+ while (n < need) n <<= 1;
947
+ const nb = new StaticArray<u16>(n);
948
+ if (this._kused)
949
+ memory.copy(
950
+ changetype<usize>(nb),
951
+ changetype<usize>(this._kbuf),
952
+ (<usize>this._kused) << 1,
953
+ );
954
+ this._kbuf = nb;
955
+ }
956
+
957
+ /** Appends a length-prefixed key (from a source memory range). */
958
+ private pushKeyBytes(keyStart: usize, keyEnd: usize): void {
959
+ const len = <i32>((keyEnd - keyStart) >> 1);
960
+ const pos = this._kused;
961
+ this.ensureKeyCap(pos + 1 + len);
962
+ const buf = changetype<usize>(this._kbuf);
963
+ store<u16>(buf + ((<usize>pos) << 1), <u16>len);
964
+ if (len)
965
+ memory.copy(
966
+ buf + ((<usize>(pos + 1)) << 1),
967
+ keyStart,
968
+ (<usize>len) << 1,
969
+ );
970
+ this._kused = pos + 1 + len;
971
+ }
972
+
973
+ /** Materializes a key string from `len` code units starting at slot `at`. */
974
+ private makeKey(at: i32, len: i32): string {
975
+ const out = changetype<string>(__new((<usize>len) << 1, idof<string>()));
976
+ if (len)
977
+ memory.copy(
978
+ changetype<usize>(out),
979
+ changetype<usize>(this._kbuf) + ((<usize>at) << 1),
980
+ (<usize>len) << 1,
981
+ );
982
+ return out;
748
983
  }
749
984
 
750
985
  /**
751
- * Sets a key-value pair in the object.
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.
989
+ */
990
+ appendRaw<T>(keyStart: usize, keyEnd: usize, value: T): void {
991
+ this.pushKeyBytes(keyStart, keyEnd);
992
+ this._vals.push(JSON.Value.from<T>(value));
993
+ 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);
999
+ }
1000
+ }
1001
+
1002
+ /** Builds (once) and returns the lazy key -> position index. */
1003
+ private buildIndex(): Map<string, i32> {
1004
+ let idx = this._index;
1005
+ 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;
1015
+ }
1016
+ this._index = idx;
1017
+ }
1018
+ return idx;
1019
+ }
1020
+
1021
+ /**
1022
+ * Sets a key-value pair in the object, overwriting any existing value.
752
1023
  * @param key - The string key
753
1024
  * @param value - The value (will be wrapped in JSON.Value)
754
1025
  */
755
- @inline set<T>(key: string, value: T): void {
756
- this.storage.set(key, JSON.Value.from<T>(value));
1026
+ 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)));
1030
+ } else {
1031
+ this.pushKeyBytes(
1032
+ changetype<usize>(key),
1033
+ changetype<usize>(key) + ((<usize>key.length) << 1),
1034
+ );
1035
+ this._vals.push(JSON.Value.from<T>(value));
1036
+ idx.set(key, this._vals.length - 1);
1037
+ }
757
1038
  }
758
1039
 
759
1040
  /**
@@ -762,8 +1043,8 @@ export namespace JSON {
762
1043
  * @returns The JSON.Value or null if not found
763
1044
  */
764
1045
  @inline get(key: string): JSON.Value | null {
765
- if (!this.storage.has(key)) return null;
766
- return this.storage.get(key);
1046
+ const idx = this.buildIndex();
1047
+ return idx.has(key) ? unchecked(this._vals[idx.get(key)]) : null;
767
1048
  }
768
1049
 
769
1050
  /**
@@ -772,7 +1053,7 @@ export namespace JSON {
772
1053
  * @returns true if the key exists
773
1054
  */
774
1055
  @inline has(key: string): bool {
775
- return this.storage.has(key);
1056
+ return this.buildIndex().has(key);
776
1057
  }
777
1058
 
778
1059
  /**
@@ -780,24 +1061,53 @@ export namespace JSON {
780
1061
  * @param key - The key to delete
781
1062
  * @returns true if the key was found and deleted
782
1063
  */
783
- @inline delete(key: string): bool {
784
- return this.storage.delete(key);
1064
+ delete(key: string): bool {
1065
+ const idx = this.buildIndex();
1066
+ if (!idx.has(key)) return false;
1067
+ const removed = idx.get(key);
1068
+ const keys = this.keys();
1069
+ const vals = this._vals;
1070
+ this._kbuf = EMPTY_KEYS;
1071
+ this._kused = 0;
1072
+ const newVals = new Array<JSON.Value>();
1073
+ for (let j = 0; j < keys.length; j++) {
1074
+ if (j == removed) continue;
1075
+ const k = unchecked(keys[j]);
1076
+ this.pushKeyBytes(
1077
+ changetype<usize>(k),
1078
+ changetype<usize>(k) + ((<usize>k.length) << 1),
1079
+ );
1080
+ newVals.push(unchecked(vals[j]));
1081
+ }
1082
+ this._vals = newVals;
1083
+ this._index = null;
1084
+ return true;
785
1085
  }
786
1086
 
787
1087
  /**
788
1088
  * Gets all keys in the object.
789
- * @returns Array of string keys
1089
+ * @returns Array of string keys (in insertion order)
790
1090
  */
791
- @inline keys(): string[] {
792
- return this.storage.keys();
1091
+ keys(): string[] {
1092
+ const out = new Array<string>(this._vals.length);
1093
+ const buf = changetype<usize>(this._kbuf);
1094
+ const used = this._kused;
1095
+ let pos = 0;
1096
+ let i = 0;
1097
+ while (pos < used) {
1098
+ const len = <i32>load<u16>(buf + ((<usize>pos) << 1));
1099
+ unchecked((out[i++] = this.makeKey(pos + 1, len)));
1100
+ pos += 1 + len;
1101
+ }
1102
+ return out;
793
1103
  }
794
1104
 
795
1105
  /**
796
1106
  * Gets all values in the object.
797
- * @returns Array of JSON.Value instances
1107
+ * @returns Array of JSON.Value instances (in insertion order)
798
1108
  */
799
1109
  @inline values(): JSON.Value[] {
800
- return this.storage.values();
1110
+ return this._vals.slice();
801
1111
  }
802
1112
 
803
1113
  /**
@@ -813,7 +1123,7 @@ export namespace JSON {
813
1123
  * @param value - The value to convert
814
1124
  * @returns A new JSON.Obj instance
815
1125
  */
816
- @inline static from<T>(value: T): JSON.Obj {
1126
+ static from<T>(value: T): JSON.Obj {
817
1127
  if (value instanceof JSON.Obj) return value;
818
1128
  if (value instanceof Map) {
819
1129
  const out = new JSON.Obj();
@@ -933,7 +1243,7 @@ export namespace JSON {
933
1243
  serializeStruct(changetype<nonnull<T>>(data));
934
1244
  } else if (data instanceof Date) {
935
1245
  // @ts-expect-error
936
- inline.always(serializeDate(changetype<nonnull<T>>(data)));
1246
+ serializeDate(changetype<nonnull<T>>(data));
937
1247
  } else {
938
1248
  serializeReference<T>(data);
939
1249
  }
@@ -1085,55 +1395,71 @@ export namespace JSON {
1085
1395
  return srcStart;
1086
1396
  }
1087
1397
  // @ts-expect-error: decorator
1088
- @inline export function scanValueEnd(
1089
- srcStart: usize,
1090
- srcEnd: usize,
1091
- ): usize {
1092
- if (srcStart >= srcEnd) return 0;
1093
- let ptr = srcStart;
1094
- while (ptr < srcEnd && isSpace(load<u16>(ptr))) ptr += 2;
1095
- if (ptr >= srcEnd) return 0;
1096
- const first = load<u16>(ptr);
1097
-
1098
- if (first == QUOTE) {
1099
- const endQuote = scanStringEnd(ptr, srcEnd);
1100
- return endQuote >= srcEnd ? 0 : endQuote + 2;
1101
- }
1102
-
1103
- if (first == BRACE_LEFT || first == BRACKET_LEFT) {
1104
- let depth: i32 = 1;
1105
- ptr += 2;
1106
- while (ptr < srcEnd) {
1107
- const code = load<u16>(ptr);
1108
- if (code == QUOTE) {
1109
- const endQuote = scanStringEnd(ptr, srcEnd);
1110
- if (endQuote >= srcEnd) return 0;
1111
- ptr = endQuote + 2;
1112
- continue;
1113
- }
1114
- if (code == BRACE_LEFT || code == BRACKET_LEFT) {
1115
- depth++;
1116
- } else if (code == BRACE_RIGHT || code == BRACKET_RIGHT) {
1117
- if (--depth == 0) return ptr + 2;
1118
- }
1119
- ptr += 2;
1120
- }
1121
- return 0;
1122
- }
1123
-
1398
+ @inline function scanQuotedValueEnd(srcStart: usize, srcEnd: usize): usize {
1399
+ const endQuote = scanStringEnd(srcStart, srcEnd);
1400
+ return endQuote >= srcEnd ? 0 : endQuote + 2;
1401
+ }
1402
+ function scanCompositeValueEnd(srcStart: usize, srcEnd: usize): usize {
1403
+ let depth: i32 = 1;
1404
+ let ptr = srcStart + 2;
1124
1405
  while (ptr < srcEnd) {
1125
1406
  const code = load<u16>(ptr);
1407
+ if (code == QUOTE) {
1408
+ ptr = scanQuotedValueEnd(ptr, srcEnd);
1409
+ if (!ptr) return 0;
1410
+ continue;
1411
+ }
1412
+ if (code == BRACE_LEFT || code == BRACKET_LEFT) {
1413
+ depth++;
1414
+ } else if (code == BRACE_RIGHT || code == BRACKET_RIGHT) {
1415
+ if (--depth == 0) return ptr + 2;
1416
+ }
1417
+ ptr += 2;
1418
+ }
1419
+ return 0;
1420
+ }
1421
+ function scanScalarValueEnd(srcStart: usize, srcEnd: usize): usize {
1422
+ while (srcStart < srcEnd) {
1423
+ const code = load<u16>(srcStart);
1126
1424
  if (
1127
1425
  code == COMMA ||
1128
1426
  code == BRACKET_RIGHT ||
1129
1427
  code == BRACE_RIGHT ||
1130
1428
  isSpace(code)
1131
1429
  )
1132
- return ptr;
1133
- ptr += 2;
1430
+ return srcStart;
1431
+ srcStart += 2;
1134
1432
  }
1135
1433
 
1136
- return ptr;
1434
+ return srcStart;
1435
+ }
1436
+ // @ts-expect-error: decorator
1437
+ @inline export function scanValueEnd<T = JSON.Value>(
1438
+ srcStart: usize,
1439
+ srcEnd: usize,
1440
+ ): usize {
1441
+ if (srcStart >= srcEnd) return 0;
1442
+ let ptr = skipWhitespace(srcStart, srcEnd);
1443
+ if (ptr >= srcEnd) return 0;
1444
+
1445
+ if (ASC_FEATURE_SIMD) return scanValueEnd_SIMD<T>(ptr, srcEnd);
1446
+ if (JSON_MODE == JSONMode.SWAR) return scanValueEnd_SWAR<T>(ptr, srcEnd);
1447
+
1448
+ const first = load<u16>(ptr);
1449
+ if (isString<nonnull<T>>() && first == QUOTE)
1450
+ return scanQuotedValueEnd(ptr, srcEnd);
1451
+ if (isArray<nonnull<T>>() && first == BRACKET_LEFT)
1452
+ return scanCompositeValueEnd(ptr, srcEnd);
1453
+ if (
1454
+ (isManaged<nonnull<T>>() || isReference<nonnull<T>>()) &&
1455
+ first == BRACE_LEFT
1456
+ )
1457
+ return scanCompositeValueEnd(ptr, srcEnd);
1458
+
1459
+ if (first == QUOTE) return scanQuotedValueEnd(ptr, srcEnd);
1460
+ if (first == BRACE_LEFT || first == BRACKET_LEFT)
1461
+ return scanCompositeValueEnd(ptr, srcEnd);
1462
+ return scanScalarValueEnd(ptr, srcEnd);
1137
1463
  }
1138
1464
  // @ts-expect-error: decorator
1139
1465
  @inline export function ptrToStr(start: usize, end: usize): string {
@@ -1154,11 +1480,7 @@ export namespace JSON {
1154
1480
  * @param out - string | null
1155
1481
  * @returns - string
1156
1482
  */
1157
- // @ts-expect-error: inline
1158
- @inline export function stringify<T>(
1159
- data: T,
1160
- out: string | null = null,
1161
- ): string {
1483
+ export function stringify<T>(data: T, out: string | null = null): string {
1162
1484
  bs.saveState();
1163
1485
  JSON.__serialize<T>(data);
1164
1486
  const result = bs.cpyOut<string>();