feat(compiler): bootstrap the LLVM emitter natively

- Run the production LLVM backend natively and reproduce its output across compiler generations.
- Replace unchecked emitter contexts with explicit shared state and callback hosts.
- Preserve NaN in serialized IR and respect optional record storage during spreads.
This commit is contained in:
Chris Tate
2026-09-27 22:29:24 -05:00
committed by GitHub
parent 37a25ebf7f
commit 7b007e03ef
50 changed files with 1053 additions and 614 deletions
@@ -0,0 +1,44 @@
interface Entry { kind: string; tag: number; values: string[] }
const source: Entry[] = [
{ kind: "first", tag: 1, values: ["one"] },
{ kind: "second", tag: 2, values: ["two"] },
];
const defaults: Entry = { kind: "absent", tag: -1, values: ["default"] };
// Even dense slice callbacks use the runtime-optional record ABI.
const dense = source.slice().map((entry) => ({ ...defaults, ...entry, label: "copy" }));
console.log("dense", dense.map((entry) => entry.kind).join(","));
const selected: (Entry | undefined)[] = source.slice();
// An explicit undefined slot is visited by map, unlike a hole.
selected.push(undefined);
const copied = selected.map((entry, index) => ({ ...defaults, ...entry, index }));
for (const entry of copied) {
console.log(entry.kind, entry.tag, entry.values.join(","), entry.index);
}
const firstCopy = copied[0];
if (firstCopy !== undefined) firstCopy.kind = "changed";
console.log("fresh", source[0]!.kind, copied[0]!.kind);
console.log("shared", copied[0]!.values === source[0]!.values);
console.log("default", copied[2]!.values === defaults.values);
copied[0]!.values.push("retained");
console.log("source", source[0]!.values.join(","));
// The lowered source can also be narrower than its checker union.
const narrowed = selected.filter((entry) => entry !== undefined).map((entry) => {
if (entry === undefined) return { ...defaults, label: "empty" };
return { ...defaults, ...entry, label: "present:" + entry.kind };
});
for (const entry of narrowed) console.log(entry.kind, entry.label);
// Later explicit fields replace source fields, including absent sources.
const overwritten = selected.map((entry, index) => ({
...entry, kind: "override", tag: index, values: ["new"],
}));
for (const entry of overwritten) console.log(entry.kind, entry.tag, entry.values.join(","));
// Declared optional destinations represent the no-copy arm directly.
interface Optional { kind?: string; tag?: number; values?: string[]; index: number }
const optional = selected.map((entry, index): Optional => ({ ...entry, index }));
for (const entry of optional) {
console.log(entry.kind ?? "missing", entry.tag ?? -2, entry.values?.join(",") ?? "missing", entry.index);
}
+15
View File
@@ -0,0 +1,15 @@
// These constants must also survive --emit=ir and a native emitter's input.
const numbers = [NaN, Number.NaN, Infinity, -Infinity, -0, 0, Number.MIN_VALUE, Number.MAX_VALUE];
for (const value of numbers) {
console.log(String(value), Number.isNaN(value), Number.isFinite(value), Object.is(value, -0));
}
function equal(left: number, right: number): boolean { return left === right; }
console.log("nan", equal(NaN, NaN), Object.is(NaN, NaN), [NaN].includes(NaN));
const values = new Map<number, string>();
values.set(NaN, "first");
values.set(Number.NaN, "second");
values.set(-0, "zero");
console.log("map", values.size, values.get(NaN), values.get(0));
const bytes = new Float64Array([NaN, -0, Infinity, -Infinity]);
console.log("bytes", Number.isNaN(bytes[0]), Object.is(bytes[1], -0), bytes[2], bytes[3]);
console.log("json", JSON.stringify({ numbers, nan: NaN, negativeZero: -0 }));
@@ -0,0 +1,5 @@
const values = [{ name: "first", id: 1 }].slice();
// The array callback ABI includes undefined; spreading that arm cannot
// initialize the required fields inferred by TypeScript.
const copies = values.map((value) => ({ ...value, label: "copy" }));
console.log(copies.length);
+40
View File
@@ -0,0 +1,40 @@
import { readFileSync, writeFileSync } from "node:fs";
import { deserializeModule } from "../../../packages/compiler/src/ir/serialize.js";
import { emitLlvmModule } from "../../../packages/compiler/src/backend/llvm/emitter.js";
import { LlvmUnsupportedError } from "../../../packages/compiler/src/backend/llvm/unsupported.js";
interface Request {
targetTriple?: string;
debug: boolean;
sources: { file: string; text: string }[];
pointerBits: 32 | 64;
wasi: boolean;
emitLibraryIdentity: boolean;
runtimeAbiMarker: boolean;
}
const module = deserializeModule(readFileSync(process.argv[2]!, "utf8"));
const request = JSON.parse(readFileSync(process.argv[4]!, "utf8")) as Request;
const sources = new Map<string, string>();
for (const source of request.sources) sources.set(source.file, source.text);
try {
const text = emitLlvmModule(module, {
...(request.targetTriple === undefined ? {} : { targetTriple: request.targetTriple }),
...(request.debug ? { debugSources: sources } : {}),
pointerBits: request.pointerBits,
wasi: request.wasi,
emitLibraryIdentity: request.emitLibraryIdentity,
runtimeAbiMarker: request.runtimeAbiMarker,
});
writeFileSync(process.argv[3]!, text);
} catch (error) {
if (error instanceof LlvmUnsupportedError) {
console.log("unsupported: " + error.message);
process.exitCode = 2;
} else if (error instanceof Error) {
console.log(error.name + ": " + error.message);
process.exitCode = 1;
} else {
throw error;
}
}
+2 -2
View File
@@ -9,8 +9,8 @@ try {
console.log(JSON.stringify(errors));
process.exitCode = 1;
} else {
// Exercise the serializer's refusal through a real number literal,
// without placing an invalid non-JSON number in the input document.
// Introduce a native NaN before serialization, independently of the
// input document's sentinel decoding.
if (process.argv[3] === "nan") {
const statement = mod.functions[0]!.body[0];
if (statement?.kind === "exprStmt" && statement.expr.kind === "numLit") statement.expr.value = NaN;
@@ -0,0 +1,6 @@
spread-runtime-optional.ts:4:41 - error SC1090: object spread of '{ id: number; name: string } | undefined' sources where 'id' has no earlier contributor (the absent arm leaves the required field unset — spread defaults first: { ...defaults, ...overrides }) is not supported yet
3 | // initialize the required fields inferred by TypeScript.
4 | const copies = values.map((value) => ({ ...value, label: "copy" }));
| ^~~~~~~~
5 | console.log(copies.length);
@@ -0,0 +1,60 @@
import { deflateRawSync } from "node:zlib";
import { expect, test } from "vitest";
import { llvmBytes } from "../../packages/compiler/src/backend/literals.js";
import { emitLlvmModule } from "../../packages/compiler/src/backend/llvm/emitter.js";
import { cEmitterCases } from "./self-hosting-c-emitter-cases.js";
import { normalizedEmbeddingLlvm } from "./self-hosting-llvm-embedding.js";
const item = cEmitterCases().find((item) => item.name === "level-nine module and facade compression")!;
const original = emitLlvmModule(item.module);
function withSource(bytes: Buffer): string {
return original
.replace(/@sc_npm_src_0 = internal constant \[\d+ x i8\] c"[^"\n]*"/, `@sc_npm_src_0 = internal constant [${bytes.length + 1} x i8] c"${llvmBytes(bytes)}"`)
.replace(/(ptr @sc_npm_src_0, i64 )\d+/, `$1${bytes.length}`);
}
test("LLVM comparison accepts different valid encodings of the same source", () => {
const source = item.module.embedded!.modules[0]!.source;
const stored = deflateRawSync(source, { level: 0 });
const compressed = deflateRawSync(source, { level: 9 });
expect(stored.equals(compressed)).toBe(false);
expect(normalizedEmbeddingLlvm(withSource(stored), item.module)).toBe(normalizedEmbeddingLlvm(withSource(compressed), item.module));
});
test("LLVM comparison rejects a corrupted DEFLATE stream", () => {
expect(() => normalizedEmbeddingLlvm(withSource(Buffer.from([255, 255, 255])), item.module)).toThrow();
});
test("LLVM comparison rejects a valid stream containing different source", () => {
const bytes = deflateRawSync("a different module");
expect(() => normalizedEmbeddingLlvm(withSource(bytes), item.module)).toThrow("embedded source differs");
});
test("LLVM comparison verifies declared array and encoded lengths", () => {
const array = original.replace(/(@sc_npm_src_0 = internal constant \[)\d+/, "$11");
expect(() => normalizedEmbeddingLlvm(array, item.module)).toThrow("incorrect array length");
const encoded = original.replace(/(ptr @sc_npm_src_0, i64 )\d+/, "$10");
expect(() => normalizedEmbeddingLlvm(encoded, item.module)).toThrow("incorrect encoded length");
});
test("LLVM comparison verifies inflated byte length and NUL termination", () => {
const raw = original.replace(/(ptr @sc_npm_src_0, i64 \d+, i64 )\d+/, "$11");
expect(() => normalizedEmbeddingLlvm(raw, item.module)).toThrow("incorrect raw length");
const terminator = original.replace(/(@sc_npm_src_0 = internal constant \[\d+ x i8\] c"[^"\n]*)\\00"/, "$1\\01\"");
expect(() => normalizedEmbeddingLlvm(terminator, item.module)).toThrow("NUL terminator");
});
test("LLVM comparison preserves edge conditions and runtime installation", () => {
const changed = original.replace("@scr_zlib_inflate_exact", "@wrong_inflater");
expect(normalizedEmbeddingLlvm(changed, item.module)).not.toBe(normalizedEmbeddingLlvm(original, item.module));
const changedEdge = original.replace(/(ptr @sc_npm_edge_0_to, i32 )\d+/, "$199");
expect(changedEdge).not.toBe(original);
expect(normalizedEmbeddingLlvm(changedEdge, item.module)).not.toBe(normalizedEmbeddingLlvm(original, item.module));
});
test("LLVM comparison checks ESM facades as well as source modules", () => {
const changed = structuredClone(item.module);
changed.embedded!.modules[0]!.esm = "export default 'wrong';";
expect(() => normalizedEmbeddingLlvm(original, changed)).toThrow("embedded source differs for sc_npm_esm_0");
});
@@ -0,0 +1,51 @@
import { inflateRawSync } from "node:zlib";
import type { IrModule } from "../../packages/compiler/src/ir/ir.js";
/** Parse exactly the LLVM byte-string grammar emitted by llvmBytes. */
function literalBytes(literal: string): Buffer {
const bytes: number[] = [];
for (let i = 0; i < literal.length; i++) {
const code = literal.charCodeAt(i);
if (code === 92) {
const escape = literal.slice(i + 1, i + 3);
if (!/^[0-9a-fA-F]{2}$/.test(escape)) throw new Error("invalid LLVM byte escape");
bytes.push(parseInt(escape, 16));
i += 2;
} else {
if (code < 32 || code > 126 || code === 34) throw new Error("invalid LLVM literal byte");
bytes.push(code);
}
}
if (bytes.pop() !== 0) throw new Error("LLVM literal lacks its NUL terminator");
return Buffer.from(bytes);
}
/** Different zlib implementations can produce different valid compressed
* streams. Check payload bytes, array bounds and both table lengths before
* canonicalizing only those encoding-dependent parts of the LLVM module. */
export function normalizedEmbeddingLlvm(text: string, mod: IrModule): string {
let normalized = text;
const check = (name: string, source: string): void => {
const declaration = new RegExp(`@${name} = internal constant \\[(\\d+) x i8\\] c"([^"\\n]*)"`);
const match = declaration.exec(normalized);
if (!match) throw new Error(`missing embedded declaration ${name}`);
const encoded = literalBytes(match[2]!);
if (Number(match[1]) !== encoded.length + 1) throw new Error(`incorrect array length for ${name}`);
const metadata = new RegExp(`ptr @${name}, (i32|i64) (\\d+), (i32|i64) (\\d+)`);
const lengths = metadata.exec(normalized);
if (!lengths) throw new Error(`missing length metadata for ${name}`);
if (lengths[1] !== lengths[3]) throw new Error(`inconsistent length types for ${name}`);
if (Number(lengths[2]) !== encoded.length) throw new Error(`incorrect encoded length for ${name}`);
const rawLength = Number(lengths[4]);
const decoded = rawLength === 0 ? encoded : inflateRawSync(encoded);
if (!decoded.equals(Buffer.from(source, "utf8"))) throw new Error(`embedded source differs for ${name}`);
if (rawLength !== 0 && rawLength !== decoded.length) throw new Error(`incorrect raw length for ${name}`);
normalized = normalized.replace(declaration, `@${name} = internal constant [decoded] c"${decoded.toString("base64")}"`);
normalized = normalized.replace(metadata, `ptr @${name}, ${lengths[1]} encoded, ${lengths[3]} ${rawLength}`);
};
for (const [index, module] of (mod.embedded?.modules ?? []).entries()) {
check(`sc_npm_src_${index}`, module.source);
if (module.esm !== undefined) check(`sc_npm_esm_${index}`, module.esm);
}
return normalized;
}
@@ -0,0 +1,99 @@
import { F64, type IrModule } from "../../packages/compiler/src/ir/ir.js";
import { numLit } from "../../packages/compiler/src/ir/build.js";
import type { LlvmTargetOptions } from "../../packages/compiler/src/backend/llvm/emitter.js";
import { cEmitterCases } from "./self-hosting-c-emitter-cases.js";
export interface LlvmEmitterRequest {
targetTriple?: string;
debug: boolean;
sources: { file: string; text: string }[];
pointerBits: 32 | 64;
wasi: boolean;
emitLibraryIdentity: boolean;
runtimeAbiMarker: boolean;
}
export function llvmEmitterRequest(options: Partial<LlvmEmitterRequest> = {}): LlvmEmitterRequest {
return { debug: false, sources: [], pointerBits: 64, wasi: false, emitLibraryIdentity: true, runtimeAbiMarker: false, ...options };
}
export function llvmEmitterOptions(request: LlvmEmitterRequest): LlvmTargetOptions {
return {
...(request.targetTriple === undefined ? {} : { targetTriple: request.targetTriple }),
...(request.debug ? { debugSources: new Map(request.sources.map((source) => [source.file, source.text])) } : {}),
pointerBits: request.pointerBits,
wasi: request.wasi,
emitLibraryIdentity: request.emitLibraryIdentity,
runtimeAbiMarker: request.runtimeAbiMarker,
};
}
export interface LlvmEmitterCase {
name: string;
module: IrModule;
request: LlvmEmitterRequest;
contains: string[];
excludes: string[];
}
export function llvmEmitterCases(): LlvmEmitterCase[] {
// Both complete backends consume the same IR, including library entry
// points, recursive layouts, closure boxes and embedded package tables.
const cases: LlvmEmitterCase[] = cEmitterCases().map((item) => ({
name: item.name, module: item.module,
request: llvmEmitterRequest({ debug: item.sources.length > 0, sources: item.sources }),
contains: item.module.lib ? ["@native_init", "@native_collect"] : ["define i32 @main("],
excludes: item.module.lib ? ["define i32 @main("] : [],
}));
const find = (name: string) => cases.find((item) => item.name === name)!;
find("source locations with multibyte text").contains.push("!DILocation", "!DICompileUnit");
find("global initialization and shutdown ownership").contains.push("@scr_str_release", "@scr_arr_release");
find("recursive record trace and teardown").contains.push("@scr_cyc_alloc", "@scr_cyc_free");
find("closure capture boxes").contains.push("@sc_retain_box", "@scr_closure_new");
find("thread-local library globals").contains.push("thread_local");
find("level-nine module and facade compression").contains.push("@scr_zlib_inflate_exact");
for (const name of ["empty executable", "scalar, reference and tuple layouts", "class forest and virtual dispatch tables", "union scalar payloads and immortal units"]) {
const base = find(name);
cases.push({
...base, name: `WASI 32-bit ${name}`,
request: llvmEmitterRequest({ pointerBits: 32, wasi: true }),
contains: ["%ScrStr = type { i32, i32, i32 }"],
excludes: ["%ScrStr = type { i64, i64, i64 }"],
});
}
const library = find("library entry points and identity constants");
cases.push({
...library, name: "library with external identity getters",
request: llvmEmitterRequest({ emitLibraryIdentity: false }),
contains: ["@native_init"], excludes: ["define ptr @native_build_id", "define i32 @native_abi"],
});
const empty = find("empty executable");
cases.push({
...empty, name: "runtime ABI marker reference", request: llvmEmitterRequest({ runtimeAbiMarker: true }),
contains: ["call void @scr_runtime_abi_"], excludes: [],
});
const ffi = structuredClone(empty.module);
const loc = ffi.functions[0]!.loc;
ffi.ffiImports = [{ name: "narrow", symbol: "native_narrow", params: ["i8", "u16", "f32"], returns: "i16" }];
ffi.functions[0]!.body = [{
kind: "exprStmt", loc,
expr: { kind: "ffiCall", import: "narrow", args: [-7, 65535, 0.5].map((value) => numLit(value, loc)), type: F64, loc },
}];
for (const [targetTriple, extend] of [
["arm64-apple-macos", true],
["x86_64-unknown-linux-gnu", true],
["aarch64-unknown-linux-gnu", false],
["x86_64-pc-windows-msvc", false],
["wasm32-unknown-wasi", true],
] as const) {
const declaration = extend
? "declare signext i16 @native_narrow(i8 signext, i16 zeroext, float)"
: "declare i16 @native_narrow(i8, i16, float)";
cases.push({
name: `narrow FFI ABI ${targetTriple}`, module: ffi,
request: llvmEmitterRequest({ targetTriple, pointerBits: targetTriple.startsWith("wasm32") ? 32 : 64 }),
contains: [declaration, "fptrunc double"], excludes: [],
});
}
return cases;
}
@@ -0,0 +1,182 @@
import { execFile, spawnSync } from "node:child_process";
import { mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { fileURLToPath, pathToFileURL } from "node:url";
import { promisify } from "node:util";
import { expect, test } from "vitest";
import { compile, compileC, deserializeModule, serializeModule, validateModule } from "@scriptc/compiler";
import { emitLlvmModule } from "../../packages/compiler/src/backend/llvm/emitter.js";
import {
moduleUsesAssert, moduleUsesBigInt, moduleUsesCopying, moduleUsesDynInvoke, moduleUsesInspect,
moduleUsesLegacyTextDecoder, moduleUsesRegex, moduleUsesSymbol, moduleUsesZlib, type IrModule,
} from "../../packages/compiler/src/ir/ir.js";
import { llvmEmitterCases, llvmEmitterOptions, llvmEmitterRequest, type LlvmEmitterRequest } from "./self-hosting-llvm-emitter-cases.js";
import { normalizedEmbeddingLlvm } from "./self-hosting-llvm-embedding.js";
const root = fileURLToPath(new URL("../..", import.meta.url));
const entry = join(root, "tests/fixtures/self-hosting/llvm-emitter.ts");
const execFileAsync = promisify(execFile);
const runOptions = { cwd: root, timeout: 60_000, maxBuffer: 256 * 1024 * 1024 };
const sanitize = process.env["SCRIPTC_SAN"] === "1";
function nativeFeatures(mod: IrModule) {
return {
regex: moduleUsesRegex(mod), copying: moduleUsesCopying(mod),
inspect: moduleUsesInspect(mod), dynInvoke: moduleUsesDynInvoke(mod),
symbol: moduleUsesSymbol(mod), bigint: moduleUsesBigInt(mod), zlib: moduleUsesZlib(mod),
assert: moduleUsesAssert(mod),
textDecoderLegacy: moduleUsesLegacyTextDecoder(mod),
};
}
// Execute generated modules across expression dispatch, shared mutable
// emitter state, exception paths, dynamic walkers and callback adapters.
const programs = [
"001-hello.ts",
"101-arithmetic.ts",
"600-closures-basic.ts",
"711-inheritance-dispatch.ts",
"756-cycle-inheritance.ts",
"803-switch-rc-stress.ts",
"1005-json-nested.ts",
"1023-async-rc-stress.ts",
"1401-typedarray-slice-set.ts",
"1452-return-through-finally.ts",
"1637-inspect-dyn.ts",
"1672-symbol-containers.ts",
"1726-promise-with-resolvers.ts",
"2012-generators-return-throw.ts",
"2537-destructuring-assign-member-targets.ts",
"2840-finally-completions.ts",
"2905-bigint-edges.ts",
"3103-scalar-record-nested-control-flow.ts",
"3104-native-analysis-tables-loops.ts",
"3109-identity-union-collections.ts",
"3112-union-switch-control-flow.ts",
"3113-union-array-removal.ts",
"3114-union-record-field-write.ts",
"3115-zlib-static-compression-levels.ts",
"3116-sequence-expression-lifetimes.ts",
"3117-unknown-switch-control-flow.ts",
"3118-recursive-union-spread.ts",
"3119-runtime-optional-spread.ts",
"3120-ir-nonfinite-numbers.ts",
"text-codec-values/main.ts",
];
for (const backend of ["c", "llvm"] as const) {
test(`the complete LLVM emitter bootstraps natively (${backend})`, async () => {
const dir = mkdtempSync(join(process.platform === "win32" ? tmpdir() : "/tmp", "scriptc-llvm-emitter-"));
const executable = (name: string) => join(dir, name + (process.platform === "win32" ? ".exe" : ""));
const stage = executable("emitter");
const input = join(dir, "input.json");
const output = join(dir, "native.ll");
const config = join(dir, "config.json");
try {
// Lower the full production graph in a child so synchronous frontend
// work cannot block Vitest's worker RPC while it compiles the seed.
const api = pathToFileURL(join(root, "packages/compiler/src/index.ts")).href;
const { stdout } = await execFileAsync(process.execPath, [
"--import", "tsx", "--input-type=module", "--eval",
`import { compile } from ${JSON.stringify(api)};
const result = await compile(process.argv[1], {
outDir: process.argv[2], outPath: process.argv[3], backend: process.argv[4],
dynamic: false, optimization: 'dev', sanitize: process.argv[5] === '1', emitIr: true,
});
console.log(JSON.stringify(result));`,
entry, dir, stage, backend, sanitize ? "1" : "0",
], { ...runOptions, timeout: 600_000 });
const built = JSON.parse(stdout) as Awaited<ReturnType<typeof compile>>;
if (!built.ok) throw new Error(built.diagnostics.map((d) => `${d.code}: ${d.message}`).join("\n"));
if (!("binaryPath" in built)) throw new Error("bootstrap did not produce an executable");
expect(built.backend).toBe(backend);
expect(built.llvmRefusal).toBeUndefined();
expect(built.irPath).toBeDefined();
const ownIr = deserializeModule(readFileSync(built.irPath!, "utf8"));
expect(ownIr.functions.length).toBeGreaterThan(800);
expect(validateModule(ownIr)).toEqual([]);
const emit = async (mod: IrModule, name: string, request = llvmEmitterRequest()): Promise<string> => {
writeFileSync(input, serializeModule(mod));
writeFileSync(config, JSON.stringify(request));
const result = await execFileAsync(stage, [input, output, config], { ...runOptions, timeout: 300_000 })
.catch((cause: unknown) => { throw new Error(`native LLVM emission failed for ${name}`, { cause }); });
expect(result.stdout, name).toBe("");
expect(result.stderr, name).toBe("");
const text = readFileSync(output, "utf8");
const expected = emitLlvmModule(mod, llvmEmitterOptions(request));
expect(normalizedEmbeddingLlvm(text, mod), name).toBe(normalizedEmbeddingLlvm(expected, mod));
return text;
};
for (const item of llvmEmitterCases()) {
expect(validateModule(item.module), item.name).toEqual([]);
const text = await emit(item.module, item.name, item.request);
for (const fragment of item.contains) expect(text, item.name).toContain(fragment);
for (const fragment of item.excludes) expect(text, item.name).not.toContain(fragment);
}
// A refusal must leave the requested output untouched, with the same
// exception category/message under Node and the compiled emitter.
const rejected = llvmEmitterCases()[0]!.module;
rejected.classes = [{ name: "UnimplementedNative", fields: [], runtime: true, loc: { file: rejected.sourceFile, start: 0, end: 0 } }];
writeFileSync(input, serializeModule(rejected));
writeFileSync(config, JSON.stringify(llvmEmitterRequest()));
writeFileSync(output, "untouched");
const args = [input, output, config];
const oracleRefusal = spawnSync(process.execPath, ["--import", "tsx", entry, ...args], runOptions);
const nativeRefusal = spawnSync(stage, args, runOptions);
for (const result of [oracleRefusal, nativeRefusal]) {
expect(result.error).toBeUndefined();
expect(result.signal).toBeNull();
expect(result.status).toBe(2);
expect(result.stderr.toString()).toBe("");
}
expect(nativeRefusal.stdout).toEqual(oracleRefusal.stdout);
expect(nativeRefusal.stdout.toString()).toContain("classDef:UnimplementedNative");
expect(readFileSync(output, "utf8")).toBe("untouched");
for (const source of programs) {
const sourcePath = join(root, "tests/corpus", source);
const irPath = join(dir, "program.ir.json");
const lowered = await compile(sourcePath, { outDir: dir, outPath: irPath, outputKind: "ir", dynamic: false });
if (!lowered.ok) throw new Error(`${source}: ${lowered.diagnostics.map((d) => d.message).join("\n")}`);
const mod = deserializeModule(readFileSync(irPath, "utf8"));
const request: LlvmEmitterRequest = llvmEmitterRequest({
debug: true, sources: [{ file: sourcePath, text: readFileSync(sourcePath, "utf8") }],
});
const text = await emit(mod, source, request);
const llvmPath = join(dir, "program.ll");
const outPath = executable("program");
writeFileSync(llvmPath, text);
await compileC({ cPath: llvmPath, outPath, sanitize, optimization: "dev", ...nativeFeatures(mod) });
const node = spawnSync(process.execPath, [sourcePath], runOptions);
const native = spawnSync(outPath, [], runOptions);
for (const result of [node, native]) {
expect(result.error, source).toBeUndefined();
expect(result.signal, `${source}: ${result.stderr}`).toBeNull();
expect(result.status, `${source}: ${result.stderr}`).toBe(0);
}
expect(native.stdout, source).toEqual(node.stdout);
expect(native.stderr, source).toEqual(node.stderr);
}
// The seed emits its own complete IR, including the production
// optimizer and LLVM expression/library graph. Compile those bytes
// and require a second native generation to reproduce the module.
const self = await emit(ownIr, "LLVM emitter emits itself");
const selfPath = join(dir, "self.ll");
const secondStage = executable("emitter-second");
writeFileSync(selfPath, self);
await compileC({ cPath: selfPath, outPath: secondStage, sanitize, optimization: "dev", ...nativeFeatures(ownIr) });
const again = join(dir, "again.ll");
const second = await execFileAsync(secondStage, [input, again, config], { ...runOptions, timeout: 300_000 });
expect(second.stdout).toBe("");
expect(second.stderr).toBe("");
expect(readFileSync(again, "utf8")).toBe(self);
} finally {
rmSync(dir, { recursive: true, force: true });
}
}, 1_200_000);
}
@@ -22,7 +22,7 @@ function numericModule(): IrModule {
functions: [{
name: "main", params: [], returnType: VOID, locals: [], loc,
body: [
// The fixture's failure mode changes this first literal to NaN.
// The fixture can change this first literal to NaN before encoding.
{ kind: "exprStmt", expr: numLit(0, loc), loc },
{ kind: "exprStmt", expr: { kind: "intrinsic", name: "console.log", args: numbers.map((n) => numLit(n, loc)), type: VOID, loc }, loc },
{ kind: "exprStmt", expr: { kind: "intrinsic", name: "console.log", args: [{
@@ -126,9 +126,13 @@ for (const backend of ["c", "llvm"] as const) {
expect(mismatch.native.stdout).toEqual(mismatch.oracle.stdout);
expect(mismatch.native.stdout.toString()).toContain("IR version mismatch");
const nan = run(serializeModule(numericModule()), ["nan"]);
expect(nan.native.status).toBe(1);
expect(nan.native.stdout).toEqual(nan.oracle.stdout);
expect(nan.native.stdout.toString()).toContain("IR contains NaN; refusing to serialize");
expect(nan.native.status).toBe(0);
expect(deserializeModule(nan.native.stdout.toString())).toEqual(deserializeModule(nan.oracle.stdout.toString()));
expect(nan.native.stdout.toString()).toContain('"$nonfinite": "nan"');
const fromNan = run(nan.native.stdout.toString());
expect(fromNan.native.status).toBe(0);
expect(fromNan.native.stdout).toEqual(nan.native.stdout);
expect(fromNan.native.stdout).toEqual(fromNan.oracle.stdout);
} finally {
rmSync(dir, { recursive: true, force: true });
}
@@ -4,7 +4,7 @@ import { boolLit, numLit, strLit, varRef } from "../../packages/compiler/src/ir/
const loc = { file: "validator-input.ts", start: 12, end: 34 };
const expression = (expr: IrExpr): IrStmt => ({ kind: "exprStmt", expr, loc });
const base = (): IrModule => ({
irVersion: 12, sourceFile: loc.file, entry: "main",
irVersion: 13, sourceFile: loc.file, entry: "main",
functions: [{ name: "main", params: [], locals: [], returnType: VOID, body: [], loc }],
});