From 06161edcbd0a682c915a9d3af535f0b1d05017ec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Juli=C3=A1n=20Espina?= Date: Thu, 18 May 2023 00:58:22 +0000 Subject: [PATCH] Implement module execution (#2922) * Implement draft of module execution * Fix recursion bug * Re-enable JsObject's debug printing * Modify API and document some methods * Add missing documentation * Add newline to module scripts * npx prettier * Apply reviews * Add reference to parent struct on source module * Document more steps on the example * cargo clippy * Revert `BoaGc` changes * Lower `GcRefCell` to `Inner` of `SourceTextModule` * Replace weak ref to module with strong ref * Apply review * Clarify reasoning of manual `Trace` impl * Apply review pt. 2 * Revert gc changes --- .gitignore | 4 +- Cargo.lock | 1 + boa_ast/src/declaration/export.rs | 114 + boa_ast/src/declaration/import.rs | 49 + boa_ast/src/module_item_list/mod.rs | 228 +- boa_cli/src/main.rs | 50 +- boa_engine/src/builtins/json/mod.rs | 14 +- boa_engine/src/builtins/promise/mod.rs | 13 +- boa_engine/src/bytecompiler/mod.rs | 2 +- boa_engine/src/bytecompiler/module.rs | 52 +- boa_engine/src/context/intrinsics.rs | 20 +- boa_engine/src/context/mod.rs | 199 +- .../environments/runtime/declarative/mod.rs | 23 +- .../runtime/declarative/module.rs | 120 ++ boa_engine/src/environments/runtime/mod.rs | 22 +- boa_engine/src/lib.rs | 3 + boa_engine/src/module/mod.rs | 737 +++++++ boa_engine/src/module/source.rs | 1913 +++++++++++++++++ .../internal_methods/immutable_prototype.rs | 39 + boa_engine/src/object/internal_methods/mod.rs | 2 + .../internal_methods/module_namespace.rs | 329 +++ boa_engine/src/object/jsobject.rs | 14 +- boa_engine/src/object/mod.rs | 44 + boa_engine/src/realm.rs | 21 +- boa_examples/scripts/modules/operations.mjs | 21 + boa_examples/scripts/modules/trig.mjs | 11 + boa_examples/src/bin/modules.rs | 131 ++ boa_gc/src/cell.rs | 1 + boa_gc/src/pointers/ephemeron.rs | 2 +- boa_gc/src/pointers/gc.rs | 3 +- boa_interner/Cargo.toml | 1 + boa_interner/src/sym.rs | 9 +- .../parser/statement/declaration/export.rs | 6 +- .../parser/statement/declaration/import.rs | 4 +- .../src/parser/statement/declaration/mod.rs | 13 +- boa_parser/src/parser/statement/mod.rs | 27 +- boa_tester/src/exec/mod.rs | 261 ++- 37 files changed, 4239 insertions(+), 264 deletions(-) create mode 100644 boa_engine/src/environments/runtime/declarative/module.rs create mode 100644 boa_engine/src/module/mod.rs create mode 100644 boa_engine/src/module/source.rs create mode 100644 boa_engine/src/object/internal_methods/immutable_prototype.rs create mode 100644 boa_engine/src/object/internal_methods/module_namespace.rs create mode 100644 boa_examples/scripts/modules/operations.mjs create mode 100644 boa_examples/scripts/modules/trig.mjs create mode 100644 boa_examples/src/bin/modules.rs diff --git a/.gitignore b/.gitignore index 56396d4050..0de391828a 100644 --- a/.gitignore +++ b/.gitignore @@ -15,8 +15,8 @@ node_modules yarn-error.log .vscode/settings.json -# tests/js/test.js is used for testing changes locally -tests/js/test.js +# tests/js is used for testing changes locally +tests/js .boa_history # Profiling diff --git a/Cargo.lock b/Cargo.lock index 6f8ba2d27e..4aec6623a3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -492,6 +492,7 @@ name = "boa_interner" version = "0.16.0" dependencies = [ "arbitrary", + "boa_gc", "boa_macros", "hashbrown 0.13.2", "indexmap", diff --git a/boa_ast/src/declaration/export.rs b/boa_ast/src/declaration/export.rs index b174a82b4d..89c4bea40b 100644 --- a/boa_ast/src/declaration/export.rs +++ b/boa_ast/src/declaration/export.rs @@ -13,6 +13,7 @@ use std::ops::ControlFlow; use super::{ModuleSpecifier, VarDeclaration}; use crate::{ + expression::Identifier, function::{AsyncFunction, AsyncGenerator, Class, Function, Generator}, try_break, visitor::{VisitWith, Visitor, VisitorMut}, @@ -213,3 +214,116 @@ impl VisitWith for ExportSpecifier { visitor.visit_sym_mut(&mut self.private_name) } } + +/// The name under which a reexported binding is exported by a module. +/// +/// This differs slightly from the spec, since `[[ImportName]]` can be either a name, `all-but-default` +/// or `all`, but the last two exports can be identified with the `export_name` field from +/// [`ExportEntry`], which joins both variants into a single `Star` variant. +#[derive(Debug, Clone, Copy)] +pub enum ReExportImportName { + /// A binding of the imported module. + Name(Sym), + /// All exports of the module. + Star, +} + +/// [`ExportEntry`][spec] record. +/// +/// [spec]: https://tc39.es/ecma262/#table-exportentry-records +#[derive(Debug, Clone, Copy)] +pub enum ExportEntry { + /// An ordinary export entry + Ordinary(LocalExportEntry), + /// A star reexport entry. + StarReExport { + /// The module from where this reexport will import. + module_request: Sym, + }, + /// A reexport entry with an export name. + ReExport(IndirectExportEntry), +} + +impl From for ExportEntry { + fn from(v: IndirectExportEntry) -> Self { + Self::ReExport(v) + } +} + +impl From for ExportEntry { + fn from(v: LocalExportEntry) -> Self { + Self::Ordinary(v) + } +} + +/// A local export entry +#[derive(Debug, Clone, Copy)] +pub struct LocalExportEntry { + local_name: Identifier, + export_name: Sym, +} + +impl LocalExportEntry { + /// Creates a new `LocalExportEntry`. + #[must_use] + pub const fn new(local_name: Identifier, export_name: Sym) -> Self { + Self { + local_name, + export_name, + } + } + + /// Gets the local name of this export entry. + #[must_use] + pub const fn local_name(&self) -> Identifier { + self.local_name + } + + /// Gets the export name of this export entry. + #[must_use] + pub const fn export_name(&self) -> Sym { + self.export_name + } +} + +/// A reexported export entry. +#[derive(Debug, Clone, Copy)] +pub struct IndirectExportEntry { + module_request: Sym, + import_name: ReExportImportName, + export_name: Sym, +} + +impl IndirectExportEntry { + /// Creates a new `IndirectExportEntry`. + #[must_use] + pub const fn new( + module_request: Sym, + import_name: ReExportImportName, + export_name: Sym, + ) -> Self { + Self { + module_request, + import_name, + export_name, + } + } + + /// Gets the module from where this entry reexports. + #[must_use] + pub const fn module_request(&self) -> Sym { + self.module_request + } + + /// Gets the import name of the reexport. + #[must_use] + pub const fn import_name(&self) -> ReExportImportName { + self.import_name + } + + /// Gets the public alias of the reexport. + #[must_use] + pub const fn export_name(&self) -> Sym { + self.export_name + } +} diff --git a/boa_ast/src/declaration/import.rs b/boa_ast/src/declaration/import.rs index 0420c73839..cdb4b215f6 100644 --- a/boa_ast/src/declaration/import.rs +++ b/boa_ast/src/declaration/import.rs @@ -204,3 +204,52 @@ impl VisitWith for ImportSpecifier { visitor.visit_sym_mut(&mut self.export_name) } } + +/// The name under which the imported binding is exported by a module. +#[derive(Debug, Clone, Copy)] +pub enum ImportName { + /// The namespace object of the imported module. + Namespace, + /// A binding of the imported module. + Name(Sym), +} + +/// [`ImportEntry`][spec] record. +/// +/// [spec]: https://tc39.es/ecma262/#table-importentry-record-fields +#[derive(Debug, Clone, Copy)] +pub struct ImportEntry { + module_request: Sym, + import_name: ImportName, + local_name: Identifier, +} + +impl ImportEntry { + /// Creates a new `ImportEntry`. + #[must_use] + pub const fn new(module_request: Sym, import_name: ImportName, local_name: Identifier) -> Self { + Self { + module_request, + import_name, + local_name, + } + } + + /// Gets the module from where the binding must be imported. + #[must_use] + pub const fn module_request(&self) -> Sym { + self.module_request + } + + /// Gets the import name of the imported binding. + #[must_use] + pub const fn import_name(&self) -> ImportName { + self.import_name + } + + /// Gets the local name of the imported binding. + #[must_use] + pub const fn local_name(&self) -> Identifier { + self.local_name + } +} diff --git a/boa_ast/src/module_item_list/mod.rs b/boa_ast/src/module_item_list/mod.rs index 25542d6192..cbb622b996 100644 --- a/boa_ast/src/module_item_list/mod.rs +++ b/boa_ast/src/module_item_list/mod.rs @@ -11,9 +11,13 @@ use boa_interner::Sym; use rustc_hash::FxHashSet; use crate::{ - declaration::{ExportDeclaration, ExportSpecifier, ImportDeclaration, ReExportKind}, + declaration::{ + ExportDeclaration, ExportEntry, ExportSpecifier, ImportDeclaration, ImportEntry, + ImportKind, ImportName, IndirectExportEntry, LocalExportEntry, ModuleSpecifier, + ReExportImportName, ReExportKind, + }, expression::Identifier, - operations::BoundNamesVisitor, + operations::{bound_names, BoundNamesVisitor}, try_break, visitor::{VisitWith, Visitor, VisitorMut}, StatementListItem, @@ -21,7 +25,7 @@ use crate::{ /// Module item list AST node. /// -/// It contains a list of +/// It contains a list of module items. /// /// More information: /// - [ECMAScript specification][spec] @@ -192,6 +196,224 @@ impl ModuleItemList { names } + + /// Operation [`ModuleRequests`][spec]. + /// + /// Gets the list of modules that need to be fetched by the module resolver to link this module. + /// + /// [spec]: https://tc39.es/ecma262/#sec-static-semantics-modulerequests + #[inline] + #[must_use] + pub fn requests(&self) -> FxHashSet { + #[derive(Debug)] + struct RequestsVisitor<'vec>(&'vec mut FxHashSet); + + impl<'ast> Visitor<'ast> for RequestsVisitor<'_> { + type BreakTy = Infallible; + + fn visit_statement_list_item( + &mut self, + _: &'ast StatementListItem, + ) -> ControlFlow { + ControlFlow::Continue(()) + } + fn visit_module_specifier( + &mut self, + node: &'ast ModuleSpecifier, + ) -> ControlFlow { + self.0.insert(node.sym()); + ControlFlow::Continue(()) + } + } + + let mut requests = FxHashSet::default(); + + RequestsVisitor(&mut requests).visit_module_item_list(self); + + requests + } + + /// Operation [`ImportEntries`][spec]. + /// + /// Gets the list of import entries of this module. + /// + /// [spec]: https://tc39.es/ecma262/#sec-static-semantics-importentries + #[inline] + #[must_use] + pub fn import_entries(&self) -> Vec { + #[derive(Debug)] + struct ImportEntriesVisitor<'vec>(&'vec mut Vec); + + impl<'ast> Visitor<'ast> for ImportEntriesVisitor<'_> { + type BreakTy = Infallible; + + fn visit_module_item(&mut self, node: &'ast ModuleItem) -> ControlFlow { + match node { + ModuleItem::ImportDeclaration(import) => self.visit_import_declaration(import), + ModuleItem::ExportDeclaration(_) | ModuleItem::StatementListItem(_) => { + ControlFlow::Continue(()) + } + } + } + + fn visit_import_declaration( + &mut self, + node: &'ast ImportDeclaration, + ) -> ControlFlow { + let module = node.specifier().sym(); + + if let Some(default) = node.default() { + self.0.push(ImportEntry::new( + module, + ImportName::Name(Sym::DEFAULT), + default, + )); + } + + match node.kind() { + ImportKind::DefaultOrUnnamed => {} + ImportKind::Namespaced { binding } => { + self.0 + .push(ImportEntry::new(module, ImportName::Namespace, *binding)); + } + ImportKind::Named { names } => { + for name in &**names { + self.0.push(ImportEntry::new( + module, + ImportName::Name(name.export_name()), + name.binding(), + )); + } + } + } + + ControlFlow::Continue(()) + } + } + + let mut entries = Vec::default(); + + ImportEntriesVisitor(&mut entries).visit_module_item_list(self); + + entries + } + + /// Operation [`ExportEntries`][spec]. + /// + /// Gets the list of export entries of this module. + /// + /// [spec]: https://tc39.es/ecma262/#sec-static-semantics-exportentries + #[inline] + #[must_use] + pub fn export_entries(&self) -> Vec { + #[derive(Debug)] + struct ExportEntriesVisitor<'vec>(&'vec mut Vec); + + impl<'ast> Visitor<'ast> for ExportEntriesVisitor<'_> { + type BreakTy = Infallible; + + fn visit_module_item(&mut self, node: &'ast ModuleItem) -> ControlFlow { + match node { + ModuleItem::ExportDeclaration(import) => self.visit_export_declaration(import), + ModuleItem::ImportDeclaration(_) | ModuleItem::StatementListItem(_) => { + ControlFlow::Continue(()) + } + } + } + + fn visit_export_declaration( + &mut self, + node: &'ast ExportDeclaration, + ) -> ControlFlow { + let name = match node { + ExportDeclaration::ReExport { kind, specifier } => { + let module = specifier.sym(); + + match kind { + ReExportKind::Namespaced { name } => { + if let Some(name) = *name { + self.0.push( + IndirectExportEntry::new( + module, + ReExportImportName::Star, + name, + ) + .into(), + ); + } else { + self.0.push(ExportEntry::StarReExport { + module_request: module, + }); + } + } + ReExportKind::Named { names } => { + for name in &**names { + self.0.push( + IndirectExportEntry::new( + module, + ReExportImportName::Name(name.private_name()), + name.alias(), + ) + .into(), + ); + } + } + } + + return ControlFlow::Continue(()); + } + ExportDeclaration::List(names) => { + for name in &**names { + self.0.push( + LocalExportEntry::new( + Identifier::from(name.private_name()), + name.alias(), + ) + .into(), + ); + } + return ControlFlow::Continue(()); + } + ExportDeclaration::VarStatement(var) => { + for name in bound_names(var) { + self.0.push(LocalExportEntry::new(name, name.sym()).into()); + } + return ControlFlow::Continue(()); + } + ExportDeclaration::Declaration(decl) => { + for name in bound_names(decl) { + self.0.push(LocalExportEntry::new(name, name.sym()).into()); + } + return ControlFlow::Continue(()); + } + ExportDeclaration::DefaultFunction(f) => f.name(), + ExportDeclaration::DefaultGenerator(g) => g.name(), + ExportDeclaration::DefaultAsyncFunction(af) => af.name(), + ExportDeclaration::DefaultAsyncGenerator(ag) => ag.name(), + ExportDeclaration::DefaultClassDeclaration(c) => c.name(), + ExportDeclaration::DefaultAssignmentExpression(_) => { + Some(Identifier::from(Sym::DEFAULT_EXPORT)) + } + }; + + self.0.push( + LocalExportEntry::new( + name.unwrap_or_else(|| Identifier::from(Sym::DEFAULT_EXPORT)), + Sym::DEFAULT, + ) + .into(), + ); + + ControlFlow::Continue(()) + } + } + + let mut entries = Vec::default(); + + ExportEntriesVisitor(&mut entries).visit_module_item_list(self); + + entries + } } impl From for ModuleItemList diff --git a/boa_cli/src/main.rs b/boa_cli/src/main.rs index b5be81642a..e7e7210c7a 100644 --- a/boa_cli/src/main.rs +++ b/boa_cli/src/main.rs @@ -64,12 +64,14 @@ mod helper; use boa_ast::StatementList; use boa_engine::{ + builtins::promise::PromiseState, context::ContextBuilder, job::{FutureJob, JobQueue, NativeJob}, + module::{Module, ModuleLoader, SimpleModuleLoader}, optimizer::OptimizerOptions, property::Attribute, vm::flowgraph::{Direction, Graph}, - Context, JsResult, Source, + Context, JsNativeError, JsResult, Source, }; use boa_runtime::Console; use clap::{Parser, ValueEnum, ValueHint}; @@ -155,6 +157,14 @@ struct Opt { /// Inject debugging object `$boa`. #[arg(long)] debug_object: bool, + + /// Treats the input files as modules. + #[arg(long, short = 'm', group = "mod")] + module: bool, + + /// Root path from where the module resolver will try to load the modules. + #[arg(long, short = 'r', default_value_os_t = PathBuf::from("."), requires = "mod")] + root: PathBuf, } impl Opt { @@ -272,7 +282,11 @@ fn generate_flowgraph( Ok(result) } -fn evaluate_files(args: &Opt, context: &mut Context<'_>) -> Result<(), io::Error> { +fn evaluate_files( + args: &Opt, + context: &mut Context<'_>, + loader: &SimpleModuleLoader, +) -> Result<(), io::Error> { for file in &args.files { let buffer = read(file)?; @@ -290,6 +304,32 @@ fn evaluate_files(args: &Opt, context: &mut Context<'_>) -> Result<(), io::Error Ok(v) => println!("{v}"), Err(v) => eprintln!("Uncaught {v}"), } + } else if args.module { + let result = (|| { + let module = Module::parse(Source::from_bytes(&buffer), None, context)?; + + loader.insert( + file.canonicalize() + .map_err(|e| JsNativeError::typ().with_message(e.to_string()))?, + module.clone(), + ); + + let promise = module.load_link_evaluate(context)?; + + context.run_jobs(); + promise.state() + })(); + + match result { + Ok(PromiseState::Pending) => { + eprintln!("module `{}` didn't execute", file.display()); + } + Ok(PromiseState::Fulfilled(_)) => {} + Ok(PromiseState::Rejected(err)) => { + eprintln!("Uncaught {}", err.display()); + } + Err(err) => eprintln!("Uncaught {err}"), + } } else { match context.eval_script(Source::from_bytes(&buffer)) { Ok(v) => println!("{}", v.display()), @@ -306,8 +346,12 @@ fn main() -> Result<(), io::Error> { let args = Opt::parse(); let queue: &dyn JobQueue = &Jobs::default(); + let loader = &SimpleModuleLoader::new(&args.root) + .map_err(|e| io::Error::new(io::ErrorKind::Other, e.to_string()))?; + let dyn_loader: &dyn ModuleLoader = loader; let mut context = ContextBuilder::new() .job_queue(queue) + .module_loader(dyn_loader) .build() .expect("cannot fail with default global object"); @@ -404,7 +448,7 @@ fn main() -> Result<(), io::Error> { .save_history(CLI_HISTORY) .expect("could not save CLI history"); } else { - evaluate_files(&args, &mut context)?; + evaluate_files(&args, &mut context, loader)?; } Ok(()) diff --git a/boa_engine/src/builtins/json/mod.rs b/boa_engine/src/builtins/json/mod.rs index d079d5e7ed..26fedf0cf3 100644 --- a/boa_engine/src/builtins/json/mod.rs +++ b/boa_engine/src/builtins/json/mod.rs @@ -23,7 +23,7 @@ use crate::{ context::intrinsics::Intrinsics, error::JsNativeError, js_string, - object::{JsObject, RecursionLimiter}, + object::JsObject, property::{Attribute, PropertyNameKind}, realm::Realm, string::{utf16, CodePoint}, @@ -571,15 +571,14 @@ impl Json { context: &mut Context<'_>, ) -> JsResult { // 1. If state.[[Stack]] contains value, throw a TypeError exception because the structure is cyclical. - let limiter = RecursionLimiter::new(value); - if limiter.live { + if state.stack.contains(value) { return Err(JsNativeError::typ() .with_message("cyclic object value") .into()); } // 2. Append value to state.[[Stack]]. - state.stack.push(value.clone().into()); + state.stack.push(value.clone()); // 3. Let stepback be state.[[Indent]]. let stepback = state.indent.clone(); @@ -705,15 +704,14 @@ impl Json { context: &mut Context<'_>, ) -> JsResult { // 1. If state.[[Stack]] contains value, throw a TypeError exception because the structure is cyclical. - let limiter = RecursionLimiter::new(value); - if limiter.live { + if state.stack.contains(value) { return Err(JsNativeError::typ() .with_message("cyclic object value") .into()); } // 2. Append value to state.[[Stack]]. - state.stack.push(value.clone().into()); + state.stack.push(value.clone()); // 3. Let stepback be state.[[Indent]]. let stepback = state.indent.clone(); @@ -810,7 +808,7 @@ impl Json { struct StateRecord { replacer_function: Option, - stack: Vec, + stack: Vec, indent: JsString, gap: JsString, property_list: Option>, diff --git a/boa_engine/src/builtins/promise/mod.rs b/boa_engine/src/builtins/promise/mod.rs index 8e14f56b15..252f238332 100644 --- a/boa_engine/src/builtins/promise/mod.rs +++ b/boa_engine/src/builtins/promise/mod.rs @@ -21,7 +21,7 @@ use crate::{ value::JsValue, Context, JsArgs, JsError, JsResult, }; -use boa_gc::{Finalize, Gc, GcRefCell, Trace}; +use boa_gc::{custom_trace, Finalize, Gc, GcRefCell, Trace}; use boa_profiler::Profiler; use std::{cell::Cell, rc::Rc}; use tap::{Conv, Pipe}; @@ -29,7 +29,7 @@ use tap::{Conv, Pipe}; // ==================== Public API ==================== /// The current state of a [`Promise`]. -#[derive(Debug, Clone, Trace, Finalize, PartialEq, Eq)] +#[derive(Debug, Clone, Finalize, PartialEq, Eq)] pub enum PromiseState { /// The promise hasn't been resolved. Pending, @@ -39,6 +39,15 @@ pub enum PromiseState { Rejected(JsValue), } +unsafe impl Trace for PromiseState { + custom_trace!(this, { + match this { + PromiseState::Fulfilled(v) | PromiseState::Rejected(v) => mark(v), + PromiseState::Pending => {} + } + }); +} + impl PromiseState { /// Gets the inner `JsValue` of a fulfilled promise state, or returns `None` if /// the state is not `Fulfilled`. diff --git a/boa_engine/src/bytecompiler/mod.rs b/boa_engine/src/bytecompiler/mod.rs index 1d451fd8a5..86d7d667b6 100644 --- a/boa_engine/src/bytecompiler/mod.rs +++ b/boa_engine/src/bytecompiler/mod.rs @@ -346,7 +346,7 @@ impl<'ctx, 'host> ByteCompiler<'ctx, 'host> { } } - fn interner(&self) -> &Interner { + pub(crate) fn interner(&self) -> &Interner { self.context.interner() } diff --git a/boa_engine/src/bytecompiler/module.rs b/boa_engine/src/bytecompiler/module.rs index 6e2de81a97..6326a3c799 100644 --- a/boa_engine/src/bytecompiler/module.rs +++ b/boa_engine/src/bytecompiler/module.rs @@ -1,7 +1,8 @@ -use crate::{js_string, vm::Opcode}; +use crate::vm::BindingOpcode; -use super::{ByteCompiler, Literal}; -use boa_ast::{ModuleItem, ModuleItemList}; +use super::ByteCompiler; +use boa_ast::{declaration::ExportDeclaration, expression::Identifier, ModuleItem, ModuleItemList}; +use boa_interner::Sym; impl ByteCompiler<'_, '_> { /// Compiles a [`ModuleItemList`]. @@ -14,18 +15,49 @@ impl ByteCompiler<'_, '_> { /// Compiles a [`ModuleItem`]. #[inline] - #[allow(clippy::single_match_else)] pub fn compile_module_item(&mut self, item: &ModuleItem) { match item { ModuleItem::StatementListItem(stmt) => { self.compile_stmt_list_item(stmt, false, false); } - _ => { - // TODO: Remove after implementing modules. - let msg = self.get_or_insert_literal(Literal::String(js_string!( - "modules are unimplemented" - ))); - self.emit(Opcode::ThrowNewTypeError, &[msg]); + ModuleItem::ImportDeclaration(_) => { + // ModuleItem : ImportDeclaration + + // 1. Return empty. + } + ModuleItem::ExportDeclaration(export) => { + #[allow(clippy::match_same_arms)] + match export { + ExportDeclaration::ReExport { .. } | ExportDeclaration::List(_) => { + // ExportDeclaration : + // export ExportFromClause FromClause ; + // export NamedExports ; + // 1. Return empty. + } + ExportDeclaration::DefaultFunction(_) + | ExportDeclaration::DefaultGenerator(_) + | ExportDeclaration::DefaultAsyncFunction(_) + | ExportDeclaration::DefaultAsyncGenerator(_) => { + // Already instantiated in `initialize_environment`. + } + ExportDeclaration::VarStatement(var) => self.compile_var_decl(var), + ExportDeclaration::Declaration(decl) => self.compile_decl(decl, false), + ExportDeclaration::DefaultClassDeclaration(cl) => { + self.class(cl, cl.name().is_none()); + if cl.name().is_none() { + self.emit_binding( + BindingOpcode::InitLet, + Identifier::from(Sym::DEFAULT_EXPORT), + ); + } + } + ExportDeclaration::DefaultAssignmentExpression(expr) => { + let name = Identifier::from(Sym::DEFAULT_EXPORT); + self.create_mutable_binding(name, false); + self.compile_expr(expr, true); + self.emit_binding(BindingOpcode::InitLet, name); + } + } } } } diff --git a/boa_engine/src/context/intrinsics.rs b/boa_engine/src/context/intrinsics.rs index 879ce8dd01..9d2ff4e0b1 100644 --- a/boa_engine/src/context/intrinsics.rs +++ b/boa_engine/src/context/intrinsics.rs @@ -156,8 +156,11 @@ pub struct StandardConstructors { impl Default for StandardConstructors { fn default() -> Self { Self { + object: StandardConstructor::with_prototype(JsObject::from_proto_and_data( + None, + ObjectData::object_prototype(), + )), async_generator_function: StandardConstructor::default(), - object: StandardConstructor::default(), proxy: StandardConstructor::default(), date: StandardConstructor::default(), function: StandardConstructor { @@ -996,6 +999,8 @@ pub(crate) struct ObjectTemplates { function_without_proto: ObjectTemplate, function_with_prototype_without_proto: ObjectTemplate, + + namespace: ObjectTemplate, } impl ObjectTemplates { @@ -1101,6 +1106,9 @@ impl ObjectTemplates { Attribute::WRITABLE | Attribute::CONFIGURABLE | Attribute::ENUMERABLE, ); + let mut namespace = ObjectTemplate::new(root_shape); + namespace.property(JsSymbol::to_string_tag().into(), Attribute::empty()); + Self { iterator_result, ordinary_object, @@ -1118,6 +1126,7 @@ impl ObjectTemplates { async_function, function_without_proto, function_with_prototype_without_proto, + namespace, } } @@ -1285,4 +1294,13 @@ impl ObjectTemplates { pub(crate) const fn function_with_prototype_without_proto(&self) -> &ObjectTemplate { &self.function_with_prototype_without_proto } + + /// Cached namespace object template. + /// + /// Transitions: + /// + /// 1. `@@toStringTag`: (`READONLY`, `NON_ENUMERABLE`, `PERMANENT`) + pub(crate) const fn namespace(&self) -> &ObjectTemplate { + &self.namespace + } } diff --git a/boa_engine/src/context/mod.rs b/boa_engine/src/context/mod.rs index 9280c9eee0..8bedb55205 100644 --- a/boa_engine/src/context/mod.rs +++ b/boa_engine/src/context/mod.rs @@ -14,30 +14,26 @@ pub use maybe_shared::MaybeShared; #[cfg(not(feature = "intl"))] pub use std::marker::PhantomData; -use std::{io::Read, rc::Rc}; +use std::{io::Read, path::Path, rc::Rc}; use crate::{ builtins, - bytecompiler::{ByteCompiler, NodeKind}, + bytecompiler::ByteCompiler, class::{Class, ClassBuilder}, job::{JobQueue, NativeJob, SimpleJobQueue}, + module::{ModuleLoader, SimpleModuleLoader}, native_function::NativeFunction, object::{shape::SharedShape, FunctionObjectBuilder, JsObject}, optimizer::{Optimizer, OptimizerOptions, OptimizerStatistics}, property::{Attribute, PropertyDescriptor, PropertyKey}, realm::Realm, - vm::{CallFrame, CodeBlock, Opcode, Vm}, + vm::{CallFrame, CodeBlock, Vm}, JsResult, JsValue, Source, }; -use boa_ast::{ - declaration::LexicalDeclaration, - expression::Identifier, - operations::{bound_names, lexically_scoped_declarations, var_scoped_declarations}, - Declaration, ModuleItemList, StatementList, -}; +use boa_ast::{expression::Identifier, StatementList}; use boa_gc::Gc; use boa_interner::{Interner, Sym}; -use boa_parser::{Error as ParseError, Parser}; +use boa_parser::Parser; use boa_profiler::Profiler; use crate::vm::RuntimeLimits; @@ -110,6 +106,8 @@ pub struct Context<'host> { job_queue: MaybeShared<'host, dyn JobQueue>, + module_loader: MaybeShared<'host, dyn ModuleLoader>, + optimizer_options: OptimizerOptions, root_shape: SharedShape, @@ -128,6 +126,7 @@ impl std::fmt::Debug for Context<'_> { .field("strict", &self.strict) .field("promise_job_queue", &"JobQueue") .field("hooks", &"HostHooks") + .field("module_loader", &"ModuleLoader") .field("optimizer_options", &self.optimizer_options); #[cfg(feature = "intl")] @@ -150,7 +149,7 @@ impl<'host> Context<'host> { /// Create a new [`ContextBuilder`] to specify the [`Interner`] and/or /// the icu data provider. #[must_use] - pub fn builder() -> ContextBuilder<'static, 'static, 'static> { + pub fn builder() -> ContextBuilder<'static, 'static, 'static, 'static> { ContextBuilder::default() } @@ -186,37 +185,6 @@ impl<'host> Context<'host> { result } - // TODO: remove `ignore` after we implement module execution - /// Evaluates the given module `src` by compiling down to bytecode, then interpreting the - /// bytecode into a value. - /// - /// # Examples - /// ```ignore - /// # use boa_engine::{Context, Source}; - /// let mut context = Context::default(); - /// - /// let source = Source::from_bytes("1 + 3"); - /// - /// let value = context.eval_module(source).unwrap(); - /// - /// assert!(value.is_number()); - /// assert_eq!(value.as_number().unwrap(), 4.0); - /// ``` - #[allow(clippy::unit_arg, clippy::drop_copy)] - pub fn eval_module(&mut self, src: Source<'_, R>) -> JsResult { - let main_timer = Profiler::global().start_event("Module evaluation", "Main"); - - let module_item_list = self.parse_module(src)?; - let code_block = self.compile_module(&module_item_list)?; - let result = self.execute(code_block); - - // The main_timer needs to be dropped before the Profiler is. - drop(main_timer); - Profiler::global().drop(); - - result - } - /// Applies optimizations to the [`StatementList`] inplace. pub fn optimize_statement_list( &mut self, @@ -227,10 +195,7 @@ impl<'host> Context<'host> { } /// Parse the given source script. - pub fn parse_script( - &mut self, - src: Source<'_, R>, - ) -> Result { + pub fn parse_script(&mut self, src: Source<'_, R>) -> JsResult { let _timer = Profiler::global().start_event("Script parsing", "Main"); let mut parser = Parser::new(src); parser.set_identifier(self.next_parser_identifier()); @@ -244,17 +209,6 @@ impl<'host> Context<'host> { Ok(result) } - /// Parse the given source script. - pub fn parse_module( - &mut self, - src: Source<'_, R>, - ) -> Result { - let _timer = Profiler::global().start_event("Module parsing", "Main"); - let mut parser = Parser::new(src); - parser.set_identifier(self.next_parser_identifier()); - parser.parse_module(&mut self.interner) - } - /// Compile the script AST into a `CodeBlock` ready to be executed by the VM. pub fn compile_script(&mut self, statement_list: &StatementList) -> JsResult> { let _timer = Profiler::global().start_event("Script compilation", "Main"); @@ -271,88 +225,12 @@ impl<'host> Context<'host> { Ok(Gc::new(compiler.finish())) } - /// Compile the module AST into a `CodeBlock` ready to be executed by the VM. - pub fn compile_module(&mut self, statement_list: &ModuleItemList) -> JsResult> { - let _timer = Profiler::global().start_event("Module compilation", "Main"); - - let mut compiler = ByteCompiler::new( - Sym::MAIN, - true, - false, - self.realm.environment().compile_env(), - self, - ); - let var_declarations = var_scoped_declarations(statement_list); - let mut declared_var_names = Vec::new(); - for var in var_declarations { - for name in var.bound_names() { - if !declared_var_names.contains(&name) { - compiler.create_mutable_binding(name, false); - let binding = compiler.initialize_mutable_binding(name, false); - let index = compiler.get_or_insert_binding(binding); - compiler.emit_opcode(Opcode::PushUndefined); - compiler.emit(Opcode::DefInitVar, &[index]); - declared_var_names.push(name); - } - } - } - - let lex_declarations = lexically_scoped_declarations(statement_list); - for declaration in lex_declarations { - match &declaration { - Declaration::Lexical(LexicalDeclaration::Const(declaration)) => { - for name in bound_names(declaration) { - compiler.create_immutable_binding(name, true); - } - } - Declaration::Lexical(LexicalDeclaration::Let(declaration)) => { - for name in bound_names(declaration) { - compiler.create_mutable_binding(name, false); - } - } - Declaration::Function(function) => { - for name in bound_names(function) { - compiler.create_mutable_binding(name, false); - } - compiler.function(function.into(), NodeKind::Declaration, false); - } - Declaration::Generator(function) => { - for name in bound_names(function) { - compiler.create_mutable_binding(name, false); - } - compiler.function(function.into(), NodeKind::Declaration, false); - } - Declaration::AsyncFunction(function) => { - for name in bound_names(function) { - compiler.create_mutable_binding(name, false); - } - compiler.function(function.into(), NodeKind::Declaration, false); - } - Declaration::AsyncGenerator(function) => { - for name in bound_names(function) { - compiler.create_mutable_binding(name, false); - } - compiler.function(function.into(), NodeKind::Declaration, false); - } - Declaration::Class(class) => { - for name in bound_names(class) { - compiler.create_mutable_binding(name, false); - } - } - } - } - - compiler.compile_module_item_list(statement_list); - Ok(Gc::new(compiler.finish())) - } - /// Call the VM with a `CodeBlock` and return the result. /// /// Since this function receives a `Gc`, cloning the code is very cheap, since it's /// just a pointer copy. Therefore, if you'd like to execute the same `CodeBlock` multiple /// times, there is no need to re-compile it, and you can just call `clone()` on the - /// `Gc` returned by the [`Context::compile_script`] or [`Context::compile_module`] - /// functions. + /// `Gc` returned by the [`Context::compile_script`] function. /// /// Note that this won't run any scheduled promise jobs; you need to call [`Context::run_jobs`] /// on the context or [`JobQueue::run_jobs`] on the provided queue to run them. @@ -634,6 +512,11 @@ impl<'host> Context<'host> { self.job_queue.clone() } + /// Gets the module loader. + pub fn module_loader(&self) -> MaybeShared<'host, dyn ModuleLoader> { + self.module_loader.clone() + } + /// Get the [`RuntimeLimits`]. #[inline] pub const fn runtime_limits(&self) -> RuntimeLimits { @@ -882,10 +765,11 @@ impl<'host> Context<'host> { doc = "The required data in a valid provider is specified in [`BoaProvider`]" )] #[derive(Default)] -pub struct ContextBuilder<'icu, 'hooks, 'queue> { +pub struct ContextBuilder<'icu, 'hooks, 'queue, 'module> { interner: Option, host_hooks: Option>, job_queue: Option>, + module_loader: Option>, #[cfg(feature = "intl")] icu: Option>, #[cfg(not(feature = "intl"))] @@ -894,17 +778,24 @@ pub struct ContextBuilder<'icu, 'hooks, 'queue> { instructions_remaining: usize, } -impl std::fmt::Debug for ContextBuilder<'_, '_, '_> { +impl std::fmt::Debug for ContextBuilder<'_, '_, '_, '_> { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { #[derive(Clone, Copy, Debug)] struct JobQueue; #[derive(Clone, Copy, Debug)] struct HostHooks; + #[derive(Clone, Copy, Debug)] + struct ModuleLoader; + let mut out = f.debug_struct("ContextBuilder"); out.field("interner", &self.interner) .field("host_hooks", &self.host_hooks.as_ref().map(|_| HostHooks)) - .field("job_queue", &self.job_queue.as_ref().map(|_| JobQueue)); + .field("job_queue", &self.job_queue.as_ref().map(|_| JobQueue)) + .field( + "module_loader", + &self.module_loader.as_ref().map(|_| ModuleLoader), + ); #[cfg(feature = "intl")] out.field("icu", &self.icu); @@ -916,7 +807,7 @@ impl std::fmt::Debug for ContextBuilder<'_, '_, '_> { } } -impl<'icu, 'hooks, 'queue> ContextBuilder<'icu, 'hooks, 'queue> { +impl<'icu, 'hooks, 'queue, 'module> ContextBuilder<'icu, 'hooks, 'queue, 'module> { /// Creates a new [`ContextBuilder`] with a default empty [`Interner`] /// and a default `BoaProvider` if the `intl` feature is enabled. #[must_use] @@ -952,7 +843,7 @@ impl<'icu, 'hooks, 'queue> ContextBuilder<'icu, 'hooks, 'queue> { pub fn icu_provider( self, provider: BoaProvider<'_>, - ) -> Result, IcuError> { + ) -> Result, IcuError> { Ok(ContextBuilder { icu: Some(icu::Icu::new(provider)?), ..self @@ -966,7 +857,7 @@ impl<'icu, 'hooks, 'queue> ContextBuilder<'icu, 'hooks, 'queue> { pub fn host_hooks<'new_hooks, H>( self, host_hooks: H, - ) -> ContextBuilder<'icu, 'new_hooks, 'queue> + ) -> ContextBuilder<'icu, 'new_hooks, 'queue, 'module> where H: Into>, { @@ -978,7 +869,10 @@ impl<'icu, 'hooks, 'queue> ContextBuilder<'icu, 'hooks, 'queue> { /// Initializes the [`JobQueue`] for the context. #[must_use] - pub fn job_queue<'new_queue, Q>(self, job_queue: Q) -> ContextBuilder<'icu, 'hooks, 'new_queue> + pub fn job_queue<'new_queue, Q>( + self, + job_queue: Q, + ) -> ContextBuilder<'icu, 'hooks, 'new_queue, 'module> where Q: Into>, { @@ -988,6 +882,21 @@ impl<'icu, 'hooks, 'queue> ContextBuilder<'icu, 'hooks, 'queue> { } } + /// Initializes the [`ModuleLoader`] for the context. + #[must_use] + pub fn module_loader<'new_module, M>( + self, + module_loader: M, + ) -> ContextBuilder<'icu, 'hooks, 'queue, 'new_module> + where + M: Into>, + { + ContextBuilder { + module_loader: Some(module_loader.into()), + ..self + } + } + /// Specifies the number of instructions remaining to the [`Context`]. /// /// This function is only available if the `fuzz` feature is enabled. @@ -1005,6 +914,7 @@ impl<'icu, 'hooks, 'queue> ContextBuilder<'icu, 'hooks, 'queue> { 'icu: 'host, 'hooks: 'host, 'queue: 'host, + 'module: 'host, { let root_shape = SharedShape::root(); @@ -1034,6 +944,13 @@ impl<'icu, 'hooks, 'queue> ContextBuilder<'icu, 'hooks, 'queue> { let queue: Rc = Rc::new(SimpleJobQueue::new()); queue.into() }), + module_loader: self.module_loader.unwrap_or_else(|| { + let loader: Rc = Rc::new( + SimpleModuleLoader::new(Path::new(".")) + .expect("failed to initialize default module loader"), + ); + loader.into() + }), optimizer_options: OptimizerOptions::OPTIMIZE_ALL, root_shape, parser_identifier: 0, diff --git a/boa_engine/src/environments/runtime/declarative/mod.rs b/boa_engine/src/environments/runtime/declarative/mod.rs index 6d38765ae5..37ede1c45c 100644 --- a/boa_engine/src/environments/runtime/declarative/mod.rs +++ b/boa_engine/src/environments/runtime/declarative/mod.rs @@ -1,6 +1,7 @@ mod function; mod global; mod lexical; +mod module; use std::cell::Cell; @@ -8,6 +9,7 @@ use boa_gc::{Finalize, Gc, GcRefCell, Trace}; pub(crate) use function::{FunctionEnvironment, FunctionSlots, ThisBindingStatus}; pub(crate) use global::GlobalEnvironment; pub(crate) use lexical::LexicalEnvironment; +pub(crate) use module::ModuleEnvironment; use crate::{environments::CompileTimeEnvironment, JsObject, JsResult, JsValue}; @@ -133,6 +135,8 @@ pub(crate) enum DeclarativeEnvironmentKind { Global(GlobalEnvironment), /// Stores lexical bindings, var bindings and the `FunctionSlots` of the function environment. Function(FunctionEnvironment), + /// Stores module bindings, which include references to bindings on other environments. + Module(ModuleEnvironment), } impl DeclarativeEnvironmentKind { @@ -154,6 +158,15 @@ impl DeclarativeEnvironmentKind { } } + /// Unwraps the inner module environment if possible. Returns `None` otherwise. + pub(crate) const fn as_module(&self) -> Option<&ModuleEnvironment> { + if let Self::Module(module) = &self { + Some(module) + } else { + None + } + } + /// Get the binding value from the environment by it's index. /// /// # Panics @@ -165,6 +178,7 @@ impl DeclarativeEnvironmentKind { DeclarativeEnvironmentKind::Lexical(inner) => inner.get(index), DeclarativeEnvironmentKind::Global(inner) => inner.get(index), DeclarativeEnvironmentKind::Function(inner) => inner.get(index), + DeclarativeEnvironmentKind::Module(inner) => inner.get(index), } } @@ -179,6 +193,7 @@ impl DeclarativeEnvironmentKind { DeclarativeEnvironmentKind::Lexical(inner) => inner.set(index, value), DeclarativeEnvironmentKind::Global(inner) => inner.set(index, value), DeclarativeEnvironmentKind::Function(inner) => inner.set(index, value), + DeclarativeEnvironmentKind::Module(inner) => inner.set(index, value), } } @@ -195,6 +210,7 @@ impl DeclarativeEnvironmentKind { DeclarativeEnvironmentKind::Lexical(_) => Ok(None), DeclarativeEnvironmentKind::Global(g) => Ok(Some(g.get_this_binding().into())), DeclarativeEnvironmentKind::Function(f) => f.get_this_binding(), + DeclarativeEnvironmentKind::Module(_) => Ok(Some(JsValue::undefined())), } } @@ -209,8 +225,8 @@ impl DeclarativeEnvironmentKind { pub(crate) fn has_this_binding(&self) -> bool { match self { DeclarativeEnvironmentKind::Lexical(_) => false, - DeclarativeEnvironmentKind::Global(_) => true, DeclarativeEnvironmentKind::Function(f) => f.has_this_binding(), + DeclarativeEnvironmentKind::Global(_) | DeclarativeEnvironmentKind::Module(_) => true, } } @@ -220,6 +236,7 @@ impl DeclarativeEnvironmentKind { DeclarativeEnvironmentKind::Lexical(lex) => lex.poisonable_environment().poisoned(), DeclarativeEnvironmentKind::Global(g) => g.poisonable_environment().poisoned(), DeclarativeEnvironmentKind::Function(f) => f.poisonable_environment().poisoned(), + DeclarativeEnvironmentKind::Module(_) => false, } } @@ -229,6 +246,7 @@ impl DeclarativeEnvironmentKind { DeclarativeEnvironmentKind::Lexical(lex) => lex.poisonable_environment().with(), DeclarativeEnvironmentKind::Global(g) => g.poisonable_environment().with(), DeclarativeEnvironmentKind::Function(f) => f.poisonable_environment().with(), + DeclarativeEnvironmentKind::Module(_) => false, } } @@ -238,6 +256,9 @@ impl DeclarativeEnvironmentKind { DeclarativeEnvironmentKind::Lexical(lex) => lex.poisonable_environment().poison(), DeclarativeEnvironmentKind::Global(g) => g.poisonable_environment().poison(), DeclarativeEnvironmentKind::Function(f) => f.poisonable_environment().poison(), + DeclarativeEnvironmentKind::Module(_) => { + unreachable!("modules are always run in strict mode") + } } } } diff --git a/boa_engine/src/environments/runtime/declarative/module.rs b/boa_engine/src/environments/runtime/declarative/module.rs new file mode 100644 index 0000000000..b21b668a16 --- /dev/null +++ b/boa_engine/src/environments/runtime/declarative/module.rs @@ -0,0 +1,120 @@ +use std::cell::Cell; + +use boa_ast::expression::Identifier; +use boa_gc::{Finalize, GcRefCell, Trace}; + +use crate::{module::Module, JsValue}; + +/// Type of accessor used to access an indirect binding. +#[derive(Debug, Clone, Copy)] +enum BindingAccessor { + Identifier(Identifier), + Index(usize), +} + +/// An indirect reference to a binding inside an environment. +#[derive(Clone, Debug, Trace, Finalize)] +struct IndirectBinding { + module: Module, + #[unsafe_ignore_trace] + accessor: Cell, +} + +/// The type of binding a [`ModuleEnvironment`] can contain. +#[derive(Clone, Debug, Trace, Finalize)] +enum BindingType { + Direct(Option), + Indirect(IndirectBinding), +} + +/// A [**Module Environment Record**][spec]. +/// +/// Module environments allow referencing bindings inside other environments, in addition +/// to the usual declarative environment functionality. +/// +/// +/// [spec]: https://tc39.es/ecma262/#sec-module-environment-records +#[derive(Debug, Trace, Finalize)] +pub(crate) struct ModuleEnvironment { + bindings: GcRefCell>, +} + +impl ModuleEnvironment { + /// Creates a new `LexicalEnvironment`. + pub(crate) fn new(bindings: usize) -> Self { + Self { + bindings: GcRefCell::new(vec![BindingType::Direct(None); bindings]), + } + } + + /// Get the binding value from the environment by it's index. + /// + /// # Panics + /// + /// Panics if the binding value is out of range or not initialized. + #[track_caller] + pub(crate) fn get(&self, index: usize) -> Option { + let bindings = self.bindings.borrow(); + + match &bindings[index] { + BindingType::Direct(v) => v.clone(), + BindingType::Indirect(IndirectBinding { module, accessor }) => { + let env = module.environment()?; + + match accessor.get() { + BindingAccessor::Identifier(name) => { + let index = env + .compile_env() + .borrow() + .get_binding(name) + .expect("linking must ensure the binding exists"); + + let value = env.get(index.binding_index)?; + + accessor.set(BindingAccessor::Index(index.binding_index)); + + Some(value) + } + BindingAccessor::Index(index) => env.get(index), + } + } + } + } + + /// Sets the binding value from the environment by index. + /// + /// # Panics + /// + /// Panics if the binding value is out of range. + #[track_caller] + pub(crate) fn set(&self, index: usize, value: JsValue) { + let mut bindings = self.bindings.borrow_mut(); + + match &mut bindings[index] { + BindingType::Direct(v) => *v = Some(value), + BindingType::Indirect(_) => { + panic!("cannot modify indirect references to other environments") + } + } + } + + /// Creates an indirect binding reference to another environment binding. + /// + /// # Panics + /// + /// Panics if the binding value is out of range. + #[track_caller] + pub(crate) fn set_indirect( + &self, + index: usize, + target_module: Module, + target_binding: Identifier, + ) { + let mut bindings = self.bindings.borrow_mut(); + + bindings[index] = BindingType::Indirect(IndirectBinding { + module: target_module, + accessor: Cell::new(BindingAccessor::Identifier(target_binding)), + }); + } +} diff --git a/boa_engine/src/environments/runtime/mod.rs b/boa_engine/src/environments/runtime/mod.rs index 636fec61ce..a537779bb2 100644 --- a/boa_engine/src/environments/runtime/mod.rs +++ b/boa_engine/src/environments/runtime/mod.rs @@ -8,6 +8,7 @@ use rustc_hash::FxHashSet; mod declarative; +use self::declarative::ModuleEnvironment; pub(crate) use self::declarative::{ DeclarativeEnvironment, DeclarativeEnvironmentKind, FunctionEnvironment, FunctionSlots, LexicalEnvironment, ThisBindingStatus, @@ -344,6 +345,25 @@ impl EnvironmentStack { ))); } + /// Push a module environment on the environments stack. + /// + /// # Panics + /// + /// Panics if no environment exists on the stack. + #[track_caller] + pub(crate) fn push_module( + &mut self, + compile_environment: Gc>, + ) { + let num_bindings = compile_environment.borrow().num_bindings(); + self.stack.push(Environment::Declarative(Gc::new( + DeclarativeEnvironment::new( + DeclarativeEnvironmentKind::Module(ModuleEnvironment::new(num_bindings)), + compile_environment, + ), + ))); + } + /// Pop environment from the environments stack. #[track_caller] pub(crate) fn pop(&mut self) -> Environment { @@ -359,7 +379,7 @@ impl EnvironmentStack { /// /// Panics if no environment exists on the stack. #[track_caller] - pub(crate) fn current(&mut self) -> Environment { + pub(crate) fn current(&self) -> Environment { self.stack .last() .expect("global environment must always exist") diff --git a/boa_engine/src/lib.rs b/boa_engine/src/lib.rs index 9a5c8d3699..1206387ca4 100644 --- a/boa_engine/src/lib.rs +++ b/boa_engine/src/lib.rs @@ -133,6 +133,7 @@ pub mod context; pub mod environments; pub mod error; pub mod job; +pub mod module; pub mod native_function; pub mod object; pub mod optimizer; @@ -151,6 +152,7 @@ pub mod vm; pub mod prelude { pub use crate::{ error::{JsError, JsNativeError, JsNativeErrorKind}, + module::Module, native_function::NativeFunction, object::JsObject, Context, JsBigInt, JsResult, JsString, JsValue, @@ -166,6 +168,7 @@ pub use crate::{ bigint::JsBigInt, context::Context, error::{JsError, JsNativeError, JsNativeErrorKind}, + module::Module, native_function::NativeFunction, object::JsObject, string::JsString, diff --git a/boa_engine/src/module/mod.rs b/boa_engine/src/module/mod.rs new file mode 100644 index 0000000000..7c8214c103 --- /dev/null +++ b/boa_engine/src/module/mod.rs @@ -0,0 +1,737 @@ +//! Boa's implementation of the ECMAScript's module system. +//! +//! This module contains the [`Module`] type, which represents an [**Abstract Module Record**][module], +//! a [`ModuleLoader`] trait for custom module loader implementations, and [`SimpleModuleLoader`], +//! the default `ModuleLoader` for [`Context`] which can be used for most simple usecases. +//! +//! Every module roughly follows the same lifecycle: +//! - Parse using [`Module::parse`]. +//! - Load all its dependencies using [`Module::load`]. +//! - Link its dependencies together using [`Module::link`]. +//! - Evaluate the module and its dependencies using [`Module::evaluate`]. +//! +//! The [`ModuleLoader`] trait allows customizing the "load" step on the lifecycle +//! of a module, which allows doing things like fetching modules from urls, having multiple +//! "modpaths" from where to import modules, or using Rust futures to avoid blocking the main thread +//! on loads. +//! +//! More information: +//! - [ECMAScript reference][spec] +//! +//! [spec]: https://tc39.es/ecma262/#sec-modules +//! [module]: https://tc39.es/ecma262/#sec-abstract-module-records + +mod source; +use source::SourceTextModule; + +use std::cell::{Cell, RefCell}; +use std::hash::Hash; +use std::io::Read; +use std::path::{Path, PathBuf}; +use std::rc::Rc; +use std::{collections::HashSet, hash::BuildHasherDefault}; + +use indexmap::IndexMap; +use rustc_hash::{FxHashMap, FxHashSet, FxHasher}; + +use boa_ast::expression::Identifier; +use boa_gc::{Finalize, Gc, GcRefCell, Trace}; +use boa_interner::Sym; +use boa_parser::{Parser, Source}; +use boa_profiler::Profiler; + +use crate::object::FunctionObjectBuilder; +use crate::{ + builtins::promise::{PromiseCapability, PromiseState}, + environments::DeclarativeEnvironment, + object::{JsObject, JsPromise, ObjectData}, + realm::Realm, + Context, JsError, JsResult, JsString, JsValue, +}; +use crate::{js_string, JsNativeError, NativeFunction}; + +/// The referrer from which a load request of a module originates. +#[derive(Debug)] +pub enum Referrer { + /// A [**Source Text Module Record**](https://tc39.es/ecma262/#sec-source-text-module-records). + Module(Module), + /// A [**Realm**](https://tc39.es/ecma262/#sec-code-realms). + Realm(Realm), // TODO: script +} + +/// Module loading related host hooks. +/// +/// This trait allows to customize the behaviour of the engine on module load requests and +/// `import.meta` requests. +pub trait ModuleLoader { + /// Host hook [`HostLoadImportedModule ( referrer, specifier, hostDefined, payload )`][spec]. + /// + /// This hook allows to customize the module loading functionality of the engine. Technically, + /// this should call the [`FinishLoadingImportedModule`][finish] operation, but this simpler API just provides + /// a closure that replaces `FinishLoadingImportedModule`. + /// + /// # Requirements + /// + /// - The host environment must perform `FinishLoadingImportedModule(referrer, specifier, payload, result)`, + /// where result is either a normal completion containing the loaded Module Record or a throw + /// completion, either synchronously or asynchronously. This is equivalent to calling the `finish_load` + /// callback. + /// - If this operation is called multiple times with the same `(referrer, specifier)` pair and + /// it performs FinishLoadingImportedModule(referrer, specifier, payload, result) where result + /// is a normal completion, then it must perform + /// `FinishLoadingImportedModule(referrer, specifier, payload, result)` with the same result each + /// time. + /// - The operation must treat payload as an opaque value to be passed through to + /// `FinishLoadingImportedModule`. (can be ignored) + /// + /// [spec]: https://tc39.es/ecma262/#sec-HostLoadImportedModule + /// [finish]: https://tc39.es/ecma262/#sec-FinishLoadingImportedModule + #[allow(clippy::type_complexity)] + fn load_imported_module( + &self, + referrer: Referrer, + specifier: JsString, + finish_load: Box, &mut Context<'_>)>, + context: &mut Context<'_>, + ); + + /// Host hooks [`HostGetImportMetaProperties ( moduleRecord )`][meta] and + /// [`HostFinalizeImportMeta ( importMeta, moduleRecord )`][final]. + /// + /// This unifies both APIs into a single hook that can be overriden on both cases. + /// The most common usage is to add properties to `import_meta` and return, but this also + /// allows modifying the import meta object in more exotic ways before exposing it to ECMAScript + /// code. + /// + /// The default implementation of `HostGetImportMetaProperties` is to return a new empty List. + /// + /// [meta]: https://tc39.es/ecma262/#sec-hostgetimportmetaproperties + /// [final]: https://tc39.es/ecma262/#sec-hostfinalizeimportmeta + fn init_import_meta( + &self, + _import_meta: JsObject, + _module: Module, + _context: &mut Context<'_>, + ) { + } +} + +/// A simple module loader that loads modules relative to a root path. +#[derive(Debug)] +pub struct SimpleModuleLoader { + root: PathBuf, + module_map: GcRefCell>, +} + +impl SimpleModuleLoader { + /// Creates a new `SimpleModuleLoader` from a root module path. + pub fn new>(root: P) -> JsResult { + let root = root.as_ref(); + let absolute = root.canonicalize().map_err(|e| { + JsNativeError::typ() + .with_message(format!("could not set module root `{}`", root.display())) + .with_cause(JsError::from_opaque(js_string!(e.to_string()).into())) + })?; + Ok(Self { + root: absolute, + module_map: GcRefCell::default(), + }) + } + + /// Inserts a new module onto the module map. + #[inline] + pub fn insert(&self, path: PathBuf, module: Module) { + self.module_map.borrow_mut().insert(path, module); + } + + /// Gets a module from its original path. + #[inline] + pub fn get(&self, path: &Path) -> Option { + self.module_map.borrow().get(path).cloned() + } +} + +impl ModuleLoader for SimpleModuleLoader { + fn load_imported_module( + &self, + _referrer: Referrer, + specifier: JsString, + finish_load: Box, &mut Context<'_>)>, + context: &mut Context<'_>, + ) { + let result = (|| { + let path = specifier + .to_std_string() + .map_err(|err| JsNativeError::typ().with_message(err.to_string()))?; + let short_path = Path::new(&path); + let path = self.root.join(short_path); + let path = path.canonicalize().map_err(|err| { + JsNativeError::typ() + .with_message(format!( + "could not canonicalize path `{}`", + short_path.display() + )) + .with_cause(JsError::from_opaque(js_string!(err.to_string()).into())) + })?; + if let Some(module) = self.get(&path) { + return Ok(module); + } + let source = Source::from_filepath(&path).map_err(|err| { + JsNativeError::typ() + .with_message(format!("could not open file `{}`", short_path.display())) + .with_cause(JsError::from_opaque(js_string!(err.to_string()).into())) + })?; + let module = Module::parse(source, None, context).map_err(|err| { + JsNativeError::error() + .with_message(format!("could not parse module `{}`", short_path.display())) + .with_cause(err) + })?; + self.insert(path, module.clone()); + Ok(module) + })(); + + finish_load(result, context); + } +} + +/// ECMAScript's [**Abstract module record**][spec]. +/// +/// [spec]: https://tc39.es/ecma262/#sec-abstract-module-records +#[derive(Clone, Trace, Finalize)] +pub struct Module { + inner: Gc, +} + +impl std::fmt::Debug for Module { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("Module") + .field("realm", &self.inner.realm.addr()) + .field("environment", &self.inner.environment) + .field("namespace", &self.inner.namespace) + .field("kind", &self.inner.kind) + .field("host_defined", &self.inner.host_defined) + .finish() + } +} + +#[derive(Trace, Finalize)] +struct Inner { + realm: Realm, + environment: GcRefCell>>, + namespace: GcRefCell>, + kind: ModuleKind, + host_defined: (), +} + +/// The kind of a [`Module`]. +#[derive(Debug, Trace, Finalize)] +pub(crate) enum ModuleKind { + /// A [**Source Text Module Record**](https://tc39.es/ecma262/#sec-source-text-module-records) + SourceText(SourceTextModule), + /// A [**Synthetic Module Record**](https://tc39.es/proposal-json-modules/#sec-synthetic-module-records) + #[allow(unused)] + Synthetic, +} + +/// Return value of the [`Module::resolve_export`] operation. +/// +/// Indicates how to access a specific export in a module. +#[derive(Debug, Clone)] +pub(crate) struct ResolvedBinding { + module: Module, + binding_name: BindingName, +} + +/// The local name of the resolved binding within its containing module. +/// +/// Note that a resolved binding can resolve to a single binding inside a module (`export var a = 1"`) +/// or to a whole module namespace (`export * as ns from "mod.js"`). +#[derive(Debug, Clone, Copy)] +pub(crate) enum BindingName { + /// A local binding. + Name(Identifier), + /// The whole namespace of the containing module. + Namespace, +} + +impl ResolvedBinding { + /// Gets the module from which the export resolved. + pub(crate) const fn module(&self) -> &Module { + &self.module + } + + /// Gets the binding associated with the resolved export. + pub(crate) const fn binding_name(&self) -> BindingName { + self.binding_name + } +} + +#[derive(Debug, Clone)] +struct GraphLoadingState { + capability: PromiseCapability, + loading: Cell, + pending_modules: Cell, + visited: RefCell>, +} + +#[derive(Debug, Clone, Copy)] +pub(crate) enum ResolveExportError { + NotFound, + Ambiguous, +} + +impl Module { + /// Abstract operation [`ParseModule ( sourceText, realm, hostDefined )`][spec]. + /// + /// Parses the provided `src` as an ECMAScript module, returning an error if parsing fails. + /// + /// [spec]: https://tc39.es/ecma262/#sec-parsemodule + #[inline] + pub fn parse( + src: Source<'_, R>, + realm: Option, + context: &mut Context<'_>, + ) -> JsResult { + let _timer = Profiler::global().start_event("Module parsing", "Main"); + let mut parser = Parser::new(src); + parser.set_identifier(context.next_parser_identifier()); + let module = parser.parse_module(context.interner_mut())?; + + let src = SourceTextModule::new(module); + + let module = Module { + inner: Gc::new(Inner { + realm: realm.unwrap_or_else(|| context.realm().clone()), + environment: GcRefCell::default(), + namespace: GcRefCell::default(), + kind: ModuleKind::SourceText(src.clone()), + host_defined: (), + }), + }; + + src.set_parent(module.clone()); + + Ok(module) + } + + /// Gets the realm of this `Module`. + #[inline] + pub fn realm(&self) -> &Realm { + &self.inner.realm + } + + /// Gets the kind of this `Module`. + pub(crate) fn kind(&self) -> &ModuleKind { + &self.inner.kind + } + + /// Gets the environment of this `Module`. + pub(crate) fn environment(&self) -> Option> { + self.inner.environment.borrow().clone() + } + + /// Abstract method [`LoadRequestedModules ( [ hostDefined ] )`][spec]. + /// + /// Prepares the module for linking by loading all its module dependencies. Returns a `JsPromise` + /// that will resolve when the loading process either completes or fails. + /// + /// [spec]: https://tc39.es/ecma262/#table-abstract-methods-of-module-records + #[allow(clippy::missing_panics_doc)] + #[inline] + pub fn load(&self, context: &mut Context<'_>) -> JsPromise { + match self.kind() { + ModuleKind::SourceText(_) => { + // Concrete method [`LoadRequestedModules ( [ hostDefined ] )`][spec]. + // + // [spec]: https://tc39.es/ecma262/#sec-LoadRequestedModules + // TODO: 1. If hostDefined is not present, let hostDefined be empty. + + // 2. Let pc be ! NewPromiseCapability(%Promise%). + let pc = PromiseCapability::new( + &context.intrinsics().constructors().promise().constructor(), + context, + ) + .expect( + "capability creation must always succeed when using the `%Promise%` intrinsic", + ); + + // 4. Perform InnerModuleLoading(state, module). + self.inner_load( + // 3. Let state be the GraphLoadingState Record { + // [[IsLoading]]: true, [[PendingModulesCount]]: 1, [[Visited]]: « », + // [[PromiseCapability]]: pc, [[HostDefined]]: hostDefined + // }. + &Rc::new(GraphLoadingState { + capability: pc.clone(), + loading: Cell::new(true), + pending_modules: Cell::new(1), + visited: RefCell::default(), + }), + context, + ); + + // 5. Return pc.[[Promise]]. + JsPromise::from_object(pc.promise().clone()) + .expect("promise created from the %Promise% intrinsic is always native") + } + ModuleKind::Synthetic => todo!("synthetic.load()"), + } + } + + /// Abstract operation [`InnerModuleLoading`][spec]. + /// + /// [spec]: https://tc39.es/ecma262/#sec-InnerModuleLoading + fn inner_load(&self, state: &Rc, context: &mut Context<'_>) { + // 1. Assert: state.[[IsLoading]] is true. + assert!(state.loading.get()); + + if let ModuleKind::SourceText(src) = self.kind() { + // continues on `inner_load + src.inner_load(state, context); + if !state.loading.get() { + return; + } + } + + // 3. Assert: state.[[PendingModulesCount]] ≥ 1. + assert!(state.pending_modules.get() >= 1); + + // 4. Set state.[[PendingModulesCount]] to state.[[PendingModulesCount]] - 1. + state.pending_modules.set(state.pending_modules.get() - 1); + // 5. If state.[[PendingModulesCount]] = 0, then + + if state.pending_modules.get() == 0 { + // a. Set state.[[IsLoading]] to false. + state.loading.set(false); + // b. For each Cyclic Module Record loaded of state.[[Visited]], do + // i. If loaded.[[Status]] is new, set loaded.[[Status]] to unlinked. + // By default, all modules start on `unlinked`. + + // c. Perform ! Call(state.[[PromiseCapability]].[[Resolve]], undefined, « undefined »). + state + .capability + .resolve() + .call(&JsValue::undefined(), &[], context) + .expect("marking a module as loaded should not fail"); + } + // 6. Return unused. + } + + /// Abstract method [`GetExportedNames([exportStarSet])`][spec]. + /// + /// Returns a list of all the names exported from this module. + /// + /// # Note + /// + /// This must only be called if the [`JsPromise`] returned by [`Module::load`] has fulfilled. + /// + /// [spec]: https://tc39.es/ecma262/#table-abstract-methods-of-module-records + fn get_exported_names(&self, export_star_set: &mut Vec) -> FxHashSet { + match self.kind() { + ModuleKind::SourceText(src) => src.get_exported_names(export_star_set), + ModuleKind::Synthetic => todo!("synthetic.get_exported_names()"), + } + } + + /// Abstract method [`ResolveExport(exportName [, resolveSet])`][spec]. + /// + /// Returns the corresponding local binding of a binding exported by this module. + /// The spec requires that this operation must be idempotent; calling this multiple times + /// with the same `export_name` and `resolve_set` should always return the same result. + /// + /// # Note + /// + /// This must only be called if the [`JsPromise`] returned by [`Module::load`] has fulfilled. + /// + /// [spec]: https://tc39.es/ecma262/#table-abstract-methods-of-module-records + #[allow(clippy::mutable_key_type)] + pub(crate) fn resolve_export( + &self, + export_name: Sym, + resolve_set: &mut FxHashSet<(Module, Sym)>, + ) -> Result { + match self.kind() { + ModuleKind::SourceText(src) => src.resolve_export(export_name, resolve_set), + ModuleKind::Synthetic => todo!("synthetic.resolve_export()"), + } + } + + /// Abstract method [`Link() `][spec]. + /// + /// Prepares this module for evaluation by resolving all its module dependencies and initializing + /// its environment. + /// + /// # Note + /// + /// This must only be called if the [`JsPromise`] returned by [`Module::load`] has fulfilled. + /// + /// [spec]: https://tc39.es/ecma262/#table-abstract-methods-of-module-records + #[allow(clippy::missing_panics_doc)] + #[inline] + pub fn link(&self, context: &mut Context<'_>) -> JsResult<()> { + match self.kind() { + ModuleKind::SourceText(src) => src.link(context), + ModuleKind::Synthetic => todo!("synthetic.link()"), + } + } + + /// Abstract operation [`InnerModuleLinking ( module, stack, index )`][spec]. + /// + /// [spec]: https://tc39.es/ecma262/#sec-InnerModuleLinking + fn inner_link( + &self, + stack: &mut Vec, + index: usize, + context: &mut Context<'_>, + ) -> JsResult { + match self.kind() { + ModuleKind::SourceText(src) => src.inner_link(stack, index, context), + #[allow(unreachable_code)] + // If module is not a Cyclic Module Record, then + ModuleKind::Synthetic => { + // a. Perform ? module.Link(). + todo!("synthetic.link()"); + // b. Return index. + Ok(index) + } + } + } + + /// Abstract method [`Evaluate()`][spec]. + /// + /// Evaluates this module, returning a promise for the result of the evaluation of this module + /// and its dependencies. + /// If the promise is rejected, hosts are expected to handle the promise rejection and rethrow + /// the evaluation error. + /// + /// # Note + /// + /// This must only be called if the [`Module::link`] method finished successfully. + /// + /// [spec]: https://tc39.es/ecma262/#table-abstract-methods-of-module-records + #[allow(clippy::missing_panics_doc)] + #[inline] + pub fn evaluate(&self, context: &mut Context<'_>) -> JsPromise { + match self.kind() { + ModuleKind::SourceText(src) => src.evaluate(context), + ModuleKind::Synthetic => todo!("synthetic.evaluate()"), + } + } + + /// Abstract operation [`InnerModuleLinking ( module, stack, index )`][spec]. + /// + /// [spec]: https://tc39.es/ecma262/#sec-InnerModuleLinking + fn inner_evaluate( + &self, + stack: &mut Vec, + index: usize, + context: &mut Context<'_>, + ) -> JsResult { + match self.kind() { + ModuleKind::SourceText(src) => src.inner_evaluate(stack, index, None, context), + // 1. If module is not a Cyclic Module Record, then + #[allow(unused, clippy::diverging_sub_expression)] + ModuleKind::Synthetic => { + // a. Let promise be ! module.Evaluate(). + let promise: JsPromise = todo!("module.Evaluate()"); + let state = promise.state()?; + match state { + PromiseState::Pending => { + unreachable!("b. Assert: promise.[[PromiseState]] is not pending.") + } + // d. Return index. + PromiseState::Fulfilled(_) => Ok(index), + // c. If promise.[[PromiseState]] is rejected, then + // i. Return ThrowCompletion(promise.[[PromiseResult]]). + PromiseState::Rejected(err) => Err(JsError::from_opaque(err)), + } + } + } + } + + /// Loads, links and evaluates this module, returning a promise that will resolve after the module + /// finishes its lifecycle. + /// + /// # Examples + /// ``` + /// # use std::path::Path; + /// # use boa_engine::{Context, Source, Module, JsValue}; + /// # use boa_engine::builtins::promise::PromiseState; + /// # use boa_engine::module::{ModuleLoader, SimpleModuleLoader}; + /// let loader = &SimpleModuleLoader::new(Path::new(".")).unwrap(); + /// let dyn_loader: &dyn ModuleLoader = loader; + /// let mut context = &mut Context::builder().module_loader(dyn_loader).build().unwrap(); + /// + /// let source = Source::from_bytes("1 + 3"); + /// + /// let module = Module::parse(source, None, context).unwrap(); + /// + /// loader.insert(Path::new("main.mjs").to_path_buf(), module.clone()); + /// + /// let promise = module.load_link_evaluate(context).unwrap(); + /// + /// context.run_jobs(); + /// + /// assert_eq!(promise.state().unwrap(), PromiseState::Fulfilled(JsValue::undefined())); + /// ``` + #[allow(clippy::drop_copy)] + #[inline] + pub fn load_link_evaluate(&self, context: &mut Context<'_>) -> JsResult { + let main_timer = Profiler::global().start_event("Module evaluation", "Main"); + + let promise = self + .load(context) + .then( + Some( + FunctionObjectBuilder::new( + context, + NativeFunction::from_copy_closure_with_captures( + |_, _, module, context| { + module.link(context)?; + Ok(JsValue::undefined()) + }, + self.clone(), + ), + ) + .build(), + ), + None, + context, + )? + .then( + Some( + FunctionObjectBuilder::new( + context, + NativeFunction::from_copy_closure_with_captures( + |_, _, module, context| Ok(module.evaluate(context).into()), + self.clone(), + ), + ) + .build(), + ), + None, + context, + )?; + + // The main_timer needs to be dropped before the Profiler is. + drop(main_timer); + Profiler::global().drop(); + + Ok(promise) + } + + /// Abstract operation [`GetModuleNamespace ( module )`][spec]. + /// + /// Gets the [**Module Namespace Object**][ns] that represents this module's exports. + /// + /// [spec]: https://tc39.es/ecma262/#sec-getmodulenamespace + /// [ns]: https://tc39.es/ecma262/#sec-module-namespace-exotic-objects + pub fn namespace(&self, context: &mut Context<'_>) -> JsObject { + // 1. Assert: If module is a Cyclic Module Record, then module.[[Status]] is not new or unlinked. + // 2. Let namespace be module.[[Namespace]]. + // 3. If namespace is empty, then + // 4. Return namespace. + self.inner + .namespace + .borrow_mut() + .get_or_insert_with(|| { + // a. Let exportedNames be module.GetExportedNames(). + let exported_names = self.get_exported_names(&mut Vec::default()); + + // b. Let unambiguousNames be a new empty List. + let unambiguous_names = exported_names + .into_iter() + // c. For each element name of exportedNames, do + .filter_map(|name| { + // i. Let resolution be module.ResolveExport(name). + // ii. If resolution is a ResolvedBinding Record, append name to unambiguousNames. + self.resolve_export(name, &mut HashSet::default()) + .ok() + .map(|_| name) + }) + .collect(); + + // d. Set namespace to ModuleNamespaceCreate(module, unambiguousNames). + ModuleNamespace::create(self.clone(), unambiguous_names, context) + }) + .clone() + } +} + +impl PartialEq for Module { + #[inline] + fn eq(&self, other: &Self) -> bool { + std::ptr::eq(self.inner.as_ref(), other.inner.as_ref()) + } +} + +impl Eq for Module {} + +impl Hash for Module { + #[inline] + fn hash(&self, state: &mut H) { + std::ptr::hash(self.inner.as_ref(), state); + } +} + +/// Module namespace exotic object. +/// +/// Exposes the bindings exported by a [`Module`] to be accessed from ECMAScript code. +#[derive(Debug, Trace, Finalize)] +pub struct ModuleNamespace { + module: Module, + #[unsafe_ignore_trace] + exports: IndexMap>, +} + +impl ModuleNamespace { + /// Abstract operation [`ModuleNamespaceCreate ( module, exports )`][spec]. + /// + /// [spec]: https://tc39.es/ecma262/#sec-modulenamespacecreate + pub(crate) fn create(module: Module, names: Vec, context: &mut Context<'_>) -> JsObject { + // 1. Assert: module.[[Namespace]] is empty. + // ignored since this is ensured by `Module::namespace`. + + // 6. Let sortedExports be a List whose elements are the elements of exports ordered as if an Array of the same values had been sorted using %Array.prototype.sort% using undefined as comparefn. + let mut exports = names + .into_iter() + .map(|sym| { + ( + context + .interner() + .resolve_expect(sym) + .into_common::(false), + sym, + ) + }) + .collect::>(); + exports.sort_keys(); + + // 2. Let internalSlotsList be the internal slots listed in Table 32. + // 3. Let M be MakeBasicObject(internalSlotsList). + // 4. Set M's essential internal methods to the definitions specified in 10.4.6. + // 5. Set M.[[Module]] to module. + // 7. Set M.[[Exports]] to sortedExports. + // 8. Create own properties of M corresponding to the definitions in 28.3. + let namespace = context.intrinsics().templates().namespace().create( + ObjectData::module_namespace(ModuleNamespace { module, exports }), + vec![js_string!("Module").into()], + ); + + // 9. Set module.[[Namespace]] to M. + // Ignored because this is done by `Module::namespace` + + // 10. Return M. + namespace + } + + /// Gets the export names of the Module Namespace object. + pub(crate) const fn exports(&self) -> &IndexMap> { + &self.exports + } + + /// Gest the module associated with this Module Namespace object. + pub(crate) const fn module(&self) -> &Module { + &self.module + } +} diff --git a/boa_engine/src/module/source.rs b/boa_engine/src/module/source.rs new file mode 100644 index 0000000000..b0fc490ca3 --- /dev/null +++ b/boa_engine/src/module/source.rs @@ -0,0 +1,1913 @@ +use std::{cell::Cell, collections::HashSet, hash::Hash, rc::Rc}; + +use boa_ast::{ + declaration::{ + ExportEntry, ImportEntry, ImportName, IndirectExportEntry, LexicalDeclaration, + LocalExportEntry, ReExportImportName, + }, + operations::{ + bound_names, contains, lexically_scoped_declarations, var_scoped_declarations, + ContainsSymbol, + }, + Declaration, ModuleItemList, +}; +use boa_gc::{custom_trace, empty_trace, Finalize, Gc, GcRefCell, Trace}; +use boa_interner::Sym; +use rustc_hash::{FxHashMap, FxHashSet}; + +use crate::{ + builtins::{promise::PromiseCapability, Promise}, + bytecompiler::{ByteCompiler, NodeKind}, + environments::{BindingLocator, CompileTimeEnvironment, EnvironmentStack}, + module::ModuleKind, + object::{FunctionObjectBuilder, JsPromise, RecursionLimiter}, + realm::Realm, + vm::{CallFrame, CodeBlock, CompletionRecord, Opcode}, + Context, JsArgs, JsError, JsNativeError, JsObject, JsResult, JsString, JsValue, NativeFunction, +}; + +use super::{ + BindingName, GraphLoadingState, Module, Referrer, ResolveExportError, ResolvedBinding, +}; + +/// Information for the [**Depth-first search**] algorithm used in the +/// [`Module::link`] and [`Module::evaluate`] methods. +#[derive(Clone, Copy, Debug, Finalize)] +pub(super) struct DfsInfo { + dfs_index: usize, + dfs_ancestor_index: usize, +} + +// SAFETY: `DfsInfo` only contains primitive types, making this safe. +unsafe impl Trace for DfsInfo { + empty_trace!(); +} + +/// Current status of a [`SourceTextModule`]. +/// +/// Roughly corresponds to the `[[Status]]` field of [**Cyclic Module Records**][cyclic], +/// but with a state machine-like design for better correctness. +/// +/// [cyclic]: https://tc39.es/ecma262/#table-cyclic-module-fields +#[derive(Debug, Finalize, Default)] +enum Status { + #[default] + Unlinked, + Linking { + info: DfsInfo, + }, + PreLinked { + context: SourceTextContext, + info: DfsInfo, + }, + Linked { + context: SourceTextContext, + info: DfsInfo, + }, + Evaluating { + context: SourceTextContext, + top_level_capability: Option, + cycle_root: SourceTextModule, + info: DfsInfo, + async_eval_index: Option, + }, + EvaluatingAsync { + context: SourceTextContext, + top_level_capability: Option, + cycle_root: SourceTextModule, + async_eval_index: usize, + pending_async_dependencies: usize, + }, + Evaluated { + top_level_capability: Option, + cycle_root: SourceTextModule, + error: Option, + }, +} + +// SAFETY: This must be synced with `Status` to mark any new data added that needs to be traced. +// `Status` doesn't implement `Drop`, making this manual implementation safe. +// +// The `Trace` macro adds an empty `Drop` implementation to encourage using `Finalize` instead. +// However, this has the downside of disallowing destructuring, which is pretty +// useful to have for state machines like `Status`. This is solved by manually implementing +// `Trace`. +unsafe impl Trace for Status { + custom_trace!(this, { + match this { + Status::Unlinked | Status::Linking { info: _ } => {} + Status::PreLinked { context, info: _ } | Status::Linked { context, info: _ } => { + mark(context); + } + Status::Evaluating { + top_level_capability, + cycle_root, + context, + info: _, + async_eval_index: _, + } + | Status::EvaluatingAsync { + top_level_capability, + cycle_root, + context, + pending_async_dependencies: _, + async_eval_index: _, + } => { + mark(top_level_capability); + mark(cycle_root); + mark(context); + } + Status::Evaluated { + top_level_capability, + cycle_root, + error, + } => { + mark(top_level_capability); + mark(cycle_root); + mark(error); + } + } + }); +} + +impl Status { + /// Gets the current index info of the module within the dependency graph, or `None` if the + /// module is not in a state executing the dfs algorithm. + const fn dfs_info(&self) -> Option<&DfsInfo> { + match self { + Status::Unlinked | Status::EvaluatingAsync { .. } | Status::Evaluated { .. } => None, + Status::Linking { info } + | Status::PreLinked { info, .. } + | Status::Linked { info, .. } + | Status::Evaluating { info, .. } => Some(info), + } + } + + /// Gets a mutable reference to the current index info of the module within the dependency graph, + /// or `None` if the module is not in a state executing the dfs algorithm. + fn dfs_info_mut(&mut self) -> Option<&mut DfsInfo> { + match self { + Status::Unlinked | Status::EvaluatingAsync { .. } | Status::Evaluated { .. } => None, + Status::Linking { info } + | Status::PreLinked { info, .. } + | Status::Linked { info, .. } + | Status::Evaluating { info, .. } => Some(info), + } + } + + /// If this module is the top module being evaluated and is in the evaluating state, gets its top + /// level capability. + const fn top_level_capability(&self) -> Option<&PromiseCapability> { + match &self { + Status::Unlinked + | Status::Linking { .. } + | Status::PreLinked { .. } + | Status::Linked { .. } => None, + Status::Evaluating { + top_level_capability, + .. + } + | Status::EvaluatingAsync { + top_level_capability, + .. + } + | Status::Evaluated { + top_level_capability, + .. + } => top_level_capability.as_ref(), + } + } + + /// If this module is in the evaluated state, gets its `error` field. + const fn evaluation_error(&self) -> Option<&JsError> { + match &self { + Status::Evaluated { error, .. } => error.as_ref(), + _ => None, + } + } + + /// If this module is in the evaluating state, gets its cycle root. + const fn cycle_root(&self) -> Option<&SourceTextModule> { + match &self { + Status::Evaluating { cycle_root, .. } + | Status::EvaluatingAsync { cycle_root, .. } + | Status::Evaluated { cycle_root, .. } => Some(cycle_root), + _ => None, + } + } + + /// Transition from one state to another, taking the current state by value to move data + /// between states. + fn transition(&mut self, f: F) + where + F: FnOnce(Status) -> Status, + { + *self = f(std::mem::take(self)); + } +} + +/// The execution context of a [`SourceTextModule`]. +/// +/// Stores the required context data that needs to be in place before executing the +/// inner code of the module. +#[derive(Clone, Finalize)] +struct SourceTextContext { + codeblock: Gc, + environments: EnvironmentStack, + realm: Realm, +} + +impl std::fmt::Debug for SourceTextContext { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("SourceTextContext") + .field("codeblock", &self.codeblock) + .field("environments", &self.environments) + .field("realm", &self.realm.addr()) + .finish() + } +} + +unsafe impl Trace for SourceTextContext { + custom_trace!(this, { + mark(&this.codeblock); + mark(&this.environments); + mark(&this.realm); + }); +} + +/// ECMAScript's [**Source Text Module Records**][spec]. +/// +/// [spec]: https://tc39.es/ecma262/#sec-source-text-module-records +#[derive(Clone, Trace, Finalize)] +pub(crate) struct SourceTextModule { + inner: Gc, +} + +impl std::fmt::Debug for SourceTextModule { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let limiter = RecursionLimiter::new(&*self.inner); + + if !limiter.visited && !limiter.live { + f.debug_struct("SourceTextModule") + .field("status", &self.inner.status) + .field("loaded_modules", &self.inner.loaded_modules) + .field("async_parent_modules", &self.inner.async_parent_modules) + .field("import_meta", &self.inner.import_meta) + .finish_non_exhaustive() + } else { + f.write_str("{ ... }") + } + } +} + +#[derive(Trace, Finalize)] +struct Inner { + parent: GcRefCell>, + status: GcRefCell, + loaded_modules: GcRefCell>, + async_parent_modules: GcRefCell>, + import_meta: GcRefCell>, + #[unsafe_ignore_trace] + code: ModuleCode, +} + +#[derive(Debug)] +struct ModuleCode { + has_tla: bool, + requested_modules: FxHashSet, + node: ModuleItemList, + import_entries: Vec, + local_export_entries: Vec, + indirect_export_entries: Vec, + star_export_entries: Vec, +} + +impl SourceTextModule { + /// Sets the parent module of this source module. + pub(super) fn set_parent(&self, parent: Module) { + *self.inner.parent.borrow_mut() = Some(parent); + } + + /// Gets the parent module of this source module. + fn parent(&self) -> Module { + self.inner + .parent + .borrow() + .clone() + .expect("parent module must be initialized") + } + + /// Creates a new `SourceTextModule` from a parsed `ModuleItemList`. + /// + /// Contains part of the abstract operation [`ParseModule`][parse]. + /// + /// [parse]: https://tc39.es/ecma262/#sec-parsemodule + pub(super) fn new(code: ModuleItemList) -> Self { + // 3. Let requestedModules be the ModuleRequests of body. + let requested_modules = code.requests(); + // 4. Let importEntries be ImportEntries of body. + let import_entries = code.import_entries(); + + // 5. Let importedBoundNames be ImportedLocalNames(importEntries). + // Can be ignored because this is just a simple `Iter::map` + + // 6. Let indirectExportEntries be a new empty List. + let mut indirect_export_entries = Vec::new(); + // 7. Let localExportEntries be a new empty List. + let mut local_export_entries = Vec::new(); + // 8. Let starExportEntries be a new empty List. + let mut star_export_entries = Vec::new(); + + // 10. For each ExportEntry Record ee of exportEntries, do + for ee in code.export_entries() { + match ee { + // a. If ee.[[ModuleRequest]] is null, then + ExportEntry::Ordinary(entry) => { + // ii. Else, + // 1. Let ie be the element of importEntries whose [[LocalName]] is ee.[[LocalName]]. + if let Some((module, import)) = + import_entries.iter().find_map(|ie| match ie.import_name() { + ImportName::Name(name) if ie.local_name() == entry.local_name() => { + Some((ie.module_request(), name)) + } + _ => None, + }) + { + // 3. Else, + // a. NOTE: This is a re-export of a single name. + // b. Append the ExportEntry Record { [[ModuleRequest]]: ie.[[ModuleRequest]], + // [[ImportName]]: ie.[[ImportName]], [[LocalName]]: null, + // [[ExportName]]: ee.[[ExportName]] } to indirectExportEntries. + indirect_export_entries.push(IndirectExportEntry::new( + module, + ReExportImportName::Name(import), + entry.export_name(), + )); + } else { + // i. If importedBoundNames does not contain ee.[[LocalName]], then + // 1. Append ee to localExportEntries. + + // 2. If ie.[[ImportName]] is namespace-object, then + // a. NOTE: This is a re-export of an imported module namespace object. + // b. Append ee to localExportEntries. + local_export_entries.push(entry); + } + } + // b. Else if ee.[[ImportName]] is all-but-default, then + ExportEntry::StarReExport { module_request } => { + // i. Assert: ee.[[ExportName]] is null. + // ii. Append ee to starExportEntries. + star_export_entries.push(module_request); + } + // c. Else, + // i. Append ee to indirectExportEntries. + ExportEntry::ReExport(entry) => indirect_export_entries.push(entry), + } + } + + // 11. Let async be body Contains await. + let has_tla = contains(&code, ContainsSymbol::AwaitExpression); + + // 12. Return Source Text Module Record { + // [[Realm]]: realm, [[Environment]]: empty, [[Namespace]]: empty, [[CycleRoot]]: empty, + // [[HasTLA]]: async, [[AsyncEvaluation]]: false, [[TopLevelCapability]]: empty, + // [[AsyncParentModules]]: « », [[PendingAsyncDependencies]]: empty, + // [[Status]]: new, [[EvaluationError]]: empty, [[HostDefined]]: hostDefined, + // [[ECMAScriptCode]]: body, [[Context]]: empty, [[ImportMeta]]: empty, + // [[RequestedModules]]: requestedModules, [[LoadedModules]]: « », + // [[ImportEntries]]: importEntries, [[LocalExportEntries]]: localExportEntries, + // [[IndirectExportEntries]]: indirectExportEntries, + // [[StarExportEntries]]: starExportEntries, + // [[DFSIndex]]: empty, [[DFSAncestorIndex]]: empty + // }. + // Most of this can be ignored, since `Status` takes care of the remaining state. + Self { + inner: Gc::new(Inner { + parent: GcRefCell::default(), + status: GcRefCell::default(), + loaded_modules: GcRefCell::default(), + async_parent_modules: GcRefCell::default(), + import_meta: GcRefCell::default(), + code: ModuleCode { + node: code, + requested_modules, + has_tla, + import_entries, + local_export_entries, + indirect_export_entries, + star_export_entries, + }, + }), + } + } + + /// Abstract operation [`InnerModuleLoading`][spec]. + /// + /// [spec]: https://tc39.es/ecma262/#sec-InnerModuleLoading + pub(super) fn inner_load(&self, state: &Rc, context: &mut Context<'_>) { + // 2. If module is a Cyclic Module Record, module.[[Status]] is new, and state.[[Visited]] does not contain + // module, then + // a. Append module to state.[[Visited]]. + if matches!(&*self.inner.status.borrow(), Status::Unlinked) + && state.visited.borrow_mut().insert(self.clone()) + { + // b. Let requestedModulesCount be the number of elements in module.[[RequestedModules]]. + let requested = &self.inner.code.requested_modules; + // c. Set state.[[PendingModulesCount]] to state.[[PendingModulesCount]] + requestedModulesCount. + state + .pending_modules + .set(state.pending_modules.get() + requested.len()); + // d. For each String required of module.[[RequestedModules]], do + for &required in requested { + // i. If module.[[LoadedModules]] contains a Record whose [[Specifier]] is required, then + let loaded = self.inner.loaded_modules.borrow().get(&required).cloned(); + if let Some(loaded) = loaded { + // 1. Let record be that Record. + // 2. Perform InnerModuleLoading(state, record.[[Module]]). + loaded.inner_load(state, context); + } else { + // ii. Else, + // 1. Perform HostLoadImportedModule(module, required, state.[[HostDefined]], state). + // 2. NOTE: HostLoadImportedModule will call FinishLoadingImportedModule, which re-enters + // the graph loading process through ContinueModuleLoading. + let name_specifier: JsString = context + .interner() + .resolve_expect(required) + .into_common(false); + let src = self.clone(); + let state = state.clone(); + context.module_loader().load_imported_module( + Referrer::Module(self.parent()), + name_specifier, + Box::new(move |completion, context| { + // FinishLoadingImportedModule ( referrer, specifier, payload, result ) + // https://tc39.es/ecma262/#sec-FinishLoadingImportedModule + + // 1. If result is a normal completion, then + if let Ok(loaded) = &completion { + // a. If referrer.[[LoadedModules]] contains a Record whose [[Specifier]] is specifier, then + // b. Else, + // i. Append the Record { [[Specifier]]: specifier, [[Module]]: result.[[Value]] } to referrer.[[LoadedModules]]. + let mut loaded_modules = src.inner.loaded_modules.borrow_mut(); + let entry = loaded_modules + .entry(required) + .or_insert_with(|| loaded.clone()); + + // i. Assert: That Record's [[Module]] is result.[[Value]]. + debug_assert_eq!(entry, loaded); + } + + // 2. If payload is a GraphLoadingState Record, then + // a. Perform ContinueModuleLoading(payload, result). + + // Abstract operation `ContinueModuleLoading ( state, moduleCompletion )`. + // + // https://tc39.es/ecma262/#sec-ContinueModuleLoading + + // 1. If state.[[IsLoading]] is false, return unused. + if !state.loading.get() { + return; + } + + // 2. If moduleCompletion is a normal completion, then + match completion { + Ok(m) => { + // a. Perform InnerModuleLoading(state, moduleCompletion.[[Value]]). + m.inner_load(&state, context); + } + // 3. Else, + Err(err) => { + // a. Set state.[[IsLoading]] to false. + state.loading.set(false); + + // b. Perform ! Call(state.[[PromiseCapability]].[[Reject]], undefined, « moduleCompletion.[[Value]] »). + state + .capability + .reject() + .call( + &JsValue::undefined(), + &[err.to_opaque(context)], + context, + ) + .expect("cannot fail for the default reject function"); + } + } + + // 4. Return unused. + }), + context, + ); + } + // iii. If state.[[IsLoading]] is false, return unused. + if !state.loading.get() { + return; + } + } + } + } + + /// Concrete method [`GetExportedNames ( [ exportStarSet ] )`][spec]. + /// + /// [spec]: https://tc39.es/ecma262/#sec-getexportednames + pub(super) fn get_exported_names( + &self, + export_star_set: &mut Vec, + ) -> FxHashSet { + // 1. Assert: module.[[Status]] is not new. + // 2. If exportStarSet is not present, set exportStarSet to a new empty List. + + // 3. If exportStarSet contains module, then + if export_star_set.contains(self) { + // a. Assert: We've reached the starting point of an export * circularity. + // b. Return a new empty List. + return FxHashSet::default(); + } + + // 4. Append module to exportStarSet. + export_star_set.push(self.clone()); + + // 5. Let exportedNames be a new empty List. + let mut exported_names = FxHashSet::default(); + + // 6. For each ExportEntry Record e of module.[[LocalExportEntries]], do + for e in &self.inner.code.local_export_entries { + // a. Assert: module provides the direct binding for this export. + // b. Append e.[[ExportName]] to exportedNames. + exported_names.insert(e.export_name()); + } + + // 7. For each ExportEntry Record e of module.[[IndirectExportEntries]], do + for e in &self.inner.code.indirect_export_entries { + // a. Assert: module imports a specific binding for this export. + // b. Append e.[[ExportName]] to exportedNames. + exported_names.insert(e.export_name()); + } + + // 8. For each ExportEntry Record e of module.[[StarExportEntries]], do + for e in &self.inner.code.star_export_entries { + // a. Let requestedModule be GetImportedModule(module, e.[[ModuleRequest]]). + let requested_module = self.inner.loaded_modules.borrow()[e].clone(); + + // b. Let starNames be requestedModule.GetExportedNames(exportStarSet). + // c. For each element n of starNames, do + for n in requested_module.get_exported_names(export_star_set) { + // i. If SameValue(n, "default") is false, then + if n != Sym::DEFAULT { + // 1. If exportedNames does not contain n, then + // a. Append n to exportedNames. + exported_names.insert(n); + } + } + } + + // 9. Return exportedNames. + exported_names + } + + /// Concrete method [`ResolveExport ( exportName [ , resolveSet ] )`][spec] + /// + /// [spec]: https://tc39.es/ecma262/#sec-resolveexport + #[allow(clippy::mutable_key_type)] + pub(super) fn resolve_export( + &self, + export_name: Sym, + resolve_set: &mut FxHashSet<(Module, Sym)>, + ) -> Result { + let parent = self.parent(); + // 1. Assert: module.[[Status]] is not new. + // 2. If resolveSet is not present, set resolveSet to a new empty List. + // 3. For each Record { [[Module]], [[ExportName]] } r of resolveSet, do + // a. If module and r.[[Module]] are the same Module Record and SameValue(exportName, r.[[ExportName]]) is true, then + if resolve_set.contains(&(parent.clone(), export_name)) { + // i. Assert: This is a circular import request. + // ii. Return null. + return Err(ResolveExportError::NotFound); + } + + // 4. Append the Record { [[Module]]: module, [[ExportName]]: exportName } to resolveSet. + resolve_set.insert((parent.clone(), export_name)); + + // 5. For each ExportEntry Record e of module.[[LocalExportEntries]], do + for e in &self.inner.code.local_export_entries { + // a. If SameValue(exportName, e.[[ExportName]]) is true, then + if export_name == e.export_name() { + // i. Assert: module provides the direct binding for this export. + // ii. Return ResolvedBinding Record { [[Module]]: module, [[BindingName]]: e.[[LocalName]] }. + return Ok(ResolvedBinding { + module: parent, + binding_name: BindingName::Name(e.local_name()), + }); + } + } + + // 6. For each ExportEntry Record e of module.[[IndirectExportEntries]], do + for e in &self.inner.code.indirect_export_entries { + // a. If SameValue(exportName, e.[[ExportName]]) is true, then + if export_name == e.export_name() { + // i. Let importedModule be GetImportedModule(module, e.[[ModuleRequest]]). + let imported_module = + self.inner.loaded_modules.borrow()[&e.module_request()].clone(); + return match e.import_name() { + // ii. If e.[[ImportName]] is all, then + // 1. Assert: module does not provide the direct binding for this export. + // 2. Return ResolvedBinding Record { [[Module]]: importedModule, [[BindingName]]: namespace }. + ReExportImportName::Star => Ok(ResolvedBinding { + module: imported_module, + binding_name: BindingName::Namespace, + }), + // iii. Else, + // 1. Assert: module imports a specific binding for this export. + // 2. Return importedModule.ResolveExport(e.[[ImportName]], resolveSet). + ReExportImportName::Name(_) => { + imported_module.resolve_export(export_name, resolve_set) + } + }; + } + } + + // 7. If SameValue(exportName, "default") is true, then + if export_name == Sym::DEFAULT { + // a. Assert: A default export was not explicitly defined by this module. + // b. Return null. + // c. NOTE: A default export cannot be provided by an export * from "mod" declaration. + return Err(ResolveExportError::NotFound); + } + + // 8. Let starResolution be null. + let mut star_resolution: Option = None; + + // 9. For each ExportEntry Record e of module.[[StarExportEntries]], do + for e in &self.inner.code.star_export_entries { + // a. Let importedModule be GetImportedModule(module, e.[[ModuleRequest]]). + let imported_module = self.inner.loaded_modules.borrow()[e].clone(); + // b. Let resolution be importedModule.ResolveExport(exportName, resolveSet). + let resolution = match imported_module.resolve_export(export_name, resolve_set) { + // d. If resolution is not null, then + Ok(resolution) => resolution, + // c. If resolution is ambiguous, return ambiguous. + Err(e @ ResolveExportError::Ambiguous) => return Err(e), + Err(ResolveExportError::NotFound) => continue, + }; + + // i. Assert: resolution is a ResolvedBinding Record. + if let Some(star_resolution) = &star_resolution { + // iii. Else, + // 1. Assert: There is more than one * import that includes the requested name. + // 2. If resolution.[[Module]] and starResolution.[[Module]] are not the same Module Record, + // return ambiguous. + if resolution.module != star_resolution.module { + return Err(ResolveExportError::Ambiguous); + } + match (resolution.binding_name, star_resolution.binding_name) { + // 3. If resolution.[[BindingName]] is not starResolution.[[BindingName]] and either + // resolution.[[BindingName]] or starResolution.[[BindingName]] is namespace, + // return ambiguous. + (BindingName::Namespace, BindingName::Name(_)) + | (BindingName::Name(_), BindingName::Namespace) => { + return Err(ResolveExportError::Ambiguous); + } + // 4. If resolution.[[BindingName]] is a String, starResolution.[[BindingName]] is a + // String, and SameValue(resolution.[[BindingName]], starResolution.[[BindingName]]) + // is false, return ambiguous. + (BindingName::Name(res), BindingName::Name(star)) if res != star => { + return Err(ResolveExportError::Ambiguous); + } + _ => {} + } + } else { + // ii. If starResolution is null, then + // 1. Set starResolution to resolution. + star_resolution = Some(resolution); + } + } + + // 10. Return starResolution. + star_resolution.ok_or(ResolveExportError::NotFound) + } + + /// Concrete method [`Link ( )`][spec]. + /// + /// [spec]: https://tc39.es/ecma262/#sec-moduledeclarationlinking + pub(super) fn link(&self, context: &mut Context<'_>) -> JsResult<()> { + // 1. Assert: module.[[Status]] is one of unlinked, linked, evaluating-async, or evaluated. + debug_assert!(matches!( + &*self.inner.status.borrow(), + Status::Unlinked + | Status::Linked { .. } + | Status::EvaluatingAsync { .. } + | Status::Evaluated { .. } + )); + + // 2. Let stack be a new empty List. + let mut stack = Vec::new(); + + // 3. Let result be Completion(InnerModuleLinking(module, stack, 0)). + // 4. If result is an abrupt completion, then + if let Err(err) = self.inner_link(&mut stack, 0, context) { + // a. For each Cyclic Module Record m of stack, do + for m in stack { + // i. Assert: m.[[Status]] is linking. + debug_assert!(matches!(&*m.inner.status.borrow(), Status::Linking { .. })); + // ii. Set m.[[Status]] to unlinked. + *m.inner.status.borrow_mut() = Status::Unlinked; + } + // b. Assert: module.[[Status]] is unlinked. + assert!(matches!(&*self.inner.status.borrow(), Status::Unlinked)); + // c. Return ? result. + return Err(err); + } + + // 5. Assert: module.[[Status]] is one of linked, evaluating-async, or evaluated. + debug_assert!(matches!( + &*self.inner.status.borrow(), + Status::Linked { .. } | Status::EvaluatingAsync { .. } | Status::Evaluated { .. } + )); + // 6. Assert: stack is empty. + assert!(stack.is_empty()); + + // 7. Return unused. + Ok(()) + } + + /// Abstract operation [`InnerModuleLinking ( module, stack, index )`][spec]. + /// + /// [spec]: https://tc39.es/ecma262/#sec-InnerModuleLinking + pub(super) fn inner_link( + &self, + stack: &mut Vec, + mut index: usize, + context: &mut Context<'_>, + ) -> JsResult { + // 2. If module.[[Status]] is one of linking, linked, evaluating-async, or evaluated, then + if matches!( + &*self.inner.status.borrow(), + Status::Linking { .. } + | Status::PreLinked { .. } + | Status::Linked { .. } + | Status::EvaluatingAsync { .. } + | Status::Evaluated { .. } + ) { + // a. Return index. + return Ok(index); + } + + // 3. Assert: module.[[Status]] is unlinked. + debug_assert!(matches!(&*self.inner.status.borrow(), Status::Unlinked)); + + // 4. Set module.[[Status]] to linking. + // 5. Set module.[[DFSIndex]] to index. + // 6. Set module.[[DFSAncestorIndex]] to index. + *self.inner.status.borrow_mut() = Status::Linking { + info: DfsInfo { + dfs_index: index, + dfs_ancestor_index: index, + }, + }; + + // 7. Set index to index + 1. + index += 1; + + // 8. Append module to stack. + stack.push(self.clone()); + + // 9. For each String required of module.[[RequestedModules]], do + + for required in &self.inner.code.requested_modules { + // a. Let requiredModule be GetImportedModule(module, required). + let required_module = self.inner.loaded_modules.borrow()[required].clone(); + + // b. Set index to ? InnerModuleLinking(requiredModule, stack, index). + index = required_module.inner_link(stack, index, context)?; + // c. If requiredModule is a Cyclic Module Record, then + if let ModuleKind::SourceText(required_module) = required_module.kind() { + // i. Assert: requiredModule.[[Status]] is one of linking, linked, evaluating-async, or evaluated. + // ii. Assert: requiredModule.[[Status]] is linking if and only if stack contains requiredModule. + debug_assert!(match &*required_module.inner.status.borrow() { + Status::PreLinked { .. } + | Status::Linked { .. } + | Status::EvaluatingAsync { .. } + | Status::Evaluated { .. } => true, + Status::Linking { .. } if stack.contains(required_module) => true, + _ => false, + }); + + // iii. If requiredModule.[[Status]] is linking, then + let required_index = if let Status::Linking { + info: + DfsInfo { + dfs_ancestor_index, .. + }, + } = &*required_module.inner.status.borrow() + { + // 1. Set module.[[DFSAncestorIndex]] to min(module.[[DFSAncestorIndex]], + // requiredModule.[[DFSAncestorIndex]]). + + Some(*dfs_ancestor_index) + } else { + None + }; + + if let Some(required_index) = required_index { + let mut status = self.inner.status.borrow_mut(); + + let DfsInfo { + dfs_ancestor_index, .. + } = status + .dfs_info_mut() + .expect("should be on the linking state"); + *dfs_ancestor_index = usize::min(*dfs_ancestor_index, required_index); + } + } + } + + // 10. Perform ? module.InitializeEnvironment(). + self.initialize_environment(context)?; + + // 11. Assert: module occurs exactly once in stack. + debug_assert_eq!(stack.iter().filter(|module| *module == self).count(), 1); + // 12. Assert: module.[[DFSAncestorIndex]] ≤ module.[[DFSIndex]]. + debug_assert!({ + let DfsInfo { + dfs_ancestor_index, + dfs_index, + } = self + .inner + .status + .borrow() + .dfs_info() + .copied() + .expect("should be linking"); + dfs_ancestor_index <= dfs_index + }); + + let info = self.inner.status.borrow().dfs_info().copied(); + match info { + // 13. If module.[[DFSAncestorIndex]] = module.[[DFSIndex]], then + + // a. Let done be false. + // b. Repeat, while done is false, + Some(info) if info.dfs_ancestor_index == info.dfs_index => loop { + // i. Let requiredModule be the last element of stack. + // ii. Remove the last element of stack. + // iii. Assert: requiredModule is a Cyclic Module Record. + let last = stack.pop().expect("should have at least one element"); + // iv. Set requiredModule.[[Status]] to linked. + last.inner + .status + .borrow_mut() + .transition(|current| match current { + Status::PreLinked { info, context } => Status::Linked { info, context }, + _ => { + unreachable!( + "can only transition to `Linked` from the `PreLinked` state" + ) + } + }); + + // v. If requiredModule and module are the same Module Record, set done to true. + if &last == self { + break; + } + }, + _ => {} + } + + // 14. Return index. + Ok(index) + } + + /// Concrete method [`Evaluate ( )`][spec]. + /// + /// [spec]: https://tc39.es/ecma262/#sec-moduleevaluation + pub(super) fn evaluate(&self, context: &mut Context<'_>) -> JsPromise { + // 1. Assert: This call to Evaluate is not happening at the same time as another call to Evaluate within the surrounding agent. + let (module, promise) = { + match &*self.inner.status.borrow() { + Status::Unlinked + | Status::Linking { .. } + | Status::PreLinked { .. } + | Status::Evaluating { .. } => { + unreachable!("2. Assert: module.[[Status]] is one of linked, evaluating-async, or evaluated.") + } + Status::Linked { .. } => (self.clone(), None), + // 3. If module.[[Status]] is either evaluating-async or evaluated, set module to module.[[CycleRoot]]. + Status::EvaluatingAsync { + cycle_root, + top_level_capability, + .. + } + | Status::Evaluated { + cycle_root, + top_level_capability, + .. + } => ( + cycle_root.clone(), + top_level_capability.as_ref().map(|cap| { + JsPromise::from_object(cap.promise().clone()) + .expect("promise created from the %Promise% intrinsic is always native") + }), + ), + } + }; + + // 4. If module.[[TopLevelCapability]] is not empty, then + if let Some(promise) = promise { + // a. Return module.[[TopLevelCapability]].[[Promise]]. + return promise; + } + + // 5. Let stack be a new empty List. + let mut stack = Vec::new(); + + // 6. Let capability be ! NewPromiseCapability(%Promise%). + // 7. Set module.[[TopLevelCapability]] to capability. + let capability = PromiseCapability::new( + &context.intrinsics().constructors().promise().constructor(), + context, + ) + .expect("capability creation must always succeed when using the `%Promise%` intrinsic"); + + // 8. Let result be Completion(InnerModuleEvaluation(module, stack, 0)). + let result = module.inner_evaluate(&mut stack, 0, Some(capability.clone()), context); + + match result { + Ok(_) => { + // 10. Else, + // a. Assert: module.[[Status]] is either evaluating-async or evaluated. + assert!(match &*module.inner.status.borrow() { + Status::EvaluatingAsync { .. } => true, + // b. Assert: module.[[EvaluationError]] is empty. + Status::Evaluated { error, .. } if error.is_none() => true, + _ => false, + }); + + // c. If module.[[AsyncEvaluation]] is false, then + if matches!(&*module.inner.status.borrow(), Status::Evaluated { .. }) { + // i. Assert: module.[[Status]] is evaluated. + // ii. Perform ! Call(capability.[[Resolve]], undefined, « undefined »). + capability + .resolve() + .call(&JsValue::undefined(), &[], context) + .expect("cannot fail for the default resolve function"); + } + + // d. Assert: stack is empty. + assert!(stack.is_empty()); + } + // 9. If result is an abrupt completion, then + Err(err) => { + // a. For each Cyclic Module Record m of stack, do + for m in stack { + m.inner.status.borrow_mut().transition(|current| match current { + // i. Assert: m.[[Status]] is evaluating. + Status::Evaluating { + top_level_capability, + cycle_root, + .. + } | Status::EvaluatingAsync { + top_level_capability, + cycle_root, + .. + } => { + // ii. Set m.[[Status]] to evaluated. + // iii. Set m.[[EvaluationError]] to result. + Status::Evaluated { + top_level_capability, + cycle_root, + error: Some(err.clone()), + } + }, + _ => panic!( + "can only transition to `Evaluated` from the `Evaluating` or `EvaluatingAsync states" + ), + }); + } + // b. Assert: module.[[Status]] is evaluated. + // c. Assert: module.[[EvaluationError]] is result. + assert!( + matches!(&*module.inner.status.borrow(), Status::Evaluated { error, .. } if error.is_some()) + ); + + // d. Perform ! Call(capability.[[Reject]], undefined, « result.[[Value]] »). + capability + .reject() + .call(&JsValue::undefined(), &[err.to_opaque(context)], context) + .expect("cannot fail for the default reject function"); + } + } + + // 11. Return capability.[[Promise]]. + JsPromise::from_object(capability.promise().clone()) + .expect("promise created from the %Promise% intrinsic is always native") + } + + /// Abstract operation [`InnerModuleEvaluation ( module, stack, index )`][spec] + /// + /// [spec]: https://tc39.es/ecma262/#sec-innermoduleevaluation + pub(super) fn inner_evaluate( + &self, + stack: &mut Vec, + mut index: usize, + capability: Option, + context: &mut Context<'_>, + ) -> JsResult { + /// Gets the next evaluation index of an async module. + /// + /// Returns an error if there's no more available indices. + fn get_async_eval_index() -> JsResult { + thread_local! { + static ASYNC_EVAL_QUEUE_INDEX: Cell = Cell::new(0); + } + + ASYNC_EVAL_QUEUE_INDEX + .with(|idx| { + let next = idx.get().checked_add(1)?; + Some(idx.replace(next)) + }) + .ok_or_else(|| { + JsNativeError::range() + .with_message("exceeded the maximum number of async modules") + .into() + }) + } + + // 2. If module.[[Status]] is either evaluating-async or evaluated, then + match &*self.inner.status.borrow() { + // 3. If module.[[Status]] is evaluating, return index. + Status::Evaluating { .. } | Status::EvaluatingAsync { .. } => return Ok(index), + // a. If module.[[EvaluationError]] is empty, return index. + // b. Otherwise, return ? module.[[EvaluationError]]. + Status::Evaluated { error, .. } => return error.clone().map_or(Ok(index), Err), + Status::Linked { .. } => { + // 4. Assert: module.[[Status]] is linked. + // evaluate a linked module + } + _ => unreachable!( + "2. Assert: module.[[Status]] is one of linked, evaluating-async, or evaluated." + ), + } + + let this = self.clone(); + // 5. Set module.[[Status]] to evaluating. + // 6. Set module.[[DFSIndex]] to index. + // 7. Set module.[[DFSAncestorIndex]] to index. + // 8. Set module.[[PendingAsyncDependencies]] to 0. + self.inner + .status + .borrow_mut() + .transition(|status| match status { + Status::Linked { context, .. } => Status::Evaluating { + context, + top_level_capability: capability, + cycle_root: this, + info: DfsInfo { + dfs_index: index, + dfs_ancestor_index: index, + }, + async_eval_index: None, + }, + _ => unreachable!("already asserted that this state is `Linked`. "), + }); + + // 9. Set index to index + 1. + index += 1; + + let mut pending_async_dependencies = 0; + // 10. Append module to stack. + stack.push(self.clone()); + + // 11. For each String required of module.[[RequestedModules]], do + for &required in &self.inner.code.requested_modules { + // a. Let requiredModule be GetImportedModule(module, required). + let required_module = self.inner.loaded_modules.borrow()[&required].clone(); + // b. Set index to ? InnerModuleEvaluation(requiredModule, stack, index). + index = required_module.inner_evaluate(stack, index, context)?; + + // c. If requiredModule is a Cyclic Module Record, then + if let ModuleKind::SourceText(required_module) = required_module.kind() { + // i. Assert: requiredModule.[[Status]] is one of evaluating, evaluating-async, or evaluated. + // ii. Assert: requiredModule.[[Status]] is evaluating if and only if stack contains requiredModule. + debug_assert!(match &*required_module.inner.status.borrow() { + Status::EvaluatingAsync { .. } | Status::Evaluated { .. } => true, + Status::Evaluating { .. } if stack.contains(required_module) => true, + _ => false, + }); + + let (required_module, async_eval, req_info) = match &*required_module.inner.status.borrow() { + // iii. If requiredModule.[[Status]] is evaluating, then + Status::Evaluating { + info, + async_eval_index, + .. + } => { + // 1. Set module.[[DFSAncestorIndex]] to min(module.[[DFSAncestorIndex]], requiredModule.[[DFSAncestorIndex]]). + (required_module.clone(), async_eval_index.is_some(), Some(*info)) + } + // iv. Else, + Status::EvaluatingAsync { cycle_root, .. } + | Status::Evaluated { cycle_root, .. } => { + // 1. Set requiredModule to requiredModule.[[CycleRoot]]. + // 2. Assert: requiredModule.[[Status]] is either evaluating-async or evaluated. + match &*cycle_root.inner.status.borrow() { + Status::EvaluatingAsync { .. } => (cycle_root.clone(), true, None), + // 3. If requiredModule.[[EvaluationError]] is not empty, return ? requiredModule.[[EvaluationError]]. + Status::Evaluated { error: Some(error), .. } => return Err(error.clone()), + Status::Evaluated { .. } => (cycle_root.clone(), false, None), + _ => unreachable!("2. Assert: requiredModule.[[Status]] is either evaluating-async or evaluated."), + } + } + _ => unreachable!("i. Assert: requiredModule.[[Status]] is one of evaluating, evaluating-async, or evaluated."), + }; + + if let Some(req_info) = req_info { + let mut status = self.inner.status.borrow_mut(); + let info = status + .dfs_info_mut() + .expect("self should still be in the evaluating state"); + info.dfs_ancestor_index = + usize::min(info.dfs_ancestor_index, req_info.dfs_ancestor_index); + } + + // v. If requiredModule.[[AsyncEvaluation]] is true, then + if async_eval { + // 1. Set module.[[PendingAsyncDependencies]] to module.[[PendingAsyncDependencies]] + 1. + pending_async_dependencies += 1; + // 2. Append module to requiredModule.[[AsyncParentModules]]. + required_module + .inner + .async_parent_modules + .borrow_mut() + .push(self.clone()); + } + } + } + + // 12. If module.[[PendingAsyncDependencies]] > 0 or module.[[HasTLA]] is true, then + if pending_async_dependencies > 0 || self.inner.code.has_tla { + // a. Assert: module.[[AsyncEvaluation]] is false and was never previously set to true. + { + let Status::Evaluating { async_eval_index, .. } = &mut *self.inner.status.borrow_mut() else { + unreachable!("self should still be in the evaluating state") + }; + + // b. Set module.[[AsyncEvaluation]] to true. + // c. NOTE: The order in which module records have their [[AsyncEvaluation]] fields transition to true is significant. (See 16.2.1.5.3.4.) + *async_eval_index = Some(get_async_eval_index()?); + } + + // d. If module.[[PendingAsyncDependencies]] = 0, perform ExecuteAsyncModule(module). + if pending_async_dependencies == 0 { + self.execute_async(context); + } + } else { + // 13. Else, + // a. Perform ? module.ExecuteModule(). + self.execute(None, context)?; + } + + let dfs_info = self.inner.status.borrow().dfs_info().copied().expect( + "haven't transitioned from the `Evaluating` state, so it should have its dfs info", + ); + + // 14. Assert: module occurs exactly once in stack. + debug_assert_eq!(stack.iter().filter(|m| *m == self).count(), 1); + // 15. Assert: module.[[DFSAncestorIndex]] ≤ module.[[DFSIndex]]. + assert!(dfs_info.dfs_ancestor_index <= dfs_info.dfs_index); + + // 16. If module.[[DFSAncestorIndex]] = module.[[DFSIndex]], then + if dfs_info.dfs_ancestor_index == dfs_info.dfs_index { + // a. Let done be false. + // b. Repeat, while done is false, + loop { + // i. Let requiredModule be the last element of stack. + // ii. Remove the last element of stack. + let required_module = stack + .pop() + .expect("should at least have `self` in the stack"); + let is_self = self == &required_module; + + // iii. Assert: requiredModule is a Cyclic Module Record. + required_module.inner.status.borrow_mut().transition(|current| match current { + Status::Evaluating { + top_level_capability, + cycle_root, + async_eval_index, + context, + .. + } => if let Some(async_eval_index) = async_eval_index { + // v. Otherwise, set requiredModule.[[Status]] to evaluating-async. + Status::EvaluatingAsync { + top_level_capability, + // vii. Set requiredModule.[[CycleRoot]] to module. + cycle_root: if is_self { + cycle_root + } else { + self.clone() + }, + async_eval_index, + pending_async_dependencies, + context + } + } else { + // iv. If requiredModule.[[AsyncEvaluation]] is false, set requiredModule.[[Status]] to evaluated. + Status::Evaluated { + top_level_capability, + cycle_root: if is_self { + cycle_root + } else { + self.clone() + }, + error: None, + } + } + _ => unreachable!( + "should only transition to `Evaluated` or `EvaluatingAsync` from the `Evaluating` state" + ) + } + ); + + // vi. If requiredModule and module are the same Module Record, set done to true. + if is_self { + break; + } + } + } + + // 17. Return index. + Ok(index) + } + + /// Abstract operation [`ExecuteAsyncModule ( module )`][spec]. + /// + /// [spec]: https://tc39.es/ecma262/#sec-execute-async-module + fn execute_async(&self, context: &mut Context<'_>) { + // 1. Assert: module.[[Status]] is either evaluating or evaluating-async. + debug_assert!(matches!( + &*self.inner.status.borrow(), + Status::Evaluating { .. } | Status::EvaluatingAsync { .. } + )); + // 2. Assert: module.[[HasTLA]] is true. + debug_assert!(self.inner.code.has_tla); + + // 3. Let capability be ! NewPromiseCapability(%Promise%). + let capability = PromiseCapability::new( + &context.intrinsics().constructors().promise().constructor(), + context, + ) + .expect("cannot fail for the %Promise% intrinsic"); + + // 4. Let fulfilledClosure be a new Abstract Closure with no parameters that captures module and performs the following steps when called: + // 5. Let onFulfilled be CreateBuiltinFunction(fulfilledClosure, 0, "", « »). + let on_fulfilled = FunctionObjectBuilder::new( + context, + NativeFunction::from_copy_closure_with_captures( + |_, _, module, context| { + // a. Perform AsyncModuleExecutionFulfilled(module). + async_module_execution_fulfilled(module, context); + // b. Return undefined. + Ok(JsValue::undefined()) + }, + self.clone(), + ), + ) + .build(); + + // 6. Let rejectedClosure be a new Abstract Closure with parameters (error) that captures module and performs the following steps when called: + // 7. Let onRejected be CreateBuiltinFunction(rejectedClosure, 0, "", « »). + let on_rejected = FunctionObjectBuilder::new( + context, + NativeFunction::from_copy_closure_with_captures( + |_, args, module, context| { + let error = JsError::from_opaque(args.get_or_undefined(0).clone()); + // a. Perform AsyncModuleExecutionRejected(module, error). + async_module_execution_rejected(module, &error, context); + // b. Return undefined. + Ok(JsValue::undefined()) + }, + self.clone(), + ), + ) + .build(); + + // 8. Perform PerformPromiseThen(capability.[[Promise]], onFulfilled, onRejected). + Promise::perform_promise_then( + capability.promise(), + Some(on_fulfilled), + Some(on_rejected), + None, + context, + ); + + // 9. Perform ! module.ExecuteModule(capability). + // 10. Return unused. + self.execute(Some(capability), context) + .expect("async modules cannot directly throw"); + } + + /// Abstract operation [`GatherAvailableAncestors ( module, execList )`][spec]. + /// + /// [spec]: https://tc39.es/ecma262/#sec-gather-available-ancestors + #[allow(clippy::mutable_key_type)] + fn gather_available_ancestors(&self, exec_list: &mut FxHashSet) { + // 1. For each Cyclic Module Record m of module.[[AsyncParentModules]], do + for m in &*self.inner.async_parent_modules.borrow() { + // a. If execList does not contain m and m.[[CycleRoot]].[[EvaluationError]] is empty, then + if !exec_list.contains(m) + && m.inner.status.borrow().cycle_root().map_or(false, |cr| { + cr.inner.status.borrow().evaluation_error().is_none() + }) + { + let (deps, has_tla) = { + // i. Assert: m.[[Status]] is evaluating-async. + // ii. Assert: m.[[EvaluationError]] is empty. + // iii. Assert: m.[[AsyncEvaluation]] is true. + let Status::EvaluatingAsync { pending_async_dependencies, .. } = &mut *m.inner.status.borrow_mut() else { + unreachable!("i. Assert: m.[[Status]] is evaluating-async."); + }; + // iv. Assert: m.[[PendingAsyncDependencies]] > 0. + assert!(*pending_async_dependencies > 0); + + // v. Set m.[[PendingAsyncDependencies]] to m.[[PendingAsyncDependencies]] - 1. + *pending_async_dependencies -= 1; + (*pending_async_dependencies, m.inner.code.has_tla) + }; + + // vi. If m.[[PendingAsyncDependencies]] = 0, then + if deps == 0 { + // 1. Append m to execList. + exec_list.insert(m.clone()); + // 2. If m.[[HasTLA]] is false, perform GatherAvailableAncestors(m, execList). + if !has_tla { + m.gather_available_ancestors(exec_list); + } + } + } + } + // 2. Return unused. + } + + /// Abstract operation [`InitializeEnvironment ( )`][spec]. + /// + /// [spec]: https://tc39.es/ecma262/#sec-source-text-module-record-initialize-environment + fn initialize_environment(&self, context: &mut Context<'_>) -> JsResult<()> { + #[derive(Debug)] + enum ImportBinding { + Namespace { + locator: BindingLocator, + module: Module, + }, + Single { + locator: BindingLocator, + export_locator: ResolvedBinding, + }, + } + + let parent = self.parent(); + + { + // 1. For each ExportEntry Record e of module.[[IndirectExportEntries]], do + for e in &self.inner.code.indirect_export_entries { + // a. Let resolution be module.ResolveExport(e.[[ExportName]]). + parent + .resolve_export(e.export_name(), &mut HashSet::default()) + // b. If resolution is either null or ambiguous, throw a SyntaxError exception. + .map_err(|err| match err { + ResolveExportError::NotFound => { + JsNativeError::syntax().with_message(format!( + "could not find export `{}`", + context.interner().resolve_expect(e.export_name()) + )) + } + ResolveExportError::Ambiguous => { + JsNativeError::syntax().with_message(format!( + "could not resolve ambiguous export `{}`", + context.interner().resolve_expect(e.export_name()) + )) + } + })?; + // c. Assert: resolution is a ResolvedBinding Record. + } + } + + // 2. Assert: All named exports from module are resolvable. + // 3. Let realm be module.[[Realm]]. + // 4. Assert: realm is not undefined. + let mut realm = parent.realm().clone(); + + // 5. Let env be NewModuleEnvironment(realm.[[GlobalEnv]]). + // 6. Set module.[[Environment]] to env. + let global_env = realm.environment().clone(); + let global_compile_env = global_env.compile_env(); + let module_compile_env = Gc::new(GcRefCell::new(CompileTimeEnvironment::new( + global_compile_env, + true, + ))); + + let mut compiler = + ByteCompiler::new(Sym::MAIN, true, false, module_compile_env.clone(), context); + let mut imports = Vec::new(); + + let codeblock = { + // 7. For each ImportEntry Record in of module.[[ImportEntries]], do + for entry in &self.inner.code.import_entries { + // a. Let importedModule be GetImportedModule(module, in.[[ModuleRequest]]). + let imported_module = + self.inner.loaded_modules.borrow()[&entry.module_request()].clone(); + + if let ImportName::Name(name) = entry.import_name() { + // c. Else, + // i. Let resolution be importedModule.ResolveExport(in.[[ImportName]]). + let resolution = + imported_module + .resolve_export(name, &mut HashSet::default()) + // ii. If resolution is either null or ambiguous, throw a SyntaxError exception. + .map_err(|err| match err { + ResolveExportError::NotFound => JsNativeError::syntax() + .with_message(format!( + "could not find export `{}`", + compiler.interner().resolve_expect(name) + )), + ResolveExportError::Ambiguous => JsNativeError::syntax() + .with_message(format!( + "could not resolve ambiguous export `{}`", + compiler.interner().resolve_expect(name) + )), + })?; + + // 2. Perform ! env.CreateImmutableBinding(in.[[LocalName]], true). + // 3. Perform ! env.InitializeBinding(in.[[LocalName]], namespace). + compiler.create_immutable_binding(entry.local_name(), true); + let locator = compiler.initialize_immutable_binding(entry.local_name()); + + if let BindingName::Name(_) = resolution.binding_name { + // 1. Perform env.CreateImportBinding(in.[[LocalName]], resolution.[[Module]], + // resolution.[[BindingName]]). + // deferred to initialization below + imports.push(ImportBinding::Single { + locator, + export_locator: resolution, + }); + } else { + // 1. Let namespace be GetModuleNamespace(resolution.[[Module]]). + // deferred to initialization below + imports.push(ImportBinding::Namespace { + locator, + module: imported_module, + }); + } + } else { + // b. If in.[[ImportName]] is namespace-object, then + // ii. Perform ! env.CreateImmutableBinding(in.[[LocalName]], true). + compiler.create_immutable_binding(entry.local_name(), true); + // iii. Perform ! env.InitializeBinding(in.[[LocalName]], namespace). + let locator = compiler.initialize_immutable_binding(entry.local_name()); + + // i. Let namespace be GetModuleNamespace(importedModule). + // deferred to initialization below + imports.push(ImportBinding::Namespace { + locator, + module: imported_module.clone(), + }); + } + } + + // 18. Let code be module.[[ECMAScriptCode]]. + // 19. Let varDeclarations be the VarScopedDeclarations of code. + let var_declarations = var_scoped_declarations(&self.inner.code.node); + // 20. Let declaredVarNames be a new empty List. + let mut declared_var_names = Vec::new(); + // 21. For each element d of varDeclarations, do + for var in var_declarations { + // a. For each element dn of the BoundNames of d, do + for name in var.bound_names() { + // i. If declaredVarNames does not contain dn, then + if !declared_var_names.contains(&name) { + // 1. Perform ! env.CreateMutableBinding(dn, false). + compiler.create_mutable_binding(name, false); + // 2. Perform ! env.InitializeBinding(dn, undefined). + let binding = compiler.initialize_mutable_binding(name, false); + let index = compiler.get_or_insert_binding(binding); + compiler.emit_opcode(Opcode::PushUndefined); + compiler.emit(Opcode::DefInitVar, &[index]); + // 3. Append dn to declaredVarNames. + declared_var_names.push(name); + } + } + } + + // 22. Let lexDeclarations be the LexicallyScopedDeclarations of code. + // 23. Let privateEnv be null. + let lex_declarations = lexically_scoped_declarations(&self.inner.code.node); + // 24. For each element d of lexDeclarations, do + for declaration in lex_declarations { + match &declaration { + // i. If IsConstantDeclaration of d is true, then + Declaration::Lexical(LexicalDeclaration::Const(declaration)) => { + // a. For each element dn of the BoundNames of d, do + for name in bound_names(declaration) { + // 1. Perform ! env.CreateImmutableBinding(dn, true). + compiler.create_immutable_binding(name, true); + } + } + // ii. Else, + Declaration::Lexical(LexicalDeclaration::Let(declaration)) => { + // a. For each element dn of the BoundNames of d, do + for name in bound_names(declaration) { + // 1. Perform ! env.CreateMutableBinding(dn, false). + compiler.create_mutable_binding(name, false); + } + } + // iii. If d is either a FunctionDeclaration, a GeneratorDeclaration, an + // AsyncFunctionDeclaration, or an AsyncGeneratorDeclaration, then + Declaration::Function(function) => { + // 1. Let fo be InstantiateFunctionObject of d with arguments env and privateEnv. + // 2. Perform ! env.InitializeBinding(dn, fo). + for name in bound_names(function) { + compiler.create_mutable_binding(name, false); + } + compiler.function(function.into(), NodeKind::Declaration, false); + } + Declaration::Generator(function) => { + // 1. Let fo be InstantiateFunctionObject of d with arguments env and privateEnv. + // 2. Perform ! env.InitializeBinding(dn, fo). + for name in bound_names(function) { + compiler.create_mutable_binding(name, false); + } + compiler.function(function.into(), NodeKind::Declaration, false); + } + Declaration::AsyncFunction(function) => { + // 1. Let fo be InstantiateFunctionObject of d with arguments env and privateEnv. + // 2. Perform ! env.InitializeBinding(dn, fo). + for name in bound_names(function) { + compiler.create_mutable_binding(name, false); + } + compiler.function(function.into(), NodeKind::Declaration, false); + } + Declaration::AsyncGenerator(function) => { + // 1. Let fo be InstantiateFunctionObject of d with arguments env and privateEnv. + // 2. Perform ! env.InitializeBinding(dn, fo). + for name in bound_names(function) { + compiler.create_mutable_binding(name, false); + } + compiler.function(function.into(), NodeKind::Declaration, false); + } + Declaration::Class(class) => { + // 1. Let fo be InstantiateFunctionObject of d with arguments env and privateEnv. + // 2. Perform ! env.InitializeBinding(dn, fo). + for name in bound_names(class) { + compiler.create_mutable_binding(name, false); + } + } + } + } + + compiler.compile_module_item_list(&self.inner.code.node); + + Gc::new(compiler.finish()) + }; + + // 8. Let moduleContext be a new ECMAScript code execution context. + // 12. Set the ScriptOrModule of moduleContext to module. + let mut envs = EnvironmentStack::new(global_env); + envs.push_module(module_compile_env); + + // 13. Set the VariableEnvironment of moduleContext to module.[[Environment]]. + // 14. Set the LexicalEnvironment of moduleContext to module.[[Environment]]. + // 15. Set the PrivateEnvironment of moduleContext to null. + std::mem::swap(&mut context.vm.environments, &mut envs); + let stack = std::mem::take(&mut context.vm.stack); + + // 9. Set the Function of moduleContext to null. + let active_function = context.vm.active_function.take(); + + // 10. Assert: module.[[Realm]] is not undefined. + // 11. Set the Realm of moduleContext to module.[[Realm]]. + context.swap_realm(&mut realm); + // 17. Push moduleContext onto the execution context stack; moduleContext is now the running execution context. + + // deferred initialization of import bindings + for import in imports { + match import { + ImportBinding::Namespace { locator, module } => { + // i. Let namespace be GetModuleNamespace(importedModule). + let namespace = module.namespace(context); + context.vm.environments.put_lexical_value( + locator.environment_index(), + locator.binding_index(), + namespace.into(), + ); + } + ImportBinding::Single { + locator, + export_locator, + } => match export_locator.binding_name() { + BindingName::Name(name) => context + .vm + .environments + .current() + .declarative_expect() + .kind() + .as_module() + .expect("last environment should be the module env") + .set_indirect(locator.binding_index(), export_locator.module, name), + BindingName::Namespace => { + let namespace = export_locator.module.namespace(context); + context.vm.environments.put_lexical_value( + locator.environment_index(), + locator.binding_index(), + namespace.into(), + ); + } + }, + } + } + + // 25. Remove moduleContext from the execution context stack. + std::mem::swap(&mut context.vm.environments, &mut envs); + context.vm.stack = stack; + context.vm.active_function = active_function; + context.swap_realm(&mut realm); + + debug_assert!(envs.current().as_declarative().is_some()); + *parent.inner.environment.borrow_mut() = envs.current().as_declarative().cloned(); + + // 16. Set module.[[Context]] to moduleContext. + self.inner + .status + .borrow_mut() + .transition(|state| match state { + Status::Linking { info } => Status::PreLinked { + info, + context: SourceTextContext { + codeblock, + environments: envs, + realm, + }, + }, + _ => unreachable!( + "should only transition to the `PreLinked` state from the `Linking` state" + ), + }); + + // 26. Return unused. + Ok(()) + } + + /// Abstract operation [`ExecuteModule ( [ capability ] )`][spec]. + /// + /// [spec]: https://tc39.es/ecma262/#sec-source-text-module-record-execute-module + fn execute( + &self, + capability: Option, + context: &mut Context<'_>, + ) -> JsResult<()> { + // 1. Let moduleContext be a new ECMAScript code execution context. + let SourceTextContext { + codeblock, + mut environments, + mut realm, + } = match &*self.inner.status.borrow() { + Status::Evaluating { context, .. } | Status::EvaluatingAsync { context, .. } => { + context.clone() + } + _ => unreachable!("`execute` should only be called for evaluating modules."), + }; + + let mut callframe = CallFrame::new(codeblock); + callframe.promise_capability = capability; + + // 4. Set the ScriptOrModule of moduleContext to module. + // 5. Assert: module has been linked and declarations in its module environment have been instantiated. + // 6. Set the VariableEnvironment of moduleContext to module.[[Environment]]. + // 7. Set the LexicalEnvironment of moduleContext to module.[[Environment]]. + std::mem::swap(&mut context.vm.environments, &mut environments); + let stack = std::mem::take(&mut context.vm.stack); + // 2. Set the Function of moduleContext to null. + let function = context.vm.active_function.take(); + // 3. Set the Realm of moduleContext to module.[[Realm]]. + context.swap_realm(&mut realm); + // 8. Suspend the running execution context. + context.vm.push_frame(callframe); + + // 9. If module.[[HasTLA]] is false, then + // a. Assert: capability is not present. + // b. Push moduleContext onto the execution context stack; moduleContext is now the running execution context. + // c. Let result be Completion(Evaluation of module.[[ECMAScriptCode]]). + // d. Suspend moduleContext and remove it from the execution context stack. + // e. Resume the context that is now on the top of the execution context stack as the running execution context. + // 10. Else, + // a. Assert: capability is a PromiseCapability Record. + // b. Perform AsyncBlockStart(capability, module.[[ECMAScriptCode]], moduleContext). + let result = context.run(); + + std::mem::swap(&mut context.vm.environments, &mut environments); + context.vm.stack = stack; + context.vm.active_function = function; + context.swap_realm(&mut realm); + context.vm.pop_frame(); + + // f. If result is an abrupt completion, then + if let CompletionRecord::Throw(err) = result { + // i. Return ? result. + Err(err) + } else { + // 11. Return unused. + Ok(()) + } + } +} + +/// Abstract operation [`AsyncModuleExecutionFulfilled ( module )`][spec]. +/// +/// [spec]: https://tc39.es/ecma262/#sec-async-module-execution-fulfilled +#[allow(clippy::mutable_key_type)] +fn async_module_execution_fulfilled(module: &SourceTextModule, context: &mut Context<'_>) { + // 1. If module.[[Status]] is evaluated, then + if let Status::Evaluated { error, .. } = &*module.inner.status.borrow() { + // a. Assert: module.[[EvaluationError]] is not empty. + assert!(error.is_some()); + // b. Return unused. + return; + } + + // 2. Assert: module.[[Status]] is evaluating-async. + // 3. Assert: module.[[AsyncEvaluation]] is true. + // 4. Assert: module.[[EvaluationError]] is empty. + // 5. Set module.[[AsyncEvaluation]] to false. + // 6. Set module.[[Status]] to evaluated. + module + .inner + .status + .borrow_mut() + .transition(|status| match status { + Status::EvaluatingAsync { + top_level_capability, + cycle_root, + .. + } => Status::Evaluated { + top_level_capability, + cycle_root, + error: None, + }, + _ => unreachable!(), + }); + + // 7. If module.[[TopLevelCapability]] is not empty, then + if let Some(cap) = module.inner.status.borrow().top_level_capability() { + // a. Assert: module.[[CycleRoot]] is module. + debug_assert_eq!(module.inner.status.borrow().cycle_root(), Some(module)); + + // b. Perform ! Call(module.[[TopLevelCapability]].[[Resolve]], undefined, « undefined »). + cap.resolve() + .call(&JsValue::undefined(), &[], context) + .expect("default `resolve` function cannot fail"); + } + + // 8. Let execList be a new empty List. + let mut ancestors = FxHashSet::default(); + + // 9. Perform GatherAvailableAncestors(module, execList). + module.gather_available_ancestors(&mut ancestors); + + // 11. Assert: All elements of sortedExecList have their [[AsyncEvaluation]] field set to true, [[PendingAsyncDependencies]] field set to 0, and [[EvaluationError]] field set to empty. + let mut ancestors = ancestors.into_iter().collect::>(); + + // 10. Let sortedExecList be a List whose elements are the elements of execList, in the order in which they had their [[AsyncEvaluation]] fields set to true in InnerModuleEvaluation. + ancestors.sort_by_cached_key(|m| { + let Status::EvaluatingAsync { async_eval_index, .. } = &*m.inner.status.borrow() else { + unreachable!("GatherAvailableAncestors: i. Assert: m.[[Status]] is evaluating-async."); + }; + + *async_eval_index + }); + + // 12. For each Cyclic Module Record m of sortedExecList, do + for m in ancestors { + // a. If m.[[Status]] is evaluated, then + if let Status::Evaluated { error, .. } = &*m.inner.status.borrow() { + // i. Assert: m.[[EvaluationError]] is not empty. + assert!(error.is_some()); + continue; + } + + // b. Else if m.[[HasTLA]] is true, then + let has_tla = m.inner.code.has_tla; + if has_tla { + // i. Perform ExecuteAsyncModule(m). + m.execute_async(context); + } else { + // c. Else, + // i. Let result be m.ExecuteModule(). + let result = m.execute(None, context); + + // ii. If result is an abrupt completion, then + if let Err(e) = result { + // 1. Perform AsyncModuleExecutionRejected(m, result.[[Value]]). + async_module_execution_rejected(module, &e, context); + } else { + // iii. Else, + // 1. Set m.[[Status]] to evaluated. + m.inner + .status + .borrow_mut() + .transition(|status| match status { + Status::EvaluatingAsync { + top_level_capability, + cycle_root, + .. + } => Status::Evaluated { + top_level_capability, + cycle_root, + error: None, + }, + _ => unreachable!(), + }); + + let status = m.inner.status.borrow(); + // 2. If m.[[TopLevelCapability]] is not empty, then + if let Some(cap) = status.top_level_capability() { + // a. Assert: m.[[CycleRoot]] is m. + debug_assert_eq!(status.cycle_root(), Some(&m)); + + // b. Perform ! Call(m.[[TopLevelCapability]].[[Resolve]], undefined, « undefined »). + cap.resolve() + .call(&JsValue::undefined(), &[], context) + .expect("default `resolve` function cannot fail"); + } + } + } + } + // 13. Return unused. +} + +/// Abstract operation [`AsyncModuleExecutionRejected ( module, error )`][spec]. +/// +/// [spec]: https://tc39.es/ecma262/#sec-async-module-execution-rejected +fn async_module_execution_rejected( + module: &SourceTextModule, + error: &JsError, + context: &mut Context<'_>, +) { + // 1. If module.[[Status]] is evaluated, then + if let Status::Evaluated { error, .. } = &*module.inner.status.borrow() { + // a. Assert: module.[[EvaluationError]] is not empty. + assert!(error.is_some()); + // b. Return unused. + return; + } + + // 2. Assert: module.[[Status]] is evaluating-async. + // 3. Assert: module.[[AsyncEvaluation]] is true. + // 4. Assert: module.[[EvaluationError]] is empty. + // 5. Set module.[[EvaluationError]] to ThrowCompletion(error). + // 6. Set module.[[Status]] to evaluated. + module + .inner + .status + .borrow_mut() + .transition(|status| match status { + Status::EvaluatingAsync { + top_level_capability, + cycle_root, + .. + } => Status::Evaluated { + top_level_capability, + cycle_root, + error: Some(error.clone()), + }, + _ => unreachable!(), + }); + + // 7. For each Cyclic Module Record m of module.[[AsyncParentModules]], do + for m in &*module.inner.async_parent_modules.borrow() { + // a. Perform AsyncModuleExecutionRejected(m, error). + async_module_execution_rejected(m, error, context); + } + + let status = module.inner.status.borrow(); + // 8. If module.[[TopLevelCapability]] is not empty, then + if let Some(cap) = status.top_level_capability() { + // a. Assert: module.[[CycleRoot]] is module. + debug_assert_eq!(status.cycle_root(), Some(module)); + + // b. Perform ! Call(module.[[TopLevelCapability]].[[Reject]], undefined, « error »). + cap.reject() + .call(&JsValue::undefined(), &[error.to_opaque(context)], context) + .expect("default `reject` function cannot fail"); + } + // 9. Return unused. +} + +impl PartialEq for SourceTextModule { + fn eq(&self, other: &Self) -> bool { + std::ptr::eq(self.inner.as_ref(), other.inner.as_ref()) + } +} + +impl Eq for SourceTextModule {} + +impl Hash for SourceTextModule { + fn hash(&self, state: &mut H) { + std::ptr::hash(self.inner.as_ref(), state); + } +} diff --git a/boa_engine/src/object/internal_methods/immutable_prototype.rs b/boa_engine/src/object/internal_methods/immutable_prototype.rs new file mode 100644 index 0000000000..f9cf56c741 --- /dev/null +++ b/boa_engine/src/object/internal_methods/immutable_prototype.rs @@ -0,0 +1,39 @@ +use crate::{ + object::{JsObject, JsPrototype}, + Context, JsResult, +}; + +use super::{InternalObjectMethods, ORDINARY_INTERNAL_METHODS}; + +/// Definitions of the internal object methods for [**Immutable Prototype Exotic Objects**][spec]. +/// +/// [spec]: https://tc39.es/ecma262/#sec-immutable-prototype-exotic-objects +pub(crate) static IMMUTABLE_PROTOTYPE_EXOTIC_INTERNAL_METHODS: InternalObjectMethods = + InternalObjectMethods { + __set_prototype_of__: immutable_prototype_exotic_set_prototype_of, + ..ORDINARY_INTERNAL_METHODS + }; + +/// [`[[SetPrototypeOf]] ( V )`][spec]. +/// +/// [spec]: https://tc39.es/ecma262/#sec-immutable-prototype-exotic-objects-setprototypeof-v +#[allow(clippy::needless_pass_by_value)] +pub(crate) fn immutable_prototype_exotic_set_prototype_of( + obj: &JsObject, + val: JsPrototype, + context: &mut Context<'_>, +) -> JsResult { + // 1. Return ? SetImmutablePrototype(O, V). + + // inlined since other implementations can just use `set_prototype_of` directly. + + // SetImmutablePrototype ( O, V ) + // + + // 1. Let current be ? O.[[GetPrototypeOf]](). + let current = obj.__get_prototype_of__(context)?; + + // 2. If SameValue(V, current) is true, return true. + // 3. Return false. + Ok(val == current) +} diff --git a/boa_engine/src/object/internal_methods/mod.rs b/boa_engine/src/object/internal_methods/mod.rs index 2660b4c444..976f282422 100644 --- a/boa_engine/src/object/internal_methods/mod.rs +++ b/boa_engine/src/object/internal_methods/mod.rs @@ -19,7 +19,9 @@ pub(super) mod arguments; pub(super) mod array; pub(super) mod bound_function; pub(super) mod function; +pub(super) mod immutable_prototype; pub(super) mod integer_indexed; +pub(super) mod module_namespace; pub(super) mod proxy; pub(super) mod string; diff --git a/boa_engine/src/object/internal_methods/module_namespace.rs b/boa_engine/src/object/internal_methods/module_namespace.rs new file mode 100644 index 0000000000..ceb3e53b60 --- /dev/null +++ b/boa_engine/src/object/internal_methods/module_namespace.rs @@ -0,0 +1,329 @@ +use std::collections::HashSet; + +use crate::{ + module::BindingName, + object::{JsObject, JsPrototype}, + property::{PropertyDescriptor, PropertyKey}, + Context, JsNativeError, JsResult, JsValue, +}; + +use super::{ + immutable_prototype, ordinary_define_own_property, ordinary_delete, ordinary_get, + ordinary_get_own_property, ordinary_has_property, ordinary_own_property_keys, + InternalObjectMethods, ORDINARY_INTERNAL_METHODS, +}; + +/// Definitions of the internal object methods for [**Module Namespace Exotic Objects**][spec]. +/// +/// [spec]: https://tc39.es/ecma262/#sec-module-namespace-exotic-objects +pub(crate) static MODULE_NAMESPACE_EXOTIC_INTERNAL_METHODS: InternalObjectMethods = + InternalObjectMethods { + __get_prototype_of__: module_namespace_exotic_get_prototype_of, + __set_prototype_of__: module_namespace_exotic_set_prototype_of, + __is_extensible__: module_namespace_exotic_is_extensible, + __prevent_extensions__: module_namespace_exotic_prevent_extensions, + __get_own_property__: module_namespace_exotic_get_own_property, + __define_own_property__: module_namespace_exotic_define_own_property, + __has_property__: module_namespace_exotic_has_property, + __get__: module_namespace_exotic_get, + __set__: module_namespace_exotic_set, + __delete__: module_namespace_exotic_delete, + __own_property_keys__: module_namespace_exotic_own_property_keys, + ..ORDINARY_INTERNAL_METHODS + }; + +/// [`[[GetPrototypeOf]] ( )`][spec]. +/// +/// [spec]: https://tc39.es/ecma262/#sec-module-namespace-exotic-objects-getprototypeof +#[allow(clippy::unnecessary_wraps)] +fn module_namespace_exotic_get_prototype_of( + _: &JsObject, + _: &mut Context<'_>, +) -> JsResult { + // 1. Return null. + Ok(None) +} + +/// [`[[SetPrototypeOf]] ( V )`][spec]. +/// +/// [spec]: https://tc39.es/ecma262/#sec-module-namespace-exotic-objects-setprototypeof-v +#[allow(clippy::unnecessary_wraps)] +fn module_namespace_exotic_set_prototype_of( + obj: &JsObject, + val: JsPrototype, + context: &mut Context<'_>, +) -> JsResult { + // 1. Return ! SetImmutablePrototype(O, V). + Ok( + immutable_prototype::immutable_prototype_exotic_set_prototype_of(obj, val, context) + .expect("this must not fail per the spec"), + ) +} + +/// [`[[IsExtensible]] ( )`][spec]. +/// +/// [spec]: https://tc39.es/ecma262/#sec-module-namespace-exotic-objects-isextensible +#[allow(clippy::unnecessary_wraps)] +fn module_namespace_exotic_is_extensible(_: &JsObject, _: &mut Context<'_>) -> JsResult { + // 1. Return false. + Ok(false) +} + +/// [`[[PreventExtensions]] ( )`][spec]. +/// +/// [spec]: https://tc39.es/ecma262/#sec-module-namespace-exotic-objects-preventextensions +#[allow(clippy::unnecessary_wraps)] +fn module_namespace_exotic_prevent_extensions(_: &JsObject, _: &mut Context<'_>) -> JsResult { + Ok(true) +} + +/// [`[[GetOwnProperty]] ( P )`][spec] +/// +/// [spec]: https://tc39.es/ecma262/#sec-module-namespace-exotic-objects-getownproperty-p +fn module_namespace_exotic_get_own_property( + obj: &JsObject, + key: &PropertyKey, + context: &mut Context<'_>, +) -> JsResult> { + // 1. If P is a Symbol, return OrdinaryGetOwnProperty(O, P). + let key = match key { + PropertyKey::Symbol(_) => return ordinary_get_own_property(obj, key, context), + PropertyKey::Index(_) => return Ok(None), + PropertyKey::String(s) => s, + }; + + { + let obj = obj.borrow(); + let obj = obj + .as_module_namespace() + .expect("internal method can only be called on module namespace objects"); + // 2. Let exports be O.[[Exports]]. + let exports = obj.exports(); + + // 3. If exports does not contain P, return undefined. + if !exports.contains_key(key) { + return Ok(None); + } + } + + // 4. Let value be ? O.[[Get]](P, O). + let value = obj.get(key.clone(), context)?; + + // 5. Return PropertyDescriptor { [[Value]]: value, [[Writable]]: true, [[Enumerable]]: true, [[Configurable]]: false }. + Ok(Some( + PropertyDescriptor::builder() + .value(value) + .writable(true) + .enumerable(true) + .configurable(false) + .build(), + )) +} + +/// [`[[DefineOwnProperty]] ( P, Desc )`][spec] +/// +/// [spec]: https://tc39.es/ecma262/#sec-module-namespace-exotic-objects-defineownproperty-p-desc +fn module_namespace_exotic_define_own_property( + obj: &JsObject, + key: &PropertyKey, + desc: PropertyDescriptor, + context: &mut Context<'_>, +) -> JsResult { + // 1. If P is a Symbol, return ! OrdinaryDefineOwnProperty(O, P, Desc). + if let PropertyKey::Symbol(_) = key { + return ordinary_define_own_property(obj, key, desc, context); + } + + // 2. Let current be ? O.[[GetOwnProperty]](P). + let Some(current) = obj.__get_own_property__(key, context)? else { + // 3. If current is undefined, return false. + return Ok(false); + }; + + // 4. If Desc has a [[Configurable]] field and Desc.[[Configurable]] is true, return false. + // 5. If Desc has an [[Enumerable]] field and Desc.[[Enumerable]] is false, return false. + // 6. If IsAccessorDescriptor(Desc) is true, return false. + // 7. If Desc has a [[Writable]] field and Desc.[[Writable]] is false, return false. + if desc.configurable() == Some(true) + || desc.enumerable() == Some(false) + || desc.is_accessor_descriptor() + || desc.writable() == Some(false) + { + return Ok(false); + } + + // 8. If Desc has a [[Value]] field, return SameValue(Desc.[[Value]], current.[[Value]]). + // 9. Return true. + Ok(desc.value().map_or(true, |v| v == current.expect_value())) +} + +/// [`[[HasProperty]] ( P )`][spec] +/// +/// [spec]: https://tc39.es/ecma262/#sec-module-namespace-exotic-objects-hasproperty-p +fn module_namespace_exotic_has_property( + obj: &JsObject, + key: &PropertyKey, + context: &mut Context<'_>, +) -> JsResult { + // 1. If P is a Symbol, return ! OrdinaryHasProperty(O, P). + let key = match key { + PropertyKey::Symbol(_) => return ordinary_has_property(obj, key, context), + PropertyKey::Index(_) => return Ok(false), + PropertyKey::String(s) => s, + }; + + let obj = obj.borrow(); + let obj = obj + .as_module_namespace() + .expect("internal method can only be called on module namespace objects"); + + // 2. Let exports be O.[[Exports]]. + let exports = obj.exports(); + + // 3. If exports contains P, return true. + // 4. Return false. + Ok(exports.contains_key(key)) +} + +/// [`[[Get]] ( P, Receiver )`][spec] +/// +/// [spec]: https://tc39.es/ecma262/#sec-module-namespace-exotic-objects-get-p-receiver +fn module_namespace_exotic_get( + obj: &JsObject, + key: &PropertyKey, + receiver: JsValue, + context: &mut Context<'_>, +) -> JsResult { + // 1. If P is a Symbol, then + // a. Return ! OrdinaryGet(O, P, Receiver). + let key = match key { + PropertyKey::Symbol(_) => return ordinary_get(obj, key, receiver, context), + PropertyKey::Index(_) => return Ok(JsValue::undefined()), + PropertyKey::String(s) => s, + }; + + let obj = obj.borrow(); + let obj = obj + .as_module_namespace() + .expect("internal method can only be called on module namespace objects"); + + // 2. Let exports be O.[[Exports]]. + let exports = obj.exports(); + // 3. If exports does not contain P, return undefined. + let Some(export_name) = exports.get(key).copied() else { + return Ok(JsValue::undefined()); + }; + + // 4. Let m be O.[[Module]]. + let m = obj.module(); + + // 5. Let binding be m.ResolveExport(P). + let binding = m + .resolve_export(export_name, &mut HashSet::default()) + .expect("6. Assert: binding is a ResolvedBinding Record."); + + // 7. Let targetModule be binding.[[Module]]. + // 8. Assert: targetModule is not undefined. + let target_module = binding.module(); + + // TODO: cache binding resolution instead of doing the whole process on every access. + if let BindingName::Name(name) = binding.binding_name() { + // 10. Let targetEnv be targetModule.[[Environment]]. + let Some(env) = target_module.environment() else { + // 11. If targetEnv is empty, throw a ReferenceError exception. + let import = context.interner().resolve_expect(export_name); + return Err(JsNativeError::reference().with_message( + format!("cannot get import `{import}` from an uninitialized module") + ).into()); + }; + + let locator = env + .compile_env() + .borrow() + .get_binding(name) + .expect("checked before that the name was reachable"); + + // 12. Return ? targetEnv.GetBindingValue(binding.[[BindingName]], true). + env.get(locator.binding_index()).ok_or_else(|| { + let import = context.interner().resolve_expect(export_name); + + JsNativeError::reference() + .with_message(format!("cannot get uninitialized import `{import}`")) + .into() + }) + } else { + // 9. If binding.[[BindingName]] is namespace, then + // a. Return GetModuleNamespace(targetModule). + Ok(target_module.namespace(context).into()) + } +} + +/// [`[[Set]] ( P, V, Receiver )`][spec]. +/// +/// [spec]: https://tc39.es/ecma262/#sec-module-namespace-exotic-objects-set-p-v-receiver +#[allow(clippy::needless_pass_by_value, clippy::unnecessary_wraps)] +fn module_namespace_exotic_set( + _obj: &JsObject, + _key: PropertyKey, + _value: JsValue, + _receiver: JsValue, + _context: &mut Context<'_>, +) -> JsResult { + // 1. Return false. + Ok(false) +} + +/// [`[[Delete]] ( P )`][spec]. +/// +/// [spec]: https://tc39.es/ecma262/#sec-module-namespace-exotic-objects-delete-p +fn module_namespace_exotic_delete( + obj: &JsObject, + key: &PropertyKey, + context: &mut Context<'_>, +) -> JsResult { + // 1. If P is a Symbol, then + // a. Return ! OrdinaryDelete(O, P). + let key = match key { + PropertyKey::Symbol(_) => return ordinary_delete(obj, key, context), + PropertyKey::Index(_) => return Ok(true), + PropertyKey::String(s) => s, + }; + + let obj = obj.borrow(); + let obj = obj + .as_module_namespace() + .expect("internal method can only be called on module namespace objects"); + + // 2. Let exports be O.[[Exports]]. + let exports = obj.exports(); + + // 3. If exports contains P, return false. + // 4. Return true. + Ok(!exports.contains_key(key)) +} + +/// [`[[OwnPropertyKeys]] ( )`][spec]. +/// +/// [spec]: https://tc39.es/ecma262/#sec-module-namespace-exotic-objects-ownpropertykeys +fn module_namespace_exotic_own_property_keys( + obj: &JsObject, + context: &mut Context<'_>, +) -> JsResult> { + // 2. Let symbolKeys be OrdinaryOwnPropertyKeys(O). + let symbol_keys = ordinary_own_property_keys(obj, context)?; + + let obj = obj.borrow(); + let obj = obj + .as_module_namespace() + .expect("internal method can only be called on module namespace objects"); + + // 1. Let exports be O.[[Exports]]. + let exports = obj.exports(); + + // 3. Return the list-concatenation of exports and symbolKeys. + Ok(exports + .keys() + .map(|k| PropertyKey::String(k.clone())) + .chain(symbol_keys) + .collect()) +} diff --git a/boa_engine/src/object/jsobject.rs b/boa_engine/src/object/jsobject.rs index 9ad7cd52f6..df62ce1b06 100644 --- a/boa_engine/src/object/jsobject.rs +++ b/boa_engine/src/object/jsobject.rs @@ -237,7 +237,7 @@ impl JsObject { // a recursive structure // We can follow v8 & SpiderMonkey's lead and return a default value for the hint in this situation // (see https://repl.it/repls/IvoryCircularCertification#index.js) - let recursion_limiter = RecursionLimiter::new(self); + let recursion_limiter = RecursionLimiter::new(self.as_ref()); if recursion_limiter.live { // we're in a recursive object, bail return Ok(match hint { @@ -1053,15 +1053,15 @@ thread_local! { } impl RecursionLimiter { - /// Determines if the specified `JsObject` has been visited, and returns a struct that will free it when dropped. + /// Determines if the specified `T` has been visited, and returns a struct that will free it when dropped. /// - /// This is done by maintaining a thread-local hashset containing the pointers of `JsObject` values that have been - /// visited. The first `JsObject` visited will clear the hashset, while any others will check if they are contained + /// This is done by maintaining a thread-local hashset containing the pointers of `T` values that have been + /// visited. The first `T` visited will clear the hashset, while any others will check if they are contained /// by the hashset. - pub fn new(o: &JsObject) -> Self { + pub fn new(o: &T) -> Self { // We shouldn't have to worry too much about this being moved during Debug::fmt. #[allow(trivial_casts)] - let ptr = (o.as_ref() as *const _) as usize; + let ptr = (o as *const _) as usize; let (top_level, visited, live) = SEEN.with(|hm| { let mut hm = hm.borrow_mut(); let top_level = hm.is_empty(); @@ -1086,7 +1086,7 @@ impl RecursionLimiter { impl Debug for JsObject { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> std::fmt::Result { let ptr: *const _ = self.vtable(); - let limiter = RecursionLimiter::new(self); + let limiter = RecursionLimiter::new(self.as_ref()); // Typically, using `!limiter.live` would be good enough here. // However, the JS object hierarchy involves quite a bit of repitition, and the sheer amount of data makes diff --git a/boa_engine/src/object/mod.rs b/boa_engine/src/object/mod.rs index a9b8a1f57f..39033caeed 100644 --- a/boa_engine/src/object/mod.rs +++ b/boa_engine/src/object/mod.rs @@ -16,7 +16,9 @@ use self::{ BOUND_CONSTRUCTOR_EXOTIC_INTERNAL_METHODS, BOUND_FUNCTION_EXOTIC_INTERNAL_METHODS, }, function::{CONSTRUCTOR_INTERNAL_METHODS, FUNCTION_INTERNAL_METHODS}, + immutable_prototype::IMMUTABLE_PROTOTYPE_EXOTIC_INTERNAL_METHODS, integer_indexed::INTEGER_INDEXED_EXOTIC_INTERNAL_METHODS, + module_namespace::MODULE_NAMESPACE_EXOTIC_INTERNAL_METHODS, proxy::{ PROXY_EXOTIC_INTERNAL_METHODS_ALL, PROXY_EXOTIC_INTERNAL_METHODS_BASIC, PROXY_EXOTIC_INTERNAL_METHODS_WITH_CALL, @@ -55,6 +57,7 @@ use crate::{ DataView, Date, Promise, RegExp, }, js_string, + module::ModuleNamespace, native_function::NativeFunction, property::{Attribute, PropertyDescriptor, PropertyKey}, string::utf16, @@ -307,6 +310,9 @@ pub enum ObjectKind { /// The `WeakSet` object kind. WeakSet(boa_gc::WeakMap), + /// The `ModuleNamespace` object kind. + ModuleNamespace(ModuleNamespace), + /// The `Intl.Collator` object kind. #[cfg(feature = "intl")] Collator(Box), @@ -362,6 +368,7 @@ unsafe impl Trace for ObjectKind { Self::WeakRef(wr) => mark(wr), Self::WeakMap(wm) => mark(wm), Self::WeakSet(ws) => mark(ws), + Self::ModuleNamespace(m) => mark(m), #[cfg(feature = "intl")] Self::DateTimeFormat(f) => mark(f), #[cfg(feature = "intl")] @@ -388,6 +395,14 @@ unsafe impl Trace for ObjectKind { } impl ObjectData { + /// Create the immutable `%Object.prototype%` object data + pub(crate) fn object_prototype() -> Self { + Self { + kind: ObjectKind::Ordinary, + internal_methods: &IMMUTABLE_PROTOTYPE_EXOTIC_INTERNAL_METHODS, + } + } + /// Create the `AsyncFromSyncIterator` object data pub fn async_from_sync_iterator(async_from_sync_iterator: AsyncFromSyncIterator) -> Self { Self { @@ -694,6 +709,7 @@ impl ObjectData { } /// Creates the `IntegerIndexed` object data + #[must_use] pub fn integer_indexed(integer_indexed: IntegerIndexed) -> Self { Self { kind: ObjectKind::IntegerIndexed(integer_indexed), @@ -701,6 +717,15 @@ impl ObjectData { } } + /// Creates the `ModuleNamespace` object data + #[must_use] + pub fn module_namespace(namespace: ModuleNamespace) -> Self { + Self { + kind: ObjectKind::ModuleNamespace(namespace), + internal_methods: &MODULE_NAMESPACE_EXOTIC_INTERNAL_METHODS, + } + } + /// Create the `Collator` object data #[cfg(feature = "intl")] #[must_use] @@ -811,6 +836,7 @@ impl Debug for ObjectKind { Self::WeakRef(_) => "WeakRef", Self::WeakMap(_) => "WeakMap", Self::WeakSet(_) => "WeakSet", + Self::ModuleNamespace(_) => "ModuleNamespace", #[cfg(feature = "intl")] Self::Collator(_) => "Collator", #[cfg(feature = "intl")] @@ -1550,6 +1576,24 @@ impl Object { } } + /// Gets a reference to the module namespace if the object is a `ModuleNamespace`. + #[inline] + pub const fn as_module_namespace(&self) -> Option<&ModuleNamespace> { + match &self.kind { + ObjectKind::ModuleNamespace(ns) => Some(ns), + _ => None, + } + } + + /// Gets a mutable reference module namespace if the object is a `ModuleNamespace`. + #[inline] + pub fn as_module_namespace_mut(&mut self) -> Option<&mut ModuleNamespace> { + match &mut self.kind { + ObjectKind::ModuleNamespace(ns) => Some(ns), + _ => None, + } + } + /// Gets the `Collator` data if the object is a `Collator`. #[inline] #[cfg(feature = "intl")] diff --git a/boa_engine/src/realm.rs b/boa_engine/src/realm.rs index ee2a871c6e..f777289972 100644 --- a/boa_engine/src/realm.rs +++ b/boa_engine/src/realm.rs @@ -9,12 +9,13 @@ use crate::{ context::{intrinsics::Intrinsics, HostHooks}, environments::DeclarativeEnvironment, + module::Module, object::{shape::shared_shape::SharedShape, JsObject}, + JsString, }; use boa_gc::{Finalize, Gc, GcRefCell, Trace}; use boa_profiler::Profiler; use rustc_hash::FxHashMap; -use std::fmt; /// Representation of a Realm. /// @@ -32,8 +33,8 @@ impl PartialEq for Realm { } } -impl fmt::Debug for Realm { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { +impl std::fmt::Debug for Realm { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.debug_struct("Realm") .field("intrinsics", &self.inner.intrinsics) .field("environment", &self.inner.environment) @@ -50,6 +51,7 @@ struct Inner { global_object: JsObject, global_this: JsObject, template_map: GcRefCell>, + loaded_modules: GcRefCell>, } impl Realm { @@ -71,7 +73,8 @@ impl Realm { environment, global_object, global_this, - template_map: GcRefCell::new(FxHashMap::default()), + template_map: GcRefCell::default(), + loaded_modules: GcRefCell::default(), }), }; @@ -97,6 +100,11 @@ impl Realm { &self.inner.global_this } + #[allow(unused)] + pub(crate) fn loaded_modules(&self) -> &GcRefCell> { + &self.inner.loaded_modules + } + /// Resizes the number of bindings on the global environment. pub(crate) fn resize_global_env(&self) { let binding_number = self.environment().compile_env().borrow().num_bindings(); @@ -120,4 +128,9 @@ impl Realm { pub(crate) fn lookup_template(&self, site: u64) -> Option { self.inner.template_map.borrow().get(&site).cloned() } + + pub(crate) fn addr(&self) -> *const () { + let ptr: *const _ = &*self.inner; + ptr.cast() + } } diff --git a/boa_examples/scripts/modules/operations.mjs b/boa_examples/scripts/modules/operations.mjs new file mode 100644 index 0000000000..036fb789f7 --- /dev/null +++ b/boa_examples/scripts/modules/operations.mjs @@ -0,0 +1,21 @@ +function sum(a, b) { + return a + b; +} + +function sub(a, b) { + return a - b; +} + +function mult(a, b) { + return a * b; +} + +function div(a, b) { + return a / b; +} + +function sqrt(a) { + return Math.sqrt(a); +} + +export { sum, sub, mult, div, sqrt }; diff --git a/boa_examples/scripts/modules/trig.mjs b/boa_examples/scripts/modules/trig.mjs new file mode 100644 index 0000000000..8407c566f1 --- /dev/null +++ b/boa_examples/scripts/modules/trig.mjs @@ -0,0 +1,11 @@ +import { sum, mult, sqrt } from "./operations.mjs"; + +function pyth(a, b) { + let a2 = mult(a, a); + let b2 = mult(b, b); + let a2b2 = sum(a2, b2); + + return sqrt(a2b2); +} + +export { pyth }; diff --git a/boa_examples/src/bin/modules.rs b/boa_examples/src/bin/modules.rs new file mode 100644 index 0000000000..f1ea129e80 --- /dev/null +++ b/boa_examples/src/bin/modules.rs @@ -0,0 +1,131 @@ +use std::{error::Error, path::Path}; + +use boa_engine::{ + builtins::promise::PromiseState, + module::{ModuleLoader, SimpleModuleLoader}, + object::FunctionObjectBuilder, + Context, JsError, JsNativeError, JsValue, Module, NativeFunction, +}; +use boa_parser::Source; + +// This example demonstrates how to use Boa's module API +fn main() -> Result<(), Box> { + // A simple module that we want to compile from Rust code. + const MODULE_SRC: &str = r#" + import { pyth } from "./trig.mjs"; + import * as ops from "./operations.mjs"; + + export let result = pyth(3, 4); + export function mix(a, b) { + return ops.sum(ops.mult(a, ops.sub(b, a)), 10); + } + "#; + + // This can be overriden with any custom implementation of `ModuleLoader`. + let loader = &SimpleModuleLoader::new("./boa_examples/scripts/modules")?; + let dyn_loader: &dyn ModuleLoader = loader; + + // Just need to cast to a `ModuleLoader` before passing it to the builder. + let context = &mut Context::builder().module_loader(dyn_loader).build()?; + let source = Source::from_reader(MODULE_SRC.as_bytes(), Some(Path::new("./main.mjs"))); + + // Can also pass a `Some(realm)` if you need to execute the module in another realm. + let module = Module::parse(source, None, context)?; + + // Don't forget to insert the parsed module into the loader itself! Since the root module + // is not automatically inserted by the `ModuleLoader::load_imported_module` impl. + // + // Simulate as if the "fake" module is located in the modules root, just to ensure that + // the loader won't double load in case someone tries to import "./main.mjs". + loader.insert( + Path::new("./boa_examples/scripts/modules") + .canonicalize()? + .join("main.mjs"), + module.clone(), + ); + + // The lifecycle of the module is tracked using promises, which can be a bit cumbersome + // for simple uses but that use case is better suited by the `Module::load_link_evaluate` method. + // This does the full version for demonstration purposes. + // + // parse -> load -> link -> evaluate + let promise_result = module + // Initial load that recursively loads the module's dependencies. + // This returns a `JsPromise` that will be resolved when loading finishes, + // which allows async loads and async fetches. + .load(context) + .then( + Some( + FunctionObjectBuilder::new( + context, + NativeFunction::from_copy_closure_with_captures( + |_, _, module, context| { + // After loading, link all modules by resolving the imports + // and exports on the full module graph, initializing module + // environments. This returns a plain `Err` since all modules + // must link at the same time. + module.link(context)?; + Ok(JsValue::undefined()) + }, + module.clone(), + ), + ) + .build(), + ), + None, + context, + )? + .then( + Some( + FunctionObjectBuilder::new( + context, + NativeFunction::from_copy_closure_with_captures( + // Finally, evaluate the root module. + // This returns a `JsPromise` since a module could have + // top-level await statements, which defers module execution to the + // job queue. + |_, _, module, context| Ok(module.evaluate(context).into()), + module.clone(), + ), + ) + .build(), + ), + None, + context, + )?; + + // Very important to push forward the job queue after queueing promises. + context.run_jobs(); + + // Checking if the final promise didn't return an error. + match promise_result.state()? { + PromiseState::Pending => return Err("module didn't execute!".into()), + PromiseState::Fulfilled(v) => { + assert_eq!(v, JsValue::undefined()) + } + PromiseState::Rejected(err) => { + return Err(JsError::from_opaque(err).try_native(context)?.into()) + } + } + + // We can access the full namespace of the module with all its exports. + let namespace = module.namespace(context); + let result = namespace.get("result", context)?; + + println!("result = {}", result.display()); + + assert_eq!(namespace.get("result", context)?, JsValue::from(5)); + + let mix = namespace + .get("mix", context)? + .as_callable() + .cloned() + .ok_or_else(|| JsNativeError::typ().with_message("mix export wasn't a function!"))?; + let result = mix.call(&JsValue::undefined(), &[5.into(), 10.into()], context)?; + + println!("mix(5, 10) = {}", result.display()); + + assert_eq!(result, 35.into()); + + Ok(()) +} diff --git a/boa_gc/src/cell.rs b/boa_gc/src/cell.rs index faf5a6d392..ce7d6d2cae 100644 --- a/boa_gc/src/cell.rs +++ b/boa_gc/src/cell.rs @@ -161,6 +161,7 @@ impl GcRefCell { /// # Panics /// /// Panics if the value is currently borrowed. + #[track_caller] pub fn borrow_mut(&self) -> GcRefMut<'_, T> { match self.try_borrow_mut() { Ok(value) => value, diff --git a/boa_gc/src/pointers/ephemeron.rs b/boa_gc/src/pointers/ephemeron.rs index e780470014..c63da65fd7 100644 --- a/boa_gc/src/pointers/ephemeron.rs +++ b/boa_gc/src/pointers/ephemeron.rs @@ -73,7 +73,7 @@ impl Ephemeron { } pub(crate) fn inner_ptr(&self) -> NonNull> { - assert!(finalizer_safe()); + assert!(finalizer_safe() || self.is_rooted()); self.inner_ptr.get().as_ptr() } diff --git a/boa_gc/src/pointers/gc.rs b/boa_gc/src/pointers/gc.rs index dda97c30cf..4cc308294d 100644 --- a/boa_gc/src/pointers/gc.rs +++ b/boa_gc/src/pointers/gc.rs @@ -30,6 +30,7 @@ impl Gc { // // Note: Allocator can cause Collector to run let inner_ptr = Allocator::alloc_gc(GcBox::new(value)); + // SAFETY: inner_ptr was just allocated, so it must be a valid value that implements [`Trace`] unsafe { (*inner_ptr.as_ptr()).value().unroot() } @@ -94,7 +95,7 @@ impl Gc { } pub(crate) fn inner_ptr(&self) -> NonNull> { - assert!(finalizer_safe()); + assert!(finalizer_safe() || self.is_rooted()); self.inner_ptr.get().as_ptr() } diff --git a/boa_interner/Cargo.toml b/boa_interner/Cargo.toml index 461adf37ab..23f099b536 100644 --- a/boa_interner/Cargo.toml +++ b/boa_interner/Cargo.toml @@ -16,6 +16,7 @@ std = ["once_cell/std"] [dependencies] boa_macros.workspace = true +boa_gc.workspace = true phf = { version = "0.11.1", default-features = false, features = ["macros"] } rustc-hash = { version = "1.1.0", default-features = false } static_assertions = "1.1.0" diff --git a/boa_interner/src/sym.rs b/boa_interner/src/sym.rs index a29db7b3da..3129f60c3a 100644 --- a/boa_interner/src/sym.rs +++ b/boa_interner/src/sym.rs @@ -1,3 +1,4 @@ +use boa_gc::{empty_trace, Finalize, Trace}; use boa_macros::static_syms; use core::num::NonZeroUsize; @@ -12,11 +13,17 @@ use core::num::NonZeroUsize; )] #[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))] #[allow(clippy::unsafe_derive_deserialize)] -#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Finalize)] pub struct Sym { value: NonZeroUsize, } +// SAFETY: `NonZeroUsize` is a constrained `usize`, and all primitive types don't need to be traced +// by the garbage collector. +unsafe impl Trace for Sym { + empty_trace!(); +} + impl Sym { /// Creates a new [`Sym`] from the provided `value`, or returns `None` if `index` is zero. pub(super) fn new(value: usize) -> Option { diff --git a/boa_parser/src/parser/statement/declaration/export.rs b/boa_parser/src/parser/statement/declaration/export.rs index bd1cddfbcc..9d30090697 100644 --- a/boa_parser/src/parser/statement/declaration/export.rs +++ b/boa_parser/src/parser/statement/declaration/export.rs @@ -126,7 +126,7 @@ where TokenKind::Keyword((Keyword::Var, false)) => VariableStatement::new(false, true) .parse(cursor, interner) .map(AstExportDeclaration::VarStatement)?, - TokenKind::Keyword((Keyword::Default, _)) => { + TokenKind::Keyword((Keyword::Default, false)) => { cursor.advance(interner); let tok = cursor.peek(0, interner).or_abrupt()?; @@ -201,9 +201,10 @@ where let mut list = Vec::new(); loop { - let tok = cursor.next(interner).or_abrupt()?; + let tok = cursor.peek(0, interner).or_abrupt()?; match tok.kind() { TokenKind::Punctuator(Punctuator::CloseBlock) => { + cursor.advance(interner); break; } TokenKind::Punctuator(Punctuator::Comma) => { @@ -219,6 +220,7 @@ where "export declaration", )); } + cursor.advance(interner); } TokenKind::StringLiteral(_) | TokenKind::IdentifierName(_) => { list.push(ExportSpecifier.parse(cursor, interner)?); diff --git a/boa_parser/src/parser/statement/declaration/import.rs b/boa_parser/src/parser/statement/declaration/import.rs index 6824188d56..0f4f426c0c 100644 --- a/boa_parser/src/parser/statement/declaration/import.rs +++ b/boa_parser/src/parser/statement/declaration/import.rs @@ -171,9 +171,10 @@ where let mut list = Vec::new(); loop { - let tok = cursor.next(interner).or_abrupt()?; + let tok = cursor.peek(0, interner).or_abrupt()?; match tok.kind() { TokenKind::Punctuator(Punctuator::CloseBlock) => { + cursor.advance(interner); break; } TokenKind::Punctuator(Punctuator::Comma) => { @@ -189,6 +190,7 @@ where "import declaration", )); } + cursor.advance(interner); } TokenKind::StringLiteral(_) | TokenKind::IdentifierName(_) => { list.push(ImportSpecifier.parse(cursor, interner)?); diff --git a/boa_parser/src/parser/statement/declaration/mod.rs b/boa_parser/src/parser/statement/declaration/mod.rs index 33d67c1a7d..302e1a3d0a 100644 --- a/boa_parser/src/parser/statement/declaration/mod.rs +++ b/boa_parser/src/parser/statement/declaration/mod.rs @@ -79,7 +79,18 @@ where .parse(cursor, interner) .map(Into::into) } - _ => unreachable!("unknown token found: {:?}", tok), + _ => Err(Error::expected( + [ + Keyword::Function.to_string(), + Keyword::Async.to_string(), + Keyword::Class.to_string(), + Keyword::Const.to_string(), + Keyword::Let.to_string(), + ], + tok.to_string(interner), + tok.span(), + "export declaration", + )), } } } diff --git a/boa_parser/src/parser/statement/mod.rs b/boa_parser/src/parser/statement/mod.rs index 70f846dbc7..33809428fa 100644 --- a/boa_parser/src/parser/statement/mod.rs +++ b/boa_parser/src/parser/statement/mod.rs @@ -39,10 +39,7 @@ use self::{ with::WithStatement, }; use crate::{ - lexer::{ - token::{ContainsEscapeSequence, EscapeSequence}, - Error as LexError, InputElement, Token, TokenKind, - }, + lexer::{token::EscapeSequence, Error as LexError, InputElement, Token, TokenKind}, parser::{ expression::{BindingIdentifier, Initializer, PropertyName}, AllowAwait, AllowReturn, AllowYield, Cursor, OrAbrupt, ParseResult, TokenParser, @@ -58,7 +55,7 @@ use boa_ast::{ pattern::{ArrayPattern, ArrayPatternElement, ObjectPatternElement}, Keyword, Punctuator, }; -use boa_interner::{Interner, Sym}; +use boa_interner::Interner; use boa_macros::utf16; use boa_profiler::Profiler; use std::io::Read; @@ -954,20 +951,12 @@ where let tok = cursor.peek(0, interner).or_abrupt()?; match tok.kind() { - TokenKind::IdentifierName((ident, ContainsEscapeSequence(false))) - if *ident == Sym::IMPORT => - { - ImportDeclaration - .parse(cursor, interner) - .map(Self::Output::ImportDeclaration) - } - TokenKind::IdentifierName((ident, ContainsEscapeSequence(false))) - if *ident == Sym::EXPORT => - { - ExportDeclaration - .parse(cursor, interner) - .map(Self::Output::ExportDeclaration) - } + TokenKind::Keyword((Keyword::Import, false)) => ImportDeclaration + .parse(cursor, interner) + .map(Self::Output::ImportDeclaration), + TokenKind::Keyword((Keyword::Export, false)) => ExportDeclaration + .parse(cursor, interner) + .map(Self::Output::ExportDeclaration), _ => StatementListItem::new(false, true, false) .parse(cursor, interner) .map(Self::Output::StatementListItem), diff --git a/boa_tester/src/exec/mod.rs b/boa_tester/src/exec/mod.rs index c226dfcd23..1b06526716 100644 --- a/boa_tester/src/exec/mod.rs +++ b/boa_tester/src/exec/mod.rs @@ -7,8 +7,13 @@ use crate::{ TestFlags, TestOutcomeResult, TestResult, TestSuite, VersionedStats, }; use boa_engine::{ - native_function::NativeFunction, object::FunctionObjectBuilder, optimizer::OptimizerOptions, - property::Attribute, Context, JsArgs, JsNativeErrorKind, JsValue, Source, + builtins::promise::PromiseState, + module::{Module, ModuleLoader, SimpleModuleLoader}, + native_function::NativeFunction, + object::FunctionObjectBuilder, + optimizer::OptimizerOptions, + property::Attribute, + Context, JsArgs, JsError, JsNativeErrorKind, JsValue, Source, }; use colored::Colorize; use fxhash::FxHashSet; @@ -141,12 +146,16 @@ impl Test { optimizer_options: OptimizerOptions, ) -> Vec { let mut results = Vec::new(); - if self.flags.contains(TestFlags::STRICT) && !self.flags.contains(TestFlags::RAW) { - results.push(self.run_once(harness, true, verbose, optimizer_options)); - } - - if self.flags.contains(TestFlags::NO_STRICT) || self.flags.contains(TestFlags::RAW) { + if self.flags.contains(TestFlags::MODULE) { results.push(self.run_once(harness, false, verbose, optimizer_options)); + } else { + if self.flags.contains(TestFlags::STRICT) && !self.flags.contains(TestFlags::RAW) { + results.push(self.run_once(harness, true, verbose, optimizer_options)); + } + + if self.flags.contains(TestFlags::NO_STRICT) || self.flags.contains(TestFlags::RAW) { + results.push(self.run_once(harness, false, verbose, optimizer_options)); + } } results @@ -209,22 +218,61 @@ impl Test { let result = std::panic::catch_unwind(|| match self.expected_outcome { Outcome::Positive => { let async_result = AsyncResult::default(); - let context = &mut Context::default(); + let loader = &SimpleModuleLoader::new( + self.path.parent().expect("test should have a parent dir"), + ) + .expect("test path should be canonicalizable"); + let dyn_loader: &dyn ModuleLoader = loader; + let context = &mut Context::builder() + .module_loader(dyn_loader) + .build() + .expect("cannot fail with default global object"); if let Err(e) = self.set_up_env(harness, context, async_result.clone()) { return (false, e); } - context.strict(strict); + context.set_optimizer_options(optimizer_options); // TODO: timeout - let value = match if self.is_module() { - context.eval_module(source) + let value = if self.is_module() { + let module = match Module::parse(source, None, context) { + Ok(module) => module, + Err(err) => return (false, format!("Uncaught {err}")), + }; + + loader.insert( + self.path + .canonicalize() + .expect("test path should be canonicalizable"), + module.clone(), + ); + + let promise = match module.load_link_evaluate(context) { + Ok(promise) => promise, + Err(err) => return (false, format!("Uncaught {err}")), + }; + + context.run_jobs(); + + match promise + .state() + .expect("tester can only use builtin promises") + { + PromiseState::Pending => { + return (false, "module should have been executed".to_string()) + } + PromiseState::Fulfilled(v) => v, + PromiseState::Rejected(err) => { + return (false, format!("Uncaught {}", err.display())) + } + } } else { - context.eval_script(source) - } { - Ok(v) => v, - Err(e) => return (false, format!("Uncaught {e}")), + context.strict(strict); + match context.eval_script(source) { + Ok(v) => v, + Err(err) => return (false, format!("Uncaught {err}")), + } }; context.run_jobs(); @@ -254,15 +302,16 @@ impl Test { ); let context = &mut Context::default(); - context.strict(strict); + context.set_optimizer_options(OptimizerOptions::OPTIMIZE_ALL); if self.is_module() { - match context.parse_module(source) { + match Module::parse(source, None, context) { Ok(_) => (false, "ModuleItemList parsing should fail".to_owned()), Err(e) => (true, format!("Uncaught {e}")), } } else { + context.strict(strict); match context.parse_script(source) { Ok(_) => (false, "StatementList parsing should fail".to_owned()), Err(e) => (true, format!("Uncaught {e}")), @@ -271,37 +320,134 @@ impl Test { } Outcome::Negative { phase: Phase::Resolution, - error_type: _, - } => (false, "Modules are not implemented yet".to_string()), + error_type, + } => { + let loader = &SimpleModuleLoader::new( + self.path.parent().expect("test should have a parent dir"), + ) + .expect("test path should be canonicalizable"); + let dyn_loader: &dyn ModuleLoader = loader; + let context = &mut Context::builder() + .module_loader(dyn_loader) + .build() + .expect("cannot fail with default global object"); + + let module = match Module::parse(source, None, context) { + Ok(module) => module, + Err(err) => return (false, format!("Uncaught {err}")), + }; + + loader.insert( + self.path + .canonicalize() + .expect("test path should be canonicalizable"), + module.clone(), + ); + + let promise = module.load(context); + + context.run_jobs(); + + match promise + .state() + .expect("tester can only use builtin promises") + { + PromiseState::Pending => { + return (false, "module didn't try to load".to_string()) + } + PromiseState::Fulfilled(_) => { + // Try to link to see if the resolution error shows there. + } + PromiseState::Rejected(err) => { + let err = JsError::from_opaque(err); + return ( + is_error_type(&err, error_type, context), + format!("Uncaught {err}"), + ); + } + } + + if let Err(err) = module.link(context) { + ( + is_error_type(&err, error_type, context), + format!("Uncaught {err}"), + ) + } else { + (false, "module resolution didn't fail".to_string()) + } + } Outcome::Negative { phase: Phase::Runtime, error_type, } => { - let context = &mut Context::default(); + let loader = &SimpleModuleLoader::new( + self.path.parent().expect("test should have a parent dir"), + ) + .expect("test path should be canonicalizable"); + let dyn_loader: &dyn ModuleLoader = loader; + let context = &mut Context::builder() + .module_loader(dyn_loader) + .build() + .expect("cannot fail with default global object"); context.strict(strict); context.set_optimizer_options(optimizer_options); if let Err(e) = self.set_up_env(harness, context, AsyncResult::default()) { return (false, e); } - - let e = if self.is_module() { - let module = match context.parse_module(source) { - Ok(code) => code, + let error = if self.is_module() { + let module = match Module::parse(source, None, context) { + Ok(module) => module, Err(e) => return (false, format!("Uncaught {e}")), }; - match context - .compile_module(&module) - .and_then(|code| context.execute(code)) + + loader.insert( + self.path + .canonicalize() + .expect("test path should be canonicalizable"), + module.clone(), + ); + + let promise = module.load(context); + + context.run_jobs(); + + match promise + .state() + .expect("tester can only use builtin promises") { - Ok(_) => return (false, "Module execution should fail".to_owned()), - Err(e) => e, + PromiseState::Pending => { + return (false, "module didn't try to load".to_string()) + } + PromiseState::Fulfilled(_) => {} + PromiseState::Rejected(err) => { + return (false, format!("Uncaught {}", err.display())) + } + } + + if let Err(err) = module.link(context) { + return (false, format!("Uncaught {err}")); + } + + let promise = module.evaluate(context); + + match promise + .state() + .expect("tester can only use builtin promises") + { + PromiseState::Pending => { + return (false, "module didn't try to evaluate".to_string()) + } + PromiseState::Fulfilled(val) => return (false, val.display().to_string()), + PromiseState::Rejected(err) => JsError::from_opaque(err), } } else { + context.strict(strict); let script = match context.parse_script(source) { Ok(code) => code, Err(e) => return (false, format!("Uncaught {e}")), }; + match context .compile_script(&script) .and_then(|code| context.execute(code)) @@ -311,31 +457,10 @@ impl Test { } }; - if let Ok(e) = e.try_native(context) { - match &e.kind { - JsNativeErrorKind::Syntax if error_type == ErrorType::SyntaxError => {} - JsNativeErrorKind::Reference if error_type == ErrorType::ReferenceError => { - } - JsNativeErrorKind::Range if error_type == ErrorType::RangeError => {} - JsNativeErrorKind::Type if error_type == ErrorType::TypeError => {} - _ => return (false, format!("Uncaught {e}")), - } - (true, format!("Uncaught {e}")) - } else { - let passed = e - .as_opaque() - .expect("try_native cannot fail if e is not opaque") - .as_object() - .and_then(|o| o.get("constructor", context).ok()) - .as_ref() - .and_then(JsValue::as_object) - .and_then(|o| o.get("name", context).ok()) - .as_ref() - .and_then(JsValue::as_string) - .map(|s| s == error_type.as_str()) - .unwrap_or_default(); - (passed, format!("Uncaught {e}")) - } + ( + is_error_type(&error, error_type, context), + format!("Uncaught {error}"), + ) } }); @@ -451,6 +576,34 @@ impl Test { } } +/// Returns `true` if `error` is a `target_type` error. +fn is_error_type(error: &JsError, target_type: ErrorType, context: &mut Context<'_>) -> bool { + if let Ok(error) = error.try_native(context) { + match &error.kind { + JsNativeErrorKind::Syntax if target_type == ErrorType::SyntaxError => {} + JsNativeErrorKind::Reference if target_type == ErrorType::ReferenceError => {} + JsNativeErrorKind::Range if target_type == ErrorType::RangeError => {} + JsNativeErrorKind::Type if target_type == ErrorType::TypeError => {} + _ => return false, + } + true + } else { + let passed = error + .as_opaque() + .expect("try_native cannot fail if e is not opaque") + .as_object() + .and_then(|o| o.get("constructor", context).ok()) + .as_ref() + .and_then(JsValue::as_object) + .and_then(|o| o.get("name", context).ok()) + .as_ref() + .and_then(JsValue::as_string) + .map(|s| s == target_type.as_str()) + .unwrap_or_default(); + passed + } +} + /// Registers the print function in the context. fn register_print_fn(context: &mut Context<'_>, async_result: AsyncResult) { // We use `FunctionBuilder` to define a closure with additional captures.