Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions ChangeLog.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
----------------
Expand Down
34 changes: 23 additions & 11 deletions src/jsifier.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,8 @@ import {
warningOccured,
localFile,
timer,
toValidIdentifier,
quoteExportName,
} from './utility.mjs';
import {LibraryManager, librarySymbols, nativeAliases} from './modules.mjs';

Expand Down Expand Up @@ -604,6 +606,11 @@ function(${args}) {
let isStub = false;

const mangled = mangleCSymbolName(symbol);
// The JS binding identifier for this symbol. This is the same as
// `mangled` for the common case, but differs when `mangled` is not a
// valid JS identifier (e.g. a reserved word), in which case we bind it to
// a legal name and export it under its original name via an alias.
const binding = toValidIdentifier(mangled);

if (!LibraryManager.library.hasOwnProperty(symbol)) {
const isWeakImport = WEAK_IMPORTS.has(symbol);
Expand Down Expand Up @@ -761,20 +768,20 @@ function(${args}) {
// modifyJSFunction which could have changed or removed the name.
if (contentText.match(/^\s*([^}]*)\s*=>/s)) {
// Handle arrow functions
contentText = `var ${mangled} = ` + contentText + ';';
contentText = `var ${binding} = ` + contentText + ';';
} else if (contentText.startsWith('class ')) {
// Handle class declarations (which also have typeof == 'function'.)
contentText = contentText.replace(/^class(?:\s+(?!extends\b)[^{\s]+)?/, `class ${mangled}`);
contentText = contentText.replace(/^class(?:\s+(?!extends\b)[^{\s]+)?/, `class ${binding}`);
} else {
// Handle regular (non-arrow) functions
contentText = contentText.replace(/function(?:\s+([^(]+))?\s*\(/, `function ${mangled}(`);
contentText = contentText.replace(/function(?:\s+([^(]+))?\s*\(/, `function ${binding}(`);
}
} else if (typeof snippet == 'string' && snippet.startsWith(';')) {
// In JS libraries
// foo: ';[code here verbatim]'
// emits
// 'var foo;[code here verbatim];'
contentText = 'var ' + mangled + snippet;
contentText = 'var ' + binding + snippet;
if (snippet[snippet.length - 1] != ';' && snippet[snippet.length - 1] != '}') {
contentText += ';';
}
Expand All @@ -786,7 +793,7 @@ function(${args}) {
if (isNativeAlias) {
contentText = '';
} else {
contentText = `var ${mangled};`;
contentText = `var ${binding};`;
}
} else {
// In JS libraries
Expand All @@ -796,28 +803,33 @@ function(${args}) {
if (typeof snippet == 'string' && snippet[0] == '=') {
snippet = snippet.slice(1);
}
contentText = `var ${mangled} = ${snippet};`;
contentText = `var ${binding} = ${snippet};`;
}

if (contentText && MODULARIZE == 'instance' && (EXPORT_ALL || EXPORTED_FUNCTIONS.has(mangled)) && !isStub) {
// In MODULARIZE=instance mode mark JS library symbols are exported at
// the point of declaration.
contentText = 'export ' + contentText;
// the point of declaration. When the binding had to be renamed to a
// legal identifier, export it under its original name via an alias.
if (binding == mangled) {
contentText = 'export ' + contentText;
} else {
contentText += `\nexport { ${binding} as ${quoteExportName(mangled)} };`;
}
}

// Dynamic linking needs signatures to create proper wrappers.
if (sig && MAIN_MODULE) {
if (!WASM_BIGINT) {
sig = sig[0].replace('j', 'i') + sig.slice(1).replace(/j/g, 'ii');
}
contentText += `\n${mangled}.sig = '${sig}';`;
contentText += `\n${binding}.sig = '${sig}';`;
}
if (ASYNCIFY && isAsyncFunction) {
assert(isFunction);
contentText += `\n${mangled}.isAsync = true;`;
contentText += `\n${binding}.isAsync = true;`;
}
if (isStub) {
contentText += `\n${mangled}.stub = true;`;
contentText += `\n${binding}.stub = true;`;
if (ASYNCIFY && MAIN_MODULE) {
contentText += `\nasyncifyStubs['${symbol}'] = undefined;`;
}
Expand Down
7 changes: 5 additions & 2 deletions src/modules.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ import {
mergeInto,
localFile,
timer,
toValidIdentifier,
quoteExportName,
} from './utility.mjs';
import {preprocess, processMacros} from './parseTools.mjs';

Expand Down Expand Up @@ -463,12 +465,13 @@ function addMissingLibraryStubs(unusedLibSymbols) {
}

function exportSymbol(name) {
const binding = toValidIdentifier(name);
// In MODULARIZE=instance mode symbols are exported by being included in
// an export { foo, bar } list so we build up the simple list of names
if (MODULARIZE === 'instance') {
return name;
return binding == name ? name : `${binding} as ${quoteExportName(name)}`;
}
return `Module['${name}'] = ${name};`;
return `Module['${name}'] = ${binding};`;
}

// export parts of the JS runtime that the user asked for
Expand Down
53 changes: 53 additions & 0 deletions src/utility.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,59 @@ export function safeQuote(x) {
return x.replace(/"/g, '\\"').replace(/'/g, "\\'");
}

// JS reserved words that are otherwise valid identifiers but cannot be used as
// binding names (e.g. `var default = ...` is a syntax error).
const JS_RESERVED_WORDS = new Set([
'break', 'case', 'catch', 'class', 'const', 'continue', 'debugger',
'default', 'delete', 'do', 'else', 'enum', 'export', 'extends', 'false',
'finally', 'for', 'function', 'if', 'import', 'in', 'instanceof', 'new',
'null', 'return', 'super', 'switch', 'this', 'throw', 'true', 'try',
'typeof', 'var', 'void', 'while', 'with', 'yield', 'let', 'static',
'implements', 'interface', 'package', 'private', 'protected', 'public',
'await',
]);

// Whether `name` can be used verbatim as a JS binding identifier (e.g. after
// `var` or in an `export { x }` clause).
export function isValidIdentifier(name) {
return /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(name) && !JS_RESERVED_WORDS.has(name);
}

// Format `name` for the exported-name position of an ESM export/import clause
// (the part after `as`). Reserved words are legal there verbatim, but names
// with illegal characters must use the string-literal form.
export function quoteExportName(name) {
return /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(name) ? name : JSON.stringify(name);
}

// Maps a desired name to the JS identifier we use as its internal binding.
// Names that are already valid identifiers map to themselves. Others (reserved
// words, or names with illegal characters) are rewritten to a legal identifier,
// with a numeric suffix appended to disambiguate any collisions. The mapping is
// memoized so that a given name always resolves to the same binding.
const legalizedIdentifiers = new Map();
const usedIdentifiers = new Set();

export function toValidIdentifier(name) {
let ident = legalizedIdentifiers.get(name);
if (ident !== undefined) return ident;
if (isValidIdentifier(name)) {
ident = name;
} else {
let base = name.replace(/[^A-Za-z0-9_$]/g, '_');
if (!/^[A-Za-z_$]/.test(base) || JS_RESERVED_WORDS.has(base)) {
base = '$' + base;
}
ident = base;
for (let i = 0; usedIdentifiers.has(ident); i++) {
ident = base + '_' + i;
}
}
legalizedIdentifiers.set(name, ident);
usedIdentifiers.add(ident);
return ident;
}

export function dump(item) {
let funcData;
try {
Expand Down
29 changes: 29 additions & 0 deletions test/codesize/test_codesize_hello_esm_integration.json
Original file line number Diff line number Diff line change
@@ -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"
]
}
22 changes: 22 additions & 0 deletions test/codesize/test_codesize_minimal_esm_integration.json
Original file line number Diff line number Diff line change
@@ -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"
]
}
19 changes: 19 additions & 0 deletions test/js_optimizer/applyDCEGraphRemovals-esm-output.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
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 };

var HEAP32;

export { HEAP32 };

var $default = {};

export { $default as default };
39 changes: 39 additions & 0 deletions test/js_optimizer/applyDCEGraphRemovals-esm.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
// 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 };

// MODULARIZE=instance runtime exports (bare form) must be left untouched.
var HEAP32;
export { HEAP32 };

// A reserved-word runtime export (aliased `$default as default`) must never be
// treated as a wasm import and dropped, even though it is aliased.
var $default = {};
export { $default as default };

// EXTRA_INFO: { "unusedImports": ["unused_import"], "unusedExports": ["unused_export", "__indirect_function_table"] }
13 changes: 13 additions & 0 deletions test/js_optimizer/applyImportAndExportNameChanges-esm-output.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
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 };

var HEAP32;

export { HEAP32 };
30 changes: 30 additions & 0 deletions test/js_optimizer/applyImportAndExportNameChanges-esm.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
// 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 };

// 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" } }
Loading