mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-11-07 08:33:50 +08:00
Propagate exceptions by unwinding from directly called functions on Linux (#2115)
This commit is contained in:
@@ -31,6 +31,7 @@ Application-shaped workloads for measuring the speed of compiled executables. Ea
|
||||
| `regex-logs` | cpu | regex `exec` with groups, `test`, global `replace` |
|
||||
| `region-builds` | cpu | a watch-mode build loop over `LifetimeRegion`: each build binds a program in a new region and releases the previous one, which a cache keeps referenced for two builds |
|
||||
| `template-render` | cpu | template literals, escaping, `map`/`join` |
|
||||
| `throw-chains` | cpu | exceptions caught 2, 5 and 20 recursive calls above the throw, through direct calls and through a callback, and a backtracking parser that throws on a failed alternative |
|
||||
| `thread-slots`, `thread-slots-cache` | cpu | worker threads reading one published type graph from an `InternTable` and keeping per-thread state for each type they visit: a `ThreadSlots` family, or a direct-mapped cache by id in front of a `Map` by id (the `-cache` entry); under Node.js both are per thread |
|
||||
| `typed-array-kernels` | cpu | element-wise loops that run to one typed array's length and index others at the same position: `Float64Array` axpy, `Float32Array` blend, `Uint8Array` mask, wrapping `Int32Array` add, `Uint8ClampedArray` brighten, RGBA to gray, a row-major `Float32Array` matrix-vector product, and `reduce`/`some`/`map` callbacks |
|
||||
| `typed-array-scan`, `typed-array-scan-workers` | cpu | element-by-element scan loops over `Uint8Array`, `Uint16Array`, `Int32Array` and `Float64Array`; the second entry is a worker-enabled executable (it can start a worker), so each access also checks for shared storage |
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"entry": "workloads/throw-chains.ts",
|
||||
"args": [
|
||||
"1"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,196 @@
|
||||
// Exceptions that cross many frames: throws from helpers below a recursion,
|
||||
// caught 2, 5 and 20 recursive calls up (direct calls, then calls through a
|
||||
// callback), with each frame holding an array across its call, and a
|
||||
// backtracking parser that uses exceptions for control flow.
|
||||
class ParseError extends Error {
|
||||
readonly at: number;
|
||||
constructor(at: number, message: string) {
|
||||
super(message);
|
||||
this.at = at;
|
||||
}
|
||||
}
|
||||
|
||||
/** A thrown value that is not an Error (no stack to capture). */
|
||||
class Stop {
|
||||
readonly code: number;
|
||||
constructor(code: number) {
|
||||
this.code = code;
|
||||
}
|
||||
}
|
||||
|
||||
const held: number[][] = [];
|
||||
function fail(n: number, plain: boolean): number {
|
||||
if (plain) throw new Stop(n);
|
||||
throw new RangeError("chain " + n);
|
||||
}
|
||||
function check(n: number, plain: boolean): number {
|
||||
return (n & 3) === 0 ? fail(n, plain) : n;
|
||||
}
|
||||
function validate(n: number, plain: boolean): number {
|
||||
return check(n, plain) + (n & 1);
|
||||
}
|
||||
function bottom(n: number, plain: boolean): number {
|
||||
const box = [n, -1];
|
||||
const r = validate(n, plain);
|
||||
held[0] = box;
|
||||
return r + box.length;
|
||||
}
|
||||
function walk(n: number, depth: number, plain: boolean): number {
|
||||
const box = [n, depth];
|
||||
const r = depth === 0 ? bottom(n, plain) : walk(n, depth - 1, plain);
|
||||
held[depth] = box;
|
||||
return r + box.length;
|
||||
}
|
||||
|
||||
function chains(rounds: number): string {
|
||||
const out: string[] = [];
|
||||
for (const depth of [2, 5, 20])
|
||||
for (const plain of [false, true]) {
|
||||
let caught = 0;
|
||||
let sum = 0;
|
||||
for (let i = 0; i < rounds; i++) {
|
||||
try {
|
||||
sum += walk(i, depth - 2, plain);
|
||||
} catch (error) {
|
||||
caught++;
|
||||
sum += error instanceof Stop ? error.code & 7 : 1;
|
||||
}
|
||||
}
|
||||
out.push(`depth ${depth} ${plain ? "object" : "error"}: caught ${caught} sum ${sum}`);
|
||||
}
|
||||
return out.join("\n");
|
||||
}
|
||||
|
||||
// The same chains through a callback: each level visits the next with a
|
||||
// closure, as tree visitors do.
|
||||
function visit(n: number, depth: number, each: (n: number, depth: number) => number): number {
|
||||
const box = [n, depth];
|
||||
const r = depth === 0 ? bottom(n, false) : each(n, depth - 1);
|
||||
held[depth] = box;
|
||||
return r + box.length;
|
||||
}
|
||||
function visits(rounds: number): string {
|
||||
const out: string[] = [];
|
||||
const each = (n: number, depth: number): number => visit(n, depth, each);
|
||||
for (const depth of [2, 5, 20]) {
|
||||
let caught = 0;
|
||||
let sum = 0;
|
||||
for (let i = 0; i < rounds; i++) {
|
||||
try {
|
||||
sum += visit(i, depth - 2, each);
|
||||
} catch (error) {
|
||||
caught++;
|
||||
sum += error instanceof RangeError ? 1 : 0;
|
||||
}
|
||||
}
|
||||
out.push(`visit depth ${depth}: caught ${caught} sum ${sum}`);
|
||||
}
|
||||
return out.join("\n");
|
||||
}
|
||||
|
||||
// expr := term (("+" | "-") term)*; term := atom ("*" atom)*;
|
||||
// atom := number | "(" expr ")" | name "(" expr ("," expr)* ")" | name.
|
||||
// A call and a name start alike: the parser tries the call first and
|
||||
// backtracks when it fails.
|
||||
let text = "";
|
||||
let pos = 0;
|
||||
function peek(): string {
|
||||
return pos < text.length ? text[pos]! : "";
|
||||
}
|
||||
function expect(c: string): void {
|
||||
if (peek() !== c) throw new ParseError(pos, `expected ${c}`);
|
||||
pos++;
|
||||
}
|
||||
function name(): string {
|
||||
const start = pos;
|
||||
while (pos < text.length && text.charCodeAt(pos) >= 97 && text.charCodeAt(pos) <= 122) pos++;
|
||||
if (pos === start) throw new ParseError(pos, "expected a name");
|
||||
return text.slice(start, pos);
|
||||
}
|
||||
function call(): number {
|
||||
const callee = name();
|
||||
expect("(");
|
||||
let value = expr() + callee.length;
|
||||
while (peek() === ",") {
|
||||
pos++;
|
||||
value += expr();
|
||||
}
|
||||
expect(")");
|
||||
return value;
|
||||
}
|
||||
function atom(): number {
|
||||
const c = peek();
|
||||
if (c >= "0" && c <= "9") {
|
||||
let value = 0;
|
||||
while (peek() >= "0" && peek() <= "9") value = value * 10 + (text.charCodeAt(pos++) - 48);
|
||||
return value;
|
||||
}
|
||||
if (c === "(") {
|
||||
pos++;
|
||||
const value = expr();
|
||||
expect(")");
|
||||
return value;
|
||||
}
|
||||
const start = pos;
|
||||
try {
|
||||
return call();
|
||||
} catch (error) {
|
||||
if (!(error instanceof ParseError)) throw error;
|
||||
pos = start;
|
||||
return name().length;
|
||||
}
|
||||
}
|
||||
function term(): number {
|
||||
let value = atom();
|
||||
while (peek() === "*") {
|
||||
pos++;
|
||||
value = (value * atom()) % 1000003;
|
||||
}
|
||||
return value;
|
||||
}
|
||||
function expr(): number {
|
||||
let value = term();
|
||||
while (peek() === "+" || peek() === "-") {
|
||||
const op = text[pos++];
|
||||
value = op === "+" ? value + term() : value - term();
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
let seed = 4242;
|
||||
function random(): number {
|
||||
seed = (Math.imul(seed, 1103515245) + 12345) >>> 0;
|
||||
return seed / 4294967296;
|
||||
}
|
||||
function source(depth: number): string {
|
||||
const r = random();
|
||||
if (depth > 6 || r < 0.25) return String(Math.floor(random() * 100));
|
||||
if (r < 0.45) return "abcdefgh".slice(0, 1 + Math.floor(random() * 8));
|
||||
if (r < 0.6) return `(${source(depth + 1)})`;
|
||||
if (r < 0.8) return `${source(depth + 1)}${random() < 0.5 ? "+" : "*"}${source(depth + 1)}`;
|
||||
return `f${"xyz".slice(0, Math.floor(random() * 3))}(${source(depth + 1)},${source(depth + 1)})`;
|
||||
}
|
||||
|
||||
function parse(count: number): string {
|
||||
let total = 0;
|
||||
let failed = 0;
|
||||
for (let i = 0; i < count; i++) {
|
||||
// Every tenth input lacks its last character, which fails deep inside it.
|
||||
const full = source(0);
|
||||
text = i % 10 === 0 ? full.slice(0, -1) : full;
|
||||
pos = 0;
|
||||
try {
|
||||
total = (total + expr()) % 1000003;
|
||||
if (pos !== text.length) throw new ParseError(pos, "trailing input");
|
||||
} catch (error) {
|
||||
if (!(error instanceof ParseError)) throw error;
|
||||
failed++;
|
||||
}
|
||||
}
|
||||
return `parsed ${count}: total ${total} failed ${failed}`;
|
||||
}
|
||||
|
||||
const scale = Number(process.argv[2] ?? "1");
|
||||
console.log(chains(Math.floor(60000 * scale)));
|
||||
console.log(visits(Math.floor(60000 * scale)));
|
||||
console.log(parse(Math.floor(40000 * scale)));
|
||||
@@ -9,6 +9,7 @@
|
||||
* code after it); here the lines are DROPPED instead, and emitStmts skips
|
||||
* whole statements once the current block is terminated, so no dropped
|
||||
* definition can leak into a later block. */
|
||||
import { UNWIND_RESUME, UNWIND_SLOT } from "./unwind-abi.js";
|
||||
|
||||
export class BlockBuilder {
|
||||
private readonly blocks: { label: string; lines: string[]; term: string | null }[] = [];
|
||||
@@ -86,7 +87,18 @@ export class BlockBuilder {
|
||||
|
||||
terminate(s: string): void {
|
||||
if (this.cur.term === null) {
|
||||
if (this.returnEpilogue && s.startsWith("ret ")) this.line(this.returnEpilogue);
|
||||
if (
|
||||
this.returnEpilogue &&
|
||||
(s.startsWith("ret ") ||
|
||||
s.startsWith("call void @scr_unwind_raise()") ||
|
||||
s === UNWIND_RESUME)
|
||||
)
|
||||
this.line(this.returnEpilogue);
|
||||
if (s === UNWIND_RESUME) {
|
||||
const exception = this.tmp();
|
||||
this.line(`${exception} = load { ptr, i32 }, ptr ${UNWIND_SLOT}`);
|
||||
s = `resume { ptr, i32 } ${exception}`;
|
||||
}
|
||||
this.cur.term = ` ${this.withDebugLocation(s)}`;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -56,11 +56,14 @@ import { emitIntegerClone, emitIntegerDispatcher } from "./integer-clones.js";
|
||||
import { beginFunctionLoops, LOOPS } from "./loop-state.js";
|
||||
import { runningExtremumUpdates } from "./running-extrema.js";
|
||||
import { findIntegerOrNaNLocals } from "./wrapping-sums.js";
|
||||
import { UNWIND_PERSONALITY, UNWIND_SLOT } from "./unwind-abi.js";
|
||||
|
||||
export function emitFunction(emitter: LlEmitter, fn: IrFunction): string {
|
||||
const B = new BlockBuilder();
|
||||
emitter.B = B;
|
||||
emitter.currentFunctionName = fn.name;
|
||||
emitter.currentUnwindAbi = emitter.unwindAbi.has(fn.name);
|
||||
emitter.usesLandingPads = false;
|
||||
emitter.findInt32Allocations(fn);
|
||||
emitter.currentTier = fn.tier;
|
||||
// Fiber switches restore the active exception cell before control
|
||||
@@ -597,6 +600,8 @@ export function emitFunction(emitter: LlEmitter, fn: IrFunction): string {
|
||||
B.terminate(cleanup.terminator);
|
||||
}
|
||||
|
||||
if (emitter.usesLandingPads && emitter.currentUnwindAbi)
|
||||
B.entryAllocas.push(`${UNWIND_SLOT} = alloca { ptr, i32 }`);
|
||||
if (emitter.logArgSlots > 0) {
|
||||
B.entryAllocas.push(`%logargs = alloca [${emitter.logArgSlots} x %ScrLogArg]`);
|
||||
}
|
||||
@@ -637,7 +642,10 @@ export function emitFunction(emitter: LlEmitter, fn: IrFunction): string {
|
||||
const debug = emitter.debugScope === null ? "" : ` !dbg ${emitter.debugScope}`;
|
||||
const symbol = borrowed ? mangleBorrowedFunction(fn.name) : mangleFunction(fn.name);
|
||||
emitter.privateGlobalSlots.clear();
|
||||
let body = `define internal ${ret} @${symbol}${int32Abi ? INT32_ABI_SUFFIX : ""}(${params.join(", ")}) ${attrs}${debug} { ; ${fn.name}\n${B.render()}\n}`;
|
||||
// Landing pads (emitDirectCall) need the personality.
|
||||
const personality = emitter.usesLandingPads ? ` ${UNWIND_PERSONALITY}` : "";
|
||||
if (emitter.usesLandingPads) emitter.declare(`declare i32 @__gcc_personality_v0(...)`);
|
||||
let body = `define internal ${ret} @${symbol}${int32Abi ? INT32_ABI_SUFFIX : ""}(${params.join(", ")}) ${attrs}${personality}${debug} { ; ${fn.name}\n${B.render()}\n}`;
|
||||
if (int32Abi) body += "\n" + emitter.emitInt32AbiWrapper(fn, symbol, int32Abi);
|
||||
if (emitter.integerClones.members.has(fn.name)) {
|
||||
body += "\n" + emitIntegerClone(fn, emitter, emitter.stackChecks?.has(fn.name) === true);
|
||||
|
||||
@@ -131,6 +131,16 @@ import {
|
||||
type FfiCallbackAdapter,
|
||||
} from "../ffi-callbacks.js";
|
||||
import { computeMayThrow } from "../may-throw.js";
|
||||
import {
|
||||
UNWIND_PERSONALITY,
|
||||
UNWIND_RAISE,
|
||||
UNWIND_RAISE_DECLARATION,
|
||||
UNWIND_RESUME,
|
||||
UNWIND_SLOT,
|
||||
UnwindAbiViolation,
|
||||
bridgePlainUnwindCalls,
|
||||
unwindAbiFunctions,
|
||||
} from "./unwind-abi.js";
|
||||
import {
|
||||
mangleAsyncInlineAwaitCall,
|
||||
mangleAsyncSpawn,
|
||||
@@ -442,6 +452,12 @@ export interface ForkPrimitives {
|
||||
export interface LlvmTargetOptions {
|
||||
/** C ABI target; omitted direct emission uses the host ABI. */
|
||||
targetTriple?: string;
|
||||
/** Linux (glibc and musl) executables: functions reached only by direct
|
||||
* calls propagate exceptions by unwinding the native stack (unwind-abi.ts). */
|
||||
unwindAbi?: boolean;
|
||||
/** Functions kept on the pending-flag ABI under `unwindAbi`: the ones an
|
||||
* earlier emission found breaking its rules (UnwindAbiViolation). */
|
||||
unwindExcluded?: ReadonlySet<string>;
|
||||
/** Emit the function bodies in this many contiguous ranges, each by its
|
||||
* own emitter, and merge them in function order (emit-journal.ts). The
|
||||
* output is the same as one emitter's. Debug builds (`debugSources`)
|
||||
@@ -595,16 +611,36 @@ function moduleEmitter(input: IrModule, options: LlvmTargetOptions): LlEmitter {
|
||||
return new LlEmitter(emitted, options, null, prepared?.mod === emitted ? prepared : null);
|
||||
}
|
||||
|
||||
/** Emits the module's parts, emitting it again with the functions an
|
||||
* UnwindAbiViolation names on the pending-flag ABI. */
|
||||
function emitModuleText(input: IrModule, options: LlvmTargetOptions): string[] {
|
||||
for (let excluded = options.unwindExcluded ?? new Set<string>(); ;) {
|
||||
const emitter = moduleEmitter(input, { ...options, unwindExcluded: excluded });
|
||||
try {
|
||||
return emitter.emitParts();
|
||||
} catch (error) {
|
||||
if (!(error instanceof UnwindAbiViolation)) throw error;
|
||||
const names = new Set(excluded);
|
||||
for (const name of emitter.unwindAbi)
|
||||
for (const symbol of [mangleFunction(name), mangleBorrowedFunction(name)])
|
||||
if (error.symbols.has(symbol) || error.symbols.has(symbol + INT32_ABI_SUFFIX))
|
||||
names.add(name);
|
||||
if (names.size === excluded.size) throw error;
|
||||
excluded = names;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function emitLlvmModule(input: IrModule, options: LlvmTargetOptions = {}): string {
|
||||
return moduleEmitter(input, options).emit();
|
||||
return emitModuleText(input, options).join("\n");
|
||||
}
|
||||
|
||||
/** emitLlvmModule's text as parts that concatenate to it, without the copy
|
||||
* that joining them makes: a large program's text is 150 MB. */
|
||||
export function emitLlvmModuleParts(input: IrModule, options: LlvmTargetOptions = {}): string[] {
|
||||
return moduleEmitter(input, options)
|
||||
.emitParts()
|
||||
.flatMap((part, index) => (index === 0 ? [part] : ["\n", part]));
|
||||
return emitModuleText(input, options).flatMap((part, index) =>
|
||||
index === 0 ? [part] : ["\n", part],
|
||||
);
|
||||
}
|
||||
|
||||
/** Keep large translation units below the host's single-string limit. */
|
||||
@@ -614,7 +650,7 @@ export function emitLlvmModuleSource(
|
||||
): string | readonly string[] {
|
||||
// IR validation, contracts and the frontend's disposal run after lowering without checks.
|
||||
relieveHeapPressure(true);
|
||||
const parts = moduleEmitter(input, options).emitParts();
|
||||
const parts = emitModuleText(input, options);
|
||||
const length = parts.reduce((total, part) => total + part.length, Math.max(0, parts.length - 1));
|
||||
if (length <= 256 * 1024 * 1024) return parts.join("\n");
|
||||
return parts.flatMap((part, index) => (index === 0 ? [part] : ["\n", part]));
|
||||
@@ -1258,6 +1294,10 @@ export class LlEmitter {
|
||||
/** May-throw analysis (computeMayThrow): pending
|
||||
* checks are emitted only after calls that can actually raise. */
|
||||
readonly mayThrow: Set<string>;
|
||||
/** The functions that propagate exceptions by unwinding (unwind-abi.ts). */
|
||||
readonly unwindAbi: Set<string>;
|
||||
/** Whether the function being emitted is one of unwindAbi. */
|
||||
currentUnwindAbi = false;
|
||||
readonly indirectMayThrow: boolean;
|
||||
/** Worker programs: the functions that poll for termination on entry
|
||||
* (workerTerminationPolls); null outside worker programs. */
|
||||
@@ -1725,6 +1765,16 @@ export class LlEmitter {
|
||||
nodes: this.nodeIndex,
|
||||
});
|
||||
this.mayThrow = mt.fns;
|
||||
this.unwindAbi =
|
||||
options.unwindAbi === true &&
|
||||
!this.wasi &&
|
||||
mod.lib === undefined &&
|
||||
options.debugSources === undefined &&
|
||||
(options.targetTriple
|
||||
? /-linux-(?:gnu|musl)/.test(options.targetTriple)
|
||||
: process.platform === "linux")
|
||||
? unwindAbiFunctions(mod, this.mayThrow, options.unwindExcluded)
|
||||
: new Set();
|
||||
this.workerEntryPolls = mt.workerEntryPolls ?? null;
|
||||
this.stackChecks = mt.stackChecks ?? null;
|
||||
// A stackless call's eager prefix borrows its caller's stack; the
|
||||
@@ -1860,6 +1910,9 @@ export class LlEmitter {
|
||||
: undefined,
|
||||
)
|
||||
: NO_INTEGER_CLONES;
|
||||
// Integer clones and their dispatchers call each other through their
|
||||
// own lowering, which keeps plain calls and flag tests.
|
||||
for (const name of this.integerClones.members) this.unwindAbi.delete(name);
|
||||
this.stackCallbacks = new StackCallbacks(this.fnByName);
|
||||
this.receiverReaders = new AmbientReceiverReaders(this.fnByName, this.nodeIndex);
|
||||
this.stackCaptures = new StackCaptures(this.fnByName, this.callLifetimes, this.stackCallbacks);
|
||||
@@ -2139,7 +2192,8 @@ export class LlEmitter {
|
||||
return this.emitParts().join("\n");
|
||||
}
|
||||
emitParts(): string[] {
|
||||
return emitParts(this);
|
||||
const parts = emitParts(this);
|
||||
return this.unwindAbi.size > 0 ? bridgePlainUnwindCalls(parts) : parts;
|
||||
}
|
||||
|
||||
/** The dispose-observer gates the emitted frees and record writers test
|
||||
@@ -3249,6 +3303,13 @@ export class LlEmitter {
|
||||
terminator = `br label %${target.label}`;
|
||||
} else if (this.currentCoro !== null) {
|
||||
terminator = `br label %${this.currentCoro.finalLabel}`;
|
||||
} else if (this.currentUnwindAbi && this.inLandingPad) {
|
||||
// The unwind that entered the landing pad goes on to the caller's.
|
||||
terminator = UNWIND_RESUME;
|
||||
} else if (this.currentUnwindAbi) {
|
||||
// The caller's invoke takes the exception at its landing pad.
|
||||
this.declare(UNWIND_RAISE_DECLARATION);
|
||||
terminator = UNWIND_RAISE;
|
||||
} else {
|
||||
const t = this.llType(this.currentReturnType);
|
||||
terminator =
|
||||
@@ -3279,6 +3340,7 @@ export class LlEmitter {
|
||||
passThrough !== undefined &&
|
||||
!target &&
|
||||
this.currentCoro === null &&
|
||||
!this.currentUnwindAbi &&
|
||||
!this.currentInt32Result
|
||||
) {
|
||||
const t = this.llType(this.currentReturnType);
|
||||
@@ -3465,6 +3527,46 @@ export class LlEmitter {
|
||||
B.startBlock(B.newLabel("exc.dead"));
|
||||
}
|
||||
|
||||
/** Emits `line`, a `call` of `callee`, and reports whether the call
|
||||
* needs no pending check after it: a call of an unwinding-ABI function
|
||||
* becomes an `invoke` whose landing pad runs this frame's unwind (the
|
||||
* same releases and handler as emitPendingCheck's unwind path), and the
|
||||
* normal path continues in a fresh block. Coroutine bodies keep plain
|
||||
* calls (unwindAbiFunctions excludes their callees). */
|
||||
emitDirectCall(line: string, callee: string): boolean {
|
||||
const B = this.B;
|
||||
if (!this.unwindAbi.has(callee) || this.currentCoro !== null || B.isTerminated()) {
|
||||
B.line(line);
|
||||
return false;
|
||||
}
|
||||
const ok = B.newLabel("inv.ok");
|
||||
const pad = B.newLabel("inv.lp");
|
||||
this.usesLandingPads = true;
|
||||
// A trailing comment goes after the invoke's labels.
|
||||
const comment = line.indexOf(" ; ");
|
||||
const text = comment < 0 ? line : line.slice(0, comment);
|
||||
B.terminate(
|
||||
`${text.replace(/^((?:%[\w.]+ = )?)call /, "$1invoke ")} to label %${ok} unwind label %${pad}`,
|
||||
);
|
||||
B.startBlock(pad);
|
||||
const exception = B.tmp();
|
||||
B.line(`${exception} = landingpad { ptr, i32 } cleanup`);
|
||||
if (this.currentUnwindAbi) B.line(`store { ptr, i32 } ${exception}, ptr ${UNWIND_SLOT}`);
|
||||
this.inLandingPad = true;
|
||||
this.emitUnwind();
|
||||
this.inLandingPad = false;
|
||||
B.startBlock(ok);
|
||||
return true;
|
||||
}
|
||||
|
||||
/** Whether emitUnwind runs for a landing pad, whose unwind goes on with
|
||||
* `resume` instead of raising again. */
|
||||
private inLandingPad = false;
|
||||
|
||||
/** Whether the function being emitted has a landing pad (its definition
|
||||
* needs the personality). */
|
||||
usesLandingPads = false;
|
||||
|
||||
/** A recursive function's entry check: below the running stack's guard,
|
||||
* throw Node's RangeError and unwind like any other pending exception. */
|
||||
emitStackCheck(): void {
|
||||
@@ -5238,7 +5340,22 @@ export class LlEmitter {
|
||||
if (fn.captures !== undefined) params.unshift("ptr %sc_env");
|
||||
const ret = this.llType(fn.returnType);
|
||||
const call = `call ${ret} @${mangleBorrowedFunction(fn.name)}(${params.join(", ")})`;
|
||||
B.line(ret === "void" ? call : `%result = ${call}`);
|
||||
// The owned entry of an unwinding-ABI body releases the borrowed
|
||||
// parameters on the unwind too, then resumes it to its caller.
|
||||
const unwinding = this.unwindAbi.has(fn.name) && kept.size === 0;
|
||||
if (unwinding) {
|
||||
const ok = B.newLabel("adapt.ok");
|
||||
const pad = B.newLabel("adapt.lp");
|
||||
B.terminate(
|
||||
`${ret === "void" ? "" : "%result = "}invoke ${call.slice(5)} to label %${ok} unwind label %${pad}`,
|
||||
);
|
||||
B.startBlock(pad);
|
||||
const exception = B.tmp();
|
||||
B.line(`${exception} = landingpad { ptr, i32 } cleanup`);
|
||||
for (const index of borrowed) this.releaseFrame(`%p${index}`, fn.params[index]!.type);
|
||||
B.terminate(`resume { ptr, i32 } ${exception}`);
|
||||
B.startBlock(ok);
|
||||
} else B.line(ret === "void" ? call : `%result = ${call}`);
|
||||
// A borrowed result is reachable from the parameters: own it first. A
|
||||
// throwing body returns a null dummy, which needs no owner (and string
|
||||
// or array retains do not accept NULL).
|
||||
@@ -5264,7 +5381,9 @@ export class LlEmitter {
|
||||
B.terminate(ret === "void" ? "ret void" : `ret ${ret} ${result}`);
|
||||
const symbol =
|
||||
kept.size > 0 ? `${mangleBorrowedFunction(fn.name)}.virtual` : mangleFunction(fn.name);
|
||||
return `define internal ${ret} @${symbol}(${params.join(", ")}) ${FN_ATTRS} {\n${B.render()}\n}`;
|
||||
if (unwinding) this.declare(`declare i32 @__gcc_personality_v0(...)`);
|
||||
const personality = unwinding ? ` ${UNWIND_PERSONALITY}` : "";
|
||||
return `define internal ${ret} @${symbol}(${params.join(", ")}) ${FN_ATTRS}${personality} {\n${B.render()}\n}`;
|
||||
}
|
||||
canBorrowCallArgument(value: IrExpr): boolean {
|
||||
return canBorrowCallArgument(this, value);
|
||||
|
||||
@@ -244,38 +244,46 @@ export function emitCallExpr(
|
||||
? INTEGER_DISPATCH_SUFFIX
|
||||
: "";
|
||||
const target = `@${borrowed ? mangleBorrowedFunction(e.callee) : host.callTarget(e.callee)}${abi ? INT32_ABI_SUFFIX : ""}${dispatch}`;
|
||||
// A call of an unwinding-ABI callee is an invoke (emitDirectCall):
|
||||
// its landing pad unwinds, so no pending check follows it.
|
||||
if (e.type.kind === "void") {
|
||||
B.line(`call void ${target}(${argList})`);
|
||||
if (host.mayThrow.has(e.callee)) host.emitPendingCheck();
|
||||
const invoked = host.emitDirectCall(`call void ${target}(${argList})`, e.callee);
|
||||
if (!invoked && host.mayThrow.has(e.callee)) host.emitPendingCheck();
|
||||
if (borrowed) host.releaseTemps(host.frames.pop()!);
|
||||
return { name: "", type: e.type };
|
||||
}
|
||||
const t = B.tmp();
|
||||
if (abi?.union) {
|
||||
B.line(`${t} = call ${SCALAR_UNION_RESULT} ${target}(${argList})`);
|
||||
if (host.mayThrow.has(e.callee)) host.emitPendingCheck();
|
||||
const invoked = host.emitDirectCall(
|
||||
`${t} = call ${SCALAR_UNION_RESULT} ${target}(${argList})`,
|
||||
e.callee,
|
||||
);
|
||||
if (!invoked && host.mayThrow.has(e.callee)) host.emitPendingCheck();
|
||||
if (borrowed) host.releaseTemps(host.frames.pop()!);
|
||||
if (stackResult) return host.stackScalarUnion(t, e.type);
|
||||
return host.own({ name: host.boxScalarUnion(t, e.type), type: e.type });
|
||||
}
|
||||
if (abi?.result) {
|
||||
B.line(`${t} = call i32 ${target}(${argList})`);
|
||||
const invoked = host.emitDirectCall(`${t} = call i32 ${target}(${argList})`, e.callee);
|
||||
const out = host.int32Result(t, e.type);
|
||||
if (host.mayThrow.has(e.callee)) host.emitPendingCheck(out);
|
||||
if (!invoked && host.mayThrow.has(e.callee)) host.emitPendingCheck(out);
|
||||
if (borrowed) host.releaseTemps(host.frames.pop()!);
|
||||
return out;
|
||||
}
|
||||
B.line(`${t} = call ${host.llType(e.type)} ${target}(${argList})`);
|
||||
const invoked = host.emitDirectCall(
|
||||
`${t} = call ${host.llType(e.type)} ${target}(${argList})`,
|
||||
e.callee,
|
||||
);
|
||||
let out: LlValue;
|
||||
if (borrowed && host.borrowedReturns.has(e.callee)) {
|
||||
// A borrowed-return body hands back +0 (a null dummy when it threw):
|
||||
// test the exception first, then own the result before the
|
||||
// argument snapshots that keep it reachable are released.
|
||||
if (host.mayThrow.has(e.callee)) host.emitPendingCheck();
|
||||
if (!invoked && host.mayThrow.has(e.callee)) host.emitPendingCheck();
|
||||
out = host.own({ name: host.retainFrame(t, e.type), type: e.type });
|
||||
} else {
|
||||
out = host.own({ name: t, type: e.type });
|
||||
if (host.mayThrow.has(e.callee)) host.emitPendingCheck(out);
|
||||
if (!invoked && host.mayThrow.has(e.callee)) host.emitPendingCheck(out);
|
||||
}
|
||||
if (borrowed) {
|
||||
host.toFrame(out);
|
||||
@@ -1366,10 +1374,11 @@ export function emitBorrowedResultCall(host: LlvmEmitterContext, e: ExprOf<"call
|
||||
)
|
||||
.join(", ");
|
||||
const t = host.B.tmp();
|
||||
host.B.line(
|
||||
const invoked = host.emitDirectCall(
|
||||
`${t} = call ${host.llType(e.type)} @${mangleBorrowedFunction(e.callee)}${abi ? INT32_ABI_SUFFIX : ""}(${argList}) ; borrowed result`,
|
||||
e.callee,
|
||||
);
|
||||
if (host.mayThrow.has(e.callee)) host.emitPendingCheck();
|
||||
if (!invoked && host.mayThrow.has(e.callee)) host.emitPendingCheck();
|
||||
return { name: t, type: e.type };
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,285 @@
|
||||
import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { expect, test } from "vitest";
|
||||
import { compile, deserializeModule } from "../../index.js";
|
||||
import { validateModule } from "../../ir/validate.js";
|
||||
import { emitLlvmModule } from "./emitter.js";
|
||||
import { UnwindAbiViolation, bridgePlainUnwindCalls } from "./unwind-abi.js";
|
||||
|
||||
async function moduleFor(source: string) {
|
||||
const dir = await mkdtemp(join(tmpdir(), "scriptc-unwind-abi-"));
|
||||
try {
|
||||
const entry = join(dir, "main.ts"),
|
||||
output = join(dir, "main.ir.json");
|
||||
await writeFile(entry, source);
|
||||
const result = await compile(entry, { outDir: dir, outPath: output, outputKind: "ir" });
|
||||
if (!result.ok)
|
||||
throw new Error(result.diagnostics.map((item) => `${item.code}: ${item.message}`).join("\n"));
|
||||
const module = deserializeModule(await readFile(output, "utf8"));
|
||||
expect(validateModule(module)).toEqual([]);
|
||||
return module;
|
||||
} finally {
|
||||
await rm(dir, { recursive: true, force: true });
|
||||
}
|
||||
}
|
||||
|
||||
/** The definition of the source function `name` (its i32 or borrowing entry when it has one). */
|
||||
function body(ir: string, name: string): string {
|
||||
return new RegExp(
|
||||
`^define internal [^\\n]*@sc_b?f_[^\\n(]*${name}(?:\\.i32)?\\([^]*?^}`,
|
||||
"m",
|
||||
).exec(ir)![0];
|
||||
}
|
||||
|
||||
const SOURCE = `
|
||||
function leaf(n: number): number {
|
||||
if (n === 3) throw new RangeError("three");
|
||||
return n * 2;
|
||||
}
|
||||
function first(n: number): number {
|
||||
return leaf(n) + 1;
|
||||
}
|
||||
function second(n: number): number {
|
||||
return first(n) * 3;
|
||||
}
|
||||
function third(n: number): number {
|
||||
return second(n) - 2;
|
||||
}
|
||||
function mid(n: number): number {
|
||||
const parts: string[] = ["a" + n];
|
||||
const r = third(n);
|
||||
parts.push("b");
|
||||
return r + parts.length;
|
||||
}
|
||||
class Counter {
|
||||
total = 0;
|
||||
add(n: number): void { this.total += mid(n); }
|
||||
}
|
||||
function outer(n: number): string {
|
||||
try { return "ok " + mid(n); } catch (e) { return "caught " + (e as Error).message; }
|
||||
}
|
||||
const counter = new Counter();
|
||||
for (let i = 0; i < 5; i++) { console.log(outer(i)); try { counter.add(i); } catch { console.log("add", i); } }
|
||||
const f = (x: number) => mid(x);
|
||||
console.log(f(1), counter.total);
|
||||
`;
|
||||
|
||||
const LINUX = { targetTriple: "x86_64-unknown-linux-gnu", unwindAbi: true } as const;
|
||||
|
||||
test("directly called functions raise, and their callers invoke them without a pending check", async () => {
|
||||
const ir = emitLlvmModule(await moduleFor(SOURCE), LINUX);
|
||||
// leaf throws: the exception starts on the pending flag, and the three
|
||||
// calls above it test the flag too; mid raises it on.
|
||||
for (const name of ["leaf", "first", "second", "third"])
|
||||
expect(body(ir, name)).not.toContain("@scr_unwind_raise");
|
||||
const mid = body(ir, "mid");
|
||||
expect(mid).toMatch(/= call double @sc_b?f_[^(]*third\(/);
|
||||
expect(mid).toContain("call void @scr_unwind_raise()");
|
||||
// outer invokes mid; the landing pad takes the exception to its handler.
|
||||
const outer = body(ir, "outer");
|
||||
expect(outer).toMatch(
|
||||
/= invoke double @sc_b?f_[^(]*mid[^\n]* to label %inv\.ok\d+ unwind label %inv\.lp\d+/,
|
||||
);
|
||||
expect(outer).toMatch(/inv\.lp\d+:\n\s+%t\d+ = landingpad \{ ptr, i32 \} cleanup/);
|
||||
expect(outer).toContain("personality ptr @__gcc_personality_v0");
|
||||
expect(ir).toContain("declare i32 @__gcc_personality_v0(...)");
|
||||
expect(ir).toContain("declare void @scr_unwind_raise() cold");
|
||||
});
|
||||
|
||||
test("a method of a class outside any hierarchy is on the unwinding ABI; closures keep the flag", async () => {
|
||||
const ir = emitLlvmModule(await moduleFor(SOURCE), LINUX);
|
||||
// Counter.add invokes mid and resumes the unwind from its landing pad; the
|
||||
// closure over mid is a pointer target and returns with the flag.
|
||||
const add = body(ir, "Counter_add");
|
||||
expect(add).toMatch(/invoke double @sc_b?f_[^(]*mid/);
|
||||
expect(add).toMatch(/resume \{ ptr, i32 \} %[\w.]+/);
|
||||
expect(add).not.toContain("@scr_unwind_raise");
|
||||
const closures = ir
|
||||
.split(/\n(?=define )/)
|
||||
.filter((def) => /^define internal [^\n]*@sc_b?f_[^\n(]*fn_/.test(def));
|
||||
expect(closures.length).toBeGreaterThan(0);
|
||||
for (const def of closures) expect(def).not.toContain("call void @scr_unwind_raise()");
|
||||
});
|
||||
|
||||
test("recursive functions keep the pending flag, also when they recurse through a callback", async () => {
|
||||
const source = `
|
||||
function leaf(n: number): number {
|
||||
if (n < 0) throw new RangeError("negative");
|
||||
return n;
|
||||
}
|
||||
function one(n: number): number { return leaf(n) + 1; }
|
||||
function two(n: number): number { return one(n) + 1; }
|
||||
function three(n: number): number { return two(n) + 1; }
|
||||
function four(n: number): number {
|
||||
const parts: string[] = ["a" + n];
|
||||
const r = three(n);
|
||||
return r + parts.length;
|
||||
}
|
||||
function walk(n: number, depth: number): number {
|
||||
const parts: string[] = ["w" + n];
|
||||
const r = depth === 0 ? four(n) : walk(n, depth - 1);
|
||||
return r + parts.length;
|
||||
}
|
||||
function visit(n: number, depth: number, each: (n: number, depth: number) => number): number {
|
||||
const parts: string[] = ["v" + n];
|
||||
const r = depth === 0 ? four(n) : each(n, depth - 1);
|
||||
return r + parts.length;
|
||||
}
|
||||
const each = (n: number, depth: number): number => visit(n, depth, each);
|
||||
for (let i = -1; i < 2; i++) {
|
||||
try { console.log(walk(i, 3), visit(i, 3, each)); } catch (e) { console.log((e as Error).message); }
|
||||
}
|
||||
`;
|
||||
const ir = emitLlvmModule(await moduleFor(source), LINUX);
|
||||
expect(body(ir, "four")).toContain("call void @scr_unwind_raise()");
|
||||
for (const name of ["walk", "visit"]) {
|
||||
expect(body(ir, name)).not.toContain("@scr_unwind_raise");
|
||||
expect(body(ir, name)).toMatch(/invoke double @sc_b?f_[^(]*four/);
|
||||
}
|
||||
});
|
||||
|
||||
test("without the option, and off Linux, emission keeps the pending-flag ABI", async () => {
|
||||
const mod = await moduleFor(SOURCE);
|
||||
for (const options of [{}, { targetTriple: "arm64-apple-macosx14.0.0", unwindAbi: true }]) {
|
||||
const ir = emitLlvmModule(mod, options);
|
||||
expect(ir).not.toContain("invoke ");
|
||||
expect(ir).not.toContain("@scr_unwind_raise");
|
||||
expect(ir).not.toContain("personality");
|
||||
}
|
||||
});
|
||||
|
||||
test("bridgePlainUnwindCalls keeps the pending-flag contract at plain calls of raising functions", () => {
|
||||
const raising = [
|
||||
"define internal ptr @sc_bf_raises(ptr %p) #0 { ; raises",
|
||||
"entry:",
|
||||
" call void @scr_unwind_raise()",
|
||||
" unreachable",
|
||||
"}",
|
||||
].join("\n");
|
||||
const caller = [
|
||||
"define internal ptr @sc_f_adapter(ptr %p0) #0 {",
|
||||
"entry:",
|
||||
" %result = call ptr @sc_bf_raises(ptr %p0) ; borrowed result",
|
||||
" br label %next",
|
||||
"next:",
|
||||
" %v = phi ptr [ %result, %entry ]",
|
||||
" ret ptr %v",
|
||||
"}",
|
||||
].join("\n");
|
||||
const [out] = bridgePlainUnwindCalls([`${raising}\n${caller}`]);
|
||||
expect(out).toContain(
|
||||
"%result.ub = invoke ptr @sc_bf_raises(ptr %p0) to label %ub.n.0 unwind label %ub.lp.0",
|
||||
);
|
||||
expect(out).toContain("%result = phi ptr [ %result.ub, %ub.n.0 ], [ null, %ub.lp.0 ]");
|
||||
// The split block's successor now comes from its last part.
|
||||
expect(out).toContain("%v = phi ptr [ %result, %ub.ok.0 ]");
|
||||
expect(out).toMatch(
|
||||
/define internal ptr @sc_f_adapter\(ptr %p0\) #0 personality ptr @__gcc_personality_v0 \{/,
|
||||
);
|
||||
expect(out).toContain("declare i32 @__gcc_personality_v0(...)");
|
||||
});
|
||||
|
||||
test("an invoked wrapper of a raising function resumes the unwind; its address must not escape", () => {
|
||||
const raising = [
|
||||
"define internal double @sc_bf_body.i32(i32 %a) #0 {",
|
||||
"entry:",
|
||||
" call void @scr_unwind_raise()",
|
||||
" unreachable",
|
||||
"}",
|
||||
].join("\n");
|
||||
const wrapper = [
|
||||
"define internal double @sc_bf_body(double %a) #0 {",
|
||||
"entry:",
|
||||
" %i = fptosi double %a to i32",
|
||||
" %r = call double @sc_bf_body.i32(i32 %i)",
|
||||
" ret double %r",
|
||||
"}",
|
||||
].join("\n");
|
||||
const caller = [
|
||||
"define internal double @sc_bf_caller(double %a) #0 personality ptr @__gcc_personality_v0 {",
|
||||
"entry:",
|
||||
" %r = invoke double @sc_bf_body(double %a) to label %ok unwind label %lp",
|
||||
"lp:",
|
||||
" %x = landingpad { ptr, i32 } cleanup",
|
||||
" call void @scr_unwind_raise()",
|
||||
" unreachable",
|
||||
"ok:",
|
||||
" ret double %r",
|
||||
"}",
|
||||
].join("\n");
|
||||
const [out] = bridgePlainUnwindCalls([[raising, wrapper, caller].join("\n")]);
|
||||
expect(out).toContain(
|
||||
"%r = invoke double @sc_bf_body.i32(i32 %i) to label %ub.ok.0 unwind label %ub.lp.0",
|
||||
);
|
||||
expect(out).toContain("resume { ptr, i32 } %ub.v.0");
|
||||
expect(() =>
|
||||
bridgePlainUnwindCalls([
|
||||
`${raising}\n@table = internal constant [1 x ptr] [ptr @sc_bf_body.i32]`,
|
||||
]),
|
||||
).toThrow(UnwindAbiViolation);
|
||||
try {
|
||||
bridgePlainUnwindCalls([
|
||||
`${raising}\n@table = internal constant [1 x ptr] [ptr @sc_bf_body.i32]`,
|
||||
]);
|
||||
} catch (error) {
|
||||
expect([...(error as UnwindAbiViolation).symbols]).toEqual(["sc_bf_body.i32"]);
|
||||
}
|
||||
});
|
||||
|
||||
test("an invoked function that never tests the pending flag needs no raise", () => {
|
||||
const raising = [
|
||||
"define internal void @sc_bf_raises() #0 {",
|
||||
"entry:",
|
||||
" call void @scr_unwind_raise()",
|
||||
" unreachable",
|
||||
"}",
|
||||
].join("\n");
|
||||
const leaf = [
|
||||
"define internal double @sc_bf_leaf() #0 {",
|
||||
"entry:",
|
||||
" ret double 1.0",
|
||||
"}",
|
||||
].join("\n");
|
||||
const caller = (callee: string) =>
|
||||
[
|
||||
"define internal double @sc_bf_caller() #0 personality ptr @__gcc_personality_v0 {",
|
||||
"entry:",
|
||||
` %r = invoke double @${callee}() to label %ok unwind label %lp`,
|
||||
"lp:",
|
||||
" %x = landingpad { ptr, i32 } cleanup",
|
||||
" call void @scr_unwind_raise()",
|
||||
" unreachable",
|
||||
"ok:",
|
||||
" ret double %r",
|
||||
"}",
|
||||
].join("\n");
|
||||
expect(() =>
|
||||
bridgePlainUnwindCalls([[raising, leaf, caller("sc_bf_leaf")].join("\n")]),
|
||||
).not.toThrow();
|
||||
const testing = leaf.replace(
|
||||
" ret double 1.0",
|
||||
" %f = load i32, ptr @scr_exc_flag\n ret double 1.0",
|
||||
);
|
||||
expect(() =>
|
||||
bridgePlainUnwindCalls([[raising, testing, caller("sc_bf_leaf")].join("\n")]),
|
||||
).toThrow(UnwindAbiViolation);
|
||||
});
|
||||
|
||||
test("calls that cannot unwind are marked nounwind", () => {
|
||||
const raising = [
|
||||
"define internal void @sc_bf_raises(ptr %b) #0 {",
|
||||
"entry:",
|
||||
" %n = call i64 @scr_bytes_length(ptr %b) ; length",
|
||||
" call void @scr_bytes_release(ptr %b), !dbg !4",
|
||||
" %r = call ptr %b(ptr %b)",
|
||||
" call void @scr_unwind_raise()",
|
||||
" unreachable",
|
||||
"}",
|
||||
].join("\n");
|
||||
const [out] = bridgePlainUnwindCalls([raising]);
|
||||
expect(out).toContain("%n = call i64 @scr_bytes_length(ptr %b) nounwind ; length");
|
||||
expect(out).toContain("call void @scr_bytes_release(ptr %b) nounwind, !dbg !4");
|
||||
expect(out).toContain("%r = call ptr %b(ptr %b) nounwind");
|
||||
expect(out).toContain(" call void @scr_unwind_raise()\n");
|
||||
});
|
||||
@@ -0,0 +1,491 @@
|
||||
/* The functions that propagate exceptions by unwinding instead of the
|
||||
* pending flag (see unwindAbiFunctions). */
|
||||
import type { IrExpr, IrModule } from "../../ir/ir.js";
|
||||
import { everyStmtList } from "../../ir/traverse.js";
|
||||
|
||||
/** How many calls above a `throw` functions keep the pending flag. */
|
||||
const THROW_LEVELS = 3;
|
||||
/** The call graph's node for calls through values. */
|
||||
const ANY_FUNCTION = " any function";
|
||||
|
||||
/** The nodes of `edges` on a cycle (a recursion), by Tarjan's algorithm
|
||||
* without recursion of its own: a large module's call chains are deeper
|
||||
* than the host stack. */
|
||||
function recursiveFunctions(edges: ReadonlyMap<string, readonly string[]>): Set<string> {
|
||||
const index = new Map<string, number>();
|
||||
const low = new Map<string, number>();
|
||||
const stack: string[] = [];
|
||||
const onStack = new Set<string>();
|
||||
const result = new Set<string>();
|
||||
let counter = 0;
|
||||
const enter = (name: string): { name: string; next: number } => {
|
||||
index.set(name, counter);
|
||||
low.set(name, counter++);
|
||||
stack.push(name);
|
||||
onStack.add(name);
|
||||
return { name, next: 0 };
|
||||
};
|
||||
for (const root of edges.keys()) {
|
||||
if (index.has(root)) continue;
|
||||
const work = [enter(root)];
|
||||
while (work.length > 0) {
|
||||
const top = work[work.length - 1]!;
|
||||
const out = edges.get(top.name) ?? [];
|
||||
if (top.next < out.length) {
|
||||
const callee = out[top.next++]!;
|
||||
if (!index.has(callee)) work.push(enter(callee));
|
||||
else if (onStack.has(callee))
|
||||
low.set(top.name, Math.min(low.get(top.name)!, index.get(callee)!));
|
||||
continue;
|
||||
}
|
||||
work.pop();
|
||||
const parent = work[work.length - 1];
|
||||
if (parent !== undefined)
|
||||
low.set(parent.name, Math.min(low.get(parent.name)!, low.get(top.name)!));
|
||||
if (low.get(top.name) !== index.get(top.name)) continue;
|
||||
const members: string[] = [];
|
||||
let member: string;
|
||||
do {
|
||||
member = stack.pop()!;
|
||||
onStack.delete(member);
|
||||
members.push(member);
|
||||
} while (member !== top.name);
|
||||
if (members.length > 1 || out.includes(top.name)) for (const m of members) result.add(m);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/** The may-throw functions whose exceptions reach their callers by
|
||||
* unwinding the native stack (scr_unwind_raise) instead of returning with
|
||||
* the pending flag set. Their callers call them with `invoke`, so the call
|
||||
* needs no flag test; the landing pad runs the frame's unwind.
|
||||
*
|
||||
* A function qualifies only when every way it can be reached is a direct
|
||||
* `call` from emitted code that can lower it as an `invoke`: it is not
|
||||
* async or a generator, not a constructor, not a method a virtual call can
|
||||
* reach or one a vtable holds (an overridden method), not a closure
|
||||
* target, not the entry or a library export, not named by any node other
|
||||
* than a `call`'s callee, and not called from an async or generator body
|
||||
* (coroutine bodies keep plain calls).
|
||||
*
|
||||
* A native unwind costs about 2 µs a raise and 0.1 µs a frame, against a
|
||||
* few nanoseconds for a flag test, so the functions an exception is likely to
|
||||
* cross many of keep the flag: those with a `throw` or rethrow of their own
|
||||
* and those up to THROW_LEVELS calls above one (a handler near the throw
|
||||
* takes it without a native unwind), and the recursive ones (deep stacks
|
||||
* are recursions). */
|
||||
export function unwindAbiFunctions(
|
||||
mod: IrModule,
|
||||
mayThrow: ReadonlySet<string>,
|
||||
excluded: ReadonlySet<string> = new Set(),
|
||||
): Set<string> {
|
||||
const names = new Set(mod.functions.map((fn) => fn.name));
|
||||
const blocked = new Set<string>(excluded);
|
||||
blocked.add(mod.entry);
|
||||
// The runtime calls the error property readers through the pointers the
|
||||
// module installs (emit-module.ts).
|
||||
blocked.add("%error.name.read");
|
||||
blocked.add("%error.message.read");
|
||||
for (const entry of mod.lib?.exports ?? []) blocked.add(entry.fnName);
|
||||
// Methods a virtual call can reach, and the overridden methods of class
|
||||
// hierarchies (whose vtables list them), are called through pointers; the
|
||||
// others are called directly, like functions. A virtual call reaches its listed
|
||||
// targets, or else the method in its receiver class's hierarchy; a
|
||||
// receiver that is no class of the module (an interface) may reach any
|
||||
// class's method of that name.
|
||||
const classes = new Map((mod.classes ?? []).map((cls) => [cls.name, cls]));
|
||||
const anyClass = new Set<string>();
|
||||
for (const fn of mod.functions)
|
||||
everyStmtList(fn.body, {
|
||||
expr: (e: IrExpr) => {
|
||||
if (e.kind !== "virtualCall") return true;
|
||||
if (e.targets !== undefined) for (const target of e.targets) blocked.add(target);
|
||||
else if (classes.has(e.className)) blocked.add(`%${e.className}.${e.method}`);
|
||||
else anyClass.add(e.method);
|
||||
return true;
|
||||
},
|
||||
stmt: () => true,
|
||||
});
|
||||
// A hierarchy's vtables hold the methods declared by more than one of its
|
||||
// classes (an override and what it overrides).
|
||||
const rootOf = (name: string): string => {
|
||||
let cls = classes.get(name);
|
||||
while (cls?.base !== undefined && classes.has(cls.base)) cls = classes.get(cls.base);
|
||||
return cls?.name ?? name;
|
||||
};
|
||||
const declarers = new Map<string, number>();
|
||||
for (const cls of mod.classes ?? [])
|
||||
for (const m of cls.methods ?? []) {
|
||||
const key = `${rootOf(cls.name)}\0${m}`;
|
||||
declarers.set(key, (declarers.get(key) ?? 0) + 1);
|
||||
}
|
||||
for (const cls of mod.classes ?? []) {
|
||||
blocked.add(`%${cls.name}.constructor`);
|
||||
blocked.add(`%${cls.name}.construct`);
|
||||
const root = rootOf(cls.name);
|
||||
for (const m of cls.methods ?? [])
|
||||
if (anyClass.has(m) || declarers.get(`${root}\0${m}`)! > 1) blocked.add(`%${cls.name}.${m}`);
|
||||
}
|
||||
// Any function a node names other than as a call's callee may be reached
|
||||
// through a pointer.
|
||||
const referenced = new Set<string>();
|
||||
const noteNames = (node: object, skip: string | undefined): void => {
|
||||
for (const key in node) {
|
||||
if (key === skip) continue;
|
||||
const value = (node as Record<string, unknown>)[key];
|
||||
if (typeof value === "string" && names.has(value)) referenced.add(value);
|
||||
}
|
||||
};
|
||||
// The call graph, for the functions near a throw and the recursive ones.
|
||||
// Calls through values (closures, class values, dynamic calls, callbacks
|
||||
// passed to the runtime) reach ANY_FUNCTION, which reaches every function
|
||||
// a value can hold.
|
||||
const throws = new Set<string>();
|
||||
const callers = new Map<string, string[]>();
|
||||
const callees = new Map<string, string[]>();
|
||||
const edge = (from: string, to: string): void => {
|
||||
let list = callers.get(to);
|
||||
if (list === undefined) callers.set(to, (list = []));
|
||||
list.push(from);
|
||||
let out = callees.get(from);
|
||||
if (out === undefined) callees.set(from, (out = []));
|
||||
out.push(to);
|
||||
};
|
||||
const methodsNamed = new Map<string, string[]>();
|
||||
for (const cls of mod.classes ?? [])
|
||||
for (const m of cls.methods ?? []) {
|
||||
let list = methodsNamed.get(m);
|
||||
if (list === undefined) methodsNamed.set(m, (list = []));
|
||||
list.push(`%${cls.name}.${m}`);
|
||||
}
|
||||
for (const fn of mod.functions) {
|
||||
const coroutine = fn.async === true || fn.generator !== undefined;
|
||||
everyStmtList(fn.body, {
|
||||
expr: (e: IrExpr) => {
|
||||
const kind = e.kind as string;
|
||||
if (kind === "throw" || kind === "rethrow") throws.add(fn.name);
|
||||
if (e.kind === "call") {
|
||||
edge(fn.name, e.callee);
|
||||
// The entry's callees (the module initializers) run once: on the
|
||||
// flag they keep landing pads out of the top-level code inlined
|
||||
// into the entry.
|
||||
if (coroutine || fn.name === mod.entry) blocked.add(e.callee);
|
||||
noteNames(e, "callee");
|
||||
return true;
|
||||
}
|
||||
noteNames(e, undefined);
|
||||
if (e.kind === "virtualCall")
|
||||
for (const target of e.targets ?? methodsNamed.get(e.method) ?? []) edge(fn.name, target);
|
||||
else if (e.kind === "new") {
|
||||
edge(fn.name, `%${e.className}.constructor`);
|
||||
edge(fn.name, `%${e.className}.construct`);
|
||||
} else if (kind === "callValue" || kind === "dynCall" || kind === "newValue")
|
||||
edge(fn.name, ANY_FUNCTION);
|
||||
else if ("args" in e) {
|
||||
const args: unknown = e.args;
|
||||
if (Array.isArray(args))
|
||||
for (const arg of args as IrExpr[])
|
||||
if (arg.type.kind === "func") edge(fn.name, ANY_FUNCTION);
|
||||
}
|
||||
return true;
|
||||
},
|
||||
stmt: (s) => {
|
||||
const kind = s.kind as string;
|
||||
if (kind === "throw" || kind === "rethrow") throws.add(fn.name);
|
||||
noteNames(s, undefined);
|
||||
return true;
|
||||
},
|
||||
});
|
||||
}
|
||||
for (const name of referenced) {
|
||||
blocked.add(name);
|
||||
edge(ANY_FUNCTION, name);
|
||||
}
|
||||
// A throw starts on the pending flag, and so do the functions up to
|
||||
// THROW_LEVELS calls above it. The union narrowing helpers' throws (a
|
||||
// failed narrowing) don't count for their callers.
|
||||
for (const name of throws) blocked.add(name);
|
||||
let level = [...throws].filter((name) => !name.startsWith("%union."));
|
||||
const near = new Set(level);
|
||||
for (let i = 0; i < THROW_LEVELS; i++) {
|
||||
const next: string[] = [];
|
||||
for (const name of level)
|
||||
for (const caller of callers.get(name) ?? [])
|
||||
if (!near.has(caller)) {
|
||||
near.add(caller);
|
||||
next.push(caller);
|
||||
}
|
||||
level = next;
|
||||
}
|
||||
for (const name of near) blocked.add(name);
|
||||
for (const name of recursiveFunctions(callees)) blocked.add(name);
|
||||
const result = new Set<string>();
|
||||
for (const fn of mod.functions) {
|
||||
if (!mayThrow.has(fn.name) || blocked.has(fn.name)) continue;
|
||||
if (fn.async === true || fn.generator !== undefined) continue;
|
||||
result.add(fn.name);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/** What an unwinding-ABI function's unwind ends with when no handler of
|
||||
* its own takes the exception. */
|
||||
export const UNWIND_RAISE = "call void @scr_unwind_raise()\n unreachable";
|
||||
/** What the unwind of a landing pad ends with when no handler of the
|
||||
* function takes the exception: the unwind goes on (`resume`) with the
|
||||
* exception the landing pad stored in UNWIND_SLOT. The block builder
|
||||
* expands it (BlockBuilder.terminate). */
|
||||
export const UNWIND_RESUME = "resume { ptr, i32 } <unwind slot>";
|
||||
/** The function's slot for the exception its landing pads take. */
|
||||
export const UNWIND_SLOT = "%unw.exc";
|
||||
/** scr_unwind_raise's declaration. It is not `noreturn`: sanitized builds
|
||||
* would call the sanitizer's no-return hook before it, which fails on fiber
|
||||
* stacks; scr_unwind_raise clears the unwound frames' shadow itself. */
|
||||
export const UNWIND_RAISE_DECLARATION = "declare void @scr_unwind_raise() cold";
|
||||
|
||||
/** The personality of functions with landing pads: the landing pads are
|
||||
* cleanups that scr_unwind_raise's forced unwind runs. */
|
||||
export const UNWIND_PERSONALITY = "personality ptr @__gcc_personality_v0";
|
||||
|
||||
/** An unwind leaving the function: a raise, or a landing pad's `resume`. */
|
||||
const UNWINDS = /call void @scr_unwind_raise\(\)|\n\s+resume /;
|
||||
/** A pending-flag test: the thread's exception word, the active cell, or the runtime query. */
|
||||
const PENDING_TEST = /@scr_exc_flag\b|%exc\.cell\b|@scr_exc_active\b|@scr_exc_pending\(/;
|
||||
|
||||
/** Functions emission found breaking the unwinding ABI's rules
|
||||
* (bridgePlainUnwindCalls), by symbol: the caller emits the module again
|
||||
* with them on the pending-flag ABI. */
|
||||
export class UnwindAbiViolation extends Error {
|
||||
readonly symbols: ReadonlySet<string>;
|
||||
constructor(symbols: ReadonlySet<string>) {
|
||||
super(`unwinding ABI: ${[...symbols].slice(0, 3).join(", ")} must keep the pending flag`);
|
||||
this.symbols = symbols;
|
||||
}
|
||||
}
|
||||
const DEFINE_NAME = /^define [^@\n]*@([\w.$%-]+)\(/;
|
||||
const PLAIN_CALL = /^(\s*)(%[\w.$-]+) = call (.*?)@([\w.$%-]+)\(|^(\s*)call (.*?)@([\w.$%-]+)\(/;
|
||||
const RETURN_ATTRIBUTES =
|
||||
/\b(?:zeroext|signext|noalias|nonnull|noundef|inreg|dereferenceable(?:_or_null)?\(\d+\)|align \d+)\s+/g;
|
||||
|
||||
/** A call instruction's line. */
|
||||
const CALL_LINE = /^\s*(?:%[\w.$-]+ = )?(?:(?:tail|musttail|notail) )?call /;
|
||||
/** A call's callee (a function or a register) and the parenthesis that
|
||||
* opens its arguments. */
|
||||
const CALLEE = /[@%][\w.$%-]+\(/;
|
||||
|
||||
/** The index of the parenthesis that closes the argument list opened at `open`. */
|
||||
function argumentsEnd(line: string, open: number): number {
|
||||
let depth = 0;
|
||||
let close = open;
|
||||
for (; close < line.length; close++) {
|
||||
const c = line.charAt(close);
|
||||
if (c === "(") depth++;
|
||||
else if (c === ")" && --depth === 0) break;
|
||||
}
|
||||
return close;
|
||||
}
|
||||
|
||||
/** A call that cannot unwind, marked so: only scr_unwind_raise and the
|
||||
* raising functions propagate an unwind (runtime code and every function
|
||||
* reached through a pointer return normally), so every other call is
|
||||
* `nounwind`, and the inliner keeps it a call where it inlines a raising
|
||||
* function at an invoke. */
|
||||
function markNounwind(line: string): string {
|
||||
const call = line.indexOf("call ");
|
||||
const callee = CALLEE.exec(line.slice(call));
|
||||
if (callee === null || callee[0] === "@scr_unwind_raise(") return line;
|
||||
const close = argumentsEnd(line, call + callee.index + callee[0].length - 1);
|
||||
return close >= line.length
|
||||
? line
|
||||
: `${line.slice(0, close + 1)} nounwind${line.slice(close + 1)}`;
|
||||
}
|
||||
|
||||
/** The value a bridged call yields on its landing pad: the dummy a
|
||||
* pending-flag callee returns with an exception pending. */
|
||||
function dummyValue(type: string): string {
|
||||
if (type === "ptr") return "null";
|
||||
if (type === "double" || type === "float") return "0.0";
|
||||
if (/^i\d+$/.test(type)) return "0";
|
||||
return "zeroinitializer";
|
||||
}
|
||||
|
||||
/** Rewrites every remaining plain call of an unwinding-ABI function (one
|
||||
* that raises, scr_unwind_raise, or resumes an unwind) in `parts`, the module's text, into an
|
||||
* invoke whose landing pad continues after the call with the exception
|
||||
* pending and the call's dummy result: the pending-flag contract those
|
||||
* call sites were emitted for, whichever lowering emitted them. Calls of
|
||||
* these functions that emitDirectCall lowered are invokes already. */
|
||||
export function bridgePlainUnwindCalls(parts: string[]): string[] {
|
||||
const raisers = new Set<string>();
|
||||
const split = parts.map((part) => part.split(/\n(?=define )/));
|
||||
for (const chunks of split)
|
||||
for (const chunk of chunks) {
|
||||
const m = DEFINE_NAME.exec(chunk);
|
||||
if (m !== null && UNWINDS.test(chunk)) raisers.add(m[1]!);
|
||||
}
|
||||
if (raisers.size === 0) return parts;
|
||||
// An invoke's callee must raise or never test the pending flag: one that
|
||||
// returned with the flag pending would pass the exception by its invoking
|
||||
// caller unseen. A wrapper an invoke reaches (an int32 or borrowed-ABI
|
||||
// entry forwarding to the body) propagates its callee's unwind instead
|
||||
// (`resume`), so it raises too. A raising function must be reached only by
|
||||
// calls: a pointer to it (a vtable or table entry, a runtime callback)
|
||||
// could be called without a landing pad. Functions that break either rule
|
||||
// go back to the pending-flag ABI (UnwindAbiViolation).
|
||||
const violations = new Set<string>();
|
||||
const invoked = new Set<string>();
|
||||
const testsFlag = new Set<string>();
|
||||
for (const chunks of split)
|
||||
for (const chunk of chunks) {
|
||||
const m = DEFINE_NAME.exec(chunk);
|
||||
if (m !== null && PENDING_TEST.test(chunk)) testsFlag.add(m[1]!);
|
||||
for (const c of chunk.matchAll(/ invoke [^@\n]*@([\w.$%-]+)\(/g)) invoked.add(c[1]!);
|
||||
}
|
||||
const resumers = new Set<string>();
|
||||
for (let grew = true; grew;) {
|
||||
grew = false;
|
||||
for (const chunks of split)
|
||||
for (const chunk of chunks) {
|
||||
const m = DEFINE_NAME.exec(chunk);
|
||||
const name = m?.[1];
|
||||
if (name === undefined || raisers.has(name) || !invoked.has(name)) continue;
|
||||
if (![...chunk.matchAll(/ call [^@\n]*@([\w.$%-]+)\(/g)].some((c) => raisers.has(c[1]!)))
|
||||
continue;
|
||||
if (testsFlag.has(name)) continue;
|
||||
raisers.add(name);
|
||||
resumers.add(name);
|
||||
grew = true;
|
||||
}
|
||||
}
|
||||
for (const name of invoked) if (!raisers.has(name) && testsFlag.has(name)) violations.add(name);
|
||||
for (const chunks of split)
|
||||
for (const chunk of chunks)
|
||||
for (const line of chunk.split("\n")) {
|
||||
if (line.startsWith("define ")) continue;
|
||||
for (const m of line.matchAll(/@([\w.$%-]+)/g)) {
|
||||
if (!raisers.has(m[1]!)) continue;
|
||||
const at = m.index!;
|
||||
if (
|
||||
/(?:call|invoke) [^@]*$/.test(line.slice(0, at)) &&
|
||||
line.charAt(at + m[0].length) === "("
|
||||
)
|
||||
continue;
|
||||
violations.add(m[1]!);
|
||||
}
|
||||
}
|
||||
if (violations.size > 0) throw new UnwindAbiViolation(violations);
|
||||
let personality = false;
|
||||
let counter = 0;
|
||||
const out = split.map((chunks) =>
|
||||
chunks
|
||||
.map((chunk) => {
|
||||
if (!DEFINE_NAME.test(chunk) || !chunk.includes("call ")) return chunk;
|
||||
const lines = chunk.split("\n");
|
||||
const resume = resumers.has(DEFINE_NAME.exec(chunk)![1]!);
|
||||
const result: string[] = [];
|
||||
// The label a block's terminator now follows, for phi operands.
|
||||
const moved = new Map<string, string>();
|
||||
let block = "";
|
||||
let changed = false;
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
const line = lines[i]!;
|
||||
const label = /^([\w.$-]+):/.exec(line);
|
||||
if (label !== null) block = label[1]!;
|
||||
const m = PLAIN_CALL.exec(line);
|
||||
const callee = m === null ? undefined : (m[4] ?? m[7]);
|
||||
if (m === null || callee === undefined || !raisers.has(callee)) {
|
||||
const marked = CALL_LINE.test(line) ? markNounwind(line) : line;
|
||||
changed ||= marked !== line;
|
||||
result.push(marked);
|
||||
continue;
|
||||
}
|
||||
// The call's text from its callee to the end of the argument list.
|
||||
const close = argumentsEnd(line, line.indexOf(`@${callee}(`) + callee.length + 1);
|
||||
const head = line.slice(line.indexOf("call ") + 5, close + 1);
|
||||
const tail = line.slice(close + 1);
|
||||
const meta = /,\s*!/.exec(tail);
|
||||
const attrs = (
|
||||
meta === null ? tail.replace(/;.*$/, "") : tail.slice(0, meta.index)
|
||||
).trimEnd();
|
||||
const dbg = meta === null ? "" : tail.slice(meta.index).replace(/\s;.*$/, "");
|
||||
const n = counter++;
|
||||
if (block === "") {
|
||||
result.splice(1, 0, "entry.ub:");
|
||||
block = "entry.ub";
|
||||
}
|
||||
const indent = m[1] ?? m[5] ?? " ";
|
||||
const ok = `ub.ok.${n}`;
|
||||
const pad = `ub.lp.${n}`;
|
||||
if (resume) {
|
||||
// The wrapper holds nothing to release: the unwind goes on to
|
||||
// its caller's landing pad.
|
||||
const assign = m[2] !== undefined ? `${m[2]} = ` : "";
|
||||
result.push(
|
||||
`${indent}${assign}invoke ${head}${attrs} to label %${ok} unwind label %${pad}${dbg}`,
|
||||
);
|
||||
result.push(
|
||||
`${pad}:`,
|
||||
`${indent}%ub.v.${n} = landingpad { ptr, i32 } cleanup`,
|
||||
`${indent}resume { ptr, i32 } %ub.v.${n}`,
|
||||
);
|
||||
result.push(`${ok}:`);
|
||||
} else if (m[2] !== undefined) {
|
||||
const type = (m[3] ?? "").replace(RETURN_ATTRIBUTES, "").trim();
|
||||
const normal = `ub.n.${n}`;
|
||||
result.push(
|
||||
`${indent}${m[2]}.ub = invoke ${head}${attrs} to label %${normal} unwind label %${pad}${dbg}`,
|
||||
);
|
||||
result.push(`${normal}:`, `${indent}br label %${ok}`);
|
||||
result.push(
|
||||
`${pad}:`,
|
||||
`${indent}%ub.v.${n} = landingpad { ptr, i32 } cleanup`,
|
||||
`${indent}br label %${ok}`,
|
||||
);
|
||||
result.push(
|
||||
`${ok}:`,
|
||||
`${indent}${m[2]} = phi ${type} [ ${m[2]}.ub, %${normal} ], [ ${dummyValue(type)}, %${pad} ]`,
|
||||
);
|
||||
} else {
|
||||
result.push(
|
||||
`${indent}invoke ${head}${attrs} to label %${ok} unwind label %${pad}${dbg}`,
|
||||
);
|
||||
result.push(
|
||||
`${pad}:`,
|
||||
`${indent}%ub.v.${n} = landingpad { ptr, i32 } cleanup`,
|
||||
`${indent}br label %${ok}`,
|
||||
);
|
||||
result.push(`${ok}:`);
|
||||
}
|
||||
moved.set(block, ok);
|
||||
changed = true;
|
||||
}
|
||||
if (!changed) return chunk;
|
||||
if (moved.size === 0) return result.join("\n");
|
||||
personality = true;
|
||||
const header = result[0]!;
|
||||
const brace = header.lastIndexOf(" {");
|
||||
if (!header.includes("personality") && brace >= 0) {
|
||||
// Before the function's metadata attachments (`!dbg`), if any.
|
||||
const dbg = header.slice(0, brace).search(/\s!dbg !\d+$/);
|
||||
const at = dbg >= 0 ? dbg : brace;
|
||||
result[0] = `${header.slice(0, at)} ${UNWIND_PERSONALITY}${header.slice(at)}`;
|
||||
}
|
||||
// A split block's successors now come from its last part.
|
||||
if (moved.size > 0)
|
||||
for (let i = 0; i < result.length; i++) {
|
||||
const line = result[i]!;
|
||||
if (!line.includes(" phi ")) continue;
|
||||
result[i] = line.replace(/, %([\w.$-]+) \]/g, (all, name: string) => {
|
||||
const to = moved.get(name);
|
||||
return to === undefined ? all : `, %${to} ]`;
|
||||
});
|
||||
}
|
||||
return result.join("\n");
|
||||
})
|
||||
.join("\n"),
|
||||
);
|
||||
if (personality && !out.some((part) => part.includes("@__gcc_personality_v0(...)")))
|
||||
out[0] = `declare i32 @__gcc_personality_v0(...)\n${out[0]}`;
|
||||
return out;
|
||||
}
|
||||
@@ -43,6 +43,7 @@ import {
|
||||
cacheTargetIdentity,
|
||||
isZigDriver,
|
||||
isMuslTarget,
|
||||
type CcDriver,
|
||||
} from "./driver.js";
|
||||
import {
|
||||
resolveProgramShardMergeIdentity,
|
||||
@@ -821,6 +822,7 @@ export async function compileCInternal(
|
||||
// generated/native input that references inflate symbols. Cross
|
||||
// and targetless Zig builds use vendored zlib objects in the input section above.
|
||||
...(((opts.zlib ?? false) || nativeFetch) && !isZigDriver(driver) ? ["-lz"] : []),
|
||||
...unwinderLinkArgs(driver),
|
||||
// glibc keeps libm separate from libc. This must trail the generated
|
||||
// program and every native FFI input because GNU ld resolves archives
|
||||
// from left to right.
|
||||
@@ -1068,6 +1070,7 @@ export async function compileCInternal(
|
||||
...(mbedtls && targetPlatform(driver) === "win32" ? ["-lbcrypt"] : []),
|
||||
...(tlsCa && targetPlatform(driver) === "win32" ? ["-lcrypt32"] : []),
|
||||
...(((opts.zlib ?? false) || nativeFetch) && !isZigDriver(driver) ? ["-lz"] : []),
|
||||
...unwinderLinkArgs(driver),
|
||||
...driver.linkArgs,
|
||||
...executableLinkFlags,
|
||||
];
|
||||
@@ -1080,6 +1083,7 @@ export async function compileCInternal(
|
||||
...(mbedtls && targetPlatform(driver) === "win32" ? ["-lbcrypt"] : []),
|
||||
...(tlsCa && targetPlatform(driver) === "win32" ? ["-lcrypt32"] : []),
|
||||
...(((opts.zlib ?? false) || nativeFetch) && !isZigDriver(driver) ? ["-lz"] : []),
|
||||
...unwinderLinkArgs(driver),
|
||||
...driver.linkArgs,
|
||||
...executableLinkFlags,
|
||||
];
|
||||
@@ -1689,3 +1693,11 @@ export async function compileC(opts: CcOptions): Promise<void> {
|
||||
await rm(`${opts.outPath}.dSYM`, { recursive: true, force: true });
|
||||
}
|
||||
}
|
||||
|
||||
/** Linux runtimes raise through the native unwinder (scr_unwind_raise):
|
||||
* Zig's libunwind, or the toolchain's libgcc_eh. Both are static archives,
|
||||
* so programs load no shared library for them. */
|
||||
function unwinderLinkArgs(driver: CcDriver): string[] {
|
||||
if (targetPlatform(driver) !== "linux") return [];
|
||||
return isZigDriver(driver) ? ["-lunwind"] : ["-lgcc_eh"];
|
||||
}
|
||||
|
||||
@@ -53,6 +53,7 @@ export const EXECUTABLE_RUNTIME_SOURCES = [
|
||||
"scr_object.c",
|
||||
"scr_union.c",
|
||||
"scr_exception.c",
|
||||
"scr_unwind.c",
|
||||
"scr_error.c",
|
||||
"scr_console.c",
|
||||
"scr_lib.c",
|
||||
|
||||
@@ -875,6 +875,7 @@ async function prepareExecutableInput(
|
||||
pointerBits: buildPlatform === "wasi" ? 32 : 64,
|
||||
wasi: buildPlatform === "wasi",
|
||||
runtimeAbiMarker: useRuntimePack,
|
||||
unwindAbi: buildPlatform === "linux",
|
||||
objectAudit: opts.sanitize === true,
|
||||
lowRegion: lowRegionTarget(buildPlatform, opts.sanitize === true),
|
||||
...(opts.sanitize === true ? { wasmBoundsChecks: true } : {}),
|
||||
|
||||
@@ -408,6 +408,7 @@ export function prepareNativeExecutable(
|
||||
pointerBits: toolchain.target.pointerBits,
|
||||
wasi: toolchain.target.platform === "wasi",
|
||||
runtimeAbiMarker: outputKind === "obj" || outputKind === "exe",
|
||||
unwindAbi: outputKind === "exe" && toolchain.target.platform === "linux",
|
||||
objectAudit: options.sanitize === true,
|
||||
lowRegion: lowRegionTarget(toolchain.target.platform, options.sanitize === true),
|
||||
functionWorkers: emitWorkers(),
|
||||
|
||||
@@ -24,6 +24,10 @@ process.env.SCRIPTC_RUNTIME_PACK_CONFIG = JSON.stringify({
|
||||
},
|
||||
...toolchain,
|
||||
compileFlags: ["-ffunction-sections", "-fdata-sections"],
|
||||
systemLibraries: [{ name: "m", predicate: true }],
|
||||
// scr_unwind_raise: the static libgcc_eh, which loads no shared library.
|
||||
systemLibraries: [
|
||||
{ name: "m", predicate: true },
|
||||
{ name: "gcc_eh", predicate: true },
|
||||
],
|
||||
});
|
||||
await import("../../runtime-pack-common/scripts/build.mjs");
|
||||
|
||||
@@ -26,6 +26,10 @@ process.env.SCRIPTC_RUNTIME_PACK_CONFIG = JSON.stringify({
|
||||
},
|
||||
targetArgs: ["-target", "aarch64-linux-musl"],
|
||||
compileFlags: ["-ffunction-sections", "-fdata-sections"],
|
||||
systemLibraries: [{ name: "m", predicate: true }],
|
||||
// scr_unwind_raise: Zig's libunwind.
|
||||
systemLibraries: [
|
||||
{ name: "m", predicate: true },
|
||||
{ name: "unwind", predicate: true },
|
||||
],
|
||||
});
|
||||
await import("../../runtime-pack-common/scripts/build.mjs");
|
||||
|
||||
@@ -24,6 +24,10 @@ process.env.SCRIPTC_RUNTIME_PACK_CONFIG = JSON.stringify({
|
||||
},
|
||||
...toolchain,
|
||||
compileFlags: ["-ffunction-sections", "-fdata-sections"],
|
||||
systemLibraries: [{ name: "m", predicate: true }],
|
||||
// scr_unwind_raise: the static libgcc_eh, which loads no shared library.
|
||||
systemLibraries: [
|
||||
{ name: "m", predicate: true },
|
||||
{ name: "gcc_eh", predicate: true },
|
||||
],
|
||||
});
|
||||
await import("../../runtime-pack-common/scripts/build.mjs");
|
||||
|
||||
@@ -26,6 +26,10 @@ process.env.SCRIPTC_RUNTIME_PACK_CONFIG = JSON.stringify({
|
||||
},
|
||||
targetArgs: ["-target", "x86_64-linux-musl"],
|
||||
compileFlags: ["-ffunction-sections", "-fdata-sections"],
|
||||
systemLibraries: [{ name: "m", predicate: true }],
|
||||
// scr_unwind_raise: Zig's libunwind.
|
||||
systemLibraries: [
|
||||
{ name: "m", predicate: true },
|
||||
{ name: "unwind", predicate: true },
|
||||
],
|
||||
});
|
||||
await import("../../runtime-pack-common/scripts/build.mjs");
|
||||
|
||||
@@ -30,6 +30,7 @@ export const BASE_RUNTIME_SOURCES = [
|
||||
"scr_object.c",
|
||||
"scr_union.c",
|
||||
"scr_exception.c",
|
||||
"scr_unwind.c",
|
||||
"scr_error.c",
|
||||
"scr_console.c",
|
||||
"scr_lib.c",
|
||||
|
||||
@@ -123,6 +123,11 @@ static inline ScrExcCell *scr_exc_swap_cell_inline(ScrExcCell *cell) {
|
||||
_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");
|
||||
|
||||
/* Unwinds the native stack to the nearest landing pad of emitted code with
|
||||
* the exception pending in the active cell (the unwinding ABI of emitted
|
||||
* functions, unwind-abi.ts). Linux (glibc and musl) only; it never
|
||||
* returns. */
|
||||
SCR_COLD _Noreturn void scr_unwind_raise(void);
|
||||
SCR_COLD void scr_throw_f64(double v);
|
||||
SCR_COLD void scr_throw_bool(bool v);
|
||||
SCR_COLD void scr_throw_str(ScrStr *v); /* takes ownership */
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
/* scr_unwind_raise: the native unwind of the unwinding exception ABI of
|
||||
* emitted functions (the compiler's unwind-abi.ts). Linux (glibc and musl)
|
||||
* only; elsewhere this unit is empty. */
|
||||
#include "scr_runtime.h"
|
||||
|
||||
#if defined(__linux__) && !defined(__ANDROID__) && !defined(__wasi__)
|
||||
#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <unwind.h>
|
||||
|
||||
/* Weak, so the runtime unit alone does not link the unwinder into every
|
||||
* program: programs that raise reference the personality routine from
|
||||
* their landing pads, and its unit references the unwinder's entry points,
|
||||
* which link it in. */
|
||||
#pragma weak _Unwind_ForcedUnwind
|
||||
#pragma weak _Unwind_GetCFA
|
||||
|
||||
#if defined(__SANITIZE_ADDRESS__)
|
||||
#define SCR_UNWIND_ASAN 1
|
||||
#elif defined(__has_feature)
|
||||
#if __has_feature(address_sanitizer)
|
||||
#define SCR_UNWIND_ASAN 1
|
||||
#endif
|
||||
#endif
|
||||
#ifdef SCR_UNWIND_ASAN
|
||||
#include <sanitizer/asan_interface.h>
|
||||
#endif
|
||||
|
||||
#ifdef SCR_UNWIND_ASAN
|
||||
/* The lowest stack address the unwind has not cleared yet. The unwind
|
||||
* outlives the frame that started it: landing pads resume it. */
|
||||
static SCR_TL uintptr_t scr_unwind_low;
|
||||
#endif
|
||||
|
||||
/* Emitted landing pads are cleanups: they release the frame's references
|
||||
* and then either take the exception (a handler of theirs, or a frame on
|
||||
* the pending-flag ABI that returns with it pending) or resume the unwind.
|
||||
* So the unwind is forced, a single pass through every landing pad; the
|
||||
* exception itself stays in the active cell. */
|
||||
static _Unwind_Reason_Code scr_unwind_stop(int version, _Unwind_Action actions,
|
||||
_Unwind_Exception_Class cls,
|
||||
struct _Unwind_Exception *exc,
|
||||
struct _Unwind_Context *context, void *arg) {
|
||||
(void)version;
|
||||
(void)cls;
|
||||
(void)exc;
|
||||
#ifdef SCR_UNWIND_ASAN
|
||||
/* The frames this unwind passes never return, so it clears their
|
||||
* redzones' shadow itself. Emitted code calls scr_unwind_raise as an
|
||||
* ordinary function: the sanitizer's hook before no-return calls needs
|
||||
* stack bounds, which fiber stacks do not report. */
|
||||
uintptr_t cfa = _Unwind_GetCFA(context);
|
||||
if (cfa > scr_unwind_low) {
|
||||
__asan_unpoison_memory_region((void *)scr_unwind_low, cfa - scr_unwind_low);
|
||||
scr_unwind_low = cfa;
|
||||
}
|
||||
#else
|
||||
(void)context;
|
||||
#endif
|
||||
(void)arg;
|
||||
if (actions & _UA_END_OF_STACK) {
|
||||
fputs("scriptc: an exception unwound past the program's outermost frame\n", stderr);
|
||||
abort();
|
||||
}
|
||||
return _URC_NO_REASON;
|
||||
}
|
||||
|
||||
static SCR_TL struct _Unwind_Exception scr_unwind_exception;
|
||||
|
||||
void scr_unwind_raise(void) {
|
||||
struct _Unwind_Exception *exc = &scr_unwind_exception;
|
||||
memset(exc, 0, sizeof *exc);
|
||||
exc->exception_class = 0x5343524950540000ull; /* "SCRIPT\0\0" */
|
||||
#ifdef SCR_UNWIND_ASAN
|
||||
scr_unwind_low = (uintptr_t)__builtin_frame_address(0);
|
||||
#endif
|
||||
if (_Unwind_ForcedUnwind != NULL) _Unwind_ForcedUnwind(exc, scr_unwind_stop, NULL);
|
||||
fputs("scriptc: the native unwinder could not raise an exception\n", stderr);
|
||||
abort();
|
||||
}
|
||||
#endif
|
||||
@@ -395,7 +395,9 @@ export function dynamicTierSymbols(names) {
|
||||
* bench/channel-messages and bench/worker-messages, added later, link them
|
||||
* through worker messages:
|
||||
* node:worker_threads values are dyn objects. bench/json-unions, added
|
||||
* later too, decodes JSON.parse trees. bench/dynamic-methods and
|
||||
* later too, decodes JSON.parse trees. bench/throw-chains, also added
|
||||
* later, tests caught values with `instanceof`, as bench/validate-errors
|
||||
* does. bench/dynamic-methods and
|
||||
* bench/dynamic-properties are untyped JavaScript. Every other bench workload is fully typed and must link none. The gate and
|
||||
* tests/harness/bench-dynamic-tier.test.ts enforce that, and the test also
|
||||
* fails when a listed workload stops linking them, so remove a workload here
|
||||
@@ -417,6 +419,7 @@ export const DYNAMIC_WORKLOADS = new Set([
|
||||
"bench/log-summary",
|
||||
"bench/inventory-report",
|
||||
"bench/json-unions",
|
||||
"bench/throw-chains",
|
||||
]);
|
||||
|
||||
/** Names of the functions (text and weak symbols) in `nm` output. Mach-O
|
||||
|
||||
Reference in New Issue
Block a user