Give every native function an unwind table entry (#2377)

This commit is contained in:
Chris Tate
2026-10-26 22:54:13 -05:00
committed by GitHub
7 changed files with 32 additions and 21 deletions
+1 -1
View File
@@ -54,7 +54,7 @@ test.runIf(process.platform === "linux" && process.arch === "x64")(
expect(baseline).not.toContain("sanitize_address");
expect(await build(["--target-cpu=baseline"])).toBe(baseline);
const v3 = await build(["--target-cpu=x86-64-v3"]);
expect(v3).toMatch(/^attributes #0 = \{ "target-cpu"="x86-64-v3" \}$/m);
expect(v3).toMatch(/^attributes #0 = \{ uwtable "target-cpu"="x86-64-v3" \}$/m);
if (runsV3) expect((await execFileAsync(outPath)).stdout).toBe(expected);
// The bootstrap's cached route builds without a level.
expect(await build([], true)).toBe(baseline);
@@ -18,10 +18,10 @@ const COPY_GROUPS: ReadonlyMap<string, string> = new Map([
["2", "4"],
]);
export function cpuDispatchAttributes(asan: string): string[] {
export function cpuDispatchAttributes(common: string): string[] {
return [
`attributes #3 = {${asan} "target-cpu"="x86-64-v3" }`,
`attributes #4 = { noinline${asan} "target-cpu"="x86-64-v3" }`,
`attributes #3 = {${common} "target-cpu"="x86-64-v3" }`,
`attributes #4 = { noinline${common} "target-cpu"="x86-64-v3" }`,
];
}
@@ -199,19 +199,22 @@ function readRange(dir: string, range: number): { result: ChunkResult; texts: st
/** The attribute groups of the emitted functions: #0 for every function,
* #1 for coroutine bodies and #2 for out-of-line record clones. Each carries
* the build's `--target-cpu` level, and `sanitize_address` in sanitized
* builds. A group needs one attribute: without either, #0 holds a string
* attribute that LLVM ignores. Dispatching builds add the x86-64-v3 copies'
* #3 and #4 (cpu-dispatch.ts). */
* the build's `--target-cpu` level, `sanitize_address` in sanitized builds,
* and `uwtable` on native targets, so every function has an unwind table
* entry, including those LLVM infers `nounwind` (profilers and debuggers
* unwind through them). A group needs one attribute: without any, #0 holds a
* string attribute that LLVM ignores. Dispatching builds add the x86-64-v3
* copies' #3 and #4 (cpu-dispatch.ts). */
function functionAttributes(emitter: LlEmitter, hasNoInlineRecordClone: boolean): string[] {
const cpu = targetCpuAttribute(emitter.targetCpu);
const asan = emitter.sanitizeAttributes ? " sanitize_address" : "";
const base = `${asan}${cpu}` || ` "scriptc-function"`;
const common = `${asan}${emitter.wasi ? "" : " uwtable"}`;
const base = `${common}${cpu}` || ` "scriptc-function"`;
return [
`attributes #0 = {${base} }`,
...(emitter.usesCoroutines ? [`attributes #1 = {${asan} presplitcoroutine${cpu} }`] : []),
...(hasNoInlineRecordClone ? [`attributes #2 = { noinline${asan}${cpu} }`] : []),
...(emitter.cpuDispatch ? cpuDispatchAttributes(asan) : []),
...(emitter.usesCoroutines ? [`attributes #1 = {${common} presplitcoroutine${cpu} }`] : []),
...(hasNoInlineRecordClone ? [`attributes #2 = { noinline${common}${cpu} }`] : []),
...(emitter.cpuDispatch ? cpuDispatchAttributes(common) : []),
];
}
@@ -206,7 +206,7 @@ test("a target CPU level reaches every function attribute group and nothing else
const plain = emitLlvmModule(module);
expect(plain).not.toContain("target-cpu");
const v3 = emitLlvmModule(module, { targetCpu: "x86-64-v3" });
expect(v3).toMatch(/^attributes #0 = \{ sanitize_address "target-cpu"="x86-64-v3" \}$/m);
expect(v3).toMatch(/^attributes #0 = \{ sanitize_address uwtable "target-cpu"="x86-64-v3" \}$/m);
expect(v3.replaceAll(' "target-cpu"="x86-64-v3"', "")).toBe(plain);
expect(emitLlvmModule(module, { targetCpu: "baseline" })).toBe(plain);
});
@@ -215,13 +215,21 @@ test("only sanitized builds mark the emitted functions sanitize_address", () =>
const module = moduleFor([call()]);
const sanitized = emitLlvmModule(module);
expect(emitLlvmModule(module, { sanitizeAttributes: true })).toBe(sanitized);
expect(sanitized).toMatch(/^attributes #0 = \{ sanitize_address \}$/m);
expect(sanitized).toMatch(/^attributes #0 = \{ sanitize_address uwtable \}$/m);
const plain = emitLlvmModule(module, { sanitizeAttributes: false });
expect(plain).not.toContain("sanitize_address");
// A group needs an attribute: an ignored string attribute stands in.
expect(plain).toMatch(/^attributes #0 = \{ "scriptc-function" \}$/m);
expect(plain).toMatch(/^attributes #0 = \{ uwtable \}$/m);
const v3 = emitLlvmModule(module, { sanitizeAttributes: false, targetCpu: "x86-64-v3" });
expect(v3).toMatch(/^attributes #0 = \{ "target-cpu"="x86-64-v3" \}$/m);
expect(v3).toMatch(/^attributes #0 = \{ uwtable "target-cpu"="x86-64-v3" \}$/m);
});
test("native functions get unwind tables and wasm32-wasi functions do not", () => {
const module = moduleFor([call()]);
expect(emitLlvmModule(module)).toMatch(/^attributes #0 = \{ sanitize_address uwtable \}$/m);
const wasi = emitLlvmModule(module, { wasi: true, pointerBits: 32, sanitizeAttributes: false });
expect(wasi).not.toContain("uwtable");
// A group needs an attribute: an ignored string attribute stands in.
expect(wasi).toMatch(/^attributes #0 = \{ "scriptc-function" \}$/m);
});
test("inline RC inlines every fast path at speed and keeps box releases as calls in release", () => {
@@ -404,7 +404,7 @@ test("large record clones stay outlined while small clones remain inlineable", (
expect(small).toContain("define internal ptr @sc_rclone_small(ptr %src) #0");
expect(small).not.toContain("attributes #2");
expect(large).toContain("define internal ptr @sc_rclone_large(ptr %src) #2");
expect(large).toContain("attributes #2 = { noinline sanitize_address }");
expect(large).toContain("attributes #2 = { noinline sanitize_address uwtable }");
});
test("Uint8ClampedArray stores clamp and round without a runtime call", () => {
@@ -119,10 +119,10 @@ test("a dispatching x86-64 build gives each program function a baseline and an x
for (const [name] of coroutines) expect(defs.has(`${name}.v3`), name).toBe(false);
// Functions used as values are referenced by their dispatching name.
expect(ll).not.toMatch(/ptr @[\w.$-]+\.(v3|base)(?!\()/);
expect(ll).toMatch(/^attributes #3 = \{ sanitize_address "target-cpu"="x86-64-v3" \}$/m);
expect(ll).toMatch(/^attributes #3 = \{ sanitize_address uwtable "target-cpu"="x86-64-v3" \}$/m);
// Unsanitized builds leave sanitize_address out of the copies' groups too.
expect(emit({ ...x86, cpuDispatch: true, sanitizeAttributes: false })).toMatch(
/^attributes #3 = \{ "target-cpu"="x86-64-v3" \}$/m,
/^attributes #3 = \{ uwtable "target-cpu"="x86-64-v3" \}$/m,
);
// main sets the level before anything else runs.
expect(defs.get("main")).toMatch(
@@ -56,7 +56,7 @@ console.log(sum);
expect(definition(llvm, "sc_f_range")).not.toContain("@scr_coro_yield_f64(");
expect(llvm).not.toContain("@scr_gen_take_out_f64(");
expect(llvm).toContain("%ScrGenHead = type { i64, ptr, ptr, i32, %ScrGenSlot }");
expect(llvm).toContain("attributes #1 = { presplitcoroutine }");
expect(llvm).toContain("attributes #1 = { uwtable presplitcoroutine }");
expect(definition(llvm, "sc_gs_range")).toContain("call ptr @scr_gen_new_stackless(");
expect(llvm).not.toContain("@scr_gen_yield_f64(");
});