Infer untyped JavaScript collection types once, without re-entering

The inference reads types while it runs: resolving a candidate's
constructor through a destructured require (const { TextDecoder } = util)
asks for the namespace's type, which asked for the inferred collection
types again and restarted the inference until the stack overflowed. The
lowerer now stores an empty table before inferring, so reads during the
inference see no inferred types, and stores the result after.
This commit is contained in:
Chris Tate
2026-10-24 22:34:45 -05:00
parent 0fbb47382d
commit 33f63655a0
2 changed files with 36 additions and 3 deletions
@@ -75,9 +75,15 @@ export function initializedCollectionType(
/** The inferred collection type of an identifier that names a proven
* binding, or null. Computed for every JavaScript file of the program on
* first use. */
* first use. The inference itself reads types (a candidate's constructor
* through a destructured require, for example); while it runs, those reads
* see no inferred types rather than starting it again. */
export function inferredCollectionType(lowerer: Lowerer, node: ts.Identifier): ts.Type | null {
const types = (lowerer.inferredCollectionTypes ??= inferProgram(lowerer));
if (lowerer.inferredCollectionTypes === undefined) {
lowerer.inferredCollectionTypes = new Map();
lowerer.inferredCollectionTypes = inferProgram(lowerer);
}
const types = lowerer.inferredCollectionTypes;
return types.size === 0 ? null : (types.get(node) ?? null);
}
@@ -1,4 +1,4 @@
import { mkdtempSync, readFileSync, rmSync } from "node:fs";
import { mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { expect, test } from "vitest";
@@ -62,3 +62,30 @@ test("untyped JavaScript collections take the types their uses prove, and only t
rmSync(dir, { recursive: true, force: true });
}
});
// Resolving a candidate's constructor through a destructured require reads
// types while the inference runs; those reads must not start it again.
test("the inference runs once when a candidate's values come from a destructured require", async () => {
const dir = mkdtempSync(join(tmpdir(), "scriptc-js-collection-reentry-"));
try {
const entry = join(dir, "main.cjs");
writeFileSync(
entry,
[
'const util = require("node:util");',
"const { TextDecoder } = util;",
"const decoder = new TextDecoder();",
'const decoders = new Map([["utf-8", decoder]]);',
"const seen = new Set([decoder]);",
"console.log(decoders.size, seen.size);",
"",
].join("\n"),
);
const output = join(dir, "main.ir.json");
const lowered = await compile(entry, { outputKind: "ir", outDir: dir, outPath: output });
if (!lowered.ok) throw new Error(JSON.stringify(lowered.diagnostics));
expect(validateModule(deserializeModule(readFileSync(output, "utf8")))).toEqual([]);
} finally {
rmSync(dir, { recursive: true, force: true });
}
});