Find bridgePlainUnwindCalls' calls and mentions with indexOf

bridgePlainUnwindCalls rewrites the module text after emission on Linux.
It split the text with a lookahead regular expression, ran a regular
expression over every line to find mentions of raising functions, and
split every function that has calls into lines to test each one.

- The text splits before each `define` line with indexOf.
- Mentions of raising functions are found by scanning for `@` and reading
  the name that follows it.
- A function without a plain call of a raising function rewrites only the
  lines that hold `call `, marking them nounwind. A function with such a
  call takes the line-by-line path as before.

The emitted text is unchanged.
This commit is contained in:
Chris Tate
2026-10-26 00:40:44 -05:00
parent e2a22aa1bc
commit 2e142d6121
2 changed files with 129 additions and 12 deletions
@@ -283,3 +283,43 @@ test("calls that cannot unwind are marked nounwind", () => {
expect(out).toContain("%r = call ptr %b(ptr %b) nounwind");
expect(out).toContain(" call void @scr_unwind_raise()\n");
});
test("functions without a plain call of a raising function change only their call lines", () => {
const raising = [
"define internal void @sc_bf_raises() #0 {",
"entry:",
" call void @scr_unwind_raise()",
" unreachable",
"}",
].join("\n");
const other = [
"define internal void @sc_bf_raises.copy() #0 {",
"entry:",
" ret void",
"}",
].join("\n");
const plain = [
"define internal double @sc_f_plain(ptr %p) #0 {",
"entry: ; call sites below",
" %a = call double @sc_f_other(ptr %p)",
" %b = tail call double @sc_f_other(ptr %p) ; mentions @ and @sc_bf_raises.copy",
" store ptr @sc_bf_raises.copy, ptr %p",
" ret double %a",
"}",
].join("\n");
const parts = [`${raising}\n${other}`, plain, "@table = internal constant ptr @sc_f_plain"];
const out = bridgePlainUnwindCalls(parts);
expect(out[0]).toBe(parts[0]);
expect(out[1]).toBe(
plain
.replace("@sc_f_other(ptr %p)\n", "@sc_f_other(ptr %p) nounwind\n")
.replace("@sc_f_other(ptr %p) ;", "@sc_f_other(ptr %p) nounwind ;"),
);
expect(out[2]).toBe(parts[2]);
expect(() =>
bridgePlainUnwindCalls([
raising,
plain.replace("store ptr @sc_bf_raises.copy", "store ptr @sc_bf_raises"),
]),
).toThrow(UnwindAbiViolation);
});
@@ -301,6 +301,46 @@ function markNounwind(line: string): string {
: `${line.slice(0, close + 1)} nounwind${line.slice(close + 1)}`;
}
/** The functions of `part`: its text split before each `define` line
* (`part.split(/\n(?=define )/)`), found with indexOf. */
function splitFunctions(part: string): string[] {
const chunks: string[] = [];
let start = 0;
for (let at = part.indexOf("\ndefine "); at >= 0; at = part.indexOf("\ndefine ", at + 1)) {
chunks.push(part.slice(start, at));
start = at + 1;
}
chunks.push(part.slice(start));
return chunks;
}
/** Whether `code` can be part of a symbol name (`[\w.$%-]`). */
function isNameCode(code: number): boolean {
return (
(code >= 97 && code <= 122) ||
(code >= 65 && code <= 90) ||
(code >= 48 && code <= 57) ||
code === 95 /* _ */ ||
code === 46 /* . */ ||
code === 36 /* $ */ ||
code === 37 /* % */ ||
code === 45 /* - */
);
}
/** Calls `each` with every line of `chunk` that holds `call `, by its start
* and end offsets, once per line and in order. Lines without `call ` match
* neither CALL_LINE nor PLAIN_CALL. */
function eachCallLine(chunk: string, each: (start: number, end: number) => void): void {
for (let at = chunk.indexOf("call "); at >= 0;) {
const start = chunk.lastIndexOf("\n", at) + 1;
let end = chunk.indexOf("\n", at);
if (end < 0) end = chunk.length;
each(start, end);
at = chunk.indexOf("call ", end);
}
}
/** 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 {
@@ -318,7 +358,7 @@ function dummyValue(type: string): string {
* 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 )/));
const split = parts.map(splitFunctions);
for (const chunks of split)
for (const chunk of chunks) {
const m = DEFINE_NAME.exec(chunk);
@@ -359,20 +399,29 @@ export function bridgePlainUnwindCalls(parts: string[]): string[] {
}
}
for (const name of invoked) if (!raisers.has(name) && testsFlag.has(name)) violations.add(name);
// Every mention of a raising function outside its `define` line, other
// than as the callee of a call or invoke, found with indexOf.
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]!);
for (let at = chunk.indexOf("@"); at >= 0;) {
let end = at + 1;
while (end < chunk.length && isNameCode(chunk.charCodeAt(end))) end++;
const next = chunk.indexOf("@", end);
if (end === at + 1) {
at = next;
continue;
}
const name = chunk.slice(at + 1, end);
if (raisers.has(name)) {
const start = chunk.lastIndexOf("\n", at) + 1;
const line = chunk.slice(start, at);
if (
!line.startsWith("define ") &&
!(/(?:call|invoke) [^@]*$/.test(line) && chunk.charAt(end) === "(")
)
violations.add(name);
}
at = next;
}
if (violations.size > 0) throw new UnwindAbiViolation(violations);
let personality = false;
@@ -381,6 +430,34 @@ export function bridgePlainUnwindCalls(parts: string[]): string[] {
chunks
.map((chunk) => {
if (!DEFINE_NAME.test(chunk) || !chunk.includes("call ")) return chunk;
// A function without a plain call of a raising function only marks
// its calls nounwind: rewrite just its call lines.
let bridged = false;
const marks: { start: number; end: number; line: string }[] = [];
eachCallLine(chunk, (start, end) => {
if (bridged) return;
const line = chunk.slice(start, end);
const m = PLAIN_CALL.exec(line);
const callee = m === null ? undefined : (m[4] ?? m[7]);
if (m !== null && callee !== undefined && raisers.has(callee)) {
bridged = true;
return;
}
if (!CALL_LINE.test(line)) return;
const marked = markNounwind(line);
if (marked !== line) marks.push({ start, end, line: marked });
});
if (!bridged) {
if (marks.length === 0) return chunk;
const pieces: string[] = [];
let from = 0;
for (const mark of marks) {
pieces.push(chunk.slice(from, mark.start), mark.line);
from = mark.end;
}
pieces.push(chunk.slice(from));
return pieces.join("");
}
const lines = chunk.split("\n");
const resume = resumers.has(DEFINE_NAME.exec(chunk)![1]!);
const result: string[] = [];