Repository navigation
Importing one name from a module also binds that module's OTHER exported classes — a user class Request shadows the global Request in the importer (OpenCode TUI bootstrap wall) #10356
Description
Activity
This is the third instance of one family, and worth naming
All three are a rule that is correct about the case its author enumerated and silently wrong about the case they did not:
the rule the case it was right about the case nothing covered #9847 a bare X = native.method()registers Xone module, one Xthe name was registered module-wide, so an unrelated Xelsewhere inherited it — fixed by keying onLocalIdinstead of the name#10356 (this) implicit class registration is safe because "local classes with the same name take precedence" a same-named local class in the importer a global intrinsic is not a local class, so nothing outranked the implicit entry #10346 area OBJ_FLAG_HAS_DESCRIPTORSdisqualifies the receivera receiver genuinely carrying descriptors the fact is per-key, so one descriptor key disqualified every key on the object — in four separate places The shared mechanism in #9847 and this one is the same: registration keyed by NAME. Here it is visible directly in the consumers —
class_table,imported_class_source_nameandmethod_param_countsare all keyed byeffective_name/ic.nameincrates/perry-codegen/src/codegen/mod.rs(~618, ~1023, ~1663).That name-keying is also why the narrower fix does not exist. Keeping the registration "for dispatch only" but excluding it from
imported_class_ctorswould not help:class_table["Request"]still shadows the global for anything name-based. There is no way to keep the registration without keeping the shadowing, which is why the fix skips it outright.Scope in a real program
OpenCode's graph shadows far more than
Request:packages/sdk/js/src/v2/gen/sdk.gen.ts:1385 export class Event extends HeyApiClient packages/sdk/js/src/v2/gen/sdk.gen.ts:1829 export class File extends HeyApiClient packages/sdk/js/src/v2/gen/sdk.gen.ts:6319 export class Request extends HeyApiClient packages/core/src/snapshot.ts:18 export class Error extends Schema.TaggedErrorClass packages/core/src/ripgrep.ts:41 export class Error extends Schema.TaggedErrorClass packages/core/src/config/provider.ts:7 export class Request extends Schema.Class effect/src/unstable/cluster/Entity.ts:580 export class Request extends Data.Class effect/src/unstable/socket/Socket.ts:573 export class WebSocket extends Context.Service effect/src/unstable/http/HttpBody.ts:395 export class FormData extends Proto effect/.../EventLogServer{Un,}encrypted.ts export class Storage extends Context.Servicesdk.gen.tsalone shadowsEvent,FileandRequest. That matches the observed symptom: across builds v8→v15 the OpenCode TUI failed with a different error each time, each build clearing one wall and exposing the next. A single defect producing a sequence of unrelated-lookingTypeErrors is what a per-name shadowing looks like from the outside.- added a commit that references this issue
on Sep 16, 2026 Fixed on
mainby #10358, which landed via merge train 203 (v0.5.1580,33690c5635).Closing manually: a merge train cherry-picks its source PRs and lands them as its own PR, so #10358 was closed rather than merged and its
Fixes #10356keyword 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.- added 4 commits that reference this issue
on Sep 17, 2026 - added a commit that references this issue
on Oct 3, 2026
Summary
A module that imports anything from another module also picks up every exported class of that module as a bare identifier — including names that are global intrinsics. So a user module exporting
class Requestmakes a completely unrelatednew Request(url, init)in the importing module construct that user class instead of the global fetchRequest.This is the current OpenCode TUI bootstrap failure.
packages/sdk/js/src/v2/client.tsdoes:gen/sdk.gen.ts:6319happens to containexport class Request extends HeyApiClient. It is not in the specifier list, so per ESM it is not inclient.ts's scope at all. perry binds it anyway,nextis aHeyApiClient,next.headersisundefined, and the TUI dies with:Reproducer
mod.ts:main.ts:Measured
new OpencodeClient().nameOpencodeClientOpencodeClientbase.methodGETundefinedbase.urlhttp://example.com/x?a=1undefinedtypeof base.headersobjectundefinednext.methodGETundefinedtypeof next.headersobjectundefinednext.headers.delete(...)okTHREW Cannot read properties of undefined (reading 'delete')base instanceof Requesttruetruebase.kindundefineduser-sdk-requestCell 9 names the mechanism directly:
basecarries a field from a class the module never imported.Node and tsc agree with bun —
Requestis simply not in scope from that import.Root cause
crates/perry/src/commands/compile/run_pipeline.rs~4790–4828, in the import handling:The over-registration is deliberate and its stated safety argument is that local classes of the same name win. That argument holds for local classes — but a global intrinsic is not a local class, so nothing outranks the implicit entry. Every name in
is_builtin_global_value_name(crates/perry-hir/src/analysis/builtins.rs) is exposed:Request,Response,Headers,URL,Event,Blob,File,FormData,WebSocket,Error,Map,Set, …A generated SDK exporting a class named
Request/Response/Headersis extremely common (hey-api, openapi-typescript, oazapfts all do it), so this is not an exotic collision.The entry lands in
imported_class_ctors, whichlower_call/new.rsconsults before the intrinsic path — the disassembly of the real OpenCode binary confirmsrewritecontains nojs_request_newcall at all, onlyjs_new_target_set(the generic construct path) and a call toopencode_packages_sdk_js_src_v2_gen_sdk_gen_ts__Request_constructor.Proposed fix
In that implicit registration loop only, skip classes whose name is a builtin global value name. Explicitly imported classes are pushed by the earlier specifier-driven sites and already win the
any(|c| c.name == ...)dedup, so an explicitimport { Request } from "./mod.js"keeps working — only the un-imported leak is removed.