Skip to content

Commit 660cac8

Browse files
committed
Support source-phase imports in the acorn/terser optimizer pipeline
When -sSOURCE_PHASE_IMPORTS=1, emcc emits ```js import source wasmModule from './foo.wasm'; ``` in its JS runtime. At -O2/-O3/-Os/-Oz the emitted JS is run through tools/acorn-optimizer.mjs which currently fails with a SyntaxError at parse time because acorn 8.x does not yet understand the source-phase imports proposal (https://github.com/tc39/proposal-source-phase-imports). Wire in the acorn-import-phases plugin so acorn can parse the syntax, and pull in the matching terser support (downstream of emscripten-core/terser#1) so the round-trip through from_mozilla_ast / to_mozilla_ast preserves the `phase` keyword on the way back out. Without the terser side, terser would silently drop the keyword and the host would return the module's exports namespace instead of a WebAssembly.Module, changing runtime semantics. * package.json: add acorn-import-phases dependency. * tools/acorn-optimizer.mjs: extend acorn with the plugin and use the extended parser at the parse site. * third_party/terser/terser.js: rebuilt from the emscripten-core/terser branch with source-phase imports support (PR emscripten-core/terser#1, the v5.18.2 downstream port of upstream terser PR terser/terser#1682). * test/js_optimizer/sourcePhaseImports{,-output}.js: new fixture that feeds two `import source` declarations through the JSDCE pass and checks the keyword survives. * test/test_other.py: register the fixture in test_js_optimizer, and parametrize test_esm_source_phase_imports across no-args and -O2 to exercise the optimizer pipeline.
1 parent 85ee810 commit 660cac8

7 files changed

Lines changed: 174 additions & 35 deletions

File tree

package-lock.json

Lines changed: 5 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
},
1919
"dependencies": {
2020
"acorn": "^8.15.0",
21+
"acorn-import-phases": "^1.0.4",
2122
"google-closure-compiler": "20260429.0.0",
2223
"html-minifier-terser": "7.2.0"
2324
},
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import source wasmModule from "./foo.wasm";
2+
3+
import source otherModule from "./bar.wasm";
4+
5+
function use() {
6+
return [ wasmModule, otherModule ];
7+
}
8+
9+
use();
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
// Source-phase imports (https://github.com/tc39/proposal-source-phase-imports).
2+
// The acorn optimizer must parse these via the acorn-import-phases plugin and
3+
// preserve the `source` phase keyword through the terser from_mozilla_ast ->
4+
// print round-trip used at -O2+.
5+
import source wasmModule from './foo.wasm';
6+
import source otherModule from './bar.wasm';
7+
8+
function use() {
9+
return [wasmModule, otherModule];
10+
}
11+
use();

test/test_other.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -421,11 +421,15 @@ def test_esm(self, args):
421421
self.assertContained('Hello, world!', self.run_js('hello_world.mjs'))
422422

423423
@requires_node_25
424-
def test_esm_source_phase_imports(self):
424+
@parameterized({
425+
'': ([],),
426+
'O3': (['-O3'],),
427+
})
428+
def test_esm_source_phase_imports(self, args):
425429
self.node_args += ['--experimental-wasm-modules', '--no-warnings']
426430
self.run_process([EMCC, '-o', 'hello_world.mjs', '-sSOURCE_PHASE_IMPORTS',
427431
'--extern-post-js', test_file('modularize_post_js.js'),
428-
test_file('hello_world.c')])
432+
test_file('hello_world.c')] + args)
429433
self.assertContained('import source wasmModule from', read_file('hello_world.mjs'))
430434
self.assertContained('Hello, world!', self.run_js('hello_world.mjs'))
431435

@@ -3012,6 +3016,7 @@ def test_extern_prepost(self):
30123016
'minifyGlobals': (['minifyGlobals'],),
30133017
'minifyLocals': (['minifyLocals'],),
30143018
'JSDCE': (['JSDCE', '--export-es6'],),
3019+
'sourcePhaseImports': (['JSDCE', '--export-es6'],),
30153020
'JSDCE-hasOwnProperty': (['JSDCE'],),
30163021
'JSDCE-defaultArg': (['JSDCE'],),
30173022
'JSDCE-fors': (['JSDCE'],),

third_party/terser/terser.js

Lines changed: 132 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
(function (global, factory) {
22
typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) :
33
typeof define === 'function' && define.amd ? define(['exports'], factory) :
4-
(global = global || self, factory(global.Terser = {}));
5-
}(this, (function (exports) { 'use strict';
4+
(global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global.Terser = {}));
5+
})(this, (function (exports) { 'use strict';
66

77
/***********************************************************************
88
@@ -1161,7 +1161,7 @@ function parse($TEXT, options) {
11611161
// Example: /* I count */ ( /* I don't */ foo() )
11621162
// Useful because comments_before property of call with parens outside
11631163
// contains both comments inside and outside these parens. Used to find the
1164-
// right #__PURE__ comments for an expression
1164+
11651165
const outer_comments_before_counts = new WeakMap();
11661166

11671167
options = defaults(options, {
@@ -2404,7 +2404,7 @@ function parse($TEXT, options) {
24042404
return new_(allow_calls);
24052405
}
24062406
if (is("name", "import") && is_token(peek(), "punc", ".")) {
2407-
return import_meta(allow_calls);
2407+
return parse_import_expr(allow_calls);
24082408
}
24092409
var start = S.token;
24102410
var peeked;
@@ -2864,6 +2864,17 @@ function parse($TEXT, options) {
28642864
function import_statement() {
28652865
var start = prev();
28662866

2867+
// import source x from "..."
2868+
// import defer * as x from "..."
2869+
var phase = null;
2870+
if (is("name", "source") || is("name", "defer")) {
2871+
var peeked = peek();
2872+
if (!is_token(peeked, "name", "from") && !is_token(peeked, "punc", ",")) {
2873+
phase = S.token.value;
2874+
next();
2875+
}
2876+
}
2877+
28672878
var imported_name;
28682879
var imported_names;
28692880
if (is("name")) {
@@ -2898,14 +2909,33 @@ function parse($TEXT, options) {
28982909
end: mod_str,
28992910
}),
29002911
assert_clause,
2912+
phase,
29012913
end: S.token,
29022914
});
29032915
}
29042916

2905-
function import_meta(allow_calls) {
2917+
// import.meta
2918+
// import.source("module")
2919+
// import.defer("module")
2920+
function parse_import_expr(allow_calls) {
29062921
var start = S.token;
29072922
expect_token("name", "import");
29082923
expect_token("punc", ".");
2924+
if (is("name", "source") || is("name", "defer")) {
2925+
var phase = S.token.value;
2926+
next();
2927+
if (!is("punc", "(")) {
2928+
croak("'import." + phase + "' can only be used in a dynamic import");
2929+
}
2930+
next();
2931+
var args = expr_list(")");
2932+
return subscripts(new AST_DynamicImport({
2933+
start: start,
2934+
phase: phase,
2935+
args: args,
2936+
end: prev()
2937+
}), allow_calls);
2938+
}
29092939
expect_token("name", "meta");
29102940
return subscripts(new AST_ImportMeta({
29112941
start: start,
@@ -5071,9 +5101,10 @@ var AST_NameMapping = DEFNODE("NameMapping", "foreign_name name", function AST_N
50715101

50725102
var AST_Import = DEFNODE(
50735103
"Import",
5074-
"imported_name imported_names module_name assert_clause",
5104+
"phase imported_name imported_names module_name assert_clause",
50755105
function AST_Import(props) {
50765106
if (props) {
5107+
this.phase = props.phase;
50775108
this.imported_name = props.imported_name;
50785109
this.imported_names = props.imported_names;
50795110
this.module_name = props.module_name;
@@ -5087,6 +5118,7 @@ var AST_Import = DEFNODE(
50875118
{
50885119
$documentation: "An `import` statement",
50895120
$propdoc: {
5121+
phase: "[string?] Phase keyword: 'source', 'defer', or null.",
50905122
imported_name: "[AST_SymbolImport] The name of the variable holding the module's default export.",
50915123
imported_names: "[AST_NameMapping*] The names of non-default imported variables",
50925124
module_name: "[AST_String] String literal describing where this module came from",
@@ -5127,6 +5159,40 @@ var AST_ImportMeta = DEFNODE("ImportMeta", null, function AST_ImportMeta(props)
51275159
$documentation: "A reference to import.meta",
51285160
});
51295161

5162+
var AST_DynamicImport = DEFNODE(
5163+
"DynamicImport",
5164+
"phase args",
5165+
function AST_DynamicImport(props) {
5166+
if (props) {
5167+
this.phase = props.phase;
5168+
this.args = props.args;
5169+
this.start = props.start;
5170+
this.end = props.end;
5171+
}
5172+
5173+
this.flags = 0;
5174+
},
5175+
{
5176+
$documentation: "A phased dynamic import expression: `import.source(specifier [, options])` or `import.defer(specifier [, options])`. Plain `import(x)` continues to be parsed as an AST_Call with a synthetic `import` SymbolRef callee.",
5177+
$propdoc: {
5178+
phase: "[string] Phase keyword ('source' or 'defer').",
5179+
args: "[AST_Node*] specifier followed by optional options argument"
5180+
},
5181+
_walk: function(visitor) {
5182+
return visitor._visit(this, function() {
5183+
var args = this.args;
5184+
for (var i = 0, len = args.length; i < len; i++) {
5185+
args[i]._walk(visitor);
5186+
}
5187+
});
5188+
},
5189+
_children_backwards(push) {
5190+
let i = this.args.length;
5191+
while (i--) push(this.args[i]);
5192+
},
5193+
}
5194+
);
5195+
51305196
var AST_Export = DEFNODE(
51315197
"Export",
51325198
"exported_definition exported_value is_default exported_names module_name assert_clause",
@@ -6485,7 +6551,7 @@ var AST_Null = DEFNODE("Null", null, function AST_Null(props) {
64856551
value: null
64866552
}, AST_Atom);
64876553

6488-
var AST_NaN = DEFNODE("NaN", null, function AST_NaN(props) {
6554+
DEFNODE("NaN", null, function AST_NaN(props) {
64896555
if (props) {
64906556
this.start = props.start;
64916557
this.end = props.end;
@@ -6497,7 +6563,7 @@ var AST_NaN = DEFNODE("NaN", null, function AST_NaN(props) {
64976563
value: 0/0
64986564
}, AST_Atom);
64996565

6500-
var AST_Undefined = DEFNODE("Undefined", null, function AST_Undefined(props) {
6566+
DEFNODE("Undefined", null, function AST_Undefined(props) {
65016567
if (props) {
65026568
this.start = props.start;
65036569
this.end = props.end;
@@ -6521,7 +6587,7 @@ var AST_Hole = DEFNODE("Hole", null, function AST_Hole(props) {
65216587
value: (function() {}())
65226588
}, AST_Atom);
65236589

6524-
var AST_Infinity = DEFNODE("Infinity", null, function AST_Infinity(props) {
6590+
DEFNODE("Infinity", null, function AST_Infinity(props) {
65256591
if (props) {
65266592
this.start = props.start;
65276593
this.end = props.end;
@@ -6706,7 +6772,7 @@ const _NOINLINE = 0b00000100;
67066772
const _KEY = 0b00001000;
67076773
const _MANGLEPROP = 0b00010000;
67086774

6709-
// XXX Emscripten: export TreeWalker for walking through AST in acorn-optimizer.mjs.
6775+
// XXX Emscripten: export TreeWalker for walking through AST in acorn-optimizer.js.
67106776
exports.TreeWalker = TreeWalker;
67116777

67126778
/***********************************************************************
@@ -7449,7 +7515,8 @@ def_transform(AST_PrefixedTemplateString, function(self, tw) {
74497515
imported_name: imported_name,
74507516
imported_names : imported_names,
74517517
module_name : from_moz(M.source),
7452-
assert_clause: assert_clause_from_moz(M.assertions)
7518+
assert_clause: assert_clause_from_moz(M.assertions),
7519+
phase: M.phase || null
74537520
});
74547521
},
74557522

@@ -7475,6 +7542,31 @@ def_transform(AST_PrefixedTemplateString, function(self, tw) {
74757542
});
74767543
},
74777544

7545+
ImportExpression: function(M) {
7546+
const args = [from_moz(M.source)];
7547+
if (M.options) {
7548+
args.push(from_moz(M.options));
7549+
}
7550+
if (M.phase) {
7551+
return new AST_DynamicImport({
7552+
start: my_start_token(M),
7553+
end: my_end_token(M),
7554+
phase: M.phase,
7555+
args: args
7556+
});
7557+
}
7558+
return new AST_Call({
7559+
start: my_start_token(M),
7560+
end: my_end_token(M),
7561+
expression: from_moz({
7562+
type: "Identifier",
7563+
name: "import"
7564+
}),
7565+
optional: false,
7566+
args
7567+
});
7568+
},
7569+
74787570
ExportAllDeclaration: function(M) {
74797571
var foreign_name = M.exported == null ?
74807572
new AST_SymbolExportForeign({ name: "*" }) :
@@ -7887,19 +7979,6 @@ def_transform(AST_PrefixedTemplateString, function(self, tw) {
78877979
});
78887980
},
78897981

7890-
ImportExpression: function(M) {
7891-
let import_token = my_start_token(M);
7892-
return new AST_Call({
7893-
start : import_token,
7894-
end : my_end_token(M),
7895-
expression : new AST_SymbolRef({
7896-
start : import_token,
7897-
end : import_token,
7898-
name : "import"
7899-
}),
7900-
args : [from_moz(M.source)]
7901-
});
7902-
}
79037982
};
79047983

79057984
MOZ_TO_ME.UpdateExpression =
@@ -8120,6 +8199,16 @@ def_transform(AST_PrefixedTemplateString, function(self, tw) {
81208199
};
81218200
});
81228201

8202+
def_to_moz(AST_DynamicImport, function To_Moz_ImportExpression(M) {
8203+
const [source, options] = M.args.map(to_moz);
8204+
return {
8205+
type: "ImportExpression",
8206+
source,
8207+
options: options || null,
8208+
phase: M.phase
8209+
};
8210+
});
8211+
81238212
def_to_moz(AST_Toplevel, function To_Moz_Program(M) {
81248213
return to_moz_scope("Program", M);
81258214
});
@@ -8347,12 +8436,14 @@ def_transform(AST_PrefixedTemplateString, function(self, tw) {
83478436
});
83488437
}
83498438
}
8350-
return {
8439+
var moz = {
83518440
type: "ImportDeclaration",
83528441
specifiers: specifiers,
83538442
source: to_moz(M.module_name),
83548443
assertions: assert_clause_to_moz(M.assert_clause)
83558444
};
8445+
if (M.phase) moz.phase = M.phase;
8446+
return moz;
83568447
});
83578448

83588449
def_to_moz(AST_ImportMeta, function To_Moz_MetaProperty() {
@@ -9962,9 +10053,9 @@ function OutputStream(options) {
996210053
// XXX Emscripten localmod: Add a node type for a parenthesized expression so that we can retain
996310054
// Closure annotations that need a form "/**annotation*/(expression)"
996410055
DEFPRINT(AST_ParenthesizedExpression, function(self, output) {
9965-
output.print('(');
10056+
output.print("(");
996610057
self.body.print(output);
9967-
output.print(')');
10058+
output.print(")");
996810059
});
996910060
// XXX End Emscripten localmod
997010061
function print_braced_empty(self, output) {
@@ -10398,6 +10489,10 @@ function OutputStream(options) {
1039810489
DEFPRINT(AST_Import, function(self, output) {
1039910490
output.print("import");
1040010491
output.space();
10492+
if (self.phase) {
10493+
output.print(self.phase);
10494+
output.space();
10495+
}
1040110496
if (self.imported_name) {
1040210497
self.imported_name.print(output);
1040310498
}
@@ -10438,6 +10533,15 @@ function OutputStream(options) {
1043810533
DEFPRINT(AST_ImportMeta, function(self, output) {
1043910534
output.print("import.meta");
1044010535
});
10536+
DEFPRINT(AST_DynamicImport, function(self, output) {
10537+
output.print("import." + self.phase);
10538+
output.with_parens(function() {
10539+
self.args.forEach(function(arg, i) {
10540+
if (i) output.comma();
10541+
arg.print(output);
10542+
});
10543+
});
10544+
});
1044110545

1044210546
DEFPRINT(AST_NameMapping, function(self, output) {
1044310547
var is_import = output.parent() instanceof AST_Import;
@@ -12134,4 +12238,4 @@ const base54 = (() => {
1213412238
exports.AST_Node = AST_Node;
1213512239
exports.AST_Token = AST_Token;
1213612240

12137-
})));
12241+
}));

0 commit comments

Comments
 (0)