Skip to content

import_function_origin_names collision for namespace-reexport members (Context.Service resolves to wrong suffix) #5924

Description

@proggeramlug

Summary

Follow-up to #5922/#5923: fixing the import_function_prefixes/namespace_member_prefixes collision alone was not enough for a real-world sst/opencode compile — a companion map, import_function_origin_names, has the exact same flat-map collision architecture and independently broke the same real code.

provider.ts does export class Service extends Context.Service<Service, Interface>()("@opencode/Provider") {} — a genuine call to Context.Service, where Context is reached via import { Effect, Layer, Context, Schema, Types } from "effect" (a named import of a value the source module re-exports as a namespace).

Context.ts (effect's real TS source) directly exports Service — no rename needed. But linking failed with:

Undefined symbols for architecture arm64:
  "_perry_fn_..._effect_src_Context_ts__a", referenced from:
      _perry_closure_packages_opencode_src_provider_provider_ts__297
      _perry_closure_packages_opencode_src_provider_provider_ts__307

Note the symbol suffix is __a, not __Service — the compiler is looking for the wrong symbol entirely, not just at the wrong module.

Root cause

import_function_origin_names: HashMap<String, String> (issue #678) records symbol-suffix overrides for re-export renames (export { default as render } → call perry_fn_<origin>__default, not __render). It's a flat map keyed by bare member name, populated once per specifier in the handled_as_namespace_reexport loop (run_pipeline.rs) that processes import { Effect, Layer, Context, Schema, Types } from "effect" — one iteration per specifier, all writing into the same shared map for this file.

Effect (processed before Context in specifier order) apparently re-exports something named Service from an internal chunk under a different origin identifier (a rename), so its iteration inserts import_function_origin_names["Service"] = "<effect's internal origin name>". When Context's own turn comes (a direct, unrenamed Service export), no override is needed, so nothing overwrites the earlier (wrong) entry — it survives in the flat map. Codegen then resolves Context.Service via this contaminated flat entry instead of Context's own (correct, unrenamed) symbol.

This is architecturally identical to #5922/#680: the fix there added a per-namespace-scoped companion map (namespace_member_prefixes) for import_function_prefixes. import_function_origin_names needs the same treatment (namespace_member_origin_names, keyed by (namespace_local, member_name)), consulted by every codegen site that builds a perry_fn_<src>__<suffix> symbol for a namespace-member access (expr/static_method.rs, lower_call/namespace_call.rs, expr/property_get.rs).

One subtlety the #5922 fix didn't need: a sparse per-namespace map (only inserting entries when a rename applies) is not sufficient here, because an unrenamed member with no entry in the per-namespace map would still fall through to the (possibly contaminated) flat map. The fix registers every namespace member unconditionally (renamed or not), mirroring how namespace_member_prefixes is already populated for every export, not just the interesting ones.

Repro

// ns_a_impl.ts
export const a = () => "NsA-Service"
// ns_a.ts — re-export rename: origin name "a" != export name "Service"
export { a as Service } from "./ns_a_impl.ts"
// ns_b.ts — direct export, no rename
export const Service = () => "NsB-Service"
// barrel.ts
export * as NsA from "./ns_a.ts"
export * as NsB from "./ns_b.ts"
// main.ts
import { NsA, NsB } from "./barrel.ts"
console.log(NsA.Service())
console.log(NsB.Service())

Node: NsA-Service / NsB-Service.
perry (pre-fix): link failure, perry_fn_..._ns_b_ts__a undefined — NsB.Service() incorrectly tries to resolve via NsA's rename.

Impact

This was the actual remaining blocker for a real-world sst/opencode native compile after #5918/#5919 and #5922/#5923Context.Service/Option-adjacent effect namespace members.

PR incoming.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions