From 2f3e023bc9778e08c8648e4424969e4bb6afbcce Mon Sep 17 00:00:00 2001 From: Malte Ubl Date: Thu, 8 Oct 2026 16:56:56 -0700 Subject: [PATCH] Unwind directly after runtime throws that always leave an exception The compiler-resolved Node throws (error.nodeThrow, the argument and property type throws, undeclared global reads) always leave an exception pending, yet each was followed by the generic pending test and a branch. tsc-ts has 12,177 such sites, mostly the mismatch arms of checked casts and member reads of possibly-undefined values. Unwind unconditionally instead; emission continues in a block without predecessors so the caller's remaining instructions stay well-formed, and LLVM deletes it. --- packages/compiler/src/backend/llvm/emitter.test.ts | 3 ++- packages/compiler/src/backend/llvm/emitter.ts | 12 ++++++++++++ packages/compiler/src/backend/llvm/lib-filesystem.ts | 2 +- packages/compiler/src/backend/llvm/lib-process.ts | 12 ++++++------ 4 files changed, 21 insertions(+), 8 deletions(-) diff --git a/packages/compiler/src/backend/llvm/emitter.test.ts b/packages/compiler/src/backend/llvm/emitter.test.ts index 5c0dc6308..cd364f414 100644 --- a/packages/compiler/src/backend/llvm/emitter.test.ts +++ b/packages/compiler/src/backend/llvm/emitter.test.ts @@ -411,7 +411,8 @@ test("checked field receivers borrow only the successful projection", () => { }; const llvm = work(text(child(checked)), union); expect(llvm).toContain("@scr_throw_node_coded"); - expect(llvm).toMatch(PENDING_CHECK); + // The throw always leaves an exception pending: it unwinds directly. + expect(llvm).not.toMatch(PENDING_CHECK); expect(llvm).not.toContain("@scr_union_retain_v"); expect(llvm).not.toMatch(/call ptr @sc_rretain_/); const captured = work(text(child(checked)), union, true); diff --git a/packages/compiler/src/backend/llvm/emitter.ts b/packages/compiler/src/backend/llvm/emitter.ts index aa582e059..02c90cdb7 100644 --- a/packages/compiler/src/backend/llvm/emitter.ts +++ b/packages/compiler/src/backend/llvm/emitter.ts @@ -3626,6 +3626,18 @@ export class LlEmitter { B.startBlock(lk); } + /** After a runtime call that ALWAYS leaves an exception pending (the + * compiler-resolved Node throws): the pending test would always answer + * true, so unwind unconditionally. Emission continues in a block with no + * predecessors so the caller's remaining instructions stay well-formed; + * LLVM deletes it. */ + emitAlwaysPendingUnwind(): void { + const B = this.B; + if (B.isTerminated()) return; + this.emitUnwind(); + B.startBlock(B.newLabel("exc.dead")); + } + private workerLoopBudget(): string | null { if (!this.mod.workers) return null; const slot = this.B.slot(); diff --git a/packages/compiler/src/backend/llvm/lib-filesystem.ts b/packages/compiler/src/backend/llvm/lib-filesystem.ts index f2490ae03..5a70a55a8 100644 --- a/packages/compiler/src/backend/llvm/lib-filesystem.ts +++ b/packages/compiler/src/backend/llvm/lib-filesystem.ts @@ -114,7 +114,7 @@ export function emitDynamicLibCall(host: LlvmEmitterContext, e: LibCallExpr): Ll const ty = host.llType(e.type); const dummy = ty === "double" ? f64Lit(0) : ty === "i1" ? "false" : "null"; const out = host.own({ name: dummy, type: e.type }); - host.emitPendingCheck(); + host.emitAlwaysPendingUnwind(); return out; } return host.emitGenericLibCall(e); diff --git a/packages/compiler/src/backend/llvm/lib-process.ts b/packages/compiler/src/backend/llvm/lib-process.ts index ef0bacdcb..70b2cda83 100644 --- a/packages/compiler/src/backend/llvm/lib-process.ts +++ b/packages/compiler/src/backend/llvm/lib-process.ts @@ -636,12 +636,12 @@ export function emitErrorsEventsLibCall(host: LlvmEmitterContext, e: LibCallExpr B.line(`call void @scr_throw_arg_type(ptr ${an.name}, ptr ${ex.name}, ptr ${got.name})`); const ty = host.llType(e.type); if (ty === "void") { - host.emitPendingCheck(); + host.emitAlwaysPendingUnwind(); return { name: "", type: e.type }; } const dummy = ty === "double" ? f64Lit(0) : ty === "i1" ? "false" : "null"; const out = host.own({ name: dummy, type: e.type }); - host.emitPendingCheck(); + host.emitAlwaysPendingUnwind(); return out; } if (e.fn === "error.propTypeThrow") { @@ -653,12 +653,12 @@ export function emitErrorsEventsLibCall(host: LlvmEmitterContext, e: LibCallExpr B.line(`call void @scr_throw_prop_type(ptr ${an.name}, ptr ${ex.name}, ptr ${got.name})`); const ty = host.llType(e.type); if (ty === "void") { - host.emitPendingCheck(); + host.emitAlwaysPendingUnwind(); return { name: "", type: e.type }; } const dummy = ty === "double" ? f64Lit(0) : ty === "i1" ? "false" : "null"; const out = host.own({ name: dummy, type: e.type }); - host.emitPendingCheck(); + host.emitAlwaysPendingUnwind(); return out; } if (e.fn === "error.nodeThrow") { @@ -673,12 +673,12 @@ export function emitErrorsEventsLibCall(host: LlvmEmitterContext, e: LibCallExpr ); const ty = host.llType(e.type); if (ty === "void") { - host.emitPendingCheck(); + host.emitAlwaysPendingUnwind(); return { name: "", type: e.type }; } const dummy = ty === "double" ? f64Lit(0) : ty === "i1" ? "false" : "null"; const out = host.own({ name: dummy, type: e.type }); - host.emitPendingCheck(); + host.emitAlwaysPendingUnwind(); return out; } if (e.fn === "emitter.on") {