Avoid call-stack overflow from spread pushes over large compiler lists (#2553)

This commit is contained in:
Chris Tate
2026-10-29 15:22:36 -05:00
committed by GitHub
9 changed files with 96 additions and 27 deletions
@@ -2566,7 +2566,7 @@ export function lowerObjectLiteral(
) {
throw err;
}
lowerer.runtimeFences.push(...lowerer.diags.splice(propDiagsBefore));
for (const d of lowerer.diags.splice(propDiagsBefore)) lowerer.runtimeFences.push(d);
droppedNames.add(name);
continue;
}
@@ -0,0 +1,58 @@
import { describe, expect, test } from "vitest";
import {
DYN,
STRING,
VOID,
type IrExpr,
type IrFunction,
type IrStmt,
type SrcLoc,
type IrType,
} from "../../ir/ir.js";
import { freezableTypes } from "./frozen-records.js";
import type { Lowerer } from "./lowerer.js";
const loc: SrcLoc = { file: "frozen.ts", start: 0, end: 1 };
/** An untyped conversion of a `type`-typed value: a live-reference root. */
function dynFrom(type: IrType, count: number): IrStmt[] {
const expr: IrExpr = {
kind: "dynFrom",
value: { kind: "varRef", localId: "s.0", type, loc },
type: DYN,
loc,
};
const body: IrStmt[] = [];
for (let i = 0; i < count; i++) body.push({ kind: "exprStmt", expr, loc });
return body;
}
function fn(body: IrStmt[]): IrFunction {
return { name: "f", params: [], returnType: VOID, locals: [], body, loc };
}
function stub(shapes: [string, unknown][] = []): Lowerer {
return {
untypedIntegrity: true,
classes: new Map(),
shapes: new Map(shapes),
unions: new Map(),
} as unknown as Lowerer;
}
describe("freezableTypes", () => {
test("seeds a program-sized live list without a spread argument limit", () => {
// A spread (`stack.push(...live)`) throws RangeError above roughly 100k
// arguments, which a large program's live-reference roots reach.
const roots = 200_000;
const { shapes, arrays } = freezableTypes(stub(), [fn(dynFrom(STRING, roots))]);
expect(shapes.size).toBe(0);
expect(arrays.size).toBe(0);
});
test("a live record type makes its shape freezable", () => {
const lowerer = stub([["r0", { id: "r0", fields: [] }]]);
const live = dynFrom({ kind: "record", shapeId: "r0" }, 1);
expect([...freezableTypes(lowerer, [fn(live)]).shapes]).toEqual(["r0"]);
});
});
@@ -114,7 +114,7 @@ function holdsAbsentState(e: IrExpr): boolean {
/** The record shapes and array types (typeKeys) whose values the program
* may freeze. */
function freezableTypes(
export function freezableTypes(
lowerer: Lowerer,
functions: IrFunction[],
): { shapes: Set<string>; arrays: Set<string> } {
@@ -141,16 +141,21 @@ function freezableTypes(
});
if (!lowerer.untypedIntegrity && !freezesUntyped) return { shapes, arrays };
const stack: IrType[] = [];
// Both seeds are program-sized (one entry per untyped conversion or check in
// every function), so they are pushed with loops: a spread argument list
// exceeds the engine's limit and throws RangeError on a large program.
// A record checked from an untyped value copies it, frozen when the value
// is (a fresh literal frozen in a JavaScript file included), so the shapes
// a check builds are freezable in a program that freezes untyped values.
if (freezesUntyped) stack.push(...checked);
if (freezesUntyped) {
for (const type of checked) stack.push(type);
}
if (lowerer.untypedIntegrity) {
// An untyped reference to a class instance, or to a record or array
// boxed live, reaches the values its fields and elements hold as further
// live references. Class instances also cross into untyped code through
// compiled callbacks, so every class is a root.
stack.push(...live);
for (const type of live) stack.push(type);
for (const info of lowerer.classes.values())
for (const field of info.def.fields) stack.push(field.type);
}
@@ -578,8 +578,8 @@ export function resolveInferredReturn(
// per-statement deferral, applied to one return of an instance).
const captured = lowerer.diags.splice(diagsBefore);
const ice = captured.filter((d) => d.code === "SC9001");
if (ice.length > 0) lowerer.diags.push(...ice);
lowerer.runtimeFences.push(...captured.filter((d) => d.code !== "SC9001"));
for (const d of ice) lowerer.diags.push(d);
for (const d of captured) if (d.code !== "SC9001") lowerer.runtimeFences.push(d);
const first = captured.find((d) => d.code !== "SC9001");
// Property writes, not literal element writes: the class method
// census reads `x["kind"] = …` as a possible write to every method
@@ -1189,8 +1189,8 @@ function deferJsCollectionDiags(lowerer: Lowerer, sf: ts.SourceFile, diagsBefore
if (!isJsSourceFile(sf)) return;
const captured = lowerer.diags.splice(diagsBefore);
const ice = captured.filter((d) => d.code === "SC9001");
if (ice.length > 0) lowerer.diags.push(...ice);
lowerer.runtimeFences.push(...captured.filter((d) => d.code !== "SC9001"));
for (const d of ice) lowerer.diags.push(d);
for (const d of captured) if (d.code !== "SC9001") lowerer.runtimeFences.push(d);
}
/** A scalar-literal expression a single-value `module.exports =` can carry
@@ -961,8 +961,8 @@ function lowerOneStatement(
if (deferFences && lowerer.diagSink === null && lowerer.diags.length > diagsBefore) {
const flushed = lowerer.diags.splice(diagsBefore);
const ice = flushed.filter((d) => d.code === "SC9001");
lowerer.diags.push(...ice);
lowerer.runtimeFences.push(...flushed.filter((d) => d.code !== "SC9001"));
for (const d of ice) lowerer.diags.push(d);
for (const d of flushed) if (d.code !== "SC9001") lowerer.runtimeFences.push(d);
}
}
@@ -1222,7 +1222,10 @@ export function predeclareForwardFnDecl(lowerer: Lowerer, symbol: ts.Symbol): bo
entry.out.push(lowerer.lowerNestedFunctionDecl(decl));
} catch (e) {
entry.ctx.hoistedFnDecls.delete(decl);
if (previousSink !== null) previousSink.push(...lowerer.diags.splice(diagsBefore));
if (previousSink !== null) {
const flushed = lowerer.diags.splice(diagsBefore);
for (const d of flushed) previousSink.push(d);
}
throw e;
} finally {
lowerer.diagSink = previousSink;
@@ -9644,9 +9644,9 @@ export class Lowerer {
// array-HOF loop functions, which ride the same list), and the
// implicit-any instances lowered eagerly at their first call sites.
this.settleSharedHelperLocations([...functions, ...this.liftedFns, ...this.implicitFns]);
functions.push(...this.liftedFns);
functions.push(...this.implicitFns);
functions.push(...runtimeClassConstructs(this, functions));
for (const fn of this.liftedFns) functions.push(fn);
for (const fn of this.implicitFns) functions.push(fn);
for (const fn of runtimeClassConstructs(this, functions)) functions.push(fn);
if (this.remainder) {
// Deferred collection diagnostics nothing flushed — declarations no
@@ -9933,9 +9933,11 @@ export class Lowerer {
private reportBlockedBody(decl: ts.FunctionDeclaration): void {
const diags = blockedBodyDiagnostics(this, decl);
const symbol = declSymbolOf(this, decl);
if (symbol && this.alreadyFlushed.has(symbol))
this.reachedBlockedBodyDiagnostics.push(...diags);
else for (const d of diags) this.pushDiag(d);
if (symbol && this.alreadyFlushed.has(symbol)) {
for (const d of diags) this.reachedBlockedBodyDiagnostics.push(d);
} else {
for (const d of diags) this.pushDiag(d);
}
}
/** Whether run() counts a signature-blocked declaration in
@@ -10647,7 +10649,7 @@ export class Lowerer {
...byName(this.liftedFns),
...byName(this.implicitFns),
];
functions.push(...runtimeClassConstructs(this, functions));
for (const fn of runtimeClassConstructs(this, functions)) functions.push(fn);
if (this.regexGroupResults) for (const fn of functions) foldRegexSpanReads(this, fn);
// The worklist can leave a virtual site with a single implementation.
// Devirtualize after construction/override reachability has settled.
@@ -10936,10 +10938,10 @@ export class Lowerer {
}
const captured = this.diags.splice(diagsBefore);
if (captured.some((d) => d.code === "SC9001")) {
this.diags.push(...captured);
for (const d of captured) this.diags.push(d);
return null;
}
this.runtimeFences.push(...captured);
for (const d of captured) this.runtimeFences.push(d);
const fence = this.runtimeFenceStmt(captured[0], node, target.fallback, target.bareMessage);
const loc = locOf(node);
@@ -11228,7 +11230,7 @@ export class Lowerer {
.slice(diagsBefore)
.filter((d) => d.rejects !== undefined || this.loweredOnceDiags.has(d));
this.diags.length = diagsBefore;
this.diags.push(...kept);
for (const d of kept) this.diags.push(d);
}
/** A unit that still fails after its retries commits its first
@@ -11241,7 +11243,8 @@ export class Lowerer {
d.rejects !== undefined || this.loweredOnceDiags.has(d);
const kept = this.diags.slice(diagsBefore).filter(carried);
this.diags.length = diagsBefore;
this.diags.push(...first.filter((d) => !carried(d)), ...kept);
for (const d of first) if (!carried(d)) this.diags.push(d);
for (const d of kept) this.diags.push(d);
}
/** Lowers one unit whose body-level poison skips it (a generic
+2 -2
View File
@@ -455,8 +455,8 @@ function scanDroppedPackages(
return at !== null && wanted.has(at.name);
});
const scanned = scanFiles(load.program, files, { userJs: false, dropped: true, edges: true });
into.modules.push(...scanned.modules);
into.edges.push(...scanned.edges);
for (const module of scanned.modules) into.modules.push(module);
for (const edge of scanned.edges) into.edges.push(edge);
} catch {
/* report-only detection */
}
+3 -3
View File
@@ -977,7 +977,7 @@ function loadProgram7(
);
if (extraRoots.length === 0) break;
program.dispose();
programRoots.push(...extraRoots);
for (const root of extraRoots) programRoots.push(root);
program = host.createProgram([...programRoots, overridesDtsPath()], options);
if (pass === 31) throw new Error("static program-root discovery did not converge");
}
@@ -4727,7 +4727,7 @@ function cycleReachedDeferredReferences7(
if (symbol === undefined) continue;
const units = unitsOfName.get(name)!;
unitsBySymbol.set(symbol, [...(unitsBySymbol.get(symbol) ?? []), ...units]);
allUnits.push(...units);
for (const unit of units) allUnits.push(unit);
}
// A named class expression's inner name (`class Impl { static x =
// Impl.m(); }`) names the class inside its own body, as the binding it
@@ -4896,7 +4896,7 @@ export function moduleEarlyBindings(
const out: CycleEarlyBinding[] = [];
for (const sf of order) {
if (sf.isDeclarationFile || exact.has(sf)) continue;
out.push(...moduleEarlyBindingsOf7(program, sf, order));
for (const binding of moduleEarlyBindingsOf7(program, sf, order)) out.push(binding);
}
program.analysis.moduleEarlyBindings = { key, bindings: out };
return out;