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.
- package/CHANGELOG.md +24 -0
- package/README.md +49 -1
- package/assembly/deserialize/index/arbitrary.ts +2 -2
- package/assembly/deserialize/naive/array/arbitrary.ts +3 -136
- package/assembly/deserialize/naive/array/array.ts +30 -1
- package/assembly/deserialize/naive/array/integer.ts +1 -6
- package/assembly/deserialize/naive/array/map.ts +10 -14
- package/assembly/deserialize/naive/array/object.ts +10 -14
- package/assembly/deserialize/naive/float.ts +2 -4
- package/assembly/deserialize/naive/integer.ts +1 -2
- package/assembly/deserialize/naive/map.ts +40 -202
- package/assembly/deserialize/naive/object.ts +153 -174
- package/assembly/deserialize/naive/set.ts +1 -2
- package/assembly/deserialize/naive/staticarray.ts +1 -2
- package/assembly/deserialize/naive/string.ts +65 -18
- package/assembly/deserialize/naive/typedarray.ts +1 -2
- package/assembly/deserialize/naive/unsigned.ts +1 -2
- package/assembly/deserialize/simd/array/integer.ts +3 -6
- package/assembly/deserialize/simd/float.ts +2 -7
- package/assembly/deserialize/simd/integer.ts +4 -8
- package/assembly/deserialize/simd/string.ts +16 -21
- package/assembly/deserialize/swar/array/array.ts +1 -2
- package/assembly/deserialize/swar/array/bool.ts +1 -2
- package/assembly/deserialize/swar/array/float.ts +2 -3
- package/assembly/deserialize/swar/array/generic.ts +1 -2
- package/assembly/deserialize/swar/array/integer.ts +6 -11
- package/assembly/deserialize/swar/array/object.ts +1 -2
- package/assembly/deserialize/swar/array/shared.ts +3 -8
- package/assembly/deserialize/swar/array/string.ts +1 -2
- package/assembly/deserialize/swar/array/struct.ts +1 -1
- package/assembly/deserialize/swar/float.ts +3 -8
- package/assembly/deserialize/swar/integer.ts +4 -8
- package/assembly/deserialize/swar/string.ts +29 -41
- package/assembly/index.d.ts +248 -15
- package/assembly/index.ts +468 -146
- package/assembly/serialize/index/object.ts +18 -15
- package/assembly/serialize/naive/string.ts +9 -2
- package/assembly/serialize/swar/string.ts +1 -2
- package/assembly/util/atoi.ts +1 -2
- package/assembly/util/dragonbox.ts +0 -8
- package/assembly/util/itoa-fast.ts +3 -6
- package/assembly/util/parsefloat-fast.ts +1 -2
- package/assembly/util/scanValueEnd.ts +1 -2
- package/assembly/util/scanValueEndSimd.ts +160 -0
- package/assembly/util/scanValueEndSwar.ts +142 -0
- package/assembly/util/scientific.ts +3 -6
- package/assembly/util/simd-int.ts +4 -8
- package/assembly/util/snp.ts +1 -5
- package/assembly/util/stringScan.ts +2 -4
- package/assembly/util/swar-int.ts +3 -6
- package/lib/as-bs.ts +37 -0
- package/package.json +14 -4
- package/transform/lib/builder.d.ts +0 -1
- package/transform/lib/builder.js +0 -1
- package/transform/lib/index.d.ts +0 -1
- package/transform/lib/index.js +537 -290
- package/transform/lib/linkers/alias.d.ts +0 -1
- package/transform/lib/linkers/alias.js +0 -1
- package/transform/lib/linkers/custom.d.ts +0 -1
- package/transform/lib/linkers/custom.js +0 -1
- package/transform/lib/linkers/imports.d.ts +0 -1
- package/transform/lib/linkers/imports.js +0 -1
- package/transform/lib/types.d.ts +3 -2
- package/transform/lib/types.js +2 -1
- package/transform/lib/util.d.ts +0 -1
- package/transform/lib/util.js +0 -1
- package/transform/lib/visitor.d.ts +0 -1
- package/transform/lib/visitor.js +0 -1
- package/transform/lib/builder.d.ts.map +0 -1
- package/transform/lib/builder.js.map +0 -1
- package/transform/lib/index.d.ts.map +0 -1
- package/transform/lib/index.js.map +0 -1
- package/transform/lib/linkers/alias.d.ts.map +0 -1
- package/transform/lib/linkers/alias.js.map +0 -1
- package/transform/lib/linkers/custom.d.ts.map +0 -1
- package/transform/lib/linkers/custom.js.map +0 -1
- package/transform/lib/linkers/imports.d.ts.map +0 -1
- package/transform/lib/linkers/imports.js.map +0 -1
- package/transform/lib/types.d.ts.map +0 -1
- package/transform/lib/types.js.map +0 -1
- package/transform/lib/util.d.ts.map +0 -1
- package/transform/lib/util.js.map +0 -1
- package/transform/lib/visitor.d.ts.map +0 -1
- package/transform/lib/visitor.js.map +0 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,29 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 2026-06-05 - v1.4.0
|
|
4
|
+
|
|
5
|
+
- 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
|
|
6
|
+
- 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
|
|
7
|
+
- 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`
|
|
8
|
+
- 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
|
|
9
|
+
- 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
|
|
10
|
+
- 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)
|
|
11
|
+
- 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)
|
|
12
|
+
- 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
|
|
13
|
+
- 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`)
|
|
14
|
+
|
|
15
|
+
- 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`)
|
|
16
|
+
- 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
|
|
17
|
+
- 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
|
|
18
|
+
- 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
|
|
19
|
+
- 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`
|
|
20
|
+
- 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)
|
|
21
|
+
- 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
|
|
22
|
+
|
|
23
|
+
## 2026-06-01 - v1.3.9
|
|
24
|
+
|
|
25
|
+
- fix: remove readFileSync which failed under virtual file systems
|
|
26
|
+
|
|
3
27
|
## 2026-06-01 - v1.3.8
|
|
4
28
|
|
|
5
29
|
- 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
|
package/README.md
CHANGED
|
@@ -497,7 +497,7 @@ The following charts compare JSON-AS against JavaScript's native `JSON` implemen
|
|
|
497
497
|
|
|
498
498
|
> Note: Benchmarks reflect the **latest version**. Older versions may show different performance.
|
|
499
499
|
>
|
|
500
|
-
> Current local benchmark machine:
|
|
500
|
+
> Current local benchmark machine: Apple M4 Max (16 cores — 12 performance + 4 efficiency), 64 GB RAM, macOS 26.
|
|
501
501
|
>
|
|
502
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.
|
|
503
503
|
|
|
@@ -537,6 +537,46 @@ The following charts compare JSON-AS against JavaScript's native `JSON` implemen
|
|
|
537
537
|
<img src="https://raw.githubusercontent.com/JairusSW/json-as/refs/heads/docs/charts/chart10.png" alt="Performance Chart 10">
|
|
538
538
|
</details>
|
|
539
539
|
|
|
540
|
+
<details>
|
|
541
|
+
<summary>Primitive (de)serialize charts (click to expand)</summary>
|
|
542
|
+
|
|
543
|
+
<img src="https://raw.githubusercontent.com/JairusSW/json-as/refs/heads/docs/charts/chart11.svg" alt="Primitive serialization performance">
|
|
544
|
+
|
|
545
|
+
<img src="https://raw.githubusercontent.com/JairusSW/json-as/refs/heads/docs/charts/chart12.svg" alt="Primitive deserialization performance">
|
|
546
|
+
</details>
|
|
547
|
+
|
|
548
|
+
### Library comparison
|
|
549
|
+
|
|
550
|
+
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.
|
|
551
|
+
|
|
552
|
+
<img src="https://raw.githubusercontent.com/JairusSW/json-as/refs/heads/docs/charts/chart14.png" alt="Library comparison - deserialize throughput">
|
|
553
|
+
|
|
554
|
+
<img src="https://raw.githubusercontent.com/JairusSW/json-as/refs/heads/docs/charts/chart13.png" alt="Library comparison - serialize throughput">
|
|
555
|
+
|
|
556
|
+
### Lazy Fields
|
|
557
|
+
|
|
558
|
+
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.
|
|
559
|
+
|
|
560
|
+
Skipping the deferred fields makes deserialization several times faster, and the win grows with payload size:
|
|
561
|
+
|
|
562
|
+
<img src="https://raw.githubusercontent.com/JairusSW/json-as/refs/heads/docs/charts/lazy-deserialize.svg" alt="Lazy deserialize: eager vs lazy by payload size">
|
|
563
|
+
|
|
564
|
+
The proxy / filter / forward case - parse then re-serialize without reading the deferred fields - copies their raw bytes straight through:
|
|
565
|
+
|
|
566
|
+
<img src="https://raw.githubusercontent.com/JairusSW/json-as/refs/heads/docs/charts/lazy-roundtrip.svg" alt="Lazy round-trip: eager vs lazy by payload size">
|
|
567
|
+
|
|
568
|
+
Re-emitting a parsed object forwards the untouched fields' raw bytes instead of rebuilding them:
|
|
569
|
+
|
|
570
|
+
<img src="https://raw.githubusercontent.com/JairusSW/json-as/refs/heads/docs/charts/lazy-serialize.svg" alt="Lazy serialize: eager vs lazy by payload size">
|
|
571
|
+
|
|
572
|
+
<details>
|
|
573
|
+
<summary>Access-pattern comparison (click to expand)</summary>
|
|
574
|
+
|
|
575
|
+
Skipping, reading one field, or forwarding is far faster than eager; reading every deferred field costs a little more (the work is deferred, not removed):
|
|
576
|
+
|
|
577
|
+
<img src="https://raw.githubusercontent.com/JairusSW/json-as/refs/heads/docs/charts/lazy-access-pattern.svg" alt="Lazy access pattern: eager vs lazy">
|
|
578
|
+
</details>
|
|
579
|
+
|
|
540
580
|
### Performance Tuning
|
|
541
581
|
|
|
542
582
|
Instead of using flags for setting options, `json-as` is configured by environmental variables.
|
|
@@ -583,6 +623,14 @@ npm run bench:as
|
|
|
583
623
|
npm run bench:js
|
|
584
624
|
```
|
|
585
625
|
|
|
626
|
+
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:
|
|
627
|
+
|
|
628
|
+
```bash
|
|
629
|
+
bun run bench:as medium.lazy --mode simd
|
|
630
|
+
```
|
|
631
|
+
|
|
632
|
+
> 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.
|
|
633
|
+
|
|
586
634
|
5. Build charts from the latest local logs:
|
|
587
635
|
|
|
588
636
|
```bash
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { JSON } from "../..";
|
|
2
2
|
import { deserializeArray } from "./array";
|
|
3
3
|
import { deserializeBoolean } from "./bool";
|
|
4
|
-
import {
|
|
4
|
+
import { deserializeFloat } from "./float";
|
|
5
5
|
import { deserializeObject } from "./object";
|
|
6
6
|
import { deserializeString } from "./string";
|
|
7
7
|
import { BRACE_LEFT, BRACKET_LEFT, CHAR_N, QUOTE } from "../../custom/chars";
|
|
@@ -17,7 +17,7 @@ export function deserializeArbitrary(
|
|
|
17
17
|
} else if (firstChar == BRACE_LEFT) {
|
|
18
18
|
return JSON.Value.from(deserializeObject(srcStart, srcEnd, 0));
|
|
19
19
|
} else if (firstChar - 48 <= 9 || firstChar == 45) {
|
|
20
|
-
return JSON.Value.from(
|
|
20
|
+
return JSON.Value.from(deserializeFloat<f64>(srcStart, srcEnd));
|
|
21
21
|
} else if (firstChar == BRACKET_LEFT) {
|
|
22
22
|
return JSON.Value.from(deserializeArray<JSON.Value[]>(srcStart, srcEnd, 0));
|
|
23
23
|
} else if (firstChar == 116 || firstChar == 102) {
|
|
@@ -1,18 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
BACK_SLASH,
|
|
3
|
-
BRACE_LEFT,
|
|
4
|
-
BRACE_RIGHT,
|
|
5
|
-
BRACKET_LEFT,
|
|
6
|
-
BRACKET_RIGHT,
|
|
7
|
-
CHAR_F,
|
|
8
|
-
CHAR_N,
|
|
9
|
-
CHAR_T,
|
|
10
|
-
COMMA,
|
|
11
|
-
QUOTE,
|
|
12
|
-
} from "../../../custom/chars";
|
|
13
1
|
import { JSON } from "../../../";
|
|
14
|
-
import {
|
|
15
|
-
import { ptrToStr } from "../../../util/ptrToStr";
|
|
2
|
+
import { parseArrayBody } from "../object";
|
|
16
3
|
|
|
17
4
|
export function deserializeArbitraryArray(
|
|
18
5
|
srcStart: usize,
|
|
@@ -22,127 +9,7 @@ export function deserializeArbitraryArray(
|
|
|
22
9
|
const out = changetype<JSON.Value[]>(
|
|
23
10
|
dst || changetype<usize>(instantiate<JSON.Value[]>()),
|
|
24
11
|
);
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
// if (load<u16>(srcStart) != BRACKET_LEFT)
|
|
28
|
-
srcStart += 2;
|
|
29
|
-
while (srcStart < srcEnd) {
|
|
30
|
-
const code = load<u16>(srcStart);
|
|
31
|
-
// console.log("code: " + String.fromCharCode(code));
|
|
32
|
-
if (code == BRACE_LEFT) {
|
|
33
|
-
lastIndex = srcStart;
|
|
34
|
-
depth++;
|
|
35
|
-
srcStart += 2;
|
|
36
|
-
while (srcStart < srcEnd) {
|
|
37
|
-
const code = load<u16>(srcStart);
|
|
38
|
-
if (code == BRACE_RIGHT) {
|
|
39
|
-
if (--depth == 0) {
|
|
40
|
-
// @ts-ignore: type
|
|
41
|
-
out.push(JSON.__deserialize<JSON.Value>(lastIndex, srcStart + 2));
|
|
42
|
-
// console.log("Value (object): " + ptrToStr(lastIndex, srcStart + 2));
|
|
43
|
-
break;
|
|
44
|
-
}
|
|
45
|
-
} else if (code == BRACE_LEFT) depth++;
|
|
46
|
-
srcStart += 2;
|
|
47
|
-
}
|
|
48
|
-
} else if (code == QUOTE) {
|
|
49
|
-
lastIndex = srcStart;
|
|
50
|
-
srcStart = scanStringEnd(srcStart, srcEnd);
|
|
51
|
-
if (srcStart >= srcEnd)
|
|
52
|
-
throw new Error("Unterminated string in JSON array");
|
|
53
|
-
// @ts-ignore: exists
|
|
54
|
-
out.push(JSON.__deserialize<JSON.Value>(lastIndex, srcStart + 2));
|
|
55
|
-
srcStart += 2;
|
|
56
|
-
// console.log("next: " + String.fromCharCode(load<u16>(srcStart)));
|
|
57
|
-
} else if (code - 48 <= 9 || code == 45) {
|
|
58
|
-
// console.log("trigger int")
|
|
59
|
-
lastIndex = srcStart;
|
|
60
|
-
srcStart += 2;
|
|
61
|
-
while (srcStart < srcEnd) {
|
|
62
|
-
const code = load<u16>(srcStart);
|
|
63
|
-
if (code == COMMA || code == BRACKET_RIGHT || isSpace(code)) {
|
|
64
|
-
// @ts-ignore: type
|
|
65
|
-
out.push(JSON.__deserialize<JSON.Value>(lastIndex, srcStart));
|
|
66
|
-
// console.log("Value (number): " + ptrToStr(lastIndex, srcStart));
|
|
67
|
-
break;
|
|
68
|
-
}
|
|
69
|
-
srcStart += 2;
|
|
70
|
-
}
|
|
71
|
-
// console.log("next: " + String.fromCharCode(load<u16>(srcStart)));
|
|
72
|
-
} else if (code == BRACE_LEFT) {
|
|
73
|
-
lastIndex = srcStart;
|
|
74
|
-
depth++;
|
|
75
|
-
srcStart += 2;
|
|
76
|
-
while (srcStart < srcEnd) {
|
|
77
|
-
const code = load<u16>(srcStart);
|
|
78
|
-
if (code == BRACE_RIGHT) {
|
|
79
|
-
if (--depth == 0) {
|
|
80
|
-
// @ts-ignore: type
|
|
81
|
-
out.push(JSON.__deserialize<JSON.Value>(lastIndex, srcStart + 2));
|
|
82
|
-
// console.log("Value (object): " + ptrToStr(lastIndex, srcStart + 2));
|
|
83
|
-
while (isSpace(load<u16>((srcStart += 2)))) {
|
|
84
|
-
/* empty */
|
|
85
|
-
}
|
|
86
|
-
break;
|
|
87
|
-
}
|
|
88
|
-
} else if (code == BRACE_LEFT) depth++;
|
|
89
|
-
srcStart += 2;
|
|
90
|
-
}
|
|
91
|
-
} else if (code == BRACKET_LEFT) {
|
|
92
|
-
lastIndex = srcStart;
|
|
93
|
-
depth++;
|
|
94
|
-
srcStart += 2;
|
|
95
|
-
while (srcStart < srcEnd) {
|
|
96
|
-
const code = load<u16>(srcStart);
|
|
97
|
-
if (code == QUOTE) {
|
|
98
|
-
srcStart = scanStringEnd(srcStart, srcEnd);
|
|
99
|
-
if (srcStart >= srcEnd)
|
|
100
|
-
throw new Error("Unterminated string in JSON array");
|
|
101
|
-
} else if (code == BRACKET_RIGHT) {
|
|
102
|
-
if (--depth == 0) {
|
|
103
|
-
// @ts-ignore: type
|
|
104
|
-
out.push(JSON.__deserialize<JSON.Value>(lastIndex, srcStart + 2));
|
|
105
|
-
// console.log("Value (array): " + ptrToStr(lastIndex, srcStart + 2));
|
|
106
|
-
while (isSpace(load<u16>((srcStart += 2)))) {
|
|
107
|
-
/* empty */
|
|
108
|
-
}
|
|
109
|
-
break;
|
|
110
|
-
}
|
|
111
|
-
} else if (code == BRACKET_LEFT) depth++;
|
|
112
|
-
srcStart += 2;
|
|
113
|
-
}
|
|
114
|
-
} else if (code == CHAR_T) {
|
|
115
|
-
if (load<u64>(srcStart) == 28429475166421108) {
|
|
116
|
-
// @ts-ignore: type
|
|
117
|
-
out.push(JSON.__deserialize<JSON.Value>(srcStart, (srcStart += 8)));
|
|
118
|
-
// console.log("Value (bool): " + ptrToStr(srcStart - 8, srcStart));
|
|
119
|
-
// while (isSpace(load<u16>((srcStart += 2)))) {
|
|
120
|
-
// /* empty */
|
|
121
|
-
// }
|
|
122
|
-
// console.log("next: " + String.fromCharCode(load<u16>(srcStart)));
|
|
123
|
-
}
|
|
124
|
-
} else if (code == CHAR_F) {
|
|
125
|
-
if (load<u64>(srcStart, 2) == 28429466576093281) {
|
|
126
|
-
// @ts-ignore: type
|
|
127
|
-
out.push(JSON.__deserialize<JSON.Value>(srcStart, (srcStart += 10)));
|
|
128
|
-
// console.log("Value (bool): " + ptrToStr(srcStart - 10, srcStart));
|
|
129
|
-
// while (isSpace(load<u16>((srcStart += 2)))) {
|
|
130
|
-
// /* empty */
|
|
131
|
-
// }
|
|
132
|
-
// console.log("next: " + String.fromCharCode(load<u16>(srcStart)));
|
|
133
|
-
}
|
|
134
|
-
} else if (code == CHAR_N) {
|
|
135
|
-
if (load<u64>(srcStart) == 30399761348886638) {
|
|
136
|
-
// console.log("Value (null): " + ptrToStr(srcStart, srcStart + 8));
|
|
137
|
-
// @ts-ignore: type
|
|
138
|
-
out.push(JSON.__deserialize<JSON.Value>(srcStart, (srcStart += 8)));
|
|
139
|
-
// while (isSpace(load<u16>((srcStart += 2)))) {
|
|
140
|
-
// /* empty */
|
|
141
|
-
// }
|
|
142
|
-
}
|
|
143
|
-
}
|
|
144
|
-
srcStart += 2;
|
|
145
|
-
}
|
|
146
|
-
// @ts-ignore: type
|
|
12
|
+
// Skip the opening '[' and parse elements single-pass until the matching ']'.
|
|
13
|
+
parseArrayBody(out, srcStart + 2, srcEnd);
|
|
147
14
|
return out;
|
|
148
15
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { BRACKET_LEFT, BRACKET_RIGHT } from "../../../custom/chars";
|
|
2
2
|
import { JSON } from "../../../";
|
|
3
|
+
import { parseArrayBody } from "../object";
|
|
3
4
|
|
|
4
5
|
export function deserializeArrayArray<T extends unknown[][]>(
|
|
5
6
|
srcStart: usize,
|
|
@@ -9,9 +10,37 @@ export function deserializeArrayArray<T extends unknown[][]>(
|
|
|
9
10
|
const out = changetype<nonnull<T>>(
|
|
10
11
|
dst || changetype<usize>(instantiate<T>()),
|
|
11
12
|
);
|
|
13
|
+
|
|
14
|
+
// Compile-time: is each inner array a dynamic JSON.Value[] (vs a typed array)?
|
|
15
|
+
let arbitraryInner = false;
|
|
16
|
+
if (isManaged<valueof<valueof<T>>>() || isReference<valueof<valueof<T>>>()) {
|
|
17
|
+
// @ts-ignore: instanceof on the (reference) inner element type
|
|
18
|
+
arbitraryInner =
|
|
19
|
+
changetype<nonnull<valueof<valueof<T>>>>(0) instanceof JSON.Value;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
srcStart += 2; // skip the outer '['
|
|
23
|
+
|
|
24
|
+
if (isReference<valueof<valueof<T>>>() && arbitraryInner) {
|
|
25
|
+
// Single-pass: each inner `[...]` is a JSON.Value[] parsed in one scan
|
|
26
|
+
// (this also correctly skips ']' inside strings, unlike the depth scan).
|
|
27
|
+
while (srcStart < srcEnd) {
|
|
28
|
+
if (load<u16>(srcStart) == BRACKET_LEFT) {
|
|
29
|
+
const inner = instantiate<JSON.Value[]>();
|
|
30
|
+
srcStart = parseArrayBody(inner, srcStart + 2, srcEnd);
|
|
31
|
+
// @ts-ignore: valueof<T> is JSON.Value[] in this branch
|
|
32
|
+
out.push(changetype<valueof<T>>(changetype<usize>(inner)));
|
|
33
|
+
} else {
|
|
34
|
+
srcStart += 2;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
return out;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Typed inner arrays: scan each element's bounds, then hand the exact range
|
|
41
|
+
// to its generated (bounds-taking) deserializer.
|
|
12
42
|
let lastIndex: usize = 0;
|
|
13
43
|
let depth: u32 = 0;
|
|
14
|
-
srcStart += 2;
|
|
15
44
|
while (srcStart < srcEnd - 2) {
|
|
16
45
|
const code = load<u16>(srcStart);
|
|
17
46
|
if (code == BRACKET_LEFT && depth++ == 0) {
|
|
@@ -4,12 +4,7 @@ import { COMMA, BRACKET_LEFT, BRACKET_RIGHT } from "../../../custom/chars";
|
|
|
4
4
|
// Strict RFC 8259 integer-token check over [start, end): optional minus (signed
|
|
5
5
|
// types only), then a lone `0` or [1-9] digits — no leading zeros, fraction,
|
|
6
6
|
// exponent, or trailing garbage. Throws otherwise.
|
|
7
|
-
|
|
8
|
-
@inline function validateJSONInteger(
|
|
9
|
-
start: usize,
|
|
10
|
-
end: usize,
|
|
11
|
-
signed: bool,
|
|
12
|
-
): void {
|
|
7
|
+
function validateJSONInteger(start: usize, end: usize, signed: bool): void {
|
|
13
8
|
let ptr = start;
|
|
14
9
|
if (ptr < end && load<u16>(ptr) == 45) {
|
|
15
10
|
if (!signed) throw new Error("Invalid JSON number: minus on unsigned");
|
|
@@ -1,11 +1,7 @@
|
|
|
1
|
-
import {
|
|
2
|
-
BRACE_LEFT,
|
|
3
|
-
BRACE_RIGHT,
|
|
4
|
-
BRACKET_LEFT,
|
|
5
|
-
BRACKET_RIGHT,
|
|
6
|
-
} from "../../../custom/chars";
|
|
1
|
+
import { BRACE_LEFT, BRACKET_LEFT, BRACKET_RIGHT } from "../../../custom/chars";
|
|
7
2
|
import { JSON } from "../../..";
|
|
8
3
|
import { isSpace } from "util/string";
|
|
4
|
+
import { deserializeMapBody } from "../map";
|
|
9
5
|
|
|
10
6
|
export function deserializeMapArray<T extends Map<any, any>[]>(
|
|
11
7
|
srcStart: usize,
|
|
@@ -15,8 +11,6 @@ export function deserializeMapArray<T extends Map<any, any>[]>(
|
|
|
15
11
|
const out = changetype<nonnull<T>>(
|
|
16
12
|
dst || changetype<usize>(instantiate<T>()),
|
|
17
13
|
);
|
|
18
|
-
let lastIndex: usize = 0;
|
|
19
|
-
let depth: u32 = 0;
|
|
20
14
|
|
|
21
15
|
while (srcEnd > srcStart && isSpace(load<u16>(srcEnd - 2))) srcEnd -= 2;
|
|
22
16
|
|
|
@@ -34,14 +28,16 @@ export function deserializeMapArray<T extends Map<any, any>[]>(
|
|
|
34
28
|
(srcEnd - srcStart).toString(),
|
|
35
29
|
);
|
|
36
30
|
|
|
31
|
+
// Each `{...}` map element is parsed in a single pass via deserializeMapBody,
|
|
32
|
+
// which reports where it ended — no separate scan to find the closing brace.
|
|
37
33
|
while (srcStart < srcEnd) {
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
34
|
+
if (load<u16>(srcStart) == BRACE_LEFT) {
|
|
35
|
+
const m = instantiate<valueof<T>>();
|
|
36
|
+
srcStart = deserializeMapBody<valueof<T>>(srcStart, srcEnd, m);
|
|
37
|
+
out.push(m);
|
|
38
|
+
} else {
|
|
39
|
+
srcStart += 2;
|
|
43
40
|
}
|
|
44
|
-
srcStart += 2;
|
|
45
41
|
}
|
|
46
42
|
return out;
|
|
47
43
|
}
|
|
@@ -1,11 +1,7 @@
|
|
|
1
|
-
import {
|
|
2
|
-
BRACE_LEFT,
|
|
3
|
-
BRACE_RIGHT,
|
|
4
|
-
BRACKET_LEFT,
|
|
5
|
-
BRACKET_RIGHT,
|
|
6
|
-
} from "../../../custom/chars";
|
|
1
|
+
import { BRACE_LEFT, BRACKET_LEFT, BRACKET_RIGHT } from "../../../custom/chars";
|
|
7
2
|
import { JSON } from "../../..";
|
|
8
3
|
import { isSpace } from "util/string";
|
|
4
|
+
import { parseObjectBody } from "../object";
|
|
9
5
|
|
|
10
6
|
export function deserializeObjectArray<T extends unknown[]>(
|
|
11
7
|
srcStart: usize,
|
|
@@ -15,8 +11,6 @@ export function deserializeObjectArray<T extends unknown[]>(
|
|
|
15
11
|
const out = changetype<nonnull<T>>(
|
|
16
12
|
dst || changetype<usize>(instantiate<T>()),
|
|
17
13
|
);
|
|
18
|
-
let lastIndex: usize = 0;
|
|
19
|
-
let depth: u32 = 0;
|
|
20
14
|
|
|
21
15
|
while (srcEnd > srcStart && isSpace(load<u16>(srcEnd - 2))) srcEnd -= 2;
|
|
22
16
|
|
|
@@ -34,14 +28,16 @@ export function deserializeObjectArray<T extends unknown[]>(
|
|
|
34
28
|
(srcEnd - srcStart).toString(),
|
|
35
29
|
);
|
|
36
30
|
|
|
31
|
+
// Each `{...}` element is parsed in a single pass via parseObjectBody, which
|
|
32
|
+
// reports where it ended — no separate scan to find the closing brace.
|
|
37
33
|
while (srcStart < srcEnd) {
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
34
|
+
if (load<u16>(srcStart) == BRACE_LEFT) {
|
|
35
|
+
const obj = new JSON.Obj();
|
|
36
|
+
srcStart = parseObjectBody(obj, srcStart + 2, srcEnd);
|
|
37
|
+
out.push(changetype<valueof<T>>(changetype<usize>(obj)));
|
|
38
|
+
} else {
|
|
39
|
+
srcStart += 2;
|
|
43
40
|
}
|
|
44
|
-
srcStart += 2;
|
|
45
41
|
}
|
|
46
42
|
return out;
|
|
47
43
|
}
|
|
@@ -7,8 +7,7 @@ import { isSpace } from "../../util";
|
|
|
7
7
|
// garbage. f64.parse alone is lenient (parses a numeric prefix and ignores the
|
|
8
8
|
// rest), so this guard is what makes the naive value path reject malformed
|
|
9
9
|
// numbers like `0e`, `-01`, `1.`, `2.e3`, `0x42`.
|
|
10
|
-
|
|
11
|
-
@inline function validateJSONNumber(srcStart: usize, srcEnd: usize): void {
|
|
10
|
+
function validateJSONNumber(srcStart: usize, srcEnd: usize): void {
|
|
12
11
|
let ptr = srcStart;
|
|
13
12
|
while (ptr < srcEnd && isSpace(load<u16>(ptr))) ptr += 2;
|
|
14
13
|
let end = srcEnd;
|
|
@@ -76,8 +75,7 @@ import { isSpace } from "../../util";
|
|
|
76
75
|
return f32.parse(ptrToStr(srcStart, srcEnd));
|
|
77
76
|
}
|
|
78
77
|
|
|
79
|
-
|
|
80
|
-
@inline function scanFloatEnd(srcStart: usize, srcEnd: usize): usize {
|
|
78
|
+
function scanFloatEnd(srcStart: usize, srcEnd: usize): usize {
|
|
81
79
|
let ptr = srcStart;
|
|
82
80
|
if (ptr < srcEnd && load<u16>(ptr) == 45) ptr += 2; // optional minus
|
|
83
81
|
|
|
@@ -8,8 +8,7 @@ import { atoi } from "../../util/atoi";
|
|
|
8
8
|
return atoi<T>(srcStart, srcEnd);
|
|
9
9
|
}
|
|
10
10
|
|
|
11
|
-
|
|
12
|
-
@inline export function deserializeIntegerField_NAIVE<T extends number>(
|
|
11
|
+
export function deserializeIntegerField_NAIVE<T extends number>(
|
|
13
12
|
srcStart: usize,
|
|
14
13
|
srcEnd: usize,
|
|
15
14
|
dstObj: usize,
|