mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-10-29 07:04:09 +08:00
Inline the pending-exception test in helpers and runtime units
main's emitPendingCheck (#785) already tests `scr_exc_active->kind` inline after every may-throw call in executables. The helper and adapter bodies the backend generates (dyn check helpers, async/generator trampolines, inline-async wrappers, FFI callback adapters, island and stream adapters, IPC message thunks, dynamic-key record stores) still called `scr_exc_pending()`, an opaque call into the prebuilt runtime, and so did every runtime C unit. On the tsc-ts self-check that function was about 4% of self time. Those sites now share pendingTestLines, which follows emitPendingCheck's policy: executables load the active cell and compare its i32 kind inline; libraries and worker programs keep the runtime call (workers also observe cross-thread termination there). Runtime units outside thread-instanced libraries and worker executables get the same inline test through a scr_runtime.h macro; the exported scr_exc_pending definition stays for those lanes and for emitted slow paths. A static assert pins the i32 kind at offset 0. This replays the work item's inline-check commit on top of #785: its own emitPendingCheck rewrite is dropped in favor of main's. Adds corpus 4431 pinning exception propagation across fiber switches, generators, sort/map callbacks, and runtime JSON failures.
This commit is contained in:
@@ -86,6 +86,8 @@ export interface DynHost extends WalkerHost {
|
||||
dynPromiseAdapter(t: IrType): string;
|
||||
isErrorClass(className: string): boolean;
|
||||
classSubtypes(className: string): readonly string[];
|
||||
/** emitter.ts's pendingTestLines: the inline pending-exception test. */
|
||||
pendingTestLines(dest: string): string[];
|
||||
}
|
||||
|
||||
const FN_ATTRS = "#0";
|
||||
@@ -484,9 +486,8 @@ export class LlDyn {
|
||||
/** Emit the standard in-helper pending check: on a pending exception
|
||||
* run `cleanup` and return `dummy`. */
|
||||
private pendingBail(B: BlockBuilder, hint: string, cleanup: () => void, dummy: string): void {
|
||||
this.host.declare(`declare zeroext i1 @scr_exc_pending()`);
|
||||
const p = B.tmp();
|
||||
B.line(`${p} = call zeroext i1 @scr_exc_pending()`);
|
||||
for (const line of this.host.pendingTestLines(p)) B.line(line.trimStart());
|
||||
const lu = B.newLabel(`${hint}.u`);
|
||||
const lk = B.newLabel(`${hint}.k`);
|
||||
B.condBr(p, lu, lk);
|
||||
|
||||
@@ -819,6 +819,7 @@ export class LlEmitter {
|
||||
)
|
||||
.map((meta) => meta.def.name);
|
||||
},
|
||||
pendingTestLines: (dest) => this.pendingTestLines(dest),
|
||||
};
|
||||
this.walkers = new LlWalkers(this.shapeHost);
|
||||
this.dyn = new LlDyn(this.shapeHost);
|
||||
@@ -916,7 +917,6 @@ export class LlEmitter {
|
||||
const globals: string[] = [];
|
||||
const defs: string[] = [];
|
||||
if (this.ffiCallbackAdapters.size === 0) return { globals, defs };
|
||||
this.declare(`declare zeroext i1 @scr_exc_pending()`);
|
||||
this.declare(`declare void @scr_trap(ptr)`);
|
||||
const expired = this.cstr(
|
||||
"scriptc: native callback invoked outside its call-scoped lifetime\n",
|
||||
@@ -1081,7 +1081,7 @@ export class LlEmitter {
|
||||
` call void @scr_trap(ptr ${adapter.callback.lifetime === "call" ? expired : released})`,
|
||||
` unreachable`,
|
||||
`ready:`,
|
||||
` %pending = call zeroext i1 @scr_exc_pending()`,
|
||||
...this.pendingTestLines("%pending"),
|
||||
` br i1 %pending, label %skip, label %invoke`,
|
||||
`skip:`,
|
||||
` ret ${ffiCallbackDummyLl(cb)}`,
|
||||
@@ -1519,8 +1519,9 @@ export class LlEmitter {
|
||||
const topPendingReleases = asyncEntry ? globalReleaseLines("gp") : [];
|
||||
const loopReportedReleases = runsLoop ? globalReleaseLines("gq") : [];
|
||||
// main's epilogues read the flag / the loop entry points — declared
|
||||
// HERE, before the extern block flushes (a pending check usually
|
||||
// declared the flag already; the Set dedupes).
|
||||
// HERE, before the extern block flushes. They run once, so they keep
|
||||
// the out-of-line scr_exc_pending call; per-call checks inline the
|
||||
// test (pendingTestLines).
|
||||
if (entryMayThrow || runsLoop) this.declare(`declare zeroext i1 @scr_exc_pending()`);
|
||||
if (runsLoop) {
|
||||
this.declare(`declare zeroext i1 @scr_loop_run(ptr)`);
|
||||
@@ -2745,7 +2746,6 @@ export class LlEmitter {
|
||||
|
||||
this.declare(`declare void @free(ptr)`);
|
||||
this.declare(`declare ptr @malloc(${this.sizeType})`);
|
||||
this.declare(`declare zeroext i1 @scr_exc_pending()`);
|
||||
|
||||
const tr: string[] = [
|
||||
`define internal void @${mangleTrampoline(fn.name)}(ptr %self, ptr %ap) ${FN_ATTRS} {`,
|
||||
@@ -2899,7 +2899,6 @@ export class LlEmitter {
|
||||
this.declare(`declare void @scr_async_inline_enter(ptr)`);
|
||||
this.declare(`declare ptr @scr_async_inline_leave(ptr)`);
|
||||
this.declare(`declare void @scr_promise_reject_pending(ptr)`);
|
||||
this.declare(`declare zeroext i1 @scr_exc_pending()`);
|
||||
if (lifted) {
|
||||
this.declare(`declare ptr @scr_closure_retain_v(ptr)`);
|
||||
this.declare(`declare void @scr_closure_release(ptr)`);
|
||||
@@ -2915,7 +2914,7 @@ export class LlEmitter {
|
||||
` call void @scr_async_inline_enter(ptr %frame)`,
|
||||
retTy === "void" ? ` ${bodyCall}` : ` %r = ${bodyCall}`,
|
||||
...(lifted ? [` call void @scr_closure_release(ptr %a0)`] : []),
|
||||
` %pend = call zeroext i1 @scr_exc_pending()`,
|
||||
...this.pendingTestLines("%pend"),
|
||||
` %p = call ptr @scr_async_inline_leave(ptr %frame)`,
|
||||
` br i1 %pend, label %thrown, label %clean`,
|
||||
`clean:`,
|
||||
@@ -2985,7 +2984,7 @@ export class LlEmitter {
|
||||
this.declare(`declare void @scr_closure_release(ptr)`);
|
||||
}
|
||||
tr.push(
|
||||
` %pend = call zeroext i1 @scr_exc_pending()`,
|
||||
...this.pendingTestLines("%pend"),
|
||||
` br i1 %pend, label %thrown, label %clean`,
|
||||
`clean:`,
|
||||
` %pr = call ptr @scr_fiber_promise(ptr %self)`,
|
||||
@@ -3105,7 +3104,7 @@ export class LlEmitter {
|
||||
} else {
|
||||
tr.push(
|
||||
` %g = call ptr @scr_gen_of_fiber(ptr %self)`,
|
||||
` %pend = call zeroext i1 @scr_exc_pending()`,
|
||||
...this.pendingTestLines("%pend"),
|
||||
` br i1 %pend, label %thrown, label %clean`,
|
||||
`clean:`,
|
||||
);
|
||||
@@ -3540,6 +3539,28 @@ export class LlEmitter {
|
||||
this.B.br(cleanup.label);
|
||||
}
|
||||
|
||||
/** Instructions (two-space indented) that set the i1 `dest` to "an
|
||||
* exception is pending in the active context" — scr_exc_pending()'s
|
||||
* meaning — for the helper and adapter bodies that test it outside
|
||||
* emitPendingCheck. Executables read the runtime's exported active-cell
|
||||
* pointer and compare its i32 kind (offset 0) against SCR_EXC_NONE inline,
|
||||
* as emitPendingCheck does; the out-of-line call would be opaque to LLVM
|
||||
* because the runtime links as prebuilt objects. Libraries and worker
|
||||
* programs keep the runtime call, which is emitPendingCheck's path there
|
||||
* too (workers also observe cross-thread termination in it). */
|
||||
pendingTestLines(dest: string): string[] {
|
||||
if (this.mod.lib !== undefined || this.mod.workers === true) {
|
||||
this.declare(`declare zeroext i1 @scr_exc_pending()`);
|
||||
return [` ${dest} = call zeroext i1 @scr_exc_pending()`];
|
||||
}
|
||||
this.declare(`@scr_exc_active = external global ptr`);
|
||||
return [
|
||||
` ${dest}.cell = load ptr, ptr @scr_exc_active`,
|
||||
` ${dest}.kind = load i32, ptr ${dest}.cell`,
|
||||
` ${dest} = icmp ne i32 ${dest}.kind, 0`,
|
||||
];
|
||||
}
|
||||
|
||||
/** The emitter contract for exceptions: after EVERY call that can throw
|
||||
* (per the may-throw analysis), test the pending flag and unwind. The
|
||||
* call's result temp must join its frame BEFORE this runs so the unwind
|
||||
@@ -5224,9 +5245,8 @@ export class LlEmitter {
|
||||
B.line(`call void @scr_dyn_release(ptr ${v.name})`);
|
||||
// Mismatched write: TypeError pending, field untouched — the
|
||||
// statement-level check below unwinds.
|
||||
this.declare(`declare zeroext i1 @scr_exc_pending()`);
|
||||
const pend = B.tmp();
|
||||
B.line(`${pend} = call zeroext i1 @scr_exc_pending()`);
|
||||
for (const line of this.pendingTestLines(pend)) B.line(line.trimStart());
|
||||
const lw = B.newLabel("rks.w");
|
||||
B.condBr(pend, join, lw);
|
||||
B.startBlock(lw);
|
||||
|
||||
@@ -568,9 +568,8 @@ export function ipcMessageThunkFor(
|
||||
} else {
|
||||
const helper = host.dyn.dynCheckHelper(param);
|
||||
d.push(` %value = call ${ty} @${helper}(ptr %message, ptr null)`);
|
||||
host.declare(`declare zeroext i1 @scr_exc_pending()`);
|
||||
d.push(
|
||||
` %bad = call zeroext i1 @scr_exc_pending()`,
|
||||
...host.pendingTestLines("%bad"),
|
||||
` br i1 %bad, label %fail, label %invoke`,
|
||||
`fail:`,
|
||||
...(isRefCounted(param)
|
||||
|
||||
@@ -653,7 +653,6 @@ export function islandTypedAdapter(
|
||||
`cont${b}:`,
|
||||
);
|
||||
};
|
||||
host.declare(`declare zeroext i1 @scr_exc_pending()`);
|
||||
fn.params.forEach((p, i) => {
|
||||
d.push(
|
||||
` %ap${i} = getelementptr inbounds ptr, ptr %argv, i64 ${i}`,
|
||||
@@ -742,7 +741,7 @@ export function islandTypedAdapter(
|
||||
`cont${b3}:`,
|
||||
` %cv${i} = call ${host.llType(p)} @${host.dyn.dynCheckHelper(p)}(ptr %dp${i}, ptr null)`,
|
||||
` call void @scr_dyn_release(ptr %dp${i})`,
|
||||
` %pe${i} = call zeroext i1 @scr_exc_pending()`,
|
||||
...host.pendingTestLines(`%pe${i}`),
|
||||
);
|
||||
const b4 = blk++;
|
||||
d.push(
|
||||
@@ -852,7 +851,7 @@ export function islandTypedAdapter(
|
||||
d.push(
|
||||
` %jbuf = alloca %ScrJsonBuf`,
|
||||
` %rv = call ${host.llType(fn.ret)} %fn(${passed.join(", ")})`,
|
||||
` %rpend = call zeroext i1 @scr_exc_pending()`,
|
||||
...host.pendingTestLines("%rpend"),
|
||||
` br i1 %rpend, label %jfail, label %jok`,
|
||||
`jfail:`,
|
||||
...(isRefCounted(fn.ret)
|
||||
|
||||
@@ -70,7 +70,6 @@ export function dynPromiseAdapter(host: LlvmEmitterContext, inner: IrType): stri
|
||||
host.dynPromiseAdapters.set(key, sym);
|
||||
host.declare(`declare ptr @scr_promise_payload_ref(ptr)`);
|
||||
host.declare(`declare void @scr_dyn_release_v(ptr)`);
|
||||
host.declare(`declare zeroext i1 @scr_exc_pending()`);
|
||||
host.declare(`declare void @scr_promise_reject_pending(ptr)`);
|
||||
const B = new BlockBuilder();
|
||||
const dyn = B.tmp();
|
||||
@@ -83,7 +82,7 @@ export function dynPromiseAdapter(host: LlvmEmitterContext, inner: IrType): stri
|
||||
: `${value} = call ${host.llType(inner)} @${host.dyn.dynCheckHelper(inner)}(ptr ${dyn}, ptr null)`,
|
||||
);
|
||||
B.line(`call void @scr_dyn_release_v(ptr ${dyn})`);
|
||||
B.line(`${pending} = call zeroext i1 @scr_exc_pending()`);
|
||||
for (const line of host.pendingTestLines(pending)) B.line(line.trimStart());
|
||||
const fail = B.newLabel("sra.fail");
|
||||
const ok = B.newLabel("sra.ok");
|
||||
B.condBr(pending, fail, ok);
|
||||
@@ -210,7 +209,6 @@ export function streamTypedRefCommitAdapter(
|
||||
const commit = `${snapshot}_commit`;
|
||||
host.declare(`declare ptr @scr_dyn_obj_get(ptr, ptr, ${host.sizeType})`);
|
||||
host.declare(`declare ptr @scr_dyn_undefined()`);
|
||||
host.declare(`declare zeroext i1 @scr_exc_pending()`);
|
||||
const lines = [
|
||||
`define internal void @${commit}(ptr %target, ptr %d) ${FN_ATTRS} { ; commit unknown class ${typeKey(t)}`,
|
||||
`entry:`,
|
||||
@@ -304,7 +302,7 @@ export function streamTypedRefCommitAdapter(
|
||||
` %${input} = select i1 %${missing}, ptr %${undef}, ptr %${raw}`,
|
||||
` %${next} = call ${field.type.kind === "bool" ? "zeroext " : ""}${checkTy} @${host.dyn.dynCheckHelper(field.type)}(ptr %${input}, ptr null)`,
|
||||
...(symbolGlobal ? [` call void @scr_dyn_release_v(ptr %${raw})`] : []),
|
||||
` %${pending} = call zeroext i1 @scr_exc_pending()`,
|
||||
...host.pendingTestLines(`%${pending}`),
|
||||
` br i1 %${pending}, label %done, label %${store}`,
|
||||
`${store}:`,
|
||||
` %f${index}_ptr = getelementptr inbounds %${classStructSym(t.className)}, ptr %target, i64 0, i32 ${fieldIndex}`,
|
||||
@@ -360,7 +358,7 @@ export function streamTypedRefCommitAdapter(
|
||||
` %bag = call ptr @scr_dyn_new_obj()`,
|
||||
` %bag_copy = call ptr @scr_dyn_copy_property_descriptors(ptr %bag, ptr %d)`,
|
||||
` call void @scr_dyn_release_v(ptr %bag_copy)`,
|
||||
` %bag_pending = call zeroext i1 @scr_exc_pending()`,
|
||||
...host.pendingTestLines("%bag_pending"),
|
||||
` br i1 %bag_pending, label %bag_fail, label %bag_keys`,
|
||||
`bag_fail:`,
|
||||
` call void @scr_dyn_release_v(ptr %bag)`,
|
||||
|
||||
@@ -15079,6 +15079,12 @@
|
||||
],
|
||||
"diags": []
|
||||
},
|
||||
"<repo>/tests/corpus/4431-exception-pending-contexts.ts": {
|
||||
"order": [
|
||||
"<repo>/tests/corpus/4431-exception-pending-contexts.ts"
|
||||
],
|
||||
"diags": []
|
||||
},
|
||||
"<repo>/tests/corpus/array-sort-runtime-comparator.ts": {
|
||||
"order": [
|
||||
"<repo>/tests/corpus/array-sort-runtime-comparator.ts"
|
||||
|
||||
@@ -122,7 +122,9 @@ ScrStr *scr_stack_capture(void) {
|
||||
#define scr_exc_release_fn (SCR_EXC_CUR()->release_fn)
|
||||
#define scr_exc_trace_fn (SCR_EXC_CUR()->trace_fn)
|
||||
|
||||
bool scr_exc_pending(void) {
|
||||
/* Parenthesized: scr_runtime.h maps runtime-internal calls to an inline
|
||||
* test with a function-like macro; this keeps the exported symbol. */
|
||||
bool(scr_exc_pending)(void) {
|
||||
#ifdef SCR_WORKERS
|
||||
if (scr_context_checkpoint()) return true;
|
||||
#endif
|
||||
|
||||
@@ -2666,6 +2666,18 @@ ScrExcCell *scr_exc_current_cell(void); /* the ACTIVE cell */
|
||||
|
||||
bool scr_exc_pending(void);
|
||||
|
||||
#if !((defined(SCR_LIB) && defined(SCR_THREAD_INSTANCES)) || defined(SCR_WORKERS))
|
||||
/* Runtime units test the active cell's kind inline, as emitted exception
|
||||
* polls do (scr_exc_active is never NULL outside thread-instanced libraries
|
||||
* and worker executables, which also poll cross-thread termination in
|
||||
* scr_exc_pending). The out-of-line definition in scr_exception.c stays
|
||||
* for those lanes and for emitted code's slow paths. */
|
||||
static inline bool scr_exc_pending_inline(void) { return scr_exc_active->kind != SCR_EXC_NONE; }
|
||||
#define scr_exc_pending() scr_exc_pending_inline()
|
||||
#endif
|
||||
_Static_assert(offsetof(ScrExcCell, kind) == 0 && sizeof(ScrExcKind) == 4 && SCR_EXC_NONE == 0,
|
||||
"the LLVM emitter's inline pending check loads an i32 kind at offset 0");
|
||||
|
||||
void scr_throw_f64(double v);
|
||||
void scr_throw_bool(bool v);
|
||||
void scr_throw_str(ScrStr *v); /* takes ownership */
|
||||
|
||||
@@ -0,0 +1,142 @@
|
||||
// The pending-exception test after may-throw calls reads the ACTIVE
|
||||
// exception context (one per fiber). These cases interleave throws with
|
||||
// fiber switches, generators, runtime callbacks, and runtime-internal
|
||||
// failures so a check that read the wrong context would either miss an
|
||||
// exception or unwind a frame that never threw.
|
||||
|
||||
class Bad extends Error {
|
||||
readonly code: number;
|
||||
constructor(code: number) {
|
||||
super(`bad ${code}`);
|
||||
this.code = code;
|
||||
}
|
||||
}
|
||||
|
||||
function mayThrow(n: number): number {
|
||||
if (n % 7 === 3) throw new Bad(n);
|
||||
return n * 2;
|
||||
}
|
||||
|
||||
// Many checks on the hot path, rare unwinds.
|
||||
function sumChecked(limit: number): number {
|
||||
let sum = 0;
|
||||
let caught = 0;
|
||||
for (let i = 0; i < limit; i++) {
|
||||
try {
|
||||
sum += mayThrow(i);
|
||||
} catch (e) {
|
||||
if (e instanceof Bad) caught += e.code;
|
||||
else throw e;
|
||||
}
|
||||
}
|
||||
return sum * 1000 + caught;
|
||||
}
|
||||
console.log("loop", sumChecked(200));
|
||||
|
||||
// Exceptions across awaits: each fiber owns its own pending state, and a
|
||||
// throw in one fiber must not be observed by another that resumes first.
|
||||
async function tick(): Promise<void> {
|
||||
await Promise.resolve();
|
||||
}
|
||||
|
||||
async function failsLater(id: number): Promise<number> {
|
||||
await tick();
|
||||
if (id % 2 === 1) throw new Bad(id);
|
||||
await tick();
|
||||
return id * 10;
|
||||
}
|
||||
|
||||
async function finallyAcrossAwait(id: number): Promise<string> {
|
||||
const log: string[] = [];
|
||||
try {
|
||||
try {
|
||||
log.push(`try${id}`);
|
||||
await failsLater(id);
|
||||
log.push("resumed");
|
||||
} finally {
|
||||
await tick();
|
||||
log.push("finally");
|
||||
}
|
||||
} catch (e) {
|
||||
log.push(e instanceof Bad ? `caught ${e.code}` : "other");
|
||||
}
|
||||
return log.join(",");
|
||||
}
|
||||
|
||||
// Generators run on their own fibers too.
|
||||
function* numbers(): Generator<number, string, unknown> {
|
||||
yield mayThrow(1);
|
||||
try {
|
||||
yield mayThrow(3);
|
||||
} catch (e) {
|
||||
yield e instanceof Bad ? -e.code : 0;
|
||||
}
|
||||
return "done";
|
||||
}
|
||||
|
||||
function drain(): string {
|
||||
const out: string[] = [];
|
||||
const it = numbers();
|
||||
for (let r = it.next(); ; r = it.next()) {
|
||||
if (r.done) {
|
||||
out.push(String(r.value));
|
||||
break;
|
||||
}
|
||||
out.push(String(r.value));
|
||||
}
|
||||
return out.join(" ");
|
||||
}
|
||||
|
||||
async function main(): Promise<void> {
|
||||
const results = await Promise.all([0, 1, 2, 3].map((id) => finallyAcrossAwait(id)));
|
||||
for (const r of results) console.log("async", r);
|
||||
|
||||
// Two fibers in flight; the second throws while the first is suspended.
|
||||
const pending = [failsLater(4), failsLater(5)];
|
||||
for (const p of pending) {
|
||||
try {
|
||||
console.log("settled", await p);
|
||||
} catch (e) {
|
||||
console.log("rejected", e instanceof Bad ? e.code : "other");
|
||||
}
|
||||
}
|
||||
|
||||
console.log("gen", drain());
|
||||
|
||||
// A throw from a sort comparator unwinds through the sort loop.
|
||||
const values = [5, 3, 9, 1, 7];
|
||||
try {
|
||||
values.sort((a, b) => {
|
||||
if (a === 9 || b === 9) throw new Bad(9);
|
||||
return a - b;
|
||||
});
|
||||
} catch (e) {
|
||||
console.log("sort", e instanceof Bad ? e.code : "other");
|
||||
}
|
||||
|
||||
// A throw from an array callback mid-iteration.
|
||||
try {
|
||||
const mapped = [1, 2, 3, 4].map((x) => mayThrow(x));
|
||||
console.log("map", mapped.join(","));
|
||||
} catch (e) {
|
||||
console.log("map caught", e instanceof Bad ? e.code : "other");
|
||||
}
|
||||
|
||||
// Runtime-internal failures (the runtime's own pending checks).
|
||||
for (const text of ['{"a": [1, 2, {"b": true}]}', '{"a": [1, 2,', "[1, 2, 3]"]) {
|
||||
try {
|
||||
const parsed = JSON.parse(text) as unknown;
|
||||
console.log("json", JSON.stringify(parsed));
|
||||
} catch (e) {
|
||||
console.log("json error", e instanceof SyntaxError);
|
||||
}
|
||||
}
|
||||
|
||||
// After everything above, nothing may still be pending.
|
||||
await tick();
|
||||
console.log("after", sumChecked(10));
|
||||
}
|
||||
|
||||
main().catch((e: unknown) => {
|
||||
console.log("main failed", e instanceof Error ? e.message : String(e));
|
||||
});
|
||||
Reference in New Issue
Block a user