Skip to content

An import alias that collides with another import's exported name binds to the wrong module during module init #10286

Description

@proggeramlug

Parent: #10107 (OpenCode v1.18.30 native bring-up).

Summary

When a module has two aliased imports, and the local alias of one is the same identifier as the exported name of the other's source module, perry binds the alias to the wrong module — but only for calls made during module initialization. The call gets the other module's value, which is typically not a function, so it throws TypeError: value is not a function with no callee name.

This is the shape minifiers emit constantly. Short aliases like t, e, n get reused across import statements in the same file, so any minified dependency that calls an imported function at module scope can hit it.

Repro (4 files)

idx.js

var cat = { br: 1 }
function extend(o) { Object.assign(cat, o) }
function getCat() { return cat }
export { getCat, extend, cat }

leafT.mjs — exports a binding literally named t

const t = { kind: "obj-not-a-function" }
export { t }

consO.mjs — imports t under the alias e, and extend under the alias t

import { t as e } from "./leafT.mjs"
import { extend as t } from "./idx.js"
function n() { t({ spinner: e }) }
n()
export { n as reg }

mainO.ts

import { getCat } from "./idx.js"
import { reg } from "./consO.mjs"
console.log("typeof:", typeof reg, "catalogue:", JSON.stringify(getCat()))

bun 1.3.14 and node:

typeof: function catalogue: {"br":1,"spinner":{"kind":"obj-not-a-function"}}

perry 0.5.1571 (+ PRs 10279/10280/10282/10283), perry compile mainO.ts --platform bun:

TypeError: value is not a function
    at <anonymous>

t inside consO.mjs resolves to leafT's t — the object — instead of idx's extend.

Control matrix

All four controls pass under perry, which pins the trigger to the conjunction of three conditions:

variant change from the repro perry
repro fails
P alias extend as q instead of as t passes
Q leafT exports z instead of t passes
R the leafT import moved out of this module passes
S leafT imported without an alias, extend as u passes
T identical, but the call happens after init rather than during it passes

So all three are required: a local alias colliding with another import's exported name, both imports in the same module, and the call executing during module initialization. T is the interesting one — the same binding is correct once initialization is over, which points at init-time resolution rather than a permanently wrong symbol.

Where to look

crates/perry/src/commands/compile/run_pipeline.rs builds import_function_prefixes / import_function_origin_names keyed by identifier. There is already a comment there (marked "Issue #", so apparently never filed) describing a sibling collision:

exported_name is whatever the ORIGIN module happens to call it, so it can collide with an unrelated LOCAL alias elsewhere in the same file. Concrete repro: import { a as n, c as a } from "./x"; import { a as t } from "./y"

That fix only inserts the aliased case under local_name. This repro is aliased on both sides, so it should already be keyed by local name — meaning the collision is somewhere else on the init path, not in that map. Same family as #9847/#9857 (name-keyed instead of local-id-keyed registration).

Why it matters

This is the last blocker for OpenCode's terminal interface under perry. The real case is opentui-spinner@0.0.7/dist/solid.mjs, four minified statements:

import{t as e}from"./src-DjeqLSfu.mjs";import{extend as t}from"@opentui/solid/components";function n(){t({spinner:e})}n();export{n as registerSpinner};

extend is a plain function declaration in @opentui/solid. A module-body trace of the subgraph under both engines shows perry running 17 modules against bun's 147, stopping immediately after @opentui/solid/components.js — the module right before this one. The at n / at n frames in the original stack are this file's minified n.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

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