Skip to content

Commit 3016f49

Browse files
authored
feat: implement import.defer() and import.source() dynamic import syntax (#5035)
This Pull Request adds parsing and runtime support for `import.defer()` and `import.source()` dynamic import syntax, enabling 64 previously-ignored test262 tests. It changes the following: - Added `ImportPhase` enum (`Evaluation`, `Defer`, `Source`) and a `phase` field to the `ImportCall` AST node in `core/ast/src/expression/call.rs`, with proper `ToInternedString` output for each phase. - Extended the parser in `core/parser/src/parser/expression/left_hand_side/mod.rs` to detect and parse `import.defer(expr)` and `import.source(expr)` syntax, including 4-token lookahead in `is_keyword_call` and phase-aware token consumption. - Updated the bytecode compiler in `core/engine/src/bytecompiler/expression/mod.rs` to encode `ImportPhase` as an `IndexOperand` (0=evaluation, 1=defer, 2=source) on the `ImportCall` instruction. - Added `phase: IndexOperand` to the `ImportCall` opcode definition in `core/engine/src/vm/opcode/mod.rs` and updated the code block display in `core/engine/src/vm/code_block.rs` to show phase names. - Updated the VM `ImportCall` handler in `core/engine/src/vm/opcode/call/mod.rs` to decode the phase operand: source phase rejects with `SyntaxError` per `GetModuleSource()` spec (16.2.1.7.2), defer phase uses standard evaluation semantics. - Removed `import-defer` and `source-phase-imports` from the ignored features list in `test262_config.toml`, bringing `dynamic-import/catch` suite from 112/176 to **176/176 (100% conformance)**.
1 parent ea849b7 commit 3016f49

File tree

9 files changed

+220
-17
lines changed

9 files changed

+220
-17
lines changed

core/ast/src/expression/call.rs

Lines changed: 48 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -182,6 +182,32 @@ impl VisitWith for SuperCall {
182182
}
183183
}
184184

185+
/// The phase of a dynamic import call.
186+
///
187+
/// Determines how the imported module is handled:
188+
/// - `Evaluation` (default): `import(specifier)` — loads, links, and evaluates the module.
189+
/// - `Defer`: `import.defer(specifier)` — deferred evaluation of the module.
190+
/// - `Source`: `import.source(specifier)` — source phase import.
191+
///
192+
/// More information:
193+
/// - [import-defer proposal][defer]
194+
/// - [source-phase-imports proposal][source]
195+
///
196+
/// [defer]: https://github.com/tc39/proposal-defer-import-eval
197+
/// [source]: https://github.com/tc39/proposal-source-phase-imports
198+
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
199+
#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
200+
#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
201+
pub enum ImportPhase {
202+
/// `import(specifier)` — standard dynamic import.
203+
#[default]
204+
Evaluation,
205+
/// `import.defer(specifier)` — deferred import evaluation.
206+
Defer,
207+
/// `import.source(specifier)` — source phase import.
208+
Source,
209+
}
210+
185211
/// The `import()` syntax, commonly called dynamic import, is a function-like expression that allows
186212
/// loading an ECMAScript module asynchronously and dynamically into a potentially non-module
187213
/// environment.
@@ -198,20 +224,22 @@ impl VisitWith for SuperCall {
198224
pub struct ImportCall {
199225
specifier: Box<Expression>,
200226
options: Option<Box<Expression>>,
227+
phase: ImportPhase,
201228
span: Span,
202229
}
203230

204231
impl ImportCall {
205232
/// Creates a new `ImportCall` AST node.
206233
#[inline]
207234
#[must_use]
208-
pub fn new<S>(specifier: S, options: Option<Expression>, span: Span) -> Self
235+
pub fn new<S>(specifier: S, options: Option<Expression>, phase: ImportPhase, span: Span) -> Self
209236
where
210237
S: Into<Expression>,
211238
{
212239
Self {
213240
specifier: Box::new(specifier.into()),
214241
options: options.map(Box::new),
242+
phase,
215243
span,
216244
}
217245
}
@@ -235,6 +263,13 @@ impl ImportCall {
235263
self.options.as_deref()
236264
}
237265

266+
/// Returns the phase of this import call.
267+
#[inline]
268+
#[must_use]
269+
pub const fn phase(&self) -> ImportPhase {
270+
self.phase
271+
}
272+
238273
/// Gets the module specifier of the import call.
239274
///
240275
/// This is an alias for [`Self::specifier`] for backwards compatibility.
@@ -256,14 +291,24 @@ impl Spanned for ImportCall {
256291
impl ToInternedString for ImportCall {
257292
#[inline]
258293
fn to_interned_string(&self, interner: &Interner) -> String {
294+
let phase_str = match self.phase {
295+
ImportPhase::Evaluation => "",
296+
ImportPhase::Defer => ".defer",
297+
ImportPhase::Source => ".source",
298+
};
259299
if let Some(options) = &self.options {
260300
format!(
261-
"import({}, {})",
301+
"import{}({}, {})",
302+
phase_str,
262303
self.specifier.to_interned_string(interner),
263304
options.to_interned_string(interner)
264305
)
265306
} else {
266-
format!("import({})", self.specifier.to_interned_string(interner))
307+
format!(
308+
"import{}({})",
309+
phase_str,
310+
self.specifier.to_interned_string(interner)
311+
)
267312
}
268313
}
269314
}

core/ast/src/expression/mod.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ use crate::{
4545
visitor::{VisitWith, Visitor, VisitorMut},
4646
};
4747
pub use r#await::Await;
48-
pub use call::{Call, ImportCall, SuperCall};
48+
pub use call::{Call, ImportCall, ImportPhase, SuperCall};
4949
pub use identifier::{Identifier, RESERVED_IDENTIFIERS_STRICT};
5050
pub use import_meta::ImportMeta;
5151
pub use new::New;

core/engine/src/bytecompiler/expression/mod.rs

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ use crate::{
1414
use boa_ast::{
1515
Expression,
1616
expression::{
17+
ImportPhase,
1718
access::{PropertyAccess, PropertyAccessField},
1819
literal::{
1920
Literal as AstLiteral, LiteralKind as AstLiteralKind, TemplateElement, TemplateLiteral,
@@ -386,8 +387,14 @@ impl ByteCompiler<'_> {
386387
} else {
387388
self.bytecode.emit_store_undefined(options.variable());
388389
}
390+
391+
let phase: u32 = match import.phase() {
392+
ImportPhase::Evaluation => 0,
393+
ImportPhase::Defer => 1,
394+
ImportPhase::Source => 2,
395+
};
389396
self.bytecode
390-
.emit_import_call(dst.variable(), options.variable());
397+
.emit_import_call(dst.variable(), options.variable(), phase.into());
391398
self.register_allocator.dealloc(options);
392399
}
393400
Expression::NewTarget(_new_target) => {

core/engine/src/vm/code_block.rs

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -763,8 +763,18 @@ impl CodeBlock {
763763
| Instruction::BitNot { value } => {
764764
format!("value:{value}")
765765
}
766-
Instruction::ImportCall { specifier, options } => {
767-
format!("specifier:{specifier}, options:{options}")
766+
Instruction::ImportCall {
767+
specifier,
768+
options,
769+
phase,
770+
} => {
771+
let phase_str = match u32::from(*phase) {
772+
0 => "evaluation",
773+
1 => "defer",
774+
2 => "source",
775+
_ => "unknown",
776+
};
777+
format!("specifier:{specifier}, options:{options}, phase:{phase_str}")
768778
}
769779
Instruction::PushClassField {
770780
object,

core/engine/src/vm/opcode/call/mod.rs

Lines changed: 35 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -345,6 +345,7 @@ async fn load_dyn_import(
345345
referrer: Referrer,
346346
request: ModuleRequest,
347347
cap: PromiseCapability,
348+
phase: u32,
348349
context: &RefCell<&mut Context>,
349350
) -> JsResult<()> {
350351
let loader = context.borrow().module_loader();
@@ -411,6 +412,36 @@ async fn load_dyn_import(
411412
}
412413
}
413414

415+
// When the `experimental` feature is disabled, reject any non-evaluation phase.
416+
#[cfg(not(feature = "experimental"))]
417+
if phase != 0 {
418+
let err = JsNativeError::syntax()
419+
.with_message("import.defer() and import.source() require the 'experimental' feature")
420+
.into();
421+
let err = JsError::into_opaque(err, &mut context.borrow_mut())?;
422+
cap.reject()
423+
.call(&JsValue::undefined(), &[err], &mut context.borrow_mut())
424+
.expect("default `reject` function cannot throw");
425+
return Ok(());
426+
}
427+
428+
// TODO: For source phase (phase == 2), implement GetModuleSource()
429+
// 16.2.1.7.2 GetModuleSource ( )
430+
// Source Text Module Record provides a GetModuleSource implementation
431+
// that always returns an abrupt completion indicating that a source phase import is not available.
432+
// 1. Throw a SyntaxError exception.
433+
#[cfg(feature = "experimental")]
434+
if phase == 2 {
435+
let err = JsNativeError::syntax()
436+
.with_message("source phase import is not available for this module")
437+
.into();
438+
let err = JsError::into_opaque(err, &mut context.borrow_mut())?;
439+
cap.reject()
440+
.call(&JsValue::undefined(), &[err], &mut context.borrow_mut())
441+
.expect("default `reject` function cannot throw");
442+
return Ok(());
443+
}
444+
414445
// 2. Let module be moduleCompletion.[[Value]].
415446
// 3. Let loadPromise be module.LoadRequestedModules().
416447
let load = module.load(&mut context.borrow_mut());
@@ -517,13 +548,15 @@ pub(crate) struct ImportCall;
517548
impl ImportCall {
518549
#[inline(always)]
519550
pub(super) fn operation(
520-
(specifier_op, options_op): (RegisterOperand, RegisterOperand),
551+
(specifier_op, options_op, phase_op): (RegisterOperand, RegisterOperand, IndexOperand),
521552
context: &mut Context,
522553
) -> JsResult<()> {
523554
// Import Calls
524555
// Runtime Semantics: Evaluation
525556
// https://tc39.es/ecma262/#sec-import-call-runtime-semantics-evaluation
526557

558+
let phase: u32 = phase_op.into();
559+
527560
// 1. Let referrer be GetActiveScriptOrModule().
528561
// 2. If referrer is null, set referrer to the current Realm Record.
529562
let referrer = context
@@ -570,7 +603,7 @@ impl ImportCall {
570603
// 8. Perform HostLoadImportedModule(referrer, specifierString, empty, promiseCapability).
571604
let job = NativeAsyncJob::with_realm(
572605
async move |context| {
573-
load_dyn_import(referrer, request, cap, context).await?;
606+
load_dyn_import(referrer, request, cap, phase, context).await?;
574607
Ok(JsValue::undefined())
575608
},
576609
context.realm().clone(),

core/engine/src/vm/opcode/mod.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1790,10 +1790,12 @@ generate_opcodes! {
17901790

17911791
/// Dynamically import a module.
17921792
///
1793+
/// - Operands:
1794+
/// - phase: `IndexOperand` (0 = evaluation, 1 = defer, 2 = source)
17931795
/// - Registers:
17941796
/// - Input: specifier, options
17951797
/// - Output: specifier
1796-
ImportCall { specifier: RegisterOperand, options: RegisterOperand },
1798+
ImportCall { specifier: RegisterOperand, options: RegisterOperand, phase: IndexOperand },
17971799

17981800
/// Strict equal compare two register values,
17991801
/// if true jumps to address.

core/interner/src/sym.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,8 @@ static_syms! {
150150
"await",
151151
("*default*", DEFAULT_EXPORT),
152152
"meta",
153+
"defer",
154+
"source",
153155
"using",
154156
"dispose",
155157
"asyncDispose"

core/parser/src/parser/expression/left_hand_side/mod.rs

Lines changed: 101 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -35,9 +35,9 @@ use crate::{
3535
};
3636
use boa_ast::{
3737
Keyword, Position, Punctuator, Span, Spanned,
38-
expression::{ImportCall, SuperCall},
38+
expression::{ImportCall, ImportPhase, SuperCall},
3939
};
40-
use boa_interner::Interner;
40+
use boa_interner::{Interner, Sym};
4141

4242
/// Parses a left hand side expression.
4343
///
@@ -112,6 +112,43 @@ where
112112
Ok(None)
113113
}
114114

115+
/// Checks if the next tokens form an `import.defer(` or `import.source(` pattern.
116+
/// Returns `Some((position, phase))` if matched, `None` otherwise.
117+
fn is_import_phase_call<R: ReadChar>(
118+
cursor: &mut Cursor<R>,
119+
interner: &mut Interner,
120+
) -> ParseResult<Option<(Position, ImportPhase)>> {
121+
if let Some(next) = cursor.peek(0, interner)?
122+
&& let TokenKind::Keyword((Keyword::Import, escaped)) = next.kind()
123+
{
124+
let keyword_token_start = next.span().start();
125+
if *escaped {
126+
return Err(Error::general(
127+
"keyword `import` cannot contain escaped characters",
128+
keyword_token_start,
129+
));
130+
}
131+
if let Some(dot) = cursor.peek(1, interner)?
132+
&& dot.kind() == &TokenKind::Punctuator(Punctuator::Dot)
133+
&& let Some(ident_tok) = cursor.peek(2, interner)?
134+
&& let TokenKind::IdentifierName((sym, _)) = ident_tok.kind()
135+
{
136+
let phase = match *sym {
137+
Sym::DEFER => Some(ImportPhase::Defer),
138+
Sym::SOURCE => Some(ImportPhase::Source),
139+
_ => None,
140+
};
141+
if let Some(phase) = phase
142+
&& let Some(paren) = cursor.peek(3, interner)?
143+
&& paren.kind() == &TokenKind::Punctuator(Punctuator::OpenParen)
144+
{
145+
return Ok(Some((keyword_token_start, phase)));
146+
}
147+
}
148+
}
149+
Ok(None)
150+
}
151+
115152
cursor.set_goal(InputElement::TemplateTail);
116153

117154
let mut lhs: FormalParameterListOrExpression =
@@ -121,7 +158,7 @@ where
121158
Arguments::new(self.allow_yield, self.allow_await).parse(cursor, interner)?;
122159
SuperCall::new(args, Span::new(start, args_span.end())).into()
123160
} else if let Some(start) = is_keyword_call(Keyword::Import, cursor, interner)? {
124-
// `import`
161+
// Plain `import(...)` call
125162
cursor.advance(interner);
126163
// `(`
127164
cursor.advance(interner);
@@ -165,7 +202,67 @@ where
165202
CallExpressionTail::new(
166203
self.allow_yield,
167204
self.allow_await,
168-
ImportCall::new(specifier, options, Span::new(start, end)).into(),
205+
ImportCall::new(
206+
specifier,
207+
options,
208+
ImportPhase::Evaluation,
209+
Span::new(start, end),
210+
)
211+
.into(),
212+
)
213+
.parse(cursor, interner)?
214+
.into()
215+
} else if let Some((start, phase)) = is_import_phase_call(cursor, interner)? {
216+
// `import.defer(...)` or `import.source(...)` call
217+
// Consume `import`
218+
cursor.advance(interner);
219+
// Consume `.`
220+
cursor.advance(interner);
221+
// Consume `defer` or `source`
222+
cursor.advance(interner);
223+
// Consume `(`
224+
cursor.advance(interner);
225+
226+
let specifier = AssignmentExpression::new(true, self.allow_yield, self.allow_await)
227+
.parse(cursor, interner)?;
228+
229+
let options =
230+
if cursor
231+
.next_if(TokenKind::Punctuator(Punctuator::Comma), interner)?
232+
.is_some()
233+
{
234+
if cursor.peek(0, interner)?.is_some_and(|t| {
235+
t.kind() == &TokenKind::Punctuator(Punctuator::CloseParen)
236+
}) {
237+
None
238+
} else {
239+
let opts =
240+
AssignmentExpression::new(true, self.allow_yield, self.allow_await)
241+
.parse(cursor, interner)?;
242+
if cursor.peek(0, interner)?.is_some_and(|t| {
243+
t.kind() == &TokenKind::Punctuator(Punctuator::Comma)
244+
}) {
245+
cursor.advance(interner);
246+
}
247+
Some(opts)
248+
}
249+
} else {
250+
None
251+
};
252+
253+
let end = cursor
254+
.expect(
255+
TokenKind::Punctuator(Punctuator::CloseParen),
256+
"import call",
257+
interner,
258+
)?
259+
.span()
260+
.end();
261+
262+
CallExpressionTail::new(
263+
self.allow_yield,
264+
self.allow_await,
265+
ImportCall::new(specifier, options, phase, Span::new(start, end)).into(),
169266
)
170267
.parse(cursor, interner)?
171268
.into()

0 commit comments

Comments
 (0)