You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
hir: declare const/let/var is lowered as a real binding initialized to undefined — shadows the global it describes #10363
declare const / declare let / declare var are ambient declarations: TypeScript erases them, and they must bind nothing at run time. Perry lowers them as real bindings initialized to undefined. That shadows the global they describe, so a read never reaches the real global object property (neither a data property nor an accessor). declare var also installs a non-configurable global property, which makes a later Object.defineProperty(globalThis, …) for that name throw.
This is the usual way TypeScript code refers to a global supplied by the host, a polyfill, or a script loaded earlier.
Reproduction
node --experimental-strip-types vs perry, Linux x86_64, perry compile --no-auto-optimize. v0.5.1579 (fcd108b); none of these files contains an object literal, so no in-flight PR touches the path.
// decl1.ts(globalThisasany).dData=5;declareconstdData: number;console.log("declare const over a global data property:",dData);
// decl3.tsdeclarevardVar: any;constbefore=Object.getOwnPropertyDescriptor(globalThis,"dVar");console.log("declare var property before any write:",before===undefined ? "none" : JSON.stringify({configurable: before.configurable,writable: before.writable}));try{Object.defineProperty(globalThis,"dVar",{get(){return1;},configurable: true});console.log("defineProperty ok, read:",dVar);}catch(e: any){console.log("defineProperty threw "+e.name+": "+e.message);}
// decl4.tsfunctionf(): string{(globalThisasany).dInner="global";returntypeofdInner;}declareconstdInner: string;console.log("typeof a declared global from a function:",f());
Without the declare line, perry matches node on the same reads, including bare reads and writes through accessors installed on globalThis and typeof of an accessor. The global lookup itself is fine; the ambient declaration is what breaks it. The HIR shows it directly: declare const w: any; lowers to Let { name: "w", mutable: false, init: Some(Undefined) }.
Where
crates/perry-hir/src/lower/stmt.rs:453: the ast::Decl::Var(var_decl) arm never checks var_decl.declare. By contrast, declare function is skipped at line 405 and declare module at line 1379. Other VarDecl lowering sites (function bodies, pre-scans) probably need the same check. An ambient declaration should bind nothing, so the name resolves to the global exactly as if the line were absent.
Found while
Validating #10361 (builder fold / #10357). An earlier probe there used declare const for a global getter, and I wrongly concluded that perry never consults accessors for bare identifier reads. It does; this declare shadowing was what hid the accessor.
Fixed on main by #10374, which landed via merge train 204 (v0.5.1582, 872ce07e24).
Closing manually: a merge train cherry-picks its source PRs and lands them as its own PR, so #10374 was closed rather than merged and its Fixes #10363 keyword never evaluated. The train's merge receipt records the landed tree as identical to the validated train, with per-commit patch-id and authorship proofs for every source commit.
Activity
fix(hir): keep observable user code out of a folded builder (PerryTS#…
fix(hir): an ambient `declare const/let/var` binds nothing (#10363)
proggeramlug commentedon Sep 17, 2026
Fixed on
mainby #10374, which landed via merge train 204 (v0.5.1582,872ce07e24).Closing manually: a merge train cherry-picks its source PRs and lands them as its own PR, so #10374 was closed rather than merged and its
Fixes #10363keyword never evaluated. The train's merge receipt records the landed tree as identical to the validated train, with per-commit patch-id and authorship proofs for every source commit.fix(hir): keep observable user code out of a folded builder (#10357)