mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-10-21 05:50:55 +08:00
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.
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
@@ -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 = [];
|
||||
|
||||
@@ -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" };
|
||||
|
||||
@@ -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 => ({
|
||||
|
||||
@@ -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<IrModule> {
|
||||
const dir = await mkdtemp(join(tmpdir(), "scriptc-input-lifetimes-"));
|
||||
|
||||
Reference in New Issue
Block a user