From 6ff0b7c2dfb1642002f773c8efbbb7a9847446ef Mon Sep 17 00:00:00 2001 From: Malte Ubl Date: Fri, 9 Oct 2026 05:22:54 -0700 Subject: [PATCH] Load the active exception cell once per synchronous body Every inline pending-exception check in an executable loaded the runtime's active-cell pointer and then its kind: two dependent loads after each call that may throw, because LLVM must assume any call can change the pointer. Fiber switches restore the active cell before control returns to a synchronous frame (stack switches, eager spawns and generator resumes all swap back to the caller's cell), so the pointer is one value for the whole invocation. Synchronous IR function bodies now load it once in the entry block and each check reads only the kind. The flag lives on the body's BlockBuilder, so adapters and runtime helpers keep the per-check load. Async functions, generators and worker executables (whose checks also fold in the context stop signal) keep the existing form; libraries still test through scr_exc_pending. --- packages/compiler/src/backend/llvm/blocks.ts | 6 ++++++ .../compiler/src/backend/llvm/emitter.test.ts | 19 +++++++++++++++++++ packages/compiler/src/backend/llvm/emitter.ts | 19 +++++++++++++++++-- .../backend/llvm/local-array-reads.test.ts | 3 ++- .../src/backend/llvm/string-lifetimes.test.ts | 3 ++- .../test/string-and-array-lifetimes.test.ts | 3 ++- 6 files changed, 48 insertions(+), 5 deletions(-) diff --git a/packages/compiler/src/backend/llvm/blocks.ts b/packages/compiler/src/backend/llvm/blocks.ts index 01295c549..49c9be2fe 100644 --- a/packages/compiler/src/backend/llvm/blocks.ts +++ b/packages/compiler/src/backend/llvm/blocks.ts @@ -20,6 +20,12 @@ export class BlockBuilder { private tempCounter = 0; private labelCounter = 0; debugLocation: string | null = null; + /** The body is a synchronous IR function: the active exception cell is one + * value from entry to every pending check. Other builders (adapters, + * runtime helpers) keep loading it at each check. */ + excCellInvariant = false; + /** The entry-block load of the active exception cell, once requested. */ + excCell: string | null = null; /** Function epilogue shared by normal and exceptional returns. */ returnEpilogue: string | null = null; /** Temps known to hold immortal values (temp names are only unique diff --git a/packages/compiler/src/backend/llvm/emitter.test.ts b/packages/compiler/src/backend/llvm/emitter.test.ts index cd364f414..614d88fac 100644 --- a/packages/compiler/src/backend/llvm/emitter.test.ts +++ b/packages/compiler/src/backend/llvm/emitter.test.ts @@ -532,3 +532,22 @@ test("literal zero division emits the JavaScript NaN constant in development bui ); expect(dynamic).toContain("fdiv double"); }); + +test("synchronous bodies load the active exception cell once for all pending checks", () => { + const body = (llvm: string, name: string): string => { + const start = llvm.indexOf(`@${name}(`); + return llvm.slice(start, llvm.indexOf("\n}\n", start)); + }; + const module = moduleFor([call(), call(), call()]); + const work = body(emitLlvmModule(module), "sc_f_work"); + expect(work.match(/= load ptr, ptr @scr_exc_active/g)).toHaveLength(1); + expect(work.slice(work.indexOf("entry:")).split("\n").slice(1).join("\n")).toMatch( + /^(?: %[^\n]* = alloca [^\n]*\n)* %exc\.cell = load ptr, ptr @scr_exc_active/, + ); + expect(work.match(/= load i32, ptr %exc\.cell\b/g)).toHaveLength(3); + // Worker executables fold the context signal into each check and keep + // loading their thread-local cell there. + const workers = body(emitLlvmModule({ ...module, workers: true }), "sc_f_work"); + expect(workers).not.toContain("%exc.cell"); + expect(workers.match(/= load ptr, ptr @scr_exc_active/g)).toHaveLength(3); +}); diff --git a/packages/compiler/src/backend/llvm/emitter.ts b/packages/compiler/src/backend/llvm/emitter.ts index 8565a38bb..224df49b0 100644 --- a/packages/compiler/src/backend/llvm/emitter.ts +++ b/packages/compiler/src/backend/llvm/emitter.ts @@ -3800,9 +3800,21 @@ export class LlEmitter { const tl = this.mod.workers === true ? "thread_local " : ""; this.declare(`@scr_exc_active = external ${tl}global ptr`); this.declare(`declare i1 @llvm.expect.i1(i1, i1)`); - const cell = B.tmp(); + let cell: string; + if (B.excCellInvariant && this.mod.workers !== true) { + // Fiber switches restore the active cell before control returns to a + // synchronous frame, so its pointer is one value for the whole call: + // load it once in the entry block and test only the kind here. + if (B.excCell === null) { + B.excCell = "%exc.cell"; + B.entryAllocas.push(`${B.excCell} = load ptr, ptr @scr_exc_active`); + } + cell = B.excCell; + } else { + cell = B.tmp(); + B.line(`${cell} = load ptr, ptr @scr_exc_active`); + } const kind = B.tmp(); - B.line(`${cell} = load ptr, ptr @scr_exc_active`); B.line(`${kind} = load i32, ptr ${cell}`); let word = kind; if (this.mod.workers === true) { @@ -4770,6 +4782,9 @@ export class LlEmitter { private emitFunction(fn: IrFunction): string { const B = new BlockBuilder(); this.B = B; + // Fiber switches restore the active exception cell before control + // returns to a synchronous frame (emitPendingCheck). + B.excCellInvariant = fn.async !== true && fn.generator === undefined; this.debugScope = this.debug?.function(fn) ?? null; B.debugLocation = this.debug?.location(fn.loc, this.debugScope) ?? null; this.frames = []; diff --git a/packages/compiler/src/backend/llvm/local-array-reads.test.ts b/packages/compiler/src/backend/llvm/local-array-reads.test.ts index 67a37ce56..31b2717b3 100644 --- a/packages/compiler/src/backend/llvm/local-array-reads.test.ts +++ b/packages/compiler/src/backend/llvm/local-array-reads.test.ts @@ -24,7 +24,8 @@ import { import { analyzeCallLifetimes } from "./call-lifetimes.js"; /** A pending-exception check: the inline active-cell test or a runtime call. */ -const PENDING_CHECK = /@scr_exc_(?:active|pending)\b/; +// Synchronous bodies test the kind of the entry-loaded exception cell. +const PENDING_CHECK = /@scr_exc_(?:active|pending)\b| = load i32, ptr %exc\.cell\b/; const loc = { file: "local-array.ts", start: 0, end: 0 }; const element: IrType = { kind: "record", shapeId: "cell" }; diff --git a/packages/compiler/src/backend/llvm/string-lifetimes.test.ts b/packages/compiler/src/backend/llvm/string-lifetimes.test.ts index bf54116ee..6713d5b90 100644 --- a/packages/compiler/src/backend/llvm/string-lifetimes.test.ts +++ b/packages/compiler/src/backend/llvm/string-lifetimes.test.ts @@ -18,7 +18,8 @@ import { emitLlvmModule } from "./emitter.js"; import { borrowsStringInputs } from "./string-lifetimes.js"; /** A pending-exception check: the inline active-cell test or a runtime call. */ -const PENDING_CHECK = /@scr_exc_(?:active|pending)\b/; +// Synchronous bodies test the kind of the entry-loaded exception cell. +const PENDING_CHECK = /@scr_exc_(?:active|pending)\b| = load i32, ptr %exc\.cell\b/; const loc = { file: "strings.ts", start: 0, end: 1 }; const ref = (id: string, type: IrType = STRING): IrExpr => ({ diff --git a/packages/compiler/test/string-and-array-lifetimes.test.ts b/packages/compiler/test/string-and-array-lifetimes.test.ts index 10e496a8d..4f59d8bbc 100644 --- a/packages/compiler/test/string-and-array-lifetimes.test.ts +++ b/packages/compiler/test/string-and-array-lifetimes.test.ts @@ -8,7 +8,8 @@ import { emitLlvmModule } from "../src/backend/llvm/emitter.js"; import { type IrModule } from "../src/ir/ir.js"; /** A pending-exception check: the inline active-cell test or a runtime call. */ -const PENDING_CHECK = /@scr_exc_(?:active|pending)\b/; +// Synchronous bodies test the kind of the entry-loaded exception cell. +const PENDING_CHECK = /@scr_exc_(?:active|pending)\b| = load i32, ptr %exc\.cell\b/; async function lower(source: string): Promise { const dir = await mkdtemp(join(tmpdir(), "scriptc-input-lifetimes-"));