Skip to content

Commit c23d691

Browse files
committed
release: v1.3.2
1 parent 9b57246 commit c23d691

13 files changed

Lines changed: 66 additions & 1212 deletions

File tree

CHANGELOG.md

Lines changed: 9 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -2,22 +2,15 @@
22

33
## Unreleased
44

5-
- feat: support broad `Map<K,V>` key round-trips by encoding non-string keys as quoted JSON key strings and decoding keys back through typed parse
6-
- feat: allow `JSON.Value` to store and re-serialize built-in typed arrays and `ArrayBuffer`
7-
- feat: let explicit `__SERIALIZE_CUSTOM` / `__DESERIALIZE_CUSTOM` hooks override built-in typed-array and `ArrayBuffer` handling while keeping generated hooks last
8-
- feat: support optional JSON shape hints on `@serializer(...)` / `@deserializer(...)` decorators, defaulting to `any` and allowing nullable forms like `string | null`
9-
- fix: preserve `bs` state across `JSON.internal.stringify(...)` and `JSON.internal.parse(...)`
10-
- fix: make `JSON.Value` follow built-in subclass rules consistently for typed-array subclasses and custom `@json` subclasses
11-
- fix: make generated custom serializer wrappers use the provided `ptr` so indirect-call sites like `JSON.Value` serialize correctly
12-
- fix: stop preloading transform imports through `parser.parseFile(...)`, so repeated `asc()` calls in the same process no longer poison parser state or trip `lookupForeignFile` assertionsfect
13-
- fix: stop naive string deserialization scratch allocations from growing across repeated large payload parses by using local `ensureSize(...)` scratch capacity instead of serialization-style proposed growth
14-
- perf: reserve SIMD string serializer buffer growth once per hit block to reduce dense-escape overhead
15-
- perf: raise the serialization buffer minimum size to 1024 bytes and add adaptive `bs.shrink()`
16-
- perf: add a packed SWAR `u16_to_hex4_swar` helper for `\uXXXX` emission and use it across simple, SWAR, and SIMD string serializers
17-
- perf: rewrite float serialization to Dragonbox per-width writers and share them across arrays, typed arrays, `JSON.Value`, and `JSON.stringify`
18-
- perf: tune the Canada benchmark serialization pipeline with buffered estimates, delimiter helpers, and feature-segment concatenation to eliminate repeated `bs` growth
19-
- bench: add SWAR hex and SWAR string serializer head-to-head microbenchmarks
20-
- bench: add SIMD string serializer and SWAR escape/string variant head-to-head benchmarks
5+
## 2026-04-13 - 1.3.2
6+
7+
- fix: remove the fast double parser dependency and return float deserialization to the local legacy parser path
8+
- fix: restrict string field destination reuse/renewal to heap-backed strings only and avoid writing into static literal storage
9+
- perf: reduce branching in string field write paths while preserving heap-backed reuse (`simple`, `swar`, `simd`, and shared `bs.toField`)
10+
- tests: add string-field regression coverage for literal defaults and heap-backed output pointers
11+
- tooling: fix d8 bench runner lint issues (`print` global and unused buffer id vars)
12+
- tooling: align `bench` script to use `charts:build`
13+
- docs: streamline README benchmark/docs sections and update benchmark chart command references
2114

2215
## 2026-03-19 - 1.3.0
2316

README.md

Lines changed: 1 addition & 80 deletions
Original file line numberDiff line numberDiff line change
@@ -552,73 +552,6 @@ Here's a short list:
552552

553553
**JSON_WRITE** (default: "") - Select a series of files to output after transform and optimization passes have completed for easy inspection. Usage: `JSON_WRITE=.path-to-file-a.ts,./path-to-file-b.ts`
554554

555-
### Fast-Path Compatibility Matrix
556-
557-
Fast path applies to generated struct deserialization only (`JSON_USE_FAST_PATH=1`, `JSON_MODE=SWAR|SIMD`).
558-
559-
| Area | Status | Notes |
560-
| --- | --- | --- |
561-
| `JSON_MODE=NAIVE` | Not supported | Fast path is only emitted for `SWAR` / `SIMD`. |
562-
| Canonical minified object layout | Required | No whitespace, canonical generated key order, no extra/missing keys. |
563-
| Class-level `@deserializer` | Not supported | Custom deserializer takes over. |
564-
| Class-level `@serializer` | Supported | Does not block fast-path deserialization. |
565-
| `@alias` fields | Supported | Fast path matches the aliased key names directly. |
566-
| `@omitnull` fields | Supported | Optional field keys can be absent in canonical order. |
567-
| `@omitif` fields | Supported | Optional field keys can be absent in canonical order. |
568-
| Mixed `@omitnull` + `@omitif` | Supported | Supported in fast-path tests. |
569-
| `@omit` fields | Supported (serialize-side) | Omitted fields are not part of the parse contract. |
570-
571-
Field-type support in fast path:
572-
573-
| Field category | Status | Implementation |
574-
| --- | --- | --- |
575-
| Integers / floats / bool | Supported | Direct field parsers. |
576-
| `string` / nullable string | Supported | SWAR/SIMD string field parser. |
577-
| Nested generated struct | Supported | Direct nested `__DESERIALIZE` call. |
578-
| `Array<string>` / `Array<@json class>` | Supported | Specialized direct array paths. |
579-
| Other `Array<T>` | Supported | Delegated field helper. |
580-
| `Set<T>` | Supported | Field helper. |
581-
| `Map<K,V>` | Supported | Field helper (`scanValueEnd` + typed value parse). |
582-
| `StaticArray<T>` | Supported | Field helper (`scanValueEnd` + static array parse). |
583-
| `JSON.Raw` | Supported | Direct field path. |
584-
| `JSON.Value` / `JSON.Obj` | Supported | `scanValueEnd` + `JSON.__deserialize<T>`. |
585-
| Enums | Supported | `scanValueEnd` + `JSON.__deserialize<Enum>`. |
586-
| `JSON.Box<T>` | Supported | Direct box-aware field path. |
587-
| `Date` | Supported | Direct date field path. |
588-
589-
For the detailed evolving list of optimizations and known gaps, see [FAST_PATH_DESERIALIZE.md](./FAST_PATH_DESERIALIZE.md).
590-
591-
### Container Compatibility Matrix
592-
593-
This matrix describes the current **main parser/runtime** behavior (not just fast-path struct parsing).
594-
595-
| Container | Serialization | Deserialization | Notes |
596-
| --- | --- | --- | --- |
597-
| `Array<T>` | Broad (`JSON.__serialize<valueof<T>>`) | Selective dispatch by element type | Deserialization supports primitives, nested arrays, `JSON.Value`, `JSON.Box`, `JSON.Obj`, `JSON.Raw`, `Date`, `Set`, `Map`, and `@json`/custom struct types. |
598-
| `Map<K,V>` | Broad values and broad key types | Broad key types | Non-string keys are encoded as JSON text and then written as JSON object keys (quoted strings). Deserialization decodes key strings back with typed parsing. |
599-
| `Set<T>` | Broad (`JSON.__serialize<indexof<T>>`) | Broad, element-scan + `JSON.__deserialize<indexof<T>>` for managed/reference types | Primitive element fast branches exist; managed/reference elements are delegated to generic typed deserialization. |
600-
601-
Container-type examples inferred from current code paths:
602-
603-
| Type | Serialize | Deserialize |
604-
| --- | --- | --- |
605-
| `Array<JSON.Raw>` | Yes | Yes |
606-
| `Array<Map<string, i32>>` | Yes | Yes |
607-
| `Array<Date>` | Yes | Yes |
608-
| `Array<Set<string>>` | Yes | Yes |
609-
| `Map<string, Date>` | Yes | Yes |
610-
| `Map<bool, i32>` | Yes | Yes |
611-
| `Map<Date, i32>` | Yes | Yes |
612-
| `Map<@json class, i32>` | Yes | Yes |
613-
| `Map<i32[], string>` | Yes | Yes |
614-
| `Set<Date>` | Yes | Yes |
615-
616-
TODO (container coverage):
617-
618-
- decide and document whether array dispatch should also include typed-array/ArrayBuffer element types
619-
- decide whether to keep broad `Map<K,V>` key support or provide a strict JSON-object-key mode (`string` only)
620-
- add explicit tests for each matrix row above so support status is locked by CI
621-
622555
### Running Benchmarks Locally
623556

624557
Benchmarks are run directly on top of `v8` for tighter control over the engine configuration.
@@ -653,19 +586,7 @@ npm run bench:js
653586
5. Build charts from the latest local logs:
654587

655588
```bash
656-
npm run build:charts
657-
```
658-
659-
6. Publish benchmark charts to the `docs` branch:
660-
661-
```bash
662-
npm run bench:publish
663-
```
664-
665-
If you already have fresh logs and only want to rebuild charts and push them:
666-
667-
```bash
668-
npm run bench:publish -- --no-run
589+
npm run charts:build
669590
```
670591

671592
Or run the full local benchmark flow in one step:

assembly/__tests__/string.spec.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
11
import { JSON } from "..";
22
import { describe, expect } from "as-test";
3+
import { __heap_base } from "memory";
4+
5+
@json
6+
class LiteralStringFieldBox {
7+
value: string = "alpha";
8+
}
39

410
describe("Should serialize strings - Basic", () => {
511
expect(JSON.stringify("abcdefg")).toBe('"abcdefg"');
@@ -230,6 +236,19 @@ describe("Should deserialize strings - Roundtrip", () => {
230236
}
231237
});
232238

239+
describe("Should deserialize string fields without mutating literal defaults", () => {
240+
const originalLiteral = "alpha";
241+
const box = JSON.parse<LiteralStringFieldBox>('{"value":"omega"}');
242+
const fresh = new LiteralStringFieldBox();
243+
244+
expect((changetype<usize>(fresh.value) < __heap_base).toString()).toBe("true");
245+
expect((changetype<usize>(box.value) >= __heap_base).toString()).toBe("true");
246+
expect(box.value).toBe("omega");
247+
expect(originalLiteral).toBe("alpha");
248+
expect(fresh.value).toBe("alpha");
249+
expect((originalLiteral == box.value).toString()).toBe("false");
250+
});
251+
233252
describe("Additional regression coverage - primitives and arrays", () => {
234253
expect(JSON.stringify(JSON.parse<string>('"regression"'))).toBe('"regression"');
235254
expect(JSON.stringify(JSON.parse<i32>("-42"))).toBe("-42");

assembly/deserialize/simd/string.ts

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { bs } from "../../../lib/as-bs";
22
import { OBJECT, TOTAL_OVERHEAD } from "rt/common";
3+
import { __heap_base } from "memory";
34
import { QUOTE } from "../../custom/chars";
45
import { BACK_SLASH } from "../../custom/chars";
56
import { DESERIALIZE_ESCAPE_TABLE, ESCAPE_HEX_TABLE } from "../../globals/tables";
@@ -81,11 +82,13 @@ import { deserializeStringField_SWAR } from "../swar/string";
8182

8283
const current = load<usize>(dstFieldPtr);
8384
let stringPtr: usize;
84-
if (current != 0 && changetype<OBJECT>(current - TOTAL_OVERHEAD).rtSize == byteLength) {
85-
stringPtr = current;
86-
} else if (current != 0 && current != changetype<usize>("")) {
87-
stringPtr = __renew(current, byteLength);
88-
store<usize>(dstFieldPtr, stringPtr);
85+
if (current >= __heap_base) {
86+
if (changetype<OBJECT>(current - TOTAL_OVERHEAD).rtSize == byteLength) {
87+
stringPtr = current;
88+
} else {
89+
stringPtr = __renew(current, byteLength);
90+
store<usize>(dstFieldPtr, stringPtr);
91+
}
8992
} else {
9093
stringPtr = __new(byteLength, idof<string>());
9194
store<usize>(dstFieldPtr, stringPtr);

assembly/deserialize/simple/float.ts

Lines changed: 0 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,4 @@
11
import { ptrToStr } from "../../util/ptrToStr";
2-
import { fastDoubleParse } from "../../util/fast-double-parser";
3-
4-
const FAST_DOUBLE_SCRATCH = memory.data(8);
52

63
// @ts-ignore: inline
74
@inline function pow10Fast(exponent: u32): f64 {
@@ -39,17 +36,6 @@ const FAST_DOUBLE_SCRATCH = memory.data(8);
3936

4037
// @ts-ignore: inline
4138
@inline export function deserializeFloat<T>(srcStart: usize, srcEnd: usize): T {
42-
const fastEnd = fastDoubleParse(srcStart, srcEnd, FAST_DOUBLE_SCRATCH);
43-
if (fastEnd != 0) {
44-
const value = load<f64>(FAST_DOUBLE_SCRATCH);
45-
// @ts-ignore
46-
const type: T = 0;
47-
// @ts-ignore
48-
if (type instanceof f64) return <T>value;
49-
// @ts-ignore
50-
return <T>(<f32>value);
51-
}
52-
5339
let negative = false;
5440
if (load<u16>(srcStart) == 45) {
5541
negative = true;
@@ -135,17 +121,6 @@ const FAST_DOUBLE_SCRATCH = memory.data(8);
135121
// @ts-ignore: inline
136122
@inline export function deserializeFloatField<T extends number>(srcStart: usize, srcEnd: usize, dstObj: usize, dstOffset: usize = 0): usize {
137123
const fieldPtr = dstObj + dstOffset;
138-
const fastEnd = fastDoubleParse(srcStart, srcEnd, FAST_DOUBLE_SCRATCH);
139-
if (fastEnd != 0) {
140-
const value = load<f64>(FAST_DOUBLE_SCRATCH);
141-
if (sizeof<T>() == sizeof<f32>()) {
142-
store<f32>(fieldPtr, <f32>value);
143-
} else {
144-
store<f64>(fieldPtr, value);
145-
}
146-
return fastEnd;
147-
}
148-
149124
let negative = false;
150125
if (load<u16>(srcStart) == 45) {
151126
negative = true;

assembly/deserialize/simple/string.ts

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { OBJECT, TOTAL_OVERHEAD } from "rt/common";
2+
import { __heap_base } from "memory";
23
import { bs } from "../../../lib/as-bs";
34
import { BACK_SLASH, QUOTE } from "../../custom/chars";
45
import { DESERIALIZE_ESCAPE_TABLE, ESCAPE_HEX_TABLE } from "../../globals/tables";
@@ -13,11 +14,13 @@ import { hex4_to_u16_swar } from "../../util/swar";
1314

1415
const current = load<usize>(dstFieldPtr);
1516
let stringPtr: usize;
16-
if (current != 0 && changetype<OBJECT>(current - TOTAL_OVERHEAD).rtSize == byteLength) {
17-
stringPtr = current;
18-
} else if (current != 0 && current != changetype<usize>("")) {
19-
stringPtr = __renew(current, byteLength);
20-
store<usize>(dstFieldPtr, stringPtr);
17+
if (current >= __heap_base) {
18+
if (changetype<OBJECT>(current - TOTAL_OVERHEAD).rtSize == byteLength) {
19+
stringPtr = current;
20+
} else {
21+
stringPtr = __renew(current, byteLength);
22+
store<usize>(dstFieldPtr, stringPtr);
23+
}
2124
} else {
2225
stringPtr = __new(byteLength, idof<string>());
2326
store<usize>(dstFieldPtr, stringPtr);

assembly/deserialize/swar/string.ts

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { bs } from "../../../lib/as-bs";
22
import { OBJECT, TOTAL_OVERHEAD } from "rt/common";
3+
import { __heap_base } from "memory";
34
import { BACK_SLASH, QUOTE } from "../../custom/chars";
45
import { DESERIALIZE_ESCAPE_TABLE } from "../../globals/tables";
56
import { hex4_to_u16_swar } from "../../util/swar";
@@ -194,11 +195,13 @@ export function deserializeString_SWAR(srcStart: usize, srcEnd: usize): string {
194195

195196
const current = load<usize>(dstFieldPtr);
196197
let stringPtr: usize;
197-
if (current != 0 && changetype<OBJECT>(current - TOTAL_OVERHEAD).rtSize == byteLength) {
198-
stringPtr = current;
199-
} else if (current != 0 && current != changetype<usize>("")) {
200-
stringPtr = __renew(current, byteLength);
201-
store<usize>(dstFieldPtr, stringPtr);
198+
if (current >= __heap_base) {
199+
if (changetype<OBJECT>(current - TOTAL_OVERHEAD).rtSize == byteLength) {
200+
stringPtr = current;
201+
} else {
202+
stringPtr = __renew(current, byteLength);
203+
store<usize>(dstFieldPtr, stringPtr);
204+
}
202205
} else {
203206
stringPtr = __new(byteLength, idof<string>());
204207
store<usize>(dstFieldPtr, stringPtr);

0 commit comments

Comments
 (0)