diff --git a/boa_ast/src/expression/mod.rs b/boa_ast/src/expression/mod.rs index 46462c7d910..56f1513b2fa 100644 --- a/boa_ast/src/expression/mod.rs +++ b/boa_ast/src/expression/mod.rs @@ -133,7 +133,9 @@ pub enum Expression { /// The `new.target` pseudo-property expression. NewTarget, - // TODO: import.meta + /// The `import.meta` pseudo-property expression. + ImportMeta, + /// See [`Assign`]. Assign(Assign), @@ -197,6 +199,7 @@ impl Expression { Self::ImportCall(impc) => impc.to_interned_string(interner), Self::Optional(opt) => opt.to_interned_string(interner), Self::NewTarget => "new.target".to_owned(), + Self::ImportMeta => "import.meta".to_owned(), Self::TaggedTemplate(tag) => tag.to_interned_string(interner), Self::Assign(assign) => assign.to_interned_string(interner), Self::Unary(unary) => unary.to_interned_string(interner), @@ -316,7 +319,7 @@ impl VisitWith for Expression { Self::Yield(y) => visitor.visit_yield(y), Self::Parenthesized(e) => visitor.visit_parenthesized(e), Self::FormalParameterList(fpl) => visitor.visit_formal_parameter_list(fpl), - Self::This | Self::NewTarget => { + Self::This | Self::NewTarget | Self::ImportMeta => { // do nothing; can be handled as special case by visitor ControlFlow::Continue(()) } @@ -358,7 +361,7 @@ impl VisitWith for Expression { Self::Yield(y) => visitor.visit_yield_mut(y), Self::Parenthesized(e) => visitor.visit_parenthesized_mut(e), Self::FormalParameterList(fpl) => visitor.visit_formal_parameter_list_mut(fpl), - Self::This | Self::NewTarget => { + Self::This | Self::NewTarget | Self::ImportMeta => { // do nothing; can be handled as special case by visitor ControlFlow::Continue(()) } diff --git a/boa_engine/src/bytecompiler/expression/mod.rs b/boa_engine/src/bytecompiler/expression/mod.rs index a9a8106a6f7..88a9ccf74be 100644 --- a/boa_engine/src/bytecompiler/expression/mod.rs +++ b/boa_engine/src/bytecompiler/expression/mod.rs @@ -329,7 +329,12 @@ impl ByteCompiler<'_, '_> { } Expression::NewTarget => { if use_expr { - self.emit_opcode(Opcode::PushNewTarget); + self.emit_opcode(Opcode::NewTarget); + } + } + Expression::ImportMeta => { + if use_expr { + self.emit_opcode(Opcode::ImportMeta); } } Expression::Optional(opt) => { diff --git a/boa_engine/src/module/mod.rs b/boa_engine/src/module/mod.rs index 6db077ffa44..01a5914b151 100644 --- a/boa_engine/src/module/mod.rs +++ b/boa_engine/src/module/mod.rs @@ -122,8 +122,8 @@ pub trait ModuleLoader { /// [final]: https://tc39.es/ecma262/#sec-hostfinalizeimportmeta fn init_import_meta( &self, - _import_meta: JsObject, - _module: Module, + _import_meta: &JsObject, + _module: &Module, _context: &mut Context<'_>, ) { } diff --git a/boa_engine/src/module/source.rs b/boa_engine/src/module/source.rs index 8f517d778d2..3461b300e59 100644 --- a/boa_engine/src/module/source.rs +++ b/boa_engine/src/module/source.rs @@ -1769,6 +1769,11 @@ impl SourceTextModule { pub(crate) fn loaded_modules(&self) -> &GcRefCell> { &self.inner.loaded_modules } + + /// Gets the import meta object of this module. + pub(crate) fn import_meta(&self) -> &GcRefCell> { + &self.inner.import_meta + } } /// Abstract operation [`AsyncModuleExecutionFulfilled ( module )`][spec]. diff --git a/boa_engine/src/vm/code_block.rs b/boa_engine/src/vm/code_block.rs index 30d79d1791b..e486cec6d60 100644 --- a/boa_engine/src/vm/code_block.rs +++ b/boa_engine/src/vm/code_block.rs @@ -500,7 +500,8 @@ impl CodeBlock { | Opcode::PushClassField | Opcode::SuperCallDerived | Opcode::Await - | Opcode::PushNewTarget + | Opcode::NewTarget + | Opcode::ImportMeta | Opcode::SuperCallPrepare | Opcode::CallEvalSpread | Opcode::CallSpread diff --git a/boa_engine/src/vm/flowgraph/mod.rs b/boa_engine/src/vm/flowgraph/mod.rs index 3fd578db329..c9e0a5cde32 100644 --- a/boa_engine/src/vm/flowgraph/mod.rs +++ b/boa_engine/src/vm/flowgraph/mod.rs @@ -584,7 +584,8 @@ impl CodeBlock { | Opcode::PushClassField | Opcode::SuperCallDerived | Opcode::Await - | Opcode::PushNewTarget + | Opcode::NewTarget + | Opcode::ImportMeta | Opcode::CallEvalSpread | Opcode::CallSpread | Opcode::NewSpread diff --git a/boa_engine/src/vm/opcode/meta/mod.rs b/boa_engine/src/vm/opcode/meta/mod.rs new file mode 100644 index 00000000000..b097eb7978b --- /dev/null +++ b/boa_engine/src/vm/opcode/meta/mod.rs @@ -0,0 +1,95 @@ +use std::unreachable; + +use crate::{ + module::ModuleKind, + vm::{opcode::Operation, ActiveRunnable, CompletionType}, + Context, JsObject, JsResult, JsValue, +}; + +/// `NewTarget` implements the Opcode Operation for `Opcode::NewTarget` +/// +/// Operation: +/// - Push the current new target to the stack. +#[derive(Debug, Clone, Copy)] +pub(crate) struct NewTarget; + +impl Operation for NewTarget { + const NAME: &'static str = "NewTarget"; + const INSTRUCTION: &'static str = "INST - NewTarget"; + + fn execute(context: &mut Context<'_>) -> JsResult { + let new_target = if let Some(new_target) = context + .vm + .environments + .get_this_environment() + .as_function() + .and_then(|env| env.slots().new_target().cloned()) + { + new_target.into() + } else { + JsValue::undefined() + }; + context.vm.push(new_target); + Ok(CompletionType::Normal) + } +} + +/// `ImportMeta` implements the Opcode Operation for `Opcode::ImportMeta` +/// +/// Operation: +/// - Push the current `import.meta` to the stack +#[derive(Debug, Clone, Copy)] +pub(crate) struct ImportMeta; + +impl Operation for ImportMeta { + const NAME: &'static str = "ImportMeta"; + const INSTRUCTION: &'static str = "INST - ImportMeta"; + + fn execute(context: &mut Context<'_>) -> JsResult { + // Meta Properties + // + // ImportMeta : import . meta + // + // https://tc39.es/ecma262/#sec-meta-properties + + // 1. Let module be GetActiveScriptOrModule(). + + let Some(ActiveRunnable::Module(module)) = context.vm.active_runnable.clone() else { + unreachable!("2. Assert: module is a Source Text Module Record."); + }; + + let ModuleKind::SourceText(src) = module.kind() else { + unreachable!("2. Assert: module is a Source Text Module Record."); + }; + + // 3. Let importMeta be module.[[ImportMeta]]. + // 4. If importMeta is empty, then + // 5. Else, + // a. Assert: importMeta is an Object. + let import_meta = src + .import_meta() + .borrow_mut() + .get_or_insert_with(|| { + // a. Set importMeta to OrdinaryObjectCreate(null). + let import_meta = JsObject::with_null_proto(); + + // b. Let importMetaValues be HostGetImportMetaProperties(module). + // c. For each Record { [[Key]], [[Value]] } p of importMetaValues, do + // i. Perform ! CreateDataPropertyOrThrow(importMeta, p.[[Key]], p.[[Value]]). + // d. Perform HostFinalizeImportMeta(importMeta, module). + context + .module_loader() + .init_import_meta(&import_meta, &module, context); + + // e. Set module.[[ImportMeta]] to importMeta. + import_meta + }) + .clone(); + + // b. Return importMeta. + // f. Return importMeta. + context.vm.push(import_meta); + + Ok(CompletionType::Normal) + } +} diff --git a/boa_engine/src/vm/opcode/mod.rs b/boa_engine/src/vm/opcode/mod.rs index 41bc77a6c57..41f3602dd5d 100644 --- a/boa_engine/src/vm/opcode/mod.rs +++ b/boa_engine/src/vm/opcode/mod.rs @@ -18,6 +18,7 @@ mod generator; mod get; mod iteration; mod jump; +mod meta; mod new; mod nop; mod pop; @@ -62,6 +63,8 @@ pub(crate) use iteration::*; #[doc(inline)] pub(crate) use jump::*; #[doc(inline)] +pub(crate) use meta::*; +#[doc(inline)] pub(crate) use new::*; #[doc(inline)] pub(crate) use nop::*; @@ -1602,8 +1605,15 @@ generate_impl! { /// /// Operands: /// - /// Stack: **=>** new_target - PushNewTarget, + /// Stack: **=>** `new.target` + NewTarget, + + /// Push the current `import.meta` to the stack. + /// + /// Operands: + /// + /// Stack: **=>** `import.meta` + ImportMeta, /// Pushes `true` to the stack if the top stack value is an object, or `false` otherwise. /// diff --git a/boa_engine/src/vm/opcode/push/mod.rs b/boa_engine/src/vm/opcode/push/mod.rs index b2694a79655..32451aad00d 100644 --- a/boa_engine/src/vm/opcode/push/mod.rs +++ b/boa_engine/src/vm/opcode/push/mod.rs @@ -7,7 +7,6 @@ pub(crate) mod array; pub(crate) mod class; pub(crate) mod environment; pub(crate) mod literal; -pub(crate) mod new_target; pub(crate) mod numbers; pub(crate) mod object; @@ -15,7 +14,6 @@ pub(crate) use array::*; pub(crate) use class::*; pub(crate) use environment::*; pub(crate) use literal::*; -pub(crate) use new_target::*; pub(crate) use numbers::*; pub(crate) use object::*; diff --git a/boa_engine/src/vm/opcode/push/new_target.rs b/boa_engine/src/vm/opcode/push/new_target.rs deleted file mode 100644 index 24cd3e23447..00000000000 --- a/boa_engine/src/vm/opcode/push/new_target.rs +++ /dev/null @@ -1,32 +0,0 @@ -use crate::{ - vm::{opcode::Operation, CompletionType}, - Context, JsResult, JsValue, -}; - -/// `PushNewTarget` implements the Opcode Operation for `Opcode::PushNewTarget` -/// -/// Operation: -/// - Push the current new target to the stack. -#[derive(Debug, Clone, Copy)] -pub(crate) struct PushNewTarget; - -impl Operation for PushNewTarget { - const NAME: &'static str = "PushNewTarget"; - const INSTRUCTION: &'static str = "INST - PushNewTarget"; - - fn execute(context: &mut Context<'_>) -> JsResult { - let new_target = if let Some(new_target) = context - .vm - .environments - .get_this_environment() - .as_function() - .and_then(|env| env.slots().new_target().cloned()) - { - new_target.into() - } else { - JsValue::undefined() - }; - context.vm.push(new_target); - Ok(CompletionType::Normal) - } -} diff --git a/boa_interner/src/sym.rs b/boa_interner/src/sym.rs index 3129f60c3a5..7f30598ca5f 100644 --- a/boa_interner/src/sym.rs +++ b/boa_interner/src/sym.rs @@ -148,5 +148,6 @@ static_syms! { "__proto__", "name", "await", - ("*default*", DEFAULT_EXPORT) + ("*default*", DEFAULT_EXPORT), + "meta" } diff --git a/boa_parser/src/parser/expression/left_hand_side/member.rs b/boa_parser/src/parser/expression/left_hand_side/member.rs index 206c28fce8d..e58db2dc892 100644 --- a/boa_parser/src/parser/expression/left_hand_side/member.rs +++ b/boa_parser/src/parser/expression/left_hand_side/member.rs @@ -72,13 +72,53 @@ where cursor.set_goal(InputElement::RegExp); let token = cursor.peek(0, interner).or_abrupt()?; + let position = token.span().start(); let mut lhs = match token.kind() { - TokenKind::Keyword((Keyword::New | Keyword::Super, true)) => { + TokenKind::Keyword((Keyword::New | Keyword::Super | Keyword::Import, true)) => { return Err(Error::general( "keyword must not contain escaped characters", token.span().start(), )); } + TokenKind::Keyword((Keyword::Import, false)) => { + cursor.advance(interner); + + cursor.expect( + TokenKind::Punctuator(Punctuator::Dot), + "import.meta", + interner, + )?; + + let token = cursor.next(interner).or_abrupt()?; + + match token.kind() { + TokenKind::IdentifierName((Sym::META, ContainsEscapeSequence(ces))) => { + if *ces { + return Err(Error::general( + "`import.meta` cannot contain escaped characters", + token.span().start(), + )); + } + } + _ => { + return Err(Error::expected( + ["property `meta`".into()], + token.to_string(interner), + token.span(), + "import.meta", + )); + } + } + + if !cursor.module() { + return Err(Error::general( + "invalid `import.meta` expression outside a module", + position, + )); + } + + ast::Expression::ImportMeta + } TokenKind::Keyword((Keyword::New, false)) => { cursor.advance(interner); diff --git a/boa_parser/src/parser/expression/left_hand_side/mod.rs b/boa_parser/src/parser/expression/left_hand_side/mod.rs index 6b6c95c3ee3..f31d88737a9 100644 --- a/boa_parser/src/parser/expression/left_hand_side/mod.rs +++ b/boa_parser/src/parser/expression/left_hand_side/mod.rs @@ -28,7 +28,7 @@ use crate::{ }, AssignmentExpression, }, - AllowAwait, AllowYield, Cursor, OrAbrupt, ParseResult, TokenParser, + AllowAwait, AllowYield, Cursor, ParseResult, TokenParser, }, Error, }; @@ -78,24 +78,36 @@ where type Output = Expression; fn parse(self, cursor: &mut Cursor, interner: &mut Interner) -> ParseResult { - /// Checks if we need to parse a super call expression `super()`. + /// Checks if we need to parse a keyword call expression `keyword()`. /// - /// It first checks if the next token is `super`, and if it is, it checks if the second next + /// It first checks if the next token is `keyword`, and if it is, it checks if the second next /// token is the open parenthesis (`(`) punctuator. /// /// This is needed because the `if let` chain is very complex, and putting it inline in the /// initialization of `lhs` would make it very hard to return an expression over all /// possible branches of the `if let`s. Instead, we extract the check into its own function, /// then use it inside the condition of a simple `if ... else` expression. - fn is_super_call( + fn is_keyword_call( + keyword: Keyword, cursor: &mut Cursor, interner: &mut Interner, ) -> ParseResult { if let Some(next) = cursor.peek(0, interner)? { - if let TokenKind::Keyword((Keyword::Super, _)) = next.kind() { - if let Some(next) = cursor.peek(1, interner)? { - if next.kind() == &TokenKind::Punctuator(Punctuator::OpenParen) { - return Ok(true); + if let TokenKind::Keyword((kw, escaped)) = next.kind() { + if kw == &keyword { + if *escaped { + return Err(Error::general( + format!( + "keyword `{}` cannot contain escaped characters", + kw.as_str().0 + ), + next.span().start(), + )); + } + if let Some(next) = cursor.peek(1, interner)? { + if next.kind() == &TokenKind::Punctuator(Punctuator::OpenParen) { + return Ok(true); + } } } } @@ -107,54 +119,42 @@ where cursor.set_goal(InputElement::TemplateTail); - let mut lhs = if is_super_call(cursor, interner)? { + let mut lhs = if is_keyword_call(Keyword::Super, cursor, interner)? { cursor.advance(interner); let args = Arguments::new(self.allow_yield, self.allow_await).parse(cursor, interner)?; SuperCall::new(args).into() - } else { - let next = cursor.peek(0, interner).or_abrupt()?; - if let TokenKind::Keyword((Keyword::Import, escaped)) = next.kind() { - if *escaped { - return Err(Error::general( - "keyword `import` must not contain escaped characters", - next.span().start(), - )); - } - cursor.advance(interner); - cursor.expect( - TokenKind::Punctuator(Punctuator::OpenParen), - "import call", - interner, - )?; + } else if is_keyword_call(Keyword::Import, cursor, interner)? { + // `import` + cursor.advance(interner); + // `(` + cursor.advance(interner); - let arg = AssignmentExpression::new(None, true, self.allow_yield, self.allow_await) - .parse(cursor, interner)?; + let arg = AssignmentExpression::new(None, true, self.allow_yield, self.allow_await) + .parse(cursor, interner)?; - cursor.expect( - TokenKind::Punctuator(Punctuator::CloseParen), - "import call", - interner, - )?; + cursor.expect( + TokenKind::Punctuator(Punctuator::CloseParen), + "import call", + interner, + )?; - CallExpressionTail::new( - self.allow_yield, - self.allow_await, - ImportCall::new(arg).into(), - ) - .parse(cursor, interner)? - } else { - let mut member = - MemberExpression::new(self.name, self.allow_yield, self.allow_await) + CallExpressionTail::new( + self.allow_yield, + self.allow_await, + ImportCall::new(arg).into(), + ) + .parse(cursor, interner)? + } else { + let mut member = MemberExpression::new(self.name, self.allow_yield, self.allow_await) + .parse(cursor, interner)?; + if let Some(tok) = cursor.peek(0, interner)? { + if tok.kind() == &TokenKind::Punctuator(Punctuator::OpenParen) { + member = CallExpression::new(self.allow_yield, self.allow_await, member) .parse(cursor, interner)?; - if let Some(tok) = cursor.peek(0, interner)? { - if tok.kind() == &TokenKind::Punctuator(Punctuator::OpenParen) { - member = CallExpression::new(self.allow_yield, self.allow_await, member) - .parse(cursor, interner)?; - } } - member } + member }; if let Some(tok) = cursor.peek(0, interner)? {