From 5c97e60b0005e4ca06e7fac313d2df4a53a8be2d Mon Sep 17 00:00:00 2001 From: Guy Bedford Date: Mon, 29 Jun 2026 18:57:25 -0700 Subject: [PATCH 1/8] Teach metadce about the WASM_ESM_INTEGRATION module boundary 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. --- ChangeLog.md | 8 +- .../applyDCEGraphRemovals-esm-output.js | 11 ++ .../js_optimizer/applyDCEGraphRemovals-esm.js | 30 +++++ ...lyImportAndExportNameChanges-esm-output.js | 9 ++ .../applyImportAndExportNameChanges-esm.js | 26 +++++ test/js_optimizer/emitDCEGraph-esm-output.js | 78 +++++++++++++ test/js_optimizer/emitDCEGraph-esm.js | 43 +++++++ test/test_other.py | 26 +++++ tools/acorn-optimizer.mjs | 109 ++++++++++++++++++ tools/building.py | 6 +- tools/link.py | 5 +- 11 files changed, 347 insertions(+), 4 deletions(-) create mode 100644 test/js_optimizer/applyDCEGraphRemovals-esm-output.js create mode 100644 test/js_optimizer/applyDCEGraphRemovals-esm.js create mode 100644 test/js_optimizer/applyImportAndExportNameChanges-esm-output.js create mode 100644 test/js_optimizer/applyImportAndExportNameChanges-esm.js create mode 100644 test/js_optimizer/emitDCEGraph-esm-output.js create mode 100644 test/js_optimizer/emitDCEGraph-esm.js diff --git a/ChangeLog.md b/ChangeLog.md index 99d84f7ffb075..58d6f53c1ddc0 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -73,8 +73,12 @@ See docs/process.md for more on how version tagging works. - INITIAL_MEMORY - wasmMemory - wasmBinary - Anybody using these will see a clear error in their debug builds signaling - that they now need to be explicitly added to `-sINCOMING_MODULE_JS_API`. + Anybody using these will see a clear error in their debug builds signaling + that they now need to be explicitly added to `-sINCOMING_MODULE_JS_API`. +- Fixed `-sWASM_ESM_INTEGRATION` builds at `-O2` and above, which previously + failed in the JS optimizer's metadce (dead code elimination) pass. metadce now + understands the native ES import/export wasm boundary that this mode emits. + (#27217) 6.0.1 - 06/22/26 ---------------- diff --git a/test/js_optimizer/applyDCEGraphRemovals-esm-output.js b/test/js_optimizer/applyDCEGraphRemovals-esm-output.js new file mode 100644 index 0000000000000..e0b0a6d95fe37 --- /dev/null +++ b/test/js_optimizer/applyDCEGraphRemovals-esm-output.js @@ -0,0 +1,11 @@ +function fd_write_impl() {} + +function fd_close_impl() {} + +function unused_import_impl() {} + +import { memory, main as _main, used_export as _used_export, memory as wasmMemory } from "./a.out.wasm"; + +export { fd_write_impl as fd_write, fd_close_impl as fd_close }; + +export { _main }; diff --git a/test/js_optimizer/applyDCEGraphRemovals-esm.js b/test/js_optimizer/applyDCEGraphRemovals-esm.js new file mode 100644 index 0000000000000..c56e0aa1785ba --- /dev/null +++ b/test/js_optimizer/applyDCEGraphRemovals-esm.js @@ -0,0 +1,30 @@ +// WASM_ESM_INTEGRATION: unused wasm imports are dropped from the `export {..}` +// that sends JS functions to the wasm, and unused wasm exports (including +// internal ones like the indirect function table) from the `import {..}` that +// receives them, keeping the two ES module interfaces in sync. + +function fd_write_impl() { +} +function fd_close_impl() { +} +function unused_import_impl() { +} + +import { + memory, + __indirect_function_table, + main as _main, + used_export as _used_export, + unused_export as _unused_export, + memory as wasmMemory, +} from './a.out.wasm'; + +export { + fd_write_impl as fd_write, + fd_close_impl as fd_close, + unused_import_impl as unused_import, +}; + +export { _main }; + +// EXTRA_INFO: { "unusedImports": ["unused_import"], "unusedExports": ["unused_export", "__indirect_function_table"] } diff --git a/test/js_optimizer/applyImportAndExportNameChanges-esm-output.js b/test/js_optimizer/applyImportAndExportNameChanges-esm-output.js new file mode 100644 index 0000000000000..a67e0da65e890 --- /dev/null +++ b/test/js_optimizer/applyImportAndExportNameChanges-esm-output.js @@ -0,0 +1,9 @@ +function fd_write_impl() {} + +function fd_close_impl() {} + +import { memory, b as _main, c as _malloc, memory as wasmMemory } from "./a.out.wasm"; + +export { fd_write_impl as d, fd_close_impl as e }; + +export { _main }; diff --git a/test/js_optimizer/applyImportAndExportNameChanges-esm.js b/test/js_optimizer/applyImportAndExportNameChanges-esm.js new file mode 100644 index 0000000000000..1dcb67f0ac743 --- /dev/null +++ b/test/js_optimizer/applyImportAndExportNameChanges-esm.js @@ -0,0 +1,26 @@ +// WASM_ESM_INTEGRATION: minified wasm import/export names are applied to the +// native ES import/export specifiers. Only the wasm-facing name of each +// specifier is renamed; the JS-local binding name is left intact (including the +// unaliased `memory`, whose local side must survive). + +function fd_write_impl() { +} +function fd_close_impl() { +} + +import { + memory, + main as _main, + malloc as _malloc, + memory as wasmMemory, +} from './a.out.wasm'; + +export { + fd_write_impl as fd_write, + fd_close_impl as fd_close, +}; + +// Re-export of a wasm export: the local name (_main) is never in the mapping. +export { _main }; + +// EXTRA_INFO: { "mapping": { "main": "b", "malloc": "c", "fd_write": "d", "fd_close": "e" } } diff --git a/test/js_optimizer/emitDCEGraph-esm-output.js b/test/js_optimizer/emitDCEGraph-esm-output.js new file mode 100644 index 0000000000000..b32c1783b307d --- /dev/null +++ b/test/js_optimizer/emitDCEGraph-esm-output.js @@ -0,0 +1,78 @@ +[ + { + "name": "emcc$defun$fd_close_impl", + "reaches": [ + "emcc$defun$helper" + ] + }, + { + "name": "emcc$defun$fd_write_impl", + "reaches": [] + }, + { + "name": "emcc$defun$helper", + "reaches": [] + }, + { + "name": "emcc$defun$unused_import_impl", + "reaches": [] + }, + { + "name": "emcc$export$__indirect_function_table", + "export": "__indirect_function_table", + "reaches": [] + }, + { + "name": "emcc$export$_main", + "export": "main", + "reaches": [], + "root": true + }, + { + "name": "emcc$export$_unused_export", + "export": "unused_export", + "reaches": [] + }, + { + "name": "emcc$export$_used_export", + "export": "used_export", + "reaches": [], + "root": true + }, + { + "name": "emcc$export$memory", + "export": "memory", + "reaches": [], + "root": true + }, + { + "name": "emcc$import$fd_close_impl", + "import": [ + "env", + "fd_close" + ], + "reaches": [ + "emcc$defun$fd_close_impl" + ] + }, + { + "name": "emcc$import$fd_write_impl", + "import": [ + "env", + "fd_write" + ], + "reaches": [ + "emcc$defun$fd_write_impl" + ] + }, + { + "name": "emcc$import$unused_import_impl", + "import": [ + "env", + "unused_import" + ], + "reaches": [ + "emcc$defun$unused_import_impl" + ] + } +] diff --git a/test/js_optimizer/emitDCEGraph-esm.js b/test/js_optimizer/emitDCEGraph-esm.js new file mode 100644 index 0000000000000..e7261ce902d6c --- /dev/null +++ b/test/js_optimizer/emitDCEGraph-esm.js @@ -0,0 +1,43 @@ +// WASM_ESM_INTEGRATION: the wasm<->JS boundary is expressed with native ES +// import/export syntax rather than the `wasmImports` object and +// `wasmExports['x']` member uses, and emitDCEGraph must build the same graph +// from it. + +// JS functions implementing wasm imports. +function fd_write_impl() { +} +function unused_import_impl() { +} + +// A JS function only reachable from a wasm import edge. +function helper() { +} +function fd_close_impl() { + helper(); +} + +// wasm exports received as ES imports. `memory` is imported twice (plain and +// aliased) and must map to a single export node. +import { + memory, + __indirect_function_table, + main as _main, + used_export as _used_export, + unused_export as _unused_export, + memory as wasmMemory, +} from './a.out.wasm'; + +// JS functions exported to the wasm module (the wasm imports). +export { + fd_write_impl as fd_write, + fd_close_impl as fd_close, + unused_import_impl as unused_import, +}; + +// Re-export of a wasm export to the JS entry point: a top-level use that should +// root the underlying `main` export, not be treated as a wasm import. +export { _main }; + +// Top-level uses: root the memory export (via the alias) and one wasm export. +wasmMemory.buffer; +_used_export(); diff --git a/test/test_other.py b/test/test_other.py index ba2679a753110..9e5dc7b4d0a46 100644 --- a/test/test_other.py +++ b/test/test_other.py @@ -3058,10 +3058,13 @@ def test_extern_prepost(self): 'emitDCEGraph-sig': (['emitDCEGraph', '--no-print'],), 'emitDCEGraph-prefixing': (['emitDCEGraph', '--no-print'],), 'emitDCEGraph-scopes': (['emitDCEGraph', '--no-print'],), + 'emitDCEGraph-esm': (['emitDCEGraph', '--no-print', '--export-es6'],), 'minimal-runtime-applyDCEGraphRemovals': (['applyDCEGraphRemovals'],), 'applyDCEGraphRemovals': (['applyDCEGraphRemovals'],), + 'applyDCEGraphRemovals-esm': (['applyDCEGraphRemovals', '--export-es6'],), 'applyImportAndExportNameChanges': (['applyImportAndExportNameChanges'],), 'applyImportAndExportNameChanges2': (['applyImportAndExportNameChanges'],), + 'applyImportAndExportNameChanges-esm': (['applyImportAndExportNameChanges', '--export-es6'],), 'minimal-runtime-emitDCEGraph': (['emitDCEGraph', '--no-print'],), 'minimal-runtime-2-emitDCEGraph': (['emitDCEGraph', '--no-print'],), 'standalone-emitDCEGraph': (['emitDCEGraph', '--no-print'],), @@ -12313,6 +12316,29 @@ def test_metadce_wasm2js_i64(self): }''') self.do_runf('src.c', cflags=['-O3', '-sWASM=0']) + def test_metadce_esm_integration(self): + # Regression test for https://github.com/emscripten-core/emscripten/issues/27217. + # Under WASM_ESM_INTEGRATION the wasm<->JS boundary is expressed with native + # ES import/export syntax rather than the `wasmImports` object and + # `wasmExports['x']` member uses. metadce (which runs at -O2 and above) must + # understand that form rather than asserting that it cannot find the + # `wasmImports` assignment. + self.run_process([EMCC, test_file('hello_world.c'), '-O3', + '-sWASM_ESM_INTEGRATION', '-sEXPORT_ES6', '-Wno-experimental', + '-o', 'hello.mjs']) + support = read_file('hello.support.mjs') + # An unused wasm export (here the indirect function table) must be removed + # from both the wasm and the ES import that receives it, so the two module + # interfaces stay in sync. + self.assertNotContained('__indirect_function_table', support) + # The import module name is rewritten to the wasm module and is not minified + # (every import resolves through it), and the re-export of the user `main` + # export is preserved. + self.assertContained('from"./hello.wasm"', support) + self.assertContained('export{_main}', support) + if self.try_require_node_version(25, 0, 0): + self.assertContained('Hello, world!', self.run_js('hello.mjs')) + @crossplatform def test_deterministic(self): # test some things that may not be nondeterministic diff --git a/tools/acorn-optimizer.mjs b/tools/acorn-optimizer.mjs index 0f00c63106a0e..8fd7ec4ff84bb 100755 --- a/tools/acorn-optimizer.mjs +++ b/tools/acorn-optimizer.mjs @@ -438,6 +438,25 @@ function getWasmImportsValue(node) { } } +// Under WASM_ESM_INTEGRATION the wasm exports are received as a native ES +// import from the wasm module itself: +// import { malloc as _malloc, memory } from './a.out.wasm'; +function isWasmExportsImport(node) { + return ( + node.type === 'ImportDeclaration' && + isLiteralString(node.source) && + node.source.value.endsWith('.wasm') + ); +} + +// A sourceless `export { a as b, .. };` statement (not `export ` and not +// a re-export `export {..} from '..'`). +function isExportSpecifierList(node) { + return ( + node.type === 'ExportNamedDeclaration' && !node.declaration && !node.source + ); +} + function isExportUse(node) { // Match usages of symbols on the `wasmExports` object. e.g: // wasmExports['X'] @@ -494,6 +513,29 @@ function applyImportAndExportNameChanges(ast) { if (mapping[name]) { setLiteralValue(prop, mapping[name]); } + } else if (isWasmExportsImport(node)) { + // WASM_ESM_INTEGRATION: rename the wasm-facing name of each received + // export, e.g. `import { malloc as _malloc }` -> `import { a as _malloc }`. + // Replace the imported slot (rather than mutate it in place) since for an + // unaliased specifier acorn shares one node for both sides. + node.specifiers.forEach((spec) => { + const newName = mapping[spec.imported.name]; + if (newName) { + spec.imported = makeIdentifier(newName); + } + }); + } else if (isExportSpecifierList(node)) { + // WASM_ESM_INTEGRATION: rename the wasm-facing name of each JS function + // sent to wasm, e.g. `export { _fd_write as fd_write }` -> + // `export { _fd_write as a }`. Re-exports of wasm exports (`export { + // _main }`) carry a JS-local name that is never in the mapping, so they + // are left untouched. + node.specifiers.forEach((spec) => { + const newName = mapping[spec.exported.name]; + if (newName) { + spec.exported = makeIdentifier(newName); + } + }); } }); } @@ -607,6 +649,10 @@ function emitDCEGraph(ast) { const exportNameToGraphName = {}; // identical to wasmExports['..'] nameToGraphName let foundWasmImportsAssign = false; let foundMinimalRuntimeExports = false; + // Under WASM_ESM_INTEGRATION, JS names bound to wasm exports via an ES import + // from the wasm module. Lets us tell a re-export of a wasm export apart from a + // JS function that is itself exported to wasm (both are `export {..}`). + const wasmExportLocals = new Set(); function saveAsmExport(name, asmName) { // the asmName is what the wasm provides directly; the outside JS @@ -649,6 +695,49 @@ function emitDCEGraph(ast) { }); foundWasmImportsAssign = true; emptyOut(node); // ignore this in the second pass; this does not root + } else if (isWasmExportsImport(node)) { + // WASM_ESM_INTEGRATION: wasm exports received as + // import { malloc as _malloc, memory } from './a.out.wasm'; + // Each binding is a wasm export, exactly like `var _x = wasmExports['x']`. + node.specifiers.forEach((spec) => { + const jsName = spec.local.name; // JS-side name + const asmName = spec.imported.name; // wasm-provided name + if (exportNameToGraphName.hasOwnProperty(asmName)) { + // Another local already binds this wasm export (e.g. both `memory` + // and `memory as wasmMemory`): point this local at the same node so + // a use of either roots the one underlying export. + nameToGraphName[jsName] = exportNameToGraphName[asmName]; + } else { + saveAsmExport(jsName, asmName); + } + wasmExportLocals.add(jsName); + }); + // This ES form stands in for the `wasmImports`/`wasmExports` idioms the + // non-ESM build emits, so it satisfies the sanity check below. + foundWasmImportsAssign = true; + // Drop from the second pass: the local bindings must not be seen as + // top-level uses (that would root every export and defeat DCE). + emptyOut(node); + } else if (isExportSpecifierList(node)) { + // WASM_ESM_INTEGRATION emits two sourceless `export {..}` forms: + // (a) JS functions sent to wasm: export { _fd_write as fd_write }; + // (b) re-exports of wasm exports: export { _main }; + // (a) are the wasm imports; (b) are ordinary top-level uses that should + // root the underlying export (handled in the second pass), so only (a) + // is recorded and removed here. + let isImportEdge = false; + node.specifiers.forEach((spec) => { + if (wasmExportLocals.has(spec.local.name)) { + return; // (b) re-export of a wasm export + } + // (a) `export { jsName as nativeName }` - jsName implements the import. + imports.push([spec.local.name, spec.exported.name]); + isImportEdge = true; + }); + if (isImportEdge) { + foundWasmImportsAssign = true; + emptyOut(node); // does not root; second pass ignores it + } } else if (node.type === 'AssignmentExpression') { const target = node.left; // Ignore assignment to the wasmExports object (as happens in @@ -894,6 +983,26 @@ function applyDCEGraphRemovals(ast) { } return true; }); + } else if (isWasmExportsImport(node)) { + // WASM_ESM_INTEGRATION: drop unused wasm exports from + // import { malloc as _malloc, .. } from './a.out.wasm'; + node.specifiers = node.specifiers.filter((spec) => { + if (unusedExports.has(spec.imported.name)) { + foundUnusedExports.add(spec.imported.name); + return false; + } + return true; + }); + } else if (isExportSpecifierList(node)) { + // WASM_ESM_INTEGRATION: drop unused wasm imports from + // export { _fd_write as fd_write, .. }; + node.specifiers = node.specifiers.filter((spec) => { + if (unusedImports.has(spec.exported.name)) { + foundUnusedImports.add(spec.exported.name); + return false; + } + return true; + }); } else if (node.type === 'ExpressionStatement') { let expr = node.expression; // Inside the assignWasmExports function we have diff --git a/tools/building.py b/tools/building.py index ea95f2e7d66ba..cc7fde6b5d973 100644 --- a/tools/building.py +++ b/tools/building.py @@ -883,7 +883,11 @@ def metadce(js_file, wasm_file, debug_info, last): unused_imports.append(native_name) elif name.startswith('emcc$export$') and settings.DECLARE_ASM_MODULE_EXPORTS: native_name = export_name_map[name] - if shared.is_user_export(native_name): + # Under WASM_ESM_INTEGRATION the JS receives every wasm export as an ES + # 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: unused_exports.append(native_name) if not unused_exports and not unused_imports: # nothing found to be unused, so we have nothing to remove diff --git a/tools/link.py b/tools/link.py index db52cd9ced9ba..43059443a6c94 100644 --- a/tools/link.py +++ b/tools/link.py @@ -1637,7 +1637,10 @@ def limit_incoming_module_api(): not settings.MAIN_MODULE and \ settings.MINIFY_WASM_EXPORT_NAMES: settings.MINIFY_WASM_IMPORTS_AND_EXPORTS = 1 - settings.MINIFY_WASM_IMPORTED_MODULES = 1 + # Under WASM_ESM_INTEGRATION every wasm import is rewritten to come from the + # single support module (see create_esm_wrapper), so minifying the import + # module names buys nothing and would break that rewrite. + settings.MINIFY_WASM_IMPORTED_MODULES = not settings.WASM_ESM_INTEGRATION if settings.WASM_BIGINT: settings.LEGALIZE_JS_FFI = 0 From 235fd89ce99dac54591bbf12e8db5d9f4626023b Mon Sep 17 00:00:00 2001 From: Guy Bedford Date: Wed, 1 Jul 2026 12:38:25 -0700 Subject: [PATCH 2/8] pr feedback --- ChangeLog.md | 4 +- .../test_codesize_hello_esm_integration.json | 29 ++++++++ ...test_codesize_minimal_esm_integration.json | 22 ++++++ .../applyDCEGraphRemovals-esm-output.js | 4 + .../js_optimizer/applyDCEGraphRemovals-esm.js | 4 + ...lyImportAndExportNameChanges-esm-output.js | 4 + .../applyImportAndExportNameChanges-esm.js | 4 + test/js_optimizer/emitDCEGraph-esm.js | 6 ++ test/test_codesize.py | 73 ++++++++++++++----- test/test_other.py | 23 ------ tools/acorn-optimizer.mjs | 26 +++---- tools/emscripten.py | 10 +-- 12 files changed, 148 insertions(+), 61 deletions(-) create mode 100644 test/codesize/test_codesize_hello_esm_integration.json create mode 100644 test/codesize/test_codesize_minimal_esm_integration.json diff --git a/ChangeLog.md b/ChangeLog.md index 58d6f53c1ddc0..bf0d8fe5a0f94 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -73,8 +73,8 @@ See docs/process.md for more on how version tagging works. - INITIAL_MEMORY - wasmMemory - wasmBinary - Anybody using these will see a clear error in their debug builds signaling - that they now need to be explicitly added to `-sINCOMING_MODULE_JS_API`. + Anybody using these will see a clear error in their debug builds signaling + that they now need to be explicitly added to `-sINCOMING_MODULE_JS_API`. - Fixed `-sWASM_ESM_INTEGRATION` builds at `-O2` and above, which previously failed in the JS optimizer's metadce (dead code elimination) pass. metadce now understands the native ES import/export wasm boundary that this mode emits. diff --git a/test/codesize/test_codesize_hello_esm_integration.json b/test/codesize/test_codesize_hello_esm_integration.json new file mode 100644 index 0000000000000..227dfb0bb9776 --- /dev/null +++ b/test/codesize/test_codesize_hello_esm_integration.json @@ -0,0 +1,29 @@ +{ + "a.out.mjs": 671, + "a.out.mjs.gz": 401, + "a.out.support.mjs": 6397, + "a.out.support.mjs.gz": 2473, + "a.out.nodebug.wasm": 1688, + "a.out.nodebug.wasm.gz": 973, + "total": 8756, + "total_gz": 3847, + "sent": [ + "a (fd_write)" + ], + "imports": [ + "a (fd_write)" + ], + "exports": [ + "b (memory)", + "c (__wasm_call_ctors)", + "d (main)" + ], + "funcs": [ + "$__emscripten_stdout_close", + "$__emscripten_stdout_seek", + "$__stdio_write", + "$__towrite", + "$__wasm_call_ctors", + "$main" + ] +} diff --git a/test/codesize/test_codesize_minimal_esm_integration.json b/test/codesize/test_codesize_minimal_esm_integration.json new file mode 100644 index 0000000000000..f6d6b9716712d --- /dev/null +++ b/test/codesize/test_codesize_minimal_esm_integration.json @@ -0,0 +1,22 @@ +{ + "a.out.mjs": 683, + "a.out.mjs.gz": 410, + "a.out.support.mjs": 3343, + "a.out.support.mjs.gz": 1350, + "a.out.nodebug.wasm": 75, + "a.out.nodebug.wasm.gz": 87, + "total": 4101, + "total_gz": 1847, + "sent": [], + "imports": [], + "exports": [ + "a (memory)", + "b (__wasm_call_ctors)", + "c (add)", + "d (global_val)" + ], + "funcs": [ + "$__wasm_call_ctors", + "$add" + ] +} diff --git a/test/js_optimizer/applyDCEGraphRemovals-esm-output.js b/test/js_optimizer/applyDCEGraphRemovals-esm-output.js index e0b0a6d95fe37..e094a183cd44d 100644 --- a/test/js_optimizer/applyDCEGraphRemovals-esm-output.js +++ b/test/js_optimizer/applyDCEGraphRemovals-esm-output.js @@ -9,3 +9,7 @@ import { memory, main as _main, used_export as _used_export, memory as wasmMemor export { fd_write_impl as fd_write, fd_close_impl as fd_close }; export { _main }; + +var HEAP32; + +export { HEAP32 }; diff --git a/test/js_optimizer/applyDCEGraphRemovals-esm.js b/test/js_optimizer/applyDCEGraphRemovals-esm.js index c56e0aa1785ba..be702e6a9320a 100644 --- a/test/js_optimizer/applyDCEGraphRemovals-esm.js +++ b/test/js_optimizer/applyDCEGraphRemovals-esm.js @@ -27,4 +27,8 @@ export { export { _main }; +// MODULARIZE=instance runtime exports (bare form) must be left untouched. +var HEAP32; +export { HEAP32 }; + // EXTRA_INFO: { "unusedImports": ["unused_import"], "unusedExports": ["unused_export", "__indirect_function_table"] } diff --git a/test/js_optimizer/applyImportAndExportNameChanges-esm-output.js b/test/js_optimizer/applyImportAndExportNameChanges-esm-output.js index a67e0da65e890..4bd68e61e23a8 100644 --- a/test/js_optimizer/applyImportAndExportNameChanges-esm-output.js +++ b/test/js_optimizer/applyImportAndExportNameChanges-esm-output.js @@ -7,3 +7,7 @@ import { memory, b as _main, c as _malloc, memory as wasmMemory } from "./a.out. export { fd_write_impl as d, fd_close_impl as e }; export { _main }; + +var HEAP32; + +export { HEAP32 }; diff --git a/test/js_optimizer/applyImportAndExportNameChanges-esm.js b/test/js_optimizer/applyImportAndExportNameChanges-esm.js index 1dcb67f0ac743..78fbc90bc73ea 100644 --- a/test/js_optimizer/applyImportAndExportNameChanges-esm.js +++ b/test/js_optimizer/applyImportAndExportNameChanges-esm.js @@ -23,4 +23,8 @@ export { // Re-export of a wasm export: the local name (_main) is never in the mapping. export { _main }; +// MODULARIZE=instance runtime export (bare form): never in the mapping. +var HEAP32; +export { HEAP32 }; + // EXTRA_INFO: { "mapping": { "main": "b", "malloc": "c", "fd_write": "d", "fd_close": "e" } } diff --git a/test/js_optimizer/emitDCEGraph-esm.js b/test/js_optimizer/emitDCEGraph-esm.js index e7261ce902d6c..8be0d43a379f9 100644 --- a/test/js_optimizer/emitDCEGraph-esm.js +++ b/test/js_optimizer/emitDCEGraph-esm.js @@ -38,6 +38,12 @@ export { // root the underlying `main` export, not be treated as a wasm import. export { _main }; +// MODULARIZE=instance runtime exports. These are plain JS bindings, not wasm +// import edges, and must be left untouched (bare `export {..}` form). +var HEAP32; +var baz = () => {}; +export { HEAP32, baz }; + // Top-level uses: root the memory export (via the alias) and one wasm export. wasmMemory.buffer; _used_export(); diff --git a/test/test_codesize.py b/test/test_codesize.py index de0cea10d6ae9..133739229f827 100644 --- a/test/test_codesize.py +++ b/test/test_codesize.py @@ -226,39 +226,68 @@ def run_codesize_test(self, filename, cflags, check_funcs=True, check_full_js=Fa filename = test_file('codesize', filename) expected_basename = test_file('codesize', self.id().split('.')[-1]) - # Run once without closure and parse output to find wasmImports - build_cmd = [compiler_for(filename), filename, '--output-eol=linux', '--emit-minification-map=minify.map'] + cflags + self.get_cflags() + # Under WASM_ESM_INTEGRATION the wasm<->JS boundary uses native ES + # import/export syntax and the JS glue lives in a separate support module. + esm = self.get_setting('WASM_ESM_INTEGRATION') or any(a.startswith('-sWASM_ESM_INTEGRATION') for a in cflags) + outfile = 'a.out.mjs' if esm else 'a.out.js' + js_file = 'a.out.support.mjs' if esm else outfile + + # Run once without closure and parse output to find the JS->wasm imports. + build_cmd = [compiler_for(filename), filename, '-o', outfile, '--output-eol=linux', '--emit-minification-map=minify.map'] + cflags + self.get_cflags() self.run_process(build_cmd + ['-g2']) # find the imports we send from JS # TODO(sbc): Find a way to do that that doesn't depend on internal details of # the generated code. - js = read_file('a.out.js') + js = read_file(js_file) if check_full_js: # Ignore absolute filenames in the generated code (they are likely /tmp files) js = re.sub(r'^// include: .*[/\\].*$', '// include: ', js, flags=re.MULTILINE) js = re.sub(r'^// end include: .*[/\\].*$', '// end include: ', js, flags=re.MULTILINE) self.assertFileContents(expected_basename + '.expected.js', js) - start = js.find('wasmImports = ') - self.assertNotEqual(start, -1) - end = js.find('}', start) - self.assertNotEqual(end, -1) - start = js.find('{', start) - self.assertNotEqual(start, -1) - relevant = js[start + 2:end - 1] - relevant = relevant.replace(' ', '').replace('"', '').replace("'", '') - relevant = relevant.replace('/**@export*/', '') - relevant = relevant.split(',') - sent = [x.split(':')[0].strip() for x in relevant] - sent = [x for x in sent if x] + if esm: + # The functions provided to wasm are emitted as `export { js as wasm }` + # specifiers under the "Export JS functions to the wasm module" comment. + # An empty set emits no export statement at all. + marker = js.find('Export JS functions to the wasm module') + self.assertNotEqual(marker, -1) + start = js.find('export', marker) + reexport = js.find('Re-export imported wasm functions', marker) + sent = [] + if start != -1 and (reexport == -1 or start < reexport): + end = js.find('}', start) + self.assertNotEqual(end, -1) + start = js.find('{', start) + for specifier in js[start + 1:end].split(','): + specifier = specifier.strip() + if specifier: + # keep the wasm-facing (minified) name after `as` + sent.append(specifier.rsplit(' as ', 1)[-1].strip()) + else: + start = js.find('wasmImports = ') + self.assertNotEqual(start, -1) + end = js.find('}', start) + self.assertNotEqual(end, -1) + start = js.find('{', start) + self.assertNotEqual(start, -1) + relevant = js[start + 2:end - 1] + relevant = relevant.replace(' ', '').replace('"', '').replace("'", '') + relevant = relevant.replace('/**@export*/', '') + relevant = relevant.split(',') + sent = [x.split(':')[0].strip() for x in relevant] + sent = [x for x in sent if x] # Deminify the sent list, if minification occurred if os.path.exists('minify.map'): sent = deminify_syms(sent, 'minify.map') os.remove('minify.map') sent.sort() - self.run_process(build_cmd + ['--profiling-funcs', '--closure=1']) + # closure is not yet compatible with the WASM_ESM_INTEGRATION module glue. + closure_args = [] if esm else ['--closure=1'] + self.run_process(build_cmd + ['--profiling-funcs'] + closure_args) - outputs = ['a.out.js'] + outputs = [outfile] + if esm: + outputs.append(js_file) info = {'sent': sent} if '-sSINGLE_FILE' not in cflags: @@ -270,7 +299,9 @@ def run_codesize_test(self, filename, cflags, check_funcs=True, check_full_js=Fa # Deminify the imports/export lists, if minification occurred if os.path.exists('minify.map'): exports = deminify_syms(exports, 'minify.map') - imports = [i.split('.', 1)[1] for i in imports] + # Under ESM integration the import module is the support module path + # (which itself contains dots), so take the field after the last dot. + imports = [i.rsplit('.', 1)[1] if esm else i.split('.', 1)[1] for i in imports] imports = deminify_syms(imports, 'minify.map') imports.sort() @@ -311,6 +342,9 @@ def strip_numeric_suffixes(funcname): # WasmFS should not be fully linked into a minimal program. 'wasmfs': (['-Oz', '-sWASMFS'],), 'esm': (['-Oz', '-sEXPORT_ES6'],), + # a program with no JS->wasm imports emits no `export {}` statement in the + # support module, exercising that metadce edge of the ES module boundary. + 'esm_integration': (['-Oz', '-sWASM_ESM_INTEGRATION', '-Wno-experimental'],), }) def test_codesize_minimal(self, args, check_full_js=False): self.set_setting('STRICT') @@ -366,6 +400,9 @@ def test_codesize_cxx(self, args): # WasmFS should not be fully linked into a hello world program. 'wasmfs': (['-O3', '-sWASMFS'],), 'single_file': (['-O3', '-sSINGLE_FILE'],), + # metadce must understand the native ES import/export module boundary and + # prune unused imports/exports from it. See #27217. + 'esm_integration': (['-O3', '-sWASM_ESM_INTEGRATION', '-Wno-experimental'],), }) def test_codesize_hello(self, args, kwargs={}): # noqa self.run_codesize_test('hello_world.c', args, **kwargs) diff --git a/test/test_other.py b/test/test_other.py index 9e5dc7b4d0a46..481a750ca7b60 100644 --- a/test/test_other.py +++ b/test/test_other.py @@ -12316,29 +12316,6 @@ def test_metadce_wasm2js_i64(self): }''') self.do_runf('src.c', cflags=['-O3', '-sWASM=0']) - def test_metadce_esm_integration(self): - # Regression test for https://github.com/emscripten-core/emscripten/issues/27217. - # Under WASM_ESM_INTEGRATION the wasm<->JS boundary is expressed with native - # ES import/export syntax rather than the `wasmImports` object and - # `wasmExports['x']` member uses. metadce (which runs at -O2 and above) must - # understand that form rather than asserting that it cannot find the - # `wasmImports` assignment. - self.run_process([EMCC, test_file('hello_world.c'), '-O3', - '-sWASM_ESM_INTEGRATION', '-sEXPORT_ES6', '-Wno-experimental', - '-o', 'hello.mjs']) - support = read_file('hello.support.mjs') - # An unused wasm export (here the indirect function table) must be removed - # from both the wasm and the ES import that receives it, so the two module - # interfaces stay in sync. - self.assertNotContained('__indirect_function_table', support) - # The import module name is rewritten to the wasm module and is not minified - # (every import resolves through it), and the re-export of the user `main` - # export is preserved. - self.assertContained('from"./hello.wasm"', support) - self.assertContained('export{_main}', support) - if self.try_require_node_version(25, 0, 0): - self.assertContained('Hello, world!', self.run_js('hello.mjs')) - @crossplatform def test_deterministic(self): # test some things that may not be nondeterministic diff --git a/tools/acorn-optimizer.mjs b/tools/acorn-optimizer.mjs index 8fd7ec4ff84bb..1e9f936119641 100755 --- a/tools/acorn-optimizer.mjs +++ b/tools/acorn-optimizer.mjs @@ -719,22 +719,22 @@ function emitDCEGraph(ast) { // top-level uses (that would root every export and defeat DCE). emptyOut(node); } else if (isExportSpecifierList(node)) { - // WASM_ESM_INTEGRATION emits two sourceless `export {..}` forms: + // Sourceless `export {..}` statements come in three forms: // (a) JS functions sent to wasm: export { _fd_write as fd_write }; // (b) re-exports of wasm exports: export { _main }; - // (a) are the wasm imports; (b) are ordinary top-level uses that should - // root the underlying export (handled in the second pass), so only (a) - // is recorded and removed here. - let isImportEdge = false; - node.specifiers.forEach((spec) => { - if (wasmExportLocals.has(spec.local.name)) { - return; // (b) re-export of a wasm export - } + // (c) runtime/library exports: export { HEAP32, baz }; + // Only (a) are wasm import edges (recorded and removed here). (b) and + // (c) are ordinary top-level uses left in place to root in the second + // pass. (a) is the only form written with an alias (`x as y`), which + // acorn represents with distinct local/exported nodes; bare specifiers + // reuse a single node for both sides. + const importEdges = node.specifiers.filter( + (spec) => + spec.local !== spec.exported && !wasmExportLocals.has(spec.local.name), + ); + if (importEdges.length) { // (a) `export { jsName as nativeName }` - jsName implements the import. - imports.push([spec.local.name, spec.exported.name]); - isImportEdge = true; - }); - if (isImportEdge) { + importEdges.forEach((spec) => imports.push([spec.local.name, spec.exported.name])); foundWasmImportsAssign = true; emptyOut(node); // does not root; second pass ignores it } diff --git a/tools/emscripten.py b/tools/emscripten.py index fd3f53f93db33..8caf7aa54bffd 100644 --- a/tools/emscripten.py +++ b/tools/emscripten.py @@ -891,11 +891,10 @@ def create_sending(metadata, library_symbols): sorted_items = sorted(send_items_map.items()) if settings.WASM_ESM_INTEGRATION: - elems = [] - for k, v in sorted_items: - elems.append(f'{v} as {k}') - elems = ',\n '.join(elems) exports = '// Export JS functions to the wasm module with demangled names.\n' + if not sorted_items: + return exports + elems = ',\n '.join(f'{v} as {k}' for k, v in sorted_items) exports += f"export {{\n {elems}\n}};" return exports @@ -928,7 +927,8 @@ def create_reexports(metadata): wasm_exports.append(exp) elif demangled == 'main' and '__main_argc_argv' in settings.WASM_EXPORTS: wasm_exports.append('_main') - exports += f"export {{ {', '.join(wasm_exports)} }};" + if wasm_exports: + exports += f"export {{ {', '.join(wasm_exports)} }};" return exports From 04a5e35e470ccc07778f2eb3263180b994161df4 Mon Sep 17 00:00:00 2001 From: Guy Bedford Date: Tue, 7 Jul 2026 11:34:25 -0700 Subject: [PATCH 3/8] Address review feedback --- test/test_codesize.py | 2 +- tools/emscripten.py | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/test/test_codesize.py b/test/test_codesize.py index 133739229f827..b720b1becf00f 100644 --- a/test/test_codesize.py +++ b/test/test_codesize.py @@ -228,7 +228,7 @@ def run_codesize_test(self, filename, cflags, check_funcs=True, check_full_js=Fa # Under WASM_ESM_INTEGRATION the wasm<->JS boundary uses native ES # import/export syntax and the JS glue lives in a separate support module. - esm = self.get_setting('WASM_ESM_INTEGRATION') or any(a.startswith('-sWASM_ESM_INTEGRATION') for a in cflags) + esm = any(a.startswith('-sWASM_ESM_INTEGRATION') for a in cflags) outfile = 'a.out.mjs' if esm else 'a.out.js' js_file = 'a.out.support.mjs' if esm else outfile diff --git a/tools/emscripten.py b/tools/emscripten.py index 8caf7aa54bffd..9e561e7e90da6 100644 --- a/tools/emscripten.py +++ b/tools/emscripten.py @@ -893,6 +893,7 @@ def create_sending(metadata, library_symbols): if settings.WASM_ESM_INTEGRATION: exports = '// Export JS functions to the wasm module with demangled names.\n' if not sorted_items: + # With no JS->wasm imports emit no `export {}` at all rather than an empty one. return exports elems = ',\n '.join(f'{v} as {k}' for k, v in sorted_items) exports += f"export {{\n {elems}\n}};" @@ -928,6 +929,7 @@ def create_reexports(metadata): elif demangled == 'main' and '__main_argc_argv' in settings.WASM_EXPORTS: wasm_exports.append('_main') if wasm_exports: + # With nothing to re-export emit no `export {}` at all rather than an empty one. exports += f"export {{ {', '.join(wasm_exports)} }};" return exports From 0d0bb9d59f5d3c813b73d4143d9afeddb9243481 Mon Sep 17 00:00:00 2001 From: Guy Bedford Date: Tue, 7 Jul 2026 12:04:49 -0700 Subject: [PATCH 4/8] update changelog --- ChangeLog.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/ChangeLog.md b/ChangeLog.md index bf0d8fe5a0f94..910c622690b2c 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -40,6 +40,10 @@ See docs/process.md for more on how version tagging works. in the next release. (#27261) - The default value for `GROWABLE_ARRAYBUFFERS` was reverted to `0` since we found issues with Web API compatibility. (#27260) +- Fixed `-sWASM_ESM_INTEGRATION` builds at `-O2` and above, which previously + failed in the JS optimizer's metadce (dead code elimination) pass. metadce now + understands the native ES import/export wasm boundary that this mode emits. + (#27217) 6.0.2 - 07/01/26 ---------------- @@ -75,10 +79,6 @@ See docs/process.md for more on how version tagging works. - wasmBinary Anybody using these will see a clear error in their debug builds signaling that they now need to be explicitly added to `-sINCOMING_MODULE_JS_API`. -- Fixed `-sWASM_ESM_INTEGRATION` builds at `-O2` and above, which previously - failed in the JS optimizer's metadce (dead code elimination) pass. metadce now - understands the native ES import/export wasm boundary that this mode emits. - (#27217) 6.0.1 - 06/22/26 ---------------- From 099f505a04735bd2a9fcaaf41c52721814f11484 Mon Sep 17 00:00:00 2001 From: Guy Bedford Date: Tue, 7 Jul 2026 12:06:41 -0700 Subject: [PATCH 5/8] rename isWasmExportsImport to isImportFromWasm --- tools/acorn-optimizer.mjs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tools/acorn-optimizer.mjs b/tools/acorn-optimizer.mjs index 1e9f936119641..951015a98438e 100755 --- a/tools/acorn-optimizer.mjs +++ b/tools/acorn-optimizer.mjs @@ -441,7 +441,7 @@ function getWasmImportsValue(node) { // Under WASM_ESM_INTEGRATION the wasm exports are received as a native ES // import from the wasm module itself: // import { malloc as _malloc, memory } from './a.out.wasm'; -function isWasmExportsImport(node) { +function isImportFromWasm(node) { return ( node.type === 'ImportDeclaration' && isLiteralString(node.source) && @@ -513,7 +513,7 @@ function applyImportAndExportNameChanges(ast) { if (mapping[name]) { setLiteralValue(prop, mapping[name]); } - } else if (isWasmExportsImport(node)) { + } else if (isImportFromWasm(node)) { // WASM_ESM_INTEGRATION: rename the wasm-facing name of each received // export, e.g. `import { malloc as _malloc }` -> `import { a as _malloc }`. // Replace the imported slot (rather than mutate it in place) since for an @@ -695,7 +695,7 @@ function emitDCEGraph(ast) { }); foundWasmImportsAssign = true; emptyOut(node); // ignore this in the second pass; this does not root - } else if (isWasmExportsImport(node)) { + } else if (isImportFromWasm(node)) { // WASM_ESM_INTEGRATION: wasm exports received as // import { malloc as _malloc, memory } from './a.out.wasm'; // Each binding is a wasm export, exactly like `var _x = wasmExports['x']`. @@ -983,7 +983,7 @@ function applyDCEGraphRemovals(ast) { } return true; }); - } else if (isWasmExportsImport(node)) { + } else if (isImportFromWasm(node)) { // WASM_ESM_INTEGRATION: drop unused wasm exports from // import { malloc as _malloc, .. } from './a.out.wasm'; node.specifiers = node.specifiers.filter((spec) => { From b7fa7fd4ae81a292d8e4546b7128191f92ec399b Mon Sep 17 00:00:00 2001 From: Guy Bedford Date: Tue, 7 Jul 2026 14:34:40 -0700 Subject: [PATCH 6/8] rsplit fixup --- test/test_codesize.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/test_codesize.py b/test/test_codesize.py index b720b1becf00f..15ef6e106c881 100644 --- a/test/test_codesize.py +++ b/test/test_codesize.py @@ -301,7 +301,7 @@ def run_codesize_test(self, filename, cflags, check_funcs=True, check_full_js=Fa exports = deminify_syms(exports, 'minify.map') # Under ESM integration the import module is the support module path # (which itself contains dots), so take the field after the last dot. - imports = [i.rsplit('.', 1)[1] if esm else i.split('.', 1)[1] for i in imports] + imports = [i.rsplit('.', 1)[1] for i in imports] imports = deminify_syms(imports, 'minify.map') imports.sort() From 458ecd4e7ddc72fe71437ff986e47fc877bcc505 Mon Sep 17 00:00:00 2001 From: Guy Bedford Date: Wed, 8 Jul 2026 17:11:06 -0700 Subject: [PATCH 7/8] Clarify internal export comment in applyDCEGraphRemovals --- tools/building.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tools/building.py b/tools/building.py index fe48ae25b25c3..d78a565c52a07 100644 --- a/tools/building.py +++ b/tools/building.py @@ -886,10 +886,10 @@ def metadce(js_file, wasm_file, debug_info, last): # Internal/system exports (e.g. memory, __asyncify_data, dynCall_*, etc.) # do not have standard JS receiving assignments (_name = wasmExports['name']), # so including them in unused_exports would fail applyDCEGraphRemovals's assertion. - # Under WASM_ESM_INTEGRATION the JS receives every wasm export as an ES - # 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. + # However, under WASM_ESM_INTEGRATION the JS receives every wasm export + # as an ES import, so any export binaryen drops (including internal ones) + # must also be dropped from the JS import to keep the two module + # interfaces in sync. if not shared.is_internal_symbol(native_name) or settings.WASM_ESM_INTEGRATION: unused_exports.append(native_name) if not unused_exports and not unused_imports: From 7fa9e53dbd4334f2b684c2603d4387201ea2c1c7 Mon Sep 17 00:00:00 2001 From: Guy Bedford Date: Fri, 10 Jul 2026 13:15:46 -0700 Subject: [PATCH 8/8] Fix emitDCEGraph crash on source-phase wasm imports Under SOURCE_PHASE_IMPORTS the glue emits `import source wasmModule from './x.wasm'`, whose lone specifier is an ImportDefaultSpecifier with no `imported`. isImportFromWasm matched it (source ends in .wasm) and emitDCEGraph then threw reading `spec.imported.name`. Gate isImportFromWasm on `!node.phase` so only value-phase wasm imports (the wasm export bindings) are treated as export edges. --- .../emitDCEGraph-sourcePhaseImports-output.js | 38 +++++++++++++++++++ .../emitDCEGraph-sourcePhaseImports.js | 29 ++++++++++++++ test/test_other.py | 1 + tools/acorn-optimizer.mjs | 4 ++ 4 files changed, 72 insertions(+) create mode 100644 test/js_optimizer/emitDCEGraph-sourcePhaseImports-output.js create mode 100644 test/js_optimizer/emitDCEGraph-sourcePhaseImports.js diff --git a/test/js_optimizer/emitDCEGraph-sourcePhaseImports-output.js b/test/js_optimizer/emitDCEGraph-sourcePhaseImports-output.js new file mode 100644 index 0000000000000..dffdb995366ea --- /dev/null +++ b/test/js_optimizer/emitDCEGraph-sourcePhaseImports-output.js @@ -0,0 +1,38 @@ +[ + { + "name": "emcc$defun$fd_write_impl", + "reaches": [] + }, + { + "name": "emcc$export$_main", + "export": "main", + "reaches": [], + "root": true + }, + { + "name": "emcc$export$_unused_export", + "export": "unused_export", + "reaches": [] + }, + { + "name": "emcc$export$_used_export", + "export": "used_export", + "reaches": [], + "root": true + }, + { + "name": "emcc$export$memory", + "export": "memory", + "reaches": [] + }, + { + "name": "emcc$import$fd_write_impl", + "import": [ + "env", + "fd_write" + ], + "reaches": [ + "emcc$defun$fd_write_impl" + ] + } +] diff --git a/test/js_optimizer/emitDCEGraph-sourcePhaseImports.js b/test/js_optimizer/emitDCEGraph-sourcePhaseImports.js new file mode 100644 index 0000000000000..b563d380f3c32 --- /dev/null +++ b/test/js_optimizer/emitDCEGraph-sourcePhaseImports.js @@ -0,0 +1,29 @@ +// SOURCE_PHASE_IMPORTS combined with WASM_ESM_INTEGRATION: the glue emits a +// source-phase import for the wasm module alongside the value-phase import that +// binds the wasm exports. emitDCEGraph must skip the source-phase import +// (its specifier has no `imported`) and only treat the value-phase `.wasm` +// import as wasm export bindings. + +// Source-phase import of the wasm module itself. This is NOT a wasm export +// binding and must be ignored by the DCE graph builder. +import source wasmModule from './a.out.wasm'; + +function fd_write_impl() { +} + +// wasm exports received as ES imports (value-phase). +import { + memory, + main as _main, + used_export as _used_export, + unused_export as _unused_export, +} from './a.out.wasm'; + +export { + fd_write_impl as fd_write, +}; + +export { _main }; + +WebAssembly.instantiate(wasmModule); +_used_export(); diff --git a/test/test_other.py b/test/test_other.py index 7061842023b4a..d8d18228cf907 100644 --- a/test/test_other.py +++ b/test/test_other.py @@ -3059,6 +3059,7 @@ def test_extern_prepost(self): 'emitDCEGraph-prefixing': (['emitDCEGraph', '--no-print'],), 'emitDCEGraph-scopes': (['emitDCEGraph', '--no-print'],), 'emitDCEGraph-esm': (['emitDCEGraph', '--no-print', '--export-es6'],), + 'emitDCEGraph-sourcePhaseImports': (['emitDCEGraph', '--no-print', '--export-es6'],), 'minimal-runtime-applyDCEGraphRemovals': (['applyDCEGraphRemovals'],), 'applyDCEGraphRemovals': (['applyDCEGraphRemovals'],), 'applyDCEGraphRemovals-esm': (['applyDCEGraphRemovals', '--export-es6'],), diff --git a/tools/acorn-optimizer.mjs b/tools/acorn-optimizer.mjs index 951015a98438e..5662cb0d54ff0 100755 --- a/tools/acorn-optimizer.mjs +++ b/tools/acorn-optimizer.mjs @@ -444,6 +444,10 @@ function getWasmImportsValue(node) { function isImportFromWasm(node) { return ( node.type === 'ImportDeclaration' && + // Skip source/defer phase imports (e.g. `import source foo from './a.wasm'` + // under SOURCE_PHASE_IMPORTS), whose default specifier binds the module + // itself rather than wasm exports. Value-phase imports leave `phase` unset. + !node.phase && isLiteralString(node.source) && node.source.value.endsWith('.wasm') );