mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-10-27 06:43:19 +08:00
Read a union's discriminant once in checked casts
A checked cast into a union whose discriminant has four or more string literals reads the discriminant field once (scr_dyn_discriminant_case) and then tests only the arms whose literal it holds and the arms without a literal, in the union's arm order. Every other arm's literal test would fail, so the arm chosen and the error for a value no arm matches are unchanged. A typed capsule, an accessor or an inherited discriminant still takes the tests arm by arm. Unions with fewer literals emit the same code as before.
This commit is contained in:
@@ -16,6 +16,7 @@ Application-shaped workloads for measuring the speed of compiled executables. Ea
|
||||
| `functional-pipeline` | cpu | `filter`/`map`/`reduce`/`flatMap` chains, closures, small records |
|
||||
| `intern-table`, `intern-table-map`, `intern-table-1`, `intern-table-map-1` | cpu | worker threads interning the same type-like objects under string keys and records under number keys, then hits and misses: one shared `InternTable` per kind of key, or a module-level `Map` per thread (the `-map` entries), with 8 threads or 1 (the `-1` entries); under Node.js every thread has its own tables |
|
||||
| `json-records` | cpu | `JSON.stringify`, typed `JSON.parse`, Map aggregation |
|
||||
| `json-unions` | cpu | `JSON.parse` into discriminated unions: an event log with eight kinds and nested expression trees with nine, evaluated after the cast |
|
||||
| `log-lines` | cpu | 200k immediate `console.log`/`process.stdout.write` lines into the runner's pipe |
|
||||
| `log-summary`, `inventory-report` | cpu | the multi-module applications from `benchmarks/builds`, on 300k-line inputs |
|
||||
| `number-maps` | cpu | number-keyed Map/Set: a dense-id table, sparse ids in many small maps, get/has/set mixes |
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"entry": "workloads/json-unions.ts",
|
||||
"args": [
|
||||
"6"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,155 @@
|
||||
// JSON text parsed into discriminated unions: an event log with eight kinds
|
||||
// of event and a nested expression tree with nine kinds of node, cast with
|
||||
// `JSON.parse(text) as T`, then aggregated and evaluated. Argument: the
|
||||
// number of rounds.
|
||||
type Event =
|
||||
| { kind: "click"; target: string; x: number; y: number }
|
||||
| { key: string; kind: "key"; repeat: boolean }
|
||||
| { kind: "scroll"; offset: number }
|
||||
| { kind: "resize"; height: number; width: number }
|
||||
| { kind: "focus"; target: string }
|
||||
| { kind: "blur"; target: string }
|
||||
| { kind: "load"; ms: number; url: string }
|
||||
| { code: number; kind: "error"; message: string };
|
||||
|
||||
type Expr =
|
||||
| { kind: "num"; value: number }
|
||||
| { kind: "var"; name: string }
|
||||
| { kind: "neg"; operand: Expr }
|
||||
| { kind: "add"; left: Expr; right: Expr }
|
||||
| { kind: "sub"; left: Expr; right: Expr }
|
||||
| { kind: "mul"; left: Expr; right: Expr }
|
||||
| { kind: "max"; items: Expr[] }
|
||||
| { else: Expr; kind: "if"; test: Expr; then: Expr }
|
||||
| { body: Expr; kind: "let"; name: string; value: Expr };
|
||||
|
||||
let seed = 12345;
|
||||
function random(): number {
|
||||
seed = (Math.imul(seed, 1103515245) + 12345) >>> 0;
|
||||
return seed / 4294967296;
|
||||
}
|
||||
|
||||
const targets = ["menu", "search", "button", "list", "footer"];
|
||||
function eventText(i: number): string {
|
||||
const target = targets[i % targets.length]!;
|
||||
switch (i % 8) {
|
||||
case 0:
|
||||
return `{"kind":"click","target":"${target}","x":${i % 640},"y":${i % 480}}`;
|
||||
case 1:
|
||||
return `{"key":"k${i % 26}","kind":"key","repeat":${i % 3 === 0}}`;
|
||||
case 2:
|
||||
return `{"kind":"scroll","offset":${(i * 7) % 2000}}`;
|
||||
case 3:
|
||||
return `{"height":${400 + (i % 300)},"kind":"resize","width":${600 + (i % 500)}}`;
|
||||
case 4:
|
||||
return `{"kind":"focus","target":"${target}"}`;
|
||||
case 5:
|
||||
return `{"kind":"blur","target":"${target}"}`;
|
||||
case 6:
|
||||
return `{"kind":"load","ms":${i % 900},"url":"/page/${i % 50}"}`;
|
||||
default:
|
||||
return `{"code":${400 + (i % 100)},"kind":"error","message":"failed ${i % 13}"}`;
|
||||
}
|
||||
}
|
||||
|
||||
const names = ["a", "b", "c", "d"];
|
||||
function exprText(depth: number): string {
|
||||
const pick = depth <= 0 ? Math.floor(random() * 2) : Math.floor(random() * 9);
|
||||
switch (pick) {
|
||||
case 0:
|
||||
return `{"kind":"num","value":${Math.floor(random() * 100)}}`;
|
||||
case 1:
|
||||
return `{"kind":"var","name":"${names[Math.floor(random() * names.length)]!}"}`;
|
||||
case 2:
|
||||
return `{"kind":"neg","operand":${exprText(depth - 1)}}`;
|
||||
case 3:
|
||||
case 4:
|
||||
case 5: {
|
||||
const kind = pick === 3 ? "add" : pick === 4 ? "sub" : "mul";
|
||||
return `{"kind":"${kind}","left":${exprText(depth - 1)},"right":${exprText(depth - 1)}}`;
|
||||
}
|
||||
case 6:
|
||||
return `{"items":[${exprText(depth - 1)},${exprText(depth - 2)},${exprText(depth - 2)}],"kind":"max"}`;
|
||||
case 7:
|
||||
return `{"else":${exprText(depth - 2)},"kind":"if","test":${exprText(depth - 1)},"then":${exprText(depth - 2)}}`;
|
||||
default:
|
||||
return `{"body":${exprText(depth - 1)},"kind":"let","name":"${names[Math.floor(random() * names.length)]!}","value":${exprText(depth - 2)}}`;
|
||||
}
|
||||
}
|
||||
|
||||
function evaluate(e: Expr, env: Map<string, number>): number {
|
||||
switch (e.kind) {
|
||||
case "num":
|
||||
return e.value;
|
||||
case "var":
|
||||
return env.get(e.name) ?? 1;
|
||||
case "neg":
|
||||
return -evaluate(e.operand, env);
|
||||
case "add":
|
||||
return (evaluate(e.left, env) + evaluate(e.right, env)) % 1000003;
|
||||
case "sub":
|
||||
return (evaluate(e.left, env) - evaluate(e.right, env)) % 1000003;
|
||||
case "mul":
|
||||
return (evaluate(e.left, env) * evaluate(e.right, env)) % 1000003;
|
||||
case "max": {
|
||||
let best = -Infinity;
|
||||
for (const item of e.items) best = Math.max(best, evaluate(item, env));
|
||||
return best;
|
||||
}
|
||||
case "if":
|
||||
return evaluate(e.test, env) > 0 ? evaluate(e.then, env) : evaluate(e.else, env);
|
||||
case "let": {
|
||||
const inner = new Map(env);
|
||||
inner.set(e.name, evaluate(e.value, env));
|
||||
return evaluate(e.body, inner);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const rounds = Number(process.argv[2] ?? "6");
|
||||
const eventsText = `[${Array.from({ length: 60000 }, (_, i) => eventText(i)).join(",")}]`;
|
||||
const treesText = `[${Array.from({ length: 400 }, () => exprText(9)).join(",")}]`;
|
||||
let clicks = 0;
|
||||
let keys = 0;
|
||||
let scrolled = 0;
|
||||
let area = 0;
|
||||
let loadMs = 0;
|
||||
let errors = 0;
|
||||
let focus = 0;
|
||||
let total = 0;
|
||||
for (let round = 0; round < rounds; round++) {
|
||||
const events = JSON.parse(eventsText) as Event[];
|
||||
for (const event of events) {
|
||||
switch (event.kind) {
|
||||
case "click":
|
||||
clicks += event.x + event.y;
|
||||
break;
|
||||
case "key":
|
||||
if (event.repeat) keys++;
|
||||
break;
|
||||
case "scroll":
|
||||
scrolled += event.offset;
|
||||
break;
|
||||
case "resize":
|
||||
area += (event.width * event.height) % 1000;
|
||||
break;
|
||||
case "focus":
|
||||
case "blur":
|
||||
focus += event.target.length;
|
||||
break;
|
||||
case "load":
|
||||
loadMs += event.ms;
|
||||
break;
|
||||
case "error":
|
||||
errors += event.code % 7;
|
||||
break;
|
||||
}
|
||||
}
|
||||
const trees = JSON.parse(treesText) as Expr[];
|
||||
const env = new Map([
|
||||
["a", 3],
|
||||
["b", 5],
|
||||
]);
|
||||
for (const tree of trees) total = (total + evaluate(tree, env)) % 1000003;
|
||||
}
|
||||
console.log(clicks, keys, scrolled, area, loadMs, errors, focus, total);
|
||||
@@ -193,6 +193,11 @@ function builtinStreamKind(className: string): number | null {
|
||||
return kinds[className] ?? null;
|
||||
}
|
||||
|
||||
/** Unions whose discriminant has fewer string literals keep one literal test
|
||||
* per arm (dispatchValues): one runtime lookup costs about what a few
|
||||
* literal tests do. */
|
||||
const DISCRIMINANT_DISPATCH_VALUES = 4;
|
||||
|
||||
export class LlDyn {
|
||||
private readonly dynMatchers = new Map<string, string>();
|
||||
private readonly dynBuilders = new Map<string, string>();
|
||||
@@ -413,6 +418,86 @@ export class LlDyn {
|
||||
return order;
|
||||
}
|
||||
|
||||
/** The string literals of `def`'s discriminant when its arms are chosen
|
||||
* by reading the discriminant once (unionArmsByDiscriminant), or null
|
||||
* when the union keeps one literal test per arm: some literal is not a
|
||||
* string, or there are fewer than DISCRIMINANT_DISPATCH_VALUES. */
|
||||
private dispatchValues(def: IrUnionDef): string[] | null {
|
||||
const values: string[] = [];
|
||||
for (const guard of def.discriminant?.cases ?? []) {
|
||||
if (guard.values.length === 0) return null;
|
||||
for (const value of guard.values) {
|
||||
if (typeof value !== "string") return null;
|
||||
if (!values.includes(value)) values.push(value);
|
||||
}
|
||||
}
|
||||
return values.length < DISCRIMINANT_DISPATCH_VALUES ? null : values;
|
||||
}
|
||||
|
||||
/** Branches to `hits[tag]` for the first arm in `order` that `%d` matches
|
||||
* (unionArmMatch), or to `none` when no arm does, for
|
||||
* a union whose discriminant literals are `values` (dispatchValues). The
|
||||
* discriminant is read once (scr_dyn_discriminant_case, against the
|
||||
* constant `table`), and only the arms whose literal it holds and the arms
|
||||
* without a literal are tested, in `order`: every other arm's literal test
|
||||
* fails. A value the runtime cannot classify that way (a typed capsule, an
|
||||
* accessor, an inherited discriminant) takes the tests arm by arm. */
|
||||
private unionArmsByDiscriminant(
|
||||
B: BlockBuilder,
|
||||
def: IrUnionDef,
|
||||
values: readonly string[],
|
||||
order: readonly number[],
|
||||
hits: readonly string[],
|
||||
none: string,
|
||||
preserveRefs: boolean,
|
||||
table: string,
|
||||
): void {
|
||||
const discriminant = def.discriminant!;
|
||||
const literalsOf = (tag: number): readonly (string | number | boolean)[] | null =>
|
||||
discriminant.cases.find((candidate) => candidate.tag === tag)?.values ?? null;
|
||||
const tryArms = (tags: readonly number[], literal: boolean): void => {
|
||||
if (tags.length === 0) {
|
||||
B.br(none);
|
||||
return;
|
||||
}
|
||||
tags.forEach((tag, index) => {
|
||||
const next = index === tags.length - 1 ? none : B.newLabel("du.c.n");
|
||||
if (literal) this.unionArmMatch(B, def, tag, hits[tag]!, next, preserveRefs);
|
||||
else this.unionArmStructure(B, def, tag, hits[tag]!, next, preserveRefs);
|
||||
if (next !== none) B.startBlock(next);
|
||||
});
|
||||
};
|
||||
const armsFor = (value: string | null): number[] =>
|
||||
order.filter((tag) => {
|
||||
const literals = literalsOf(tag);
|
||||
return literals === null || (value !== null && literals.includes(value));
|
||||
});
|
||||
const host = this.host;
|
||||
this.defs.push(
|
||||
`${table} = private unnamed_addr constant [${values.length} x ptr] [${values.map((value) => `ptr ${host.internLiteral(value)}`).join(", ")}]`,
|
||||
``,
|
||||
);
|
||||
host.declare(`declare i32 @scr_dyn_discriminant_case(ptr, ptr, ptr, ${host.sizeType})`);
|
||||
const index = B.tmp();
|
||||
B.line(
|
||||
`${index} = call i32 @scr_dyn_discriminant_case(ptr %d, ptr ${host.internLiteral(discriminant.field)}, ptr ${table}, ${host.sizeType} ${values.length})`,
|
||||
);
|
||||
const tests = B.newLabel("du.tests");
|
||||
const unmatched = B.newLabel("du.none");
|
||||
const cases = values.map(() => B.newLabel("du.case"));
|
||||
B.terminate(
|
||||
`switch i32 ${index}, label %${tests} [ i32 -1, label %${unmatched} ${cases.map((label, k) => `i32 ${k}, label %${label}`).join(" ")} ]`,
|
||||
);
|
||||
values.forEach((value, k) => {
|
||||
B.startBlock(cases[k]!);
|
||||
tryArms(armsFor(value), false);
|
||||
});
|
||||
B.startBlock(unmatched);
|
||||
tryArms(armsFor(null), false);
|
||||
B.startBlock(tests);
|
||||
tryArms(order, true);
|
||||
}
|
||||
|
||||
/** Branch to a variant only after its literal discriminator and complete
|
||||
* structure match. Use the same path for predicates and builders. */
|
||||
private unionArmMatch(
|
||||
@@ -450,6 +535,18 @@ export class LlDyn {
|
||||
});
|
||||
B.startBlock(structure);
|
||||
}
|
||||
this.unionArmStructure(B, def, tag, yes, no, preserveRefs);
|
||||
}
|
||||
|
||||
/** The part of unionArmMatch past the arm's literal test. */
|
||||
private unionArmStructure(
|
||||
B: BlockBuilder,
|
||||
def: IrUnionDef,
|
||||
tag: number,
|
||||
yes: string,
|
||||
no: string,
|
||||
preserveRefs: boolean,
|
||||
): void {
|
||||
const matches = B.tmp();
|
||||
B.line(
|
||||
`${matches} = call zeroext i1 @${this.dynMatchHelper(def.arms[tag]!, preserveRefs)}(ptr %d)`,
|
||||
@@ -1275,11 +1372,26 @@ export class LlDyn {
|
||||
);
|
||||
// Arms in canonical order; any full match answers true.
|
||||
const yes = B.newLabel("dm.y");
|
||||
for (let tag = 0; tag < def.arms.length; tag++) {
|
||||
const ln = B.newLabel("dm.n");
|
||||
this.unionArmMatch(B, def, tag, yes, ln, preserveRefs);
|
||||
B.startBlock(ln);
|
||||
}
|
||||
const values = this.dispatchValues(def);
|
||||
if (values !== null) {
|
||||
const no = B.newLabel("dm.n");
|
||||
this.unionArmsByDiscriminant(
|
||||
B,
|
||||
def,
|
||||
values,
|
||||
def.arms.map((_, tag) => tag),
|
||||
def.arms.map(() => yes),
|
||||
no,
|
||||
preserveRefs,
|
||||
`@${name}.cases`,
|
||||
);
|
||||
B.startBlock(no);
|
||||
} else
|
||||
for (let tag = 0; tag < def.arms.length; tag++) {
|
||||
const ln = B.newLabel("dm.n");
|
||||
this.unionArmMatch(B, def, tag, yes, ln, preserveRefs);
|
||||
B.startBlock(ln);
|
||||
}
|
||||
B.terminate(`ret i1 false`);
|
||||
B.startBlock(yes);
|
||||
B.terminate(`ret i1 true`);
|
||||
@@ -2459,12 +2571,8 @@ export class LlDyn {
|
||||
);
|
||||
// Arms in unionArmOrder, first FULL match wins. The matched arm's
|
||||
// builder can no longer fail.
|
||||
for (const i of this.unionArmOrder(def)) {
|
||||
const build = (i: number): void => {
|
||||
const arm = def.arms[i]!;
|
||||
const lHit = B.newLabel("dcu.h");
|
||||
const lNext = B.newLabel("dcu.n");
|
||||
this.unionArmMatch(B, def, i, lHit, lNext, preserveRefs);
|
||||
B.startBlock(lHit);
|
||||
if (arm.kind === "undefinedT" || arm.kind === "nullT") {
|
||||
// A matched unit arm builds nothing: THE interned immortal
|
||||
// instance (rc == SIZE_MAX — no retain owed).
|
||||
@@ -2503,8 +2611,36 @@ export class LlDyn {
|
||||
);
|
||||
B.terminate(`ret ptr ${u}`);
|
||||
}
|
||||
B.startBlock(lNext);
|
||||
}
|
||||
};
|
||||
const order = this.unionArmOrder(def);
|
||||
const values = this.dispatchValues(def);
|
||||
if (values !== null) {
|
||||
const hits = def.arms.map(() => B.newLabel("dcu.h"));
|
||||
const unmatched = B.newLabel("dcu.n");
|
||||
this.unionArmsByDiscriminant(
|
||||
B,
|
||||
def,
|
||||
values,
|
||||
order,
|
||||
hits,
|
||||
unmatched,
|
||||
preserveRefs,
|
||||
`@${name}.cases`,
|
||||
);
|
||||
for (const i of order) {
|
||||
B.startBlock(hits[i]!);
|
||||
build(i);
|
||||
}
|
||||
B.startBlock(unmatched);
|
||||
} else
|
||||
for (const i of order) {
|
||||
const lHit = B.newLabel("dcu.h");
|
||||
const lNext = B.newLabel("dcu.n");
|
||||
this.unionArmMatch(B, def, i, lHit, lNext, preserveRefs);
|
||||
B.startBlock(lHit);
|
||||
build(i);
|
||||
B.startBlock(lNext);
|
||||
}
|
||||
if (preserveRefs) {
|
||||
B.line(
|
||||
`call void @scr_dyn_check_fail(ptr %path, ptr ${host.cstr("reference with its original layout")}, ptr %d)`,
|
||||
|
||||
@@ -9,9 +9,10 @@ import { inlineRcOption } from "../src/backend/llvm/shapes.js";
|
||||
|
||||
// Programs whose function bodies request module-level helpers and tables
|
||||
// that the chunk journal (emit-journal.ts) carries: checked-dynamic
|
||||
// conversions, inspection, publication walkers, live record views, stream
|
||||
// adapters, regex instances, colliding literal digests, interned C strings,
|
||||
// speculative loop sites, typed message listeners and readers.
|
||||
// conversions (with their discriminant tables), inspection, publication
|
||||
// walkers, live record views, stream adapters, regex instances, colliding
|
||||
// literal digests, interned C strings, speculative loop sites, typed message
|
||||
// listeners and readers.
|
||||
const PROGRAMS = [
|
||||
"1003-json-parse-unions.ts",
|
||||
"114-live-record-delete.ts",
|
||||
@@ -21,6 +22,7 @@ const PROGRAMS = [
|
||||
"4900-console-instances.ts",
|
||||
"5612-readable-from-iterables.cjs",
|
||||
"5702-publish-candidate-buffers.ts",
|
||||
"6305-json-discriminant-dispatch.ts",
|
||||
"7321-speculative-loop-bail-sites.ts",
|
||||
"7360-worker-typed-records.ts",
|
||||
"node24-text-utilities-coercion.js",
|
||||
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/6305-json-discriminant-dispatch.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
@@ -677,6 +677,14 @@ void scr_dyn_copy_entry(ScrDyn *target, const ScrDyn *source, const ScrDynEntry
|
||||
bool scr_dyn_field_eq_str(const ScrDyn *d, const ScrStr *key, const ScrStr *value);
|
||||
bool scr_dyn_field_eq_num(const ScrDyn *d, const ScrStr *key, double value);
|
||||
bool scr_dyn_field_eq_bool(const ScrDyn *d, const ScrStr *key, bool value);
|
||||
/* The case a union's checked match takes from its string discriminant: the
|
||||
* index in `values` of the string `d`'s own data property `key` holds, or
|
||||
* -1 when every literal test above fails (`d` is not an object, or its own
|
||||
* data property `key` holds no string among `values`), or -2 when only the
|
||||
* tests themselves can tell (a typed capsule, an accessor, or a property
|
||||
* `d` does not own). Borrowed args. */
|
||||
int32_t scr_dyn_discriminant_case(const ScrDyn *d, const ScrStr *key, ScrStr *const *values,
|
||||
size_t count);
|
||||
|
||||
/* Object.keys/values/entries over the checked-dynamic tree: JS own-key order (array-index
|
||||
* keys ascending first), dyn-array results (+1); values/entries RETAIN
|
||||
|
||||
@@ -1422,6 +1422,24 @@ bool scr_dyn_field_eq_num(const ScrDyn *d, const ScrStr *key, double value) {
|
||||
return matches;
|
||||
}
|
||||
|
||||
int32_t scr_dyn_discriminant_case(const ScrDyn *d, const ScrStr *key, ScrStr *const *values,
|
||||
size_t count) {
|
||||
if (!d) return -1;
|
||||
if (d->kind == SCR_DYN_TYPED_REF) return -2;
|
||||
if (d->kind != SCR_DYN_OBJ) return -1;
|
||||
for (size_t i = 0; i < d->v.obj.len; i++) {
|
||||
const ScrDynEntry *entry = &d->v.obj.entries[i];
|
||||
if (entry->key_len != key->len || memcmp(entry->key, key->data, key->len) != 0) continue;
|
||||
if (entry->accessor) return -2;
|
||||
const ScrDyn *member = entry->value;
|
||||
if (!member || member->kind != SCR_DYN_STR) return -1;
|
||||
for (size_t k = 0; k < count; k++)
|
||||
if (scr_str_eq(member->v.str, values[k])) return (int32_t)k;
|
||||
return -1;
|
||||
}
|
||||
return -2;
|
||||
}
|
||||
|
||||
bool scr_dyn_field_eq_bool(const ScrDyn *d, const ScrStr *key, bool value) {
|
||||
ScrDyn *member = scr_dyn_discriminant(d, key);
|
||||
bool matches = member && member->kind == SCR_DYN_BOOL && member->v.b == value;
|
||||
|
||||
@@ -0,0 +1,123 @@
|
||||
// JSON cast into a union with many string discriminant literals: the cast
|
||||
// reads the discriminant once and tests only the arm whose literal it
|
||||
// holds, plus the arms that are not records. An arm can list several
|
||||
// literals, and values that are not objects reach the other arms in
|
||||
// canonical order. Valid casts only (see 1002-json-parse-cast.ts); JSON keys
|
||||
// and record fields alphabetical for stringify parity.
|
||||
|
||||
type Expr =
|
||||
| { kind: "num"; value: number }
|
||||
| { kind: "str"; text: string }
|
||||
| { kind: "neg"; operand: Expr }
|
||||
| { kind: "add" | "mul"; left: Expr; right: Expr }
|
||||
| { args: Expr[]; kind: "call"; name: string }
|
||||
| { kind: "method"; name: string; target: Expr }
|
||||
| { kind: "list"; items: Expr[] };
|
||||
|
||||
function show(e: Expr): string {
|
||||
switch (e.kind) {
|
||||
case "num":
|
||||
return String(e.value);
|
||||
case "str":
|
||||
return JSON.stringify(e.text);
|
||||
case "neg":
|
||||
return `-(${show(e.operand)})`;
|
||||
case "add":
|
||||
return `(${show(e.left)} + ${show(e.right)})`;
|
||||
case "mul":
|
||||
return `(${show(e.left)} * ${show(e.right)})`;
|
||||
case "call":
|
||||
return `${e.name}(${e.args.map(show).join(", ")})`;
|
||||
case "method":
|
||||
return `${show(e.target)}.${e.name}()`;
|
||||
case "list":
|
||||
return `[${e.items.map(show).join(", ")}]`;
|
||||
}
|
||||
}
|
||||
|
||||
function evaluate(e: Expr): number {
|
||||
switch (e.kind) {
|
||||
case "num":
|
||||
return e.value;
|
||||
case "str":
|
||||
return e.text.length;
|
||||
case "neg":
|
||||
return -evaluate(e.operand);
|
||||
case "add":
|
||||
return evaluate(e.left) + evaluate(e.right);
|
||||
case "mul":
|
||||
return evaluate(e.left) * evaluate(e.right);
|
||||
case "call":
|
||||
return e.args.reduce((sum, arg) => sum + evaluate(arg), 0);
|
||||
case "method":
|
||||
return evaluate(e.target);
|
||||
case "list":
|
||||
return e.items.length;
|
||||
}
|
||||
}
|
||||
|
||||
const source =
|
||||
'{"kind":"add","left":{"kind":"mul","left":{"kind":"num","value":3},' +
|
||||
'"right":{"kind":"neg","operand":{"kind":"num","value":4}}},' +
|
||||
'"right":{"args":[{"kind":"str","text":"abc"},{"kind":"num","value":2}],"kind":"call","name":"sum"}}';
|
||||
const tree = JSON.parse(source) as Expr;
|
||||
console.log(show(tree));
|
||||
console.log(evaluate(tree));
|
||||
console.log(JSON.stringify(tree) === source);
|
||||
|
||||
const method = JSON.parse(
|
||||
'{"kind":"method","name":"size","target":{"items":[{"kind":"num","value":1}],"kind":"list"}}',
|
||||
) as Expr;
|
||||
console.log(show(method), evaluate(method));
|
||||
|
||||
// A deep list of every arm, built from text.
|
||||
const parts: string[] = [];
|
||||
for (let i = 0; i < 200; i++) {
|
||||
parts.push(
|
||||
i % 3 === 0
|
||||
? `{"kind":"num","value":${i}}`
|
||||
: i % 3 === 1
|
||||
? `{"kind":"neg","operand":{"kind":"num","value":${i}}}`
|
||||
: `{"kind":"mul","left":{"kind":"num","value":${i}},"right":{"kind":"str","text":"xy"}}`,
|
||||
);
|
||||
}
|
||||
const list = JSON.parse(`{"items":[${parts.join(",")}],"kind":"list"}`) as Expr;
|
||||
if (list.kind === "list") {
|
||||
let total = 0;
|
||||
for (const item of list.items) total += evaluate(item);
|
||||
console.log(list.items.length, total);
|
||||
}
|
||||
|
||||
// Arms that are not records: a value that is not an object can only be one
|
||||
// of them.
|
||||
type Token =
|
||||
| { kind: "ident"; name: string }
|
||||
| { kind: "number"; value: number }
|
||||
| { kind: "string"; value: string }
|
||||
| { kind: "punct"; symbol: string }
|
||||
| { kind: "comment"; line: number; text: string }
|
||||
| string
|
||||
| number;
|
||||
|
||||
function describe(token: Token): string {
|
||||
if (typeof token === "string") return `raw ${token}`;
|
||||
if (typeof token === "number") return `count ${token}`;
|
||||
switch (token.kind) {
|
||||
case "ident":
|
||||
return `ident ${token.name}`;
|
||||
case "number":
|
||||
return `number ${token.value}`;
|
||||
case "string":
|
||||
return `string ${JSON.stringify(token.value)}`;
|
||||
case "punct":
|
||||
return `punct ${token.symbol}`;
|
||||
case "comment":
|
||||
return `comment ${token.line}: ${token.text}`;
|
||||
}
|
||||
}
|
||||
|
||||
const tokens = JSON.parse(
|
||||
'[{"kind":"ident","name":"x"},{"kind":"punct","symbol":"="},{"kind":"number","value":42},' +
|
||||
'{"kind":"string","value":"hi"},{"kind":"comment","line":3,"text":"note"},"spare",7]',
|
||||
) as Token[];
|
||||
for (const token of tokens) console.log(describe(token));
|
||||
@@ -0,0 +1 @@
|
||||
{ "plain": 5844, "san": 7609 }
|
||||
@@ -396,6 +396,54 @@ console.log("unreachable", s.kind);
|
||||
expect(r.stderr).toContain("Uncaught TypeError: expected object | object at $, got object");
|
||||
});
|
||||
|
||||
test("a union read by its discriminant fails with every arm named, as arm-by-arm tests do", async () => {
|
||||
// Five record arms with six literals, plus string and number arms: the
|
||||
// check reads `kind` once and tests only the arm it names and the
|
||||
// arms that are not records.
|
||||
const r = await compileAndRun(
|
||||
"union-discriminant-dispatch",
|
||||
`type Expr =
|
||||
| { kind: "num"; value: number }
|
||||
| { kind: "str"; text: string }
|
||||
| { kind: "neg"; operand: Expr }
|
||||
| { kind: "add" | "mul"; left: Expr; right: Expr }
|
||||
| { items: Expr[]; kind: "list" }
|
||||
| string
|
||||
| number;
|
||||
const inputs = [
|
||||
'{"kind":"num"}',
|
||||
'{"kind":"nope","value":1}',
|
||||
'{"kind":7,"value":1}',
|
||||
'{"value":1}',
|
||||
'{"kind":"neg","operand":{"kind":"add","left":{"kind":"num","value":1},"right":{"kind":"str","text":5}}}',
|
||||
'true',
|
||||
'{"items":["a",3,{"kind":"mul","left":"x","right":2}],"kind":"list"}',
|
||||
];
|
||||
for (const input of inputs) {
|
||||
try {
|
||||
console.log(JSON.stringify(JSON.parse(input) as Expr));
|
||||
} catch (error) {
|
||||
console.log((error as Error).message);
|
||||
}
|
||||
}
|
||||
`,
|
||||
);
|
||||
const any = "number | object | object | object | object | object | string";
|
||||
expect(r.exitCode).toBe(0);
|
||||
expect(r.stdout).toBe(
|
||||
[
|
||||
`expected ${any} at $, got object`,
|
||||
`expected ${any} at $, got object`,
|
||||
`expected ${any} at $, got object`,
|
||||
`expected ${any} at $, got object`,
|
||||
`expected ${any} at $, got object`,
|
||||
`expected ${any} at $, got boolean`,
|
||||
'{"items":["a",3,{"kind":"mul","left":"x","right":2}],"kind":"list"}',
|
||||
"",
|
||||
].join("\n"),
|
||||
);
|
||||
});
|
||||
|
||||
test("nested paths point at the offending element", async () => {
|
||||
const r = await compileAndRun(
|
||||
"nested-path",
|
||||
|
||||
Reference in New Issue
Block a user