json-as 1.5.0 → 1.6.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 (66) hide show
  1. package/CHANGELOG.md +5 -1
  2. package/README.md +42 -0
  3. package/assembly/deserialize/error.ts +21 -0
  4. package/assembly/deserialize/index/arbitrary.ts +29 -8
  5. package/assembly/deserialize/index/array.ts +2 -1
  6. package/assembly/deserialize/index/bool.ts +1 -1
  7. package/assembly/deserialize/index/object.ts +3 -0
  8. package/assembly/deserialize/index/string.ts +37 -0
  9. package/assembly/deserialize/index.ts +0 -1
  10. package/assembly/deserialize/naive/array/arbitrary.ts +3 -2
  11. package/assembly/deserialize/naive/array/array.ts +1 -0
  12. package/assembly/deserialize/naive/array/bool.ts +21 -11
  13. package/assembly/deserialize/naive/array/box.ts +13 -33
  14. package/assembly/deserialize/naive/array/float.ts +23 -13
  15. package/assembly/deserialize/naive/array/integer.ts +33 -21
  16. package/assembly/deserialize/naive/array/object.ts +1 -0
  17. package/assembly/deserialize/naive/array/string.ts +29 -16
  18. package/assembly/deserialize/naive/bool.ts +17 -5
  19. package/assembly/deserialize/naive/float.ts +1 -2
  20. package/assembly/deserialize/naive/map.ts +339 -39
  21. package/assembly/deserialize/naive/object.ts +122 -65
  22. package/assembly/deserialize/naive/raw.ts +13 -1
  23. package/assembly/deserialize/naive/set.ts +0 -1
  24. package/assembly/deserialize/naive/staticarray.ts +3 -2
  25. package/assembly/deserialize/naive/string.ts +41 -52
  26. package/assembly/deserialize/parseMode.ts +216 -0
  27. package/assembly/deserialize/simd/array/integer.ts +6 -3
  28. package/assembly/deserialize/simd/float.ts +35 -29
  29. package/assembly/deserialize/simd/string.ts +79 -17
  30. package/assembly/deserialize/string-validation.ts +34 -0
  31. package/assembly/deserialize/swar/array/array.ts +63 -13
  32. package/assembly/deserialize/swar/array/bool.ts +14 -3
  33. package/assembly/deserialize/swar/array/float.ts +394 -13
  34. package/assembly/deserialize/swar/array/generic.ts +12 -2
  35. package/assembly/deserialize/swar/array/integer.ts +22 -62
  36. package/assembly/deserialize/swar/array/object.ts +11 -2
  37. package/assembly/deserialize/swar/array/shared.ts +1 -1
  38. package/assembly/deserialize/swar/array/string.ts +34 -5
  39. package/assembly/deserialize/swar/array/struct.ts +109 -15
  40. package/assembly/deserialize/swar/float.ts +19 -12
  41. package/assembly/deserialize/swar/string.ts +70 -111
  42. package/assembly/index.d.ts +6 -0
  43. package/assembly/index.ts +411 -33
  44. package/assembly/serialize/index/bool.ts +1 -1
  45. package/assembly/serialize/index/float.ts +1 -5
  46. package/assembly/serialize/index/integer.ts +1 -1
  47. package/assembly/serialize/index/object.ts +17 -0
  48. package/assembly/serialize/naive/array.ts +64 -0
  49. package/assembly/serialize/naive/float.ts +0 -5
  50. package/assembly/serialize/naive/map.ts +37 -0
  51. package/assembly/serialize/naive/set.ts +0 -1
  52. package/assembly/serialize/naive/staticarray.ts +0 -1
  53. package/assembly/serialize/naive/string.ts +0 -6
  54. package/assembly/serialize/naive/typedarray.ts +0 -1
  55. package/assembly/util/eisel-lemire.ts +179 -0
  56. package/assembly/util/prettyWhitespaceSimd.ts +69 -0
  57. package/assembly/util/scanValueEndSimd.ts +59 -36
  58. package/assembly/util/validateJson.ts +338 -0
  59. package/lib/as-bs.ts +28 -6
  60. package/package.json +13 -3
  61. package/transform/lib/index.d.ts +6 -0
  62. package/transform/lib/index.js +770 -111
  63. package/transform/lib/types.d.ts +1 -0
  64. package/transform/lib/types.js +3 -0
  65. package/assembly/deserialize/index/struct.ts +0 -1
  66. package/assembly/deserialize/naive/struct.ts +0 -21
package/CHANGELOG.md CHANGED
@@ -1,6 +1,10 @@
1
1
  # Changelog
2
2
 
3
- ## Unreleased
3
+ ## 2026-08-03 - v1.6.0
4
+
5
+ - perf: much faster deserialization by using default values as a cache.
6
+ - fix(transform): resolve imported nested custom types across source-file dependencies and invoke their custom deserializers, including for nullable and reordered fields ([#215](https://github.com/JairusSW/json-as/pull/215)).
7
+ - chore: add Guillermo Diez to the contributor list.
4
8
 
5
9
  ## 2026-06-10 - v1.5.0
6
10
 
package/README.md CHANGED
@@ -604,6 +604,48 @@ How `json-as` stacks up against other JSON libraries on a ~5 KiB GitHub-repo pay
604
604
 
605
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
606
 
607
+ ### Runtime Comparison
608
+
609
+ How fast the **same** `json-as` classic bench deserializes the minified payloads across six WebAssembly runtimes — including [WARP](https://github.com/wasm-ecosystem/wasm-compiler), a single-pass compiler built for embedded targets, alongside the optimizing JITs (Wasmtime, WAVM), the pure-Go wazero, and JS engines (V8, Bun).
610
+
611
+ Each runtime runs the real `bench()` lib (warm up, time the loop, report MB/s itself) on a `NAIVE`-mode build with a shared feature set (no SIMD / bulk-memory / non-trapping float-to-int) so the executed code is equivalent. The WASI runtimes read the payload over WASI; V8/Bun via an `env`-ABI host. WARP has no WASI and — by design ("no recursions") — can't re-enter the module from a host import, so it runs through a small custom C++ host that links `performance.now`/`console.log`/`writeFile` with the payload embedded. The timed run is split into small frames with a full GC between them (the bench lib's `BENCH_FRAMES`, applied to every runtime so the measurement is identical); this excludes stop-the-world GC pauses and keeps WARP — an embedded, single-shot-oriented compiler — inside its stable envelope. Even so, WARP's single-pass codegen lands within a few percent of the optimizing JITs.
612
+
613
+ <img src="https://raw.githubusercontent.com/JairusSW/json-as/refs/heads/docs/charts/v1.5.0/runtimes-deserialize.svg" alt="Deserialization throughput across WebAssembly runtimes">
614
+
615
+ <details>
616
+ <summary>Serialization throughput (click to expand)</summary>
617
+
618
+ <img src="https://raw.githubusercontent.com/JairusSW/json-as/refs/heads/docs/charts/v1.5.0/runtimes-serialize.svg" alt="Serialization throughput across WebAssembly runtimes">
619
+ </details>
620
+
621
+ Reproduce locally (the JS engines and standalone runtimes are auto-detected; point `WARP_SRC` at a [wasm-ecosystem/wasm-compiler](https://github.com/wasm-ecosystem/wasm-compiler) checkout with its libs built — see the script header for the cmake flags):
622
+
623
+ ```bash
624
+ WARP_SRC=/path/to/wasm-compiler npm run bench:runtimes
625
+ npm run charts:runtimes
626
+ ```
627
+
628
+ ### Compiler Comparison
629
+
630
+ A different axis: same source, same runtime (V8), different **compiler**. [warpo](https://github.com/wasm-ecosystem/warpo) is a next-generation AssemblyScript compiler; this compares `json-as` when built with stock `asc` vs `warpo`, each before and after a `wasm-opt -O4` pass.
631
+
632
+ warpo produces consistently faster code on both directions, and the gap widens with number/structure density (Canada). `wasm-opt -O4` barely moves either (both already run binaryen optimizations), so the difference is the compilers' own codegen, not a missing post-pass.
633
+
634
+ <img src="https://raw.githubusercontent.com/JairusSW/json-as/refs/heads/docs/charts/v1.5.0/warpo-vs-asc-deserialize.svg" alt="json-as deserialization: asc vs warpo compiler on V8">
635
+
636
+ <details>
637
+ <summary>Serialization (click to expand)</summary>
638
+
639
+ <img src="https://raw.githubusercontent.com/JairusSW/json-as/refs/heads/docs/charts/v1.5.0/warpo-vs-asc-serialize.svg" alt="json-as serialization: asc vs warpo compiler on V8">
640
+ </details>
641
+
642
+ warpo can't run asc transform plugins, so the bench compiles the post-transform `.tmp.ts` (emitted via `JSON_WRITE`); see `scripts/run-bench.warpo.sh`. Reproduce:
643
+
644
+ ```bash
645
+ npm run bench:warpo # builds with asc + warpo, wasm-opt -O4 each, runs on v8
646
+ npm run charts:warpo
647
+ ```
648
+
607
649
  ### Lazy Fields
608
650
 
609
651
  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.
@@ -0,0 +1,21 @@
1
+ // Malformed-range failures are recorded only on the cold error path and
2
+ // consumed at a public JSON.parse boundary. Keeping the final throw there
3
+ // makes it catchable by try-as without adding a success-path store or a
4
+ // second validation scan.
5
+ let productionParseError = false;
6
+
7
+ export function markProductionParseError(): void {
8
+ productionParseError = true;
9
+ }
10
+
11
+ export function takeProductionParseError(): bool {
12
+ if (!productionParseError) return false;
13
+ productionParseError = false;
14
+ return true;
15
+ }
16
+
17
+ /** Mark a malformed cold path and return the shared zero cursor sentinel. */
18
+ export function failProductionParse(): usize {
19
+ productionParseError = true;
20
+ return 0;
21
+ }
@@ -1,22 +1,26 @@
1
1
  import { JSON } from "../..";
2
- import { deserializeBoolean } from "./bool";
2
+ import { deserializeBooleanCode } from "./bool";
3
3
  import { deserializeFloat } from "./float";
4
4
  import { deserializeObject, deserializeJsonArray, getParseSrc } from "./object";
5
5
  import { deserializeString } from "./string";
6
6
  import { BRACE_LEFT, BRACKET_LEFT, CHAR_N, QUOTE } from "../../custom/chars";
7
7
 
8
+ const NULL_WORD: u64 = 30399761348886638;
9
+
8
10
  export function deserializeArbitrary(
9
11
  srcStart: usize,
10
12
  srcEnd: usize,
11
13
  dst: usize,
12
14
  ): JSON.Value {
13
15
  const v = parseArbitraryValue(srcStart, srcEnd);
16
+ if (changetype<usize>(v) == 0) return v;
14
17
  // Reuse path (`JSON.parse<JSON.Value>(data, out)`): write the parsed bits into
15
18
  // the caller's handle (with the GC barrier for any managed payload).
16
19
  return dst != 0 ? JSON.Value.__adoptInto(dst, v) : v;
17
20
  }
18
21
 
19
22
  function parseArbitraryValue(srcStart: usize, srcEnd: usize): JSON.Value {
23
+ if (srcStart >= srcEnd) return changetype<JSON.Value>(0);
20
24
  const firstChar = load<u16>(srcStart);
21
25
  if (
22
26
  firstChar == QUOTE ||
@@ -29,19 +33,36 @@ function parseArbitraryValue(srcStart: usize, srcEnd: usize): JSON.Value {
29
33
  const src = getParseSrc();
30
34
  if (src.length != 0) {
31
35
  const end = JSON.Util.scanValueEnd<JSON.Value>(srcStart, srcEnd);
36
+ // A zero end is the scanner's failure sentinel. Never retain it in a
37
+ // lazy slice: subtracting the source base from zero would underflow and
38
+ // later materialization could read outside the source value.
39
+ if (end == 0) return changetype<JSON.Value>(0);
32
40
  return JSON.Value.fromSlice(srcStart, end, src);
33
41
  }
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));
42
+ if (firstChar == QUOTE) {
43
+ const value = deserializeString(srcStart, srcEnd);
44
+ return changetype<usize>(value) != 0
45
+ ? JSON.Value.from(value)
46
+ : changetype<JSON.Value>(0);
47
+ }
48
+ const composite =
49
+ firstChar == BRACE_LEFT
50
+ ? changetype<usize>(deserializeObject(srcStart, srcEnd, 0))
51
+ : changetype<usize>(deserializeJsonArray(srcStart, srcEnd, 0));
52
+ return composite != 0
53
+ ? firstChar == BRACE_LEFT
54
+ ? JSON.Value.from(changetype<JSON.Obj>(composite))
55
+ : JSON.Value.from(changetype<JSON.Arr>(composite))
56
+ : changetype<JSON.Value>(0);
39
57
  } else if (firstChar - 48 <= 9 || firstChar == 45) {
40
58
  return JSON.Value.from(deserializeFloat<f64>(srcStart, srcEnd));
41
59
  } else if (firstChar == 116 || firstChar == 102) {
42
- return JSON.Value.from(deserializeBoolean(srcStart, srcEnd));
60
+ const code = deserializeBooleanCode(srcStart, srcEnd);
61
+ return code != 0 ? JSON.Value.from(code == 2) : changetype<JSON.Value>(0);
43
62
  } else if (firstChar == CHAR_N) {
63
+ if (srcEnd - srcStart < 8 || load<u64>(srcStart) != NULL_WORD)
64
+ return changetype<JSON.Value>(0);
44
65
  return JSON.Value.from<usize>(0);
45
66
  }
46
- return unreachable();
67
+ return changetype<JSON.Value>(0);
47
68
  }
@@ -11,6 +11,7 @@ import { deserializeObjectArray } from "../naive/array/object";
11
11
  import { deserializeRawArray } from "../naive/array/raw";
12
12
  import { deserializeStringArray_NAIVE } from "../naive/array/string";
13
13
  import { deserializeStructArray } from "../naive/array/struct";
14
+ import { deserializeStructArray_SWAR } from "../swar/array/struct";
14
15
  import { deserializeIntegerArray_SIMD } from "../simd/array/integer";
15
16
  import { deserializeIntegerArray_SWAR } from "../swar/array/integer";
16
17
  import { deserializeFloatArray_SWAR } from "../swar/array/float";
@@ -82,7 +83,7 @@ export function deserializeArray<T extends unknown[]>(
82
83
  isDefined(type.__DESERIALIZE_SLOW) ||
83
84
  isDefined(type.__DESERIALIZE_FAST)
84
85
  ) {
85
- return deserializeStructArray<T>(srcStart, srcEnd, dst);
86
+ return deserializeStructArray_SWAR<T>(srcStart, srcEnd, dst);
86
87
  }
87
88
  throw new Error("Could not parse array of type " + nameof<T>() + "!");
88
89
  } else {
@@ -1 +1 @@
1
- export { deserializeBoolean } from "../naive/bool";
1
+ export { deserializeBoolean, deserializeBooleanCode } from "../naive/bool";
@@ -3,4 +3,7 @@ export {
3
3
  deserializeJsonArray,
4
4
  setParseSrc,
5
5
  getParseSrc,
6
+ markProductionParseError,
7
+ takeProductionParseError,
6
8
  } from "../naive/object";
9
+ export { failProductionParse } from "../error";
@@ -2,17 +2,24 @@ import { JSONMode } from "../..";
2
2
  import {
3
3
  deserializeString_NAIVE,
4
4
  deserializeStringField_NAIVE,
5
+ deserializeStringFieldTrusted_NAIVE,
5
6
  } from "../naive/string";
6
7
  import {
7
8
  deserializeString_SIMD,
8
9
  deserializeStringField_SIMD,
10
+ deserializeStringFieldTrusted_SIMD,
9
11
  } from "../simd/string";
10
12
  import {
11
13
  deserializeString_SWAR,
12
14
  deserializeStringField_SWAR,
15
+ deserializeStringFieldTrusted_SWAR,
13
16
  } from "../swar/string";
14
17
 
15
18
  export function deserializeString(srcStart: usize, srcEnd: usize): string {
19
+ // Whole-value decoders strip two UTF-16 code units before entering their
20
+ // optimized loops. Guard the actual memory-safety invariant here; complete
21
+ // RFC quote framing remains the strict-mode validator's job.
22
+ if (srcEnd - srcStart < 4) return changetype<string>(0);
16
23
  if (JSON_MODE == JSONMode.SIMD) {
17
24
  return deserializeString_SIMD(srcStart, srcEnd);
18
25
  } else if (JSON_MODE == JSONMode.NAIVE) {
@@ -36,3 +43,33 @@ export function deserializeStringField<T extends string | null>(
36
43
  return deserializeStringField_SWAR<T>(srcStart, srcEnd, dstObj, dstOffset);
37
44
  }
38
45
  }
46
+
47
+ export function deserializeStringFieldTrusted(
48
+ payloadStart: usize,
49
+ srcEnd: usize,
50
+ dstObj: usize,
51
+ dstOffset: usize = 0,
52
+ ): usize {
53
+ if (JSON_MODE == JSONMode.SIMD) {
54
+ return deserializeStringFieldTrusted_SIMD(
55
+ payloadStart,
56
+ srcEnd,
57
+ dstObj,
58
+ dstOffset,
59
+ );
60
+ } else if (JSON_MODE == JSONMode.NAIVE) {
61
+ return deserializeStringFieldTrusted_NAIVE(
62
+ payloadStart,
63
+ srcEnd,
64
+ dstObj,
65
+ dstOffset,
66
+ );
67
+ } else {
68
+ return deserializeStringFieldTrusted_SWAR(
69
+ payloadStart,
70
+ srcEnd,
71
+ dstObj,
72
+ dstOffset,
73
+ );
74
+ }
75
+ }
@@ -10,6 +10,5 @@ export * from "./index/raw";
10
10
  export * from "./index/set";
11
11
  export * from "./index/staticarray";
12
12
  export * from "./index/string";
13
- export * from "./index/struct";
14
13
  export * from "./index/typedarray";
15
14
  export * from "./index/unsigned";
@@ -10,6 +10,7 @@ export function deserializeArbitraryArray(
10
10
  dst || changetype<usize>(instantiate<JSON.Value[]>()),
11
11
  );
12
12
  // Skip the opening '[' and parse elements single-pass until the matching ']'.
13
- parseArrayBody(out, srcStart + 2, srcEnd);
14
- return out;
13
+ return parseArrayBody(out, srcStart + 2, srcEnd) != 0
14
+ ? out
15
+ : changetype<JSON.Value[]>(0);
15
16
  }
@@ -28,6 +28,7 @@ export function deserializeArrayArray<T extends unknown[][]>(
28
28
  if (load<u16>(srcStart) == BRACKET_LEFT) {
29
29
  const inner = instantiate<JSON.Value[]>();
30
30
  srcStart = parseArrayBody(inner, srcStart + 2, srcEnd);
31
+ if (srcStart == 0) return changetype<T>(0);
31
32
  // @ts-ignore: valueof<T> is JSON.Value[] in this branch
32
33
  out.push(changetype<valueof<T>>(changetype<usize>(inner)));
33
34
  } else {
@@ -6,13 +6,15 @@ import {
6
6
  FALSE_WORD_U64,
7
7
  TRUE_WORD_U64,
8
8
  } from "../../../custom/chars";
9
+ import { markProductionParseError } from "../../error";
9
10
 
10
11
  /**
11
12
  * Strict boolean-array deserializer (`bool[]`, every JSON_MODE).
12
13
  *
13
14
  * Enforces RFC 8259 array structure: `[`-framed, single-comma separated, no
14
15
  * leading / trailing / doubled commas, and each element must be exactly the
15
- * literal `true` or `false`. Throws on any deviation.
16
+ * literal `true` or `false`. Returns the shared failure sentinel on any
17
+ * deviation.
16
18
  *
17
19
  * The token check is SWAR-shaped: one `u64` load matches all four chars of
18
20
  * `true`; `false` adds one `u16` load to confirm the trailing `e`.
@@ -27,12 +29,15 @@ export function deserializeBooleanArray<T extends boolean[]>(
27
29
  );
28
30
  out.length = 0; // dst may arrive pre-sized; re-parse from empty via push
29
31
 
30
- while (srcStart < srcEnd && isSpace(load<u16>(srcStart))) srcStart += 2;
31
32
  while (srcEnd > srcStart && isSpace(load<u16>(srcEnd - 2))) srcEnd -= 2;
32
- if (srcStart >= srcEnd || load<u16>(srcStart) != BRACKET_LEFT)
33
- throw new Error("Invalid JSON array: expected '['");
34
- if (load<u16>(srcEnd - 2) != BRACKET_RIGHT)
35
- throw new Error("Invalid JSON array: expected ']'");
33
+ if (
34
+ srcStart >= srcEnd ||
35
+ load<u16>(srcStart) != BRACKET_LEFT ||
36
+ load<u16>(srcEnd - 2) != BRACKET_RIGHT
37
+ ) {
38
+ markProductionParseError();
39
+ return changetype<T>(0);
40
+ }
36
41
  srcStart += 2; // past '['
37
42
  srcEnd -= 2; // before ']'
38
43
 
@@ -51,17 +56,22 @@ export function deserializeBooleanArray<T extends boolean[]>(
51
56
  out.push(false);
52
57
  srcStart += 10;
53
58
  } else {
54
- throw new Error("Invalid JSON array: expected 'true' or 'false'");
59
+ markProductionParseError();
60
+ return changetype<T>(0);
55
61
  }
56
62
 
57
63
  while (srcStart < srcEnd && isSpace(load<u16>(srcStart))) srcStart += 2;
58
64
  if (srcStart >= srcEnd) break;
59
- if (load<u16>(srcStart) != COMMA)
60
- throw new Error("Invalid JSON array: expected ',' or ']'");
65
+ if (load<u16>(srcStart) != COMMA) {
66
+ markProductionParseError();
67
+ return changetype<T>(0);
68
+ }
61
69
  srcStart += 2;
62
70
  while (srcStart < srcEnd && isSpace(load<u16>(srcStart))) srcStart += 2;
63
- if (srcStart >= srcEnd)
64
- throw new Error("Invalid JSON array: trailing comma");
71
+ if (srcStart >= srcEnd) {
72
+ markProductionParseError();
73
+ return changetype<T>(0);
74
+ }
65
75
  }
66
76
 
67
77
  return out;
@@ -10,42 +10,22 @@ export function deserializeBoxArray<T extends JSON.Box<any>[]>(
10
10
  const out = changetype<nonnull<T>>(
11
11
  dst || changetype<usize>(instantiate<T>()),
12
12
  );
13
- if (isBoolean<valueof<T>>()) {
14
- srcStart += 2; // skip [
15
- while (srcStart < srcEnd) {
16
- const block = load<u64>(srcStart);
17
- if (block == 28429475166421108) {
18
- out.push(JSON.Box.from(true));
19
- srcStart += 10;
20
- } else if (block == 32370086184550502 && load<u16>(srcStart, 8) == 101) {
21
- out.push(JSON.Box.from(false));
22
- srcStart += 12;
23
- } else {
24
- srcStart += 2;
25
- }
26
- }
27
- return out;
28
- } else {
29
- let lastIndex: usize = 0;
30
- while (srcStart < srcEnd) {
31
- const code = load<u16>(srcStart);
32
- if (code - 48 <= 9 || code == 45) {
33
- lastIndex = srcStart;
34
- srcStart += 2;
35
- while (srcStart < srcEnd) {
36
- const code = load<u16>(srcStart);
37
- if (code == COMMA || code == BRACKET_RIGHT || isSpace(code)) {
38
- out.push(JSON.__deserialize<valueof<T>>(lastIndex, srcStart));
39
- // while (isSpace(load<u16>((srcStart += 2)))) {
40
- // /* empty */
41
- // }
42
- break;
43
- }
44
- srcStart += 2;
13
+ let lastIndex: usize = 0;
14
+ while (srcStart < srcEnd) {
15
+ const code = load<u16>(srcStart);
16
+ if (code - 48 <= 9 || code == 45) {
17
+ lastIndex = srcStart;
18
+ srcStart += 2;
19
+ while (srcStart < srcEnd) {
20
+ const code = load<u16>(srcStart);
21
+ if (code == COMMA || code == BRACKET_RIGHT || isSpace(code)) {
22
+ out.push(JSON.__deserialize<valueof<T>>(lastIndex, srcStart));
23
+ break;
45
24
  }
25
+ srcStart += 2;
46
26
  }
47
- srcStart += 2;
48
27
  }
28
+ srcStart += 2;
49
29
  }
50
30
  return out;
51
31
  }
@@ -1,6 +1,7 @@
1
1
  import { isSpace } from "../../../util";
2
2
  import { COMMA, BRACKET_LEFT, BRACKET_RIGHT } from "../../../custom/chars";
3
3
  import { JSON } from "../../..";
4
+ import { markProductionParseError } from "../../error";
4
5
 
5
6
  /**
6
7
  * Strict float-array deserializer (`f64[]` / `f32[]`).
@@ -8,8 +9,8 @@ import { JSON } from "../../..";
8
9
  * Enforces RFC 8259 array structure: `[`-framed, single-comma separated, no
9
10
  * leading / trailing / doubled commas, and no garbage between values. Each
10
11
  * element token is validated by `deserializeFloat_NAIVE` (via `JSON.__deserialize`),
11
- * so malformed numbers like `0e` / `-01` / `1.` are rejected here too. Throws on
12
- * any deviation.
12
+ * so malformed numbers like `0e` / `-01` / `1.` are rejected here too. Returns
13
+ * the shared failure sentinel on any deviation.
13
14
  */
14
15
  export function deserializeFloatArray_NAIVE<T extends number[]>(
15
16
  srcStart: usize,
@@ -24,12 +25,15 @@ export function deserializeFloatArray_NAIVE<T extends number[]>(
24
25
  out.length = 0;
25
26
 
26
27
  // Trim surrounding whitespace and require the enclosing brackets.
27
- while (srcStart < srcEnd && isSpace(load<u16>(srcStart))) srcStart += 2;
28
28
  while (srcEnd > srcStart && isSpace(load<u16>(srcEnd - 2))) srcEnd -= 2;
29
- if (srcStart >= srcEnd || load<u16>(srcStart) != BRACKET_LEFT)
30
- throw new Error("Invalid JSON array: expected '['");
31
- if (load<u16>(srcEnd - 2) != BRACKET_RIGHT)
32
- throw new Error("Invalid JSON array: expected ']'");
29
+ if (
30
+ srcStart >= srcEnd ||
31
+ load<u16>(srcStart) != BRACKET_LEFT ||
32
+ load<u16>(srcEnd - 2) != BRACKET_RIGHT
33
+ ) {
34
+ markProductionParseError();
35
+ return changetype<T>(0);
36
+ }
33
37
  srcStart += 2; // past '['
34
38
  srcEnd -= 2; // before ']'
35
39
 
@@ -45,18 +49,24 @@ export function deserializeFloatArray_NAIVE<T extends number[]>(
45
49
  if (c == COMMA || isSpace(c)) break;
46
50
  srcStart += 2;
47
51
  }
48
- if (srcStart == tokenStart)
49
- throw new Error("Invalid JSON array: missing value");
52
+ if (srcStart == tokenStart) {
53
+ markProductionParseError();
54
+ return changetype<T>(0);
55
+ }
50
56
  out.push(JSON.__deserialize<valueof<T>>(tokenStart, srcStart));
51
57
 
52
58
  while (srcStart < srcEnd && isSpace(load<u16>(srcStart))) srcStart += 2;
53
59
  if (srcStart >= srcEnd) break; // end of array body
54
- if (load<u16>(srcStart) != COMMA)
55
- throw new Error("Invalid JSON array: expected ',' or ']'");
60
+ if (load<u16>(srcStart) != COMMA) {
61
+ markProductionParseError();
62
+ return changetype<T>(0);
63
+ }
56
64
  srcStart += 2; // past ','
57
65
  while (srcStart < srcEnd && isSpace(load<u16>(srcStart))) srcStart += 2;
58
- if (srcStart >= srcEnd)
59
- throw new Error("Invalid JSON array: trailing comma");
66
+ if (srcStart >= srcEnd) {
67
+ markProductionParseError();
68
+ return changetype<T>(0);
69
+ }
60
70
  }
61
71
 
62
72
  return out;
@@ -1,28 +1,28 @@
1
1
  import { atoi, isSpace } from "../../../util";
2
2
  import { COMMA, BRACKET_LEFT, BRACKET_RIGHT } from "../../../custom/chars";
3
+ import { markProductionParseError } from "../../error";
3
4
 
4
5
  // Strict RFC 8259 integer-token check over [start, end): optional minus (signed
5
6
  // types only), then a lone `0` or [1-9] digits - no leading zeros, fraction,
6
- // exponent, or trailing garbage. Throws otherwise.
7
- function validateJSONInteger(start: usize, end: usize, signed: bool): void {
7
+ // exponent, or trailing garbage. Returns false otherwise.
8
+ function validateJSONInteger(start: usize, end: usize, signed: bool): bool {
8
9
  let ptr = start;
9
10
  if (ptr < end && load<u16>(ptr) == 45) {
10
- if (!signed) throw new Error("Invalid JSON number: minus on unsigned");
11
+ if (!signed) return false;
11
12
  ptr += 2;
12
13
  }
13
- if (ptr >= end) throw new Error("Invalid JSON number: expected digit");
14
+ if (ptr >= end) return false;
14
15
  const first = load<u16>(ptr);
15
16
  if (first == 48) {
16
17
  ptr += 2;
17
- if (ptr < end && <u32>(load<u16>(ptr) - 48) <= 9)
18
- throw new Error("Invalid JSON number: leading zero");
18
+ if (ptr < end && <u32>(load<u16>(ptr) - 48) <= 9) return false;
19
19
  } else if (<u32>(first - 48) <= 9) {
20
20
  ptr += 2;
21
21
  while (ptr < end && <u32>(load<u16>(ptr) - 48) <= 9) ptr += 2;
22
22
  } else {
23
- throw new Error("Invalid JSON number: expected digit");
23
+ return false;
24
24
  }
25
- if (ptr != end) throw new Error("Invalid JSON number: not an integer");
25
+ return ptr == end;
26
26
  }
27
27
 
28
28
  /**
@@ -30,7 +30,8 @@ function validateJSONInteger(start: usize, end: usize, signed: bool): void {
30
30
  *
31
31
  * Enforces RFC 8259 array structure: `[`-framed, single-comma separated, no
32
32
  * leading / trailing / doubled commas, and each element must be a valid JSON
33
- * integer (no leading zeros, fraction, or exponent). Throws on any deviation.
33
+ * integer (no leading zeros, fraction, or exponent). Returns the shared failure
34
+ * sentinel on any deviation.
34
35
  */
35
36
  export function deserializeIntegerArray_NAIVE<T extends number[]>(
36
37
  srcStart: usize,
@@ -42,12 +43,15 @@ export function deserializeIntegerArray_NAIVE<T extends number[]>(
42
43
  );
43
44
  out.length = 0; // dst may arrive pre-sized; re-parse from empty via push
44
45
 
45
- while (srcStart < srcEnd && isSpace(load<u16>(srcStart))) srcStart += 2;
46
46
  while (srcEnd > srcStart && isSpace(load<u16>(srcEnd - 2))) srcEnd -= 2;
47
- if (srcStart >= srcEnd || load<u16>(srcStart) != BRACKET_LEFT)
48
- throw new Error("Invalid JSON array: expected '['");
49
- if (load<u16>(srcEnd - 2) != BRACKET_RIGHT)
50
- throw new Error("Invalid JSON array: expected ']'");
47
+ if (
48
+ srcStart >= srcEnd ||
49
+ load<u16>(srcStart) != BRACKET_LEFT ||
50
+ load<u16>(srcEnd - 2) != BRACKET_RIGHT
51
+ ) {
52
+ markProductionParseError();
53
+ return changetype<T>(0);
54
+ }
51
55
  srcStart += 2; // past '['
52
56
  srcEnd -= 2; // before ']'
53
57
 
@@ -62,19 +66,27 @@ export function deserializeIntegerArray_NAIVE<T extends number[]>(
62
66
  if (c == COMMA || isSpace(c)) break;
63
67
  srcStart += 2;
64
68
  }
65
- if (srcStart == tokenStart)
66
- throw new Error("Invalid JSON array: missing value");
67
- validateJSONInteger(tokenStart, srcStart, signed);
69
+ if (
70
+ srcStart == tokenStart ||
71
+ !validateJSONInteger(tokenStart, srcStart, signed)
72
+ ) {
73
+ markProductionParseError();
74
+ return changetype<T>(0);
75
+ }
68
76
  out.push(atoi<valueof<T>>(tokenStart, srcStart));
69
77
 
70
78
  while (srcStart < srcEnd && isSpace(load<u16>(srcStart))) srcStart += 2;
71
79
  if (srcStart >= srcEnd) break;
72
- if (load<u16>(srcStart) != COMMA)
73
- throw new Error("Invalid JSON array: expected ',' or ']'");
80
+ if (load<u16>(srcStart) != COMMA) {
81
+ markProductionParseError();
82
+ return changetype<T>(0);
83
+ }
74
84
  srcStart += 2;
75
85
  while (srcStart < srcEnd && isSpace(load<u16>(srcStart))) srcStart += 2;
76
- if (srcStart >= srcEnd)
77
- throw new Error("Invalid JSON array: trailing comma");
86
+ if (srcStart >= srcEnd) {
87
+ markProductionParseError();
88
+ return changetype<T>(0);
89
+ }
78
90
  }
79
91
 
80
92
  return out;
@@ -34,6 +34,7 @@ export function deserializeObjectArray<T extends unknown[]>(
34
34
  if (load<u16>(srcStart) == BRACE_LEFT) {
35
35
  const obj = new JSON.Obj();
36
36
  srcStart = parseObjectBody(obj, srcStart + 2, srcEnd);
37
+ if (srcStart == 0) return changetype<T>(0);
37
38
  out.push(changetype<valueof<T>>(changetype<usize>(obj)));
38
39
  } else {
39
40
  srcStart += 2;