Summary
Third companion bug to #5918/#5922/#5924 in the same import_function_prefixes/import_function_origin_names flat-map family, found via the same real-world sst/opencode source compile — after fixing #5918 (plain-vs-plain collision), #5922 (namespace-vs-namespace prefix collision), and #5924 (namespace-vs-namespace origin-name collision), opencode's provider.ts still failed to link.
provider.ts does:
import { mapValues, mergeDeep, omit, pickBy, sortBy } from "remeda"
// ...
import { Effect, Layer, Context, Schema, Types } from "effect"
omit (a bare, PLAIN named import from remeda) and Context (a NAMED import of a value effect's barrel re-exports as a NAMESPACE, per the #5922/#5924 pattern) both end up writing into the SAME flat import_function_prefixes/import_function_origin_names maps for the bare key "omit" — because effect's real Context.ts ALSO exports a member literally named omit (export const omit = (...keys) => …, a genuine Context API for omitting keys from a Context).
Since Context is imported (and thus its namespace members registered) AFTER omit in provider.ts's source order, Context's registration for omit overwrites remeda's — even though these two omits have nothing to do with each other. The bare call omit(x, y) (from remeda) then resolves against Context.ts's prefix, and since remeda's omit needed an origin-name rename (remeda ships heavily chunk-split dist output; the real implementation lives under a short internal identifier) that rename ALSO survives in the flat import_function_origin_names map (Context's own omit needs no rename, so it never touches that map) — the combination produces a request for perry_fn_<Context.ts's prefix>__<remeda's internal short name>, an undefined symbol.
Why #5922/#5924's fix didn't cover this
#5922 and #5924 added namespace_member_prefixes / namespace_member_origin_names (keyed by (namespace_local, member_name)) to disambiguate collisions between two namespaces. Both fixes left the original flat-map writes for namespace members in place (as a fallback for codegen paths that might not consult the namespace-scoped maps). That flat write is exactly what collides with a plain import here — a plain (non-namespaced) import has no other resolution path, so it must always win the flat map, but a straight .insert() from namespace processing can still clobber it depending on import-statement order.
Fix
In crates/perry/src/commands/compile/run_pipeline.rs, both namespace-processing branches (import * as X / namespace_like_local, and the named-import-of-namespace-reexport handled_as_namespace_reexport branch) now use .entry(name).or_insert_with(...) instead of .insert(...) for the flat import_function_prefixes / import_function_origin_names writes. Plain named imports are untouched and still use unconditional .insert(). This means:
- A plain import processed before a colliding namespace: the namespace's
or_insert sees the slot taken and skips — plain import's entry survives.
- A plain import processed after a colliding namespace: the namespace's
or_insert claims the (empty) slot first, but the plain import's later unconditional insert overwrites it anyway.
Either way, the plain import wins, independent of source order. Namespace-member resolution is unaffected because the three real consumers (expr/static_method.rs, lower_call/namespace_call.rs, expr/property_get.rs) already prefer the namespace-scoped maps (per #5922/#5924) and never actually need the flat-map fallback for genuine namespace members.
Repro
// plain_mod.ts
export const omit = () => "plain-omit"
// ns_impl.ts
export const omit = () => "namespace-omit"
// barrel.ts
export * as NsA from "./ns_impl.ts"
// main.ts (order A)
import { omit } from "./plain_mod.ts"
import { NsA } from "./barrel.ts"
console.log(omit()) // expect "plain-omit"
console.log(NsA.omit()) // expect "namespace-omit"
Verified both import orderings (plain-then-namespace and namespace-then-plain) match Node exactly after the fix; pre-fix, whichever import was processed second silently won the flat map for the loser's binding.
PR incoming.
Summary
Third companion bug to #5918/#5922/#5924 in the same
import_function_prefixes/import_function_origin_namesflat-map family, found via the same real-worldsst/opencodesource compile — after fixing #5918 (plain-vs-plain collision), #5922 (namespace-vs-namespace prefix collision), and #5924 (namespace-vs-namespace origin-name collision), opencode'sprovider.tsstill failed to link.provider.tsdoes:omit(a bare, PLAIN named import from remeda) andContext(a NAMED import of a valueeffect's barrel re-exports as a NAMESPACE, per the #5922/#5924 pattern) both end up writing into the SAME flatimport_function_prefixes/import_function_origin_namesmaps for the bare key"omit"— becauseeffect's realContext.tsALSO exports a member literally namedomit(export const omit = (...keys) => …, a genuine Context API for omitting keys from aContext).Since
Contextis imported (and thus its namespace members registered) AFTERomitinprovider.ts's source order, Context's registration foromitoverwrites remeda's — even though these twoomits have nothing to do with each other. The bare callomit(x, y)(from remeda) then resolves againstContext.ts's prefix, and since remeda'somitneeded an origin-name rename (remeda ships heavily chunk-split dist output; the real implementation lives under a short internal identifier) that rename ALSO survives in the flatimport_function_origin_namesmap (Context's ownomitneeds no rename, so it never touches that map) — the combination produces a request forperry_fn_<Context.ts's prefix>__<remeda's internal short name>, an undefined symbol.Why #5922/#5924's fix didn't cover this
#5922 and #5924 added
namespace_member_prefixes/namespace_member_origin_names(keyed by(namespace_local, member_name)) to disambiguate collisions between two namespaces. Both fixes left the original flat-map writes for namespace members in place (as a fallback for codegen paths that might not consult the namespace-scoped maps). That flat write is exactly what collides with a plain import here — a plain (non-namespaced) import has no other resolution path, so it must always win the flat map, but a straight.insert()from namespace processing can still clobber it depending on import-statement order.Fix
In
crates/perry/src/commands/compile/run_pipeline.rs, both namespace-processing branches (import * as X/namespace_like_local, and the named-import-of-namespace-reexporthandled_as_namespace_reexportbranch) now use.entry(name).or_insert_with(...)instead of.insert(...)for the flatimport_function_prefixes/import_function_origin_nameswrites. Plain named imports are untouched and still use unconditional.insert(). This means:or_insertsees the slot taken and skips — plain import's entry survives.or_insertclaims the (empty) slot first, but the plain import's later unconditional insert overwrites it anyway.Either way, the plain import wins, independent of source order. Namespace-member resolution is unaffected because the three real consumers (
expr/static_method.rs,lower_call/namespace_call.rs,expr/property_get.rs) already prefer the namespace-scoped maps (per #5922/#5924) and never actually need the flat-map fallback for genuine namespace members.Repro
Verified both import orderings (plain-then-namespace and namespace-then-plain) match Node exactly after the fix; pre-fix, whichever import was processed second silently won the flat map for the loser's binding.
PR incoming.