#3153

Add the Temporal declarations for a reference to the global, not for the word

Opencramforcewants to merge intofrom

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 24

On 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 Temporal in a position that looks a binding up (not a member name, another module's export name, or a label) that resolveName finds 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.ts and 8929-temporal-reexport (new, from the review): a type-only local Temporal beside a typeof test, destructuring from globalThis, and a re-export of another module's Temporal, 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 own Temporal, 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:check clean; program-facts.test.ts passes 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

16
vercel[bot]6d ago

The latest updates on your projects. Learn more about Vercel for GitHub.

ProjectDeploymentActionsUpdated
scriptc ReadyPreview, v0Nov 7, 2026 12:35pm UTC
vercel[bot]reviewed6d ago
cramforcereviewed6d ago
cramforcereviewed6d ago
cramforcereviewed6d ago
vercel[bot]reviewed6d ago
vercel[bot]reviewed6d ago
vercel[bot]reviewed6d ago
cramforcereviewed6d ago
cramforcereviewed6d ago
vercel[bot]reviewed6d ago
cramforce5d ago

Merged 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 on ts.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, was sf.text.includes(…); it is ts.sourceTextIncludes(sf, …) now. A plain sf.text read 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 of packages/compiler/src/frontend, library/, and prepare.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.