Pin frame records that are arms of record unions in the corpus

A record shape that is an arm of a union of records carries a hidden
shape word at struct index 1, so its fields start at index 2. The new
program keeps such records in frame storage while they own string and
array fields, reads and writes those fields, passes the shapes through
the union, compares identities and serializes them. With the frame
storage's reference-field slots addressed at index + 1 instead of the
shape's field base, the compiled program segfaults in the plain lane
and ASan reports a heap-buffer-overflow in the sanitized one.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Malte Ubl
2026-10-18 14:40:37 -07:00
co-authored by Claude Opus 5.5
parent ea7bcd9846
commit 6eb11bf7f4
2 changed files with 94 additions and 0 deletions
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/5997-frame-records-in-record-unions.ts"
],
"diags": []
}
@@ -0,0 +1,88 @@
// Records that are arms of a union of records and also live in frame
// storage. An arm of such a union carries a hidden shape word before its
// fields, so every field address has to skip it: the reference fields a
// frame record owns are read, overwritten and released through the same
// slots as heap records of the shape. The same shapes also flow through
// the union, are compared by identity and are serialized.
interface Ident {
kind: "ident";
name: string;
tags: string[];
}
interface Call {
kind: "call";
callee: string;
args: Ident[];
}
type Node = Ident | Call;
const words = ["alpha", "beta", "gamma", "delta", "epsilon"];
function word(i: number): string {
return words[i % words.length]! + i;
}
function describe(node: Node): string {
switch (node.kind) {
case "ident":
return `${node.name}[${node.tags.join(",")}]`;
case "call":
return `${node.callee}(${node.args.map((a) => a.name).join(", ")})`;
}
}
function weight(node: Node): number {
return node.kind === "ident" ? node.name.length + node.tags.length : node.args.length * 10;
}
// Heap values of the union: the arms are tagged shapes.
const nodes: Node[] = [];
for (let i = 0; i < 6; i++) {
const id: Ident = { kind: "ident", name: word(i), tags: [word(i + 1)] };
nodes.push(i % 2 === 0 ? id : { kind: "call", callee: word(i + 2), args: [id, id] });
}
console.log(nodes.map(describe).join(" | "));
// Frame records of the arms that only flow into field reads.
let total = 0;
let log = "";
for (let i = 0; i < 10; i++) {
const w = word(i);
const id: Ident = { kind: "ident", name: w + "!", tags: [w, word(i + 3)] };
total += id.name.length + id.tags.length + id.tags[1]!.length;
const call: Call = { kind: "call", callee: w.toUpperCase(), args: [] };
total += call.callee.length + call.args.length;
log += id.kind[0]! + call.kind[0]! + id.name[0]!;
}
console.log(total, log);
// A reassigned frame binding: each new value is built apart and copied over
// the slot, and the replaced reference fields are released.
let cur: Ident = { kind: "ident", name: word(0), tags: [] };
for (let i = 1; i < 8; i++) {
cur = { kind: "ident", name: cur.name.slice(0, 3) + word(i), tags: [...cur.tags, cur.name] };
if (i % 3 === 0) cur = { kind: "ident", name: cur.tags[0] ?? "none", tags: cur.tags.slice(1) };
}
console.log(cur.name, cur.tags.length, cur.tags.join("+"));
// Field writes into a frame record's reference slots.
for (let i = 0; i < 4; i++) {
const id: Ident = { kind: "ident", name: word(i), tags: [word(i)] };
id.name = id.name + "/" + i;
id.tags = [id.name, ...id.tags];
id.tags.push(word(i + 7));
console.log(id.name, id.tags.join(" "));
}
// Through the union: describe and weigh by union, keep identity, and
// serialize the arms and the union values.
const a: Ident = { kind: "ident", name: "x", tags: ["t1", "t2"] };
const b: Call = { kind: "call", callee: "f", args: [a] };
const asNode: Node = a;
const picked: Node = weight(b) > weight(a) ? b : a;
console.log(describe(asNode), describe(b), weight(asNode), weight(picked));
console.log(asNode === a, picked === b, b.args[0] === a, (picked as Call).args[0] === asNode);
a.tags.push("t3");
console.log(describe(asNode), describe(picked));
console.log(JSON.stringify(a), JSON.stringify(picked));
console.log(JSON.stringify(nodes.slice(0, 2)));
const mixed: Node[] = [a, b, nodes[1]!];
console.log(mixed.filter((n) => n.kind === "ident").length, mixed.indexOf(asNode), mixed.indexOf(b));