Skip to content

hir: declare const/let/var is lowered as a real binding initialized to undefined — shadows the global it describes #10363

Description

@proggeramlug

Summary

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
(globalThis as any).dData = 5;
declare const dData: number;
console.log("declare const over a global data property:", dData);
// decl2.ts
(globalThis as any).dLet = "let-value";
declare let dLet: string;
console.log("declare let:", dLet);
// decl3.ts
declare var dVar: any;
const before = 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() { return 1; }, configurable: true });
  console.log("defineProperty ok, read:", dVar);
} catch (e: any) { console.log("defineProperty threw " + e.name + ": " + e.message); }
// decl4.ts
function f(): string {
  (globalThis as any).dInner = "global";
  return typeof dInner;
}
declare const dInner: string;
console.log("typeof a declared global from a function:", f());
node perry
decl1 5 undefined
decl2 let-value undefined
decl3 none / defineProperty ok, read: 1 {"configurable":false,"writable":true} / TypeError: Cannot redefine property: dVar
decl4 string undefined

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.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions