mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-11-01 07:33:12 +08:00
Convert dynamic values into nested unions in one pass
Converting a dynamic value into a union (`JSON.parse(text) as T`, a cast from `any`) tries the arms in order and builds the first arm whose literal discriminator and whole structure match. The builder of that arm converts its members, and a member typed as another union matched its arm's structure again. Every value was matched once per union around it, so converting a tree of depth d cost about d times as much as a flat one. The arm a union matched now builds as a trusted conversion (scr_dyn_trust_enter). Inside it, a nested union picks an arm without matching the arm's structure, which the enclosing match already checked, when the value's arm is known: a plain object's arm by its literal, when the arms' literals are disjoint and no arm without a literal takes plain objects, or the only arm that takes plain objects (a record) or arrays (an array), as in an optional or nullable member. Other unions, and values that are not plain objects or arrays, match as before. Getters and proxy traps run outside the trusted conversion. Failed conversions throw the same TypeError with the same path.
This commit is contained in:
@@ -135,6 +135,18 @@ export const DYN_KIND = {
|
||||
SYMBOL: 15,
|
||||
} as const;
|
||||
|
||||
/** Union arm kinds whose matchers reject every plain dyn object
|
||||
* (trustedArmByLiteral). */
|
||||
const NON_OBJECT_ARM_KINDS: ReadonlySet<IrType["kind"]> = new Set<IrType["kind"]>([
|
||||
"f64",
|
||||
"string",
|
||||
"bool",
|
||||
"nullT",
|
||||
"undefinedT",
|
||||
"bigint",
|
||||
"symbol",
|
||||
]);
|
||||
|
||||
/** What the dyn helpers need beyond the walker host: interned immortal
|
||||
* unit instances (undefined-armed dynCheck targets build them). */
|
||||
export interface DynHost extends WalkerHost {
|
||||
@@ -365,8 +377,56 @@ export class LlDyn {
|
||||
B.line(`call void @scr_dyn_build_add(ptr %d, ptr @${name}, ptr ${value})`);
|
||||
}
|
||||
|
||||
/** Whether a builder of `def` may pick a plain object's arm by its literal
|
||||
* discriminator alone inside a trusted build (scr_dyn_trust_enter): the
|
||||
* arms' literals are disjoint, and every arm without a literal has a kind
|
||||
* no plain object matches. The value then matched exactly one arm with a
|
||||
* literal, the arm whose literal it carries. */
|
||||
private trustedArmByLiteral(def: IrUnionDef): boolean {
|
||||
const discriminant = def.discriminant;
|
||||
if (!discriminant) return false;
|
||||
const seen = new Set<string>();
|
||||
for (const { values } of discriminant.cases) {
|
||||
for (const value of values) {
|
||||
const key = `${typeof value}:${String(value)}`;
|
||||
if (seen.has(key)) return false;
|
||||
seen.add(key);
|
||||
}
|
||||
}
|
||||
return def.arms.every(
|
||||
(arm, tag) =>
|
||||
discriminant.cases.some((candidate) => candidate.tag === tag) ||
|
||||
NON_OBJECT_ARM_KINDS.has(arm.kind),
|
||||
);
|
||||
}
|
||||
|
||||
/** Per arm of `def`, the dyn kind of the values a builder may convert into
|
||||
* that arm without matching its structure inside a trusted build, or null.
|
||||
* A value there already matched one of the union's arms, so the arm is
|
||||
* known when it is the only arm that takes values of its kind: the arm
|
||||
* whose literal a plain object carries (trustedArmByLiteral), or the one
|
||||
* arm a plain object or an array can match at all. */
|
||||
private trustedArmKinds(def: IrUnionDef): (number | null)[] {
|
||||
const byLiteral = this.trustedArmByLiteral(def);
|
||||
const objectArms = def.arms.flatMap((arm, tag) =>
|
||||
NON_OBJECT_ARM_KINDS.has(arm.kind) ? [] : [tag],
|
||||
);
|
||||
return def.arms.map((arm, tag) => {
|
||||
if (def.discriminant?.cases.some((candidate) => candidate.tag === tag))
|
||||
return byLiteral ? DYN_KIND.OBJ : null;
|
||||
if (objectArms.length !== 1 || objectArms[0] !== tag) return null;
|
||||
if (arm.kind === "array") return DYN_KIND.ARR;
|
||||
if (arm.kind === "record" && this.host.recordsById.get(arm.shapeId)?.tuple !== true)
|
||||
return DYN_KIND.OBJ;
|
||||
return null;
|
||||
});
|
||||
}
|
||||
|
||||
/** Branch to a variant only after its literal discriminator and complete
|
||||
* structure match. Use the same path for predicates and builders. */
|
||||
* structure match. Use the same path for predicates and builders. With a
|
||||
* `trustedKind` (builders, trustedArmKinds), a value of that dyn kind
|
||||
* inside a trusted build skips the structure match once its literal
|
||||
* matched. */
|
||||
private unionArmMatch(
|
||||
B: BlockBuilder,
|
||||
def: IrUnionDef,
|
||||
@@ -374,6 +434,7 @@ export class LlDyn {
|
||||
yes: string,
|
||||
no: string,
|
||||
preserveRefs = false,
|
||||
trustedKind: number | null = null,
|
||||
): void {
|
||||
const guard = def.discriminant?.cases.find((candidate) => candidate.tag === tag);
|
||||
if (guard && def.discriminant) {
|
||||
@@ -402,6 +463,19 @@ export class LlDyn {
|
||||
});
|
||||
B.startBlock(structure);
|
||||
}
|
||||
if (trustedKind !== null) {
|
||||
this.host.declare(`declare zeroext i1 @scr_dyn_trusted_build()`);
|
||||
const kind = this.kindOf(B, "%d");
|
||||
const sameKind = B.tmp();
|
||||
const trusted = B.tmp();
|
||||
const skip = B.tmp();
|
||||
B.line(`${sameKind} = icmp eq i32 ${kind}, ${trustedKind}`);
|
||||
B.line(`${trusted} = call zeroext i1 @scr_dyn_trusted_build()`);
|
||||
B.line(`${skip} = and i1 ${sameKind}, ${trusted}`);
|
||||
const full = B.newLabel("du.full");
|
||||
B.condBr(skip, yes, full);
|
||||
B.startBlock(full);
|
||||
}
|
||||
const matches = B.tmp();
|
||||
B.line(
|
||||
`${matches} = call zeroext i1 @${this.dynMatchHelper(def.arms[tag]!, preserveRefs)}(ptr %d)`,
|
||||
@@ -2357,11 +2431,22 @@ export class LlDyn {
|
||||
`llvm emitter bug: dynCheck of unknown union ${t.unionId}`,
|
||||
);
|
||||
// Arms in CANONICAL order, first FULL match wins. The matched
|
||||
// arm's builder can no longer fail.
|
||||
// arm's builder can no longer fail, and it runs as a trusted build:
|
||||
// a nested union inside it picks the arm trustedArmKinds names
|
||||
// without matching the structure that this match already checked.
|
||||
const trustedKinds = preserveRefs ? def.arms.map(() => null) : this.trustedArmKinds(def);
|
||||
const trustedCall = (call: () => void): void => {
|
||||
host.declare(`declare zeroext i1 @scr_dyn_trust_enter()`);
|
||||
host.declare(`declare void @scr_dyn_trust_exit(i1 zeroext)`);
|
||||
const previous = B.tmp();
|
||||
B.line(`${previous} = call zeroext i1 @scr_dyn_trust_enter()`);
|
||||
call();
|
||||
B.line(`call void @scr_dyn_trust_exit(i1 zeroext ${previous})`);
|
||||
};
|
||||
def.arms.forEach((arm, i) => {
|
||||
const lHit = B.newLabel("dcu.h");
|
||||
const lNext = B.newLabel("dcu.n");
|
||||
this.unionArmMatch(B, def, i, lHit, lNext, preserveRefs);
|
||||
this.unionArmMatch(B, def, i, lHit, lNext, preserveRefs, trustedKinds[i]!);
|
||||
B.startBlock(lHit);
|
||||
if (arm.kind === "undefinedT" || arm.kind === "nullT") {
|
||||
// A matched unit arm builds nothing: THE interned immortal
|
||||
@@ -2388,14 +2473,22 @@ export class LlDyn {
|
||||
} else if (host.nullableUnions.has(t.unionId)) {
|
||||
// A nullable union is its (+1) reference payload.
|
||||
const x = B.tmp();
|
||||
B.line(`${x} = call ptr @${this.dynCheckHelper(arm, preserveRefs)}(ptr %d, ptr %path)`);
|
||||
trustedCall(() =>
|
||||
B.line(
|
||||
`${x} = call ptr @${this.dynCheckHelper(arm, preserveRefs)}(ptr %d, ptr %path)`,
|
||||
),
|
||||
);
|
||||
B.terminate(`ret ptr ${x}`);
|
||||
} else {
|
||||
const rc = vAdapters(host, arm);
|
||||
host.declare(`declare ptr @scr_union_new_ref(i32, ptr, ptr, ptr, ptr)`);
|
||||
const x = B.tmp();
|
||||
const u = B.tmp();
|
||||
B.line(`${x} = call ptr @${this.dynCheckHelper(arm, preserveRefs)}(ptr %d, ptr %path)`);
|
||||
trustedCall(() =>
|
||||
B.line(
|
||||
`${x} = call ptr @${this.dynCheckHelper(arm, preserveRefs)}(ptr %d, ptr %path)`,
|
||||
),
|
||||
);
|
||||
B.line(
|
||||
`${u} = call ptr @scr_union_new_ref(i32 ${i}, ptr ${x}, ptr ${rc.retain}, ptr ${rc.release}, ptr ${traceArg(host, arm)})`,
|
||||
);
|
||||
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/7160-json-parse-deep-unions.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/7161-dynamic-values-deep-unions.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
@@ -1442,6 +1442,18 @@ void scr_dyn_build_note(const ScrDyn *d, const void *builder, void *value);
|
||||
void scr_dyn_build_rekey(const ScrDyn *d, const void *builder, void *value);
|
||||
ScrDyn *scr_dyn_visit_view(const ScrDyn *capsule);
|
||||
|
||||
/* A union's builder converts the arm whose literal discriminator and
|
||||
* complete structure matched; every value inside that arm then matched its
|
||||
* own type too. Between scr_dyn_trust_enter and scr_dyn_trust_exit (which
|
||||
* restores the value enter returned), scr_dyn_trusted_build answers true,
|
||||
* and the builder of a nested union whose arms' literals are disjoint picks
|
||||
* a plain object's arm by its literal alone instead of matching the arm's
|
||||
* structure again (dyn.ts unionArmMatch). Code a getter or proxy trap runs
|
||||
* sees false. */
|
||||
bool scr_dyn_trust_enter(void);
|
||||
void scr_dyn_trust_exit(bool previous);
|
||||
bool scr_dyn_trusted_build(void);
|
||||
|
||||
/* ── native handles in the checked-dynamic tree (SCR_DYN_HANDLE) ────────────────────────
|
||||
* Per-tag dispatch ops, registered by the handle's owning unit
|
||||
* (scr_http_dyn_install / scr_net_dyn_install — emitted main() calls
|
||||
|
||||
@@ -9245,6 +9245,9 @@ static SCR_TL ScrDynVisitLog scr_dyn_viewed_log;
|
||||
/* The member value a matcher or builder read last and holds one
|
||||
* reference to until it calls scr_dyn_release_held. */
|
||||
static SCR_TL const ScrDyn *scr_dyn_visit_held;
|
||||
/* Set while a union's builder builds the arm it matched in full
|
||||
* (scr_dyn_trust_enter). */
|
||||
static SCR_TL bool scr_dyn_trusted;
|
||||
|
||||
/* Tables up to this many slots stay allocated for the next session. */
|
||||
#define SCR_DYN_VISIT_KEEP 4096
|
||||
@@ -9356,13 +9359,15 @@ typedef struct {
|
||||
ScrDynVisitLog built_log, viewed_log;
|
||||
bool track;
|
||||
const ScrDyn *held;
|
||||
bool trusted;
|
||||
} ScrDynVisitState;
|
||||
|
||||
static void scr_dyn_visit_suspend(ScrDynVisitState *saved) {
|
||||
*saved = (ScrDynVisitState){scr_dyn_visit_depth, scr_dyn_matching, scr_dyn_built,
|
||||
scr_dyn_viewed, scr_dyn_built_log, scr_dyn_viewed_log,
|
||||
scr_dyn_build_track, scr_dyn_visit_held};
|
||||
*saved = (ScrDynVisitState){scr_dyn_visit_depth, scr_dyn_matching, scr_dyn_built,
|
||||
scr_dyn_viewed, scr_dyn_built_log, scr_dyn_viewed_log,
|
||||
scr_dyn_build_track, scr_dyn_visit_held, scr_dyn_trusted};
|
||||
scr_dyn_visit_depth = 0;
|
||||
scr_dyn_trusted = false;
|
||||
scr_dyn_matching = scr_dyn_built = scr_dyn_viewed = (ScrDynVisitSet){NULL, 0, 0};
|
||||
scr_dyn_built_log = scr_dyn_viewed_log = (ScrDynVisitLog){NULL, 0, 0};
|
||||
scr_dyn_visit_held = NULL;
|
||||
@@ -9382,6 +9387,7 @@ static void scr_dyn_visit_resume(const ScrDynVisitState *saved) {
|
||||
scr_dyn_viewed_log = saved->viewed_log;
|
||||
scr_dyn_build_track = saved->track;
|
||||
scr_dyn_visit_held = saved->held;
|
||||
scr_dyn_trusted = saved->trusted;
|
||||
}
|
||||
|
||||
/* Calls a getter for a member read, outside any visit session. */
|
||||
@@ -9404,6 +9410,16 @@ static ScrDyn *scr_dyn_visit_proxy_get(const ScrDyn *d, ScrStr *name) {
|
||||
return result;
|
||||
}
|
||||
|
||||
bool scr_dyn_trust_enter(void) {
|
||||
bool previous = scr_dyn_trusted;
|
||||
scr_dyn_trusted = true;
|
||||
return previous;
|
||||
}
|
||||
|
||||
void scr_dyn_trust_exit(bool previous) { scr_dyn_trusted = previous; }
|
||||
|
||||
bool scr_dyn_trusted_build(void) { return scr_dyn_trusted; }
|
||||
|
||||
ScrDyn *scr_dyn_obj_read_held(const ScrDyn *d, const char *key, size_t key_len) {
|
||||
ScrDyn *value = scr_dyn_obj_read(d, key, key_len);
|
||||
scr_dyn_visit_held = value;
|
||||
|
||||
@@ -0,0 +1,164 @@
|
||||
// JSON.parse into recursive discriminated unions: deep and wide trees,
|
||||
// unions with primitive arms beside the records, unions whose records share
|
||||
// a literal, and unions with a record arm that has no literal.
|
||||
|
||||
type Expr =
|
||||
| { kind: "num"; value: number }
|
||||
| { kind: "str"; text: string }
|
||||
| { kind: "add"; left: Expr; right: Expr }
|
||||
| { kind: "neg"; operand: Expr }
|
||||
| { kind: "call"; callee: string; args: Expr[] }
|
||||
| { kind: "cond"; test: Expr; then: Expr; otherwise: Expr | null }
|
||||
| { kind: "label"; inner: Expr; name?: string };
|
||||
|
||||
type Stmt =
|
||||
| { kind: "expr"; expr: Expr }
|
||||
| { kind: "ret"; value: Expr | undefined }
|
||||
| { kind: "block"; body: Stmt[] };
|
||||
|
||||
interface Fn {
|
||||
name: string;
|
||||
body: Stmt[];
|
||||
}
|
||||
|
||||
function evaluate(e: Expr): number {
|
||||
switch (e.kind) {
|
||||
case "num":
|
||||
return e.value;
|
||||
case "str":
|
||||
return e.text.length;
|
||||
case "add":
|
||||
return evaluate(e.left) + evaluate(e.right);
|
||||
case "neg":
|
||||
return -evaluate(e.operand);
|
||||
case "call":
|
||||
return e.args.reduce((sum, arg) => sum + evaluate(arg), e.callee.length);
|
||||
case "cond":
|
||||
return evaluate(e.test) !== 0
|
||||
? evaluate(e.then)
|
||||
: e.otherwise === null
|
||||
? 0
|
||||
: evaluate(e.otherwise);
|
||||
case "label":
|
||||
return evaluate(e.inner) + (e.name?.length ?? 0);
|
||||
}
|
||||
}
|
||||
|
||||
function depth(e: Expr): number {
|
||||
switch (e.kind) {
|
||||
case "num":
|
||||
case "str":
|
||||
return 1;
|
||||
case "add":
|
||||
return 1 + Math.max(depth(e.left), depth(e.right));
|
||||
case "neg":
|
||||
return 1 + depth(e.operand);
|
||||
case "call":
|
||||
return 1 + e.args.reduce((max, arg) => Math.max(max, depth(arg)), 0);
|
||||
case "cond":
|
||||
return 1 + Math.max(depth(e.test), depth(e.then), e.otherwise ? depth(e.otherwise) : 0);
|
||||
case "label":
|
||||
return 1 + depth(e.inner);
|
||||
}
|
||||
}
|
||||
|
||||
function chain(n: number): string {
|
||||
let text = '{"kind":"num","value":1}';
|
||||
for (let i = 0; i < n; i++) {
|
||||
text =
|
||||
i % 3 === 0
|
||||
? `{"kind":"neg","operand":${text}}`
|
||||
: i % 3 === 1
|
||||
? `{"kind":"add","left":${text},"right":{"kind":"str","text":"ab"}}`
|
||||
: `{"kind":"label","inner":${text}${i % 2 === 0 ? ',"name":"x"' : ""}}`;
|
||||
}
|
||||
return text;
|
||||
}
|
||||
|
||||
function wide(n: number, level: number): string {
|
||||
if (level === 0) return `{"kind":"num","value":${n}}`;
|
||||
const args: string[] = [];
|
||||
for (let i = 0; i < 3; i++) args.push(wide(n + i, level - 1));
|
||||
return `{"kind":"call","callee":"f${level}","args":[${args.join(",")}]}`;
|
||||
}
|
||||
|
||||
for (const n of [0, 1, 5, 60, 400]) {
|
||||
const e = JSON.parse(chain(n)) as Expr;
|
||||
console.log("chain", n, e.kind, depth(e), evaluate(e));
|
||||
}
|
||||
|
||||
const tree = JSON.parse(wide(1, 6)) as Expr;
|
||||
console.log("wide", depth(tree), evaluate(tree));
|
||||
|
||||
const cond = JSON.parse(
|
||||
'{"kind":"cond","test":{"kind":"num","value":0},"then":{"kind":"num","value":5},"otherwise":null}',
|
||||
) as Expr;
|
||||
console.log("cond", evaluate(cond));
|
||||
|
||||
const fn = JSON.parse(
|
||||
JSON.stringify({
|
||||
name: "main",
|
||||
body: [
|
||||
{ kind: "expr", expr: JSON.parse(chain(30)) },
|
||||
{ kind: "block", body: [{ kind: "ret", value: JSON.parse(wide(2, 3)) }, { kind: "ret" }] },
|
||||
],
|
||||
}),
|
||||
) as Fn;
|
||||
let statements = 0;
|
||||
const visit = (stmts: Stmt[]): void => {
|
||||
for (const s of stmts) {
|
||||
statements++;
|
||||
if (s.kind === "block") visit(s.body);
|
||||
else if (s.kind === "expr") console.log("stmt expr", evaluate(s.expr));
|
||||
else console.log("stmt ret", s.value === undefined ? "none" : evaluate(s.value));
|
||||
}
|
||||
};
|
||||
visit(fn.body);
|
||||
console.log("statements", statements);
|
||||
|
||||
// Primitive arms beside the records.
|
||||
type Value = number | string | null | { kind: "list"; items: Value[] } | { kind: "pair"; first: Value; second: Value };
|
||||
function show(v: Value): string {
|
||||
if (v === null || typeof v === "number" || typeof v === "string") return JSON.stringify(v);
|
||||
return v.kind === "list" ? `[${v.items.map(show).join(" ")}]` : `(${show(v.first)} . ${show(v.second)})`;
|
||||
}
|
||||
const values = JSON.parse(
|
||||
'[1,"a",null,{"kind":"list","items":[2,{"kind":"pair","first":"b","second":{"kind":"list","items":[null,3]}}]}]',
|
||||
) as Value[];
|
||||
console.log("values", values.map(show).join(" | "));
|
||||
|
||||
// Two records that share a literal: the arm follows the other fields.
|
||||
type Shared = { kind: "a"; count: number } | { kind: "a"; name: string } | { kind: "b"; next: Shared[] };
|
||||
function describe(s: Shared): string {
|
||||
if (s.kind === "b") return `b(${s.next.map(describe).join(",")})`;
|
||||
return "count" in s ? `count=${s.count}` : `name=${s.name}`;
|
||||
}
|
||||
const shared = JSON.parse(
|
||||
'{"kind":"b","next":[{"kind":"a","count":1},{"kind":"a","name":"n"},{"kind":"b","next":[{"kind":"a","name":"m"}]}]}',
|
||||
) as Shared;
|
||||
console.log("shared", describe(shared));
|
||||
|
||||
// A record arm without a literal beside records that have one.
|
||||
type Loose = { kind: "leaf"; value: number } | { kind: "node"; children: Loose[] } | { weight: number; child: Loose };
|
||||
function total(l: Loose): number {
|
||||
if ("weight" in l) return l.weight * total(l.child);
|
||||
return l.kind === "leaf" ? l.value : l.children.reduce((sum, c) => sum + total(c), 0);
|
||||
}
|
||||
const loose = JSON.parse(
|
||||
'{"kind":"node","children":[{"kind":"leaf","value":2},{"weight":3,"child":{"kind":"node","children":[{"kind":"leaf","value":4},{"weight":2,"child":{"kind":"leaf","value":5}}]}}]}',
|
||||
) as Loose;
|
||||
console.log("loose", total(loose));
|
||||
|
||||
// Optional and nullable members holding records and arrays of records.
|
||||
type Box = { value: number; child?: Box; rest: Box[] | null };
|
||||
function weigh(b: Box): number {
|
||||
let total = b.value;
|
||||
if (b.child !== undefined) total += 2 * weigh(b.child);
|
||||
for (const r of b.rest ?? []) total += weigh(r);
|
||||
return total;
|
||||
}
|
||||
let boxText = '{"value":1,"rest":null}';
|
||||
for (let i = 2; i < 120; i++)
|
||||
boxText = i % 2 === 0 ? `{"value":${i},"child":${boxText},"rest":null}` : `{"value":${i},"rest":[${boxText},{"value":1,"rest":[]}]}`;
|
||||
const boxes = JSON.parse(boxText) as Box;
|
||||
console.log("boxes", weigh(boxes) % 1000003);
|
||||
@@ -0,0 +1,73 @@
|
||||
// Dynamic values cast into recursive discriminated unions: trees built as
|
||||
// untyped objects, trees that mix typed records into untyped ones, and casts
|
||||
// of the same values twice.
|
||||
|
||||
type Node =
|
||||
| { kind: "leaf"; value: number }
|
||||
| { kind: "pair"; left: Node; right: Node }
|
||||
| { kind: "wrap"; inner: Node; tag: string }
|
||||
| { kind: "many"; items: Node[] };
|
||||
|
||||
function sum(n: Node): number {
|
||||
switch (n.kind) {
|
||||
case "leaf":
|
||||
return n.value;
|
||||
case "pair":
|
||||
return sum(n.left) + sum(n.right);
|
||||
case "wrap":
|
||||
return sum(n.inner) + n.tag.length;
|
||||
case "many":
|
||||
return n.items.reduce((total, item) => total + sum(item), 0);
|
||||
}
|
||||
}
|
||||
|
||||
function height(n: Node): number {
|
||||
switch (n.kind) {
|
||||
case "leaf":
|
||||
return 1;
|
||||
case "pair":
|
||||
return 1 + Math.max(height(n.left), height(n.right));
|
||||
case "wrap":
|
||||
return 1 + height(n.inner);
|
||||
case "many":
|
||||
return 1 + n.items.reduce((max, item) => Math.max(max, height(item)), 0);
|
||||
}
|
||||
}
|
||||
|
||||
function untyped(levels: number): any {
|
||||
let node: any = { kind: "leaf", value: 1 };
|
||||
for (let i = 0; i < levels; i++) {
|
||||
node =
|
||||
i % 3 === 0
|
||||
? { kind: "wrap", inner: node, tag: "t" }
|
||||
: i % 3 === 1
|
||||
? { kind: "pair", left: node, right: { kind: "leaf", value: i } }
|
||||
: { kind: "many", items: [node, { kind: "leaf", value: 2 }] };
|
||||
}
|
||||
return node;
|
||||
}
|
||||
|
||||
for (const levels of [0, 2, 10, 90, 300]) {
|
||||
const value: unknown = untyped(levels);
|
||||
const node = value as Node;
|
||||
console.log("untyped", levels, node.kind, height(node), sum(node));
|
||||
}
|
||||
|
||||
// Typed records inside an untyped tree.
|
||||
const typedLeaf: Node = { kind: "leaf", value: 7 };
|
||||
const typedPair: Node = { kind: "pair", left: typedLeaf, right: { kind: "leaf", value: 3 } };
|
||||
const mixed: any = { kind: "many", items: [typedPair, untyped(20), { kind: "wrap", inner: typedLeaf, tag: "xy" }] };
|
||||
const fromMixed = mixed as Node;
|
||||
console.log("mixed", height(fromMixed), sum(fromMixed));
|
||||
|
||||
// The same untyped value cast twice gives equal trees.
|
||||
const again: any = untyped(40);
|
||||
const first = again as Node;
|
||||
const second = again as Node;
|
||||
console.log("twice", sum(first) === sum(second), height(first), height(second));
|
||||
|
||||
// A tree that changes between casts.
|
||||
const changing: any = { kind: "pair", left: { kind: "leaf", value: 1 }, right: untyped(5) };
|
||||
console.log("before", sum(changing as Node));
|
||||
changing.left = { kind: "wrap", inner: { kind: "leaf", value: 10 }, tag: "abc" };
|
||||
console.log("after", sum(changing as Node));
|
||||
@@ -0,0 +1 @@
|
||||
{ "plain": 11054, "san": 84858 }
|
||||
@@ -0,0 +1 @@
|
||||
{ "plain": 5843, "san": 57446 }
|
||||
@@ -427,6 +427,104 @@ console.log("unreachable", t.label);
|
||||
);
|
||||
});
|
||||
|
||||
test("nested unions: a failure inside a matched arm names the union that holds it", async () => {
|
||||
// A union's builder converts the arm that matched; the unions nested in
|
||||
// it pick their arms by literal or by the kind of value only one arm
|
||||
// takes. A value that fails anywhere below fails its outermost union,
|
||||
// whose arms the message names.
|
||||
const r = await compileAndRun(
|
||||
"deep-discriminated",
|
||||
`type Expr =
|
||||
| { kind: "num"; value: number }
|
||||
| { kind: "str"; text: string }
|
||||
| { kind: "add"; left: Expr; right: Expr }
|
||||
| { kind: "call"; callee: string; args: Expr[] }
|
||||
| { kind: "neg"; operand: Expr }
|
||||
| { kind: "opt"; inner: Expr | undefined; label?: string };
|
||||
type Stmt =
|
||||
| { kind: "expr"; expr: Expr }
|
||||
| { kind: "ret"; value: Expr | null }
|
||||
| { kind: "block"; body: Stmt[] };
|
||||
type Fn = { name: string; body: Stmt[] };
|
||||
|
||||
function attempt(label: string, text: string, as: "expr" | "stmt" | "fn" | "list"): void {
|
||||
try {
|
||||
if (as === "expr") {
|
||||
const e = JSON.parse(text) as Expr;
|
||||
console.log(label, "ok", e.kind);
|
||||
} else if (as === "stmt") {
|
||||
const s = JSON.parse(text) as Stmt;
|
||||
console.log(label, "ok", s.kind);
|
||||
} else if (as === "fn") {
|
||||
const f = JSON.parse(text) as Fn;
|
||||
console.log(label, "ok", f.name, f.body.length);
|
||||
} else {
|
||||
const l = JSON.parse(text) as Expr[];
|
||||
console.log(label, "ok", l.length);
|
||||
}
|
||||
} catch (e) {
|
||||
console.log(label, "error", (e as Error).message);
|
||||
}
|
||||
}
|
||||
|
||||
const deep = (leaf: string, depth: number): string => {
|
||||
let s = leaf;
|
||||
for (let i = 0; i < depth; i++) s = \`{"kind":"neg","operand":\${s}}\`;
|
||||
return s;
|
||||
};
|
||||
attempt("valid", deep('{"kind":"num","value":1}', 20), "expr");
|
||||
attempt("deep wrong field", deep('{"kind":"num","value":"x"}', 5), "expr");
|
||||
attempt("deep unknown kind", deep('{"kind":"nope","value":1}', 5), "expr");
|
||||
attempt("deep string node", deep('"text"', 5), "expr");
|
||||
attempt("deep missing field", deep('{"kind":"add","left":{"kind":"num","value":1}}', 4), "expr");
|
||||
attempt("deep array arg", deep('{"kind":"call","callee":"f","args":[{"kind":"num","value":1},{"kind":"str","text":2}]}', 3), "expr");
|
||||
attempt("optional inner", deep('{"kind":"opt","inner":{"kind":"num","value":true}}', 3), "expr");
|
||||
attempt("optional label", deep('{"kind":"opt","inner":{"kind":"num","value":1},"label":3}', 3), "expr");
|
||||
attempt("stmt deep", '{"kind":"block","body":[{"kind":"expr","expr":' + deep('{"kind":"num","value":null}', 3) + '}]}', "stmt");
|
||||
attempt("ret null", '{"kind":"ret","value":null}', "stmt");
|
||||
attempt("fn deep", '{"name":"f","body":[{"kind":"ret","value":' + deep('{"kind":"str","text":5}', 4) + '}]}', "fn");
|
||||
attempt("list element", '[{"kind":"num","value":1},' + deep('{"kind":"num"}', 2) + "]", "list");
|
||||
attempt("kind not string", deep('{"kind":3,"value":1}', 2), "expr");
|
||||
attempt("valid list", '[' + deep('{"kind":"num","value":1}', 3) + ',{"kind":"str","text":"t"}]', "list");
|
||||
|
||||
type Box = { value: number; child?: Box; rest: Box[] | null };
|
||||
function box(label: string, text: string): void {
|
||||
try {
|
||||
const b = JSON.parse(text) as Box;
|
||||
console.log(label, "ok", b.value);
|
||||
} catch (e) {
|
||||
console.log(label, "error", (e as Error).message);
|
||||
}
|
||||
}
|
||||
box("box valid", '{"value":1,"rest":null,"child":{"value":2,"rest":[{"value":3,"rest":null}]}}');
|
||||
box("box deep child", '{"value":1,"rest":null,"child":{"value":2,"rest":null,"child":{"value":"x","rest":null}}}');
|
||||
box("box deep rest", '{"value":1,"rest":[{"value":2,"rest":[{"value":3,"rest":[1]}]}]}');
|
||||
box("box child array", '{"value":1,"rest":null,"child":[]}');
|
||||
`,
|
||||
);
|
||||
expect(r.exitCode).toBe(0);
|
||||
expect(r.stdout.trimEnd().split("\n")).toEqual([
|
||||
"valid ok neg",
|
||||
"deep wrong field error expected object | object | object | object | object | object at $, got object",
|
||||
"deep unknown kind error expected object | object | object | object | object | object at $, got object",
|
||||
"deep string node error expected object | object | object | object | object | object at $, got object",
|
||||
"deep missing field error expected object | object | object | object | object | object at $, got object",
|
||||
"deep array arg error expected object | object | object | object | object | object at $, got object",
|
||||
"optional inner error expected object | object | object | object | object | object at $, got object",
|
||||
"optional label error expected object | object | object | object | object | object at $, got object",
|
||||
"stmt deep error expected object | object | object at $, got object",
|
||||
"ret null ok ret",
|
||||
"fn deep error expected object | object | object at $.body[0], got object",
|
||||
"list element error expected object | object | object | object | object | object at $[1], got object",
|
||||
"kind not string error expected object | object | object | object | object | object at $, got object",
|
||||
"valid list ok 2",
|
||||
"box valid ok 1",
|
||||
"box deep child error expected number at $.child.child.value, got string",
|
||||
"box deep rest error expected object at $.rest[0].rest[0].rest[0], got number",
|
||||
"box child array error expected object | undefined at $.child, got array",
|
||||
]);
|
||||
});
|
||||
|
||||
test("array where a record is expected names the mismatch at the root", async () => {
|
||||
const r = await compileAndRun(
|
||||
"root-kind",
|
||||
|
||||
Reference in New Issue
Block a user