diff --git a/packages/compiler/src/frontend/lowering/expressions/object-literals.ts b/packages/compiler/src/frontend/lowering/expressions/object-literals.ts index e5b464edc..7947f2830 100644 --- a/packages/compiler/src/frontend/lowering/expressions/object-literals.ts +++ b/packages/compiler/src/frontend/lowering/expressions/object-literals.ts @@ -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; } diff --git a/packages/compiler/src/frontend/lowering/frozen-records.test.ts b/packages/compiler/src/frontend/lowering/frozen-records.test.ts new file mode 100644 index 000000000..47a9d7c80 --- /dev/null +++ b/packages/compiler/src/frontend/lowering/frozen-records.test.ts @@ -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"]); + }); +}); diff --git a/packages/compiler/src/frontend/lowering/frozen-records.ts b/packages/compiler/src/frontend/lowering/frozen-records.ts index 75056b3cc..2555dfc2a 100644 --- a/packages/compiler/src/frontend/lowering/frozen-records.ts +++ b/packages/compiler/src/frontend/lowering/frozen-records.ts @@ -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; arrays: Set } { @@ -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); } diff --git a/packages/compiler/src/frontend/lowering/function-returns.ts b/packages/compiler/src/frontend/lowering/function-returns.ts index 7378f26a7..0c09999c7 100644 --- a/packages/compiler/src/frontend/lowering/function-returns.ts +++ b/packages/compiler/src/frontend/lowering/function-returns.ts @@ -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 diff --git a/packages/compiler/src/frontend/lowering/lower-modules.ts b/packages/compiler/src/frontend/lowering/lower-modules.ts index f82728c6e..00299ce58 100644 --- a/packages/compiler/src/frontend/lowering/lower-modules.ts +++ b/packages/compiler/src/frontend/lowering/lower-modules.ts @@ -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 diff --git a/packages/compiler/src/frontend/lowering/lower-stmts.ts b/packages/compiler/src/frontend/lowering/lower-stmts.ts index 2d10d56b3..729ea4b40 100644 --- a/packages/compiler/src/frontend/lowering/lower-stmts.ts +++ b/packages/compiler/src/frontend/lowering/lower-stmts.ts @@ -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; diff --git a/packages/compiler/src/frontend/lowering/lowerer.ts b/packages/compiler/src/frontend/lowering/lowerer.ts index 8b1f54763..3b370e17c 100644 --- a/packages/compiler/src/frontend/lowering/lowerer.ts +++ b/packages/compiler/src/frontend/lowering/lowerer.ts @@ -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 diff --git a/packages/compiler/src/frontend/pipeline.ts b/packages/compiler/src/frontend/pipeline.ts index 25af03106..28133abd5 100644 --- a/packages/compiler/src/frontend/pipeline.ts +++ b/packages/compiler/src/frontend/pipeline.ts @@ -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 */ } diff --git a/packages/compiler/src/frontend/program.ts b/packages/compiler/src/frontend/program.ts index 2f8e4deae..56b315638 100644 --- a/packages/compiler/src/frontend/program.ts +++ b/packages/compiler/src/frontend/program.ts @@ -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;