diff --git a/packages/compiler/src/backend/llvm/emit-function.ts b/packages/compiler/src/backend/llvm/emit-function.ts index b79e2406d..802a0c384 100644 --- a/packages/compiler/src/backend/llvm/emit-function.ts +++ b/packages/compiler/src/backend/llvm/emit-function.ts @@ -66,6 +66,7 @@ export function emitFunction(emitter: LlEmitter, fn: IrFunction): string { emitter.usesLandingPads = false; emitter.aliasScopedVersions = 0; emitter.findInt32Allocations(fn); + emitter.speculativeLeaves = 0; emitter.currentTier = fn.tier; // Fiber switches restore the active exception cell before control // returns to a synchronous frame (emitPendingCheck). diff --git a/packages/compiler/src/backend/llvm/emitter.ts b/packages/compiler/src/backend/llvm/emitter.ts index a148e4fb7..ab10f0ab5 100644 --- a/packages/compiler/src/backend/llvm/emitter.ts +++ b/packages/compiler/src/backend/llvm/emitter.ts @@ -1550,8 +1550,11 @@ export class LlEmitter { speculationResume: Map | null = null; /** The rewritten values of the writes `speculationResume` resumes at. */ speculationResumeValues: ReadonlyMap | null = null; - /** Hoisted speculative leaves numbered so far in the module. */ - private speculativeLeaves = 0; + /** Hoisted speculative leaves numbered so far in the function being + * emitted (emitFunction resets it): a function's local names must not + * depend on which functions the same emitter emitted before it, since + * function workers each emit a share of the module. */ + speculativeLeaves = 0; private readonly inlinableFunctions = new Map(); /** What speculative loop planning may assume here (SpeculationContext). */ diff --git a/tests/harness/atomics-emission.test.ts b/tests/harness/atomics-emission.test.ts index 2d708e382..9068ad59e 100644 --- a/tests/harness/atomics-emission.test.ts +++ b/tests/harness/atomics-emission.test.ts @@ -59,7 +59,7 @@ console.log(work(new Int32Array(buffer), new Uint8Array(buffer), new Int16Array( // Every runtime call is the cold path for an index out of range. const calls = work.match(/call double @scr_atomics_op\([^\n]*/g) ?? []; expect(calls).toHaveLength(9); - for (const call of calls) expect(call).toMatch(/\) cold$/); + for (const call of calls) expect(call).toMatch(/\)(?: nounwind)? cold$/); }); test("an unchanged local array is borrowed across Atomics operations", async () => { diff --git a/tests/harness/interface-dispatch.test.ts b/tests/harness/interface-dispatch.test.ts index 21e455e70..bf70ecafb 100644 --- a/tests/harness/interface-dispatch.test.ts +++ b/tests/harness/interface-dispatch.test.ts @@ -71,7 +71,7 @@ describe("interface method dispatch", () => { test("calls through an interface reach the implementing methods with typed values", async () => { const llvm = await emittedLlvm("direct", shapes("")); const run = bodies(llvm, "sc_bf_run"); - expect(run).toMatch(/call double @sc_bf__x25_dyn_class_protocol_\d+/); + expect(run).toMatch(/(?:call|invoke) double @sc_bf__x25_dyn_class_protocol_\d+/); expect(run).not.toContain("@scr_dyn_call"); expect(run).not.toContain("@sc_bf__x25_dyn_class_property_"); const dispatch = llvm.slice(llvm.indexOf("; %dyn.class.protocol.0")); @@ -90,7 +90,7 @@ describe("interface method dispatch", () => { console.log(sum([circle, new Rect(1)]));`), ); const sum = bodies(llvm, "sc_bf_sum"); - expect(sum).toMatch(/call double @sc_bf__x25_dyn_class_protocol_\d+/); + expect(sum).toMatch(/(?:call|invoke) double @sc_bf__x25_dyn_class_protocol_\d+/); expect(sum).not.toContain("@scr_dyn_iterator"); expect(sum).not.toContain("@sc_bf__x25_dyn_class_computed_"); }); diff --git a/tests/harness/speculative-leaves-emission.test.ts b/tests/harness/speculative-leaves-emission.test.ts new file mode 100644 index 000000000..16fd298d4 --- /dev/null +++ b/tests/harness/speculative-leaves-emission.test.ts @@ -0,0 +1,63 @@ +/* Speculative integer loops (behavior is in the differential corpus, 7757): + * the locals that hold hoisted calls are numbered per function, so a + * function's text does not depend on the functions emitted before it (the + * native compiler's function workers each emit a share of the module). */ +import { createHash } from "node:crypto"; +import { mkdirSync, readFileSync, writeFileSync } from "node:fs"; +import { join } from "node:path"; +import { expect, test } from "vitest"; +import { compile } from "@scriptc/compiler"; + +const repoRoot = join(import.meta.dirname, "../.."); +const cacheDir = join(repoRoot, "node_modules/.cache/scriptc-tests"); + +async function emittedLlvm(name: string, source: string): Promise { + const key = createHash("sha256").update(source).digest("hex").slice(0, 16); + const outDir = join(cacheDir, `spec-leaves-${key}`); + mkdirSync(outDir, { recursive: true }); + const file = join(outDir, `${name}.ts`); + writeFileSync(file, source); + const result = await compile(file, { + outPath: join(outDir, name), + outDir, + backend: "llvm", + outputKind: "llvm", + }); + if (!result.ok) + throw new Error(result.diagnostics.map((d) => `${d.code}: ${d.message}`).join("\n")); + return readFileSync(result.artifact.path, "utf8"); +} + +function body(llvm: string, name: string): string { + const start = llvm.search(new RegExp(`^define internal [^\\n]*@${name}[.(]`, "m")); + expect(start).toBeGreaterThanOrEqual(0); + return llvm.slice(start, llvm.indexOf("\n}\n", start)); +} + +test("hoisted speculative leaves are numbered per function", async () => { + const llvm = await emittedLlvm( + "leaves", + `let calls = 0; +function noisy(x: number): number { + calls++; + return x % 5 === 0 ? x / 2 : x * 2; +} +function first(n: number): number { + let acc = 1; + for (let i = 0; i < n; i++) acc = (acc * 7 + noisy(i)) % 1000003; + return acc; +} +function second(n: number): number { + let acc = 3; + for (let i = 0; i < n; i++) acc = (acc * 5 + noisy(i)) % 1000003; + return acc; +} +console.log(first(20), second(20), calls); +`, + ); + for (const name of ["sc_f_first", "sc_f_second"]) { + const leaves = body(llvm, name).match(/%\w*spec_leaf_\d+ = alloca double/g) ?? []; + expect(leaves).toHaveLength(1); + expect(leaves[0]).toMatch(/_spec_leaf_0 = /); + } +});