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-"));