Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
7d08a74
Truncate inline on x86-64 and test typed-read presence without trunc
cramforce Oct 10, 2026
d4aeeea
Carry typed-array reads as plain numbers where undefined acts like NaN
cramforce Oct 10, 2026
d1791b9
Inline two-argument Math.min and Math.max
cramforce Oct 10, 2026
028622f
Read cached null answers without a non-null assertion
cramforce Oct 10, 2026
2d90508
Move busy small-object slices to transparent huge pages
cramforce Oct 10, 2026
1c974f0
Keep guarded and in-bounds typed-array reads out of the optional ABI
cramforce Oct 10, 2026
8e07b1c
Pin that an escaping typed array keeps its optional reads
cramforce Oct 10, 2026
b9eb525
Keep the `if`-condition bound opt-in for forwarded assertions
cramforce Oct 10, 2026
1c90711
Record the preflight/order baseline for typed-array-present-reads
cramforce Oct 10, 2026
73a99b2
Let the cycle collector's growth fraction widen to 16
cramforce Oct 10, 2026
e526880
Convert missing number arguments with ToNumber
cramforce Oct 10, 2026
0027fa3
Keep undefined in narrowed optional fields and locals
cramforce Oct 10, 2026
bf17c23
Return defaulted and missing reads from TypeScript lambdas
cramforce Oct 10, 2026
67f9de0
Convert lengths and in-range indices to doubles as non-negative
cramforce Oct 10, 2026
cc7c6cb
Decide inline truncation for the host when no triple is given
cramforce Oct 10, 2026
44f4d0b
Prove more fields construction-only and trace only types on a potenti…
cramforce Oct 10, 2026
3c2eeac
Merge pull request #830 from vercel-labs/perf/alloc-hugepages
ctate Oct 11, 2026
972557b
Merge pull request #832 from vercel-labs/fix/array-read-arguments
ctate Oct 11, 2026
43bb23e
Merge pull request #833 from vercel-labs/perf/cycle-growth-cap
ctate Oct 11, 2026
0b0b7a8
Merge pull request #835 from vercel-labs/fix/number-undefined-unions
ctate Oct 11, 2026
e44f50b
Merge pull request #837 from vercel-labs/perf/trace-cycle-candidates
ctate Oct 11, 2026
3afea88
Merge pull request #839 from vercel-labs/perf/length-to-double-nneg
ctate Oct 11, 2026
5ed8842
Merge perf/typed-array-reads into perf/alloc-hugepages
cramforce Oct 11, 2026
fb29112
Expect only cycle members to be traced in the self-hosting analysis c…
cramforce Oct 11, 2026
2077bc9
Spell non-negative size conversions as sitofp for clang 18
cramforce Oct 11, 2026
fac59cf
Merge the clang 18 size-conversion fix from perf/length-to-double-nneg
cramforce Oct 11, 2026
5c3f6f8
Land the approved performance stack (#829, #830, #832, #833, #835, #8…
cramforce Oct 11, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
164 changes: 150 additions & 14 deletions packages/compiler/src/backend/cycle-analysis.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,14 +90,27 @@ test("removes an entire acyclic dependency chain to a fixed point", () => {
check(mod, []);
});

test("retains mutual cycles and every shape that can contain them", () => {
test("retains mutual cycles but not the shapes that merely hold them", () => {
const mod = module();
mod.records = [
shape("outer", [ref("a")]),
shape("a", [ref("b")]),
shape("b", [ref("a")]),
shape("leaf", [F64]),
];
// Nothing in the a/b cycle can reach `outer`: its reference is an
// external count and it dies by reference counting alone.
check(mod, ["record:a", "record:b"]);
});

test("a holder that reaches an intrinsic reference stays traced", () => {
const mod = module();
mod.records = [
shape("outer", [ref("a")]),
shape("a", [ref("b")]),
shape("b", [ref("a"), funcOf([], F64)]),
];
// A closure can capture anything, including `outer`.
check(mod, ["record:outer", "record:a", "record:b"]);
});

Expand Down Expand Up @@ -182,7 +195,7 @@ test("map keys and set elements can close cycles without reference values", () =
shape("leaf", [F64]),
shape("acyclic", [mapOf(ref("leaf"), STRING), setOf(ref("leaf"))]),
];
check(mod, ["record:key", "record:member", "record:outer"]);
check(mod, ["record:key", "record:member"]);
});

test("boxed keys and nested array elements retain cycle capability", () => {
Expand Down Expand Up @@ -247,7 +260,7 @@ test("removing one dependency keeps other branches, cycles, and intrinsic refere
shape("intrinsic", [ref("leaf"), { kind: "caught" }]),
shape("pruned", [mapOf(ref("leaf"), arrayOf(ref("leaf")))]),
];
check(mod, ["record:outer", "record:cycle", "record:intrinsic"]);
check(mod, ["record:cycle", "record:intrinsic"]);
});

test("hierarchies and unions participate in the same dependency graph", () => {
Expand All @@ -262,12 +275,32 @@ test("hierarchies and unions participate in the same dependency graph", () => {
{ name: "Sibling", base: "Base", fields: [], loc },
{ name: "Base", fields: [], loc },
];
mod.unions = [{ id: "link", arms: [{ kind: "object", className: "Sibling" }, F64] }];
// A Base-typed arm can hold a Child, which holds the union again.
mod.unions = [{ id: "link", arms: [{ kind: "object", className: "Base" }, F64] }];
check(mod, ["object:Child", "object:Sibling", "object:Base"], ["link"]);
// A Sibling-typed arm can only hold a Sibling, which holds nothing.
mod.unions[0]!.arms = [{ kind: "object", className: "Sibling" }, F64];
check(mod, []);
mod.unions[0]!.arms = [F64, STRING];
check(mod, []);
});

test("a slot reaches the subtree of its static class, not the whole hierarchy", () => {
const mod = module();
const base: IrType = { kind: "object", className: "Base" };
const other: IrType = { kind: "object", className: "Other" };
mod.classes = [
{ name: "Base", fields: [], loc },
{ name: "Leaf", base: "Base", fields: [{ name: "callback", type: funcOf([], VOID) }], loc },
{ name: "Other", base: "Base", fields: [], loc },
{ name: "HoldsOther", fields: [{ name: "other", type: other }], loc },
{ name: "HoldsBase", fields: [{ name: "base", type: base }], loc },
];
// Leaf's closure headers the whole hierarchy, but an Other-typed slot
// can never hold a Leaf.
check(mod, ["object:Base", "object:Leaf", "object:Other", "object:HoldsBase"]);
});

test("local class captures retain tracing even when the class has no fields", () => {
const mod = module();
mod.classes = [{ name: "Local", localCaptures: [], fields: [], loc }];
Expand Down Expand Up @@ -391,20 +424,30 @@ test("field increments count as writes", () => {
checkExact(mod, ["object:Tree"], ["link"]);
});

test("this escaping from the constructor makes the hierarchy mutable", () => {
/** tree(), with `escape` placed before the field stores. */
function escapingTree(escape: IrStmt): IrModule {
const mod = tree();
mod.functions[1]!.body.unshift(escape);
return mod;
}

test("stores after this escapes from the constructor are writes", () => {
const escape: IrStmt = {
kind: "exprStmt",
expr: { kind: "call", callee: "register", args: [thisRef("Tree")], type: VOID, loc },
loc,
};
checkExact(tree([escape]), ["object:Tree"], ["link"]);
checkExact(escapingTree(escape), ["object:Tree"], ["link"]);
// Stores that complete before the escape stay construction-only.
checkExact(tree([escape]), []);
const capture: IrStmt = {
kind: "varDecl",
localId: "f.0",
init: { kind: "closure", fnName: "%fn0", captures: ["this.0"], type: funcOf([], F64), loc },
loc,
};
checkExact(tree([capture]), ["object:Tree"], ["link"]);
checkExact(escapingTree(capture), ["object:Tree"], ["link"]);
checkExact(tree([capture]), []);
const self: IrStmt = setThis("Tree", "left", link);
(self as { value: IrExpr }).value = {
kind: "unionWrap",
Expand All @@ -415,6 +458,59 @@ test("this escaping from the constructor makes the hierarchy mutable", () => {
loc,
};
checkExact(tree([self]), ["object:Tree"], ["link"]);
// A compound assignment's receiver temporary is the same `this`.
const alias = tree();
alias.functions[1]!.locals.push({ id: "r.0", name: "r", type: obj("Tree"), mutable: false });
alias.functions[1]!.body.push(
{ kind: "varDecl", localId: "r.0", init: thisRef("Tree"), loc },
{
kind: "fieldSet",
obj: { kind: "varRef", localId: "r.0", type: obj("Tree"), loc },
className: "Tree",
field: "left",
value: value(link),
loc,
},
);
checkExact(alias, []);
});

test("a subclass constructor runs after its base constructor's escape", () => {
const mod = module();
mod.classes = [
{ name: "Base", fields: [{ name: "next", type: link }], loc },
{
name: "Sub",
base: "Base",
fields: [
{ name: "next", type: link },
{ name: "other", type: link },
],
loc,
},
];
mod.unions = [{ id: "link", arms: [obj("Base"), { kind: "nullT" }] }];
const superCall: IrStmt = {
kind: "exprStmt",
expr: {
kind: "call",
callee: "%Base.constructor",
args: [{ kind: "upcast", value: thisRef("Sub"), type: obj("Base"), loc }],
type: VOID,
loc,
},
loc,
};
const register: IrStmt = {
kind: "exprStmt",
expr: { kind: "call", callee: "register", args: [thisRef("Base")], type: VOID, loc },
loc,
};
mod.functions.push(
ctor("Base", [setThis("Base", "next", link), register]),
ctor("Sub", [superCall, setThis("Sub", "other", link)]),
);
checkExact(mod, ["object:Base", "object:Sub"], ["link"]);
});

test("field reads of this inside the constructor do not escape", () => {
Expand Down Expand Up @@ -494,8 +590,9 @@ test("derived constructors may initialize through super but not escape", () => {
checkExact(method, ["object:Base", "object:Sub"], ["link"]);
});

test("an immutable edge into a mutable cycle stays traced", () => {
test("a holder of a mutable cycle needs no tracing", () => {
// X.holder is constructor-only; Holder.x is rewritten later: x -> h -> x.
// Wrapper holds X, but nothing reachable from X can lead back to it.
const mod = module();
mod.classes = [
{ name: "X", fields: [{ name: "holder", type: obj("Holder") }], loc },
Expand All @@ -512,7 +609,7 @@ test("an immutable edge into a mutable cycle stays traced", () => {
value: value({ kind: "union", unionId: "maybeX" }),
loc,
});
checkExact(mod, ["object:X", "object:Holder", "object:Wrapper"], ["maybeX"]);
checkExact(mod, ["object:X", "object:Holder"], ["maybeX"]);
});

test("collection edges are mutable even behind constructor-only fields", () => {
Expand All @@ -522,25 +619,64 @@ test("collection edges are mutable even behind constructor-only fields", () => {
checkExact(mod, ["object:Call"]);
});

/** A runtime store into a dynamic value under a computed key. */
const dynamicStore: IrStmt = {
kind: "exprStmt",
expr: {
kind: "libCall",
fn: "dyn.keySet",
args: [value(DYN), value(STRING), value(DYN)],
type: VOID,
loc,
},
loc,
};

test("without a dynamic store, crossing into checked-dynamic code writes nothing", () => {
const toDyn: IrStmt = {
kind: "exprStmt",
expr: { kind: "dynFrom", value: value(obj("Tree")), type: DYN, loc },
loc,
};
checkExact(tree([toDyn]), []);
checkExact(tree([{ kind: "throw", value: value(link), loc }]), []);
// Publishing only marks objects immortal; FFI and other intrinsics count.
const intrinsic = (name: "threads.publish" | "console.log"): IrStmt => ({
kind: "exprStmt",
expr: { kind: "intrinsic", name, args: [value(obj("Tree"))], type: VOID, loc },
loc,
});
checkExact(tree([intrinsic("threads.publish")]), []);
checkExact(tree([intrinsic("console.log")]), ["object:Tree"], ["link"]);
});

test("hidden property bags hold nothing without a dynamic store", () => {
const mod = tree();
for (const c of mod.classes!) c.fields.push({ name: "%dynProperties", type: DYN });
checkExact(mod, []);
mod.functions[0]!.body.push(dynamicStore);
checkExact(mod, ["object:Tree"], ["link"]);
});

test("types that can become checked-dynamic capsules are mutable", () => {
const toDyn: IrStmt = {
kind: "exprStmt",
expr: { kind: "dynFrom", value: value(obj("Tree")), type: DYN, loc },
loc,
};
checkExact(tree([toDyn]), ["object:Tree"], ["link"]);
checkExact(tree([toDyn, dynamicStore]), ["object:Tree"], ["link"]);
const thrown: IrStmt = { kind: "throw", value: value(link), loc };
checkExact(tree([thrown]), ["object:Tree"], ["link"]);
checkExact(tree([thrown, dynamicStore]), ["object:Tree"], ["link"]);
// Reached only through a boxed closure signature and a record field.
const holder = tree();
const holder = tree([dynamicStore]);
holder.records = [shape("box", [obj("Tree")])];
holder.functions[0]!.body.push({
kind: "exprStmt",
expr: { kind: "dynFrom", value: value(funcOf([], ref("box"))), type: DYN, loc },
loc,
});
checkExact(holder, ["object:Tree", "record:box"], ["link"]);
const logged = tree();
checkExact(holder, ["object:Tree"], ["link"]);
const logged = tree([dynamicStore]);
logged.functions[0]!.body.push({
kind: "exprStmt",
expr: { kind: "jsonStringify", value: value(arrayOf(link)), type: STRING, loc },
Expand Down
66 changes: 51 additions & 15 deletions packages/compiler/src/backend/cycle-analysis.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
import type { IrModule, IrType } from "../ir/ir.js";
import { funcOf, mapOf, RUNTIME_EMITTER_CLASS, STRING, VOID } from "../ir/ir.js";
import {
DYN_CLASS_PROPERTIES,
funcOf,
mapOf,
RUNTIME_EMITTER_CLASS,
STRING,
VOID,
} from "../ir/ir.js";
import { computeCycleMutability, fieldKey } from "./cycle-mutability.js";

/** Cycle capability for native code generation.
Expand Down Expand Up @@ -89,13 +96,28 @@ export function computeTraced(mod: IrModule): { shapes: Set<string>; unions: Set
reverse: [],
traced: false,
});
const units = new Map<string, Node>();
// One node per class and record, plus a SUBTREE node per class with
// subclasses: a slot of static type C can hold an instance of any class
// in C's subtree, and of nothing else. Reachability is computed per
// class; tracing is then decided per hierarchy unit below.
const shapes = new Map<string, Node>();
const unions = new Map((mod.unions ?? []).map((u) => [u.id, node()]));
for (const s of shapeDefs) {
let unit = units.get(s.unit);
if (!unit) units.set(s.unit, (unit = node()));
shapes.set(s.key, unit);
for (const s of shapeDefs) shapes.set(s.key, node());
const children = new Map<string, string[]>();
for (const c of classes)
if (c.base !== undefined && shapes.has(`object:${c.base}`)) {
let list = children.get(c.base);
if (!list) children.set(c.base, (list = []));
list.push(c.name);
}
// Created up front and linked without recursion (hierarchies can be
// thousands deep).
const subtrees = new Map([...children.keys()].map((name) => [name, node()] as const));
const subtreeOf = (className: string): Node | undefined =>
subtrees.get(className) ?? shapes.get(`object:${className}`);
for (const [name, sub] of subtrees) {
sub.edges.set(shapes.get(`object:${name}`)!, false);
for (const kid of children.get(name)!) sub.edges.set(subtreeOf(kid)!, false);
}
// Each field/arm is an OR of intrinsic capability and referenced nodes.
const addType = (from: Node, t: IrType, mutable: boolean): void => {
Expand All @@ -115,7 +137,9 @@ export function computeTraced(mod: IrModule): { shapes: Set<string>; unions: Set
const dependency =
t.kind === "union"
? unions.get(t.unionId)
: shapes.get(t.kind === "object" ? `object:${t.className}` : `record:${t.shapeId}`);
: t.kind === "object"
? subtreeOf(t.className)
: shapes.get(`record:${t.shapeId}`);
if (dependency)
from.edges.set(dependency, (from.edges.get(dependency) ?? false) || mutable);
break;
Expand All @@ -132,19 +156,21 @@ export function computeTraced(mod: IrModule): { shapes: Set<string>; unions: Set
};
for (const s of shapeDefs) {
const unitMutable = mutability.all || mutability.units.has(s.unit);
for (const field of s.fields)
for (const field of s.fields) {
if (mutability.emptyPropertyBags && field.name === DYN_CLASS_PROPERTIES) continue;
addType(
shapes.get(s.key)!,
field.type,
unitMutable || mutability.fields.has(fieldKey(s.unit, field.name)),
);
}
}
for (const u of mod.unions ?? [])
for (const arm of u.arms) addType(unions.get(u.id)!, arm, mutability.all);

// Tarjan's strongly connected components, iteratively (shape chains can
// be thousands deep).
const all = [...units.values(), ...unions.values()];
const all = [...shapes.values(), ...subtrees.values(), ...unions.values()];
const stack: Node[] = [];
const componentCapable: boolean[] = [];
let nextIndex = 0;
Expand Down Expand Up @@ -191,18 +217,28 @@ export function computeTraced(mod: IrModule): { shapes: Set<string>; unions: Set
componentCapable.push(capable);
}
}
// Traced: reaches a cycle-capable component (reverse reachability).
// Traced: on a potential cycle. An intrinsic node can reach anything, so
// every node that reaches one (reverse reachability) may lie on a cycle
// through it; otherwise only the members of a component with a mutable
// internal edge can. A node that merely reaches such a component holds it
// from outside: no cycle can lead back to it, so its references are
// external counts and it dies by reference counting alone.
for (const n of all) for (const dependency of n.edges.keys()) dependency.reverse.push(n);
const pending = all.filter((n) => componentCapable[n.component]);
for (const n of pending) n.traced = true;
const reachesIntrinsic = new Set<Node>(all.filter((n) => n.intrinsic));
const pending = [...reachesIntrinsic];
for (let i = 0; i < pending.length; i++) {
for (const dependent of pending[i]!.reverse) {
if (dependent.traced) continue;
dependent.traced = true;
if (reachesIntrinsic.has(dependent)) continue;
reachesIntrinsic.add(dependent);
pending.push(dependent);
}
}
const tracedShapes = new Set([...shapes].filter(([, unit]) => unit.traced).map(([key]) => key));
for (const n of all) n.traced = componentCapable[n.component]! || reachesIntrinsic.has(n);
// Header presence is uniform across a hierarchy: a unit is traced iff any
// member class is.
const tracedUnits = new Set<string>();
for (const s of shapeDefs) if (shapes.get(s.key)!.traced) tracedUnits.add(s.unit);
const tracedShapes = new Set(shapeDefs.filter((s) => tracedUnits.has(s.unit)).map((s) => s.key));
const tracedUnions = new Set([...unions].filter(([, n]) => n.traced).map(([id]) => id));
return { shapes: tracedShapes, unions: tracedUnions };
}
Loading
Loading