Skip to content

Teach metadce about WASM_ESM_INTEGRATION#27218

Open
guybedford wants to merge 6 commits into
emscripten-core:mainfrom
guybedford:metadce-esm-integration
Open

Teach metadce about WASM_ESM_INTEGRATION#27218
guybedford wants to merge 6 commits into
emscripten-core:mainfrom
guybedford:metadce-esm-integration

Conversation

@guybedford

Copy link
Copy Markdown
Collaborator

This makes -sWASM_ESM_INTEGRATION work at -O2 and above. Previously such builds failed in the JS optimizer's metadce (dead code elimination) pass with:

AssertionError: could not find the assignment to "wasmImports".

The reason is that metadce reconstructs the wasm⇄JS def/use graph by pattern-matching the idioms a normal build emits — the wasmImports object literal and wasmExports['x'] member uses. Under ESM integration those don't exist: the boundary is expressed with native ES module syntax instead, which is exactly the simpler, tooling-visible def/use graph this mode was designed to produce:

// wasm exports received by the JS:
import { main as _main, memory as wasmMemory } from './a.out.wasm';
// JS functions exported to the wasm:
export { _fd_write as fd_write };

So rather than synthesise the old shapes, metadce now reads the real module bindings directly.

  • emitDCEGraph recognises import {..} from './x.wasm' as wasm export nodes (collapsing aliases such as memory/wasmMemory onto a single node) and export { js as wasmName } as wasm import edges, while leaving re-exports (export { _main }) to root naturally.

  • applyDCEGraphRemovals prunes unused specifiers from those same import/export statements.

  • applyImportAndExportNameChanges applies binaryen's minified names to the wasm-facing side of each specifier, e.g.

    import { malloc as _malloc } from './a.out.wasm';
    export { _fd_write as fd_write };

    becomes

    import { c as _malloc } from './a.out.wasm';
    export { _fd_write as d };

    The local binding is preserved (an unaliased memory becomes b as memory, not a broken b).

Two supporting changes keep the two module interfaces in sync:

  • building.py — under ESM integration any export binaryen drops (including internal ones like the indirect function table) is also dropped from the JS import, so the JS never imports something the wasm no longer exports.
  • link.py — import/export name minification stays on for ESM, but import module name minification is disabled, since every import is rewritten to the single support-module source by create_esm_wrapper anyway (and minifying enva would break that rewrite).

Test coverage:

  • New js_optimizer fixtures exercising each pass on ESM-shaped input: emitDCEGraph-esm, applyDCEGraphRemovals-esm, applyImportAndExportNameChanges-esm.
  • New end-to-end other.test_metadce_esm_integration building hello_world.c with -O3 -sWASM_ESM_INTEGRATION, asserting the dangling table import is removed and names are minified consistently (the run is guarded on Node 25).

Resolves #27217.

Made with AI assistance under my review

@sbc100 sbc100 requested a review from kripken July 1, 2026 00:15
@kripken

kripken commented Jul 1, 2026

Copy link
Copy Markdown
Member

I can review the code here, but first, @brendandahl can you please confirm that this API (as in the description) is the one we want, long term?

@brendandahl

brendandahl commented Jul 1, 2026

Copy link
Copy Markdown
Collaborator

Looks as I'd expect. I believe we talked about trying to get rid of the _ prefix in ESM integration mode. I don't think that needs to be done in this PR though.

Comment thread test/test_other.py Outdated
Comment thread tools/building.py
# import, so any export binaryen drops (including internal ones like the
# indirect function table) must also be dropped from the JS import to
# keep the two module interfaces in sync.
if shared.is_user_export(native_name) or settings.WASM_ESM_INTEGRATION:

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@sbc100 do you know why we test is_user_export here? It seems like if binaryen drops an export, the JS side can't import it - nothing will arrive there - so we can remove it in non-esm mode too..?

@kripken kripken left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good aside from my comments/questions

@guybedford guybedford force-pushed the metadce-esm-integration branch 3 times, most recently from febb4fd to b17de01 Compare July 1, 2026 23:01
@kripken

kripken commented Jul 6, 2026

Copy link
Copy Markdown
Member

I have a problem reviewing this. I see two force-pushes since my last review. The first is

https://github.com/emscripten-core/emscripten/compare/981fe52afb062a6c38a989e14f48fec4eae40f44..febb4fd2cb50fe27b1191ef7ddb9f396abf0799d

which shows a large diff, most of it not from this PR, so I guess there was a merge of upstream mixed in? The second is

https://github.com/emscripten-core/emscripten/compare/febb4fd2cb50fe27b1191ef7ddb9f396abf0799d..b17de01857c976c65db5ac6bdf855a2ba8ae9424

which is empty.

How can I see the actual diff compared to my last review, of this PR itself?

(We have had this problem in the past with force-pushes, and I'm hoping there is a better solution now? If not, avoiding force-pushes might be the only solution?)

@guybedford

guybedford commented Jul 6, 2026

Copy link
Copy Markdown
Collaborator Author

I've undone the squash here, so the additional commit after the original PR is now visible again in the commit history.

@guybedford guybedford force-pushed the metadce-esm-integration branch 2 times, most recently from 84d9702 to 2785940 Compare July 6, 2026 22:25
@kripken

kripken commented Jul 6, 2026

Copy link
Copy Markdown
Member

Hmm, the second commit has whitespace changes to Changelog.md, and also other changes I don't recognize/expect - is the content there correct?

@kripken

kripken commented Jul 6, 2026

Copy link
Copy Markdown
Member

I also think I need to review the first commit again, unless there is a way to verify it is the code I've already reviewed? I don't see a way to do that but I might be missing it. (The dates are not helpful there, both from an hour ago, though I guess git dates are of limited use anyhow.)

If there isn't a good solution here I can just review this entire PR's content from scratch, but that will take me longer, and in that case I'd ask to avoid force-pushes in the future.

Sorry to be pedantic about this! @sbc100 has heard me complain about it many times before 😄 But I like to be able to know exactly what I've reviewed and what not.

@guybedford

Copy link
Copy Markdown
Collaborator Author

Yes it's correct - to verify you can see that the patches are the same between the original PR commit 02f63ec and the rebased one at 9cc076d.

The changelog whitespace was a regression from the original commit.

@sbc100

sbc100 commented Jul 6, 2026

Copy link
Copy Markdown
Collaborator

In that past I've managed to make @kripken / GitHub happy but recreating a version of the history that contains that exact original SHA that was used in the initial version of the PR and then ensuring that the rest of the change build on that.

I'm not sure what happened here exactly but if you can find the original SHA that @kripken reviewed you can build a new version of the history based on that (and using merge commits to pull in changes from from main). You can then force push this new version of the history and then his "reviewed" status but will be correct again for the original chunks/files.

@guybedford guybedford force-pushed the metadce-esm-integration branch from 2785940 to f62f6dc Compare July 6, 2026 23:44
@guybedford

Copy link
Copy Markdown
Collaborator Author

@sbc100 @kripken sure, I've entirely undone the rebasing so we are back to the exact 0b5218b commit from the original review.

@sbc100

sbc100 commented Jul 6, 2026

Copy link
Copy Markdown
Collaborator

Apologies for the annoying limitation of Github here. I myself use rebase and squash all the time and it also often annoys @kripken .. because it destroys the Github review history (a feature I myself much use when I'm reviewing).

@kripken kripken left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, I managed to review this now. Final comments below, lgtm otherwise.

Comment thread test/test_codesize.py Outdated
Comment thread tools/emscripten.py
Comment thread ChangeLog.md Outdated
Comment thread tools/acorn-optimizer.mjs Outdated
Building with -sWASM_ESM_INTEGRATION at -O2 or above failed in the JS
optimizer's metadce pass with 'could not find the assignment to
"wasmImports"'. In this mode the wasm<->JS boundary is expressed with
native ES import/export syntax rather than the wasmImports object and
wasmExports['x'] member uses that metadce pattern-matches.

Teach the three metadce-related acorn-optimizer passes about the ES form:
- emitDCEGraph reads 'import {..} from "./x.wasm"' as wasm export nodes
  (collapsing aliases such as memory/wasmMemory to one node) and
  'export { js as wasmName }' as wasm import edges, leaving re-exports
  (export { _main }) to root naturally.
- applyDCEGraphRemovals prunes unused specifiers from those statements.
- applyImportAndExportNameChanges applies minified names to the
  wasm-facing side of each specifier.

building.py also drops dropped exports (including internal ones like the
indirect function table) from the JS import to keep the two interfaces
in sync, and link.py keeps import/export name minification on for ESM
while disabling the now-pointless import module name minification.
@guybedford guybedford force-pushed the metadce-esm-integration branch from 1e79811 to 099f505 Compare July 7, 2026 19:09
Comment thread test/test_codesize.py Outdated
@sbc100 sbc100 changed the title Teach metadce about the WASM_ESM_INTEGRATION module boundary Teach metadce about WASM_ESM_INTEGRATION Jul 7, 2026
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.

-sWASM_ESM_INTEGRATION incompatibility with metadce

4 participants