mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-10-29 07:04:09 +08:00
Report a rejected declaration's own diagnostic (#818)
This commit is contained in:
@@ -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) {
|
||||
@@ -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"]);
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user