json-as 1.3.9 → 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 (122) hide show
  1. package/CHANGELOG.md +60 -19
  2. package/README.md +120 -21
  3. package/assembly/custom/chars.ts +39 -78
  4. package/assembly/deserialize/index/arbitrary.ts +28 -10
  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/arbitrary.ts +3 -136
  11. package/assembly/deserialize/naive/array/array.ts +30 -1
  12. package/assembly/deserialize/naive/array/integer.ts +2 -7
  13. package/assembly/deserialize/naive/array/map.ts +10 -14
  14. package/assembly/deserialize/naive/array/object.ts +10 -14
  15. package/assembly/deserialize/naive/array/struct.ts +19 -1
  16. package/assembly/deserialize/naive/bool.ts +1 -5
  17. package/assembly/deserialize/naive/date.ts +1 -2
  18. package/assembly/deserialize/naive/float.ts +4 -11
  19. package/assembly/deserialize/naive/integer.ts +2 -4
  20. package/assembly/deserialize/naive/map.ts +42 -205
  21. package/assembly/deserialize/naive/object.ts +291 -174
  22. package/assembly/deserialize/naive/raw.ts +1 -5
  23. package/assembly/deserialize/naive/set.ts +3 -6
  24. package/assembly/deserialize/naive/staticarray.ts +2 -4
  25. package/assembly/deserialize/naive/string.ts +68 -24
  26. package/assembly/deserialize/naive/typedarray.ts +1 -2
  27. package/assembly/deserialize/naive/unsigned.ts +2 -4
  28. package/assembly/deserialize/simd/array/integer.ts +5 -13
  29. package/assembly/deserialize/simd/float.ts +5 -12
  30. package/assembly/deserialize/simd/integer.ts +6 -15
  31. package/assembly/deserialize/simd/string.ts +21 -43
  32. package/assembly/deserialize/swar/array/arbitrary.ts +1 -2
  33. package/assembly/deserialize/swar/array/array.ts +2 -4
  34. package/assembly/deserialize/swar/array/bool.ts +2 -4
  35. package/assembly/deserialize/swar/array/box.ts +1 -2
  36. package/assembly/deserialize/swar/array/float.ts +8 -21
  37. package/assembly/deserialize/swar/array/generic.ts +2 -4
  38. package/assembly/deserialize/swar/array/integer.ts +13 -27
  39. package/assembly/deserialize/swar/array/map.ts +1 -2
  40. package/assembly/deserialize/swar/array/object.ts +2 -4
  41. package/assembly/deserialize/swar/array/raw.ts +1 -2
  42. package/assembly/deserialize/swar/array/shared.ts +9 -21
  43. package/assembly/deserialize/swar/array/string.ts +4 -10
  44. package/assembly/deserialize/swar/array/struct.ts +3 -9
  45. package/assembly/deserialize/swar/array.ts +1 -3
  46. package/assembly/deserialize/swar/float.ts +7 -17
  47. package/assembly/deserialize/swar/integer.ts +6 -15
  48. package/assembly/deserialize/swar/string.ts +40 -54
  49. package/assembly/deserialize/swar/typedarray.ts +4 -4
  50. package/assembly/index.d.ts +259 -21
  51. package/assembly/index.ts +1704 -266
  52. package/assembly/serialize/index/arbitrary.ts +70 -4
  53. package/assembly/serialize/index/jsonarray.ts +51 -0
  54. package/assembly/serialize/index/object.ts +39 -14
  55. package/assembly/serialize/index/string.ts +1 -2
  56. package/assembly/serialize/index/typedarray.ts +1 -2
  57. package/assembly/serialize/index.ts +1 -0
  58. package/assembly/serialize/naive/array.ts +23 -34
  59. package/assembly/serialize/naive/bool.ts +0 -1
  60. package/assembly/serialize/naive/float.ts +16 -25
  61. package/assembly/serialize/naive/integer.ts +1 -5
  62. package/assembly/serialize/naive/raw.ts +1 -2
  63. package/assembly/serialize/naive/set.ts +0 -4
  64. package/assembly/serialize/naive/staticarray.ts +0 -5
  65. package/assembly/serialize/naive/string.ts +11 -7
  66. package/assembly/serialize/naive/typedarray.ts +0 -6
  67. package/assembly/serialize/simd/string.ts +1 -3
  68. package/assembly/serialize/swar/string.ts +2 -4
  69. package/assembly/util/atoi-fast.ts +4 -14
  70. package/assembly/util/atoi.ts +1 -2
  71. package/assembly/util/bytes.ts +1 -2
  72. package/assembly/util/idofd.ts +1 -2
  73. package/assembly/util/isSpace.ts +1 -2
  74. package/assembly/util/itoa-fast.ts +9 -15
  75. package/assembly/util/nextPowerOf2.ts +1 -2
  76. package/assembly/util/parsefloat-fast.ts +4 -7
  77. package/assembly/util/ptrToStr.ts +1 -2
  78. package/assembly/util/scanValueEnd.ts +1 -2
  79. package/assembly/util/scanValueEndSimd.ts +198 -0
  80. package/assembly/util/scanValueEndSwar.ts +184 -0
  81. package/assembly/util/scientific.ts +8 -14
  82. package/assembly/util/simd-int.ts +4 -8
  83. package/assembly/util/snp.ts +2 -7
  84. package/assembly/util/stringScan.ts +2 -4
  85. package/assembly/util/swar-int.ts +8 -16
  86. package/assembly/util/swar.ts +2 -4
  87. package/lib/as-bs.ts +57 -42
  88. package/package.json +27 -10
  89. package/transform/lib/builder.d.ts +0 -1
  90. package/transform/lib/builder.js +0 -1
  91. package/transform/lib/index.d.ts +0 -1
  92. package/transform/lib/index.js +617 -326
  93. package/transform/lib/linkers/alias.d.ts +0 -1
  94. package/transform/lib/linkers/alias.js +0 -1
  95. package/transform/lib/linkers/custom.d.ts +0 -1
  96. package/transform/lib/linkers/custom.js +0 -1
  97. package/transform/lib/linkers/imports.d.ts +0 -1
  98. package/transform/lib/linkers/imports.js +0 -1
  99. package/transform/lib/types.d.ts +4 -2
  100. package/transform/lib/types.js +5 -1
  101. package/transform/lib/util.d.ts +0 -1
  102. package/transform/lib/util.js +0 -1
  103. package/transform/lib/visitor.d.ts +0 -1
  104. package/transform/lib/visitor.js +0 -1
  105. package/assembly/util/dragonbox-cache.ts +0 -445
  106. package/assembly/util/dragonbox.ts +0 -660
  107. package/transform/lib/builder.d.ts.map +0 -1
  108. package/transform/lib/builder.js.map +0 -1
  109. package/transform/lib/index.d.ts.map +0 -1
  110. package/transform/lib/index.js.map +0 -1
  111. package/transform/lib/linkers/alias.d.ts.map +0 -1
  112. package/transform/lib/linkers/alias.js.map +0 -1
  113. package/transform/lib/linkers/custom.d.ts.map +0 -1
  114. package/transform/lib/linkers/custom.js.map +0 -1
  115. package/transform/lib/linkers/imports.d.ts.map +0 -1
  116. package/transform/lib/linkers/imports.js.map +0 -1
  117. package/transform/lib/types.d.ts.map +0 -1
  118. package/transform/lib/types.js.map +0 -1
  119. package/transform/lib/util.d.ts.map +0 -1
  120. package/transform/lib/util.js.map +0 -1
  121. package/transform/lib/visitor.d.ts.map +0 -1
  122. package/transform/lib/visitor.js.map +0 -1
package/CHANGELOG.md CHANGED
@@ -1,38 +1,80 @@
1
1
  # Changelog
2
2
 
3
+ ## Unreleased
4
+
5
+ ## 2026-06-10 - v1.5.0
6
+
7
+ - perf(dynamic): **`JSON.Obj`, `JSON.Value`, and `JSON.Arr` are now lazy by default** - a near-alloc-less, simdjson On-Demand-style rework of dynamic parsing. `JSON.parse<JSON.Obj>` no longer eagerly materializes the whole tree: each nested value stores its raw source slice and is parsed only on first access (`.get<T>()` / `.getAs<T>()` / `.at(i)`), then cached - a value you never read is never parsed, and an untouched value re-serializes by copying its original source bytes verbatim. `JSON.Obj` is backed by a `StaticArray<u64>` value-slot buffer plus a length-prefixed key buffer (keys emit straight from their slice - no per-key string materialization), and a new buffer-backed `JSON.Arr` mirrors it (`.at(i)` → `JSON.Value`, `.getAs<T>(i)`, `.push<T>`, `.set<T>`, `.length`). Deferred composites reuse the NaN-boxed `JSON.Value` slot, and the SIMD/SWAR value scanners gained a vectorized composite (`{}`/`[]`) scan. Net for proxy / filter / forward workloads over large payloads: dynamic deserialize is several× faster with far fewer allocations, and untouched round-trips are byte-exact passthrough. A new `dynamic-interop` suite covers `Map`, `Date`, `JSON.Box`, `JSON.Raw`, and nested-struct interop
8
+ - perf(`JSON.Obj`): **faster dynamic key access.** `indexOf` now linear-scans objects with ≤ 6 keys (the common case) instead of allocating and hashing a key index - small objects pay no index build/probe cost, and a dynamic-access workload that touches only a few keys never builds an index it won't reuse; larger objects keep the open-addressed hash index. The per-lookup key comparison (`utf16Equals`, shared by the linear scan and the hash probe) is widened to 8 code units per step on the SIMD build (one `v128`) and 4 per step on naive/swar (one `u64`), each load bounded by the key length so it never over-reads. Warm long-key lookups **+37–133%**, short keys **+10–20%**, no cold-path change
9
+ - perf(swar): the string-field clean pre-scan (`deserializeStringField_SWAR`) tests its first 8-byte word before loading the second - short values and keys close or escape within that first word, so the common case skips the wasted load while a clean run still advances 16 bytes at a time (**+1.5%** on string-heavy eager deserialize)
10
+ - fix(dynamic): hardened the buffer-backed dynamic types against three latent faults found while validating the rework - (1) the open-addressed key index could fill to 100% and spin forever in `indexOf` (a 3rd `set` on a fresh object was enough); it's now invalidated past half load with a wrap-guarded probe. (2) `JSON.Obj.__visit` didn't trace the key-offset array (`_kpos`), so a GC between parse and access freed it and faulted on the next lookup. (3) `__INITIALIZE` seeded a ref lazy field's slot to the materialized state but not its value, so an _absent_ field with a declared default serialized `null as T` and trapped (hit by `github_events` / `gsoc-2018` lazy serialize). Also fixed the SWAR value scanner mis-reading an escaped backslash before a closing quote (`"x\\"`) as still-escaped, which over-scanned past the value and faulted
11
+ - test(dynamic): three robustness suites for the buffer-backed types, each verified to catch its bug class by reverting the fix - `gc-stress` (forces `__collect()` between parse and access across `JSON.Obj`/`Arr`/`Value`, large values, and nested peels; catches an untraced backing buffer), `objindex-property` (random set/delete/lookup vs a reference `Map` straddling the linear↔hash threshold; catches the index spin-loop), and `roundtrip-fuzz` (random valid JSON → `parse<JSON.Value>` → `stringify` must be byte-exact across all three scanners; caught the SWAR escape bug on its first run)
12
+ - feat: **`@optional` decorator** - annotate a field as optional for intent and tooling (documented in the README)
13
+ - deps: float serialization backend swapped to **`xjb-as`** (replaces the previous `zmij-as` dependency). The `dtoa_buffered` / `ftoa_buffered` writer API is byte-identical - UTF-16 written straight into the `bs` buffer, returning the code-unit count - so the swap is a drop-in across the value (`assembly/index.ts`), scalar (`serialize/naive/float.ts`), and array (`serialize/naive/array.ts`) serialize paths; `xjb-as` is the productized successor of the same xjb64 / Schubfach shortest-decimal core. Verified across the full suite (10,767 tests, all three modes) plus a `canada.json` round-trip (`JSON.stringify(JSON.parse(canada)) === canada.min.json`, byte-for-byte) on both the SIMD and SWAR digit kernels
14
+ - bench: object throughput gains **eager-vs-lazy** coverage. `assembly/__benches__/throughput/obj-{serialize,deserialize}.lazy.bench.ts` mirror the eager sweeps with `@json({ lazy: "auto" })` classes (dumped as `obj-lazy-*`): lazy deserialize measures the scan-and-record-ranges fast path (deferred fields never read), lazy serialize measures the raw-passthrough path (parse once, then re-serialize untouched). Wired into `scripts/build-chart{09,10}.ts` as a dashed AS-only series (JS has no lazy mode), and into the per-payload bars of `scripts/build-chart{01,02}.ts` as a distinct faded-copper bar at 0.8 fill opacity to flag that lazy defers work (not strictly apples-to-apples; only the 5 payloads with a `*.lazy.bench.ts` get a bar)
15
+ - bench: sub-1MB throughput sweeps step every **100 kB** instead of 50 kB (1 kB baseline + 100–900 kB + 1–10 MB) - half the sub-1MB points, same coverage shape. Applied across the AS benches (`assembly/__benches__/throughput/{str,obj}-{serialize,deserialize}.bench.ts`), their JS counterparts (`bench/throughput/`), and the chart payload lists (`scripts/build-chart{07,08,09,10}.ts`)
16
+ - fix(bench): restore the lazy **access-pattern** benchmark dropped when `lazy.bench.ts` was split into per-concern files. `assembly/__benches__/lazy/access-pattern.bench.ts` re-emits the `lz-access` suite (eager read-all baseline + lazy read-none / read-one / read-all / passthrough on a medium struct) that `scripts/build-chart15.ts` reads for `lazy-access-pattern.svg` - the chart had been aborting with ENOENT on `lz-access.eager`
17
+ - tooling: `bun run playground:tmp` builds and runs the transform-generated `assembly/playground.tmp.ts` directly (no transform) under the v8 bench runner, for hand-tuning the generated codec. `assembly/playground.ts` is now a fast-path (non-lazy, eager) deserialize micro-bench - a direct `__DESERIALIZE_FAST` into a reused object with a min-over-rounds timer
18
+ - tooling: `npm run bench:all` (`scripts/bench-all.sh`) runs the full benchmark matrix in one shot - root files plus `multilib/`, `throughput/`, `prim/` (AS + JS) and `classic/`, `lazy/` (AS-only) - forwarding flags (`--mode`, `--v8`, `--wavm`) to the AS runner and continuing past a failing category (non-zero exit if any failed). `npm run charts:publish` (`scripts/publish-benchmarks.sh`) now publishes by default even with a dirty/untracked working tree - chart output only ever commits to a separate `docs` worktree, never your main tree - with `PUBLISH_REQUIRE_CLEAN=1` to restore the old refuse-if-dirty guard
19
+ - fix(lazy/dynamic): a `null` value for a **nullable string** field on the lazy/dynamic path was mis-decoded. `JSON.__deserialize` (used by lazy-field materialization, `JSON.parse<JSON.Value>`, and `JSON.Obj`/`Arr` value slots) tested `isString<T>()` - which is `true` for `string | null` - before the `null`-literal check, so a bare `null` was parsed as a quoted string: an abort (`Invalid JSON string: missing surrounding quotes`) under NAIVE, silent garbage under SWAR/SIMD. The `null` check now precedes the string branch, matching the eager `parseInternal` path. Surfaced as a crash materializing absent-as-`null` string fields in the `classic/citm_catalog.lazy` benchmark
20
+ - perf(dynamic): **re-serializing a dynamic string emits via `memory.copy` when it needs no escaping.** A materialized `JSON.Value` / `JSON.Obj` / `JSON.Arr` string caches a 2-bit escape class in two otherwise-unused bits of its NaN-box payload (a wasm32 pointer is 32 of the 45 payload bits); it's classified once on first serialize, then reused - AS strings are immutable, so the verdict never goes stale. A clean string then round-trips as `"` + one `memory.copy` + `"` instead of a per-character escape scan: **~3×** faster re-serialize (3.8 → 11.5 GB/s on a 1 MB string). `JSON.Obj`/`Arr` persist the class back into their flat value slot so the win carries across re-serializes; typed struct `string` fields (no box to cache in) are unchanged
21
+ - perf(dynamic): rebalanced the lazy value-slot packing from **22/22 to 23/21 bits** (offset/length). Object/array fields are usually small while the document can be large, so offset overflow - a field late in a big doc - is the realistic trigger; widening the offset field lifts the compact (no-rescan) range from ~8 MB to **~16 MB** of source, at the cost of single field values over ~4 MB falling back to the existing absolute (scan-on-demand) slot form
22
+ - test(dynamic): `dynamic-string-class` (clean / escaped / surrogate / empty strings round-tripped through `JSON.Value` and re-serialized, plus `JSON.Obj`/`Arr` slot-class caching) and `lazy-slot-encoding` (the packed slot's compact↔absolute boundary, previously untested) suites
23
+ - bench/docs: benchmark chart scripts renamed from `chartNN` to descriptive names (`overview-` / `string-` / `object-` / `primitive-` / `library-{serialize,deserialize}`); every chart now emits both SVG and PNG; the README switched fully to SVG with the real-world payload charts promoted to the top of the Performance section and a "browse the full chart set" link that the publish script re-pins per release. String throughput charts gained a `JSON.Value` (dynamic) series, and the classic charts got vertical value labels
24
+
25
+ ## 2026-06-05 - v1.4.0
26
+
27
+ - feat(lazy-fields): **on-demand (lazy) field parsing.** Mark a field `@lazy`, wrap its type as `JSON.Lazy<T>`, or set a class-wide default with `@json({ lazy: "auto" | "all" })` (and opt individual fields back out with `@eager`). A lazy field stores its raw JSON slice at parse time and parses it into the field's type only on first access (then caches); a field you never read is never parsed, and an untouched field round-trips by copying its original source bytes - never re-parsed or re-serialized. `auto` defers the expensive-to-parse fields (strings, arrays, maps/sets, `JSON.Value`/`Obj`/`Raw`, non-trivial nested structs) and keeps cheap ones eager (primitives, enums, `Date`, tiny all-scalar structs); `all` defers every field (best for proxy / filter / forward over large payloads). Skipping or forwarding fields is several× faster than eager and the win grows with payload size. The slot is a single `u64` - `0` = absent, `u64.MAX_VALUE` = materialized, otherwise a packed `(start<<32)|end` source range; a ≤32-bit scalar packs its value into the slot directly (no traced memo field). `@omitnull` / `@omitif` work on lazy fields (null-ness is read from the slot without materializing). A class with a custom `@serializer`/`@deserializer` can't have lazy fields (the transform reports an error). See the Lazy Fields guide for the full API and trade-offs
28
+ - perf(transform): the generated fast-path deserializer (`__DESERIALIZE_FAST`) is split into ~32-field chunk helper methods instead of one unrolled function - for both the straight-line tiers and the `seenAny`-stateful `@omitnull`/`@omitif` tiers (the latter threads `seenAny` across chunk boundaries through the call ABI, packed into the `u64` return alongside `srcStart`). A wide struct previously emitted a single function large enough to crash the Binaryen optimizer ("crashed during optimize") a few hundred fields in; chunked, the fast path stays compilable at any width (verified to 1000 fields) and optimizes faster, since Binaryen's per-function passes are superlinear
29
+ - perf(codegen): dropped `@inline` from the (de)serialize worker functions and removed every `inline.always()` call-site directive. They forced each worker to be copied into every dispatch / call site; as plain shared calls the generated module shrinks (~18% on the all-lazy `large` struct) and the Binaryen `optimize` phase eases. Dispatchers and genuinely small helpers keep `@inline`
30
+ - fix(lazy-fields): the lazy serialize path now `ensureSize`s before the structural key literals, not just the value passthrough. A wide struct (~150 fields, long keys) could overrun the output buffer once a passthrough left it nearly full - `JSON.stringify(JSON.parse<T>(...))` trapped with `unreachable`. The passthrough reserves exactly the slice bytes (unlike eager string serialize, which over-reserves for quotes/escaping), so the key writes that follow needed their own bounds check
31
+ - fix(lazy-fields): a *constructed* (not parsed) instance keeps its declared field defaults. Lazy lowering replaced each field with a slot and dropped its initializer, so `new T()` serialized the type default (`null` / `0`) instead of e.g. `"x"` from `name: string = "x"`; the slot is now seeded in the materialized-default state. `JSON.parse` (which builds via `__new`, skipping field initializers) is unaffected
32
+ - fix(serialize): `@omitnull` / `@omitif` fields emit a **leading** comma gated on a runtime "wrote" flag, not a trailing one. Optional fields sort to the front, so a present field followed by omitted ones - or an all-optional struct - left a dangling trailing comma, i.e. invalid JSON like `{"a":"1",}`. Added a regression test (first / last / gap / all-absent / all-present, plus the mixed-with-regular case)
33
+ - feat: `JSON.Value` moved to a NaN-boxed representation, and its value scanning now runs through the SWAR / SIMD scanners (~60% faster on value-heavy input)
34
+ - bench: committed eager-vs-lazy benchmarks under `assembly/__benches__/lazy/` (deserialize / round-trip / serialize) + `lazy:"auto"` variants of small/medium/large/token/vec3, and a `lazy` json-as struct in the multi-library comparison (`__benches__/multilib/`); charting in `scripts/build-chart15.ts` and the multilib charts
35
+ - docs: Lazy Fields guide + performance charts (eager-vs-lazy by payload size and a multi-library throughput comparison), full JSDoc for the decorators in `assembly/index.d.ts`, and a single shared chart colour palette (`scripts/lib/palette.ts`)
36
+
37
+ - feat: `JSON.parse<T>(data, out)` - deserialize into an existing object graph instead of allocating a fresh one, symmetric with the existing `JSON.stringify<T>(data, out)`. On the fast path every field is reused in place (nested structs threaded as `dst`, strings `__renew`d only when the byte length changes, arrays keeping capacity), so a steady-state re-parse of the same shape allocates **nothing** after the first call - ~4× on SWAR (~720k → ~2.8M ops/s) and ~4.5× on SIMD on the multilib struct payload, heap `totalDelta` 1,034,240 B → 0. Backward compatible: `out` defaults to a type-correct zero via `__zero<T>()` (branches on `isReference`/`isManaged` at compile time, since a bare `changetype<T>(0)` is a size mismatch for value types like `bool`/`f64`)
38
+ - feat: `JSON.stringify<T>(data, out)` now reuses `out` for composite values (structs, arrays, strings), not just the scalar/`Date` fast paths. The reference serialize branches route their final write through a new `bs.outTo<T>(target)` that overwrites the existing string in place - or `__renew`s when the length differs - instead of `__new`-ing a fresh one via `bs.out`. Zero-alloc serialize on reuse: SWAR ~2.31M → ~2.61M ops/s (~+13%), heap `totalDelta` 882,000 B → 0
39
+ - perf: the fast path (`__DESERIALIZE_FAST`) is now generated for **NAIVE** mode. It was disabled for naive (`requestedFastPath = USE_FAST_PATH && codegenMode !== JSONMode.NAIVE`), so naive always ran `__DESERIALIZE_SLOW` after `__INITIALIZE` had reallocated every field - which meant `parse(data, out)` reuse reallocated the whole graph in naive. Enabling it is purely additive (the slow path remains the fallback for whitespace / reordered / missing-key input): naive fresh parse ~2× (~314k → ~644k ops/s), naive reuse ~6.7× (~316k → ~2.1M) with nested structs/arrays/strings now reused in place. Main suite (10,590) and RFC suite unchanged across all three modes
40
+ - perf: escape-free string **fields** skip the scratch-buffer round-trip in NAIVE. `deserializeStringField_NAIVE` previously copied every char into `bs` then `bs`→field; it now scans for the closing quote without touching `bs` and copies source→field directly when no backslash is present (matching the SWAR/SIMD field paths), diverting to a `bs`-based decode tail only for escaped strings. ~+6% naive fresh parse; SWAR/SIMD already did this
41
+ - fix(build): drop `@inline` from `deserializeStringField_NAIVE`. Once the fast path was enabled for naive (above), this loop-bearing scanner got inlined into every string-field call site inside the `@inline __DESERIALIZE_FAST`, exploding binaryen's optimize phase - `bun run bench:as -- large --mode naive` went to ~118 s in `optimize` alone (the transform itself is ~23 ms). Kept as a single shared function - one call per field - matching the already-non-inline SWAR/SIMD field deserializers. naive `large` build **120.6 s → 8.6 s** asc (full repro 2:17 → ~13.5 s), and it's also ~12% *faster* at runtime (the bloated inline body optimized worse). `__DESERIALIZE_FAST` itself stays `@inline`
42
+ - test: `assembly/__tests__/parseinto.spec.ts` - reuse correctness across all three modes: `parse(data, out)` fully overwrites the target (with pointer-identity asserts that nested structs and arrays are reused, not reallocated), and `stringify(data, out)` reuses the output string in place (same length) and resizes correctly (different length)
43
+ - bench: `json-as-struct-{deserialize,stringify}-reuse.bench.ts` under `__benches__/multilib/` - reuse-path counterparts to the fresh benches, used with `--memory` to confirm the zero-allocation steady state
44
+
3
45
  ## 2026-06-01 - v1.3.9
4
46
 
5
47
  - fix: remove readFileSync which failed under virtual file systems
6
48
 
7
49
  ## 2026-06-01 - v1.3.8
8
50
 
9
- - feat(strict): the **naive value-path deserializers now reject malformed JSON** instead of silently accepting it (RFC 8259). Numbers (`deserializeFloat_NAIVE`) are validated against the JSON number grammar no leading zeros, bare `-`, empty fraction/exponent, `+` sign, hex, or trailing garbage (the `NaN`/`Infinity` extension is preserved). Strings (`deserializeString_NAIVE`) reject unescaped control chars, illegal/incomplete escapes, non-hex `\u`, and missing surrounding quotes. The scalar/typed **array scanners** (`f64[]`/`i*[]`/`u*[]`/`bool[]`/`string[]`) were rewritten as strict state machines: `[`-framed, single-comma separated, no leading/trailing/doubled commas, no inter-value garbage. On the JSONTestSuite `n_` corpus (parsed against concrete types, one spec per case in `assembly/__tests__/rfc/`), naive now **rejects 179/188** reject-cases up from 55 the remaining 9 being the intentional `NaN`/`Infinity` extension (5) plus arbitrary-`JSON.Value`/formfeed/struct-trailing edges (4). Non-object rejects compile to uncatchable aborts until try-as traces the value path; object rejects (struct `__DESERIALIZE_SLOW`) are catchable today. Main suite (10,557) and curated rfc suite (801) stay green across all three modes
10
- - feat(strict): under `JSON_STRICT=true` the generated `__DESERIALIZE_SLOW` object scanner now rejects malformed key positions an unquoted / single-quoted / numeric key, or garbage after a value with no separating comma by requiring the first non-space char at each key position to be a string-opening quote, `,`, or `}` (`transform/src/index.ts`). Combined with the existing strict unknown-key-by-value-type guards, this brings RFC 8259 `n_object` coverage to **28/28** (all three modes) in the conformance suite (`assembly/__tests__/rfc/struct-reject.spec.ts`, parsed against one rich all-value-type schema so every guard is generated). Gated on `JSON_STRICT`, so the default lenient build is unchanged
11
- - test: RFC 8259 accept-side coverage is now complete all **95** `y_` (must-accept) cases parse via the dynamic `JSON.Value` type (`assembly/__tests__/rfc/accept.spec.ts`) and **31/35** `i_` (impl-defined) cases (`impl.spec.ts`), all green across NAIVE/SWAR/SIMD. The 4 deferred `i_` cases are uncatchable traps (raw UTF-16/BOM byte input, depth-500 nesting). RFC suite now 804 tests across 3 modes
12
- - fix: tier-1 (the exact byte-template fast path) over-matched whitespace after a colon. For input like `{"k": v}` (a space after the colon but otherwise compact e.g. Python's `json.dumps` default), tier-1 matched the minified `{"k":` prefix then read the value at a fixed offset that the space shifted, feeding a misaligned pointer to the value deserializer string fields `abort`ed ("Expected leading quote") and float fields hit `unreachable`. Tier-1 now checks for whitespace after the colon and bails to the (whitespace-tolerant) tier-2 instead of mis-reading. Surfaced via the RFC 8259 conformance work
13
- - fix: add `bs.reset()` to `lib/as-bs.ts` restores the shared serialization buffer to a clean state (offset + pause stacks) after a throw aborts a (de)serialize mid-flight, so the next op isn't corrupted by a dangling offset
51
+ - feat(strict): the **naive value-path deserializers now reject malformed JSON** instead of silently accepting it (RFC 8259). Numbers (`deserializeFloat_NAIVE`) are validated against the JSON number grammar - no leading zeros, bare `-`, empty fraction/exponent, `+` sign, hex, or trailing garbage (the `NaN`/`Infinity` extension is preserved). Strings (`deserializeString_NAIVE`) reject unescaped control chars, illegal/incomplete escapes, non-hex `\u`, and missing surrounding quotes. The scalar/typed **array scanners** (`f64[]`/`i*[]`/`u*[]`/`bool[]`/`string[]`) were rewritten as strict state machines: `[`-framed, single-comma separated, no leading/trailing/doubled commas, no inter-value garbage. On the JSONTestSuite `n_` corpus (parsed against concrete types, one spec per case in `assembly/__tests__/rfc/`), naive now **rejects 179/188** reject-cases - up from 55 - the remaining 9 being the intentional `NaN`/`Infinity` extension (5) plus arbitrary-`JSON.Value`/formfeed/struct-trailing edges (4). Non-object rejects compile to uncatchable aborts until try-as traces the value path; object rejects (struct `__DESERIALIZE_SLOW`) are catchable today. Main suite (10,557) and curated rfc suite (801) stay green across all three modes
52
+ - feat(strict): under `JSON_STRICT=true` the generated `__DESERIALIZE_SLOW` object scanner now rejects malformed key positions - an unquoted / single-quoted / numeric key, or garbage after a value with no separating comma - by requiring the first non-space char at each key position to be a string-opening quote, `,`, or `}` (`transform/src/index.ts`). Combined with the existing strict unknown-key-by-value-type guards, this brings RFC 8259 `n_object` coverage to **28/28** (all three modes) in the conformance suite (`assembly/__tests__/rfc/struct-reject.spec.ts`, parsed against one rich all-value-type schema so every guard is generated). Gated on `JSON_STRICT`, so the default lenient build is unchanged
53
+ - test: RFC 8259 accept-side coverage is now complete - all **95** `y_` (must-accept) cases parse via the dynamic `JSON.Value` type (`assembly/__tests__/rfc/accept.spec.ts`) and **31/35** `i_` (impl-defined) cases (`impl.spec.ts`), all green across NAIVE/SWAR/SIMD. The 4 deferred `i_` cases are uncatchable traps (raw UTF-16/BOM byte input, depth-500 nesting). RFC suite now 804 tests across 3 modes
54
+ - fix: tier-1 (the exact byte-template fast path) over-matched whitespace after a colon. For input like `{"k": v}` (a space after the colon but otherwise compact - e.g. Python's `json.dumps` default), tier-1 matched the minified `{"k":` prefix then read the value at a fixed offset that the space shifted, feeding a misaligned pointer to the value deserializer - string fields `abort`ed ("Expected leading quote") and float fields hit `unreachable`. Tier-1 now checks for whitespace after the colon and bails to the (whitespace-tolerant) tier-2 instead of mis-reading. Surfaced via the RFC 8259 conformance work
55
+ - fix: add `bs.reset()` to `lib/as-bs.ts` - restores the shared serialization buffer to a clean state (offset + pause stacks) after a throw aborts a (de)serialize mid-flight, so the next op isn't corrupted by a dangling offset
14
56
  - test: begin RFC 8259 conformance coverage from nst/JSONTestSuite as typed specs (`assembly/__tests__/rfc-object.spec.ts`, accept/round-trip cases; see `RFC-DEFERRED.md` for status and the reject-case blocker)
15
57
 
16
- - perf: pretty-printed JSON now stays on the fast path instead of collapsing to the naive scalar deserializer (the "canada 10×"). The transform's `__DESERIALIZE_FAST` gained a whitespace-tolerant **tier 2**: when the exact packed-`u64` byte-template (tier 1) misses on inter-token whitespace, instead of dumping the whole object to `__DESERIALIZE_SLOW` (~5× slower) it re-matches per field using the same packed key constants but skips whitespace between every structural token (`{`, key, `:`, value, `,`, `}`). Minified input never reaches tier 2, so tier 1 keeps peak speed. canada *pretty* deserialize **~139 → ~1,180 MB/s (SWAR)** / **~1,150 MB/s (SIMD)** on par with minified; on a value-heavy struct tier 2 costs ~1% over tier 1 (worst case ~2× on a 16-field all-primitive struct). The SWAR array bodies (`deserializeArrayArrayBody`, `deserializeFloatArrayBody`, the inline string/object array element loops) were made whitespace-tolerant to match
17
- - fix: `JSON.parse` / `JSON.__deserialize` accepted a fast-path result only when it consumed *exactly* to `srcEnd`. Pretty files end in a newline, so tier 2 stopped just past the closing `}` and the **entire object silently fell back to the slow path despite a successful parse** this was the real cause of the pretty penalty (tier 2 alone only moved canada 139 → 220 MB/s; this fix took 220 → ~1,180). The fast path is now accepted when only trailing whitespace remains (`skipWhitespace(fastEnd, srcEnd) == srcEnd`)
18
- - perf: optional-field structs (`@omitnull` / `@omitif`) get a whitespace-tolerant tier 2 as well a probe-and-commit variant that matches each field's key with a lookahead cursor and only commits past the separator + key + `:` when it matches, so omitted fields are skipped without consuming input. Previously these structs fell to slow on *any* whitespace. ~1.18× of tier-1 on a pretty 7-field optional struct, vs the ~5× slow path it used to hit
58
+ - perf: pretty-printed JSON now stays on the fast path instead of collapsing to the naive scalar deserializer (the "canada 10×"). The transform's `__DESERIALIZE_FAST` gained a whitespace-tolerant **tier 2**: when the exact packed-`u64` byte-template (tier 1) misses on inter-token whitespace, instead of dumping the whole object to `__DESERIALIZE_SLOW` (~5× slower) it re-matches per field using the same packed key constants but skips whitespace between every structural token (`{`, key, `:`, value, `,`, `}`). Minified input never reaches tier 2, so tier 1 keeps peak speed. canada *pretty* deserialize **~139 → ~1,180 MB/s (SWAR)** / **~1,150 MB/s (SIMD)** - on par with minified; on a value-heavy struct tier 2 costs ~1% over tier 1 (worst case ~2× on a 16-field all-primitive struct). The SWAR array bodies (`deserializeArrayArrayBody`, `deserializeFloatArrayBody`, the inline string/object array element loops) were made whitespace-tolerant to match
59
+ - fix: `JSON.parse` / `JSON.__deserialize` accepted a fast-path result only when it consumed *exactly* to `srcEnd`. Pretty files end in a newline, so tier 2 stopped just past the closing `}` and the **entire object silently fell back to the slow path despite a successful parse** - this was the real cause of the pretty penalty (tier 2 alone only moved canada 139 → 220 MB/s; this fix took 220 → ~1,180). The fast path is now accepted when only trailing whitespace remains (`skipWhitespace(fastEnd, srcEnd) == srcEnd`)
60
+ - perf: optional-field structs (`@omitnull` / `@omitif`) get a whitespace-tolerant tier 2 as well - a probe-and-commit variant that matches each field's key with a lookahead cursor and only commits past the separator + key + `:` when it matches, so omitted fields are skipped without consuming input. Previously these structs fell to slow on *any* whitespace. ~1.18× of tier-1 on a pretty 7-field optional struct, vs the ~5× slow path it used to hit
19
61
  - fix: enforce a uniform whitespace contract across **every** deserialize handler in all three modes (field and non-field): the entry points (`JSON.parse` / `JSON.__deserialize`) skip leading whitespace once, and every handler now assumes `srcStart` points at the first non-whitespace char and never re-skips it. Removed ~17 now-redundant leading-whitespace trims from composite handlers (naive `object`/`map`/`set` + array/staticarray composites; SWAR array bodies); composites still skip whitespace *internally* since they are the caller for their child values/keys
20
- - fix: Map and Set struct **fields** did not handle internal whitespace `deserializeMapBody` had no inter-token skips at all, and `deserializeSetDirect`'s skips were gated behind an `allowWhitespace` flag the field path left `false`. Concrete structs with `Map`/`Set` fields parsed from pretty input silently misparsed (only the generic-box slow path that existing tests exercised handled it). Both now skip whitespace unconditionally
21
- - fix: `scanValueEnd` (both `util/scanValueEnd.ts` and `swar/array/shared.ts`) now stops a scalar value at trailing whitespace, not just `,`/`]`/`}`. The returned range previously included trailing spaces (e.g. `"1 "`), which the SWAR/SIMD scalar number parsers assuming `[srcStart,srcEnd)` is the exact value misparsed (a whitespaced map value `{ "p" : 1 }` deserialized to `-6`). Now matches the already-correct `JSON.Util.scanValueEnd`
22
- - bench: add `assembly/__benches__/custom/tier-h2h.bench.ts` tier-1 (minified) vs tier-2 (one leading space, isolating pure path overhead) vs tier-2 (fully pretty), across a value-heavy `medium` struct, a 16-field all-primitive worst case, and an optional-field struct (with a non-optional twin to isolate the `seenAny` tier-1 overhead)
23
- - test: add concrete-struct whitespace coverage to `whitespace.spec.ts` tier-2 fast-path structs (nested objects, float/string/object arrays), the optional-field probe path, top-level leading whitespace for every scalar/array type, and one struct touching every field-handler family (scalar/string/nested-object/array/Map/Set) parsed from heavily-whitespaced (leading + internal + trailing) input
24
- - perf: fully vectorize the SIMD string **field** deserializer. `deserializeStringField_SIMD` previously bailed to `deserializeStringField_SWAR` on the first backslash, so SIMD mode got zero vectorization on escaped struct fields. It now runs a HYBRID escaped scanner v128 scan for `"`/`\`; escape-bearing blocks use a single whole-block v128 store to copy the plain prefix for free, then decode the escape; clean runs stream the first block then bulk-`memcpy` the remainder (bandwidth-optimal on long sparse runs, no large-input cliff). Validated against run-copy and pure-stream variants across escape densities; ~+10–20% on common small/moderate/sparse cases
25
- - perf: replace the SWAR string **field** scanner (formerly the run-copy `deserializeEscapedStringScan_SWAR_SplitTuned`) with the same HYBRID strategy, renamed `deserializeEscapedStringField_SWAR`. On the `swar-string-deser-hybrid-h2h` bench: +50–70% dense, +17–22% moderate, +37–48% sparse the prior run-copy scanner read each plain run twice (scan + `memcpy`)
26
- - perf/fix: the standalone whole-value scanners (`deserializeString_SWAR` / `deserializeString_SIMD`) also move to the HYBRID strategy, replacing the older "overflow-pattern" SIMD code. Also fixes a latent out-of-bounds read on short escaped strings the unguarded `srcEnd - 16` underflow is now guarded with `srcEnd >= 16`
62
+ - fix: Map and Set struct **fields** did not handle internal whitespace - `deserializeMapBody` had no inter-token skips at all, and `deserializeSetDirect`'s skips were gated behind an `allowWhitespace` flag the field path left `false`. Concrete structs with `Map`/`Set` fields parsed from pretty input silently misparsed (only the generic-box slow path that existing tests exercised handled it). Both now skip whitespace unconditionally
63
+ - fix: `scanValueEnd` (both `util/scanValueEnd.ts` and `swar/array/shared.ts`) now stops a scalar value at trailing whitespace, not just `,`/`]`/`}`. The returned range previously included trailing spaces (e.g. `"1 "`), which the SWAR/SIMD scalar number parsers - assuming `[srcStart,srcEnd)` is the exact value - misparsed (a whitespaced map value `{ "p" : 1 }` deserialized to `-6`). Now matches the already-correct `JSON.Util.scanValueEnd`
64
+ - bench: add `assembly/__benches__/custom/tier-h2h.bench.ts` - tier-1 (minified) vs tier-2 (one leading space, isolating pure path overhead) vs tier-2 (fully pretty), across a value-heavy `medium` struct, a 16-field all-primitive worst case, and an optional-field struct (with a non-optional twin to isolate the `seenAny` tier-1 overhead)
65
+ - test: add concrete-struct whitespace coverage to `whitespace.spec.ts` - tier-2 fast-path structs (nested objects, float/string/object arrays), the optional-field probe path, top-level leading whitespace for every scalar/array type, and one struct touching every field-handler family (scalar/string/nested-object/array/Map/Set) parsed from heavily-whitespaced (leading + internal + trailing) input
66
+ - perf: fully vectorize the SIMD string **field** deserializer. `deserializeStringField_SIMD` previously bailed to `deserializeStringField_SWAR` on the first backslash, so SIMD mode got zero vectorization on escaped struct fields. It now runs a HYBRID escaped scanner - v128 scan for `"`/`\`; escape-bearing blocks use a single whole-block v128 store to copy the plain prefix for free, then decode the escape; clean runs stream the first block then bulk-`memcpy` the remainder (bandwidth-optimal on long sparse runs, no large-input cliff). Validated against run-copy and pure-stream variants across escape densities; ~+10–20% on common small/moderate/sparse cases
67
+ - perf: replace the SWAR string **field** scanner (formerly the run-copy `deserializeEscapedStringScan_SWAR_SplitTuned`) with the same HYBRID strategy, renamed `deserializeEscapedStringField_SWAR`. On the `swar-string-deser-hybrid-h2h` bench: +50–70% dense, +17–22% moderate, +37–48% sparse - the prior run-copy scanner read each plain run twice (scan + `memcpy`)
68
+ - perf/fix: the standalone whole-value scanners (`deserializeString_SWAR` / `deserializeString_SIMD`) also move to the HYBRID strategy, replacing the older "overflow-pattern" SIMD code. Also fixes a latent out-of-bounds read on short escaped strings - the unguarded `srcEnd - 16` underflow is now guarded with `srcEnd >= 16`
27
69
  - refactor: route string-field deserialization through the `deserialize/index/string.ts` dispatcher (`deserializeStringField`), matching the integer/unsigned/float field helpers, instead of the transform hard-coding `deserializeStringField_SWAR` / `_SIMD` by `codegenMode`. Adds a real `deserializeStringField_NAIVE` (NAIVE builds previously deserialized struct string fields with the SWAR field scanner) and unifies all three mode variants on the 3-param `(srcStart, srcEnd, dstFieldPtr)` signature
28
70
  - refactor: rename the `simple/` (de)serialize implementation directories to `naive/` and carry the mode suffix on the source functions (`*_NAIVE` / `*_SWAR` / `*_SIMD`), eliminating the `as X_NAIVE` import aliases at call sites
29
71
  - fix(build): drop `@inline` from `deserializeStringField_SIMD`. As an `@inline` entry it let binaryen inline the loop-bearing escaped scanner into every struct string-field call site, exploding `large` SIMD compile ~24× under `--converge` (4 s → 99 s, 221 KB → 555 KB wasm). Kept as a single shared function (matching the already-non-inline `deserializeStringField_SWAR`); `large` SIMD build is back to ~4 s / 138 KB with unchanged runtime
30
- - bench: add string-perf benches under `assembly/__benches__/custom/` `simd-string-deser-scratch-h2h`, `simd-string-deser-variants-h2h`, `simd-string-deser-standalone-h2h`, and `swar-string-deser-hybrid-h2h` (escape-density head-to-heads for the HYBRID scanners); `serialize-string-modes-h2h` (NAIVE/SWAR/SIMD landscape, confirms streaming serialize already beats run-copy 2–6× so no HYBRID change is warranted); and `serialize-string-safety-h2h`, which checks SWAR/SIMD serialize byte-for-byte against NAIVE across 430 adversarial escape/surrogate/boundary-length inputs confirming the SIMD `store<v128>` overflow stores stay within buffer slack
72
+ - bench: add string-perf benches under `assembly/__benches__/custom/` - `simd-string-deser-scratch-h2h`, `simd-string-deser-variants-h2h`, `simd-string-deser-standalone-h2h`, and `swar-string-deser-hybrid-h2h` (escape-density head-to-heads for the HYBRID scanners); `serialize-string-modes-h2h` (NAIVE/SWAR/SIMD landscape, confirms streaming serialize already beats run-copy 2–6× so no HYBRID change is warranted); and `serialize-string-safety-h2h`, which checks SWAR/SIMD serialize byte-for-byte against NAIVE across 430 adversarial escape/surrogate/boundary-length inputs - confirming the SIMD `store<v128>` overflow stores stay within buffer slack
31
73
 
32
74
  ## 2026-05-20
33
75
 
34
76
  - perf: unify the SWAR 4-digit kernel across array entry points. `swar/array/integer.ts` and `swar/array/float.ts` now import `parse4Digits_PairMul` from `util/swar-int.ts` (Lemire-style PairMul) instead of defining a local Baseline variant. `JSON.parse<u32[]>` ~27.4 ms/op @ 64 MiB (up ~17% from ~33 ms) and `JSON.parse<Int32Array>` ~29.5 ms/op @ 64 MiB (up ~17% from ~35.6 ms). Float wins are smaller (~2-4%) since dragonbox dominates
35
- - perf: `deserializeIntegerArrayInto` (the `Array<int>` struct-field path) inlines `parseSignedIntegerSWAR` / `parseUnsignedIntegerSWAR` directly instead of routing through the per-element `deserializeIntegerField` / `deserializeUnsignedField` dispatchers. The element parser is now identical across all three call sites (top-level `JSON.parse<T[]>`, `swar/typedarray.ts`, struct field). Tuning note: array path uses parse4 + scalar even for unsigned parse8 wins for the single-token struct field where the digit run is aligned, but in arrays the `,` separator lands mid-load and forces a wasted validate, costing ~23% on `u32-64mib`
77
+ - perf: `deserializeIntegerArrayInto` (the `Array<int>` struct-field path) inlines `parseSignedIntegerSWAR` / `parseUnsignedIntegerSWAR` directly instead of routing through the per-element `deserializeIntegerField` / `deserializeUnsignedField` dispatchers. The element parser is now identical across all three call sites (top-level `JSON.parse<T[]>`, `swar/typedarray.ts`, struct field). Tuning note: array path uses parse4 + scalar even for unsigned - parse8 wins for the single-token struct field where the digit run is aligned, but in arrays the `,` separator lands mid-load and forces a wasted validate, costing ~23% on `u32-64mib`
36
78
  - refactor: fold byte-identical `deserializeObjectArrayInto` and `deserializeStructArrayInto` into one shared helper in `swar/array/shared.ts`. The two outer wrappers retain their type-specific names and now delegate; net ~130 lines removed
37
79
  - bench: add `assembly/__benches__/custom/u32-array-field.bench.ts` covering the unsigned struct-field path (no existing bench targeted `deserializeIntegerArrayInto` for `Array<u32>`); add `uint8array-512mib.bench.ts` for round-trip throughput at the largest payload AS's GC allows (BLOCK_MAXSIZE = (1<<30)-16, so a managed UTF-16 string maxes out near 1 GiB = 512 MiB of UTF-8). Built via a single `__new` + `memory.copy` to avoid concat/slice peaking past the wasm cap; the deserialize bench drops the source string and runs `__collect()` before the serialize bench so the per-op ~1 GiB output fits. SWAR ~1,350 MB/s deserialize, ~1,130 MB/s serialize at 512 MiB
38
80
 
@@ -46,7 +88,7 @@
46
88
  - perf: `JSON.stringify<u8[]>` ~5.1 GB/s, up from ~680 MB/s (7.4×), via a 256-entry UTF-16 LUT that packs the `"DDD,"` representation of each `u8` value into a single u64 plus a parallel byte-count LUT. Trailing comma is overwritten by `]` after the loop. `JSON.stringify<i8[]>` follows the same path with a peeled `-`
47
89
  - perf: `JSON.stringify<bool[]>` ~4.6 GB/s, up from ~1.1 GB/s (4.2×), by folding the per-element comma into the element write (`store<u64>(TRUE_COMMA_LO) + store<u16>(0x002c, 8)` for `"true,"`; `store<u64>(FALSE_COMMA_LO) + store<u32>(0x002c_0065, 8)` for `"false,"`)
48
90
  - perf: replace AS std's `itoa_buffered` in the integer serialize path with a jeaiii-style forward-writing itoa (`assembly/util/itoa-fast.ts`). The new path uses a 100-entry UTF-16 digit-pair LUT, computes digit count from a width-ladder so `decimalCount32` is no longer a separate call, and `@inline`s into the array loop. `JSON.stringify<i32[]>` ~1.8 GB/s, up from ~1.35 GB/s. Per-width gains range from 1.0× (1-digit) to 3.4× (2-digit) in the head-to-head bench
49
- - bench: new throughput benches at `assembly/__benches__/custom/{u8,u32,f64,bool}-64mib.bench.ts` for deserialize and `serialize-primitive-arrays.bench.ts` for stringify; head-to-head benches `itoa-h2h.bench.ts` (`itoa_buffered` vs jeaiii) and `parsefloat-h2h.bench.ts` (existing parser vs Lemire-lite at `util/parsefloat-fast.ts` research, not wired in due to test expectations encoding the existing parser's rounding behaviour)
91
+ - bench: new throughput benches at `assembly/__benches__/custom/{u8,u32,f64,bool}-64mib.bench.ts` for deserialize and `serialize-primitive-arrays.bench.ts` for stringify; head-to-head benches `itoa-h2h.bench.ts` (`itoa_buffered` vs jeaiii) and `parsefloat-h2h.bench.ts` (existing parser vs Lemire-lite at `util/parsefloat-fast.ts` - research, not wired in due to test expectations encoding the existing parser's rounding behaviour)
50
92
  - deps: pin `as-test` to `^1.1.10` and `try-as` to `^1.0.1`
51
93
 
52
94
  ## 2026-05-14 - 1.3.6
@@ -465,4 +507,3 @@
465
507
  - feat: write to a central buffer and reduce memory overhead
466
508
  - feat: rewrite the transform to properly resolve schemas and link them together
467
509
  - feat: pre-allocate and compute the minimum size of a schema to avoid memory out of range errors
468
- >>>>>>> cf237fa (chore: release 1.3.0)
package/README.md CHANGED
@@ -17,10 +17,11 @@
17
17
  - [Using Custom Serializers or Deserializers](#using-custom-serializers-or-deserializers)
18
18
  - [Overriding built-in Container Types](#overriding-built-in-container-types)
19
19
  - [Performance](#performance)
20
+ - [Real-World Throughput](#real-world-throughput)
20
21
  - [Comparison to JavaScript](#comparison-to-javascript)
22
+ - [Library Comparison](#library-comparison)
23
+ - [Lazy Fields](#lazy-fields)
21
24
  - [Performance Tuning](#performance-tuning)
22
- - [Fast-Path Compatibility Matrix](#fast-path-compatibility-matrix)
23
- - [Container Compatibility Matrix](#container-compatibility-matrix)
24
25
  - [Running Benchmarks Locally](#running-benchmarks-locally)
25
26
  - [Debugging](#debugging)
26
27
  - [Architecture](#architecture)
@@ -182,6 +183,22 @@ console.log(JSON.stringify(obj)); // { "name": "Jairus", "age": 99 }
182
183
 
183
184
  If age were higher than 18, it would be included in the serialization.
184
185
 
186
+ **@optional**
187
+
188
+ This decorator marks a field as optional for deserialization: the key may be absent from (or appear in any order in) the input, and the field keeps its default. Unlike `@omitnull` and `@omitif`, it does not affect serialization - the field is always emitted - and it has no nullability requirement. It only opts the field into the order-tolerant fast path on parse.
189
+
190
+ ```typescript
191
+ @json
192
+ class Tweet {
193
+ text!: string;
194
+ @optional
195
+ retweeted_status: Retweet | null = null; // key may be absent
196
+ }
197
+
198
+ const tweet = JSON.parse<Tweet>('{ "text": "hello" }');
199
+ console.log(tweet.retweeted_status); // null (key was absent)
200
+ ```
201
+
185
202
  ### Using nullable primitives
186
203
 
187
204
  AssemblyScript doesn't support using nullable primitive types, so instead, json-as offers the `JSON.Box` class to remedy it.
@@ -394,14 +411,14 @@ Custom deserializers should always instantiate and return a new object. They sho
394
411
  These functions are then wrapped before being consumed by the json-as library:
395
412
 
396
413
  ```typescript
397
- @inline __SERIALIZE_CUSTOM(): void {
414
+ __SERIALIZE_CUSTOM(): void {
398
415
  const data = this.serializer(this);
399
416
  const dataSize = data.length << 1;
400
417
  memory.copy(bs.offset, changetype<usize>(data), dataSize);
401
418
  bs.offset += dataSize;
402
419
  }
403
420
 
404
- @inline __DESERIALIZE_CUSTOM(data: string): Point {
421
+ _DESERIALIZE_CUSTOM(data: string): Point {
405
422
  return this.deserializer(data);
406
423
  }
407
424
  ```
@@ -489,52 +506,126 @@ This same pattern works for subclassable built-ins like `Array`, `Map`, `Set`, a
489
506
 
490
507
  ## Performance
491
508
 
492
- The `json-as` library is engineered for **multi-GB/s processing speeds**, leveraging SIMD and SWAR optimizations along with highly efficient transformations. The charts below highlight key performance metrics such as build time, operations-per-second, and throughput.
509
+ `json-as` is engineered for **multi-GB/s** serialization and deserialization. Every `@json` schema is compiled to specialized WebAssembly at build time, and bytes are scanned by one of three interchangeable backends:
493
510
 
494
- ### Comparison to JavaScript
511
+ - **NAIVE** a portable, branchy scalar scanner. The correctness baseline; needs no special CPU features.
512
+ - **SWAR** — *SIMD-Within-A-Register*: processes 8 bytes at a time with ordinary 64-bit integer math. The default.
513
+ - **SIMD** — true 128-bit vector scanning. Fastest on large and string-heavy payloads; enable with `--enable simd`.
495
514
 
496
- The following charts compare JSON-AS against JavaScript's native `JSON` implementation. It's as fair as possible and runs on V8's turboshaft optimizer. The published charts are generated locally and pushed to the `docs` branch.
515
+ The mode is chosen per build via `JSON_MODE` (see [Performance Tuning](#performance-tuning)). Orthogonal to the scan mode, the generated **struct** path can be swapped for **[lazy](#lazy-fields)** fields (defer parsing until first access) or the fully dynamic, schema-less **`JSON.Obj`** path.
497
516
 
498
- > Note: Benchmarks reflect the **latest version**. Older versions may show different performance.
499
- >
500
- > Current local benchmark machine: AMD Ryzen 7 7800X3D (8 cores, 8 threads), 96 MB L3 cache, 32 GB RAM.
517
+ > All figures below are **end-to-end**: deserialization includes allocating the destination object/array, not just scanning bytes — raw parser throughput is higher. Charts are generated locally and pushed to the [`docs`](https://github.com/JairusSW/json-as/tree/docs) branch, and reflect the **latest release** (older versions may differ).
501
518
  >
502
- > Benchmark results include normal end-to-end work such as allocating the destination object or array before deserializing into it. Raw parser throughput is higher than the published figures because these numbers intentionally include that allocation/setup cost.
519
+ > Benchmark machine: AMD Ryzen 7 7800X3D (8 cores, 96 MB 3D V-Cache), 32 GB RAM, Zorin OS 18.1 (Linux 6.17). JavaScript baselines run on V8's turboshaft optimizer.
520
+
521
+ 📊 **[Browse the full chart set for this release →](https://github.com/JairusSW/json-as/tree/docs/charts/v1.5.0)**
522
+
523
+ ### Real-World Throughput
524
+
525
+ The headline benchmark: nine standard JSON payloads — drawn from the [yyjson](https://github.com/ibireme/yyjson) and [`nativejson-benchmark`](https://github.com/miloyip/nativejson-benchmark) corpora — measured in all three scan modes, with the SIMD lazy-struct and dynamic `JSON.Obj` paths shown alongside.
503
526
 
504
- <img src="https://raw.githubusercontent.com/JairusSW/json-as/refs/heads/docs/charts/chart01.svg" alt="Performance Chart 1">
527
+ <img src="https://raw.githubusercontent.com/JairusSW/json-as/refs/heads/docs/charts/v1.5.0/classic-payload-deserialize-v8.svg" alt="Deserialization throughput across nine classic JSON payloads">
505
528
 
506
- <img src="https://raw.githubusercontent.com/JairusSW/json-as/refs/heads/docs/charts/chart02.svg" alt="Performance Chart 2">
529
+ <img src="https://raw.githubusercontent.com/JairusSW/json-as/refs/heads/docs/charts/v1.5.0/classic-payload-serialize-v8.svg" alt="Serialization throughput across nine classic JSON payloads">
530
+
531
+ Each payload stresses a different document shape:
532
+
533
+ | Payload | Size | What it stresses |
534
+ |---------|------|------------------|
535
+ | **Twitter** | 467 KB | API response, fully-modeled struct schema with `@optional` keys |
536
+ | **Canada** | 2.1 MB | GeoJSON — deeply nested arrays of floating-point coordinates |
537
+ | **CITM** | 500 KB | Concert catalog — dynamic-key `Map`s plus uniform struct arrays |
538
+ | **Poet** | 3.3 MB | ~8,900 flat `{desc, name, id}` records — pure struct fast path |
539
+ | **GitHub** | 53 KB | 30 GitHub events — a wide union of per-event-type fields |
540
+ | **GSOC** | 3.1 MB | ~1,264 org records keyed by id (schema.org JSON-LD `Map`) |
541
+ | **Lottie** | 289 KB | Vector-animation doc — structs over deeply variable layer data |
542
+ | **otfcc** | 66.4 MB | OpenType font dump — 15 tables captured as `JSON.Raw` |
543
+ | **FGO** | 48.8 MB | Game-data dump — 193 irregular tables as `Map<string, JSON.Raw>` |
544
+
545
+ The five series in each chart:
546
+
547
+ - **NAIVE / SWAR / SIMD** — the generated struct path under each scan backend, parsing every field into a typed schema.
548
+ - **Lazy (SIMD)** — `@json({ lazy: "auto" })`: each field's raw slice is stored at parse time and decoded only on first access; on serialize, untouched fields stream their original bytes straight back out. Reading or rewriting a subset is dramatically cheaper — see [Lazy Fields](#lazy-fields).
549
+ - **JSON.Obj (SIMD)** — a fully dynamic, schema-less parse into `JSON.Obj`, with no generated per-type code.
550
+
551
+ ### Comparison to JavaScript
552
+
553
+ `json-as` against JavaScript's native `JSON`, parsed and stringified in a fresh V8 — as close to apples-to-apples as a Wasm-vs-native comparison gets.
554
+
555
+ <img src="https://raw.githubusercontent.com/JairusSW/json-as/refs/heads/docs/charts/v1.5.0/overview-serialize.svg" alt="Performance Chart 1">
556
+
557
+ <img src="https://raw.githubusercontent.com/JairusSW/json-as/refs/heads/docs/charts/v1.5.0/overview-deserialize.svg" alt="Performance Chart 2">
507
558
 
508
559
  <details>
509
560
  <summary>String serialize charts (click to expand)</summary>
510
561
 
511
- <img src="https://raw.githubusercontent.com/JairusSW/json-as/refs/heads/docs/charts/chart03.png" alt="Performance Chart 3">
562
+ <img src="https://raw.githubusercontent.com/JairusSW/json-as/refs/heads/docs/charts/v1.5.0/string-serialize.svg" alt="Performance Chart 3">
512
563
 
513
- <img src="https://raw.githubusercontent.com/JairusSW/json-as/refs/heads/docs/charts/chart07.png" alt="Performance Chart 7">
564
+ <img src="https://raw.githubusercontent.com/JairusSW/json-as/refs/heads/docs/charts/v1.5.0/string-serialize-1mb.svg" alt="Performance Chart 7">
514
565
  </details>
515
566
 
516
567
  <details>
517
568
  <summary>String deserialize charts (click to expand)</summary>
518
569
 
519
- <img src="https://raw.githubusercontent.com/JairusSW/json-as/refs/heads/docs/charts/chart04.png" alt="Performance Chart 4">
570
+ <img src="https://raw.githubusercontent.com/JairusSW/json-as/refs/heads/docs/charts/v1.5.0/string-deserialize.svg" alt="Performance Chart 4">
520
571
 
521
- <img src="https://raw.githubusercontent.com/JairusSW/json-as/refs/heads/docs/charts/chart08.png" alt="Performance Chart 8">
572
+ <img src="https://raw.githubusercontent.com/JairusSW/json-as/refs/heads/docs/charts/v1.5.0/string-deserialize-1mb.svg" alt="Performance Chart 8">
522
573
  </details>
523
574
 
524
575
  <details>
525
576
  <summary>Object serialize charts (click to expand)</summary>
526
577
 
527
- <img src="https://raw.githubusercontent.com/JairusSW/json-as/refs/heads/docs/charts/chart05.png" alt="Performance Chart 5">
578
+ <img src="https://raw.githubusercontent.com/JairusSW/json-as/refs/heads/docs/charts/v1.5.0/object-serialize.svg" alt="Performance Chart 5">
528
579
 
529
- <img src="https://raw.githubusercontent.com/JairusSW/json-as/refs/heads/docs/charts/chart09.png" alt="Performance Chart 9">
580
+ <img src="https://raw.githubusercontent.com/JairusSW/json-as/refs/heads/docs/charts/v1.5.0/object-serialize-1mb.svg" alt="Performance Chart 9">
530
581
  </details>
531
582
 
532
583
  <details>
533
584
  <summary>Object deserialize charts (click to expand)</summary>
534
585
 
535
- <img src="https://raw.githubusercontent.com/JairusSW/json-as/refs/heads/docs/charts/chart06.png" alt="Performance Chart 6">
586
+ <img src="https://raw.githubusercontent.com/JairusSW/json-as/refs/heads/docs/charts/v1.5.0/object-deserialize.svg" alt="Performance Chart 6">
536
587
 
537
- <img src="https://raw.githubusercontent.com/JairusSW/json-as/refs/heads/docs/charts/chart10.png" alt="Performance Chart 10">
588
+ <img src="https://raw.githubusercontent.com/JairusSW/json-as/refs/heads/docs/charts/v1.5.0/object-deserialize-1mb.svg" alt="Performance Chart 10">
589
+ </details>
590
+
591
+ <details>
592
+ <summary>Primitive (de)serialize charts (click to expand)</summary>
593
+
594
+ <img src="https://raw.githubusercontent.com/JairusSW/json-as/refs/heads/docs/charts/v1.5.0/primitive-serialize.svg" alt="Primitive serialization performance">
595
+
596
+ <img src="https://raw.githubusercontent.com/JairusSW/json-as/refs/heads/docs/charts/v1.5.0/primitive-deserialize.svg" alt="Primitive deserialization performance">
597
+ </details>
598
+
599
+ ### Library Comparison
600
+
601
+ How `json-as` stacks up against other JSON libraries on a ~5 KiB GitHub-repo payload: JavaScript's native `JSON` and `fast-json` (each in a fresh V8), plus the `assemblyscript-json` package. The `json-as` bars (generated struct, lazy struct, and dynamic `JSON.Obj`) are averaged across the NAIVE / SWAR / SIMD scan modes.
602
+
603
+ <img src="https://raw.githubusercontent.com/JairusSW/json-as/refs/heads/docs/charts/v1.5.0/library-deserialize.svg" alt="Library comparison - deserialize throughput">
604
+
605
+ <img src="https://raw.githubusercontent.com/JairusSW/json-as/refs/heads/docs/charts/v1.5.0/library-serialize.svg" alt="Library comparison - serialize throughput">
606
+
607
+ ### Lazy Fields
608
+
609
+ Mark a field `@lazy` (or `JSON.Lazy<T>`, or a whole class with `@json({ lazy: "auto" })`) to defer it: its raw JSON slice is stored at parse time and parsed only on first access. Fields you skip are never parsed, and untouched fields pass through their original bytes on serialize. See the [Lazy Fields guide](https://docs.jairus.dev/json-as/guide/lazy-fields) for the full API and trade-offs.
610
+
611
+ Skipping the deferred fields makes deserialization several times faster, and the win grows with payload size:
612
+
613
+ <img src="https://raw.githubusercontent.com/JairusSW/json-as/refs/heads/docs/charts/v1.5.0/lazy-deserialize.svg" alt="Lazy deserialize: eager vs lazy by payload size">
614
+
615
+ The proxy / filter / forward case - parse then re-serialize without reading the deferred fields - copies their raw bytes straight through:
616
+
617
+ <img src="https://raw.githubusercontent.com/JairusSW/json-as/refs/heads/docs/charts/v1.5.0/lazy-roundtrip.svg" alt="Lazy round-trip: eager vs lazy by payload size">
618
+
619
+ Re-emitting a parsed object forwards the untouched fields' raw bytes instead of rebuilding them:
620
+
621
+ <img src="https://raw.githubusercontent.com/JairusSW/json-as/refs/heads/docs/charts/v1.5.0/lazy-serialize.svg" alt="Lazy serialize: eager vs lazy by payload size">
622
+
623
+ <details>
624
+ <summary>Access-pattern comparison (click to expand)</summary>
625
+
626
+ Lazy cost scales with how much you actually read. For each payload (vec3 → large) the chart compares a lazy parse that reads none, one, half, or all of its deferred fields against the eager full-parse baseline. Reading nothing or a single field is far faster than eager; reading everything approaches it (the work is deferred, not removed). SWAR only, since that's the mode the lazy fast-path is showcased in:
627
+
628
+ <img src="https://raw.githubusercontent.com/JairusSW/json-as/refs/heads/docs/charts/v1.5.0/lazy-access-pattern.svg" alt="Lazy mode access patterns: deferred-field reads vs eager baseline">
538
629
  </details>
539
630
 
540
631
  ### Performance Tuning
@@ -583,6 +674,14 @@ npm run bench:as
583
674
  npm run bench:js
584
675
  ```
585
676
 
677
+ The AS suite includes **lazy variants** - `small.lazy.bench.ts`, `medium.lazy.bench.ts`, `large.lazy.bench.ts`, `token.lazy.bench.ts`, and `vec3.lazy.bench.ts` - which mark their structs `@json({ lazy: "auto" })` and dump to `<name>-lazy` logs, so eager and lazy can be compared side by side. Run one on its own with:
678
+
679
+ ```bash
680
+ bun run bench:as medium.lazy --mode simd
681
+ ```
682
+
683
+ > Note: `large.lazy.bench.ts` stresses the optimizer - `lazy: "auto"` on the ~150-field `Repo` generates a getter and serialize branch per field, which can overrun **stock** Binaryen (it crashes during optimize). Build it with a patched/larger-budget Binaryen, or prefer per-field `@lazy` on very wide schemas.
684
+
586
685
  5. Build charts from the latest local logs:
587
686
 
588
687
  ```bash
@@ -1,85 +1,46 @@
1
1
  // Characters
2
- // @ts-ignore = Decorator is valid here
3
- @inline export const COMMA = 44;
4
- // @ts-ignore = Decorator is valid here
5
- @inline export const QUOTE = 34;
6
- // @ts-ignore = Decorator is valid here
7
- @inline export const BACK_SLASH = 92;
8
- // @ts-ignore: Decorator is valid here
9
- @inline export const FWD_SLASH = 47;
10
- // @ts-ignore: Decorator is valid here
11
- @inline export const BRACE_LEFT = 123;
12
- // @ts-ignore: Decorator is valid here
13
- @inline export const BRACE_RIGHT = 125;
14
- // @ts-ignore: Decorator is valid here
15
- @inline export const BRACKET_LEFT = 91;
16
- // @ts-ignore: Decorator is valid here
17
- @inline export const BRACKET_RIGHT = 93;
18
- // @ts-ignore: Decorator is valid here
19
- @inline export const COLON = 58;
20
- // @ts-ignore: Decorator is valid here
21
- @inline export const CHAR_T = 116;
22
- // @ts-ignore: Decorator is valid here
23
- @inline export const CHAR_R = 114;
24
- // @ts-ignore: Decorator is valid here
25
- @inline export const CHAR_U = 117;
26
- // @ts-ignore: Decorator is valid here
27
- @inline export const CHAR_E = 101;
28
- // @ts-ignore: Decorator is valid here
29
- @inline export const CHAR_F = 102;
30
- // @ts-ignore: Decorator is valid here
31
- @inline export const CHAR_A = 97;
32
- // @ts-ignore: Decorator is valid here
33
- @inline export const CHAR_L = 108;
34
- // @ts-ignore: Decorator is valid here
35
- @inline export const CHAR_S = 115;
36
- // @ts-ignore = Decorator is valid here
37
- @inline export const CHAR_N = 110;
38
- // @ts-ignore = Decorator is valid here
39
- @inline export const CHAR_B = 98;
2
+ export const COMMA = 44;
3
+ export const QUOTE = 34;
4
+ export const BACK_SLASH = 92;
5
+ export const FWD_SLASH = 47;
6
+ export const BRACE_LEFT = 123;
7
+ export const BRACE_RIGHT = 125;
8
+ export const BRACKET_LEFT = 91;
9
+ export const BRACKET_RIGHT = 93;
10
+ export const COLON = 58;
11
+ export const CHAR_T = 116;
12
+ export const CHAR_R = 114;
13
+ export const CHAR_U = 117;
14
+ export const CHAR_E = 101;
15
+ export const CHAR_F = 102;
16
+ export const CHAR_A = 97;
17
+ export const CHAR_L = 108;
18
+ export const CHAR_S = 115;
19
+ export const CHAR_N = 110;
20
+ export const CHAR_B = 98;
40
21
  // Strings
41
- // @ts-ignore: Decorator is valid here
42
- @inline export const TRUE_WORD = "true";
43
- // @ts-ignore: Decorator is valid here
44
- @inline export const FALSE_WORD = "false";
45
- // @ts-ignore: Decorator is valid here
46
- @inline export const NULL_WORD = "null";
47
- // @ts-ignore: Decorator is valid here
48
- @inline export const BRACE_LEFT_WORD = "{";
49
- // @ts-ignore: Decorator is valid here
50
- @inline export const BRACKET_LEFT_WORD = "[";
51
- // @ts-ignore: Decorator is valid here
52
- @inline export const EMPTY_BRACKET_WORD = "[]";
53
- // @ts-ignore: Decorator is valid here
54
- @inline export const COLON_WORD = ":";
55
- // @ts-ignore: Decorator is valid here
56
- @inline export const COMMA_WORD = ",";
57
- // @ts-ignore: Decorator is valid here
58
- @inline export const BRACE_RIGHT_WORD = "}";
59
- // @ts-ignore: Decorator is valid here
60
- @inline export const BRACKET_RIGHT_WORD = "]";
61
- // @ts-ignore: Decorator is valid here
62
- @inline export const QUOTE_WORD = '"';
63
- // @ts-ignore: Decorator is valid here
64
- @inline export const EMPTY_QUOTE_WORD = '""';
22
+ export const TRUE_WORD = "true";
23
+ export const FALSE_WORD = "false";
24
+ export const NULL_WORD = "null";
25
+ export const BRACE_LEFT_WORD = "{";
26
+ export const BRACKET_LEFT_WORD = "[";
27
+ export const EMPTY_BRACKET_WORD = "[]";
28
+ export const COLON_WORD = ":";
29
+ export const COMMA_WORD = ",";
30
+ export const BRACE_RIGHT_WORD = "}";
31
+ export const BRACKET_RIGHT_WORD = "]";
32
+ export const QUOTE_WORD = '"';
33
+ export const EMPTY_QUOTE_WORD = '""';
65
34
 
66
35
  // Escape Codes
67
- // @ts-ignore: Decorator is valid here
68
- @inline export const BACKSPACE = 8; // \b
69
- // @ts-ignore: Decorator is valid here
70
- @inline export const TAB = 9; // \t
71
- // @ts-ignore: Decorator is valid here
72
- @inline export const NEW_LINE = 10; // \n
73
- // @ts-ignore: Decorator is valid here
74
- @inline export const FORM_FEED = 12; // \f
75
- // @ts-ignore: Decorator is valid here
76
- @inline export const CARRIAGE_RETURN = 13; // \r
36
+ export const BACKSPACE = 8; // \b
37
+ export const TAB = 9; // \t
38
+ export const NEW_LINE = 10; // \n
39
+ export const FORM_FEED = 12; // \f
40
+ export const CARRIAGE_RETURN = 13; // \r
77
41
 
78
42
  // Pre-encoded u64 constants for common JSON literals
79
43
  // These represent the UTF-16 encoded bytes stored as u64 for fast comparison/storage
80
- // @ts-ignore: Decorator is valid here
81
- @inline export const NULL_WORD_U64: u64 = 30399761348886638; // "null" as u64 (n=110, u=117, l=108, l=108)
82
- // @ts-ignore: Decorator is valid here
83
- @inline export const TRUE_WORD_U64: u64 = 28429475166421108; // "true" as u64 (t=116, r=114, u=117, e=101)
84
- // @ts-ignore: Decorator is valid here
85
- @inline export const FALSE_WORD_U64: u64 = 32370086184550502; // "fals" as u64 (f=102, a=97, l=108, s=115) - first 4 chars of "false"
44
+ export const NULL_WORD_U64: u64 = 30399761348886638; // "null" as u64 (n=110, u=117, l=108, l=108)
45
+ export const TRUE_WORD_U64: u64 = 28429475166421108; // "true" as u64 (t=116, r=114, u=117, e=101)
46
+ export const FALSE_WORD_U64: u64 = 32370086184550502; // "fals" as u64 (f=102, a=97, l=108, s=115) - first 4 chars of "false"
@@ -1,8 +1,7 @@
1
1
  import { JSON } from "../..";
2
- import { deserializeArray } from "./array";
3
2
  import { deserializeBoolean } from "./bool";
4
- import { deserializeFloat_NAIVE } from "../naive/float";
5
- import { deserializeObject } from "./object";
3
+ import { deserializeFloat } from "./float";
4
+ import { deserializeObject, deserializeJsonArray, getParseSrc } from "./object";
6
5
  import { deserializeString } from "./string";
7
6
  import { BRACE_LEFT, BRACKET_LEFT, CHAR_N, QUOTE } from "../../custom/chars";
8
7
 
@@ -11,15 +10,34 @@ export function deserializeArbitrary(
11
10
  srcEnd: usize,
12
11
  dst: usize,
13
12
  ): JSON.Value {
13
+ const v = parseArbitraryValue(srcStart, srcEnd);
14
+ // Reuse path (`JSON.parse<JSON.Value>(data, out)`): write the parsed bits into
15
+ // the caller's handle (with the GC barrier for any managed payload).
16
+ return dst != 0 ? JSON.Value.__adoptInto(dst, v) : v;
17
+ }
18
+
19
+ function parseArbitraryValue(srcStart: usize, srcEnd: usize): JSON.Value {
14
20
  const firstChar = load<u16>(srcStart);
15
- if (firstChar == QUOTE) {
16
- return JSON.Value.from(deserializeString(srcStart, srcEnd));
17
- } else if (firstChar == BRACE_LEFT) {
18
- return JSON.Value.from(deserializeObject(srcStart, srcEnd, 0));
21
+ if (
22
+ firstChar == QUOTE ||
23
+ firstChar == BRACE_LEFT ||
24
+ firstChar == BRACKET_LEFT
25
+ ) {
26
+ // Lazy by default: when a parse is in flight (source anchor present), defer
27
+ // strings and composites (the allocating shapes) - store the exact raw slice
28
+ // and materialize on first access. Cheap primitives stay eager below.
29
+ const src = getParseSrc();
30
+ if (src.length != 0) {
31
+ const end = JSON.Util.scanValueEnd<JSON.Value>(srcStart, srcEnd);
32
+ return JSON.Value.fromSlice(srcStart, end, src);
33
+ }
34
+ if (firstChar == QUOTE)
35
+ return JSON.Value.from(deserializeString(srcStart, srcEnd));
36
+ return firstChar == BRACE_LEFT
37
+ ? JSON.Value.from(deserializeObject(srcStart, srcEnd, 0))
38
+ : JSON.Value.from(deserializeJsonArray(srcStart, srcEnd, 0));
19
39
  } else if (firstChar - 48 <= 9 || firstChar == 45) {
20
- return JSON.Value.from(deserializeFloat_NAIVE<f64>(srcStart, srcEnd));
21
- } else if (firstChar == BRACKET_LEFT) {
22
- return JSON.Value.from(deserializeArray<JSON.Value[]>(srcStart, srcEnd, 0));
40
+ return JSON.Value.from(deserializeFloat<f64>(srcStart, srcEnd));
23
41
  } else if (firstChar == 116 || firstChar == 102) {
24
42
  return JSON.Value.from(deserializeBoolean(srcStart, srcEnd));
25
43
  } else if (firstChar == CHAR_N) {
@@ -12,8 +12,7 @@ import {
12
12
  deserializeFloatField_SIMD,
13
13
  } from "../simd/float";
14
14
 
15
- // @ts-ignore: inline
16
- @inline export function deserializeFloat<T>(srcStart: usize, srcEnd: usize): T {
15
+ export function deserializeFloat<T>(srcStart: usize, srcEnd: usize): T {
17
16
  if (JSON_MODE == JSONMode.SIMD) {
18
17
  return deserializeFloat_SIMD<T>(srcStart, srcEnd);
19
18
  } else if (JSON_MODE == JSONMode.NAIVE) {
@@ -23,8 +22,7 @@ import {
23
22
  }
24
23
  }
25
24
 
26
- // @ts-ignore: inline
27
- @inline export function deserializeFloatField<T extends number>(
25
+ export function deserializeFloatField<T extends number>(
28
26
  srcStart: usize,
29
27
  srcEnd: usize,
30
28
  dstObj: usize,