mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-10-02 08:35:07 +08:00
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:
@@ -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);
|
||||
}
|
||||
@@ -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
@@ -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
@@ -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 }],
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user