mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-11-07 08:33:50 +08:00
Merge remote-tracking branch 'origin/main' into feat/contract-types
# Conflicts: # packages/compiler/src/backend/may-throw.ts # packages/compiler/test/ts7/baselines/order-parity.json
This commit is contained in:
@@ -268,6 +268,7 @@
|
||||
"files": [
|
||||
"packages/compiler/src/frontend/lowering/**",
|
||||
"packages/compiler/src/backend/llvm/**",
|
||||
"packages/compiler/src/backend/loop-polls.test.ts",
|
||||
"packages/compiler/src/ir/**",
|
||||
"packages/compiler/src/library/*.test.ts",
|
||||
"packages/compiler/test/**",
|
||||
|
||||
@@ -555,10 +555,34 @@ test("synchronous bodies load the active exception cell once for all pending che
|
||||
/^(?: %[^\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 test the process-wide alert word at each check
|
||||
// instead, and never read their thread-local cell inline.
|
||||
const workers = body(emitLlvmModule({ ...module, workers: true }), "sc_f_work");
|
||||
// Darwin and Windows worker executables test the process-wide alert word
|
||||
// at each check instead, and never read their thread-local cell inline.
|
||||
const darwin = { targetTriple: "arm64-apple-macosx14.0.0" };
|
||||
const workers = body(emitLlvmModule({ ...module, workers: true }, darwin), "sc_f_work");
|
||||
expect(workers).not.toContain("%exc.cell");
|
||||
expect(workers).not.toContain("@scr_exc_active");
|
||||
expect(workers.match(/= load atomic i32, ptr @scr_exc_alert monotonic/g)).toHaveLength(3);
|
||||
expect(workers.match(/call zeroext i1 @scr_exc_pending\(\)/g)).toHaveLength(3);
|
||||
// ELF worker executables test the thread's exact pending word and unwind
|
||||
// directly: no check carries an out-of-line slow path.
|
||||
const elf = { targetTriple: "x86_64-unknown-linux-gnu" };
|
||||
const flagLlvm = emitLlvmModule({ ...module, workers: true }, elf);
|
||||
expect(flagLlvm).toContain("@scr_exc_flag = external thread_local(localexec) global i32");
|
||||
const flag = body(flagLlvm, "sc_f_work");
|
||||
expect(flag).not.toContain("@scr_exc_alert");
|
||||
expect(flag).not.toContain("@scr_exc_pending");
|
||||
// Outside loops the tests are unordered loads LLVM may forward around.
|
||||
expect(flag.match(/= load atomic i32, ptr @scr_exc_flag unordered/g)).toHaveLength(3);
|
||||
expect(flag).not.toContain("monotonic");
|
||||
// Inside a source loop (whose termination poll the call's test replaces)
|
||||
// the test stays ordered, so LLVM cannot hoist it out of the loop.
|
||||
const spin: IrStmt = {
|
||||
kind: "while",
|
||||
cond: { kind: "boolLit", value: true, type: BOOL, loc },
|
||||
body: [call()],
|
||||
loc,
|
||||
};
|
||||
const looped = body(emitLlvmModule({ ...moduleFor([spin]), workers: true }, elf), "sc_f_work");
|
||||
expect(looped.match(/= load atomic i32, ptr @scr_exc_flag monotonic/g)).toHaveLength(1);
|
||||
expect(looped).not.toContain("unordered");
|
||||
});
|
||||
|
||||
@@ -28,6 +28,7 @@ import {
|
||||
undefinedArmTag,
|
||||
} from "../../ir/analysis.js";
|
||||
import { emitLibraryIdentityLines } from "../library-identity-markers.js";
|
||||
import { loopTestsEveryIteration } from "../loop-polls.js";
|
||||
import type {
|
||||
IrBytesElem,
|
||||
IrExpr,
|
||||
@@ -529,6 +530,13 @@ export class LlEmitter {
|
||||
readonly inlineTrunc: boolean;
|
||||
/** ELF worker executables give thread-locals the executable TLS models. */
|
||||
private readonly executableTls: boolean;
|
||||
/** Source loops enclosing the statement being emitted in the current
|
||||
* function body. */
|
||||
private loopDepth = 0;
|
||||
/** Darwin worker executables read the stack guard from a pthread
|
||||
* thread-specific-data slot off the thread pointer (scr_runtime.h's
|
||||
* SCR_STACK_GUARD_TSD): the architecture names the access sequence. */
|
||||
private readonly stackGuardTsd: "aarch64" | "x86_64" | null;
|
||||
private readonly emitLibraryIdentity: boolean;
|
||||
private readonly runtimeAbiMarker: boolean;
|
||||
private readonly detachedExit: boolean;
|
||||
@@ -842,6 +850,15 @@ export class LlEmitter {
|
||||
mod.lib === undefined &&
|
||||
!this.wasi &&
|
||||
!/apple|darwin|windows|mingw|cygwin|win32/.test(options.targetTriple || process.platform);
|
||||
const host = options.targetTriple || `${process.arch}-${process.platform}`;
|
||||
this.stackGuardTsd =
|
||||
mod.workers === true && mod.lib === undefined && !this.wasi && /apple|darwin/.test(host)
|
||||
? /^(arm64|aarch64)/.test(host)
|
||||
? "aarch64"
|
||||
: /^(x86_64|amd64|x64)/.test(host)
|
||||
? "x86_64"
|
||||
: null
|
||||
: null;
|
||||
this.emitLibraryIdentity = options.emitLibraryIdentity !== false;
|
||||
this.runtimeAbiMarker = options.runtimeAbiMarker === true;
|
||||
this.detachedExit = options.detachedExit === true;
|
||||
@@ -1458,11 +1475,13 @@ export class LlEmitter {
|
||||
const parts = this.emitModuleParts();
|
||||
if (!this.executableTls) return parts;
|
||||
// A worker executable's thread-locals all live in the executable's own
|
||||
// TLS block (the program's internal ones, the runtime's external ones),
|
||||
// so on ELF they can use the executable models: LLVM's default
|
||||
// general-dynamic sequence (relaxed by the linker to `mov %fs:0` plus
|
||||
// an add) becomes one %fs-relative access LLVM can fold into loads.
|
||||
// Darwin's thread-local variables have a single model.
|
||||
// TLS block: the program's internal ones, and the runtime's external
|
||||
// ones, because the runtime is always linked statically into the
|
||||
// executable. So on ELF they can all use the local-exec model: LLVM's
|
||||
// default general-dynamic sequence (relaxed by the linker to `mov %fs:0`
|
||||
// plus an add), and even initial-exec (relaxed to `mov $offset` plus an
|
||||
// indexed access), become one %fs-relative operand LLVM can fold into
|
||||
// loads and compares. Darwin's thread-local variables have one model.
|
||||
return parts.map((part) =>
|
||||
part
|
||||
.replace(
|
||||
@@ -1471,7 +1490,7 @@ export class LlEmitter {
|
||||
)
|
||||
.replace(
|
||||
/^(@[-$._A-Za-z0-9]+ = external )thread_local global /gm,
|
||||
"$1thread_local(initialexec) global ",
|
||||
"$1thread_local(localexec) global ",
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -3799,13 +3818,27 @@ export class LlEmitter {
|
||||
* return and stays eligible for tail-call and recursion elimination. */
|
||||
private emitUnwind(passThrough?: string): void {
|
||||
if (this.mod.workers && this.tryStack.length > 0) {
|
||||
this.declare(`declare zeroext i1 @scr_context_stopping()`);
|
||||
const stopped = this.B.tmp();
|
||||
this.B.line(`${stopped} = call zeroext i1 @scr_context_stopping()`);
|
||||
const stop = this.B.newLabel("context.stop");
|
||||
const ordinary = this.B.newLabel("context.throw");
|
||||
this.B.condBr(stopped, stop, ordinary);
|
||||
this.B.startBlock(stop);
|
||||
if (this.executableTls) {
|
||||
// A set stop bit unwinds while the active cell may still be clear:
|
||||
// install the termination sentinel before skipping the handlers.
|
||||
const word = this.loadExcFlag();
|
||||
const bit = this.B.tmp();
|
||||
const stopped = this.B.tmp();
|
||||
this.B.line(`${bit} = and i32 ${word}, 2`);
|
||||
this.B.line(`${stopped} = icmp ne i32 ${bit}, 0`);
|
||||
this.B.condBr(stopped, stop, ordinary);
|
||||
this.B.startBlock(stop);
|
||||
this.declare(`declare zeroext i1 @scr_context_checkpoint()`);
|
||||
this.B.line(`call zeroext i1 @scr_context_checkpoint()`);
|
||||
} else {
|
||||
this.declare(`declare zeroext i1 @scr_context_stopping()`);
|
||||
const stopped = this.B.tmp();
|
||||
this.B.line(`${stopped} = call zeroext i1 @scr_context_stopping()`);
|
||||
this.B.condBr(stopped, stop, ordinary);
|
||||
this.B.startBlock(stop);
|
||||
}
|
||||
const handlers = this.tryStack;
|
||||
this.tryStack = [];
|
||||
this.emitUnwind();
|
||||
@@ -3859,6 +3892,21 @@ export class LlEmitter {
|
||||
this.B.br(cleanup.label);
|
||||
}
|
||||
|
||||
/** Loads the running thread's exact pending word (ELF worker
|
||||
* executables, scr_exception.c): bit 0 mirrors the active cell, bit 1
|
||||
* requests the context's stop. It lives in the executable's static TLS
|
||||
* block, so the access is one thread-pointer-relative load. Monotonic by
|
||||
* default: a loop must not hoist the load out and miss a cross-thread
|
||||
* stop (see emitPendingCheck for the unordered tests). */
|
||||
private loadExcFlag(unordered = false): string {
|
||||
this.declare(`@scr_exc_flag = external thread_local(localexec) global i32, align 4`);
|
||||
const word = this.B.tmp();
|
||||
this.B.line(
|
||||
`${word} = load atomic i32, ptr @scr_exc_flag ${unordered ? "unordered" : "monotonic"}, align 4`,
|
||||
);
|
||||
return word;
|
||||
}
|
||||
|
||||
/** 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
|
||||
@@ -3886,8 +3934,9 @@ export class LlEmitter {
|
||||
* call's result temp must join its frame BEFORE this runs so the unwind
|
||||
* releases the dummy (NULL for refcounted kinds) harmlessly. A scalar
|
||||
* call result of the function's return type may be passed as the unwind's
|
||||
* dummy value (see emitUnwind). */
|
||||
emitPendingCheck(passThrough?: { name: string; type: IrType }): void {
|
||||
* dummy value (see emitUnwind). `poll` marks a worker termination poll
|
||||
* (function entry or loop budget), whose test must stay ordered. */
|
||||
emitPendingCheck(passThrough?: { name: string; type: IrType }, poll = false): void {
|
||||
const through =
|
||||
passThrough !== undefined &&
|
||||
this.llType(passThrough.type) === this.llType(this.currentReturnType) &&
|
||||
@@ -3909,16 +3958,34 @@ export class LlEmitter {
|
||||
return;
|
||||
}
|
||||
// Executables test the active exception cell's kind inline (its first
|
||||
// field). Worker programs test the process-wide alert word instead: it
|
||||
// is nonzero while any script thread may have an exception pending in
|
||||
// its active cell or has been asked to stop, so the common case is one
|
||||
// ordinary global load (a thread-local access is a call on Darwin) and
|
||||
// the out-of-line scr_exc_pending answers for this thread, observing
|
||||
// cancellation and reinstalling the termination sentinel.
|
||||
// field). ELF worker executables test the thread's exact pending word,
|
||||
// which is also nonzero once the context must stop, and unwind directly:
|
||||
// no site carries an out-of-line call, so LLVM's inliner sees the check
|
||||
// as two instructions. Other worker programs test the process-wide
|
||||
// alert word: it is nonzero while any script thread may have an
|
||||
// exception pending in its active cell or has been asked to stop, so
|
||||
// the common case is one ordinary global load (a thread-local access is
|
||||
// a call on Darwin) and the out-of-line scr_exc_pending answers for this
|
||||
// thread, observing cancellation and reinstalling the termination
|
||||
// sentinel.
|
||||
this.declare(`declare i1 @llvm.expect.i1(i1, i1)`);
|
||||
let word: string;
|
||||
if (this.mod.workers === true) {
|
||||
this.declare(`@scr_exc_alert = external global i32, align 64`);
|
||||
const exact = this.mod.workers !== true || this.executableTls;
|
||||
if (this.executableTls) {
|
||||
// Termination stays observable where the polls were dropped for it:
|
||||
// polls, tests inside source loops (exc's loop-poll rule relies on
|
||||
// them every iteration) and tail-position tests (a dead ordered load
|
||||
// keeps recursion from becoming a loop without stack growth) use an
|
||||
// ordered load, which LLVM never deletes, hoists or merges. Every
|
||||
// other test is an unordered load: it reads the same word, but LLVM's
|
||||
// memory analyses no longer treat it as a write, so loads around it
|
||||
// stay forwardable after inlining. Same-thread changes only happen in
|
||||
// runtime calls, which clobber memory.
|
||||
word = this.loadExcFlag(!poll && through === undefined && this.loopDepth === 0);
|
||||
} else if (this.mod.workers === true) {
|
||||
// dso_local: executables link the runtime statically, so the word is
|
||||
// addressed directly instead of through the GOT.
|
||||
this.declare(`@scr_exc_alert = external dso_local global i32, align 64`);
|
||||
word = B.tmp();
|
||||
B.line(`${word} = load atomic i32, ptr @scr_exc_alert monotonic, align 64`);
|
||||
} else {
|
||||
@@ -3946,7 +4013,7 @@ export class LlEmitter {
|
||||
B.line(`${cold} = call i1 @llvm.expect.i1(i1 ${hit}, i1 false)`);
|
||||
const lu = B.newLabel("exc.u");
|
||||
const lk = B.newLabel("exc.k");
|
||||
if (this.mod.workers === true) {
|
||||
if (!exact) {
|
||||
this.declare(`declare zeroext i1 @scr_exc_pending()`);
|
||||
const slow = B.newLabel("exc.s");
|
||||
B.condBr(cold, slow, lk);
|
||||
@@ -3978,30 +4045,98 @@ export class LlEmitter {
|
||||
* throw Node's RangeError and unwind like any other pending exception. */
|
||||
private emitStackCheck(): void {
|
||||
const B = this.B;
|
||||
const tl = this.mod.workers === true ? "thread_local " : "";
|
||||
this.declare(`@scr_stack_guard = external ${tl}global ptr`);
|
||||
this.declare(`declare ptr @llvm.stacksave.p0()`);
|
||||
this.declare(`declare i1 @llvm.expect.i1(i1, i1)`);
|
||||
this.declare(`declare void @scr_stack_overflow()`);
|
||||
const sp = B.tmp();
|
||||
const guard = B.tmp();
|
||||
const low = B.tmp();
|
||||
const cold = B.tmp();
|
||||
B.line(`${sp} = call ptr @llvm.stacksave.p0()`);
|
||||
B.line(`${guard} = load ptr, ptr @scr_stack_guard`);
|
||||
B.line(`${low} = icmp ult ptr ${sp}, ${guard}`);
|
||||
B.line(`${cold} = call i1 @llvm.expect.i1(i1 ${low}, i1 false)`);
|
||||
const over = B.newLabel("stack.over");
|
||||
const ok = B.newLabel("stack.ok");
|
||||
B.condBr(cold, over, ok);
|
||||
const sp = B.tmp();
|
||||
B.line(`${sp} = call ptr @llvm.stacksave.p0()`);
|
||||
const test = (guard: string): void => {
|
||||
const low = B.tmp();
|
||||
const cold = B.tmp();
|
||||
B.line(`${low} = icmp ult ptr ${sp}, ${guard}`);
|
||||
B.line(`${cold} = call i1 @llvm.expect.i1(i1 ${low}, i1 false)`);
|
||||
B.condBr(cold, over, ok);
|
||||
};
|
||||
if (this.stackGuardTsd !== null) {
|
||||
// A Darwin thread-local access is a call to its TLV thunk: read the
|
||||
// guard from its thread-specific-data slot at the runtime-validated
|
||||
// offset instead, and call out only when the runtime found no slot.
|
||||
this.declare(`@scr_stack_guard_tsd = external dso_local global i64`);
|
||||
this.declare(`declare i64 @scr_stack_guard_get()`);
|
||||
const offset = B.tmp();
|
||||
const direct = B.tmp();
|
||||
const likely = B.tmp();
|
||||
B.line(`${offset} = load i64, ptr @scr_stack_guard_tsd`);
|
||||
B.line(`${direct} = icmp ne i64 ${offset}, 0`);
|
||||
B.line(`${likely} = call i1 @llvm.expect.i1(i1 ${direct}, i1 true)`);
|
||||
const fast = B.newLabel("stack.tsd");
|
||||
const slow = B.newLabel("stack.tlv");
|
||||
B.condBr(likely, fast, slow);
|
||||
B.startBlock(fast);
|
||||
const guard = B.tmp();
|
||||
if (this.stackGuardTsd === "aarch64") {
|
||||
this.declare(`declare i64 @llvm.read_register.i64(metadata)`);
|
||||
const thread = B.tmp();
|
||||
const base = B.tmp();
|
||||
const slot = B.tmp();
|
||||
const address = B.tmp();
|
||||
B.line(`${thread} = call i64 @llvm.read_register.i64(metadata !{!"tpidrro_el0"})`);
|
||||
B.line(`${base} = and i64 ${thread}, -8`);
|
||||
B.line(`${slot} = add i64 ${base}, ${offset}`);
|
||||
B.line(`${address} = inttoptr i64 ${slot} to ptr`);
|
||||
B.line(`${guard} = load ptr, ptr ${address}`);
|
||||
} else {
|
||||
const address = B.tmp();
|
||||
B.line(`${address} = inttoptr i64 ${offset} to ptr addrspace(256)`);
|
||||
B.line(`${guard} = load ptr, ptr addrspace(256) ${address}`);
|
||||
}
|
||||
test(guard);
|
||||
B.startBlock(slow);
|
||||
// uintptr_t scr_stack_guard_get(void): an i64 in the C ABI.
|
||||
const raw = B.tmp();
|
||||
const fallback = B.tmp();
|
||||
B.line(`${raw} = call i64 @scr_stack_guard_get()`);
|
||||
B.line(`${fallback} = inttoptr i64 ${raw} to ptr`);
|
||||
test(fallback);
|
||||
} else {
|
||||
const tl = this.mod.workers === true ? "thread_local " : "";
|
||||
this.declare(`@scr_stack_guard = external ${tl}global ptr`);
|
||||
const guard = B.tmp();
|
||||
B.line(`${guard} = load ptr, ptr @scr_stack_guard`);
|
||||
test(guard);
|
||||
}
|
||||
B.startBlock(over);
|
||||
B.line(`call void @scr_stack_overflow()`);
|
||||
this.emitUnwind();
|
||||
B.startBlock(ok);
|
||||
}
|
||||
|
||||
private workerLoopBudget(): string | null {
|
||||
/** Whether the emitter follows this call with a pending test (the
|
||||
* conditions in expr-calls.ts). */
|
||||
private callTestsPending(e: IrExpr): boolean {
|
||||
switch (e.kind) {
|
||||
case "call":
|
||||
return this.mayThrow.has(e.callee);
|
||||
case "new":
|
||||
return this.mayThrow.has(`%${e.className}.constructor`);
|
||||
case "virtualCall":
|
||||
return this.mayThrowMethods.has(e.method);
|
||||
case "callValue":
|
||||
return this.indirectMayThrow;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private workerLoopBudget(
|
||||
loop: Extract<IrStmt, { kind: "while" | "doWhile" | "for" | "forOf" }>,
|
||||
): string | null {
|
||||
if (!this.mod.workers) return null;
|
||||
// A loop that runs a pending test on every iteration observes a
|
||||
// terminate request through it; a separate poll would only add work.
|
||||
if (loopTestsEveryIteration(loop, (e) => this.callTestsPending(e))) return null;
|
||||
const slot = this.B.slot();
|
||||
this.B.entryAllocas.push(`${slot} = alloca i32 ; worker cancellation budget`);
|
||||
this.B.line(`store i32 0, ptr ${slot}`);
|
||||
@@ -4026,7 +4161,7 @@ export class LlEmitter {
|
||||
// Calls and throwing operations retain their own checks. Amortize the
|
||||
// extra cancellation poll across bounded loop batches so scalar work
|
||||
// does not cross the runtime ABI on every iteration.
|
||||
this.emitPendingCheck();
|
||||
this.emitPendingCheck(undefined, true);
|
||||
B.br(next);
|
||||
B.startBlock(next);
|
||||
}
|
||||
@@ -5022,6 +5157,7 @@ export class LlEmitter {
|
||||
this.chainSlots.clear();
|
||||
this.finallyStack = [];
|
||||
this.tryStack = [];
|
||||
this.loopDepth = 0;
|
||||
this.currentReturnType = fn.returnType;
|
||||
this.currentGenerator = fn.generator ?? null;
|
||||
this.currentBorrowedReturn = this.borrowedReturns.has(fn.name);
|
||||
@@ -5190,7 +5326,7 @@ export class LlEmitter {
|
||||
B.returnEpilogue = `call void @scr_stack_leave(ptr %source_frame)`;
|
||||
}
|
||||
if (this.stackChecks?.has(fn.name)) this.emitStackCheck();
|
||||
if (this.workerEntryPolls?.has(fn.name)) this.emitPendingCheck();
|
||||
if (this.workerEntryPolls?.has(fn.name)) this.emitPendingCheck(undefined, true);
|
||||
this.emitStmts(fn.body);
|
||||
// Implicit exit of a void function: release the function scope unless
|
||||
// the body already terminated its final block (return, or a throw
|
||||
@@ -5407,10 +5543,14 @@ export class LlEmitter {
|
||||
emitStmt(s: IrStmt): void {
|
||||
const previous = this.B.debugLocation;
|
||||
this.B.debugLocation = this.debug?.location(s.loc, this.debugScope) ?? null;
|
||||
const loop =
|
||||
s.kind === "while" || s.kind === "doWhile" || s.kind === "for" || s.kind === "forOf";
|
||||
if (loop) this.loopDepth++;
|
||||
try {
|
||||
this.emitStmtBody(s);
|
||||
} finally {
|
||||
this.B.debugLocation = previous;
|
||||
if (loop) this.loopDepth--;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5980,7 +6120,7 @@ export class LlEmitter {
|
||||
break;
|
||||
}
|
||||
case "while": {
|
||||
const workerBudget = this.workerLoopBudget();
|
||||
const workerBudget = this.workerLoopBudget(s);
|
||||
const lc = B.newLabel("loop.c");
|
||||
const lb = B.newLabel("loop.b");
|
||||
const le = B.newLabel("loop.e");
|
||||
@@ -6005,7 +6145,7 @@ export class LlEmitter {
|
||||
break;
|
||||
}
|
||||
case "doWhile": {
|
||||
const workerBudget = this.workerLoopBudget();
|
||||
const workerBudget = this.workerLoopBudget(s);
|
||||
// Body first (runs at least once); continue jumps to the CONDITION.
|
||||
const lb = B.newLabel("loop.b");
|
||||
const lc = B.newLabel("loop.c");
|
||||
@@ -6209,7 +6349,7 @@ export class LlEmitter {
|
||||
};
|
||||
const lu = s.update || capturedLets.length > 0 ? B.newLabel("loop.u") : lc;
|
||||
freshenBindings();
|
||||
const workerBudget = this.workerLoopBudget();
|
||||
const workerBudget = this.workerLoopBudget(s);
|
||||
B.br(lc);
|
||||
B.startBlock(lc);
|
||||
this.emitWorkerLoopCheck(workerBudget);
|
||||
@@ -6312,7 +6452,7 @@ export class LlEmitter {
|
||||
const lb = B.newLabel("fof.b");
|
||||
const lu = B.newLabel("fof.u");
|
||||
const le = B.newLabel("fof.e");
|
||||
const workerBudget = this.workerLoopBudget();
|
||||
const workerBudget = this.workerLoopBudget(s);
|
||||
B.br(lc);
|
||||
B.startBlock(lc);
|
||||
this.emitWorkerLoopCheck(workerBudget);
|
||||
|
||||
@@ -0,0 +1,94 @@
|
||||
import { expect, test } from "vitest";
|
||||
import { BOOL, F64, VOID, type IrExpr, type IrStmt } from "../ir/ir.js";
|
||||
import { loopTestsEveryIteration } from "./loop-polls.js";
|
||||
|
||||
const loc = { file: "loops.ts", start: 0, end: 1 };
|
||||
const call = (callee: string): IrExpr => ({ kind: "call", callee, args: [], type: F64, loc });
|
||||
const stmt = (expr: IrExpr): IrStmt => ({ kind: "exprStmt", expr, loc });
|
||||
const yes: IrExpr = { kind: "boolLit", value: true, type: BOOL, loc };
|
||||
const tested = (e: IrExpr): boolean => e.kind === "call" && e.callee === "throws";
|
||||
const whileLoop = (body: IrStmt[], cond: IrExpr = yes, labels?: string[]): IrStmt => ({
|
||||
kind: "while",
|
||||
cond,
|
||||
body,
|
||||
loc,
|
||||
...(labels ? { labels } : {}),
|
||||
});
|
||||
const answer = (loop: IrStmt): boolean =>
|
||||
loopTestsEveryIteration(loop as Parameters<typeof loopTestsEveryIteration>[0], tested);
|
||||
const ifThen = (body: IrStmt[]): IrStmt => ({
|
||||
kind: "if",
|
||||
cond: yes,
|
||||
then: body,
|
||||
else_: null,
|
||||
loc,
|
||||
});
|
||||
const sum = (left: IrExpr, right: IrExpr): IrExpr => ({
|
||||
kind: "bin",
|
||||
op: "+",
|
||||
left,
|
||||
right,
|
||||
type: F64,
|
||||
loc,
|
||||
});
|
||||
|
||||
test("an unconditional tested call in the body or condition replaces the poll", () => {
|
||||
expect(answer(whileLoop([stmt(call("throws"))]))).toBe(true);
|
||||
expect(answer(whileLoop([stmt(sum(call("pure"), call("throws")))]))).toBe(true);
|
||||
expect(answer(whileLoop([], { kind: "toBool", operand: call("throws"), type: BOOL, loc }))).toBe(
|
||||
true,
|
||||
);
|
||||
// Untested calls and calls behind a condition or short circuit do not.
|
||||
expect(answer(whileLoop([stmt(call("pure"))]))).toBe(false);
|
||||
expect(answer(whileLoop([ifThen([stmt(call("throws"))])]))).toBe(false);
|
||||
const shortCircuit: IrExpr = {
|
||||
kind: "logical",
|
||||
op: "&&",
|
||||
left: yes,
|
||||
right: call("throws"),
|
||||
type: BOOL,
|
||||
loc,
|
||||
};
|
||||
expect(answer(whileLoop([stmt(shortCircuit)]))).toBe(false);
|
||||
});
|
||||
|
||||
test("a continue before the tested call keeps the poll", () => {
|
||||
const skip: IrStmt = { kind: "continue", loc };
|
||||
expect(answer(whileLoop([ifThen([skip]), stmt(call("throws"))]))).toBe(false);
|
||||
// After the call, or bound to a nested loop, a continue is harmless.
|
||||
expect(answer(whileLoop([stmt(call("throws")), ifThen([skip])]))).toBe(true);
|
||||
expect(answer(whileLoop([whileLoop([skip]), stmt(call("throws"))]))).toBe(true);
|
||||
// A labeled continue out of a nested loop still skips this iteration.
|
||||
const labeled: IrStmt = { kind: "continue", label: "outer", loc };
|
||||
expect(answer(whileLoop([whileLoop([labeled]), stmt(call("throws"))], yes, ["outer"]))).toBe(
|
||||
false,
|
||||
);
|
||||
});
|
||||
|
||||
test("for updates and do-while conditions run after every continue", () => {
|
||||
const skip: IrStmt = { kind: "continue", loc };
|
||||
const forLoop: IrStmt = {
|
||||
kind: "for",
|
||||
init: null,
|
||||
cond: null,
|
||||
update: stmt(call("throws")),
|
||||
body: [ifThen([skip])],
|
||||
loc,
|
||||
};
|
||||
expect(answer(forLoop)).toBe(true);
|
||||
const doWhile: IrStmt = {
|
||||
kind: "doWhile",
|
||||
body: [ifThen([skip])],
|
||||
cond: { kind: "toBool", operand: call("throws"), type: BOOL, loc },
|
||||
loc,
|
||||
};
|
||||
expect(answer(doWhile)).toBe(true);
|
||||
const forOf: IrStmt = {
|
||||
kind: "forOf",
|
||||
localId: "x",
|
||||
iterable: { kind: "varRef", localId: "xs", type: { kind: "array", elem: F64 }, loc },
|
||||
body: [stmt({ kind: "call", callee: "throws", args: [], type: VOID, loc })],
|
||||
loc,
|
||||
} as IrStmt;
|
||||
expect(answer(forOf)).toBe(true);
|
||||
});
|
||||
@@ -0,0 +1,107 @@
|
||||
/* Which worker-program loops can skip their termination poll. */
|
||||
import type { IrExpr, IrStmt } from "../ir/ir.js";
|
||||
import { everyExprChild, everyStmtChild } from "../ir/traverse.js";
|
||||
|
||||
type Loop = Extract<IrStmt, { kind: "while" | "doWhile" | "for" | "forOf" }>;
|
||||
|
||||
/** Expression kinds whose operands all evaluate whenever the expression does
|
||||
* (no short-circuit, deferred or conditional operand). */
|
||||
const EAGER_EXPRS: ReadonlySet<string> = new Set([
|
||||
"call",
|
||||
"new",
|
||||
"virtualCall",
|
||||
"callValue",
|
||||
"bin",
|
||||
"unary",
|
||||
"toBool",
|
||||
"strConcat",
|
||||
"strEq",
|
||||
"strCmp",
|
||||
"assignExpr",
|
||||
"fieldGet",
|
||||
"fieldIncDec",
|
||||
"arrayGet",
|
||||
"recordGet",
|
||||
"upcast",
|
||||
"downcast",
|
||||
"unionWrap",
|
||||
"dynFrom",
|
||||
]);
|
||||
|
||||
/** Statement kinds whose expressions all evaluate whenever the statement
|
||||
* does. */
|
||||
const EAGER_STMTS: ReadonlySet<string> = new Set([
|
||||
"varDecl",
|
||||
"assign",
|
||||
"exprStmt",
|
||||
"arraySet",
|
||||
"bytesSet",
|
||||
"fieldSet",
|
||||
"recordSet",
|
||||
"return",
|
||||
"throw",
|
||||
]);
|
||||
|
||||
/** True when every iteration of `loop` evaluates a call that the emitter
|
||||
* follows with a pending test (`tested`). Worker loops poll for termination
|
||||
* every 64 iterations (emitWorkerLoopCheck) so that a loop whose body calls
|
||||
* nothing that tests still observes a terminate request; a pending test reads
|
||||
* the same alert word, so a loop that always runs one needs no extra poll.
|
||||
* The tested call must be unconditional: in the condition (while, for), in
|
||||
* the update or the do-while condition (both run after `continue` too), or
|
||||
* in the body before any statement that can `continue` this loop. */
|
||||
export function loopTestsEveryIteration(loop: Loop, tested: (call: IrExpr) => boolean): boolean {
|
||||
const evaluates = (e: IrExpr): boolean => {
|
||||
if (tested(e)) return true;
|
||||
if (!EAGER_EXPRS.has(e.kind)) return false;
|
||||
let found = false;
|
||||
everyExprChild(
|
||||
e,
|
||||
(child) => !(found = evaluates(child)),
|
||||
() => true,
|
||||
);
|
||||
return found;
|
||||
};
|
||||
const statementEvaluates = (s: IrStmt): boolean => {
|
||||
if (s.kind === "if") return evaluates(s.cond);
|
||||
if (s.kind === "block") return s.body.some(statementEvaluates);
|
||||
if (!EAGER_STMTS.has(s.kind)) return false;
|
||||
let found = false;
|
||||
everyStmtChild(
|
||||
s,
|
||||
(child) => !(found = evaluates(child)),
|
||||
() => true,
|
||||
);
|
||||
return found;
|
||||
};
|
||||
const labels = new Set(loop.labels ?? []);
|
||||
// Can `s` jump to this loop's next iteration? Unlabeled continues bind to
|
||||
// the innermost enclosing loop; labeled ones may skip out of nested loops.
|
||||
const continues = (s: IrStmt, nested: boolean): boolean => {
|
||||
if (s.kind === "continue") return s.label === undefined ? !nested : labels.has(s.label);
|
||||
const inner =
|
||||
nested ||
|
||||
s.kind === "while" ||
|
||||
s.kind === "doWhile" ||
|
||||
s.kind === "for" ||
|
||||
s.kind === "forOf";
|
||||
let found = false;
|
||||
everyStmtChild(
|
||||
s,
|
||||
() => true,
|
||||
(child) => !(found = continues(child, inner)),
|
||||
);
|
||||
return found;
|
||||
};
|
||||
if (loop.kind === "while" && evaluates(loop.cond)) return true;
|
||||
if (loop.kind === "doWhile" && evaluates(loop.cond)) return true;
|
||||
if (loop.kind === "for") {
|
||||
if (loop.cond !== null && evaluates(loop.cond)) return true;
|
||||
if (loop.update !== null && statementEvaluates(loop.update)) return true;
|
||||
}
|
||||
for (const s of loop.body) {
|
||||
if (continues(s, false)) return false;
|
||||
if (statementEvaluates(s)) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
@@ -437,6 +437,38 @@ test("stack checks cover recursive components and recursion through reentrant op
|
||||
expect(computeMayThrow(mod).fns.size).toBe(0);
|
||||
});
|
||||
|
||||
test("stack-checked synchronous functions skip the worker entry poll", () => {
|
||||
const asyncSelf: IrFunction = { ...fn("asyncSelf", [call("asyncSelf")], []), async: true };
|
||||
const mod: IrModule = {
|
||||
...moduleWith(
|
||||
fn("caller", [call("caller"), call("self"), call("left"), call("walker")], []),
|
||||
fn("self", [call("self")], []),
|
||||
fn("left", [call("right")], []),
|
||||
fn("right", [call("left")], []),
|
||||
fn("walker", [callClosure("leaf")], []),
|
||||
fn("leaf", [exprStmt(value)], []),
|
||||
asyncSelf,
|
||||
),
|
||||
workers: true,
|
||||
};
|
||||
const answer = computeMayThrow(mod, { stackChecks: true });
|
||||
expect([...answer.stackChecks!].sort()).toEqual(["asyncSelf", "caller", "left", "right", "self"]);
|
||||
// Callers test after every call of a stack-checked function, and its
|
||||
// overflow unwinds through those tests, so only the module entry, async
|
||||
// bodies and unbounded functions without a stack check poll on entry.
|
||||
expect([...answer.workerEntryPolls!].sort()).toEqual(["asyncSelf", "caller", "walker"]);
|
||||
expect(answer.fns.has("self") && answer.fns.has("left") && answer.fns.has("right")).toBe(true);
|
||||
// Without stack checks (WASI, Windows, libraries) every cycle keeps its poll.
|
||||
expect([...computeMayThrow(mod).workerEntryPolls!].sort()).toEqual([
|
||||
"asyncSelf",
|
||||
"caller",
|
||||
"left",
|
||||
"right",
|
||||
"self",
|
||||
"walker",
|
||||
]);
|
||||
});
|
||||
|
||||
test("class members are reachable from reentrant operations", () => {
|
||||
const mod: IrModule = {
|
||||
...moduleWith(
|
||||
|
||||
@@ -429,7 +429,7 @@ export function computeMayThrow(
|
||||
fns: may,
|
||||
...(why !== null ? { why } : {}),
|
||||
indirect,
|
||||
...(terminations ? { workerEntryPolls: terminations.entry } : {}),
|
||||
...(terminations ? { workerEntryPolls: entryPolls(mod, terminations.entry, stackChecks) } : {}),
|
||||
...(stackChecks ? { stackChecks } : {}),
|
||||
};
|
||||
}
|
||||
@@ -512,6 +512,31 @@ function describeThrowingNode(rec: IrExpr | IrStmt): string {
|
||||
}
|
||||
}
|
||||
|
||||
/** The termination polls a worker program places on function entry. A
|
||||
* stack-checked synchronous function needs none: it may throw (the stack
|
||||
* guard seeds the fixpoint), so every caller tests the pending state after
|
||||
* the call, and a descent that never returns ends at the stack guard, whose
|
||||
* RangeError unwinds through those tests. An outstanding terminate request
|
||||
* keeps the alert word nonzero, so the first test that runs afterwards (a
|
||||
* post-call test, a loop poll or the unwinding) stops the thread; dropping
|
||||
* the entry poll only delays the stop by bounded work, which Node's
|
||||
* asynchronous terminate allows. The entry poll's slow path rejoins the
|
||||
* body, so every polled function kept its arguments live across a call. */
|
||||
function entryPolls(
|
||||
mod: IrModule,
|
||||
entry: Set<string>,
|
||||
stackChecks: Set<string> | null,
|
||||
): Set<string> {
|
||||
if (stackChecks === null) return entry;
|
||||
// Calling an async or generator function never tests the caller's pending
|
||||
// state, and the module entry has no caller: those keep their polls.
|
||||
const keep = new Set(
|
||||
mod.functions.filter((fn) => fn.async || fn.generator !== undefined).map((fn) => fn.name),
|
||||
);
|
||||
keep.add(mod.entry);
|
||||
return new Set([...entry].filter((name) => keep.has(name) || !stackChecks.has(name)));
|
||||
}
|
||||
|
||||
/** Statement and expression kinds that cannot reenter scriptc code except
|
||||
* through the direct call edges recorded below (`call`, `new`,
|
||||
* `virtualCall`), and cannot block in the runtime. Everything else, such as
|
||||
|
||||
@@ -14530,12 +14530,6 @@
|
||||
],
|
||||
"diags": []
|
||||
},
|
||||
"<repo>/tests/corpus/native-worker-arena-reuse.ts": {
|
||||
"order": [
|
||||
"<repo>/tests/corpus/native-worker-arena-reuse.ts"
|
||||
],
|
||||
"diags": []
|
||||
},
|
||||
"<repo>/tests/corpus/native-worker-buffer-consumers.ts": {
|
||||
"order": [
|
||||
"<repo>/tests/corpus/native-worker-buffer-consumers.ts"
|
||||
@@ -15749,6 +15743,30 @@
|
||||
],
|
||||
"diags": []
|
||||
},
|
||||
"<repo>/tests/corpus/native-worker-terminate-tested-loops.ts": {
|
||||
"order": [
|
||||
"<repo>/tests/corpus/native-worker-terminate-tested-loops.ts"
|
||||
],
|
||||
"diags": []
|
||||
},
|
||||
"<repo>/tests/corpus/native-worker-exception-state.ts": {
|
||||
"order": [
|
||||
"<repo>/tests/corpus/native-worker-exception-state.ts"
|
||||
],
|
||||
"diags": []
|
||||
},
|
||||
"<repo>/tests/corpus/native-worker-terminate-exception-state.ts": {
|
||||
"order": [
|
||||
"<repo>/tests/corpus/native-worker-terminate-exception-state.ts"
|
||||
],
|
||||
"diags": []
|
||||
},
|
||||
"<repo>/tests/corpus/native-worker-arena-reuse.ts": {
|
||||
"order": [
|
||||
"<repo>/tests/corpus/native-worker-arena-reuse.ts"
|
||||
],
|
||||
"diags": []
|
||||
},
|
||||
"<repo>/tests/corpus/4598-map-set-clear-reuse.ts": {
|
||||
"order": [
|
||||
"<repo>/tests/corpus/4598-map-set-clear-reuse.ts"
|
||||
@@ -15797,12 +15815,6 @@
|
||||
],
|
||||
"diags": []
|
||||
},
|
||||
"<repo>/tests/corpus/4598-os-available-parallelism.ts": {
|
||||
"order": [
|
||||
"<repo>/tests/corpus/4598-os-available-parallelism.ts"
|
||||
],
|
||||
"diags": []
|
||||
},
|
||||
"<repo>/tests/corpus/4598-live-array-ref-element-access.ts": {
|
||||
"order": [
|
||||
"<repo>/tests/corpus/4598-live-array-ref-element-access.ts"
|
||||
@@ -15835,6 +15847,19 @@
|
||||
],
|
||||
"diags": []
|
||||
},
|
||||
"<repo>/tests/corpus/4598-os-available-parallelism.ts": {
|
||||
"order": [
|
||||
"<repo>/tests/corpus/4598-os-available-parallelism.ts"
|
||||
],
|
||||
"diags": []
|
||||
},
|
||||
"<repo>/tests/corpus/constant-module-globals/main.ts": {
|
||||
"order": [
|
||||
"<repo>/tests/corpus/constant-module-globals/consts.ts",
|
||||
"<repo>/tests/corpus/constant-module-globals/main.ts"
|
||||
],
|
||||
"diags": []
|
||||
},
|
||||
"<repo>/tests/corpus/4630-contract-int32.ts": {
|
||||
"order": [
|
||||
"<repo>/tests/corpus/4630-contract-int32.ts"
|
||||
|
||||
@@ -179,7 +179,7 @@ settle().then((value) => console.log(value));
|
||||
// Worker runtimes keep one small-object allocator per script thread:
|
||||
// the inline paths address its state, live counts and dispose hook in
|
||||
// the runtime's one thread-local ScrThreadHot block.
|
||||
expect(llvm).toMatch(/@scr_thread_hot = external thread_local(\(initialexec\))? global /);
|
||||
expect(llvm).toMatch(/@scr_thread_hot = external thread_local(\(localexec\))? global /);
|
||||
expect(llvm).not.toMatch(/@scr_(sa|cyc_live|cyc_old_freed|weak_dispose_hook) = external/);
|
||||
expect(llvm).toContain("call ptr @llvm.threadlocal.address.p0(ptr @scr_thread_hot)");
|
||||
expect(llvm).toMatch(/getelementptr inbounds i8, ptr %[\w.]+\.tl\.hot, i64 808/);
|
||||
@@ -228,33 +228,92 @@ test("worker entries without a source file stay refused", async () => {
|
||||
);
|
||||
});
|
||||
|
||||
test("worker programs poll inline and skip termination polls in bounded functions", async () => {
|
||||
const request = await fixture(
|
||||
worker +
|
||||
`
|
||||
test.each([
|
||||
["arm64-apple-macosx14.0.0", "alert"],
|
||||
["x86_64-unknown-linux-gnu", "flag"],
|
||||
["aarch64-unknown-linux-gnu", "flag"],
|
||||
])(
|
||||
"worker programs for %s poll inline (%s) and skip termination polls in bounded functions",
|
||||
async (target, lane) => {
|
||||
vi.stubEnv("SCRIPTC_TARGET", target);
|
||||
const request = await fixture(
|
||||
worker +
|
||||
`
|
||||
function mix(a: number, b: number): number { return (a * 31 + b) % 1000003; }
|
||||
function spin(n: number): number { return n < 2 ? mix(n, 1) : spin(n - 1) + spin(n - 2); }
|
||||
console.log(mix(1, 2), spin(5));
|
||||
`,
|
||||
);
|
||||
const result = await compile(request.entry, request);
|
||||
expect(result.ok, JSON.stringify(result)).toBe(true);
|
||||
if (result.ok) {
|
||||
const llvm = await readFile(request.outPath, "utf8");
|
||||
const body = (name: string): string => {
|
||||
const start = llvm.search(new RegExp(`^define [^\\n]*@${name}\\(`, "m"));
|
||||
expect(start).toBeGreaterThanOrEqual(0);
|
||||
return llvm.slice(start, llvm.indexOf("\n}\n", start));
|
||||
};
|
||||
// A bounded helper neither polls on entry nor makes its callers poll.
|
||||
expect(body("sc_f_mix")).not.toMatch(/@scr_exc_(alert|flag)/);
|
||||
// Recursion tests after its recursive calls (its stack check replaces
|
||||
// the entry poll) and never reads its thread-local cell inline.
|
||||
expect(body("sc_f_spin")).not.toContain("@scr_exc_active");
|
||||
if (lane === "alert") {
|
||||
// Darwin: the inline test reads the process-wide alert word (a
|
||||
// thread-local access is a call there), and only the slow path calls
|
||||
// into the runtime.
|
||||
expect(body("sc_f_spin")).toMatch(/load atomic i32, ptr @scr_exc_alert monotonic/);
|
||||
expect(body("sc_f_spin")).toContain("call zeroext i1 @scr_exc_pending()");
|
||||
expect(llvm).toMatch(/^@scr_exc_alert = external dso_local global i32, align 64$/m);
|
||||
} else {
|
||||
// ELF: the thread's exact pending word, one TLS load, no slow path.
|
||||
expect(body("sc_f_spin")).toMatch(
|
||||
/load atomic i32, ptr @scr_exc_flag (unordered|monotonic)/,
|
||||
);
|
||||
expect(body("sc_f_spin")).not.toContain("@scr_exc_pending");
|
||||
expect(llvm).not.toContain("@scr_exc_alert");
|
||||
expect(llvm).toMatch(
|
||||
/^@scr_exc_flag = external thread_local\(localexec\) global i32, align 4$/m,
|
||||
);
|
||||
}
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
test.each([
|
||||
["arm64-apple-macosx14.0.0", "tpidrro"],
|
||||
["x86_64-apple-macosx14.0.0", "gs"],
|
||||
["x86_64-unknown-linux-gnu", "tls"],
|
||||
])("worker stack checks for %s read the guard through %s", async (target, access) => {
|
||||
vi.stubEnv("SCRIPTC_TARGET", target);
|
||||
const request = await fixture(
|
||||
worker +
|
||||
"\nfunction depth(n: number): number { return n < 1 ? 0 : depth(n - 1) + 1; }\nconsole.log(depth(9));\n",
|
||||
);
|
||||
const result = await compile(request.entry, request);
|
||||
expect(result.ok, JSON.stringify(result)).toBe(true);
|
||||
if (result.ok) {
|
||||
const llvm = await readFile(request.outPath, "utf8");
|
||||
const body = (name: string): string => {
|
||||
const start = llvm.search(new RegExp(`^define [^\\n]*@${name}\\(`, "m"));
|
||||
expect(start).toBeGreaterThanOrEqual(0);
|
||||
return llvm.slice(start, llvm.indexOf("\n}\n", start));
|
||||
};
|
||||
// A bounded helper neither polls on entry nor makes its callers poll.
|
||||
expect(body("sc_f_mix")).not.toContain("@scr_exc_alert");
|
||||
// Recursion polls on entry: the inline test reads the process-wide
|
||||
// alert word (no thread-local access), and only the slow path calls into
|
||||
// the runtime.
|
||||
expect(body("sc_f_spin")).toMatch(/load atomic i32, ptr @scr_exc_alert monotonic/);
|
||||
expect(body("sc_f_spin")).toContain("call zeroext i1 @scr_exc_pending()");
|
||||
expect(body("sc_f_spin")).not.toContain("@scr_exc_active");
|
||||
expect(llvm).toMatch(/^@scr_exc_alert = external global i32, align 64$/m);
|
||||
const start = llvm.search(/^define [^\n]*@sc_f_depth\(/m);
|
||||
expect(start).toBeGreaterThanOrEqual(0);
|
||||
const depth = llvm.slice(start, llvm.indexOf("\n}\n", start));
|
||||
expect(depth).toContain("call ptr @llvm.stacksave.p0()");
|
||||
if (access === "tls") {
|
||||
// ELF: one thread-pointer-relative load of the thread-local guard.
|
||||
expect(depth).toContain("load ptr, ptr @scr_stack_guard");
|
||||
expect(depth).not.toContain("@scr_stack_guard_tsd");
|
||||
} else {
|
||||
// Darwin: a thread-local access calls the TLV thunk, so the guard comes
|
||||
// from its thread-specific-data slot, with an out-of-line fallback.
|
||||
expect(depth).not.toContain("ptr @scr_stack_guard\n");
|
||||
expect(depth).toContain("load i64, ptr @scr_stack_guard_tsd");
|
||||
expect(depth).toContain("call i64 @scr_stack_guard_get()");
|
||||
if (access === "tpidrro") {
|
||||
expect(depth).toContain('@llvm.read_register.i64(metadata !{!"tpidrro_el0"})');
|
||||
} else {
|
||||
expect(depth).toMatch(/load ptr, ptr addrspace\(256\) %/);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -269,14 +328,15 @@ test.each([
|
||||
expect(result.ok, JSON.stringify(result)).toBe(true);
|
||||
if (result.ok) {
|
||||
const llvm = await readFile(request.outPath, "utf8");
|
||||
// Every thread-local of a worker executable lives in its own TLS block:
|
||||
// ELF uses local-exec for the program's and initial-exec for the
|
||||
// runtime's; Darwin's thread-local variables have one model.
|
||||
// Every thread-local of a worker executable lives in its own TLS block
|
||||
// (the runtime links statically): ELF uses local-exec for the program's
|
||||
// and the runtime's; Darwin's thread-local variables have one model.
|
||||
const plain = /^@[-$._A-Za-z0-9]+ = (internal|external) thread_local global /m;
|
||||
if (models) {
|
||||
expect(llvm).not.toMatch(plain);
|
||||
expect(llvm).not.toContain("initialexec");
|
||||
expect(llvm).toMatch(/^@[-$._A-Za-z0-9]+ = internal thread_local\(localexec\) global /m);
|
||||
expect(llvm).toMatch(/^@[-$._A-Za-z0-9]+ = external thread_local\(initialexec\) global /m);
|
||||
expect(llvm).toMatch(/^@[-$._A-Za-z0-9]+ = external thread_local\(localexec\) global /m);
|
||||
} else {
|
||||
expect(llvm).toMatch(plain);
|
||||
expect(llvm).not.toContain("thread_local(");
|
||||
|
||||
@@ -1256,7 +1256,7 @@ static void scr_switch(ScrCtx *from, ScrCtx *to, ScrFiber *to_fiber) {
|
||||
/* The stack guard follows the running stack. A fresh fiber starts with
|
||||
* its own; every resumed context restores the guard it saved here. */
|
||||
uintptr_t guard = scr_stack_guard;
|
||||
if (to_fiber) scr_stack_guard = scr_stack_guard_for((uintptr_t)to_fiber->stack, SCR_FIBER_STACK);
|
||||
if (to_fiber) SCR_STACK_GUARD_SET(scr_stack_guard_for((uintptr_t)to_fiber->stack, SCR_FIBER_STACK));
|
||||
#endif
|
||||
#ifdef _WIN32
|
||||
/* SwitchToFiber snapshots the outgoing fiber's state inside its own
|
||||
@@ -1287,7 +1287,7 @@ static void scr_switch(ScrCtx *from, ScrCtx *to, ScrFiber *to_fiber) {
|
||||
swapcontext(from, to);
|
||||
#endif
|
||||
#if !defined(_WIN32) && !defined(__wasi__)
|
||||
scr_stack_guard = guard;
|
||||
SCR_STACK_GUARD_SET(guard);
|
||||
#endif
|
||||
}
|
||||
|
||||
@@ -1537,8 +1537,9 @@ static void scr_fiber_finish(ScrFiber *self) {
|
||||
}
|
||||
ScrPromise *p = self->promise;
|
||||
#ifdef SCR_WORKERS
|
||||
if (scr_context_stopping()) {
|
||||
scr_context_checkpoint();
|
||||
/* Checkpoint, not just test: the body may have unwound for a stop
|
||||
* request that its cell does not show yet. */
|
||||
if (scr_context_checkpoint()) {
|
||||
scr_promise_reject_from_cell(p, &self->exc);
|
||||
p->rejection_observed = true;
|
||||
self->done = true;
|
||||
|
||||
@@ -122,9 +122,19 @@ void scr_context_stop_flag(const void *flag) {
|
||||
if (!scr_context_stopped) scr_context_signal = flag ? flag : &scr_context_unsignaled;
|
||||
}
|
||||
bool scr_context_stopping(void) { return scr_context_stopped; }
|
||||
/* Asked to stop: the owner's signal, or (ELF worker executables) the stop
|
||||
* bit of the thread's exact pending word, which worker.terminate() sets
|
||||
* directly so that emitted checks observe it. */
|
||||
static bool scr_context_signaled(void) {
|
||||
#ifdef SCR_EXC_FLAG
|
||||
if (atomic_load_explicit(&scr_exc_flag, memory_order_acquire) & SCR_EXC_FLAG_STOP) return true;
|
||||
#endif
|
||||
return atomic_load_explicit(scr_context_signal, memory_order_relaxed);
|
||||
}
|
||||
|
||||
/* Neither stopping nor asked to stop. */
|
||||
bool scr_context_quiet(void) {
|
||||
return !scr_context_stopped && !atomic_load_explicit(scr_context_signal, memory_order_relaxed);
|
||||
return !scr_context_stopped && !scr_context_signaled();
|
||||
}
|
||||
|
||||
void scr_context_stop(int code) {
|
||||
@@ -137,8 +147,7 @@ void scr_context_stop(int code) {
|
||||
}
|
||||
|
||||
bool scr_context_checkpoint(void) {
|
||||
if (!scr_context_stopped && atomic_load_explicit(scr_context_signal, memory_order_relaxed))
|
||||
scr_context_stop(1);
|
||||
if (!scr_context_stopped && scr_context_signaled()) scr_context_stop(1);
|
||||
if (!scr_context_stopped) return false;
|
||||
/* Each fiber has its own exception cell. Reinstall the sentinel after a
|
||||
* context switch or a runtime continuation consumes its pending payload. */
|
||||
@@ -198,12 +207,56 @@ ScrDyn *scr_worker_parent_port(void) { return scr_dyn_new_null(); }
|
||||
#include <pthread.h>
|
||||
#endif
|
||||
|
||||
#ifdef SCR_STACK_GUARD_TSD
|
||||
/* The guard's thread-specific-data slot (scr_runtime.h). libpthread keeps a
|
||||
* thread's values in an array at its thread pointer, indexed by key, which
|
||||
* is also how its own inline accessors read them; the probe below checks
|
||||
* that layout once before compiled code relies on it. */
|
||||
uintptr_t scr_stack_guard_tsd = 0;
|
||||
static pthread_key_t scr_stack_guard_key;
|
||||
static bool scr_stack_guard_keyed;
|
||||
static pthread_once_t scr_stack_guard_once = PTHREAD_ONCE_INIT;
|
||||
|
||||
static void *scr_stack_guard_tsd_read(uintptr_t offset) {
|
||||
void *value;
|
||||
#if defined(__aarch64__)
|
||||
uintptr_t base;
|
||||
__asm__ volatile("mrs %0, TPIDRRO_EL0" : "=r"(base));
|
||||
value = *(void *volatile *)((base & ~(uintptr_t)7) + offset);
|
||||
#else
|
||||
__asm__ volatile("movq %%gs:(%1), %0" : "=r"(value) : "r"(offset));
|
||||
#endif
|
||||
return value;
|
||||
}
|
||||
|
||||
static void scr_stack_guard_key_init(void) {
|
||||
if (pthread_key_create(&scr_stack_guard_key, NULL) != 0) return;
|
||||
scr_stack_guard_keyed = true;
|
||||
static const char probe;
|
||||
uintptr_t offset = (uintptr_t)scr_stack_guard_key * sizeof(void *);
|
||||
if (offset != 0 && pthread_setspecific(scr_stack_guard_key, &probe) == 0 &&
|
||||
scr_stack_guard_tsd_read(offset) == &probe)
|
||||
scr_stack_guard_tsd = offset;
|
||||
(void)pthread_setspecific(scr_stack_guard_key, NULL);
|
||||
}
|
||||
|
||||
void scr_stack_guard_set(uintptr_t guard) {
|
||||
scr_stack_guard = guard;
|
||||
if (scr_stack_guard_keyed) (void)pthread_setspecific(scr_stack_guard_key, (void *)guard);
|
||||
}
|
||||
|
||||
uintptr_t scr_stack_guard_get(void) { return scr_stack_guard; }
|
||||
#endif
|
||||
|
||||
void scr_stack_guard_init(void) {
|
||||
#if defined(__APPLE__) && !defined(SCR_LIB)
|
||||
#ifdef SCR_STACK_GUARD_TSD
|
||||
(void)pthread_once(&scr_stack_guard_once, scr_stack_guard_key_init);
|
||||
#endif
|
||||
pthread_t self = pthread_self();
|
||||
uintptr_t high = (uintptr_t)pthread_get_stackaddr_np(self);
|
||||
size_t size = pthread_get_stacksize_np(self);
|
||||
if (high > size) scr_stack_guard = scr_stack_guard_for(high - size, size);
|
||||
if (high > size) SCR_STACK_GUARD_SET(scr_stack_guard_for(high - size, size));
|
||||
#elif defined(__linux__) && !defined(SCR_LIB)
|
||||
#ifdef SCR_WORKERS
|
||||
if (!scr_context_is_main()) {
|
||||
|
||||
@@ -1114,7 +1114,9 @@ void scr_run_exit_listeners(double code) {
|
||||
#endif
|
||||
scr_exit_ls[i].fn(scr_exit_ls[i].cb, code);
|
||||
#ifdef SCR_WORKERS
|
||||
if (scr_context_stopping()) break;
|
||||
/* The listener may have unwound for a stop request (checkpoint installs
|
||||
* the sentinel its cell may not show yet). */
|
||||
if (scr_context_checkpoint()) break;
|
||||
#endif
|
||||
if (scr_exc_pending()) {
|
||||
/* Node: a throw in an 'exit' listener is fatal (exit code 7). */
|
||||
|
||||
@@ -62,7 +62,53 @@ ScrExcCell *scr_exc_active = &scr_main_exc;
|
||||
* localized library archive would otherwise leave shared. */
|
||||
SCR_TL uintptr_t scr_stack_guard = 0;
|
||||
|
||||
#if defined(SCR_WORKERS) && !defined(SCR_LIB)
|
||||
#if defined(SCR_WORKERS) && !defined(SCR_LIB) && defined(SCR_EXC_FLAG)
|
||||
/* The thread's exact pending word, tested inline by every emitted pending
|
||||
* check in ELF worker executables (one %fs/tpidr-relative load: the word is
|
||||
* in the executable's static TLS block). Nonzero means the running code
|
||||
* must unwind, so the emitted check needs no out-of-line answer:
|
||||
*
|
||||
* - SCR_EXC_FLAG_PENDING mirrors `active cell kind != NONE` exactly. Only
|
||||
* the owner thread writes it, at every change of its active cell (throws,
|
||||
* rethrows, take/clear, fiber switches, payload moves, the termination
|
||||
* sentinel), because a stale bit would unwind a normal path.
|
||||
* - SCR_EXC_FLAG_STOP means the context must stop. worker.terminate() sets
|
||||
* it from the owner's thread (scr_exc_flag_request_stop, through the
|
||||
* pointer the worker registers while its script runs), scr_context_stop
|
||||
* sets it for process.exit() and fatal paths, and nothing clears it.
|
||||
*
|
||||
* A set STOP bit unwinds while the active cell may still be NONE: the
|
||||
* TERMINATE sentinel is installed by whoever consumes the unwind before it
|
||||
* inspects the cell. scr_exc_pending and scr_context_checkpoint do that,
|
||||
* emitted try regions test the bit before entering a handler, and the
|
||||
* runtime's unwind consumers (fiber completion, the uncaught reporters,
|
||||
* exit listeners) checkpoint first. */
|
||||
SCR_TL _Atomic uint32_t scr_exc_flag;
|
||||
|
||||
static inline void scr_exc_flag_pending(bool pending) {
|
||||
uint32_t word = atomic_load_explicit(&scr_exc_flag, memory_order_relaxed);
|
||||
if (((word & SCR_EXC_FLAG_PENDING) != 0) == pending) return;
|
||||
if (pending) atomic_fetch_or_explicit(&scr_exc_flag, SCR_EXC_FLAG_PENDING, memory_order_relaxed);
|
||||
else atomic_fetch_and_explicit(&scr_exc_flag, ~SCR_EXC_FLAG_PENDING, memory_order_relaxed);
|
||||
}
|
||||
|
||||
void scr_exc_flag_request_stop(_Atomic uint32_t *flag) {
|
||||
/* Release: a thread that observes the bit also observes the requester's
|
||||
* earlier writes (the worker's cancel flag). */
|
||||
atomic_fetch_or_explicit(flag, SCR_EXC_FLAG_STOP, memory_order_release);
|
||||
}
|
||||
|
||||
/* The active cell, or the context's stop state, may have changed. */
|
||||
void scr_exc_cell_changed(void) {
|
||||
if (!scr_context_quiet() &&
|
||||
!(atomic_load_explicit(&scr_exc_flag, memory_order_relaxed) & SCR_EXC_FLAG_STOP))
|
||||
atomic_fetch_or_explicit(&scr_exc_flag, SCR_EXC_FLAG_STOP, memory_order_relaxed);
|
||||
scr_exc_flag_pending(SCR_EXC_CUR()->kind != SCR_EXC_NONE);
|
||||
}
|
||||
|
||||
void scr_exc_alert_thread_exit(void) {}
|
||||
#define SCR_EXC_RAISED() scr_exc_flag_pending(true)
|
||||
#elif defined(SCR_WORKERS) && !defined(SCR_LIB)
|
||||
/* The process-wide exception alert, tested inline by every emitted pending
|
||||
* check in worker executables: one ordinary load, where reading the active
|
||||
* cell would be a thread-local access (a call on Darwin). It counts the
|
||||
@@ -186,7 +232,10 @@ bool(scr_exc_pending)(void) {
|
||||
if (scr_context_checkpoint()) return true;
|
||||
#endif
|
||||
bool pending = scr_exc_kind != SCR_EXC_NONE;
|
||||
#if defined(SCR_WORKERS) && !defined(SCR_LIB)
|
||||
#if defined(SCR_WORKERS) && !defined(SCR_LIB) && defined(SCR_EXC_FLAG)
|
||||
/* The pending bit is exact; settling it here is only a safety net. */
|
||||
scr_exc_flag_pending(pending);
|
||||
#elif defined(SCR_WORKERS) && !defined(SCR_LIB)
|
||||
/* The slow path of the alert test: settle this thread's share. */
|
||||
if (pending != scr_exc_alerted) scr_exc_cell_changed();
|
||||
#endif
|
||||
@@ -518,7 +567,9 @@ static SCR_TL bool scr_uncaught_handler_failed = false;
|
||||
|
||||
bool scr_exc_handle_uncaught(bool from_promise) {
|
||||
#ifdef SCR_WORKERS
|
||||
if (scr_context_stopping()) return false;
|
||||
/* Checkpoint, not just test: the unwind that brought control here may
|
||||
* have been a stop request the cell does not show yet. */
|
||||
if (scr_context_checkpoint()) return false;
|
||||
#endif
|
||||
if (!scr_exc_pending()) return true;
|
||||
if (!scr_uncaught_exception_hook || scr_uncaught_handler_failed) return false;
|
||||
@@ -554,7 +605,7 @@ static bool scr_uncaught_print_dyn_primitive(const ScrDyn *d) {
|
||||
|
||||
void scr_exc_print_uncaught(void) {
|
||||
#ifdef SCR_WORKERS
|
||||
if (scr_context_stopping()) { scr_exc_clear(); return; }
|
||||
if (scr_context_checkpoint()) { scr_exc_clear(); return; }
|
||||
if (scr_context_report_error) {
|
||||
scr_exit_code_note(scr_uncaught_handler_failed ? 7 : 1);
|
||||
scr_context_report_error();
|
||||
@@ -610,4 +661,5 @@ void scr_exc_print_uncaught(void) {
|
||||
}
|
||||
fputc('\n', stderr);
|
||||
scr_exc_reset(); /* the payload counts as live until released */
|
||||
scr_exc_cell_changed();
|
||||
}
|
||||
|
||||
@@ -290,11 +290,26 @@ void scr_context_stop(int code);
|
||||
bool scr_context_stopping(void);
|
||||
bool scr_context_checkpoint(void);
|
||||
bool scr_context_quiet(void);
|
||||
/* The process-wide exception alert that emitted pending checks of worker
|
||||
* executables test inline (scr_exception.c): nonzero while some script
|
||||
* thread may have an exception pending in its active cell or must stop. */
|
||||
#if !defined(__APPLE__) && !defined(_WIN32) && !defined(__CYGWIN__) && !defined(__wasi__)
|
||||
/* ELF worker executables (the LLVM emitter's executableTls targets): every
|
||||
* script thread owns an exact pending word in the executable's static TLS
|
||||
* block, which emitted pending checks load with one thread-pointer-relative
|
||||
* access and no out-of-line slow path (scr_exception.c). */
|
||||
#define SCR_EXC_FLAG 1
|
||||
#define SCR_EXC_FLAG_PENDING 1u /* the active cell's kind is not NONE */
|
||||
#define SCR_EXC_FLAG_STOP 2u /* the context must stop (never cleared) */
|
||||
extern SCR_TL _Atomic uint32_t scr_exc_flag;
|
||||
/* Requests the stop of the thread that owns `flag` (scr_worker.c holds the
|
||||
* owner's word only while its script runs). */
|
||||
void scr_exc_flag_request_stop(_Atomic uint32_t *flag);
|
||||
#else
|
||||
/* The process-wide exception alert that emitted pending checks of other
|
||||
* worker executables test inline (scr_exception.c): nonzero while some
|
||||
* script thread may have an exception pending in its active cell or must
|
||||
* stop. */
|
||||
extern _Atomic uint32_t scr_exc_alert;
|
||||
void scr_exc_alert_add(int delta); /* worker.terminate() references */
|
||||
#endif
|
||||
void scr_exc_alert_thread_exit(void);
|
||||
void scr_loop_context_shutdown(void);
|
||||
extern SCR_TL void (*scr_context_report_error)(void);
|
||||
@@ -322,6 +337,22 @@ bool scr_context_is_main(void);
|
||||
extern SCR_TL uintptr_t scr_stack_guard;
|
||||
void scr_stack_guard_init(void);
|
||||
void scr_stack_overflow(void);
|
||||
#if defined(SCR_WORKERS) && !defined(SCR_LIB) && defined(__APPLE__) && \
|
||||
(defined(__aarch64__) || defined(__x86_64__))
|
||||
/* Darwin worker executables: reading a thread-local variable calls its TLV
|
||||
* thunk, so the emitted entry checks read the running stack's guard from a
|
||||
* pthread thread-specific-data slot instead, addressed from the thread
|
||||
* pointer (TPIDRRO_EL0 on arm64, %gs on x86-64) plus this byte offset.
|
||||
* scr_stack_guard_init validates the slot on first use; zero means the
|
||||
* direct read is unavailable and the checks call scr_stack_guard_get. */
|
||||
#define SCR_STACK_GUARD_TSD 1
|
||||
extern uintptr_t scr_stack_guard_tsd;
|
||||
uintptr_t scr_stack_guard_get(void);
|
||||
void scr_stack_guard_set(uintptr_t guard);
|
||||
#define SCR_STACK_GUARD_SET(guard) scr_stack_guard_set(guard)
|
||||
#else
|
||||
#define SCR_STACK_GUARD_SET(guard) (scr_stack_guard = (guard))
|
||||
#endif
|
||||
#if defined(__has_feature)
|
||||
#if __has_feature(address_sanitizer)
|
||||
#define SCR_STACK_RESERVE_MAX ((size_t)2 << 20)
|
||||
|
||||
@@ -41,9 +41,17 @@ __attribute__((constructor)) static void scr_worker_reserve_fd_table(void) {
|
||||
struct ScrWorkerThread {
|
||||
atomic_size_t references;
|
||||
atomic_bool cancel;
|
||||
#ifdef SCR_EXC_FLAG
|
||||
/* The worker thread's exact pending word (scr_exception.c) while its
|
||||
* script runs, NULL before and after: terminate() sets the stop bit
|
||||
* through it. The lock keeps the thread's TLS alive across that write. */
|
||||
pthread_mutex_t flag_lock;
|
||||
_Atomic uint32_t *flag;
|
||||
#else
|
||||
/* The exception alert reference of a terminate request: 0 none, 1 held,
|
||||
* 2 the thread finished its script (no reference is taken any more). */
|
||||
atomic_int alert;
|
||||
#endif
|
||||
atomic_bool port_closed;
|
||||
uint64_t id;
|
||||
uint32_t root;
|
||||
@@ -92,6 +100,9 @@ void scr_worker_thread_release(ScrWorkerThread *worker) {
|
||||
scr_context_env_free(worker->environment);
|
||||
for (int i = 0; i < worker->argc; i++) free(worker->argv[i]);
|
||||
free(worker->argv);
|
||||
#ifdef SCR_EXC_FLAG
|
||||
pthread_mutex_destroy(&worker->flag_lock);
|
||||
#endif
|
||||
free(worker);
|
||||
}
|
||||
|
||||
@@ -126,6 +137,15 @@ uint64_t scr_worker_thread_id(const ScrWorkerThread *worker) { return worker->id
|
||||
|
||||
void scr_worker_thread_terminate(ScrWorkerThread *worker) {
|
||||
atomic_store_explicit(&worker->cancel, true, memory_order_relaxed);
|
||||
#ifdef SCR_EXC_FLAG
|
||||
/* Emitted checks test only the worker's own pending word: raise its stop
|
||||
* bit. A worker that has not registered its word yet finds the cancel
|
||||
* flag when it does (scr_worker_thread_run); one that has finished its
|
||||
* script needs nothing. */
|
||||
pthread_mutex_lock(&worker->flag_lock);
|
||||
if (worker->flag) scr_exc_flag_request_stop(worker->flag);
|
||||
pthread_mutex_unlock(&worker->flag_lock);
|
||||
#else
|
||||
/* Emitted polls only call into the runtime while the exception alert is
|
||||
* raised: hold a reference until the worker's script has ended. */
|
||||
scr_exc_alert_add(1);
|
||||
@@ -133,6 +153,7 @@ void scr_worker_thread_terminate(ScrWorkerThread *worker) {
|
||||
if (!atomic_compare_exchange_strong_explicit(&worker->alert, &idle, 1, memory_order_acq_rel,
|
||||
memory_order_acquire))
|
||||
scr_exc_alert_add(-1);
|
||||
#endif
|
||||
scr_mailbox_post(worker->inbox, scr_worker_event(SCR_WORKER_WAKE, 0, 0, NULL));
|
||||
}
|
||||
|
||||
@@ -177,6 +198,14 @@ static void scr_worker_report_error(void) {
|
||||
|
||||
static void scr_worker_thread_run(ScrWorkerThread *worker) {
|
||||
scr_context_enter(worker->id);
|
||||
#ifdef SCR_EXC_FLAG
|
||||
pthread_mutex_lock(&worker->flag_lock);
|
||||
worker->flag = &scr_exc_flag;
|
||||
pthread_mutex_unlock(&worker->flag_lock);
|
||||
/* A terminate() that ran before the registration only set the flag. */
|
||||
if (atomic_load_explicit(&worker->cancel, memory_order_relaxed))
|
||||
scr_exc_flag_request_stop(&scr_exc_flag);
|
||||
#endif
|
||||
scr_context_stop_flag(&worker->cancel);
|
||||
scr_context_env_enter(worker->environment);
|
||||
worker->environment = NULL;
|
||||
@@ -188,7 +217,13 @@ static void scr_worker_thread_run(ScrWorkerThread *worker) {
|
||||
scr_context_cleanup();
|
||||
scr_worker_current = NULL;
|
||||
scr_exc_alert_thread_exit();
|
||||
#ifdef SCR_EXC_FLAG
|
||||
pthread_mutex_lock(&worker->flag_lock);
|
||||
worker->flag = NULL;
|
||||
pthread_mutex_unlock(&worker->flag_lock);
|
||||
#else
|
||||
if (atomic_exchange_explicit(&worker->alert, 2, memory_order_acq_rel) == 1) scr_exc_alert_add(-1);
|
||||
#endif
|
||||
/* Cleanup may itself finish owner-bound native operations. Publish exit
|
||||
* only once none of those operations can touch the worker's script heap. */
|
||||
scr_mailbox_post(worker->parent, scr_worker_event(SCR_WORKER_EXIT, worker->id, code, NULL));
|
||||
@@ -216,7 +251,12 @@ ScrWorkerThread *scr_worker_thread_start(ScrWorkerEntry entry, uint32_t root,
|
||||
if (!worker) scr_trap("scriptc: out of memory\n");
|
||||
atomic_init(&worker->references, 2); /* owner and native thread */
|
||||
atomic_init(&worker->cancel, false);
|
||||
#ifdef SCR_EXC_FLAG
|
||||
if (pthread_mutex_init(&worker->flag_lock, NULL) != 0) scr_trap("scriptc: out of memory\n");
|
||||
worker->flag = NULL;
|
||||
#else
|
||||
atomic_init(&worker->alert, 0);
|
||||
#endif
|
||||
atomic_init(&worker->port_closed, false);
|
||||
worker->id = atomic_fetch_add_explicit(&scr_worker_next_id, 1, memory_order_relaxed);
|
||||
worker->entry = entry;
|
||||
@@ -262,6 +302,9 @@ failed:
|
||||
scr_mailbox_release(worker->inbox);
|
||||
scr_mailbox_release(worker->parent);
|
||||
scr_message_free(worker->data);
|
||||
#ifdef SCR_EXC_FLAG
|
||||
pthread_mutex_destroy(&worker->flag_lock);
|
||||
#endif
|
||||
free(worker);
|
||||
if (!scr_exc_pending()) {
|
||||
static const char message[] = "Could not start worker thread";
|
||||
|
||||
@@ -0,0 +1,308 @@
|
||||
import { Worker, isMainThread, parentPort } from "node:worker_threads";
|
||||
|
||||
// Every way an exception can be raised, moved, consumed or discarded, run on
|
||||
// the main thread and in a worker of the same program. After each scenario a
|
||||
// call that may throw but does not proves that nothing is left pending: a
|
||||
// stale pending state would unwind it.
|
||||
|
||||
class Fault extends Error {
|
||||
readonly code: number;
|
||||
constructor(code: number) {
|
||||
super(`fault ${code}`);
|
||||
this.name = "Fault";
|
||||
this.code = code;
|
||||
}
|
||||
}
|
||||
|
||||
function fail(code: number): never {
|
||||
throw new Fault(code);
|
||||
}
|
||||
|
||||
function label(code: number): string {
|
||||
if (code < 0) fail(-code);
|
||||
return `item-${code}`;
|
||||
}
|
||||
|
||||
function message(error: unknown): string {
|
||||
return error instanceof Error ? `${error.name}: ${error.message}` : String(error);
|
||||
}
|
||||
|
||||
function finallyReturns(): string {
|
||||
try {
|
||||
fail(1);
|
||||
} finally {
|
||||
return "finally return";
|
||||
}
|
||||
}
|
||||
|
||||
function finallyBreaks(log: string[]): number {
|
||||
let rounds = 0;
|
||||
for (let i = 0; i < 3; i++) {
|
||||
rounds++;
|
||||
try {
|
||||
fail(2);
|
||||
} finally {
|
||||
log.push(`break round ${i}`);
|
||||
break;
|
||||
}
|
||||
}
|
||||
return rounds;
|
||||
}
|
||||
|
||||
function finallyContinues(log: string[]): number {
|
||||
let rounds = 0;
|
||||
for (let i = 0; i < 3; i++) {
|
||||
try {
|
||||
if (i !== 1) fail(3);
|
||||
rounds += 10;
|
||||
} finally {
|
||||
rounds++;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
log.push(`continue rounds ${rounds}`);
|
||||
return rounds;
|
||||
}
|
||||
|
||||
function finallyReplaces(): string {
|
||||
try {
|
||||
fail(4);
|
||||
} finally {
|
||||
label(-5);
|
||||
}
|
||||
}
|
||||
|
||||
function deep(n: number, log: string[]): string {
|
||||
if (n === 0) fail(6);
|
||||
try {
|
||||
return deep(n - 1, log);
|
||||
} finally {
|
||||
if (n % 2 === 0) log.push(`unwound ${n}`);
|
||||
}
|
||||
}
|
||||
|
||||
function catchRethrows(): string {
|
||||
try {
|
||||
return label(-7);
|
||||
} catch (error) {
|
||||
try {
|
||||
fail((error as Fault).code + 1);
|
||||
} catch (inner) {
|
||||
throw new Fault((inner as Fault).code * 10);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function labelled(log: string[]): number {
|
||||
let caught = 0;
|
||||
outer: for (let i = 0; i < 3; i++) {
|
||||
for (let j = 0; j < 3; j++) {
|
||||
try {
|
||||
if (j === 1) fail(i * 3 + j);
|
||||
} catch {
|
||||
caught++;
|
||||
continue outer;
|
||||
}
|
||||
}
|
||||
}
|
||||
log.push(`labelled caught ${caught}`);
|
||||
return caught;
|
||||
}
|
||||
|
||||
function* resumable(log: string[]): Generator<string, string, string> {
|
||||
let round = 0;
|
||||
while (round < 3) {
|
||||
try {
|
||||
const sent = yield `round ${round}`;
|
||||
log.push(`sent ${sent}`);
|
||||
} catch (error) {
|
||||
log.push(`generator caught ${message(error)}`);
|
||||
}
|
||||
round++;
|
||||
}
|
||||
return "generator done";
|
||||
}
|
||||
|
||||
function* guarded(log: string[]): Generator<number> {
|
||||
try {
|
||||
yield 1;
|
||||
yield 2;
|
||||
} finally {
|
||||
log.push("guarded finally");
|
||||
}
|
||||
}
|
||||
|
||||
function* explosive(): Generator<number> {
|
||||
yield 1;
|
||||
fail(8);
|
||||
}
|
||||
|
||||
function* finallyThrows(): Generator<number> {
|
||||
try {
|
||||
yield 1;
|
||||
} finally {
|
||||
fail(9);
|
||||
}
|
||||
}
|
||||
|
||||
async function rejectsLater(code: number): Promise<string> {
|
||||
await null;
|
||||
return label(-code);
|
||||
}
|
||||
|
||||
async function rejectsNow(code: number): Promise<string> {
|
||||
return label(-code);
|
||||
}
|
||||
|
||||
function overflow(n: number): number {
|
||||
return overflow(n + 1) + 1;
|
||||
}
|
||||
|
||||
async function scenarios(): Promise<string[]> {
|
||||
const log: string[] = [];
|
||||
const attempt = (name: string, run: () => unknown): void => {
|
||||
try {
|
||||
log.push(`${name} ${String(run())}`);
|
||||
} catch (error) {
|
||||
log.push(`${name} caught ${message(error)}`);
|
||||
}
|
||||
log.push(`${name} then ${label(name.length)}`);
|
||||
};
|
||||
|
||||
attempt("bound catch", () => {
|
||||
try {
|
||||
return label(-10);
|
||||
} catch (error) {
|
||||
return `took ${(error as Fault).code}`;
|
||||
}
|
||||
});
|
||||
attempt("bare catch", () => {
|
||||
try {
|
||||
return label(-11);
|
||||
} catch {
|
||||
return "dropped";
|
||||
}
|
||||
});
|
||||
attempt("finally return", finallyReturns);
|
||||
attempt("finally break", () => finallyBreaks(log));
|
||||
attempt("finally continue", () => finallyContinues(log));
|
||||
attempt("finally replace", finallyReplaces);
|
||||
attempt("deep", () => deep(5, log));
|
||||
attempt("catch rethrow", catchRethrows);
|
||||
attempt("labelled", () => labelled(log));
|
||||
|
||||
const gen = resumable(log);
|
||||
log.push(`next ${gen.next("ignored").value}`);
|
||||
log.push(`next ${gen.next("a").value}`);
|
||||
log.push(`throw ${gen.throw(new Fault(12)).value}`);
|
||||
const last = gen.next("b");
|
||||
log.push(`last ${last.value} ${last.done}`);
|
||||
log.push(`after generator ${label(13)}`);
|
||||
|
||||
const guard = guarded(log);
|
||||
guard.next();
|
||||
const returned = guard.return(99);
|
||||
log.push(`return ${returned.value} ${returned.done}`);
|
||||
for (const value of guarded(log)) {
|
||||
log.push(`for-of ${value}`);
|
||||
break;
|
||||
}
|
||||
attempt("generator escape", () => [...explosive()].length);
|
||||
attempt("generator finally throw", () => {
|
||||
const g = finallyThrows();
|
||||
g.next();
|
||||
return g.return(0).value;
|
||||
});
|
||||
|
||||
try {
|
||||
await rejectsLater(14);
|
||||
} catch (error) {
|
||||
log.push(`await caught ${message(error)}`);
|
||||
}
|
||||
log.push(`after await ${label(15)}`);
|
||||
await rejectsNow(16).catch((error: unknown) => log.push(`catch handler ${message(error)}`));
|
||||
const statuses: string[] = [];
|
||||
for (const pending of [rejectsLater(17), Promise.resolve("ok"), rejectsNow(18)]) {
|
||||
try {
|
||||
statuses.push(`fulfilled ${await pending}`);
|
||||
} catch (error) {
|
||||
statuses.push(`rejected ${(error as Fault).code}`);
|
||||
}
|
||||
}
|
||||
log.push(`settled ${statuses.join(",")}`);
|
||||
try {
|
||||
await Promise.all([rejectsLater(19), rejectsLater(20)]);
|
||||
} catch (error) {
|
||||
log.push(`all caught ${message(error)}`);
|
||||
}
|
||||
try {
|
||||
await new Promise<string>(() => fail(21));
|
||||
} catch (error) {
|
||||
log.push(`executor caught ${message(error)}`);
|
||||
}
|
||||
const chained = await Promise.resolve(1)
|
||||
.then(() => fail(22))
|
||||
.then(
|
||||
() => "unexpected",
|
||||
(error: unknown) => `then rejected ${message(error)}`,
|
||||
);
|
||||
log.push(chained);
|
||||
log.push(`after promises ${label(23)}`);
|
||||
|
||||
attempt("map callback", () => [1, 2, 3].map((n) => label(n === 2 ? -24 : n)).join());
|
||||
attempt("sort comparator", () =>
|
||||
[3, 1, 2]
|
||||
.sort((a, b) => {
|
||||
if (a === 1 || b === 1) fail(25);
|
||||
return a - b;
|
||||
})
|
||||
.join(),
|
||||
);
|
||||
attempt("reduce", () => [1, 2, 3].reduce((acc, n) => acc + label(n === 3 ? -26 : n), ""));
|
||||
attempt("forEach", () => {
|
||||
let seen = 0;
|
||||
try {
|
||||
[1, 2, 3].forEach((n) => {
|
||||
seen++;
|
||||
if (n === 2) fail(27);
|
||||
});
|
||||
} finally {
|
||||
log.push(`forEach seen ${seen}`);
|
||||
}
|
||||
return seen;
|
||||
});
|
||||
attempt("json", () => {
|
||||
try {
|
||||
return JSON.parse("{bad");
|
||||
} catch (error) {
|
||||
return `parse failed ${(error as Error).name}`;
|
||||
}
|
||||
});
|
||||
attempt("overflow", () => overflow(0));
|
||||
attempt("nested callbacks", () =>
|
||||
[1, 2].map((n) => {
|
||||
try {
|
||||
return [n].map(() => label(-28))[0];
|
||||
} catch (error) {
|
||||
return `inner ${(error as Fault).code}`;
|
||||
}
|
||||
}),
|
||||
);
|
||||
return log;
|
||||
}
|
||||
|
||||
if (isMainThread) {
|
||||
for (const line of await scenarios()) console.log("main", line);
|
||||
await new Promise<void>((resolve) => {
|
||||
const worker = new Worker(new URL(import.meta.url));
|
||||
worker.on("message", (lines: string[]) => {
|
||||
for (const line of lines) console.log("worker", line);
|
||||
});
|
||||
worker.once("exit", (code: number) => {
|
||||
console.log("worker exit", code);
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
} else {
|
||||
parentPort!.postMessage(await scenarios());
|
||||
}
|
||||
@@ -0,0 +1,211 @@
|
||||
import { Worker, isMainThread, parentPort, workerData } from "node:worker_threads";
|
||||
|
||||
// worker.terminate() arriving while the worker's exception state is busy:
|
||||
// inside catch and finally bodies, with an exception stashed across a finally
|
||||
// or propagating through frames, inside generator and async bodies, inside
|
||||
// callbacks the runtime calls, and before the worker has started. Termination
|
||||
// is not catchable and skips every catch and finally handler, so the worker
|
||||
// prints nothing once it has announced itself.
|
||||
|
||||
class Fault extends Error {
|
||||
readonly code: number;
|
||||
constructor(code: number) {
|
||||
super(`fault ${code}`);
|
||||
this.name = "Fault";
|
||||
this.code = code;
|
||||
}
|
||||
}
|
||||
|
||||
function fail(code: number): never {
|
||||
throw new Fault(code);
|
||||
}
|
||||
|
||||
function risky(n: number): number {
|
||||
if (n < 0) fail(-n);
|
||||
return (n * 31 + 7) % 1000003;
|
||||
}
|
||||
|
||||
let announced = false;
|
||||
function announce(): void {
|
||||
if (!announced) {
|
||||
announced = true;
|
||||
parentPort!.postMessage("ready");
|
||||
}
|
||||
}
|
||||
|
||||
function spin(): number {
|
||||
let acc = 0;
|
||||
for (let i = 0; ; i++) {
|
||||
acc = (acc + risky(i % 1000)) % 1000003;
|
||||
if (i === 1000) announce();
|
||||
}
|
||||
}
|
||||
|
||||
function inCatch(): number {
|
||||
try {
|
||||
return risky(-1);
|
||||
} catch (error) {
|
||||
if ((error as Fault).code !== 1) console.log("unexpected code");
|
||||
return spin();
|
||||
}
|
||||
}
|
||||
|
||||
function inFinally(): number {
|
||||
try {
|
||||
return risky(-2);
|
||||
} finally {
|
||||
spin();
|
||||
}
|
||||
}
|
||||
|
||||
function inNestedFinally(): number {
|
||||
try {
|
||||
try {
|
||||
return spin();
|
||||
} finally {
|
||||
console.log("unexpected inner finally");
|
||||
}
|
||||
} catch {
|
||||
console.log("unexpected outer catch");
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
function deep(n: number): number {
|
||||
if (n === 0) return risky(-3);
|
||||
try {
|
||||
return deep(n - 1);
|
||||
} finally {
|
||||
if (n === 1) risky(n);
|
||||
}
|
||||
}
|
||||
|
||||
function propagating(): number {
|
||||
let caught = 0;
|
||||
for (let i = 0; ; i++) {
|
||||
try {
|
||||
deep(40);
|
||||
} catch {
|
||||
caught++;
|
||||
if (caught === 1000) announce();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function* producer(): Generator<number> {
|
||||
yield 1;
|
||||
try {
|
||||
yield spin();
|
||||
} finally {
|
||||
console.log("unexpected generator finally");
|
||||
}
|
||||
}
|
||||
|
||||
function inGenerator(): number {
|
||||
let total = 0;
|
||||
try {
|
||||
for (const value of producer()) total += value;
|
||||
} catch {
|
||||
console.log("unexpected generator catch");
|
||||
}
|
||||
return total;
|
||||
}
|
||||
|
||||
async function asyncBody(): Promise<number> {
|
||||
await null;
|
||||
try {
|
||||
return spin();
|
||||
} catch {
|
||||
console.log("unexpected async catch");
|
||||
return 0;
|
||||
} finally {
|
||||
console.log("unexpected async finally");
|
||||
}
|
||||
}
|
||||
|
||||
function inCallback(): number {
|
||||
try {
|
||||
return [1, 2, 3].map((n) => (n === 2 ? spin() : n)).length;
|
||||
} catch {
|
||||
console.log("unexpected callback catch");
|
||||
return 0;
|
||||
} finally {
|
||||
console.log("unexpected callback finally");
|
||||
}
|
||||
}
|
||||
|
||||
function inComparator(): number {
|
||||
try {
|
||||
return [3, 1, 2].sort((a, b) => (a === 1 || b === 1 ? spin() : a - b)).length;
|
||||
} finally {
|
||||
console.log("unexpected comparator finally");
|
||||
}
|
||||
}
|
||||
|
||||
function run(kind: string): number {
|
||||
switch (kind) {
|
||||
case "catch":
|
||||
return inCatch();
|
||||
case "finally":
|
||||
return inFinally();
|
||||
case "nested":
|
||||
return inNestedFinally();
|
||||
case "propagating":
|
||||
return propagating();
|
||||
case "generator":
|
||||
return inGenerator();
|
||||
case "callback":
|
||||
return inCallback();
|
||||
case "comparator":
|
||||
return inComparator();
|
||||
default:
|
||||
return spin();
|
||||
}
|
||||
}
|
||||
|
||||
if (isMainThread) {
|
||||
const kinds = ["catch", "finally", "nested", "propagating", "generator", "async", "callback", "comparator"];
|
||||
for (const kind of kinds) {
|
||||
await new Promise<void>((resolve) => {
|
||||
const worker = new Worker(new URL(import.meta.url), { workerData: kind });
|
||||
worker.once("message", () => {
|
||||
void worker.terminate().then((code: number) => console.log(kind, "terminate", code));
|
||||
});
|
||||
worker.once("exit", (code: number) => {
|
||||
console.log(kind, "exit", code);
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
}
|
||||
// Terminated before the worker can have started its script (Node reports
|
||||
// exit code 0 here, scriptc 1: only the stop itself is pinned).
|
||||
for (let i = 0; i < 3; i++) {
|
||||
await new Promise<void>((resolve) => {
|
||||
const worker = new Worker(new URL(import.meta.url), { workerData: "early" });
|
||||
void worker.terminate();
|
||||
worker.once("exit", () => {
|
||||
console.log("early exit", i);
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
}
|
||||
// The main thread's own exceptions are unaffected by the stopped workers.
|
||||
try {
|
||||
risky(-4);
|
||||
} catch (error) {
|
||||
console.log("main caught", (error as Fault).code, risky(5));
|
||||
}
|
||||
} else if (workerData === "async") {
|
||||
asyncBody().then(
|
||||
() => console.log("unexpected async result"),
|
||||
() => console.log("unexpected async rejection"),
|
||||
);
|
||||
} else {
|
||||
try {
|
||||
console.log("unexpected result", run(String(workerData)));
|
||||
} catch {
|
||||
console.log("unexpected catch");
|
||||
} finally {
|
||||
console.log("unexpected finally");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,138 @@
|
||||
import { Worker, isMainThread, parentPort, workerData } from "node:worker_threads";
|
||||
|
||||
// Loops whose every iteration calls a function that can throw observe a
|
||||
// terminate request through that call's exception test; loops that can skip
|
||||
// such a call keep their own poll. Each worker spins until it is terminated.
|
||||
|
||||
class Box {
|
||||
value = 0;
|
||||
bump(n: number): number {
|
||||
if (n < 0) throw new RangeError("negative " + n);
|
||||
this.value = (this.value + n) % 1000003;
|
||||
return this.value;
|
||||
}
|
||||
}
|
||||
|
||||
function risky(n: number): number {
|
||||
if (n < 0) throw new RangeError("negative " + n);
|
||||
return (n * 31 + 7) % 1000003;
|
||||
}
|
||||
|
||||
let announced = false;
|
||||
function announce(i: number): void {
|
||||
if (!announced && i >= 1000) {
|
||||
announced = true;
|
||||
parentPort!.postMessage("ready");
|
||||
}
|
||||
}
|
||||
|
||||
function spinCalls(): number {
|
||||
let acc = 0;
|
||||
for (let i = 0; ; i++) {
|
||||
acc = (acc + risky(i)) % 1000003;
|
||||
announce(i);
|
||||
}
|
||||
}
|
||||
|
||||
function spinCondition(): number {
|
||||
let i = 0;
|
||||
while (risky(i) >= 0) {
|
||||
i++;
|
||||
announce(i);
|
||||
}
|
||||
return i;
|
||||
}
|
||||
|
||||
// Once i passes 2000 every iteration continues before the call.
|
||||
function spinContinue(): number {
|
||||
let acc = 0;
|
||||
for (let i = 0; ; i++) {
|
||||
if (i > 2000) continue;
|
||||
acc = (acc + risky(i)) % 1000003;
|
||||
announce(i);
|
||||
}
|
||||
}
|
||||
|
||||
function spinMethods(): number {
|
||||
const box = new Box();
|
||||
let n = 0;
|
||||
do {
|
||||
try {
|
||||
box.bump(n % 5 === 0 ? -1 : n);
|
||||
} catch (e) {
|
||||
if (!(e instanceof RangeError)) throw e;
|
||||
}
|
||||
n++;
|
||||
announce(n);
|
||||
} while (box.bump(1) >= 0);
|
||||
return n;
|
||||
}
|
||||
|
||||
function spinNested(): number {
|
||||
let acc = 0;
|
||||
for (let i = 0; ; i++) {
|
||||
for (let j = 0; j < 4; j++) acc = (acc + risky(j)) % 1000003;
|
||||
announce(i);
|
||||
}
|
||||
}
|
||||
|
||||
function run(kind: string): number {
|
||||
if (kind === "calls") return spinCalls();
|
||||
if (kind === "condition") return spinCondition();
|
||||
if (kind === "continue") return spinContinue();
|
||||
if (kind === "methods") return spinMethods();
|
||||
return spinNested();
|
||||
}
|
||||
|
||||
// The same loop shapes, bounded, with exceptions escaping mid-iteration.
|
||||
function countdown(start: number): number {
|
||||
let acc = 0;
|
||||
for (let i = start; ; i--) acc = (acc + risky(i)) % 1000003;
|
||||
}
|
||||
|
||||
function countdownWhile(start: number): number {
|
||||
let i = start;
|
||||
while (risky(i) >= 0) i--;
|
||||
return i;
|
||||
}
|
||||
|
||||
function methodsUntil(limit: number): number {
|
||||
const box = new Box();
|
||||
let n = 0;
|
||||
do {
|
||||
n++;
|
||||
} while (box.bump(limit - n) >= 0);
|
||||
return n;
|
||||
}
|
||||
|
||||
if (isMainThread) {
|
||||
for (const probe of [() => countdown(5), () => countdownWhile(7), () => methodsUntil(4)]) {
|
||||
try {
|
||||
console.log("unexpected", probe());
|
||||
} catch (e) {
|
||||
console.log("caught", (e as Error).name, (e as Error).message);
|
||||
} finally {
|
||||
console.log("finally");
|
||||
}
|
||||
}
|
||||
for (const kind of ["calls", "condition", "continue", "methods", "nested"]) {
|
||||
await new Promise<void>((resolve) => {
|
||||
const worker = new Worker(new URL(import.meta.url), { workerData: kind });
|
||||
worker.once("message", () => {
|
||||
void worker.terminate().then((code: number) => console.log(kind, "terminate", code));
|
||||
});
|
||||
worker.once("exit", (code: number) => {
|
||||
console.log(kind, "exit", code);
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
console.log("unexpected result", run(String(workerData)));
|
||||
} catch {
|
||||
console.log("unexpected catch");
|
||||
} finally {
|
||||
console.log("unexpected finally");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user