Number hoisted speculative leaves per function (#2179)

This commit is contained in:
Chris Tate
2026-10-24 23:19:34 -05:00
committed by GitHub
5 changed files with 72 additions and 5 deletions
@@ -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).
@@ -1550,8 +1550,11 @@ export class LlEmitter {
speculationResume: Map<IrStmt, { label: string; placed: boolean }> | null = null;
/** The rewritten values of the writes `speculationResume` resumes at. */
speculationResumeValues: ReadonlyMap<IrStmt, SpeculativeValue> | 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<string, InlinableFunction | undefined>();
/** What speculative loop planning may assume here (SpeculationContext). */
+1 -1
View File
@@ -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 () => {
+2 -2
View File
@@ -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_");
});
@@ -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<string> {
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 = /);
}
});