From 75fe99ebd6659f7afdb373821739bd79f13c6436 Mon Sep 17 00:00:00 2001 From: Sam Clegg Date: Tue, 20 Oct 2026 11:32:11 -0700 Subject: [PATCH] Emit JS library aliases after non-alias library items When a JS library alias target (e.g. `foo`) is visited first and transitively depends on its own alias (e.g. `foo_alias: 'foo'`), `addedLibraryItems['foo']` is marked as visited before `var _foo = ...` is emitted. Emitting `var _foo_alias = _foo;` inline during dependency traversal causes it to appear before `var _foo = ...`, leaving `_foo_alias` initialized to `undefined` for arrow functions and non-hoisted values. Collect JS library alias assignments in `libraryAliases` and emit them after `libraryItems` (and before `postSets`) so all target symbols are defined before any alias assignment runs. --- src/jsifier.mjs | 15 +++++++++++++-- test/codesize/test_codesize_hello_dylink_all.json | 4 ++-- test/test_jslib.py | 10 +++++++++- 3 files changed, 24 insertions(+), 5 deletions(-) diff --git a/src/jsifier.mjs b/src/jsifier.mjs index 9c885291d7ba6..a74ed0189cdc9 100644 --- a/src/jsifier.mjs +++ b/src/jsifier.mjs @@ -414,6 +414,7 @@ const PROXY_SYNC_ASYNC = 2; export async function runJSify(outputFile, symbolsOnly) { const libraryItems = []; + const libraryAliases = []; const symbolDeps = {}; const asyncFuncs = []; let postSets = []; @@ -728,11 +729,12 @@ function(${args}) { } } + let aliasTarget; if (LibraryManager.isAlias(snippet)) { // Redirection for aliases. We include the parent, and at runtime // make ourselves equal to it. This avoid having duplicate // functions with identical content. - const aliasTarget = snippet; + aliasTarget = snippet; if (WASM_EXPORTS.has(aliasTarget)) { debugLog(`native alias: ${mangled} -> ${aliasTarget}`); nativeAliases[mangled] = aliasTarget; @@ -884,6 +886,15 @@ function(${args}) { .filter((x) => x != '') .join('\n') + '\n' : ''; + // Defer direct JS value aliases (`var _alias = _target;`) to `libraryAliases` + // so the target symbol is guaranteed to be defined first. Skip native + // aliases (which emit no code here) and signature-mismatch aliases (where + // `isFunction` is set because they are emitted as forwarding wrapper + // functions rather than immediate value assignments). + if (aliasTarget && !isNativeAlias && !isFunction) { + libraryAliases.push(commentText + contentText); + return depsText.trim(); + } return depsText + commentText + contentText; } @@ -928,7 +939,7 @@ function(${args}) { includeSystemFile(preFile); writeOutput('// Begin JS library code\n'); - for (const item of libraryItems.concat(postSets)) { + for (const item of [...libraryItems, ...libraryAliases, ...postSets]) { writeOutput(indentify(item ?? '', 2)); } writeOutput('// End JS library code\n'); diff --git a/test/codesize/test_codesize_hello_dylink_all.json b/test/codesize/test_codesize_hello_dylink_all.json index 080c561717270..cc07778d64717 100644 --- a/test/codesize/test_codesize_hello_dylink_all.json +++ b/test/codesize/test_codesize_hello_dylink_all.json @@ -1,7 +1,7 @@ { - "a.out.js": 272713, + "a.out.js": 272709, "a.out.nodebug.wasm": 588152, - "total": 860865, + "total": 860861, "sent": [ "IMG_Init", "IMG_Load", diff --git a/test/test_jslib.py b/test/test_jslib.py index 550b44615ed0f..8a6539d6c7cab 100644 --- a/test/test_jslib.py +++ b/test/test_jslib.py @@ -604,7 +604,15 @@ def test_jslib_include(self): def test_jslib_aliases(self, args): create_file('foo.js', ''' addToLibrary({ - foo: () => 42, + // Create a dependency cycle (foo -> $call_alias -> foo_alias -> foo) + // to test that `var _foo_alias = _foo;` is still emitted after `_foo`. + $call_alias__deps: ['foo_alias'], + $call_alias: () => _foo_alias(), + foo__deps: ['$call_alias'], + foo: () => { + if (!call_alias) return 0; + return 42; + }, foo__sig: 'i', foo_alias: 'foo',