Stacked on #3133 (the registry of whole-program facts); this PR rewrites one entry of it.
The cliff
TypeScript's ESNext temporal library slice was added to a program's checker world, with one more program creation, whenever the word Temporal appeared anywhere in the text of an implementation file. The slice puts a new name on the global object's type, so a comment can flip a program between compiling and a type error:
type Names = Extract<keyof typeof globalThis, `Tempo${string}`>;
type Seen = [Names] extends [never] ? "absent" : "present";
const seen: Seen = "absent";
console.log(seen);
// Temporel is not in Node 24On main this compiles and prints absent. Change Temporel to Temporal in the comment and it is refused: Type '"absent"' is not assignable to type '"present"'.
The compiler's own sources are a second example. TEMPORAL_GLOBAL was spelled "Tempo" + "ral" so that scriptc compiling itself would not trip the scan, but two comments in lowerer.ts and host-globals.ts contain the word, so the scan matches there and every self-compile takes the extra program creation.
The fact, and the rule now
The fact is "this program refers to the Temporal global, which only the slice declares". A checker's library set belongs to its whole world, so this stays a program-wide fact, but it is decided from the syntax tree and the checker's scopes (programRefersToTemporal7):
- an identifier
Temporalin a position that looks a binding up (not a member name, another module's export name, or a label) thatresolveNamefinds no declaration for, asked with the meaning the reference needs (a type for a type reference's own name, a namespace for the root of a qualified type name, a value elsewhere); or - a member of that name read from the global object, by access (
globalThis.Temporal,globalThis["Temporal"]) or by destructuring (const { Temporal } = globalThis, with a string key or as a parameter default too). The object is the global object by one of its names, unless the program shadows it, or by the checker's type, so an alias counts.
A comment, a string, a member that happens to carry the name, another module's export of it, and a program's own declaration of it (a polyfill import, a local constant, a type alias or type parameter) add nothing. The text test that remains only skips files that cannot contain the name. The registry entry changes from names-temporal (source text) to refers-to-temporal (source symbols), and TEMPORAL_GLOBAL is spelled in one piece.
Proof
- The pair above compiles to byte-identical IR and executables in both spellings.
tests/corpus/8927-temporal-global-forms.tsand8929-temporal-reexport(new, from the review): a type-only localTemporalbeside atypeoftest, destructuring fromglobalThis, and a re-export of another module'sTemporal, which adds nothing. Both match Node.tests/corpus/8924-temporal-mentions.ts(new): the global-object type observes that the slice is absent while the file mentions the word in comments and strings, binds its ownTemporal, and has members of that name. Matches Node on both backends, plain and sanitized.- The programs that really refer to the global still match Node:
temporal-absent.ts,temporal-absent-probe.js,8904-host-global-absent-probes.ts,8831-host-global-dynamic-probes.ts,8161-global-computed-reads.cjs. - IR sweep of every corpus, diagnostics and benchmark program (5,507), main against this change: 5,226 identical IR, 281 refused with identical diagnostics, nothing else. No corpus program depended on a mention.
- Zero-cost gate,
--gate-corpus=all: PASS, 5183 identical, 96 both-fail, 8 shards, on the first head and again after each round of review fixes (8d07db57d, a61ef45e8). No differing row and no runtime-only row. pnpm test:ts7 --baselines-only,pnpm lint,pnpm format:checkclean;program-facts.test.tspasses with the updated entry.- The four PRs of the stack were also validated merged together (both test lanes, the plain and sanitized native bootstraps, and a pi build with unchanged diagnostics): the numbers are in a comment on #3133.
🤖 Generated with Claude Code
Activity
16Merged main (50bcfe1b3) into the branch: 014c03642.
Conflicts. Two files, both where this branch replaces programNamesTemporal7 (the word test) by programRefersToTemporal7 (the reference test):
packages/compiler/src/frontend/program.ts:main(#3251) rewrote the word test onts.sourceTextMatches, so the build cache can replay it after a comment edit. The word test stays deleted. The reference test's one raw-text read, the prefilter that skips files that cannot hold the name, wassf.text.includes(…); it ists.sourceTextIncludes(sf, …)now. A plainsf.textread records a read of the whole text (noteSourceText), for every source file of every program, which would undo #3251's reuse after comment edits.packages/compiler/src/program-facts.ts: the scan row is this branch's (programRefersToTemporal7,refers-to-temporal), with the count the census gives it, 2.
Re-run on the merged tree (Vercel Sandbox, Xeon 2.90GHz, 8 vCPU):
pnpm format:check,pnpm lint,pnpm test:ts7 --baselines-only: pass.- The Temporal corpus programs (8924, 8927, 8929 and the others that match), both backends: 14 passed plain, 14 passed sanitized.
program-facts.test.ts,input-tracker.test.ts,token-equivalence.test.ts, the rest ofpackages/compiler/src/frontend,library/, andprepare.test.ts(the build cache's contracts): 1,568 passed.diagnostics.test.ts: 309 passed.order-parity.test.ts: 123 passed.- Zero-cost gate,
--gate-corpus=all, no allow pattern, against 50bcfe1b3: PASS, 5,295 identical, 108 fail to build on both sides, 0 differing; bench −0.24 % [95 % CI −1.10 %, +0.65 %], self +0.40 % [−1.64 %, +0.85 %], fixtures −1.45 % [−3.31 %, +0.40 %]; the candidate compiler builds its own source.
Not re-run: the full lanes, the native bootstraps. I did not add a test that a comment edit in a program that names Temporal still reuses the frontend cache; prepare.test.ts passes, and the read now goes through the same function as main's other prefilters.
The latest updates on your projects. Learn more about Vercel for GitHub.