Skip to content

Commit 79b61c4

Browse files
Support source-phase imports (import source / import defer and dynamic import.source(...) / import.defer(...)) (terser#1682)
* add support for source-phase imports (`import source`, `import.source(...)`) Implements the TC39 source-phase imports proposal (https://github.com/tc39/proposal-source-phase-imports) for both static and dynamic forms: // static import source wasmModule from "./foo.wasm"; import defer * as ns from "./mod.js"; // dynamic const m = await import.source("./foo.wasm"); const m = await import.defer("./mod.js"); Without this, terser dropped the phase keyword on the mozilla-AST round-trip used by tools like Emscripten's acorn-optimizer, silently changing runtime semantics. Static side: * Parser recognises the contextual `source`/`defer` phase keyword after `import`, with peek-based disambiguation so existing patterns like `import source from "x"` keep parsing as default-name imports. * AST_Import gains a `phase` field (null for plain imports). * mozilla-ast and the printer carry phase through. Dynamic side: * New AST_DynamicImport node owns the whole `import.source(...)` / `import.defer(...)` expression, including its args. It is NOT an AST_Call. Plain `import(x)` is still parsed as an AST_Call with a synthetic `import` SymbolRef callee. * mozilla-ast: ImportExpression with `phase` round-trips to/from AST_DynamicImport. * size, equivalent-to and has_side_effects/may_throw updated. * address review feedback: tidy comments and use (x || null) === pattern * Update test/compress/harmony.js * Update test/compress/harmony.js --------- Co-authored-by: Fábio Santos <fabiosantosart@gmail.com>
1 parent 692a6de commit 79b61c4

10 files changed

Lines changed: 356 additions & 19 deletions

File tree

lib/ast.js

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1504,9 +1504,10 @@ var AST_NameMapping = DEFNODE("NameMapping", "foreign_name name", function AST_N
15041504

15051505
var AST_Import = DEFNODE(
15061506
"Import",
1507-
"imported_name imported_names module_name assert_clause",
1507+
"phase imported_name imported_names module_name assert_clause",
15081508
function AST_Import(props) {
15091509
if (props) {
1510+
this.phase = props.phase;
15101511
this.imported_name = props.imported_name;
15111512
this.imported_names = props.imported_names;
15121513
this.module_name = props.module_name;
@@ -1520,6 +1521,7 @@ var AST_Import = DEFNODE(
15201521
{
15211522
$documentation: "An `import` statement",
15221523
$propdoc: {
1524+
phase: "[string?] Phase keyword: 'source', 'defer', or null.",
15231525
imported_name: "[AST_SymbolImport] The name of the variable holding the module's default export.",
15241526
imported_names: "[AST_NameMapping*] The names of non-default imported variables",
15251527
module_name: "[AST_String] String literal describing where this module came from",
@@ -1560,6 +1562,40 @@ var AST_ImportMeta = DEFNODE("ImportMeta", null, function AST_ImportMeta(props)
15601562
$documentation: "A reference to import.meta",
15611563
});
15621564

1565+
var AST_DynamicImport = DEFNODE(
1566+
"DynamicImport",
1567+
"phase args",
1568+
function AST_DynamicImport(props) {
1569+
if (props) {
1570+
this.phase = props.phase;
1571+
this.args = props.args;
1572+
this.start = props.start;
1573+
this.end = props.end;
1574+
}
1575+
1576+
this.flags = 0;
1577+
},
1578+
{
1579+
$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.",
1580+
$propdoc: {
1581+
phase: "[string] Phase keyword ('source' or 'defer').",
1582+
args: "[AST_Node*] specifier followed by optional options argument"
1583+
},
1584+
_walk: function(visitor) {
1585+
return visitor._visit(this, function() {
1586+
var args = this.args;
1587+
for (var i = 0, len = args.length; i < len; i++) {
1588+
args[i]._walk(visitor);
1589+
}
1590+
});
1591+
},
1592+
_children_backwards(push) {
1593+
let i = this.args.length;
1594+
while (i--) push(this.args[i]);
1595+
},
1596+
}
1597+
);
1598+
15631599
var AST_Export = DEFNODE(
15641600
"Export",
15651601
"exported_definition exported_value is_default exported_names module_name assert_clause",
@@ -3264,6 +3300,7 @@ export {
32643300
AST_Function,
32653301
AST_Hole,
32663302
AST_If,
3303+
AST_DynamicImport,
32673304
AST_Import,
32683305
AST_ImportMeta,
32693306
AST_Infinity,

lib/compress/drop-side-effect-free.js

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ import {
5757
AST_Constant,
5858
AST_DefClass,
5959
AST_Dot,
60+
AST_DynamicImport,
6061
AST_Expansion,
6162
AST_Function,
6263
AST_Node,
@@ -147,6 +148,12 @@ def_drop_side_effect_free(AST_Call, function (compressor, first_in_statement) {
147148
return args && make_sequence(this, args);
148149
});
149150

151+
def_drop_side_effect_free(AST_DynamicImport, function (compressor, first_in_statement) {
152+
if (this.phase !== "source") return this;
153+
var args = trim(this.args, compressor, first_in_statement);
154+
return args && make_sequence(this, args);
155+
});
156+
150157
def_drop_side_effect_free(AST_Accessor, return_null);
151158

152159
def_drop_side_effect_free(AST_Function, return_null);

lib/compress/inference.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@ import {
6868
AST_Function,
6969
AST_If,
7070
AST_Import,
71+
AST_DynamicImport,
7172
AST_ImportMeta,
7273
AST_Jump,
7374
AST_LabeledStatement,
@@ -314,6 +315,10 @@ export function is_nullish(node, compressor) {
314315
|| this.alternative && this.alternative.has_side_effects(compressor);
315316
});
316317
def_has_side_effects(AST_ImportMeta, return_false);
318+
def_has_side_effects(AST_DynamicImport, function() {
319+
// `import.source(x)` only compiles the module, which is side-effect free
320+
return this.phase !== "source";
321+
});
317322
def_has_side_effects(AST_LabeledStatement, function(compressor) {
318323
return this.body.has_side_effects(compressor);
319324
});

lib/equivalent-to.js

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import {
2828
AST_ForOf,
2929
AST_If,
3030
AST_Import,
31+
AST_DynamicImport,
3132
AST_ImportMeta,
3233
AST_Jump,
3334
AST_LabeledStatement,
@@ -190,11 +191,17 @@ AST_VarDef.prototype.shallow_cmp = function(other) {
190191
AST_NameMapping.prototype.shallow_cmp = pass_through;
191192

192193
AST_Import.prototype.shallow_cmp = function(other) {
193-
return (this.imported_name == null ? other.imported_name == null : this.imported_name === other.imported_name) && (this.imported_names == null ? other.imported_names == null : this.imported_names === other.imported_names);
194+
return (this.imported_name || null) === (other.imported_name || null)
195+
&& (this.imported_names || null) === (other.imported_names || null)
196+
&& (this.phase || null) === (other.phase || null);
194197
};
195198

196199
AST_ImportMeta.prototype.shallow_cmp = pass_through;
197200

201+
AST_DynamicImport.prototype.shallow_cmp = function(other) {
202+
return (this.phase || null) === (other.phase || null) && this.args.length === other.args.length;
203+
};
204+
198205
AST_Export.prototype.shallow_cmp = function(other) {
199206
return (this.exported_definition == null ? other.exported_definition == null : this.exported_definition === other.exported_definition) && (this.exported_value == null ? other.exported_value == null : this.exported_value === other.exported_value) && (this.exported_names == null ? other.exported_names == null : this.exported_names === other.exported_names) && this.module_name === other.module_name && this.is_default === other.is_default;
200207
};

lib/mozilla-ast.js

Lines changed: 41 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,7 @@ import {
9191
AST_Function,
9292
AST_Hole,
9393
AST_If,
94+
AST_DynamicImport,
9495
AST_Import,
9596
AST_ImportMeta,
9697
AST_Label,
@@ -590,7 +591,8 @@ import { is_basic_identifier_string } from "./parse.js";
590591
imported_name: imported_name,
591592
imported_names : imported_names,
592593
module_name : from_moz(M.source),
593-
assert_clause: assert_clause_from_moz(M.assertions)
594+
assert_clause: assert_clause_from_moz(M.assertions),
595+
phase: M.phase || null
594596
});
595597
},
596598

@@ -616,6 +618,31 @@ import { is_basic_identifier_string } from "./parse.js";
616618
});
617619
},
618620

621+
ImportExpression: function(M) {
622+
const args = [from_moz(M.source)];
623+
if (M.options) {
624+
args.push(from_moz(M.options));
625+
}
626+
if (M.phase) {
627+
return new AST_DynamicImport({
628+
start: my_start_token(M),
629+
end: my_end_token(M),
630+
phase: M.phase,
631+
args: args
632+
});
633+
}
634+
return new AST_Call({
635+
start: my_start_token(M),
636+
end: my_end_token(M),
637+
expression: from_moz({
638+
type: "Identifier",
639+
name: "import"
640+
}),
641+
optional: false,
642+
args
643+
});
644+
},
645+
619646
ExportAllDeclaration: function(M) {
620647
var foreign_name = M.exported == null ?
621648
new AST_SymbolExportForeign({ name: "*" }) :
@@ -1028,19 +1055,6 @@ import { is_basic_identifier_string } from "./parse.js";
10281055
});
10291056
},
10301057

1031-
ImportExpression: function(M) {
1032-
let import_token = my_start_token(M);
1033-
return new AST_Call({
1034-
start : import_token,
1035-
end : my_end_token(M),
1036-
expression : new AST_SymbolRef({
1037-
start : import_token,
1038-
end : import_token,
1039-
name : "import"
1040-
}),
1041-
args : [from_moz(M.source)]
1042-
});
1043-
}
10441058
};
10451059

10461060
MOZ_TO_ME.UpdateExpression =
@@ -1261,6 +1275,16 @@ import { is_basic_identifier_string } from "./parse.js";
12611275
};
12621276
});
12631277

1278+
def_to_moz(AST_DynamicImport, function To_Moz_ImportExpression(M) {
1279+
const [source, options] = M.args.map(to_moz);
1280+
return {
1281+
type: "ImportExpression",
1282+
source,
1283+
options: options || null,
1284+
phase: M.phase
1285+
};
1286+
});
1287+
12641288
def_to_moz(AST_Toplevel, function To_Moz_Program(M) {
12651289
return to_moz_scope("Program", M);
12661290
});
@@ -1488,12 +1512,14 @@ import { is_basic_identifier_string } from "./parse.js";
14881512
});
14891513
}
14901514
}
1491-
return {
1515+
var moz = {
14921516
type: "ImportDeclaration",
14931517
specifiers: specifiers,
14941518
source: to_moz(M.module_name),
14951519
assertions: assert_clause_to_moz(M.assert_clause)
14961520
};
1521+
if (M.phase) moz.phase = M.phase;
1522+
return moz;
14971523
});
14981524

14991525
def_to_moz(AST_ImportMeta, function To_Moz_MetaProperty() {

lib/output.js

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,7 @@ import {
102102
AST_Function,
103103
AST_Hole,
104104
AST_If,
105+
AST_DynamicImport,
105106
AST_Import,
106107
AST_ImportMeta,
107108
AST_Jump,
@@ -1697,6 +1698,10 @@ function OutputStream(options) {
16971698
DEFPRINT(AST_Import, function(self, output) {
16981699
output.print("import");
16991700
output.space();
1701+
if (self.phase) {
1702+
output.print(self.phase);
1703+
output.space();
1704+
}
17001705
if (self.imported_name) {
17011706
self.imported_name.print(output);
17021707
}
@@ -1737,6 +1742,15 @@ function OutputStream(options) {
17371742
DEFPRINT(AST_ImportMeta, function(self, output) {
17381743
output.print("import.meta");
17391744
});
1745+
DEFPRINT(AST_DynamicImport, function(self, output) {
1746+
output.print("import." + self.phase);
1747+
output.with_parens(function() {
1748+
self.args.forEach(function(arg, i) {
1749+
if (i) output.comma();
1750+
arg.print(output);
1751+
});
1752+
});
1753+
});
17401754

17411755
DEFPRINT(AST_NameMapping, function(self, output) {
17421756
var is_import = output.parent() instanceof AST_Import;

lib/parse.js

Lines changed: 33 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,7 @@ import {
9898
AST_Function,
9999
AST_Hole,
100100
AST_If,
101+
AST_DynamicImport,
101102
AST_Import,
102103
AST_ImportMeta,
103104
AST_IterationStatement,
@@ -2311,7 +2312,7 @@ function parse($TEXT, options) {
23112312
return new_(allow_calls);
23122313
}
23132314
if (is("name", "import") && is_token(peek(), "punc", ".")) {
2314-
return import_meta(allow_calls);
2315+
return parse_import_expr(allow_calls);
23152316
}
23162317
var start = S.token;
23172318
var peeked;
@@ -2771,6 +2772,17 @@ function parse($TEXT, options) {
27712772
function import_statement() {
27722773
var start = prev();
27732774

2775+
// import source x from "..."
2776+
// import defer * as x from "..."
2777+
var phase = null;
2778+
if (is("name", "source") || is("name", "defer")) {
2779+
var peeked = peek();
2780+
if (!is_token(peeked, "name", "from") && !is_token(peeked, "punc", ",")) {
2781+
phase = S.token.value;
2782+
next();
2783+
}
2784+
}
2785+
27742786
var imported_name;
27752787
var imported_names;
27762788
if (is("name")) {
@@ -2805,14 +2817,33 @@ function parse($TEXT, options) {
28052817
end: mod_str,
28062818
}),
28072819
assert_clause,
2820+
phase,
28082821
end: S.token,
28092822
});
28102823
}
28112824

2812-
function import_meta(allow_calls) {
2825+
// import.meta
2826+
// import.source("module")
2827+
// import.defer("module")
2828+
function parse_import_expr(allow_calls) {
28132829
var start = S.token;
28142830
expect_token("name", "import");
28152831
expect_token("punc", ".");
2832+
if (is("name", "source") || is("name", "defer")) {
2833+
var phase = S.token.value;
2834+
next();
2835+
if (!is("punc", "(")) {
2836+
croak("'import." + phase + "' can only be used in a dynamic import");
2837+
}
2838+
next();
2839+
var args = expr_list(")");
2840+
return subscripts(new AST_DynamicImport({
2841+
start: start,
2842+
phase: phase,
2843+
args: args,
2844+
end: prev()
2845+
}), allow_calls);
2846+
}
28162847
expect_token("name", "meta");
28172848
return subscripts(new AST_ImportMeta({
28182849
start: start,

lib/size.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ import {
3535
AST_Hole,
3636
AST_If,
3737
AST_Import,
38+
AST_DynamicImport,
3839
AST_ImportMeta,
3940
AST_Infinity,
4041
AST_LabeledStatement,
@@ -270,6 +271,11 @@ AST_Import.prototype._size = function () {
270271

271272
AST_ImportMeta.prototype._size = () => 11;
272273

274+
AST_DynamicImport.prototype._size = function () {
275+
// `import.` + phase + `()` + arg overhead
276+
return 9 + this.phase.length + list_overhead(this.args);
277+
};
278+
273279
AST_Export.prototype._size = function () {
274280
let size = 7 + (this.is_default ? 8 : 0);
275281

0 commit comments

Comments
 (0)