mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-10-31 07:23:06 +08:00
`JSON.parse(text) as T` used to build a checked-dynamic ScrDyn tree, copy it into the native layout with the generated dynCheck builder, and then dispose the tree. Together that was about 68% of json-records. For record/array targets with number, boolean and string leaves, the backend now emits a ScrJsonSchema (field offsets in declaration order, constructors, releases) and a wrapper. The wrapper calls the new scr_json_parse_schema, a one-pass parser that writes directly into native records and arrays. Whenever it declines (syntax errors anywhere, kind mismatches, missing or duplicate declared keys, excess depth) it releases its partial value without throwing, and the wrapper reruns the unchanged scr_json_parse + dynCheck route. Every error message and position stays byte-identical. The lexer helpers are shared through a quiet mode and a factored number scanner, so values decode bit-identically. Tests: new differential corpus program json-typed-parse-direct.ts; a dyncheck harness case comparing typed and untyped error messages through the fused path; white-box C tests for accept/decline and leak-freedom in scr_json.test.c; an LLVM emission test (64-bit and wasm32). Linux (sandbox x86_64, runs=15): | workload | baseline ms | candidate ms | change | 95% CI | verdict | size | node ms | candidate/node | | --- | ---: | ---: | ---: | --- | --- | ---: | ---: | ---: | | json-records | 713.2 | 249.3 | -65.0% | -65.6% .. -64.1% | faster | +2.2% | 439.04 | 0.57x | | regex-logs | 350.8 | 353.7 | +0.8% | -0.9% .. +2.3% | neutral | +0.0% | 238.42 | 1.48x | | ast-interp | 379.1 | 376.8 | -0.6% | -1.8% .. +0.4% | neutral | +0.0% | 227.45 | 1.66x | | records-sort | 1200.5 | 1207.8 | +0.6% | -2.4% .. +5.7% | neutral | +0.0% | 232.17 | 5.20x | | template-render | 248.3 | 248.1 | -0.1% | -2.3% .. +1.0% | neutral | +0.0% | 202.16 | 1.23x | | functional-pipeline | 402.0 | 395.3 | -1.6% | -3.8% .. +0.5% | neutral | +0.0% | 254.57 | 1.55x | | async-pipeline | 202.4 | 201.7 | -0.3% | -1.4% .. +2.0% | neutral | +0.0% | 90.14 | 2.24x | | word-graph | 366.9 | 348.2 | -5.1% | -12.9% .. +4.3% | neutral | +0.0% | 576.56 | 0.60x | | alloc-trees | 513.5 | 525.5 | +2.3% | -2.1% .. +4.0% | neutral | +0.0% | 132.99 | 3.95x | | numeric-kernels | 121.4 | 122.5 | +0.8% | -1.9% .. +3.6% | neutral | +0.0% | 135.91 | 0.90x | | validate-errors | 206.0 | 209.2 | +1.5% | -3.6% .. +4.1% | neutral | +0.0% | 703.47 | 0.30x | | csv-numbers | 450.9 | 448.6 | -0.5% | -2.2% .. +2.0% | neutral | +0.0% | 499.25 | 0.90x | | log-summary | 253.5 | 252.6 | -0.3% | -1.5% .. +2.0% | neutral | +0.0% | 279.67 | 0.90x | | inventory-report | 433.7 | 433.2 | -0.1% | -6.0% .. +3.6% | neutral | +0.0% | 413.79 | 1.05x | geomean change -7.4%; geomean candidate/node 1.321x -> 1.223x macOS (arm64, runs=15): | workload | baseline ms | candidate ms | change | 95% CI | verdict | size | node ms | candidate/node | | --- | ---: | ---: | ---: | --- | --- | ---: | ---: | ---: | | json-records | 292.3 | 153.0 | -47.7% | -50.1% .. -46.6% | faster | +0.4% | 248.7 | 0.61x | | (all 13 other workloads neutral; see results/h16-typed-json/local-1.json) | geomean change -4.8%; geomean candidate/node 1.497x -> 1.425x
55 lines
2.5 KiB
TypeScript
55 lines
2.5 KiB
TypeScript
import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
|
|
import { tmpdir } from "node:os";
|
|
import { join } from "node:path";
|
|
import { expect, test } from "vitest";
|
|
import { compile, deserializeModule } from "../src/index.js";
|
|
import { emitLlvmModule } from "../src/backend/llvm/emitter.js";
|
|
|
|
test("JSON.parse casts to plain records and arrays fuse into the schema parser", async () => {
|
|
const dir = await mkdtemp(join(tmpdir(), "scriptc-json-schema-"));
|
|
try {
|
|
const entry = join(dir, "main.ts");
|
|
const outPath = join(dir, "main.ir.json");
|
|
await writeFile(
|
|
entry,
|
|
`
|
|
interface Item { sku: string; qty: number; ok: boolean }
|
|
interface Order { id: number; items: Item[]; tags: string[] }
|
|
interface Loose { name: string; note?: string }
|
|
const text = process.argv[2] ?? "[]";
|
|
const orders = JSON.parse(text) as Order[];
|
|
const typed: Item = JSON.parse(text);
|
|
const loose = JSON.parse(text) as Loose;
|
|
const pair = JSON.parse(text) as [string, number];
|
|
console.log(orders.length, typed.sku, loose.name, pair[0]);
|
|
`,
|
|
);
|
|
const result = await compile(entry, { outDir: dir, outPath, outputKind: "ir" });
|
|
if (!result.ok)
|
|
throw new Error(result.diagnostics.map((d) => `${d.code}: ${d.message}`).join("\n"));
|
|
const mod = deserializeModule(await readFile(outPath, "utf8"));
|
|
|
|
const llvm = emitLlvmModule(mod);
|
|
// Order[] and Item (an annotated binding) take the fused path; the
|
|
// record schema lists fields in declaration order with struct offsets.
|
|
expect(llvm.match(/^define internal ptr @sc_jp_\d+\(ptr %text\)/gm)).toHaveLength(2);
|
|
expect(llvm).toContain("call ptr @scr_json_parse_schema(ptr %text, ptr @sc_jsch_");
|
|
expect(llvm).toMatch(
|
|
/@sc_jsch_\d+_fields = internal constant \[3 x \{ ptr, i64, i64, ptr \}\] \[\{ ptr, i64, i64, ptr \} \{ ptr @sc_cs_\d+, i64 3, i64 ptrtoint/,
|
|
);
|
|
// Declining reruns the exact checked-dynamic route.
|
|
expect(llvm).toMatch(
|
|
/%d = call ptr @scr_json_parse\(ptr %text\)\n {2}%bad = icmp eq ptr %d, null/,
|
|
);
|
|
expect(llvm).toMatch(/%v = call ptr @sc_dc_\d+\(ptr %d, ptr null\)/);
|
|
// Optional fields (unions) and tuples keep the checked-dynamic route.
|
|
expect(llvm.match(/call ptr @scr_json_parse\(ptr /g)!.length).toBeGreaterThanOrEqual(4);
|
|
|
|
const wasm = emitLlvmModule(mod, { pointerBits: 32, wasi: true });
|
|
expect(wasm).toContain("= internal constant { i32, i32, ptr, ptr, ptr, ptr, ptr }");
|
|
expect(wasm).toMatch(/\{ ptr, i32, i32, ptr \} \{ ptr @sc_cs_\d+, i32 3, i32 ptrtoint/);
|
|
} finally {
|
|
await rm(dir, { recursive: true, force: true });
|
|
}
|
|
});
|