Report a rejected declaration's own diagnostic (#818)

This commit is contained in:
Chris Tate
2026-10-10 10:01:46 -05:00
committed by GitHub
12 changed files with 377 additions and 255 deletions
@@ -1468,14 +1468,7 @@ function collectClassShapeDeferring(lowerer: Lowerer, decl: ts.ClassDeclaration)
(m) => m.kind === ts.SyntaxKind.Decorator,
);
if (symbol && (hasDeclTimeCode(decl) || decl.members.some(hasDeclTimeCode))) {
const diags = lowerer.deferredDiags.get(symbol);
if (diags) {
lowerer.deferredDiags.delete(symbol);
if (!lowerer.alreadyFlushed.has(symbol)) {
lowerer.flushedSymbols.add(symbol);
for (const d of diags) lowerer.pushDiag(d);
}
}
lowerer.reportDeferred(symbol);
}
// A poisoned BASE this class EXTENDS must report EAGERLY for the same
// reason: the derived statement evaluates its heritage when module
@@ -1493,14 +1486,7 @@ function collectClassShapeDeferring(lowerer: Lowerer, decl: ts.ClassDeclaration)
?.types.map((t) => t.expression)
.filter(ts.isIdentifier)[0];
const baseSym = baseIdent ? lowerer.resolveValueSymbol(baseIdent) : null;
const baseDiags = baseSym ? lowerer.deferredDiags.get(baseSym) : undefined;
if (baseSym && baseDiags) {
lowerer.deferredDiags.delete(baseSym);
if (!lowerer.alreadyFlushed.has(baseSym)) {
lowerer.flushedSymbols.add(baseSym);
for (const d of baseDiags) lowerer.pushDiag(d);
}
}
if (baseSym) lowerer.reportDeferred(baseSym);
}
}
@@ -1287,7 +1287,10 @@ function lowerExprInner(lowerer: Lowerer, expr: ts.Expression): IrExpr {
// that does not exist. The blocked-binding cascade names the use;
// without this the validator's registration check ICEs on the live
// reference (`@this {T}` inference — signature 07).
if (lowerer.typeNamesUnregisteredClass(g.type)) {
const unregistered = lowerer.unregisteredClassOf(g.type, new Set());
if (unregistered !== null) {
// The class's own rejection is the root cause the cascade names.
lowerer.flushDeferredClass(unregistered);
lowerer.pushDiag(blockedBindingUseDiag(expr.text, loc));
throw new PoisonError();
}
@@ -2653,6 +2653,13 @@ export class Lowerer {
* coverage remainder as alreadyFlushed). */
readonly flushedSymbols = new Set<ts.Symbol>();
readonly alreadyFlushed: ReadonlySet<ts.Symbol>;
/** Deferred diagnostics a flush handed to a capture buffer (diagSink)
* instead of the build, keyed by the copy in that buffer. A buffer can
* be discarded (a declined probe, a collection that succeeded) or
* re-deferred under another declaration, so the declaration stays
* deferred until one of these copies reaches `diags` (pushDiag commits
* it); until then a later flush reports the diagnostics again. */
readonly pendingFlushes = new Map<ScrDiagnostic, { symbol: ts.Symbol; source: ScrDiagnostic }>();
/** The build's target platform ("win32" | "darwin" | "linux" | ...):
* selects the platform-keyed builtin surfaces (builtinModuleFnsOf /
* builtinModuleConstOf in surfaces.ts). */
@@ -5666,9 +5673,9 @@ export class Lowerer {
// Deferred collection diagnostics nothing flushed — declarations no
// reference ever made relevant. They belong to the unreached group
// (collection order keeps them deterministic).
for (const [symbol, diags] of this.deferredDiags) {
for (const [symbol, diags] of [...this.deferredDiags]) {
if (this.alreadyFlushed.has(symbol)) continue;
for (const d of diags) this.pushDiag(d);
for (const d of [...diags]) this.pushDiag(d);
}
return {
module: null,
@@ -5813,42 +5820,49 @@ export class Lowerer {
* registered ClassInfo — the shape of a JS class whose collection fenced.
* Used by run()'s global pruning; shapes/unions recurse with a seen-set
* (interned ids can nest). */
typeNamesUnregisteredClass(t: IrType, seen: Set<string> = new Set()): boolean {
typeNamesUnregisteredClass(t: IrType): boolean {
return this.unregisteredClassOf(t, new Set()) !== null;
}
/** The first class name typeNamesUnregisteredClass finds in `t`. */
unregisteredClassOf(t: IrType, seen: Set<string>): string | null {
const first = (types: readonly IrType[]): string | null => {
for (const type of types) {
const name = this.unregisteredClassOf(type, seen);
if (name !== null) return name;
}
return null;
};
switch (t.kind) {
case "object":
return !this.classes.has(t.className);
return this.classes.has(t.className) ? null : t.className;
case "array":
case "set":
return this.typeNamesUnregisteredClass(t.elem, seen);
return first([t.elem]);
case "map":
return (
this.typeNamesUnregisteredClass(t.key, seen) ||
this.typeNamesUnregisteredClass(t.value, seen)
);
return first([t.key, t.value]);
case "promise":
return this.typeNamesUnregisteredClass(t.inner, seen);
return first([t.inner]);
case "func":
return (
t.params.some((p) => this.typeNamesUnregisteredClass(p, seen)) ||
this.typeNamesUnregisteredClass(t.ret, seen)
);
return first([...t.params, t.ret]);
case "record": {
if (seen.has(t.shapeId)) return false;
if (seen.has(t.shapeId)) return null;
seen.add(t.shapeId);
const shape = this.shapes.get(t.shapeId);
if (!shape) return false;
if (shape.indexValue && this.typeNamesUnregisteredClass(shape.indexValue, seen))
return true;
return shape.fields.some((f) => this.typeNamesUnregisteredClass(f.type, seen));
if (!shape) return null;
return first([
...(shape.indexValue ? [shape.indexValue] : []),
...shape.fields.map((f) => f.type),
]);
}
case "union": {
if (seen.has(t.unionId)) return false;
if (seen.has(t.unionId)) return null;
seen.add(t.unionId);
const def = this.unions.get(t.unionId);
return !!def && def.arms.some((a) => this.typeNamesUnregisteredClass(a, seen));
return def ? first(def.arms) : null;
}
default:
return false;
return null;
}
}
@@ -6476,13 +6490,16 @@ export class Lowerer {
const d = this.instantiationContext
? { ...diag, message: `${diag.message} (${this.instantiationContext})` }
: diag;
const pending = this.pendingFlushes.get(diag);
// Deferred collection: the wrapper decides whether these ever report
// (a reference flushes them; unreferenced declarations stay silent in
// builds and report under coverage's unreached group).
if (this.diagSink) {
if (pending && d !== diag) this.pendingFlushes.set(d, pending);
this.diagSink.push(d);
return;
}
if (pending) this.commitFlush(pending.symbol, pending.source);
// One site, one report: some declarations map a type twice (module
// globals pre-register before their initializers lower) — an exact
// duplicate (code + span + message) adds noise, not information.
@@ -9081,12 +9098,44 @@ export class Lowerer {
* diagnostics the eager collector historically produced. */
flushDeferred(symbol: ts.Symbol): void {
if (this.collecting) return;
this.reportDeferred(symbol);
}
/** flushDeferred without the collection-pass guard: for declarations
* whose diagnostics must report eagerly (class statements with
* declaration-time code, extended bases — collectClassShapeDeferring). */
reportDeferred(symbol: ts.Symbol): void {
const diags = this.deferredDiags.get(symbol);
if (!diags) return;
this.deferredDiags.delete(symbol);
if (this.alreadyFlushed.has(symbol)) return; // the emit pass reported these
if (this.alreadyFlushed.has(symbol)) {
this.deferredDiags.delete(symbol); // the emit pass reported these
return;
}
if (this.diagSink === null) {
this.deferredDiags.delete(symbol);
this.flushedSymbols.add(symbol);
for (const d of diags) this.pushDiag(d);
return;
}
// A capture buffer is active (a probe, or another declaration's
// deferred collection): the copies ride that buffer, and the
// declaration stays deferred until one of them reaches the build.
for (const d of diags) {
this.pendingFlushes.set(d, { symbol, source: d });
this.pushDiag(d);
}
}
/** A flushed copy of a deferred diagnostic reached `diags`: that
* diagnostic is reported, so it leaves the declaration's deferred list
* (the declaration counts as flushed for the coverage remainder). */
private commitFlush(symbol: ts.Symbol, source: ScrDiagnostic): void {
this.flushedSymbols.add(symbol);
for (const d of diags) this.pushDiag(d);
const list = this.deferredDiags.get(symbol);
if (!list) return;
const index = list.indexOf(source);
if (index >= 0) list.splice(index, 1);
if (list.length === 0) this.deferredDiags.delete(symbol);
}
flushDeferredClass(className: string): void {
@@ -10692,18 +10692,6 @@
],
"diags": []
},
"<repo>/tests/corpus/native-worker-publish-classes.ts": {
"order": [
"<repo>/tests/corpus/native-worker-publish-classes.ts"
],
"diags": []
},
"<repo>/tests/corpus/native-worker-publish-refusals.ts": {
"order": [
"<repo>/tests/corpus/native-worker-publish-refusals.ts"
],
"diags": []
},
"<repo>/tests/corpus/nested-map-cycles.ts": {
"order": [
"<repo>/tests/corpus/nested-map-cycles.ts"
@@ -14620,12 +14608,6 @@
],
"diags": []
},
"<repo>/tests/corpus/native-worker-published-graphs.ts": {
"order": [
"<repo>/tests/corpus/native-worker-published-graphs.ts"
],
"diags": []
},
"<repo>/tests/corpus/native-worker-references.ts": {
"order": [
"<repo>/tests/corpus/native-worker-references.ts"
@@ -14662,12 +14644,6 @@
],
"diags": []
},
"<repo>/tests/corpus/native-worker-typed-arrays.ts": {
"order": [
"<repo>/tests/corpus/native-worker-typed-arrays.ts"
],
"diags": []
},
"<repo>/tests/corpus/caught-class-identity.ts": {
"order": [
"<repo>/tests/corpus/caught-class-identity.ts"
@@ -15617,6 +15593,72 @@
],
"diags": []
},
"<repo>/tests/corpus/stack-depth-async-bodies.ts": {
"order": [
"<repo>/tests/corpus/stack-depth-async-bodies.ts"
],
"diags": []
},
"<repo>/tests/corpus/4591-weak-keys-union-records.ts": {
"order": [
"<repo>/tests/corpus/4591-weak-keys-union-records.ts"
],
"diags": []
},
"<repo>/tests/corpus/4592-cycle-const-from-import/main.ts": {
"order": [
"<repo>/tests/corpus/4592-cycle-const-from-import/report.ts",
"<repo>/tests/corpus/4592-cycle-const-from-import/stock.ts",
"<repo>/tests/corpus/4592-cycle-const-from-import/markup.ts",
"<repo>/tests/corpus/4592-cycle-const-from-import/pricing.ts",
"<repo>/tests/corpus/4592-cycle-const-from-import/main.ts"
],
"diags": []
},
"<repo>/tests/corpus/4593-weak-keys-empty-objects.ts": {
"order": [
"<repo>/tests/corpus/4593-weak-keys-empty-objects.ts"
],
"diags": []
},
"<repo>/tests/diagnostics/deferred-class-binding/main.ts": {
"order": [
"<repo>/tests/diagnostics/deferred-class-binding/crate.ts",
"<repo>/tests/diagnostics/deferred-class-binding/main.ts"
],
"diags": []
},
"<repo>/tests/diagnostics/deferred-class-receiver/main.ts": {
"order": [
"<repo>/tests/diagnostics/deferred-class-receiver/shelf.ts",
"<repo>/tests/diagnostics/deferred-class-receiver/main.ts"
],
"diags": []
},
"<repo>/tests/corpus/native-worker-publish-classes.ts": {
"order": [
"<repo>/tests/corpus/native-worker-publish-classes.ts"
],
"diags": []
},
"<repo>/tests/corpus/native-worker-publish-refusals.ts": {
"order": [
"<repo>/tests/corpus/native-worker-publish-refusals.ts"
],
"diags": []
},
"<repo>/tests/corpus/native-worker-published-graphs.ts": {
"order": [
"<repo>/tests/corpus/native-worker-published-graphs.ts"
],
"diags": []
},
"<repo>/tests/corpus/native-worker-typed-arrays.ts": {
"order": [
"<repo>/tests/corpus/native-worker-typed-arrays.ts"
],
"diags": []
},
"<repo>/tests/corpus/4541-nullable-wrap-borrows.ts": {
"order": [
"<repo>/tests/corpus/4541-nullable-wrap-borrows.ts"
@@ -15653,34 +15695,6 @@
],
"diags": []
},
"<repo>/tests/corpus/stack-depth-async-bodies.ts": {
"order": [
"<repo>/tests/corpus/stack-depth-async-bodies.ts"
],
"diags": []
},
"<repo>/tests/corpus/4591-weak-keys-union-records.ts": {
"order": [
"<repo>/tests/corpus/4591-weak-keys-union-records.ts"
],
"diags": []
},
"<repo>/tests/corpus/4592-cycle-const-from-import/main.ts": {
"order": [
"<repo>/tests/corpus/4592-cycle-const-from-import/report.ts",
"<repo>/tests/corpus/4592-cycle-const-from-import/stock.ts",
"<repo>/tests/corpus/4592-cycle-const-from-import/markup.ts",
"<repo>/tests/corpus/4592-cycle-const-from-import/pricing.ts",
"<repo>/tests/corpus/4592-cycle-const-from-import/main.ts"
],
"diags": []
},
"<repo>/tests/corpus/4593-weak-keys-empty-objects.ts": {
"order": [
"<repo>/tests/corpus/4593-weak-keys-empty-objects.ts"
],
"diags": []
},
"<repo>/tests/corpus/4568-for-of-hole-elements.ts": {
"order": [
"<repo>/tests/corpus/4568-for-of-hole-elements.ts"
@@ -0,0 +1,8 @@
export class Crate {
readonly label: string;
contents: unknown;
constructor(label: string) {
this.label = label;
this.contents = undefined;
}
}
@@ -0,0 +1,5 @@
import { Crate } from "./crate.ts";
const pack = (label: string) => new Crate(label);
console.log(pack("spare parts").label);
@@ -0,0 +1,7 @@
import { Shelf } from "./shelf.ts";
function label(name: string): string {
return new Shelf(name).describe();
}
console.log(label("north"));
@@ -0,0 +1,10 @@
export class Shelf {
readonly name: string;
private readonly notes = new WeakMap<object, string>();
constructor(name: string) {
this.name = name;
}
describe(): string {
return `shelf ${this.name}`;
}
}
@@ -0,0 +1,15 @@
deferred-class-binding/crate.ts:3:3 - error SC1090: 'unknown'-typed class fields are not supported yet
2 | readonly label: string;
3 | contents: unknown;
| ^~~~~~~~
4 | constructor(label: string) {
deferred-class-binding/main.ts:5:13 - error SC2004: uses of 'pack' inherit the blocker on its declaration
4 |
5 | console.log(pack("spare parts").label);
| ^~~~
6 |
hint: the declaration of 'pack' did not compile — fix the diagnostic reported there and these sites clear with it
@@ -0,0 +1,13 @@
deferred-class-receiver/main.ts:4:10 - error SC1090: constructing through a class value whose class has no lowering (the class declaration itself was rejected — see its own diagnostic) is not supported yet
3 | function label(name: string): string {
4 | return new Shelf(name).describe();
| ^~~~~~~~~~~~~~~
5 | }
deferred-class-receiver/shelf.ts:3:20 - error SC1090: 'unknown'-typed class fields are not supported yet
2 | readonly name: string;
3 | private readonly notes = new WeakMap<object, string>();
| ^~~~~
4 | constructor(name: string) {
+8
View File
@@ -285,6 +285,14 @@ test("runtime-optional refined unions report blockers without crashing", () => {
}
});
test("a rejected class reached only through a receiver probe reports its own diagnostic once", () => {
const { coverage } = analyze(join(repoRoot, "tests/diagnostics/deferred-class-receiver/main.ts"));
const messages = coverage.diagnostics.map((d) => d.message);
expect(messages.filter((m) => m.startsWith("'unknown'-typed class fields"))).toHaveLength(1);
expect(messages.some((m) => m.startsWith("constructing through a class value"))).toBe(true);
expect(coverage.unreached?.diagnostics ?? []).toEqual([]);
});
test("settled generic rest-order fences count each source statement once", () => {
const { coverage } = analyze(join(repoRoot, "tests/diagnostics/retained-generic-rest-order.ts"));
expect(coverage.diagnostics.map((d) => d.code)).toEqual(["SC1031", "SC2004"]);
+165 -161
View File
@@ -100,180 +100,184 @@ const programs = [
];
for (const backend of ["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)};
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.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: emitTimeout,
}).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),
entry,
dir,
stage,
backend,
sanitize ? "1" : "0",
],
{ ...runOptions, timeout: 600_000 },
);
return text;
};
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);
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);
}
expect(built.irPath).toBeDefined();
const ownIr = deserializeModule(readFileSync(built.irPath!, "utf8"));
expect(ownIr.functions.length).toBeGreaterThan(800);
expect(validateModule(ownIr)).toEqual([]);
// 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");
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: emitTimeout,
}).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 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);
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,
// Emitted modules are generated programs closed over the runtime
// tree, as in compile(): reuse cached runtime objects instead of
// rebuilding every runtime unit for each program.
cacheIdentity: generatedProgram,
sanitize,
optimization: "dev",
...executableLinkFeatures(mod, false),
});
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(programStderr(native.stderr), source).toBe(programStderr(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: llvmPath,
outPath,
// Emitted modules are generated programs closed over the runtime
// tree, as in compile(): reuse cached runtime objects instead of
// rebuilding every runtime unit for each program.
cPath: selfPath,
outPath: secondStage,
cacheIdentity: generatedProgram,
sanitize,
optimization: "dev",
...executableLinkFeatures(mod, false),
...executableLinkFeatures(ownIr, false),
});
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(programStderr(native.stderr), source).toBe(programStderr(node.stderr));
const again = join(dir, "again.ll");
const second = await execFileAsync(secondStage, [input, again, config], {
...runOptions,
timeout: emitTimeout,
});
expect(second.stdout).toBe("");
expect(second.stderr).toBe("");
expect(readFileSync(again, "utf8")).toBe(self);
} finally {
rmSync(dir, { recursive: true, force: true });
}
// 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,
cacheIdentity: generatedProgram,
sanitize,
optimization: "dev",
...executableLinkFeatures(ownIr, false),
});
const again = join(dir, "again.ll");
const second = await execFileAsync(secondStage, [input, again, config], {
...runOptions,
timeout: emitTimeout,
});
expect(second.stdout).toBe("");
expect(second.stderr).toBe("");
expect(readFileSync(again, "utf8")).toBe(self);
} finally {
rmSync(dir, { recursive: true, force: true });
}
}, testTimeout);
},
testTimeout,
);
}