Skip to content

Resolve export bindings to their canonical module - #1648

Open
andreasrosdal wants to merge 3 commits into
quickjs-ng:masterfrom
nordstjernen-web:fix-ambiguous-export-bindings
Open

Resolve export bindings to their canonical module#1648
andreasrosdal wants to merge 3 commits into
quickjs-ng:masterfrom
nordstjernen-web:fix-ambiguous-export-bindings

Conversation

@andreasrosdal

Copy link
Copy Markdown
Contributor

Two independent reasons why ResolveExport reports a name as ambiguous when both routes reach the same binding. Each commit stands on its own, but the four test262/test/language/module-code/ambiguous-export-bindings/ tests need both, so they are sent together.

1. Re-export an imported name as an indirect export

import {x} from "m"; export {x}; does not create a binding in the re-exporting module. ParseModule says so: an ExportEntry whose [[LocalName]] is one of the importedBoundNames is not appended to localExportEntries but rewritten into an indirect entry {[[ModuleRequest]]: ie.[[ModuleRequest]], [[ImportName]]: ie.[[ImportName]]}, so ResolveExport walks through to the module the name came from.

add_module_variables() keeps it a local export and resolves it to the closure variable, which makes ResolveExport return the re-exporting module as the binding's [[Module]]:

// m.mjs
export const x = 1;
// r1.mjs
import { x } from './m.mjs';
export { x };
// agg.mjs
export * from './m.mjs';
export * from './r1.mjs';
// main.mjs
import { x } from './agg.mjs';   // SyntaxError: export 'x' ... is ambiguous

V8 resolves this to 1. The rewrite happens where the local exports are resolved, since that is where the import entries are available to map the local name back to (module request, import name). A namespace import goes through the same path and lands on the * import name, which js_resolve_export1() already understands as "the imported module's namespace".

2. Compare resolved bindings by their canonical module

The star-export loop decides whether two routes reach the same binding by comparing the module that held the entry and the entry's local name. For export * as ns from "m" that module is whichever module wrote the re-export, not m, so the same namespace re-exported by two modules looks like two distinct bindings:

// n1.mjs, n2.mjs
export * as ns from './m.mjs';
// agg.mjs
export * from './n1.mjs';
export * from './n2.mjs';
// main.mjs
import { ns } from './agg.mjs';   // SyntaxError: ... 'ns' is ambiguous

ResolveExport returns ResolvedBinding { [[Module]]: importedModule, [[BindingName]]: namespace } for such an entry, so the re-exporting module does not take part in the comparison at all. Map an entry to that canonical pair before comparing.

Testing

test/language/module-code/ambiguous-export-bindings goes from 4/9 failures to 0/9; the four entries are dropped from test262_errors.txt. test/language/module-code, test/language/export and test/language/import show no regressions.

Note that V8 also rejects the second example, so this follows the spec text rather than V8.

🤖 Generated with Claude Code

https://claude.ai/code/session_014mv33YvfHz7t9mmkituBnn


Generated by Claude Code

claude added 3 commits August 6, 2026 17:21
`import {x} from "m"; export {x};` does not create a binding in the
re-exporting module. ParseModule says so: an ExportEntry whose
[[LocalName]] is one of the importedBoundNames is not appended to
localExportEntries but rewritten into an indirect entry
{[[ModuleRequest]]: ie.[[ModuleRequest]], [[ImportName]]: ie.[[ImportName]]},
so ResolveExport walks through to the module the name came from.

add_module_variables() keeps it a local export and resolves it to the
closure variable instead, which makes ResolveExport return the
re-exporting module as the binding's [[Module]]. Two star exports that
reach the same binding by different routes then look like two different
bindings and the name is reported as ambiguous:

    // m.mjs
    export const x = 1;
    // r1.mjs
    import { x } from './m.mjs';
    export { x };
    // agg.mjs
    export * from './m.mjs';
    export * from './r1.mjs';
    // main.mjs
    import { x } from './agg.mjs';   // SyntaxError: export 'x' ... is ambiguous

V8 resolves this to 1.

Rewrite such an entry into an indirect export at the point where the local
exports are resolved, where the import entries are available to map the
local name back to (module request, import name). A namespace import goes
through the same path and lands on the `*` import name, which
js_resolve_export1() already understands as "the imported module's
namespace".

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014mv33YvfHz7t9mmkituBnn
The star-export loop in js_resolve_export1() decides whether two routes to
the same export name reach the same binding by comparing the module that
held the entry and the entry's local name. For `export * as ns from "m"`
that module is whichever module wrote the re-export, not m, so the same
namespace re-exported by two modules looks like two distinct bindings:

    // m.mjs
    export const v = 1;
    // n1.mjs, n2.mjs
    export * as ns from './m.mjs';
    // agg.mjs
    export * from './n1.mjs';
    export * from './n2.mjs';
    // main.mjs
    import { ns } from './agg.mjs';   // SyntaxError: ... 'ns' is ambiguous

ResolveExport returns ResolvedBinding { [[Module]]: importedModule,
[[BindingName]]: namespace } for such an entry, so the re-exporting module
does not take part in the comparison at all.

Map an entry to that canonical pair before comparing. Together with the
previous commit this fixes the four test262 ambiguous-export-bindings tests
listed in test262_errors.txt; drop the entries.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014mv33YvfHz7t9mmkituBnn
Covers both halves. `import { foo } from "m"; export { foo }` has to resolve
to the same binding as `export { foo } from "m"`, so re-exporting a module
through one of each is not ambiguous; and a namespace re-exported by two
different modules is one binding whichever of `export * as ns from` and
`import * as ns; export { ns }` produced it -- checked by identity against
the namespace object itself.

The negative side is kept too: two genuinely different bindings stay
ambiguous, which leaves the name out of the namespace object and makes a
named import of it a SyntaxError, while the other names come through. Also
checks the ordinary cases the rewrite passes through: a renamed import, a
plain local export beside it, and that the re-exported binding stays live.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01K6eRbuuuCujKgQkrHgvMrc
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants