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
new globalThis.X(...) ignores the qualifier and builds a same-named imported class instead of the intrinsic #10359
new globalThis.X(...) ignores the globalThis. qualifier and resolves X by bare name. When a module legitimately imports a user class that shadows a global, the qualified form builds the user class instead of the intrinsic.
The aliased form works, which is what makes this two lowerings rather than one broken one:
newglobalThis.Event("ping")// perry: the USER classconstE=globalThis.Event;newE("ping")// perry: the intrinsic — correct
Reproducer
ev.ts:
exportclassEvent{readonlytag="user-Event"}
main.ts:
// @ts-nocheckimport{Event}from"./ev.js"// a legitimate, explicit import that shadows the bare nameconste: any=newglobalThis.Event("ping")console.log("tag:",e.tag)console.log("type:",e.type)console.log("ctor-name:",e.constructor?.name)console.log("is-user-class:",einstanceofEvent)
Measured (perry @ v0.5.1579 + #10356, vs bun 1.3.14)
cell
bun
perry
e.tag
undefined
user-Event
✗
e.type
ping
undefined
✗
e.constructor?.name
Event
Event
ok (both classes are named Event — this cell cannot discriminate)
e instanceof Event
false
true
✗ decisive
The last cell is the proof: the value built through globalThis.Event is an instance of the imported class.
Wider matrix on the same fixture — only the direct qualified new is affected:
cell
bun
perry
new Event().tag (the local binding, legitimately the user class)
user-Event
user-Event ok
typeof globalThis.Event
function
function ok
globalThis.Event?.name
Event
Event ok
new globalThis.Event("ping").type
ping
undefined✗
const E = globalThis.Event; new E("ping").type
ping
ping ok
new globalThis.CustomEvent("x",{detail:7}).detail
7
7 ok
With no shadowing user class in scope, every globalThis.-qualified form is correct (Event, CustomEvent, URL, Headers all pass). The defect only appears once a same-named binding exists in the module.
Why it matters
globalThis.X is the idiom people reach for precisely to escape a local shadow — it is the documented workaround for this situation, so silently resolving it to the shadow defeats its only purpose.
OpenCode's graph has many such shadows: packages/sdk/js/src/v2/gen/sdk.gen.ts exports Event, File and Request; packages/core exports Error twice; Effect exports Request, WebSocket, FormData and Storage.
Relationship to other issues
Distinct from #10356. There the class was never imported and should not have been in scope at all. Here the import is legitimate and the bare name should be the user class — but the globalThis.-qualified read must still reach the intrinsic.
Same family as #10303 (global.x reads collapsing to the intrinsic surface while the write landed): a qualified access and its unqualified sibling take different lowerings, and only one honors the qualifier. The asymmetry between the direct and aliased forms above is the signature.
Correction: the trigger is an IMPORTED shadow, not a local one
My "Why it matters" section implied OpenCode's many export class Error / export class Event declarations put this on a live path. Having measured it, that is not right and I want the narrowing on the record before anyone chases it.
A local shadowing declaration in the same module does not trigger the bug. Fixture matching OpenCode's packages/core/src/snapshot.ts (a local export class Error plus instanceof globalThis.Error at line 254):
All nine agree. So snapshot.ts and ripgrep.ts are not affected, and neither is cross-spawn-spawner.ts (its globalThis.Error use has no Error import in scope).
The reproducer in the issue body stands exactly as written — it uses import { Event } from "./ev.js", an imported binding, and that is the necessary condition. Two independent axes also do not trigger it on their own:
instanceof globalThis.X appears correct in every case I measured; only the construct form (new globalThis.X(...)) is wrong.
With no shadowing binding at all, every globalThis.-qualified form is correct.
So the precise trigger is: new globalThis.X(...) in a module that has an imported binding named X.
I have not found a live instance of that conjunction in OpenCode, so this is a correctness bug without a known OpenCode repro today — it is filed on the strength of the reproducer, not on an observed failure. Worth fixing because globalThis.X exists specifically to escape a shadow, but it should be prioritized accordingly.
Fixed on main by #10375, which landed via merge train 207 (v0.5.1585, ff19bd536a).
Closing manually: a merge train cherry-picks its source PRs and lands them as its own PR, so #10375 was closed rather than merged and its Fixes #10359 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
proggeramlug commentedon Sep 16, 2026
Correction: the trigger is an IMPORTED shadow, not a local one
My "Why it matters" section implied OpenCode's many
export class Error/export class Eventdeclarations put this on a live path. Having measured it, that is not right and I want the narrowing on the record before anyone chases it.A local shadowing declaration in the same module does not trigger the bug. Fixture matching OpenCode's
packages/core/src/snapshot.ts(a localexport class Errorplusinstanceof globalThis.Errorat line 254):realError instanceof globalThis.ErrortruetruelocalError instanceof globalThis.ErrorfalsefalselocalError instanceof Error(local)truetruerealError instanceof Error(local)falsefalsemsg(localError)— the snapshot.ts:254 expression[object Object][object Object]new globalThis.Error('x').messagexxnew globalThis.Error('x')._tagundefinedundefinedglobalThis.Error === ErrorfalsefalseAll nine agree. So
snapshot.tsandripgrep.tsare not affected, and neither iscross-spawn-spawner.ts(itsglobalThis.Erroruse has noErrorimport in scope).The reproducer in the issue body stands exactly as written — it uses
import { Event } from "./ev.js", an imported binding, and that is the necessary condition. Two independent axes also do not trigger it on their own:instanceof globalThis.Xappears correct in every case I measured; only the construct form (new globalThis.X(...)) is wrong.globalThis.-qualified form is correct.So the precise trigger is:
new globalThis.X(...)in a module that has an imported binding namedX.I have not found a live instance of that conjunction in OpenCode, so this is a correctness bug without a known OpenCode repro today — it is filed on the strength of the reproducer, not on an observed failure. Worth fixing because
globalThis.Xexists specifically to escape a shadow, but it should be prioritized accordingly.fix(codegen): build the intrinsic, not a runtime read, for a shadowed…
fix(hir,codegen): construct the global in `new globalThis.X()` when a…
fix(codegen): build the intrinsic, not a runtime read, for a shadowed…
proggeramlug commentedon Sep 17, 2026
Fixed on
mainby #10375, which landed via merge train 207 (v0.5.1585,ff19bd536a).Closing manually: a merge train cherry-picks its source PRs and lands them as its own PR, so #10375 was closed rather than merged and its
Fixes #10359keyword 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.