Files
scriptc/tests/corpus/2486-recursive-record-boundaries.ts
Chris Tate b49052a389 Recursive record types compile statically as named recursive shapes
- mapType back-references mint placeholder shapes/unions finalized at frame exit; identity is per checker type, and context-sensitive (generic/mixin) knots stay fenced
- every type walk survives cyclic shape graphs: coinductive JSON/dyn-safety predicates, guarded formatIrType/jsvalLiftable, cycle headers via the existing collector fixpoint on both backends
- JSON.stringify throws V8's byte-exact circular TypeError (hop lines, ellipsis rule); cyclic values crossing into checked-dynamic slots trap instead of hanging
- console.log/util.inspect render <ref *N>/[Circular *N] exactly (SEMANTICS 129 resolved; depth:null lowers) and deepStrictEqual terminates through Node's pair memo
- corpus 2480-2488 green on C, LLVM, and the sanitized RC-audit lane, which proves child->parent cycles collect
2026-07-23 13:24:13 -05:00

69 lines
2.3 KiB
TypeScript

// Boundary crossings for recursive record types: checked casts validate
// the full recursive shape (JSON input is a tree, so structural recursion
// terminates), unknown-typed slots deep-copy acyclic values, Maps hold
// recursive values, and utility-type spellings intern the same knot.
interface TreeNode { label: string; children: TreeNode[] }
// JSON.parse + checked cast: the dynCheck walker validates recursively.
const text = '{"label":"root","children":[{"label":"a","children":[]},{"label":"b","children":[{"label":"b1","children":[]}]}]}';
const parsed = JSON.parse(text) as TreeNode;
function labels(n: TreeNode): string {
let out = n.label;
for (const c of n.children) out += "," + labels(c);
return out;
}
console.log(labels(parsed));
// Round trip through unknown (an ACYCLIC value deep-copies in; the cast
// walks it back out).
const u: unknown = parsed;
const back = u as TreeNode;
console.log(labels(back));
// Maps of recursive records.
const byLabel = new Map<string, TreeNode>();
byLabel.set("root", parsed);
byLabel.set("first", parsed.children[0]);
const hit = byLabel.get("first");
console.log(byLabel.size, hit !== undefined ? hit.label : "?");
// Arrays of recursive records + reduce (the walker idiom).
function count(n: TreeNode): number {
return n.children.reduce((acc, c) => acc + count(c), 1);
}
console.log(count(parsed));
// A recursive shape through a mapped utility type (Partial).
type SelfP = Partial<{ next: SelfP; v: number }>;
const sp: SelfP = { v: 1, next: { v: 2, next: { v: 3 } } };
function total(p: SelfP): number {
let sum = p.v ?? 0;
if (p.next !== undefined) sum += total(p.next);
return sum;
}
console.log(total(sp));
// Union of two DIFFERENT recursive records, narrowed by discriminant.
interface Section { kind: "section"; title: string; parts: Doc[] }
interface Para { kind: "para"; text: string }
type Doc = Section | Para;
const doc: Doc = {
kind: "section",
title: "top",
parts: [
{ kind: "para", text: "one" },
{ kind: "section", title: "inner", parts: [{ kind: "para", text: "two" }] },
],
};
function render(d: Doc): string {
if (d.kind === "para") return d.text;
let out = d.title + "(";
for (const p of d.parts) out += render(p) + ";";
return out + ")";
}
console.log(render(doc));
// JSON.stringify round-trips the recursive shape.
console.log(JSON.stringify(parsed) === text);