Merge origin/main into opt/builtins-t10-t12

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Malte Ubl
2026-10-18 14:40:30 -07:00
co-authored by Claude Opus 5.5
16 changed files with 240 additions and 88 deletions
@@ -4362,6 +4362,7 @@ export function lowerStaticFieldInits(lowerer: Lowerer, info: ClassInfo): IrStmt
// instantiation's bindings/context (a no-op for everything else —
// generic FAMILIES own their statics and carry no genericInstance).
const diagsBefore = lowerer.diags.length;
const sinkMark = lowerer.diagSinkMark();
try {
return withInstanceBindings(lowerer, info, () => lowerStaticFieldInitsInner(lowerer, info));
} catch (error) {
@@ -4369,8 +4370,11 @@ export function lowerStaticFieldInits(lowerer: Lowerer, info: ClassInfo): IrStmt
// Keep their refusal attached to the class declaration instead of
// letting a poisoned initializer escape the module lowering pass.
if (!(error instanceof PoisonError)) throw error;
if (info.decl && isJsSourceFile(info.decl.getSourceFile()) && lowerer.diagSink === null) {
const fence = lowerer.deferToRuntimeFence(diagsBefore, info.decl, { kind: "statement" });
if (info.decl && isJsSourceFile(info.decl.getSourceFile())) {
const fence =
lowerer.diagSink === null
? lowerer.deferToRuntimeFence(diagsBefore, info.decl, { kind: "statement" })
: lowerer.capturedRuntimeFence(sinkMark, info.decl);
if (fence) return [fence];
}
return [];
@@ -4507,6 +4511,7 @@ function lowerStaticFieldInitsInner(lowerer: Lowerer, info: ClassInfo): IrStmt[]
lowerer.stats.statementsTotal++;
lowerer.bumpFileStat(locOf(f.initializer).file, "total");
const diagsBefore = lowerer.diags.length;
const sinkMark = lowerer.diagSinkMark();
try {
const previousThis = lowerer.ctx.thisLocal;
const receiver =
@@ -4542,10 +4547,11 @@ function lowerStaticFieldInitsInner(lowerer: Lowerer, info: ClassInfo): IrStmt[]
if (!(e instanceof PoisonError)) throw e;
lowerer.stats.statementsFailed++;
lowerer.bumpFileStat(locOf(f.initializer).file, "failed");
if (isJsSourceFile(f.initializer.getSourceFile()) && lowerer.diagSink === null) {
const fence = lowerer.deferToRuntimeFence(diagsBefore, f.initializer, {
kind: "statement",
});
if (isJsSourceFile(f.initializer.getSourceFile())) {
const fence =
lowerer.diagSink === null
? lowerer.deferToRuntimeFence(diagsBefore, f.initializer, { kind: "statement" })
: lowerer.capturedRuntimeFence(sinkMark, f.initializer);
if (fence) out.push(fence);
}
}
@@ -7619,6 +7625,7 @@ export function fieldInitStmts(lowerer: Lowerer, info: ClassInfo, thisLocal: IrL
lowerer.stats.statementsTotal++;
lowerer.bumpFileStat(locOf(f.initializer).file, "total");
const diagsBefore = lowerer.diags.length;
const sinkMark = lowerer.diagSinkMark();
try {
const value = lowerer.lowerExprExpecting(f.initializer, f.type);
out.push({
@@ -7633,10 +7640,11 @@ export function fieldInitStmts(lowerer: Lowerer, info: ClassInfo, thisLocal: IrL
if (!(e instanceof PoisonError)) throw e;
lowerer.stats.statementsFailed++;
lowerer.bumpFileStat(locOf(f.initializer).file, "failed");
if (isJsSourceFile(f.initializer.getSourceFile()) && lowerer.diagSink === null) {
const fence = lowerer.deferToRuntimeFence(diagsBefore, f.initializer, {
kind: "statement",
});
if (isJsSourceFile(f.initializer.getSourceFile())) {
const fence =
lowerer.diagSink === null
? lowerer.deferToRuntimeFence(diagsBefore, f.initializer, { kind: "statement" })
: lowerer.capturedRuntimeFence(sinkMark, f.initializer);
if (fence) out.push(fence);
}
}
@@ -672,6 +672,7 @@ export function lowerStmts(lowerer: Lowerer, stmts: readonly ts.Statement[]): Ir
if (sf !== undefined) lowerer.bumpFileStat(sf.fileName, "total");
}
const diagsBefore = lowerer.diags.length;
const sinkMark = lowerer.diagSinkMark();
try {
let lowered: IrStmt | IrStmt[] | null;
let appended = false;
@@ -756,6 +757,11 @@ export function lowerStmts(lowerer: Lowerer, stmts: readonly ts.Statement[]): Ir
},
});
if (fence) out.push(fence);
} else if (deferFences) {
// Inside a capture buffer the owner keeps the diagnostics, but
// the statement still must not vanish from a result it keeps.
const fence = lowerer.capturedRuntimeFence(sinkMark, stmt);
if (fence) out.push(fence);
}
}
}
@@ -7194,23 +7194,8 @@ export class Lowerer {
}
this.runtimeFences.push(...captured);
const first = captured[0];
const fence = this.runtimeFenceStmt(captured[0], node, target.fallback, target.bareMessage);
const loc = locOf(node);
const code = first?.code ?? target.fallback!.code;
const baseMessage = first?.message ?? target.fallback!.message;
const messageLoc = first?.loc ?? loc;
const source = first
? (this.program.getSourceFile(messageLoc.file) ?? node.getSourceFile())
: node.getSourceFile();
const pos = ts.getLineAndCharacterOfPosition(source, messageLoc.start);
const fence: IrStmt = {
kind: "runtimeFence",
code,
message: target.bareMessage
? baseMessage
: `${baseMessage} [${code} at ${messageLoc.file}:${pos.line + 1}]`,
loc,
};
if (target.kind === "statement") return fence;
const params = target.params ?? [];
@@ -7238,6 +7223,54 @@ export class Lowerer {
return { kind: "closure", fnName: fn.name, captures: [], type: target.type, loc };
}
/** The runtimeFence statement that throws `first`'s message (or the
* fallback's) at `node`. */
private runtimeFenceStmt(
first: ScrDiagnostic | undefined,
node: ts.Node,
fallback: { code: string; message: string } | undefined,
bareMessage = false,
): IrStmt & { kind: "runtimeFence" } {
const loc = locOf(node);
const code = first?.code ?? fallback?.code ?? "SC1090";
const baseMessage =
first?.message ??
fallback?.message ??
"this statement uses a construct with no static lowering";
const messageLoc = first?.loc ?? loc;
const source = first
? (this.program.getSourceFile(messageLoc.file) ?? node.getSourceFile())
: node.getSourceFile();
const pos = ts.getLineAndCharacterOfPosition(source, messageLoc.start);
return {
kind: "runtimeFence",
code,
message: bareMessage
? baseMessage
: `${baseMessage} [${code} at ${messageLoc.file}:${pos.line + 1}]`,
loc,
};
}
/** The current length of the capture buffer (diagSink), or 0 without one:
* the mark capturedRuntimeFence reads the refusal from. */
diagSinkMark(): number {
return this.diagSink?.length ?? 0;
}
/** The runtimeFence for a JavaScript statement refused while a capture
* buffer (diagSink) is active. The buffer's owner decides whether its
* diagnostics report, and some owners keep the lowered result (a
* successful tryLowerExpression candidate, a computed class base whose
* diagnostics defer to the class). Without a fence the refused statement
* would vanish from that result; with one it throws the refusal when it
* runs, exactly like the fence lowerStmts emits outside a buffer. The
* diagnostics stay in the buffer for its owner. Null without a buffer. */
capturedRuntimeFence(sinkMark: number, node: ts.Node): IrStmt | null {
if (this.diagSink === null) return null;
return this.runtimeFenceStmt(this.diagSink[sinkMark], node, undefined);
}
/** All diagnostics land here; while a generic instance body is lowering,
* the instantiation context is appended so the user knows which concrete
* types made the (source-anchored) construct fail. */
@@ -9872,27 +9905,21 @@ export class Lowerer {
* instance type. Collection and body lowering own a refusal of that
* class: the poison reaches the declaration or statement that asked, as
* before. A speculative whole-program analysis owns none, so there the
* class is unmappable for the query and its diagnostics are dropped;
* lowering the class's own declaration collects it again and reports
* them if the program reaches it. */
* refusal stays reported (the build fails with it, as it did where such
* an analysis already caught the poison) and the class is unmappable for
* the query instead of the poison escaping the compiler. */
collectLocalClassForType(decl: ts.ClassLikeDeclaration): boolean {
if (this.speculativeTypeQueryDepth === 0 || this.fnStack.length > 0) {
this.lowerClassExpressionInfo(decl);
return true;
}
const sink: ScrDiagnostic[] = [];
const previousSink = this.diagSink;
this.diagSink = sink;
try {
this.lowerClassExpressionInfo(decl);
return true;
} catch (e) {
if (e instanceof PoisonError) return false;
throw e;
} finally {
this.diagSink = previousSink;
}
for (const d of sink) this.pushDiag(d);
return true;
}
lowerClassExpression(expr: ts.ClassLikeDeclaration): IrExpr {
@@ -1,6 +0,0 @@
{
"order": [
"<repo>/tests/corpus/6102-local-class-type-query.js"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/6110-cjs-speculative-wrapper-fences.cjs"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/diagnostics/local-class-type-query-js.js"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/diagnostics/speculative-function-refusal.ts"
],
"diags": []
}
@@ -1,42 +0,0 @@
// A whole-program analysis that queries types before reachability is known
// (the computed-key write census here) can name a function-local class whose
// collection is refused. That query has no owner for the refusal: the class
// stays unmapped for it, and the refusal belongs to the class's own
// declaration, which this program never executes (it used to crash the
// compiler with an uncaught PoisonError).
function main(run) {
if (!run) return "skipped";
const Project = class {
constructor() {
this.name = "p";
}
close() {
if (this.missingFilesMap) {
this.missingFilesMap.clear();
this.missingFilesMap = void 0;
}
}
add(k) {
(this.missingFilesMap || (this.missingFilesMap = new Map())).set(k, 1);
}
};
const holder = { p: new Project() };
const key = String(run);
holder.p[key] = "q";
return holder.p.name;
}
class Counter {
constructor() {
this.counts = {};
}
bump(key) {
this.counts[key] = (this.counts[key] || 0) + 1;
return this.counts[key];
}
}
const counter = new Counter();
const word = process.argv.length > 99 ? "unused" : "word";
counter.bump(word);
console.log(main(0), counter.bump(word));
@@ -0,0 +1,27 @@
// @deferred-fences: 9
// Function expressions lowered speculatively — the `.call(this)` and
// `.apply` receivers of CommonJS wrappers, and a computed class base — keep
// every statement of their bodies. A statement with no static lowering
// (here `module.exports` read as a value) compiles to its runtime fence
// instead of vanishing; these never run, so the binary matches Node.
const verbose = process.argv.length > 99;
(function () {
var root = this;
console.log("call wrapper");
if (verbose) console.log(root === module.exports);
console.log("call wrapper done");
}).call(this);
(function (label) {
console.log("apply wrapper", label);
if (verbose) console.log(typeof module.exports);
}).apply(null, ["ok"]);
class Base {
hello() {
return "hello";
}
}
class Derived extends (function () {
if (verbose) console.log(module.exports);
return Base;
})() {}
console.log(new Derived().hello());
@@ -0,0 +1,26 @@
// A whole-program analysis that queries types before reachability is known
// (the computed-key write census here, through `holder.p`) names a
// function-local class whose collection is refused. The refusal is reported,
// once, at the class; it used to crash the compiler with an uncaught
// PoisonError.
function main() {
const Project = class _Project {
constructor() {
this.name = "p";
}
close() {
if (this.missingFilesMap) {
this.missingFilesMap.clear();
this.missingFilesMap = void 0;
}
}
add(k) {
(this.missingFilesMap || (this.missingFilesMap = new Map())).set(k, 1);
}
};
const holder = { p: new Project() };
const key = process.argv.length > 5 ? String(process.argv.length) : "name";
holder.p[key] = "q";
console.log(holder.p.name);
}
main();
@@ -0,0 +1,14 @@
// Function expressions lowered speculatively — an `.apply` receiver and a
// computed class base — report a refused statement in their body like any
// other function: the statement never disappears from the build.
(function () {
console.log("wrapper");
console.log(eval("1 + 1"));
}).apply(null, []);
class Base {}
class Derived extends (function () {
console.log(new Function("return 1")());
return Base;
})() {}
console.log(typeof Derived);
@@ -0,0 +1,6 @@
local-class-type-query-js.js:14:9 - error SC1090: fields assigned outside the constructor's top level ('this.missingFilesMap' would be undefined until the first assignment runs — assign it unconditionally at the top of the constructor) is not supported yet
13 | this.missingFilesMap.clear();
14 | this.missingFilesMap = void 0;
| ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~
15 | }
@@ -0,0 +1,17 @@
speculative-function-refusal.ts:6:15 - error SC2020: 'eval' is part of the standard library types but has no scriptc lowering yet
5 | console.log("wrapper");
6 | console.log(eval("1 + 1"));
| ^~~~
7 | }).apply(null, []);
hint: runtime code evaluation cannot be compiled ahead of time
speculative-function-refusal.ts:11:15 - error SC2020: 'new Function' is part of the standard library types but has no scriptc lowering yet
10 | class Derived extends (function () {
11 | console.log(new Function("return 1")());
| ^~~~~~~~~~~~~~~~~~~~~~~~
12 | return Base;
hint: runtime code generation cannot be compiled ahead of time (the eval stance) — write the function
-2
View File
@@ -2210,7 +2210,6 @@
"605-closures-forward-capture-tdz.ts": 3277,
"6100-record-get-or-create.ts": 3771,
"6101-record-get-or-create-unchecked/main.ts": 2107,
"6102-local-class-type-query.js": 2600,
"6103-typeof-builtin-bindings.cjs": 2202,
"6104-typeof-builtin-bindings.mjs": 2031,
"6105-cjs-top-level-this/main.js": 1371,
@@ -5258,7 +5257,6 @@
"605-closures-forward-capture-tdz.ts": 4638,
"6100-record-get-or-create.ts": 3173,
"6101-record-get-or-create-unchecked/main.ts": 3228,
"6102-local-class-type-query.js": 1967,
"6103-typeof-builtin-bindings.cjs": 1796,
"6104-typeof-builtin-bindings.mjs": 1715,
"6105-cjs-top-level-this/main.js": 3213,
+53
View File
@@ -960,3 +960,56 @@ for (const input of [ciphertext, Buffer.from("forged data"), Buffer.alloc(0)]) {
expect(r.stderr).toBe("");
});
});
// Function expressions the frontend lowers speculatively (the receiver of
// `.call`/`.apply`, a computed class base) keep their refused statements as
// runtime fences. They used to vanish: the capture buffer held the refusal,
// the candidate was kept, and the statement was simply missing from it.
describe("refused statements in speculatively lowered JavaScript functions", () => {
test.each([
[
"speculative-call-this",
`(function () {
var root = this;
console.log("before");
console.log(root === module.exports);
console.log("after");
}).call(this);
`,
"before\n",
4,
],
[
"speculative-apply",
`(function () {
console.log("before");
console.log(typeof module.exports);
}).apply(null, []);
`,
"before\n",
3,
],
[
"speculative-class-base",
`class Base {}
class Derived extends (function () {
console.log("before");
console.log(typeof module.exports);
return Base;
})() {}
console.log(typeof Derived);
`,
"before\n",
4,
],
] as const)("%s", async (name, source, stdout, line) => {
const r = await compileAndRun(name, source, "cjs");
expect(r.exitCode).toBe(1);
expect(r.stdout).toBe(stdout);
expect(r.stderr).toMatch(
new RegExp(
`^Uncaught Error: 'module\\.exports' is part of the standard library types but has no scriptc lowering yet \\[SC2020 at .*${name}\\.cjs:${line}\\]\\n$`,
),
);
});
});
+2 -2
View File
@@ -73,7 +73,7 @@ const KNOWN: Record<string, Known> = {
"node24-env": { run: "SC1090" },
"node24-esm-metadata": { run: "SC1090" },
"node24-event-emitter": { run: "SC2020" },
"node24-file-url-conversion": { run: "SC2020" },
"node24-file-url-conversion": { build: "SC1090" },
"node24-fs-descriptors": { run: "SC1090" },
"node24-mime": { run: "SC2020" },
"node24-mime-errors": { run: "SC1090" },
@@ -104,7 +104,7 @@ const KNOWN: Record<string, Known> = {
"package-imports": { build: "SC2013" },
"promise-bridge": { build: "SC1090" },
"self-name": { build: "SC2013" },
"stream-shims": { run: "SC1090" },
"stream-shims": { build: "SC1090" },
"util-shims": { run: "SC2020" },
"web-streams": { run: "SC1090" },
"workspace-copied": { build: "SC2013" },