Resolve export bindings to their canonical module - #1648
Open
andreasrosdal wants to merge 3 commits into
Open
Conversation
`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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Two independent reasons why
ResolveExportreports a name as ambiguous when both routes reach the same binding. Each commit stands on its own, but the fourtest262/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 theimportedBoundNamesis not appended tolocalExportEntriesbut rewritten into an indirect entry{[[ModuleRequest]]: ie.[[ModuleRequest]], [[ImportName]]: ie.[[ImportName]]}, soResolveExportwalks through to the module the name came from.add_module_variables()keeps it a local export and resolves it to the closure variable, which makesResolveExportreturn the re-exporting module as the binding's[[Module]]: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, whichjs_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, notm, so the same namespace re-exported by two modules looks like two distinct bindings:ResolveExportreturnsResolvedBinding { [[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-bindingsgoes from 4/9 failures to 0/9; the four entries are dropped fromtest262_errors.txt.test/language/module-code,test/language/exportandtest/language/importshow 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