Skip to content

import_function_prefixes/origin_names collide between a namespace member and an unrelated plain named import #5927

Description

@proggeramlug

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.

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