From 5632decbb96f4cdd569435029af78e09f7fa59f4 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Tue, 20 Oct 2026 08:52:39 -0500 Subject: [PATCH] Skip untaken global-detection arms, and fix the shim test checks - An if statement whose condition is a global-detection test (static-typeof.ts) lowers only its taken arm, unless the untaken arm declares a var or function binding. Code Node never runs no longer counts as a deferred site, so global-detection-typeof.cjs is fully static. - The resolver parity sweep skips specifiers the package-shim registry serves: they resolve to the built-in module by design. - undici-shim-types.test.ts joins the TypeScript 5 import allowlist. - node-types-timers uses a 200 ms timer, so the immediate runs first on a loaded machine as well. --- .../src/frontend/lowering/lower-stmts.ts | 15 +++++++++++ .../src/frontend/lowering/static-typeof.ts | 25 +++++++++++++++++++ .../compiler/test/ts7/resolver-parity.test.ts | 5 ++++ scripts/test-ts7.mjs | 1 + tests/harness/node-types-timers.test.ts | 5 ++-- 5 files changed, 49 insertions(+), 2 deletions(-) diff --git a/packages/compiler/src/frontend/lowering/lower-stmts.ts b/packages/compiler/src/frontend/lowering/lower-stmts.ts index 4dd6c2968..7b8147405 100644 --- a/packages/compiler/src/frontend/lowering/lower-stmts.ts +++ b/packages/compiler/src/frontend/lowering/lower-stmts.ts @@ -3,6 +3,7 @@ import { storedMatchAllRowsReadByElements, } from "./containers/match-row-uses.js"; import { dynUndefinedExpr, nodeThrowExpr, numLit, varRef } from "../../ir/build.js"; +import { declaresHoistedBinding, staticCondition } from "./static-typeof.js"; import { lowerCollectionSpread } from "./containers/collection-methods.js"; import { lowerEventObjectPropertyWrite } from "./lower-event-target.js"; import { lowerDynObjectLiteral } from "./expressions/object-literals.js"; @@ -1501,6 +1502,20 @@ export function lowerStmt(lowerer: Lowerer, stmt: ts.Statement): IrStmt | IrStmt } if (ts.isIfStatement(stmt)) { const cond = lowerer.lowerCondition(stmt.expression); + // A global-detection condition (static-typeof.ts) folds; its untaken + // arm is code Node never runs and does not lower, unless it declares a + // hoisted binding. + const decided = staticCondition(lowerer, stmt.expression); + if (decided !== null) { + const taken = decided ? stmt.thenStatement : stmt.elseStatement; + const untaken = decided ? stmt.elseStatement : stmt.thenStatement; + if (untaken === undefined || !declaresHoistedBinding(untaken)) { + const arm = taken === undefined ? [] : lowerer.lowerScopedBlock(taken); + return decided + ? { kind: "if", cond, then: arm, else_: null, loc: locOf(stmt) } + : { kind: "if", cond, then: [], else_: arm, loc: locOf(stmt) }; + } + } // Aliased-typeof narrows (ms's `var type = typeof val` guard): the // then-branch lowers under what a TRUE condition proves, the // else-branch under a FALSE one — the var/let alias narrowing the diff --git a/packages/compiler/src/frontend/lowering/static-typeof.ts b/packages/compiler/src/frontend/lowering/static-typeof.ts index bf3382837..ea8da5e1e 100644 --- a/packages/compiler/src/frontend/lowering/static-typeof.ts +++ b/packages/compiler/src/frontend/lowering/static-typeof.ts @@ -98,3 +98,28 @@ export function staticallySelected(lowerer: Lowerer, expr: ts.Expression): ts.Ex e = taken ? e.whenTrue : e.whenFalse; } } + +/** True when `stmt` declares a binding that is visible outside it: a `var` + * or a function declaration, outside nested functions. Such an arm still + * lowers when its condition is decided, for the hoisted binding. */ +export function declaresHoistedBinding(stmt: ts.Statement): boolean { + let found = false; + const visit = (n: ts.Node): void => { + if (found) return; + if (ts.isFunctionDeclaration(n)) { + found = true; + return; + } + if (ts.isFunctionLike(n) || ts.isClassDeclaration(n) || ts.isClassExpression(n)) return; + if ( + ts.isVariableDeclarationList(n) && + (n.flags & (ts.NodeFlags.Let | ts.NodeFlags.Const)) === 0 + ) { + found = true; + return; + } + n.forEachChild(visit); + }; + visit(stmt); + return found; +} diff --git a/packages/compiler/test/ts7/resolver-parity.test.ts b/packages/compiler/test/ts7/resolver-parity.test.ts index b1788d4da..5049a5508 100644 --- a/packages/compiler/test/ts7/resolver-parity.test.ts +++ b/packages/compiler/test/ts7/resolver-parity.test.ts @@ -22,6 +22,7 @@ import { resolveTypeDirective, setProjectPathMappings, } from "../../src/frontend/resolve.js"; +import { packageShimFor } from "../../src/frontend/package-shims.js"; import { isRelativeSpecifier } from "../../src/frontend/workspace-registry.js"; import { options5 } from "./harness.js"; @@ -138,6 +139,10 @@ test("relative and bare specifiers across the whole test tree resolve identicall // the resolver follows Node — corpus 2124 pins the behavior // differentially, so parity has nothing to check here. if (spec.startsWith("#/")) continue; + // A package scriptc implements itself (package-shims.ts) resolves + // to its built-in module by design; 5.9.3 answers the installed + // package. + if (packageShimFor(file, spec) !== null) continue; bare++; // PROJECT imports (#alias / self-name): ts5's NON-external answers // for bare specifiers are exactly the package.json-mediated diff --git a/scripts/test-ts7.mjs b/scripts/test-ts7.mjs index e022c1ff5..5ee1f50fc 100644 --- a/scripts/test-ts7.mjs +++ b/scripts/test-ts7.mjs @@ -30,6 +30,7 @@ const ALLOWED_TYPESCRIPT5_IMPORTS = new Set([ "packages/compiler/src/library/semantic-source.ts", "packages/compiler/test/ts7/harness.ts", "packages/compiler/test/ts7/resolver-parity.test.ts", + "packages/compiler/test/undici-shim-types.test.ts", ]); function sourceFilesUnder(dir) { diff --git a/tests/harness/node-types-timers.test.ts b/tests/harness/node-types-timers.test.ts index 257d644ff..ec52f5b5c 100644 --- a/tests/harness/node-types-timers.test.ts +++ b/tests/harness/node-types-timers.test.ts @@ -52,8 +52,9 @@ const program = `class Holder { timer: NodeJS.Timeout | null = null; } const holder = new Holder(); -holder.timer = setTimeout(() => console.log("never"), 50); -const kept = setTimeout(() => console.log("fired", holder.timer === null), 10); +holder.timer = setTimeout(() => console.log("never"), 1000); +// Long enough that the immediate runs first even on a loaded machine. +const kept = setTimeout(() => console.log("fired", holder.timer === null), 200); kept.unref().ref(); kept.refresh(); console.log("hasRef", kept.hasRef());