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
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
4 changes: 4 additions & 0 deletions test/js_optimizer/applyDCEGraphRemovals-esm-output.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,7 @@ export { _main };
var HEAP32;

export { HEAP32 };

var $default = {};

export { $default as default };
5 changes: 5 additions & 0 deletions test/js_optimizer/applyDCEGraphRemovals-esm.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,4 +31,9 @@ export { _main };
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"] }
7 changes: 7 additions & 0 deletions test/js_optimizer/emitDCEGraph-esm.js
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,13 @@ var HEAP32;
var baz = () => {};
export { HEAP32, baz };

// A reserved-word runtime export (from the reserved-export-names support): the
// local is legalized to `$default` and aliased back to `default`. Though
// aliased like a wasm import edge, a wasm import name can never be `default`,
// so this must be treated as a plain runtime export, not a wasm import.
var $default = {};
export { $default as default };

// Top-level uses: root the memory export (via the alias) and one wasm export.
wasmMemory.buffer;
_used_export();
28 changes: 28 additions & 0 deletions test/test_other.py
Original file line number Diff line number Diff line change
Expand Up @@ -6273,6 +6273,34 @@ def test_modularize_instance_auto_init(self):

self.assertContained('main1\nmain2\nfoo\nbar\nbaz\n', self.run_js('runner.mjs'))

def test_modularize_instance_reserved_export(self):
# Export names that are JS reserved words (and so not valid binding
# identifiers) are bound to a legal name internally and exported under their
# original name via an alias. With AUTO_INIT the `default` slot is free.
create_file('library.js', '''\
addToLibrary({
$default: () => { console.log('default-export'); return 42; },
$delete: () => console.log('delete-export'),
});''')
self.run_process([EMCC, test_file('modularize_instance.c'),
'-sMODULARIZE=instance', '-sAUTO_INIT',
'-Wno-experimental',
'-sEXPORTED_RUNTIME_METHODS=default',
'-sEXPORTED_FUNCTIONS=_bar,_main,delete',
'--js-library', 'library.js',
'-o', 'modularize_instance.mjs'] + self.get_cflags())

create_file('runner.mjs', '''
import { strict as assert } from 'assert';
import theDefault, { delete as del, _foo as foo } from "./modularize_instance.mjs";
assert(typeof theDefault === 'function');
assert(theDefault() === 42); // reserved-word export via EXPORTED_RUNTIME_METHODS
del(); // reserved-word export via EXPORTED_FUNCTIONS
foo();
''')

self.assertContained('main1\nmain2\ndefault-export\ndelete-export\nfoo\n', self.run_js('runner.mjs'))

@also_with_pthreads
@requires_node_25
def test_esm_integration_auto_init(self):
Expand Down
13 changes: 10 additions & 3 deletions tools/acorn-optimizer.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -727,14 +727,19 @@ function emitDCEGraph(ast) {
// (a) JS functions sent to wasm: export { _fd_write as fd_write };
// (b) re-exports of wasm exports: export { _main };
// (c) runtime/library exports: export { HEAP32, baz };
// (d) reserved-name runtime export: export { $default as default };
// 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.
// reuse a single node for both sides. (d) is also aliased but is a JS
// runtime export, not a wasm import: a wasm import name is a C symbol
// and can never be a JS reserved word like `default`.
const importEdges = node.specifiers.filter(
(spec) =>
spec.local !== spec.exported && !wasmExportLocals.has(spec.local.name),
spec.local !== spec.exported &&
spec.exported.name !== 'default' &&
!wasmExportLocals.has(spec.local.name),
);
if (importEdges.length) {
// (a) `export { jsName as nativeName }` - jsName implements the import.
Expand Down Expand Up @@ -1000,8 +1005,10 @@ function applyDCEGraphRemovals(ast) {
} else if (isExportSpecifierList(node)) {
// WASM_ESM_INTEGRATION: drop unused wasm imports from
// export { _fd_write as fd_write, .. };
// Never drop `export { $default as default }`: it is a JS runtime export,
// not a wasm import (a wasm import name can never be `default`).
node.specifiers = node.specifiers.filter((spec) => {
if (unusedImports.has(spec.exported.name)) {
if (spec.exported.name !== 'default' && unusedImports.has(spec.exported.name)) {
foundUnusedImports.add(spec.exported.name);
return false;
}
Expand Down