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.
This commit is contained in:
Malte Ubl
2026-10-09 11:59:27 -05:00
committed by GitHub
parent 8d1969a313
commit 2f3e023bc9
4 changed files with 21 additions and 8 deletions
@@ -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);
@@ -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();
@@ -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);
@@ -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") {