mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-10-27 06:43:19 +08:00
Merge origin/main into opt/builtins-t10-t12
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -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 {
|
||||
|
||||
-6
@@ -1,6 +0,0 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/6102-local-class-type-query.js"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/6110-cjs-speculative-wrapper-fences.cjs"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/diagnostics/local-class-type-query-js.js"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -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
|
||||
@@ -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,
|
||||
|
||||
@@ -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$`,
|
||||
),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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" },
|
||||
|
||||
Reference in New Issue
Block a user