From eb0d09fb81d999a2816a3a9e5f170008a0de9082 Mon Sep 17 00:00:00 2001 From: Malte Ubl Date: Sat, 17 Oct 2026 19:40:45 -0700 Subject: [PATCH 1/3] Keep capture analyses linear in the size of a closure environment Three analyses did work proportional to the whole function once per captured binding, so a factory with thousands of bindings shared by its methods spent most of its compile time in them (48 s of LLVM emission at 8,000 bindings): - LazyCaptures walked the body again for every capture it checked and rebuilt a locals map for every capture of every creation site. Each function's rebound bindings are now collected in one walk and the unchanged-parameter sets are memoized. - int32-slots walked the declaring function once per box to find its initializer and scanned capture lists per member. Both are now per-function sets built on first use. - CaptureFlow scanned all capture edges, capture lists and entry copies per binding and searched statement lists per hoisted or TDZ binding. Edges are grouped by source, captures, parameters and entry copies are indexed, and hoisted and TDZ initializers are found in one pass. The results are unchanged: the candidate compiler emits byte-identical LLVM for the self-hosted compiler's source. Co-Authored-By: Claude Opus 5.5 --- .../src/backend/llvm/lazy-captures.test.ts | 2 +- .../src/backend/llvm/lazy-captures.ts | 49 ++++++--- packages/compiler/src/ir/capture-values.ts | 102 +++++++++--------- packages/compiler/src/ir/int32-slots.ts | 37 +++++-- 4 files changed, 116 insertions(+), 74 deletions(-) diff --git a/packages/compiler/src/backend/llvm/lazy-captures.test.ts b/packages/compiler/src/backend/llvm/lazy-captures.test.ts index b0a47d0814..b0e5c55c55 100644 --- a/packages/compiler/src/backend/llvm/lazy-captures.test.ts +++ b/packages/compiler/src/backend/llvm/lazy-captures.test.ts @@ -47,7 +47,7 @@ function lifted(name: string, body: IrStmt[], captured = "options"): IrFunction loc, }; } -function lazy(...functions: IrFunction[]): Set { +function lazy(...functions: IrFunction[]): ReadonlySet { const map = new Map(functions.map((fn) => [fn.name, fn])); return new LazyCaptures(map).parameters(functions[0]!); } diff --git a/packages/compiler/src/backend/llvm/lazy-captures.ts b/packages/compiler/src/backend/llvm/lazy-captures.ts index 12fae0bf46..2014869af3 100644 --- a/packages/compiler/src/backend/llvm/lazy-captures.ts +++ b/packages/compiler/src/backend/llvm/lazy-captures.ts @@ -26,9 +26,15 @@ function rebound(node: IrStmt | IrExpr): string | null { * closure then allocate no box. The proof follows each capture into nested * function bodies; class environments keep the eager box. */ export class LazyCaptures { - private readonly unchanged = new Map(); private readonly stable = new Map>(); private readonly once = new Map>(); + private readonly unchangedParameters = new Map>(); + /** Per function, every local rebound there or through nested captures; + * the second map counts declarations separately (see rebindings). Each + * body is walked once per map, so the queries stay linear in the IR. */ + private readonly rebound = new Map>(); + private readonly reboundAfterDeclaration = new Map>(); + private readonly pending = new Set(); private sites: Map | null = null; constructor(private readonly functions: ReadonlyMap) {} @@ -112,8 +118,11 @@ export class LazyCaptures { return sites; } - parameters(fn: IrFunction): Set { + parameters(fn: IrFunction): ReadonlySet { + const known = this.unchangedParameters.get(fn); + if (known !== undefined) return known; const result = new Set(); + this.unchangedParameters.set(fn, result); if (fn.async || fn.generator) return result; const locals = new Map(fn.locals.map((local) => [local.id, local])); for (const param of fn.params) { @@ -132,18 +141,33 @@ export class LazyCaptures { ids: ReadonlySet, declarationsBind = false, ): Set { + const all = this.reboundIn(fn, declarationsBind); const changed = new Set(); + for (const id of ids) if (all.has(id)) changed.add(id); + return changed; + } + + /** Every local rebound in `fn` or, through the closures it creates, in + * any function that captures it, directly or through further nesting. + * With `declarationsBind`, declarations are counted separately. */ + private reboundIn(fn: IrFunction, declarationsBind: boolean): ReadonlySet { + const memo = declarationsBind ? this.reboundAfterDeclaration : this.rebound; + const known = memo.get(fn); + if (known !== undefined) return known; + const changed = new Set(); + // Capture chains follow lexical nesting and cannot cycle; on malformed + // input a function reached again while its walk is pending reports + // every capture as rebound (see isUnchanged). + if (!declarationsBind) this.pending.add(fn); const nested: { target: string; local: string; source: string }[] = []; const visit = (node: IrStmt | IrExpr): boolean => { - // With `declarationsBind`, declarations are counted separately. const target = declarationsBind && node.kind === "varDecl" ? null : rebound(node); - if (target !== null && ids.has(target)) changed.add(target); + if (target !== null) changed.add(target); if (node.kind === "classRef") { - for (const id of node.captures ?? []) if (ids.has(id)) changed.add(id); + for (const id of node.captures ?? []) changed.add(id); } else if (node.kind === "closure") { const callee = this.functions.get(node.fnName); node.captures.forEach((id, index) => { - if (!ids.has(id)) return; const capture = callee?.captures?.[index]; if (!capture) changed.add(id); else nested.push({ target: node.fnName, local: capture.localId, source: id }); @@ -156,19 +180,14 @@ export class LazyCaptures { if (!changed.has(edge.source) && !this.isUnchanged(edge.target, edge.local)) changed.add(edge.source); } + if (!declarationsBind) this.pending.delete(fn); + memo.set(fn, changed); return changed; } private isUnchanged(fnName: string, localId: string): boolean { - const key = `${fnName}\0${localId}`; - const known = this.unchanged.get(key); - if (known !== undefined) return known; - // Capture chains follow lexical nesting and cannot cycle; the - // provisional entry only bounds work on malformed input. - this.unchanged.set(key, false); const fn = this.functions.get(fnName); - const result = fn !== undefined && this.rebindings(fn, new Set([localId])).size === 0; - this.unchanged.set(key, result); - return result; + if (fn === undefined || this.pending.has(fn)) return false; + return !this.reboundIn(fn, false).has(localId); } } diff --git a/packages/compiler/src/ir/capture-values.ts b/packages/compiler/src/ir/capture-values.ts index 94cff053bb..d7a1516bb1 100644 --- a/packages/compiler/src/ir/capture-values.ts +++ b/packages/compiler/src/ir/capture-values.ts @@ -244,8 +244,13 @@ interface FunctionFacts { classCaptured: Set; reads: Map; /** Closure capture edges: the creator's binding `source` flows into - * capture `local` of `target`. */ - edges: { target: string; index: number; source: string }[]; + * capture `local` of `target`, grouped by `source`. */ + edges: Map; + /** The function's own captures (first index of each id), class + * captures, and parameters, for constant-time membership tests. */ + captureIndex: Map; + classCaptures: Set; + params: Set; } interface CreationSite { @@ -317,7 +322,7 @@ export class CaptureFlow { ? value.fnName : null; } - const index = fn.captures?.findIndex((c) => c.localId === id) ?? -1; + const index = this.factsOf(fn).captureIndex.get(id) ?? -1; if (index < 0 || !this.stableCapture(fnName, id)) return null; let result: string | null = null; for (const site of this.sites.get(fnName) ?? []) { @@ -399,7 +404,7 @@ export class CaptureFlow { return true; }; everyStmtList(body.slice(declared + 1), { stmt: () => true, expr: count }); - const edges = facts.edges.filter((edge) => edge.source === id).length; + const edges = facts.edges.get(id)?.length ?? 0; return nodes === edges && !facts.classCaptured.has(id); } @@ -410,7 +415,7 @@ export class CaptureFlow { const local = facts.locals.get(id); const declaration = facts.declarations.get(id); if (local === undefined || local.boxed === true || local.tdz === true) return false; - if (fn.params.some((p) => p.localId === id) || declaration?.count !== 1) return false; + if (facts.params.has(id) || declaration?.count !== 1) return false; const writes = facts.writes.get(id) ?? 0; return declaration.initialized ? writes === 0 : facts.hoisted.has(id) && writes === 1; } @@ -450,8 +455,14 @@ export class CaptureFlow { tdzInits: new Map(), classCaptured: new Set(), reads: new Map(), - edges: [], + edges: new Map(), + captureIndex: new Map(), + classCaptures: new Set((fn.classCaptures ?? []).map((c) => c.localId)), + params: new Set(fn.params.map((p) => p.localId)), }; + (fn.captures ?? []).forEach((c, index) => { + if (!facts.captureIndex.has(c.localId)) facts.captureIndex.set(c.localId, index); + }); this.facts.set(fn.name, facts); const capturesIn = (e: IrExpr): Set => { const ids = new Set(); @@ -464,19 +475,17 @@ export class CaptureFlow { visitExpr(e); return ids; }; + // One pass over runs of empty declarations and closure assignments: + // each empty declaration pairs with the first later assignment of its + // binding in the same run. const findHoisted = (body: readonly IrStmt[]): void => { - body.forEach((s, i) => { - if (s.kind !== "varDecl" || s.init !== null) return; - for (let j = i + 1; j < body.length; j++) { - const next = body[j]!; - if (next.kind === "assign" && next.value.kind === "closure") { - if (next.localId === s.localId) { - facts.hoisted.set(s.localId, next); - return; - } - } else if (next.kind !== "varDecl" || next.init !== null) return; - } - }); + const pending = new Set(); + for (const s of body) { + if (s.kind === "assign" && s.value.kind === "closure") { + if (pending.delete(s.localId)) facts.hoisted.set(s.localId, s); + } else if (s.kind === "varDecl" && s.init === null) pending.add(s.localId); + else pending.clear(); + } }; const declare = (s: IrStmt & { kind: "varDecl" }, splittable: boolean): void => { const entry = facts.declarations.get(s.localId); @@ -514,9 +523,11 @@ export class CaptureFlow { let list = this.sites.get(e.fnName); if (list === undefined) this.sites.set(e.fnName, (list = [])); list.push({ owner: fn.name, captures: e.captures, adapts: e.adapts === true }); - e.captures.forEach((source, index) => - facts.edges.push({ target: e.fnName, index, source }), - ); + e.captures.forEach((source, index) => { + let edges = facts.edges.get(source); + if (edges === undefined) facts.edges.set(source, (edges = [])); + edges.push({ target: e.fnName, index, source }); + }); break; } case "classRef": @@ -535,12 +546,15 @@ export class CaptureFlow { // iteration, and statement lists inside expressions release their // locals with the enclosing frame; declarations there stay as they are. const findTdzInits = (body: readonly IrStmt[]): void => { + // The first empty declaration of each binding in this list. + const empty = new Map(); body.forEach((s, j) => { + if (s.kind === "varDecl" && s.init === null && !empty.has(s.localId)) + empty.set(s.localId, j); if (s.kind !== "assign" || s.initializes !== true) return; - const declared = body.findIndex( - (d, i) => i < j && d.kind === "varDecl" && d.localId === s.localId && d.init === null, - ); - if (declared >= 0) facts.tdzInits.set(s.localId, { body, declared, initialized: j }); + const declared = empty.get(s.localId); + if (declared !== undefined && declared < j) + facts.tdzInits.set(s.localId, { body, declared, initialized: j }); }); }; const list = (body: readonly IrStmt[], split: boolean): boolean => { @@ -591,10 +605,6 @@ export class CaptureFlow { return facts; } - private local(fn: IrFunction, id: string): IrLocal | undefined { - return this.factsOf(fn).locals.get(id); - } - /** Nothing rebinds `id` in `fnName` (declarations aside, and `ownWrites` * writes of its own body) or in any body that shares its box through * closure captures. */ @@ -610,8 +620,7 @@ export class CaptureFlow { result = (facts.writes.get(id) ?? 0) === ownWrites && !facts.classCaptured.has(id) && - facts.edges.every((edge) => { - if (edge.source !== id) return true; + (facts.edges.get(id) ?? []).every((edge) => { const capture = this.functions.get(edge.target)?.captures?.[edge.index]; return capture !== undefined && this.unchanged(edge.target, capture.localId); }); @@ -623,19 +632,18 @@ export class CaptureFlow { /** A boxed binding of `fn` itself (not a capture) whose box is filled * once, before any environment observes it, and never written again. */ stableOrigin(fn: IrFunction, id: string): boolean { - const local = this.local(fn, id); + const facts = this.factsOf(fn); + const local = facts.locals.get(id); if (local?.boxed !== true) return false; if (local.tdz === true) { - if (fn.captures?.some((c) => c.localId === id)) return false; - if (this.factsOf(fn).declarations.get(id)?.count !== 1) return false; + if (facts.captureIndex.has(id)) return false; + if (facts.declarations.get(id)?.count !== 1) return false; return this.orderedTdz(fn, id) && this.unchanged(fn.name, id, 1); } - if (fn.captures?.some((c) => c.localId === id)) return false; - if (fn.classCaptures?.some((c) => c.localId === id)) return false; - const facts = this.factsOf(fn); + if (facts.captureIndex.has(id)) return false; + if (facts.classCaptures.has(id)) return false; const declaration = facts.declarations.get(id); - if (fn.params.some((p) => p.localId === id)) - return declaration === undefined && this.unchanged(fn.name, id); + if (facts.params.has(id)) return declaration === undefined && this.unchanged(fn.name, id); if (declaration?.count !== 1) return false; if (declaration.initialized) return this.unchanged(fn.name, id); return facts.hoisted.has(id) && this.unchanged(fn.name, id, 1); @@ -654,8 +662,8 @@ export class CaptureFlow { private computeStableCapture(fnName: string, id: string): boolean { const fn = this.functions.get(fnName); - const index = fn?.captures?.findIndex((c) => c.localId === id) ?? -1; - if (fn === undefined || index < 0) return false; + const index = fn === undefined ? undefined : this.factsOf(fn).captureIndex.get(id); + if (fn === undefined || index === undefined) return false; const sites = this.sites.get(fnName); if (sites === undefined || sites.length === 0) return false; for (const site of sites) { @@ -679,7 +687,7 @@ export class CaptureFlow { const assign = init?.body[init.initialized]; return assign?.kind === "assign" ? plainLiteral(constantNumber(assign.value)) : own.literal; } - const index = fn.captures?.findIndex((c) => c.localId === id) ?? -1; + const index = this.factsOf(fn).captureIndex.get(id) ?? -1; if (index < 0 || !this.stableCapture(fnName, id)) return null; let value: number | null = null; for (const site of this.sites.get(fnName) ?? []) { @@ -715,8 +723,8 @@ export class CaptureFlow { const immutable = new Set(); for (const local of fn.locals) { if (local.boxed !== true) continue; - const capture = fn.captures?.some((c) => c.localId === local.id) === true; - const param = fn.params.some((p) => p.localId === local.id); + const capture = facts.captureIndex.has(local.id); + const param = facts.params.has(local.id); if (capture && local.mutable && this.stableCapture(fn.name, local.id)) immutable.add(local.id); if (!facts.reads.has(local.id)) continue; @@ -871,10 +879,8 @@ export class CaptureFlow { ); const pruned = dead.size === 0 ? body : prune(body, dead); const liveIds = new Set(live.map((s) => (s.kind === "varDecl" ? s.localId : ""))); - const added = locals.filter( - (local) => - liveIds.has(local.id) || !entry.some((s) => s.kind === "varDecl" && s.localId === local.id), - ); + const entryIds = new Set(entry.map((s) => (s.kind === "varDecl" ? s.localId : ""))); + const added = locals.filter((local) => liveIds.has(local.id) || !entryIds.has(local.id)); return { ...fn, locals: [...kept, ...added], body: [...live, ...pruned] }; } } diff --git a/packages/compiler/src/ir/int32-slots.ts b/packages/compiler/src/ir/int32-slots.ts index 5e868793c6..78d7e9e9f6 100644 --- a/packages/compiler/src/ir/int32-slots.ts +++ b/packages/compiler/src/ir/int32-slots.ts @@ -889,27 +889,44 @@ export function analyzeInt32Slots(mod: IrModule): Int32Slots { else groups.set(root, [key]); } const boxOrigin = new Map(); + // Per function, computed on first use: its capture ids and the locals an + // initialized declaration binds (one body walk each, not one per box). + const captureIds = new Map>(); + const capturedIn = (fnName: string, localId: string): boolean => { + let ids = captureIds.get(fnName); + if (ids === undefined) { + ids = new Set((functions.get(fnName)?.captures ?? []).map((c) => c.localId)); + captureIds.set(fnName, ids); + } + return ids.has(localId); + }; + const initializedDeclarations = new Map>(); + const initializedIn = (fn: IrFunction, localId: string): boolean => { + let ids = initializedDeclarations.get(fn.name); + if (ids === undefined) { + const found = new Set(); + walkFunction(fn, { + stmt: (s) => { + if (s.kind === "varDecl" && s.init) found.add(s.localId); + }, + }); + initializedDeclarations.set(fn.name, (ids = found)); + } + return ids.has(localId); + }; for (const [group, members] of groups) { if (members.some((key) => unstableBoxes.has(key))) continue; // The origin is the one member that is not a capture of its function. const origins = members.filter((key) => { const [fnName, localId] = key.split("\0") as [string, string]; - return !functions.get(fnName)?.captures?.some((c) => c.localId === localId); + return !capturedIn(fnName, localId); }); if (origins.length !== 1) continue; const [fnName, localId] = origins[0]!.split("\0") as [string, string]; const origin = functions.get(fnName)!; if (origin.params.some((p) => p.localId === localId)) { if (!slots.hasParam(fnName, localId)) continue; - } else { - let initialized = false; - walkFunction(origin, { - stmt: (s) => { - if (s.kind === "varDecl" && s.localId === localId && s.init) initialized = true; - }, - }); - if (!initialized) continue; - } + } else if (!initializedIn(origin, localId)) continue; boxOrigin.set(`param:${fnName}\0${localId}`, group); slots.addBox(group, members); // A box starts from its origin parameter's value: it can only be From 8c8ee284138995e99b6c5b81bbab03f856ae71c0 Mon Sep 17 00:00:00 2001 From: Malte Ubl Date: Sat, 17 Oct 2026 19:43:03 -0700 Subject: [PATCH 2/3] Compile bodies with thousands of capture boxes unoptimized Every box pointer of a captured binding is a value live from its binding to its last use. A factory whose methods share thousands of bindings hands LLVM thousands of such ranges, each crossing every call in between, in the factory and in each method that receives them, and the inliner, instruction selection and register allocation grow quadratically: Bun's large-environment AOT program (8,701 captured bindings) spent about three minutes in native code generation. A function holding at least 512 capture boxes (the self-hosted compiler's largest holds under 100) is now emitted `noinline optnone`, and its box slots are loaded and stored volatile so no IR pass promotes them, which the native helper's pipeline would otherwise still do for optnone bodies. Each use reloads its box pointer from the frame. Boxes, environments and semantics are unchanged; only those bodies' machine code is unoptimized. large-environment.ts now compiles in 16 s instead of 227 s (peak RSS 543 MB instead of 777 MB) and runs in the same 0.18 s. Corpus 5973 pins such a factory's behavior (shared mutation, per-iteration closures, a nested closure, a forward-captured read in its TDZ), and a harness test bounds compile-time growth between 500 and 4,000 bindings. Co-Authored-By: Claude Opus 5.5 --- .../compiler/src/backend/llvm/blocks.test.ts | 33 +- packages/compiler/src/backend/llvm/blocks.ts | 43 +- .../compiler/src/backend/llvm/emitter.test.ts | 55 + packages/compiler/src/backend/llvm/emitter.ts | 35 + .../corpus/5973-large-closure-environment.ts | 1859 +++++++++++++++++ .../closure-environment-scaling.test.ts | 128 ++ 6 files changed, 2151 insertions(+), 2 deletions(-) create mode 100644 tests/corpus/5973-large-closure-environment.ts create mode 100644 tests/harness/closure-environment-scaling.test.ts diff --git a/packages/compiler/src/backend/llvm/blocks.test.ts b/packages/compiler/src/backend/llvm/blocks.test.ts index c2ee90b5f8..b96fc851ee 100644 --- a/packages/compiler/src/backend/llvm/blocks.test.ts +++ b/packages/compiler/src/backend/llvm/blocks.test.ts @@ -1,5 +1,5 @@ import { expect, test } from "vitest"; -import { BlockBuilder } from "./blocks.js"; +import { BlockBuilder, volatileSlotAccess } from "./blocks.js"; import { f64Lit } from "./common.js"; test("entry allocations precede instructions and temporaries stay unique", () => { @@ -106,3 +106,34 @@ test("inclusive loops and nested loops keep labels separate", () => { expect(text).toContain("fcmp ole double"); expect(text.match(/alloca double/g)).toHaveLength(2); }); + +test("volatile slots render their loads and stores volatile and nothing else", () => { + const slots = new Set(["%sc_l_a_0"]); + expect(volatileSlotAccess(" %t1 = load ptr, ptr %sc_l_a_0 ; a", slots)).toBe( + " %t1 = load volatile ptr, ptr %sc_l_a_0 ; a", + ); + expect(volatileSlotAccess(" store ptr %t1, ptr %sc_l_a_0, !dbg !7", slots)).toBe( + " store volatile ptr %t1, ptr %sc_l_a_0, !dbg !7", + ); + expect(volatileSlotAccess(" store ptr null, ptr %sc_l_a_0", slots)).toBe( + " store volatile ptr null, ptr %sc_l_a_0", + ); + // Other addresses, a slot stored as a value, prefixes and other opcodes. + for (const line of [ + " %t2 = load ptr, ptr %sc_l_a_01", + " store ptr %sc_l_a_0, ptr %sc_l_b_0", + " %t3 = getelementptr inbounds ptr, ptr %sc_l_a_0, i64 1", + " call void @scr_box_release(ptr %sc_l_a_0)", + " %t4 = load volatile ptr, ptr %sc_l_a_0", + ]) + expect(volatileSlotAccess(line, slots)).toBe(line); + + const blocks = new BlockBuilder(); + blocks.volatileSlots = slots; + blocks.entryAllocas.push("%sc_l_a_0 = alloca ptr"); + blocks.line("store ptr null, ptr %sc_l_a_0"); + blocks.terminate("ret void"); + expect(blocks.render()).toBe( + "entry:\n %sc_l_a_0 = alloca ptr\n store volatile ptr null, ptr %sc_l_a_0\n ret void", + ); +}); diff --git a/packages/compiler/src/backend/llvm/blocks.ts b/packages/compiler/src/backend/llvm/blocks.ts index 5be4ef8801..6d22bf3bf6 100644 --- a/packages/compiler/src/backend/llvm/blocks.ts +++ b/packages/compiler/src/backend/llvm/blocks.ts @@ -31,6 +31,9 @@ export class BlockBuilder { /** Temps known to hold immortal values (temp names are only unique * within one builder). */ readonly immortalTemps = new Set(); + /** Entry slots whose loads and stores render volatile, so no LLVM pass + * promotes them to SSA values (see volatileSlotAccess). */ + volatileSlots: ReadonlySet | null = null; constructor() { this.cur = { label: "entry", lines: [], term: null }; @@ -130,9 +133,14 @@ export class BlockBuilder { } render(): string { + const slots = this.volatileSlots; return this.blocks .map((b, i) => { - const lines = i === 0 ? [...this.entryAllocas.map((l) => ` ${l}`), ...b.lines] : b.lines; + const body = + slots === null || slots.size === 0 + ? b.lines + : b.lines.map((line) => volatileSlotAccess(line, slots)); + const lines = i === 0 ? [...this.entryAllocas.map((l) => ` ${l}`), ...body] : body; // A block left unterminated is a structurally unreachable join // (every predecessor jumped elsewhere); the verifier still wants a // terminator. @@ -142,3 +150,36 @@ export class BlockBuilder { .join("\n"); } } + +/** `line` with its load or store made volatile when the address operand is + * one of `slots`. Lines are this builder's own instruction text, where the + * address is the first `ptr %name` operand after the opcode (a stored value + * precedes it without a comma). */ +export function volatileSlotAccess(line: string, slots: ReadonlySet): string { + let op = 0; + while (line.charCodeAt(op) === 32) op++; + if (line.charCodeAt(op) === 37) { + const assign = line.indexOf(" = ", op); + if (assign < 0) return line; + op = assign + 3; + } + const kind = line.startsWith("load ", op) ? 5 : line.startsWith("store ", op) ? 6 : 0; + if (kind === 0 || line.startsWith("volatile ", op + kind)) return line; + const address = line.indexOf(", ptr %", op + kind); + if (address < 0) return line; + const start = address + 6; + let end = start + 1; + while (end < line.length && isNameCode(line.charCodeAt(end))) end++; + if (!slots.has(line.slice(start, end))) return line; + return `${line.slice(0, op + kind)}volatile ${line.slice(op + kind)}`; +} + +function isNameCode(code: number): boolean { + return ( + (code >= 48 && code <= 57) || + (code >= 65 && code <= 90) || + (code >= 97 && code <= 122) || + code === 95 || + code === 46 + ); +} diff --git a/packages/compiler/src/backend/llvm/emitter.test.ts b/packages/compiler/src/backend/llvm/emitter.test.ts index 4fda0f7e0c..74dc8f8598 100644 --- a/packages/compiler/src/backend/llvm/emitter.test.ts +++ b/packages/compiler/src/backend/llvm/emitter.test.ts @@ -126,6 +126,61 @@ test("the shared indexed-read helpers carry an inline hint", () => { expect(define("plain")).not.toContain("inlinehint"); }); +test("bodies holding thousands of capture boxes compile unoptimized with boxes in memory", () => { + const number = (value: number): IrExpr => ({ kind: "numLit", value, type: F64, loc }); + const read = (localId: string): IrExpr => ({ kind: "varRef", localId, type: F64, loc }); + // A declaring body with `count` boxed bindings, and a closure body that + // receives `count` of them through its environment. + const declaring = (name: string, count: number): IrFunction => { + const ids = Array.from({ length: count }, (_, i) => `${name}${i}`); + return { + name, + params: [], + returnType: F64, + locals: ids.map((id) => ({ id, name: id, type: F64, mutable: true, boxed: true as const })), + body: [ + ...ids.map((id, i): IrStmt => ({ kind: "varDecl", localId: id, init: number(i), loc })), + { kind: "return", value: read(ids[count - 1]!), loc }, + ], + loc, + }; + }; + const receiving = (name: string, count: number): IrFunction => { + const ids = Array.from({ length: count }, (_, i) => `${name}${i}`); + return { + name, + params: [], + returnType: F64, + captures: ids.map((id) => ({ localId: id, name: id, type: F64 })), + locals: ids.map((id) => ({ id, name: id, type: F64, mutable: true, boxed: true as const })), + body: [{ kind: "return", value: read(ids[0]!), loc }], + loc, + }; + }; + const module = moduleFor([]); + const llvm = emitLlvmModule({ + ...module, + functions: [ + ...module.functions, + declaring("wide", 512), + declaring("narrow", 511), + receiving("env", 512), + ], + }); + const body = (name: string): string => { + const start = llvm.split("\n").findIndex((line) => line.endsWith(`{ ; ${name}`)); + const lines = llvm.split("\n").slice(start); + return lines.slice(0, lines.indexOf("}")).join("\n"); + }; + expect(body("wide")).toMatch(/\) noinline optnone #0 \{/); + expect(body("wide")).toMatch(/store volatile ptr %[st]\d+, ptr %sc_l_wide0\b/); + expect(body("wide")).toMatch(/= load volatile ptr, ptr %sc_l_wide511\b/); + expect(body("env")).toMatch(/\) noinline optnone #0 \{/); + expect(body("env")).toMatch(/store volatile ptr %t\d+, ptr %sc_l_env0 ; captured env0/); + expect(body("narrow")).not.toContain("optnone"); + expect(body("narrow")).not.toContain("volatile"); +}); + test("throwing-call count does not multiply identical scope cleanup", () => { for (const boxed of [false, true]) { const small = emitLlvmModule(moduleFor(Array.from({ length: 4 }, call), { boxed })); diff --git a/packages/compiler/src/backend/llvm/emitter.ts b/packages/compiler/src/backend/llvm/emitter.ts index 2c3a714574..5508e61f58 100644 --- a/packages/compiler/src/backend/llvm/emitter.ts +++ b/packages/compiler/src/backend/llvm/emitter.ts @@ -548,10 +548,35 @@ const AWAIT_SUSPENSIONS = /@(?:scr_await_(?:void|f64|bool|str|ref|hop)\b|sc_a[wd * own fiber and promise. */ const COROUTINE_SUSPENDS = /@(?:scr_(?:await_|module_await\b|promise_await_settled\b|test_sub\b|gen_yield_|async_gen_|fiber_(?!self\b|promise\b))|sc_a[wd]_)/; +/** Captured bindings (boxes a body declares or receives) from which a + * function compiles without LLVM optimization (see boxSlotCount). */ +const UNOPTIMIZED_BOX_SLOTS = 512; /** ScrExcKind tags of a generator slot holding a number or a boolean. */ const SCR_EXC_F64 = 1; const SCR_EXC_BOOL = 2; +/** The capture boxes `fn` holds: its boxed bindings and the bindings its + * environment brings in. Each box pointer is a value from its binding (or + * function entry) to its last use, and every one of them crosses the calls + * in between. With thousands of them in one body, as in a factory whose + * returned methods share a large environment, LLVM's inliner, instruction + * selection and register allocation grow quadratically and take minutes. + * Past UNOPTIMIZED_BOX_SLOTS, which ordinary functions stay far below, the + * body is compiled `optnone` (nothing inlines into it and code generation + * takes its fast paths) and its box slots are accessed volatile, so no + * pass promotes them to values even where a pipeline runs IR passes over + * optnone bodies, as the native helper's does. Each use reloads its box + * pointer and code generation stays linear. Semantics, boxes and + * environments are unchanged; only that body's machine code is slower. */ +function boxSlotCount(fn: IrFunction): number { + const captured = new Set(); + for (const capture of fn.captures ?? []) captured.add(capture.localId); + for (const capture of fn.classCaptures ?? []) captured.add(capture.localId); + let count = 0; + for (const local of fn.locals) if (local.boxed === true || captured.has(local.id)) count++; + return count; +} + /** Whether any node of `e` names the local (reads, writes, declarations, * captures). */ function mentionsLocal(e: IrExpr, localId: string): boolean { @@ -6817,6 +6842,15 @@ export class LlEmitter { B.line(`store ptr null, ptr %${mangleLocal(local.id)}`); } } + // A body holding thousands of capture boxes compiles unoptimized, its + // box pointers kept in their slots (see boxSlotCount). + const unoptimized = this.currentCoro === null && boxSlotCount(fn) >= UNOPTIMIZED_BOX_SLOTS; + if (unoptimized) { + const slots = new Set(); + for (const local of this.currentLocals.values()) + if (local.boxed || this.captureIds.has(local.id)) slots.add(`%${mangleLocal(local.id)}`); + B.volatileSlots = slots; + } // Captured bindings come in through the environment — borrowed for the // whole call (the closure owns them): bound here, never released here. (fn.captures ?? []).forEach((c, i) => { @@ -7020,6 +7054,7 @@ export class LlEmitter { if (fn.name.startsWith("%dyn.class.protocol.fallback.")) attrs = `noinline ${attrs}`; else if (fn.name.startsWith("%dyn.class.protocol.")) attrs = `inlinehint ${attrs}`; if (fn.name.startsWith(RESULT_ENTRY_PREFIX)) attrs = `inlinehint ${attrs}`; + if (unoptimized && attrs === FN_ATTRS) attrs = `noinline optnone ${attrs}`; const debug = this.debugScope === null ? "" : ` !dbg ${this.debugScope}`; const symbol = borrowed ? mangleBorrowedFunction(fn.name) : mangleFunction(fn.name); this.privateGlobalSlots.clear(); diff --git a/tests/corpus/5973-large-closure-environment.ts b/tests/corpus/5973-large-closure-environment.ts new file mode 100644 index 0000000000..1ade2570c9 --- /dev/null +++ b/tests/corpus/5973-large-closure-environment.ts @@ -0,0 +1,1859 @@ +// One factory with 600 captured bindings (more than any ordinary function holds), +// shared by methods that read and rebind them, closures created per loop +// iteration, a nested closure, and a function declared before the bindings +// it reads. Every environment observes the same boxes: writes through one +// method are visible through the others and in later calls, and each +// factory call gets its own bindings. +function total(list: number[]): number { + let t = 0; + for (const n of list) t += n; + return t; +} +const one = Number("1"); +function make(seed: number) { + function early(): number { + return n0 + n539 + s541.length; + } + let before = "unset"; + try { + before = String(early()); + } catch (e) { + before = (e as Error).name + ": " + (e as Error).message; + } + let n0 = seed + 0; + let n1 = seed + 1; + let n2 = seed + 2; + let n3 = seed + 3; + let n4 = seed + 4; + let n5 = seed + 5; + let n6 = seed + 6; + let n7 = seed + 7; + let n8 = seed + 8; + let n9 = seed + 9; + let n10 = seed + 10; + let n11 = seed + 11; + let n12 = seed + 12; + let n13 = seed + 13; + let n14 = seed + 14; + let n15 = seed + 15; + let n16 = seed + 16; + let n17 = seed + 17; + let n18 = seed + 18; + let n19 = seed + 19; + let n20 = seed + 20; + let n21 = seed + 21; + let n22 = seed + 22; + let n23 = seed + 23; + let n24 = seed + 24; + let n25 = seed + 25; + let n26 = seed + 26; + let n27 = seed + 27; + let n28 = seed + 28; + let n29 = seed + 29; + let n30 = seed + 30; + let n31 = seed + 31; + let n32 = seed + 32; + let n33 = seed + 33; + let n34 = seed + 34; + let n35 = seed + 35; + let n36 = seed + 36; + let n37 = seed + 37; + let n38 = seed + 38; + let n39 = seed + 39; + let n40 = seed + 40; + let n41 = seed + 41; + let n42 = seed + 42; + let n43 = seed + 43; + let n44 = seed + 44; + let n45 = seed + 45; + let n46 = seed + 46; + let n47 = seed + 47; + let n48 = seed + 48; + let n49 = seed + 49; + let n50 = seed + 50; + let n51 = seed + 51; + let n52 = seed + 52; + let n53 = seed + 53; + let n54 = seed + 54; + let n55 = seed + 55; + let n56 = seed + 56; + let n57 = seed + 57; + let n58 = seed + 58; + let n59 = seed + 59; + let n60 = seed + 60; + let n61 = seed + 61; + let n62 = seed + 62; + let n63 = seed + 63; + let n64 = seed + 64; + let n65 = seed + 65; + let n66 = seed + 66; + let n67 = seed + 67; + let n68 = seed + 68; + let n69 = seed + 69; + let n70 = seed + 70; + let n71 = seed + 71; + let n72 = seed + 72; + let n73 = seed + 73; + let n74 = seed + 74; + let n75 = seed + 75; + let n76 = seed + 76; + let n77 = seed + 77; + let n78 = seed + 78; + let n79 = seed + 79; + let n80 = seed + 80; + let n81 = seed + 81; + let n82 = seed + 82; + let n83 = seed + 83; + let n84 = seed + 84; + let n85 = seed + 85; + let n86 = seed + 86; + let n87 = seed + 87; + let n88 = seed + 88; + let n89 = seed + 89; + let n90 = seed + 90; + let n91 = seed + 91; + let n92 = seed + 92; + let n93 = seed + 93; + let n94 = seed + 94; + let n95 = seed + 95; + let n96 = seed + 96; + let n97 = seed + 97; + let n98 = seed + 98; + let n99 = seed + 99; + let n100 = seed + 100; + let n101 = seed + 101; + let n102 = seed + 102; + let n103 = seed + 103; + let n104 = seed + 104; + let n105 = seed + 105; + let n106 = seed + 106; + let n107 = seed + 107; + let n108 = seed + 108; + let n109 = seed + 109; + let n110 = seed + 110; + let n111 = seed + 111; + let n112 = seed + 112; + let n113 = seed + 113; + let n114 = seed + 114; + let n115 = seed + 115; + let n116 = seed + 116; + let n117 = seed + 117; + let n118 = seed + 118; + let n119 = seed + 119; + let n120 = seed + 120; + let n121 = seed + 121; + let n122 = seed + 122; + let n123 = seed + 123; + let n124 = seed + 124; + let n125 = seed + 125; + let n126 = seed + 126; + let n127 = seed + 127; + let n128 = seed + 128; + let n129 = seed + 129; + let n130 = seed + 130; + let n131 = seed + 131; + let n132 = seed + 132; + let n133 = seed + 133; + let n134 = seed + 134; + let n135 = seed + 135; + let n136 = seed + 136; + let n137 = seed + 137; + let n138 = seed + 138; + let n139 = seed + 139; + let n140 = seed + 140; + let n141 = seed + 141; + let n142 = seed + 142; + let n143 = seed + 143; + let n144 = seed + 144; + let n145 = seed + 145; + let n146 = seed + 146; + let n147 = seed + 147; + let n148 = seed + 148; + let n149 = seed + 149; + let n150 = seed + 150; + let n151 = seed + 151; + let n152 = seed + 152; + let n153 = seed + 153; + let n154 = seed + 154; + let n155 = seed + 155; + let n156 = seed + 156; + let n157 = seed + 157; + let n158 = seed + 158; + let n159 = seed + 159; + let n160 = seed + 160; + let n161 = seed + 161; + let n162 = seed + 162; + let n163 = seed + 163; + let n164 = seed + 164; + let n165 = seed + 165; + let n166 = seed + 166; + let n167 = seed + 167; + let n168 = seed + 168; + let n169 = seed + 169; + let n170 = seed + 170; + let n171 = seed + 171; + let n172 = seed + 172; + let n173 = seed + 173; + let n174 = seed + 174; + let n175 = seed + 175; + let n176 = seed + 176; + let n177 = seed + 177; + let n178 = seed + 178; + let n179 = seed + 179; + let n180 = seed + 180; + let n181 = seed + 181; + let n182 = seed + 182; + let n183 = seed + 183; + let n184 = seed + 184; + let n185 = seed + 185; + let n186 = seed + 186; + let n187 = seed + 187; + let n188 = seed + 188; + let n189 = seed + 189; + let n190 = seed + 190; + let n191 = seed + 191; + let n192 = seed + 192; + let n193 = seed + 193; + let n194 = seed + 194; + let n195 = seed + 195; + let n196 = seed + 196; + let n197 = seed + 197; + let n198 = seed + 198; + let n199 = seed + 199; + let n200 = seed + 200; + let n201 = seed + 201; + let n202 = seed + 202; + let n203 = seed + 203; + let n204 = seed + 204; + let n205 = seed + 205; + let n206 = seed + 206; + let n207 = seed + 207; + let n208 = seed + 208; + let n209 = seed + 209; + let n210 = seed + 210; + let n211 = seed + 211; + let n212 = seed + 212; + let n213 = seed + 213; + let n214 = seed + 214; + let n215 = seed + 215; + let n216 = seed + 216; + let n217 = seed + 217; + let n218 = seed + 218; + let n219 = seed + 219; + let n220 = seed + 220; + let n221 = seed + 221; + let n222 = seed + 222; + let n223 = seed + 223; + let n224 = seed + 224; + let n225 = seed + 225; + let n226 = seed + 226; + let n227 = seed + 227; + let n228 = seed + 228; + let n229 = seed + 229; + let n230 = seed + 230; + let n231 = seed + 231; + let n232 = seed + 232; + let n233 = seed + 233; + let n234 = seed + 234; + let n235 = seed + 235; + let n236 = seed + 236; + let n237 = seed + 237; + let n238 = seed + 238; + let n239 = seed + 239; + let n240 = seed + 240; + let n241 = seed + 241; + let n242 = seed + 242; + let n243 = seed + 243; + let n244 = seed + 244; + let n245 = seed + 245; + let n246 = seed + 246; + let n247 = seed + 247; + let n248 = seed + 248; + let n249 = seed + 249; + let n250 = seed + 250; + let n251 = seed + 251; + let n252 = seed + 252; + let n253 = seed + 253; + let n254 = seed + 254; + let n255 = seed + 255; + let n256 = seed + 256; + let n257 = seed + 257; + let n258 = seed + 258; + let n259 = seed + 259; + let n260 = seed + 260; + let n261 = seed + 261; + let n262 = seed + 262; + let n263 = seed + 263; + let n264 = seed + 264; + let n265 = seed + 265; + let n266 = seed + 266; + let n267 = seed + 267; + let n268 = seed + 268; + let n269 = seed + 269; + let n270 = seed + 270; + let n271 = seed + 271; + let n272 = seed + 272; + let n273 = seed + 273; + let n274 = seed + 274; + let n275 = seed + 275; + let n276 = seed + 276; + let n277 = seed + 277; + let n278 = seed + 278; + let n279 = seed + 279; + let n280 = seed + 280; + let n281 = seed + 281; + let n282 = seed + 282; + let n283 = seed + 283; + let n284 = seed + 284; + let n285 = seed + 285; + let n286 = seed + 286; + let n287 = seed + 287; + let n288 = seed + 288; + let n289 = seed + 289; + let n290 = seed + 290; + let n291 = seed + 291; + let n292 = seed + 292; + let n293 = seed + 293; + let n294 = seed + 294; + let n295 = seed + 295; + let n296 = seed + 296; + let n297 = seed + 297; + let n298 = seed + 298; + let n299 = seed + 299; + let n300 = seed + 300; + let n301 = seed + 301; + let n302 = seed + 302; + let n303 = seed + 303; + let n304 = seed + 304; + let n305 = seed + 305; + let n306 = seed + 306; + let n307 = seed + 307; + let n308 = seed + 308; + let n309 = seed + 309; + let n310 = seed + 310; + let n311 = seed + 311; + let n312 = seed + 312; + let n313 = seed + 313; + let n314 = seed + 314; + let n315 = seed + 315; + let n316 = seed + 316; + let n317 = seed + 317; + let n318 = seed + 318; + let n319 = seed + 319; + let n320 = seed + 320; + let n321 = seed + 321; + let n322 = seed + 322; + let n323 = seed + 323; + let n324 = seed + 324; + let n325 = seed + 325; + let n326 = seed + 326; + let n327 = seed + 327; + let n328 = seed + 328; + let n329 = seed + 329; + let n330 = seed + 330; + let n331 = seed + 331; + let n332 = seed + 332; + let n333 = seed + 333; + let n334 = seed + 334; + let n335 = seed + 335; + let n336 = seed + 336; + let n337 = seed + 337; + let n338 = seed + 338; + let n339 = seed + 339; + let n340 = seed + 340; + let n341 = seed + 341; + let n342 = seed + 342; + let n343 = seed + 343; + let n344 = seed + 344; + let n345 = seed + 345; + let n346 = seed + 346; + let n347 = seed + 347; + let n348 = seed + 348; + let n349 = seed + 349; + let n350 = seed + 350; + let n351 = seed + 351; + let n352 = seed + 352; + let n353 = seed + 353; + let n354 = seed + 354; + let n355 = seed + 355; + let n356 = seed + 356; + let n357 = seed + 357; + let n358 = seed + 358; + let n359 = seed + 359; + let n360 = seed + 360; + let n361 = seed + 361; + let n362 = seed + 362; + let n363 = seed + 363; + let n364 = seed + 364; + let n365 = seed + 365; + let n366 = seed + 366; + let n367 = seed + 367; + let n368 = seed + 368; + let n369 = seed + 369; + let n370 = seed + 370; + let n371 = seed + 371; + let n372 = seed + 372; + let n373 = seed + 373; + let n374 = seed + 374; + let n375 = seed + 375; + let n376 = seed + 376; + let n377 = seed + 377; + let n378 = seed + 378; + let n379 = seed + 379; + let n380 = seed + 380; + let n381 = seed + 381; + let n382 = seed + 382; + let n383 = seed + 383; + let n384 = seed + 384; + let n385 = seed + 385; + let n386 = seed + 386; + let n387 = seed + 387; + let n388 = seed + 388; + let n389 = seed + 389; + let n390 = seed + 390; + let n391 = seed + 391; + let n392 = seed + 392; + let n393 = seed + 393; + let n394 = seed + 394; + let n395 = seed + 395; + let n396 = seed + 396; + let n397 = seed + 397; + let n398 = seed + 398; + let n399 = seed + 399; + let n400 = seed + 400; + let n401 = seed + 401; + let n402 = seed + 402; + let n403 = seed + 403; + let n404 = seed + 404; + let n405 = seed + 405; + let n406 = seed + 406; + let n407 = seed + 407; + let n408 = seed + 408; + let n409 = seed + 409; + let n410 = seed + 410; + let n411 = seed + 411; + let n412 = seed + 412; + let n413 = seed + 413; + let n414 = seed + 414; + let n415 = seed + 415; + let n416 = seed + 416; + let n417 = seed + 417; + let n418 = seed + 418; + let n419 = seed + 419; + let n420 = seed + 420; + let n421 = seed + 421; + let n422 = seed + 422; + let n423 = seed + 423; + let n424 = seed + 424; + let n425 = seed + 425; + let n426 = seed + 426; + let n427 = seed + 427; + let n428 = seed + 428; + let n429 = seed + 429; + let n430 = seed + 430; + let n431 = seed + 431; + let n432 = seed + 432; + let n433 = seed + 433; + let n434 = seed + 434; + let n435 = seed + 435; + let n436 = seed + 436; + let n437 = seed + 437; + let n438 = seed + 438; + let n439 = seed + 439; + let n440 = seed + 440; + let n441 = seed + 441; + let n442 = seed + 442; + let n443 = seed + 443; + let n444 = seed + 444; + let n445 = seed + 445; + let n446 = seed + 446; + let n447 = seed + 447; + let n448 = seed + 448; + let n449 = seed + 449; + let n450 = seed + 450; + let n451 = seed + 451; + let n452 = seed + 452; + let n453 = seed + 453; + let n454 = seed + 454; + let n455 = seed + 455; + let n456 = seed + 456; + let n457 = seed + 457; + let n458 = seed + 458; + let n459 = seed + 459; + let n460 = seed + 460; + let n461 = seed + 461; + let n462 = seed + 462; + let n463 = seed + 463; + let n464 = seed + 464; + let n465 = seed + 465; + let n466 = seed + 466; + let n467 = seed + 467; + let n468 = seed + 468; + let n469 = seed + 469; + let n470 = seed + 470; + let n471 = seed + 471; + let n472 = seed + 472; + let n473 = seed + 473; + let n474 = seed + 474; + let n475 = seed + 475; + let n476 = seed + 476; + let n477 = seed + 477; + let n478 = seed + 478; + let n479 = seed + 479; + let n480 = seed + 480; + let n481 = seed + 481; + let n482 = seed + 482; + let n483 = seed + 483; + let n484 = seed + 484; + let n485 = seed + 485; + let n486 = seed + 486; + let n487 = seed + 487; + let n488 = seed + 488; + let n489 = seed + 489; + let n490 = seed + 490; + let n491 = seed + 491; + let n492 = seed + 492; + let n493 = seed + 493; + let n494 = seed + 494; + let n495 = seed + 495; + let n496 = seed + 496; + let n497 = seed + 497; + let n498 = seed + 498; + let n499 = seed + 499; + let n500 = seed + 500; + let n501 = seed + 501; + let n502 = seed + 502; + let n503 = seed + 503; + let n504 = seed + 504; + let n505 = seed + 505; + let n506 = seed + 506; + let n507 = seed + 507; + let n508 = seed + 508; + let n509 = seed + 509; + let n510 = seed + 510; + let n511 = seed + 511; + let n512 = seed + 512; + let n513 = seed + 513; + let n514 = seed + 514; + let n515 = seed + 515; + let n516 = seed + 516; + let n517 = seed + 517; + let n518 = seed + 518; + let n519 = seed + 519; + let n520 = seed + 520; + let n521 = seed + 521; + let n522 = seed + 522; + let n523 = seed + 523; + let n524 = seed + 524; + let n525 = seed + 525; + let n526 = seed + 526; + let n527 = seed + 527; + let n528 = seed + 528; + let n529 = seed + 529; + let n530 = seed + 530; + let n531 = seed + 531; + let n532 = seed + 532; + let n533 = seed + 533; + let n534 = seed + 534; + let n535 = seed + 535; + let n536 = seed + 536; + let n537 = seed + 537; + let n538 = seed + 538; + let n539 = seed + 539; + let o540 = { x: 540 }; + let s541 = "s" + (541 + one); + let o542 = { x: 542 }; + let s543 = "s" + (543 + one); + let o544 = { x: 544 }; + let s545 = "s" + (545 + one); + let o546 = { x: 546 }; + let s547 = "s" + (547 + one); + let o548 = { x: 548 }; + let s549 = "s" + (549 + one); + let o550 = { x: 550 }; + let s551 = "s" + (551 + one); + let o552 = { x: 552 }; + let s553 = "s" + (553 + one); + let o554 = { x: 554 }; + let s555 = "s" + (555 + one); + let o556 = { x: 556 }; + let s557 = "s" + (557 + one); + let o558 = { x: 558 }; + let s559 = "s" + (559 + one); + let o560 = { x: 560 }; + let s561 = "s" + (561 + one); + let o562 = { x: 562 }; + let s563 = "s" + (563 + one); + let o564 = { x: 564 }; + let s565 = "s" + (565 + one); + let o566 = { x: 566 }; + let s567 = "s" + (567 + one); + let o568 = { x: 568 }; + let s569 = "s" + (569 + one); + let o570 = { x: 570 }; + let s571 = "s" + (571 + one); + let o572 = { x: 572 }; + let s573 = "s" + (573 + one); + let o574 = { x: 574 }; + let s575 = "s" + (575 + one); + let o576 = { x: 576 }; + let s577 = "s" + (577 + one); + let o578 = { x: 578 }; + let s579 = "s" + (579 + one); + let o580 = { x: 580 }; + let s581 = "s" + (581 + one); + let o582 = { x: 582 }; + let s583 = "s" + (583 + one); + let o584 = { x: 584 }; + let s585 = "s" + (585 + one); + let o586 = { x: 586 }; + let s587 = "s" + (587 + one); + let o588 = { x: 588 }; + let s589 = "s" + (589 + one); + let o590 = { x: 590 }; + let s591 = "s" + (591 + one); + let o592 = { x: 592 }; + let s593 = "s" + (593 + one); + let o594 = { x: 594 }; + let s595 = "s" + (595 + one); + let o596 = { x: 596 }; + let s597 = "s" + (597 + one); + let o598 = { x: 598 }; + let s599 = "s" + (599 + one); + const readers: (() => string)[] = []; + for (let i = 0; i < 3; i++) { + const label = "r" + i; + readers.push(() => label + ":" + (n539 + i) + ":" + o540.x + ":" + s541); + } + return { + before, + early, + sum(): number { + return total([ + n0, + n1, + n2, + n3, + n4, + n5, + n6, + n7, + n8, + n9, + n10, + n11, + n12, + n13, + n14, + n15, + n16, + n17, + n18, + n19, + n20, + n21, + n22, + n23, + n24, + n25, + n26, + n27, + n28, + n29, + n30, + n31, + n32, + n33, + n34, + n35, + n36, + n37, + n38, + n39, + n40, + n41, + n42, + n43, + n44, + n45, + n46, + n47, + n48, + n49, + n50, + n51, + n52, + n53, + n54, + n55, + n56, + n57, + n58, + n59, + n60, + n61, + n62, + n63, + n64, + n65, + n66, + n67, + n68, + n69, + n70, + n71, + n72, + n73, + n74, + n75, + n76, + n77, + n78, + n79, + n80, + n81, + n82, + n83, + n84, + n85, + n86, + n87, + n88, + n89, + n90, + n91, + n92, + n93, + n94, + n95, + n96, + n97, + n98, + n99, + n100, + n101, + n102, + n103, + n104, + n105, + n106, + n107, + n108, + n109, + n110, + n111, + n112, + n113, + n114, + n115, + n116, + n117, + n118, + n119, + n120, + n121, + n122, + n123, + n124, + n125, + n126, + n127, + n128, + n129, + n130, + n131, + n132, + n133, + n134, + n135, + n136, + n137, + n138, + n139, + n140, + n141, + n142, + n143, + n144, + n145, + n146, + n147, + n148, + n149, + n150, + n151, + n152, + n153, + n154, + n155, + n156, + n157, + n158, + n159, + n160, + n161, + n162, + n163, + n164, + n165, + n166, + n167, + n168, + n169, + n170, + n171, + n172, + n173, + n174, + n175, + n176, + n177, + n178, + n179, + n180, + n181, + n182, + n183, + n184, + n185, + n186, + n187, + n188, + n189, + n190, + n191, + n192, + n193, + n194, + n195, + n196, + n197, + n198, + n199, + n200, + n201, + n202, + n203, + n204, + n205, + n206, + n207, + n208, + n209, + n210, + n211, + n212, + n213, + n214, + n215, + n216, + n217, + n218, + n219, + n220, + n221, + n222, + n223, + n224, + n225, + n226, + n227, + n228, + n229, + n230, + n231, + n232, + n233, + n234, + n235, + n236, + n237, + n238, + n239, + n240, + n241, + n242, + n243, + n244, + n245, + n246, + n247, + n248, + n249, + n250, + n251, + n252, + n253, + n254, + n255, + n256, + n257, + n258, + n259, + n260, + n261, + n262, + n263, + n264, + n265, + n266, + n267, + n268, + n269, + n270, + n271, + n272, + n273, + n274, + n275, + n276, + n277, + n278, + n279, + n280, + n281, + n282, + n283, + n284, + n285, + n286, + n287, + n288, + n289, + n290, + n291, + n292, + n293, + n294, + n295, + n296, + n297, + n298, + n299, + n300, + n301, + n302, + n303, + n304, + n305, + n306, + n307, + n308, + n309, + n310, + n311, + n312, + n313, + n314, + n315, + n316, + n317, + n318, + n319, + n320, + n321, + n322, + n323, + n324, + n325, + n326, + n327, + n328, + n329, + n330, + n331, + n332, + n333, + n334, + n335, + n336, + n337, + n338, + n339, + n340, + n341, + n342, + n343, + n344, + n345, + n346, + n347, + n348, + n349, + n350, + n351, + n352, + n353, + n354, + n355, + n356, + n357, + n358, + n359, + n360, + n361, + n362, + n363, + n364, + n365, + n366, + n367, + n368, + n369, + n370, + n371, + n372, + n373, + n374, + n375, + n376, + n377, + n378, + n379, + n380, + n381, + n382, + n383, + n384, + n385, + n386, + n387, + n388, + n389, + n390, + n391, + n392, + n393, + n394, + n395, + n396, + n397, + n398, + n399, + n400, + n401, + n402, + n403, + n404, + n405, + n406, + n407, + n408, + n409, + n410, + n411, + n412, + n413, + n414, + n415, + n416, + n417, + n418, + n419, + n420, + n421, + n422, + n423, + n424, + n425, + n426, + n427, + n428, + n429, + n430, + n431, + n432, + n433, + n434, + n435, + n436, + n437, + n438, + n439, + n440, + n441, + n442, + n443, + n444, + n445, + n446, + n447, + n448, + n449, + n450, + n451, + n452, + n453, + n454, + n455, + n456, + n457, + n458, + n459, + n460, + n461, + n462, + n463, + n464, + n465, + n466, + n467, + n468, + n469, + n470, + n471, + n472, + n473, + n474, + n475, + n476, + n477, + n478, + n479, + n480, + n481, + n482, + n483, + n484, + n485, + n486, + n487, + n488, + n489, + n490, + n491, + n492, + n493, + n494, + n495, + n496, + n497, + n498, + n499, + n500, + n501, + n502, + n503, + n504, + n505, + n506, + n507, + n508, + n509, + n510, + n511, + n512, + n513, + n514, + n515, + n516, + n517, + n518, + n519, + n520, + n521, + n522, + n523, + n524, + n525, + n526, + n527, + n528, + n529, + n530, + n531, + n532, + n533, + n534, + n535, + n536, + n537, + n538, + n539, + o540.x, + s541.length, + o542.x, + s543.length, + o544.x, + s545.length, + o546.x, + s547.length, + o548.x, + s549.length, + o550.x, + s551.length, + o552.x, + s553.length, + o554.x, + s555.length, + o556.x, + s557.length, + o558.x, + s559.length, + o560.x, + s561.length, + o562.x, + s563.length, + o564.x, + s565.length, + o566.x, + s567.length, + o568.x, + s569.length, + o570.x, + s571.length, + o572.x, + s573.length, + o574.x, + s575.length, + o576.x, + s577.length, + o578.x, + s579.length, + o580.x, + s581.length, + o582.x, + s583.length, + o584.x, + s585.length, + o586.x, + s587.length, + o588.x, + s589.length, + o590.x, + s591.length, + o592.x, + s593.length, + o594.x, + s595.length, + o596.x, + s597.length, + o598.x, + s599.length, + ]); + }, + bump(): void { + n0 += 1; + n1 += 1; + n2 += 1; + n3 += 1; + n4 += 1; + n5 += 1; + n6 += 1; + n7 += 1; + n8 += 1; + n9 += 1; + n10 += 1; + n11 += 1; + n12 += 1; + n13 += 1; + n14 += 1; + n15 += 1; + n16 += 1; + n17 += 1; + n18 += 1; + n19 += 1; + n20 += 1; + n21 += 1; + n22 += 1; + n23 += 1; + n24 += 1; + n25 += 1; + n26 += 1; + n27 += 1; + n28 += 1; + n29 += 1; + n30 += 1; + n31 += 1; + n32 += 1; + n33 += 1; + n34 += 1; + n35 += 1; + n36 += 1; + n37 += 1; + n38 += 1; + n39 += 1; + n40 += 1; + n41 += 1; + n42 += 1; + n43 += 1; + n44 += 1; + n45 += 1; + n46 += 1; + n47 += 1; + n48 += 1; + n49 += 1; + n50 += 1; + n51 += 1; + n52 += 1; + n53 += 1; + n54 += 1; + n55 += 1; + n56 += 1; + n57 += 1; + n58 += 1; + n59 += 1; + n60 += 1; + n61 += 1; + n62 += 1; + n63 += 1; + n64 += 1; + n65 += 1; + n66 += 1; + n67 += 1; + n68 += 1; + n69 += 1; + n70 += 1; + n71 += 1; + n72 += 1; + n73 += 1; + n74 += 1; + n75 += 1; + n76 += 1; + n77 += 1; + n78 += 1; + n79 += 1; + n80 += 1; + n81 += 1; + n82 += 1; + n83 += 1; + n84 += 1; + n85 += 1; + n86 += 1; + n87 += 1; + n88 += 1; + n89 += 1; + n90 += 1; + n91 += 1; + n92 += 1; + n93 += 1; + n94 += 1; + n95 += 1; + n96 += 1; + n97 += 1; + n98 += 1; + n99 += 1; + n100 += 1; + n101 += 1; + n102 += 1; + n103 += 1; + n104 += 1; + n105 += 1; + n106 += 1; + n107 += 1; + n108 += 1; + n109 += 1; + n110 += 1; + n111 += 1; + n112 += 1; + n113 += 1; + n114 += 1; + n115 += 1; + n116 += 1; + n117 += 1; + n118 += 1; + n119 += 1; + n120 += 1; + n121 += 1; + n122 += 1; + n123 += 1; + n124 += 1; + n125 += 1; + n126 += 1; + n127 += 1; + n128 += 1; + n129 += 1; + n130 += 1; + n131 += 1; + n132 += 1; + n133 += 1; + n134 += 1; + n135 += 1; + n136 += 1; + n137 += 1; + n138 += 1; + n139 += 1; + n140 += 1; + n141 += 1; + n142 += 1; + n143 += 1; + n144 += 1; + n145 += 1; + n146 += 1; + n147 += 1; + n148 += 1; + n149 += 1; + n150 += 1; + n151 += 1; + n152 += 1; + n153 += 1; + n154 += 1; + n155 += 1; + n156 += 1; + n157 += 1; + n158 += 1; + n159 += 1; + n160 += 1; + n161 += 1; + n162 += 1; + n163 += 1; + n164 += 1; + n165 += 1; + n166 += 1; + n167 += 1; + n168 += 1; + n169 += 1; + n170 += 1; + n171 += 1; + n172 += 1; + n173 += 1; + n174 += 1; + n175 += 1; + n176 += 1; + n177 += 1; + n178 += 1; + n179 += 1; + n180 += 1; + n181 += 1; + n182 += 1; + n183 += 1; + n184 += 1; + n185 += 1; + n186 += 1; + n187 += 1; + n188 += 1; + n189 += 1; + n190 += 1; + n191 += 1; + n192 += 1; + n193 += 1; + n194 += 1; + n195 += 1; + n196 += 1; + n197 += 1; + n198 += 1; + n199 += 1; + n200 += 1; + n201 += 1; + n202 += 1; + n203 += 1; + n204 += 1; + n205 += 1; + n206 += 1; + n207 += 1; + n208 += 1; + n209 += 1; + n210 += 1; + n211 += 1; + n212 += 1; + n213 += 1; + n214 += 1; + n215 += 1; + n216 += 1; + n217 += 1; + n218 += 1; + n219 += 1; + n220 += 1; + n221 += 1; + n222 += 1; + n223 += 1; + n224 += 1; + n225 += 1; + n226 += 1; + n227 += 1; + n228 += 1; + n229 += 1; + n230 += 1; + n231 += 1; + n232 += 1; + n233 += 1; + n234 += 1; + n235 += 1; + n236 += 1; + n237 += 1; + n238 += 1; + n239 += 1; + n240 += 1; + n241 += 1; + n242 += 1; + n243 += 1; + n244 += 1; + n245 += 1; + n246 += 1; + n247 += 1; + n248 += 1; + n249 += 1; + n250 += 1; + n251 += 1; + n252 += 1; + n253 += 1; + n254 += 1; + n255 += 1; + n256 += 1; + n257 += 1; + n258 += 1; + n259 += 1; + n260 += 1; + n261 += 1; + n262 += 1; + n263 += 1; + n264 += 1; + n265 += 1; + n266 += 1; + n267 += 1; + n268 += 1; + n269 += 1; + n270 += 1; + n271 += 1; + n272 += 1; + n273 += 1; + n274 += 1; + n275 += 1; + n276 += 1; + n277 += 1; + n278 += 1; + n279 += 1; + n280 += 1; + n281 += 1; + n282 += 1; + n283 += 1; + n284 += 1; + n285 += 1; + n286 += 1; + n287 += 1; + n288 += 1; + n289 += 1; + n290 += 1; + n291 += 1; + n292 += 1; + n293 += 1; + n294 += 1; + n295 += 1; + n296 += 1; + n297 += 1; + n298 += 1; + n299 += 1; + n300 += 1; + n301 += 1; + n302 += 1; + n303 += 1; + n304 += 1; + n305 += 1; + n306 += 1; + n307 += 1; + n308 += 1; + n309 += 1; + n310 += 1; + n311 += 1; + n312 += 1; + n313 += 1; + n314 += 1; + n315 += 1; + n316 += 1; + n317 += 1; + n318 += 1; + n319 += 1; + n320 += 1; + n321 += 1; + n322 += 1; + n323 += 1; + n324 += 1; + n325 += 1; + n326 += 1; + n327 += 1; + n328 += 1; + n329 += 1; + n330 += 1; + n331 += 1; + n332 += 1; + n333 += 1; + n334 += 1; + n335 += 1; + n336 += 1; + n337 += 1; + n338 += 1; + n339 += 1; + n340 += 1; + n341 += 1; + n342 += 1; + n343 += 1; + n344 += 1; + n345 += 1; + n346 += 1; + n347 += 1; + n348 += 1; + n349 += 1; + n350 += 1; + n351 += 1; + n352 += 1; + n353 += 1; + n354 += 1; + n355 += 1; + n356 += 1; + n357 += 1; + n358 += 1; + n359 += 1; + n360 += 1; + n361 += 1; + n362 += 1; + n363 += 1; + n364 += 1; + n365 += 1; + n366 += 1; + n367 += 1; + n368 += 1; + n369 += 1; + n370 += 1; + n371 += 1; + n372 += 1; + n373 += 1; + n374 += 1; + n375 += 1; + n376 += 1; + n377 += 1; + n378 += 1; + n379 += 1; + n380 += 1; + n381 += 1; + n382 += 1; + n383 += 1; + n384 += 1; + n385 += 1; + n386 += 1; + n387 += 1; + n388 += 1; + n389 += 1; + n390 += 1; + n391 += 1; + n392 += 1; + n393 += 1; + n394 += 1; + n395 += 1; + n396 += 1; + n397 += 1; + n398 += 1; + n399 += 1; + n400 += 1; + n401 += 1; + n402 += 1; + n403 += 1; + n404 += 1; + n405 += 1; + n406 += 1; + n407 += 1; + n408 += 1; + n409 += 1; + n410 += 1; + n411 += 1; + n412 += 1; + n413 += 1; + n414 += 1; + n415 += 1; + n416 += 1; + n417 += 1; + n418 += 1; + n419 += 1; + n420 += 1; + n421 += 1; + n422 += 1; + n423 += 1; + n424 += 1; + n425 += 1; + n426 += 1; + n427 += 1; + n428 += 1; + n429 += 1; + n430 += 1; + n431 += 1; + n432 += 1; + n433 += 1; + n434 += 1; + n435 += 1; + n436 += 1; + n437 += 1; + n438 += 1; + n439 += 1; + n440 += 1; + n441 += 1; + n442 += 1; + n443 += 1; + n444 += 1; + n445 += 1; + n446 += 1; + n447 += 1; + n448 += 1; + n449 += 1; + n450 += 1; + n451 += 1; + n452 += 1; + n453 += 1; + n454 += 1; + n455 += 1; + n456 += 1; + n457 += 1; + n458 += 1; + n459 += 1; + n460 += 1; + n461 += 1; + n462 += 1; + n463 += 1; + n464 += 1; + n465 += 1; + n466 += 1; + n467 += 1; + n468 += 1; + n469 += 1; + n470 += 1; + n471 += 1; + n472 += 1; + n473 += 1; + n474 += 1; + n475 += 1; + n476 += 1; + n477 += 1; + n478 += 1; + n479 += 1; + n480 += 1; + n481 += 1; + n482 += 1; + n483 += 1; + n484 += 1; + n485 += 1; + n486 += 1; + n487 += 1; + n488 += 1; + n489 += 1; + n490 += 1; + n491 += 1; + n492 += 1; + n493 += 1; + n494 += 1; + n495 += 1; + n496 += 1; + n497 += 1; + n498 += 1; + n499 += 1; + n500 += 1; + n501 += 1; + n502 += 1; + n503 += 1; + n504 += 1; + n505 += 1; + n506 += 1; + n507 += 1; + n508 += 1; + n509 += 1; + n510 += 1; + n511 += 1; + n512 += 1; + n513 += 1; + n514 += 1; + n515 += 1; + n516 += 1; + n517 += 1; + n518 += 1; + n519 += 1; + n520 += 1; + n521 += 1; + n522 += 1; + n523 += 1; + n524 += 1; + n525 += 1; + n526 += 1; + n527 += 1; + n528 += 1; + n529 += 1; + n530 += 1; + n531 += 1; + n532 += 1; + n533 += 1; + n534 += 1; + n535 += 1; + n536 += 1; + n537 += 1; + n538 += 1; + n539 += 1; + o540 = { x: o540.x + 1 }; + s541 += "!"; + o542 = { x: o542.x + 1 }; + s543 += "!"; + o544 = { x: o544.x + 1 }; + s545 += "!"; + o546 = { x: o546.x + 1 }; + s547 += "!"; + o548 = { x: o548.x + 1 }; + s549 += "!"; + o550 = { x: o550.x + 1 }; + s551 += "!"; + o552 = { x: o552.x + 1 }; + s553 += "!"; + o554 = { x: o554.x + 1 }; + s555 += "!"; + o556 = { x: o556.x + 1 }; + s557 += "!"; + o558 = { x: o558.x + 1 }; + s559 += "!"; + o560 = { x: o560.x + 1 }; + s561 += "!"; + o562 = { x: o562.x + 1 }; + s563 += "!"; + o564 = { x: o564.x + 1 }; + s565 += "!"; + o566 = { x: o566.x + 1 }; + s567 += "!"; + o568 = { x: o568.x + 1 }; + s569 += "!"; + o570 = { x: o570.x + 1 }; + s571 += "!"; + o572 = { x: o572.x + 1 }; + s573 += "!"; + o574 = { x: o574.x + 1 }; + s575 += "!"; + o576 = { x: o576.x + 1 }; + s577 += "!"; + o578 = { x: o578.x + 1 }; + s579 += "!"; + o580 = { x: o580.x + 1 }; + s581 += "!"; + o582 = { x: o582.x + 1 }; + s583 += "!"; + o584 = { x: o584.x + 1 }; + s585 += "!"; + o586 = { x: o586.x + 1 }; + s587 += "!"; + o588 = { x: o588.x + 1 }; + s589 += "!"; + o590 = { x: o590.x + 1 }; + s591 += "!"; + o592 = { x: o592.x + 1 }; + s593 += "!"; + o594 = { x: o594.x + 1 }; + s595 += "!"; + o596 = { x: o596.x + 1 }; + s597 += "!"; + o598 = { x: o598.x + 1 }; + s599 += "!"; + }, + readers, + nested(): () => string { + let calls = 0; + return () => { + calls++; + n0 += 10; + return calls + "/" + n0 + "/" + s599; + }; + }, + }; +} +const a = make(0); +const b = make(100); +console.log(a.before); +console.log(a.sum(), b.sum(), a.early()); +a.bump(); +a.bump(); +console.log(a.sum(), b.sum(), a.early()); +console.log(a.readers.map((r) => r()).join(" ")); +const next = a.nested(); +console.log(next(), next(), a.sum()); +b.bump(); +console.log(b.readers.map((r) => r()).join(" "), b.early()); diff --git a/tests/harness/closure-environment-scaling.test.ts b/tests/harness/closure-environment-scaling.test.ts new file mode 100644 index 0000000000..c6f3a09fe8 --- /dev/null +++ b/tests/harness/closure-environment-scaling.test.ts @@ -0,0 +1,128 @@ +/* Compile time stays linear in the size of a closure environment. A factory + * whose methods share thousands of captured bindings (the shape of Bun's + * large-environment AOT program) used to take minutes: capture analyses + * walked the whole body once per binding, and LLVM received every box + * pointer as a value live across the entire function. Behavior of such a + * program is pinned by corpus 5973; this test bounds how compile time grows + * between two generated sizes. */ +import { execFile } from "node:child_process"; +import { mkdtempSync, rmSync, writeFileSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { promisify } from "node:util"; +import { expect, test } from "vitest"; + +const execFileAsync = promisify(execFile); +const repoRoot = join(import.meta.dirname, "../.."); +const sanitize = process.env["SCRIPTC_SAN"] === "1"; + +/** `n` number bindings plus n/20 objects and strings, all captured by a + * reader and a writer method, and the output the program prints. */ +function program(n: number): { source: string; expected: string } { + const lines = [ + `function total(list: number[]): number {`, + ` let t = 0;`, + ` for (const v of list) t += v;`, + ` return t;`, + `}`, + `const one = Number("1");`, + `function make() {`, + ]; + const reads: string[] = []; + const writes: string[] = []; + let sum = 0; + let bumped = 0; + for (let i = 0; i < n; i++) { + lines.push(` let n${i} = ${i} + 0.5;`); + reads.push(`n${i}`); + writes.push(`n${i} += 1;`); + sum += i + 0.5; + bumped += i + 1.5; + } + for (let i = n; i < n + n / 10; i++) { + if (i % 2 === 0) { + lines.push(` let o${i} = { x: ${i} };`); + reads.push(`o${i}.x`); + writes.push(`o${i} = { x: o${i}.x + 1 };`); + sum += i; + bumped += i + 1; + } else { + lines.push(` let s${i} = "s" + (${i} + one);`); + reads.push(`s${i}.length`); + writes.push(`s${i} += "!";`); + const length = `s${i + 1}`.length; + sum += length; + bumped += length + 1; + } + } + lines.push( + ` return {`, + ` sum(): number {`, + ` return total([${reads.join(", ")}]);`, + ` },`, + ` bump(): void {`, + ...writes.map((write) => ` ${write}`), + ` },`, + ` };`, + `}`, + `const a = make();`, + `const b = make();`, + `a.bump();`, + `console.log(a.sum(), b.sum());`, + ); + return { source: lines.join("\n") + "\n", expected: `${bumped} ${sum}\n` }; +} + +/** Milliseconds from the end of the frontend load to the finished + * executable (SCRIPTC_TIMING phases): lowering, IR passes, LLVM emission, + * native code generation and the link, without process and tsgo startup. */ +async function compileTime(dir: string, n: number): Promise { + const { source, expected } = program(n); + const entry = join(dir, `env${n}.ts`); + const binary = join(dir, `env${n}`); + writeFileSync(entry, source); + const { stderr } = await execFileAsync( + process.execPath, + [ + join(repoRoot, "packages/cli/dist/main.js"), + "build", + entry, + "-o", + binary, + ...(sanitize ? ["--sanitize"] : []), + ], + { + env: { ...process.env, SCRIPTC_TIMING: "1", SCRIPTC_NO_CACHE: "1" }, + maxBuffer: 16 * 1024 * 1024, + }, + ); + const phases = new Map(); + for (const line of stderr.split("\n")) { + if (!line.startsWith("scriptc timing ")) continue; + const { phase, total_ms } = JSON.parse(line.slice("scriptc timing ".length)) as { + phase: string; + total_ms: number; + }; + phases.set(phase, total_ms); + } + const loaded = phases.get("frontend-load"); + const complete = phases.get("complete"); + if (loaded === undefined || complete === undefined) throw new Error(stderr); + const run = await execFileAsync(binary, [], { encoding: "utf8" }); + expect(run.stdout).toBe(expected); + return complete - loaded; +} + +test("compile time grows linearly with a shared closure environment", async () => { + const dir = mkdtempSync(join(tmpdir(), "scriptc-closure-scaling-")); + try { + const small = await compileTime(dir, 500); + const large = await compileTime(dir, 4000); + // Eight times the bindings. Linear growth stays at or below 8x (fixed + // costs keep it near 5x); the quadratic analyses and code generation + // measured 24x at these sizes, and every doubling beyond adds 4x. + expect(large / small, `${small.toFixed(0)} ms -> ${large.toFixed(0)} ms`).toBeLessThan(16); + } finally { + rmSync(dir, { recursive: true, force: true }); + } +}, 300_000); From 9118cbc2587d2cc568bb1ee27ab65a4ff8d2f338 Mon Sep 17 00:00:00 2001 From: Malte Ubl Date: Sat, 17 Oct 2026 20:34:49 -0700 Subject: [PATCH 3/3] Record corpus 5973's preflight baseline and skip the scaling test under ASan The order-parity baseline for the new corpus program records its module order and empty preflight diagnostics. Uncached sanitized builds rebuild the runtime for every compile, which takes longer than either measured size, so the compile-time growth test runs in the plain lane only. Co-Authored-By: Claude Opus 5.5 --- .../5973-large-closure-environment.ts.json | 6 +++ .../closure-environment-scaling.test.ts | 41 +++++++++---------- 2 files changed, 26 insertions(+), 21 deletions(-) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5973-large-closure-environment.ts.json diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5973-large-closure-environment.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5973-large-closure-environment.ts.json new file mode 100644 index 0000000000..5455362850 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5973-large-closure-environment.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/5973-large-closure-environment.ts" + ], + "diags": [] +} diff --git a/tests/harness/closure-environment-scaling.test.ts b/tests/harness/closure-environment-scaling.test.ts index c6f3a09fe8..785f073492 100644 --- a/tests/harness/closure-environment-scaling.test.ts +++ b/tests/harness/closure-environment-scaling.test.ts @@ -14,6 +14,8 @@ import { expect, test } from "vitest"; const execFileAsync = promisify(execFile); const repoRoot = join(import.meta.dirname, "../.."); +// Sanitized builds compile the runtime with each uncached build, which +// dwarfs both sizes; the growth this test bounds is the same in every lane. const sanitize = process.env["SCRIPTC_SAN"] === "1"; /** `n` number bindings plus n/20 objects and strings, all captured by a @@ -83,14 +85,7 @@ async function compileTime(dir: string, n: number): Promise { writeFileSync(entry, source); const { stderr } = await execFileAsync( process.execPath, - [ - join(repoRoot, "packages/cli/dist/main.js"), - "build", - entry, - "-o", - binary, - ...(sanitize ? ["--sanitize"] : []), - ], + [join(repoRoot, "packages/cli/dist/main.js"), "build", entry, "-o", binary], { env: { ...process.env, SCRIPTC_TIMING: "1", SCRIPTC_NO_CACHE: "1" }, maxBuffer: 16 * 1024 * 1024, @@ -113,16 +108,20 @@ async function compileTime(dir: string, n: number): Promise { return complete - loaded; } -test("compile time grows linearly with a shared closure environment", async () => { - const dir = mkdtempSync(join(tmpdir(), "scriptc-closure-scaling-")); - try { - const small = await compileTime(dir, 500); - const large = await compileTime(dir, 4000); - // Eight times the bindings. Linear growth stays at or below 8x (fixed - // costs keep it near 5x); the quadratic analyses and code generation - // measured 24x at these sizes, and every doubling beyond adds 4x. - expect(large / small, `${small.toFixed(0)} ms -> ${large.toFixed(0)} ms`).toBeLessThan(16); - } finally { - rmSync(dir, { recursive: true, force: true }); - } -}, 300_000); +test.skipIf(sanitize)( + "compile time grows linearly with a shared closure environment", + async () => { + const dir = mkdtempSync(join(tmpdir(), "scriptc-closure-scaling-")); + try { + const small = await compileTime(dir, 500); + const large = await compileTime(dir, 4000); + // Eight times the bindings. Linear growth stays at or below 8x (fixed + // costs keep it near 5x); the quadratic analyses and code generation + // measured 24x at these sizes, and every doubling beyond adds 4x. + expect(large / small, `${small.toFixed(0)} ms -> ${large.toFixed(0)} ms`).toBeLessThan(16); + } finally { + rmSync(dir, { recursive: true, force: true }); + } + }, + 300_000, +);