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.
Summary
declare const/declare let/declare varare ambient declarations: TypeScript erases them, and they must bind nothing at run time. Perry lowers them as real bindings initialized toundefined. That shadows the global they describe, so a read never reaches the real global object property (neither a data property nor an accessor).declare varalso installs a non-configurable global property, which makes a laterObject.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-typesvs 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.5undefinedlet-valueundefinednone/defineProperty ok, read: 1{"configurable":false,"writable":true}/TypeError: Cannot redefine property: dVarstringundefinedWithout the
declareline, perry matches node on the same reads, including bare reads and writes through accessors installed onglobalThisandtypeofof 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 toLet { name: "w", mutable: false, init: Some(Undefined) }.Where
crates/perry-hir/src/lower/stmt.rs:453: theast::Decl::Var(var_decl)arm never checksvar_decl.declare. By contrast,declare functionis skipped at line 405 anddeclare moduleat line 1379. OtherVarDecllowering 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 constfor a global getter, and I wrongly concluded that perry never consults accessors for bare identifier reads. It does; thisdeclareshadowing was what hid the accessor.