Files
scriptc/tests/corpus/integer-recursion-records.ts
T
Chris Tate d97e816848 Keep integer clones off records that may be views
A checked conversion can answer a record view: an object of the target
record type whose fields live in the record it views, at that record's
offsets (record-views.ts). An integer clone read the fields of a borrowed
record at its own shape's offsets, so for a view it read the view's unused
slots and returned wrong results.

Record shapes that may be views are no longer references a clone reads, so
functions that read them keep the ordinary body, which reads a view's fields
through the original record. The emitter now plans integer clones after
record views.

integer-recursion-records.ts gains a recursive sum over a view.
2026-10-27 21:15:37 -05:00

175 lines
5.7 KiB
TypeScript

// Pure integer recursion over record graphs: counts and sums read from
// fields, sums that leave the int32 range, number fields that are not
// int32s, boolean fields, null tests and narrows, endless recursion, and
// records that view another record.
interface Tree {
left: Tree | null;
right: Tree | null;
}
function build(depth: number): Tree {
return depth > 0 ? { left: build(depth - 1), right: build(depth - 1) } : { left: null, right: null };
}
// The binary-trees check: an accumulating self call after a nested one.
function count(node: Tree): number {
if (node.left === null || node.right === null) return 1;
return 1 + count(node.left) + count(node.right);
}
// The same with the calls in the other order and a ternary.
function countRight(node: Tree): number {
return node.left === null || node.right === null ? 1 : count(node.right) + countRight(node.left) + 1;
}
interface Item {
weight: number;
heavy: boolean;
next: Item | null;
}
function list(weights: number[]): Item | null {
let head: Item | null = null;
for (let i = weights.length - 1; i >= 0; i--) head = { weight: weights[i]!, heavy: weights[i]! > 100, next: head };
return head;
}
// A sum of number fields: int32 fields stay integers; a fraction, NaN, -0
// or a sum past the int32 range takes the ordinary path.
function total(item: Item): number {
if (item.next === null) return item.weight;
return item.weight + total(item.next);
}
// Boolean fields and a count of the items they select.
function heavyCount(item: Item): number {
const here = item.heavy ? 1 : 0;
return item.next === null ? here : here + heavyCount(item.next);
}
function anyHeavy(item: Item): boolean {
if (item.heavy) return true;
return item.next === null ? false : anyHeavy(item.next);
}
// The first weight above 1000, or -1.
function firstAbove(item: Item): number {
if (item.weight > 1000) return item.weight;
return item.next === null ? -1 : firstAbove(item.next);
}
// The length of a list: a cycle overflows the stack.
function length(item: Item): number {
return item.next === null ? 1 : 1 + length(item.next);
}
// A plain tail call around a cycle until a weight is found.
function find(item: Item, weight: number, steps: number): number {
if (item.weight === weight) return steps;
return steps > 100 ? -1 : find(item.next!, weight, steps + 1);
}
interface Pair {
a: number;
b: number;
}
function sumPair(p: Pair): number {
return p.a + p.b;
}
// A record parameter next to number parameters, and mutual recursion.
function evenWeight(item: Item, depth: number): number {
return item.next === null ? depth : oddWeight(item.next, depth + item.weight);
}
function oddWeight(item: Item, depth: number): number {
return item.next === null ? -depth : evenWeight(item.next, depth - item.weight);
}
// An optional field (`next?:`) and a class instance stay on the ordinary path.
interface Link {
value: number;
next?: Link;
}
function linkSum(link: Link): number {
return link.next === undefined ? link.value : link.value + linkSum(link.next);
}
class Node {
readonly value: number;
readonly next: Node | null;
constructor(value: number, next: Node | null) {
this.value = value;
this.next = next;
}
}
function nodeSum(node: Node): number {
return node.next === null ? node.value : node.value + nodeSum(node.next);
}
// A record converted from an untyped value into a narrower record type is
// a view of the original: its fields are the original's.
type Measured = { label: string; hi: number; open: boolean; lo: number };
type Span = { lo: number; hi: number };
function spanSum(span: Span, n: number): number {
return n === 0 ? span.lo : span.hi + spanSum(span, n - 1);
}
function attempt(label: string, run: () => number | boolean): void {
try {
console.log(label, run());
} catch (e) {
console.log(label, e instanceof TypeError, e instanceof RangeError, (e as Error).message);
}
}
for (const depth of [0, 1, 4, 10, 16]) {
const tree = build(depth);
console.log("tree", depth, count(tree), countRight(tree));
}
const cases: [string, number[]][] = [
["small", [1, 2, 3, 4, 5]],
["negative", [-5, 7, -2147483648, 2147483647]],
["past int32", [2147483647, 1, 2]],
["below int32", [-2147483648, -1]],
["large", [1e9, 1e9, 1e9, 1e9]],
["fraction", [1, 2.5, 3]],
["nan", [1, NaN, 2]],
["infinity", [1, Infinity, -Infinity]],
["minus zero", [-0]],
["minus zero sum", [-0, -0]],
["zero sum", [5, -5]],
["beyond int32 field", [4294967296, -4294967295]],
["heavy", [150, 20, 300, 1001]],
];
for (const [label, weights] of cases) {
const head = list(weights)!;
const t = total(head);
console.log(label, t, Object.is(t, -0), heavyCount(head), anyHeavy(head), evenWeight(head, 0), firstAbove(head));
}
const shallow = list(new Array<number>(500).fill(3))!;
console.log("shallow", length(shallow), total(shallow));
const ring: Item = { weight: 1, heavy: false, next: null };
let tail = ring;
for (let i = 2; i <= 10; i++) {
const item: Item = { weight: i, heavy: false, next: null };
tail.next = item;
tail = item;
}
tail.next = ring;
console.log("find", find(ring, 7, 0), find(ring, 11, 0));
attempt("ring length", () => length(ring));
attempt("ring total", () => total(ring));
console.log("pair", sumPair({ a: 2147483647, b: 1 }), sumPair({ a: 3, b: 4 }), sumPair({ a: 0.5, b: 0.25 }));
console.log("link", linkSum({ value: 1, next: { value: 2, next: { value: 3 } } }));
console.log("node", nodeSum(new Node(1, new Node(2, new Node(2147483647, null)))));
const measured: Measured = { label: "m", hi: 2, open: true, lo: 7 };
const untyped: unknown = measured;
const span = untyped as Span;
span.hi = 3;
console.log("view", spanSum(span, 4), measured.hi, spanSum({ lo: 1, hi: 1 }, 3));