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

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions