From a888b7753e9b9e766df146e0a21c8b79de69ffcc Mon Sep 17 00:00:00 2001 From: jedel1043 Date: Tue, 14 Feb 2023 15:51:33 -0600 Subject: [PATCH 01/18] Implement draft of module execution --- .gitignore | 4 +- 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 | 5 +- boa_engine/src/context/mod.rs | 200 +-- .../environments/runtime/declarative/mod.rs | 23 +- .../runtime/declarative/module.rs | 110 ++ boa_engine/src/environments/runtime/mod.rs | 22 +- boa_engine/src/lib.rs | 1 + boa_engine/src/module/mod.rs | 527 ++++++ boa_engine/src/module/source.rs | 1445 +++++++++++++++++ .../src/object/internal_methods/immutable.rs | 39 + boa_engine/src/object/internal_methods/mod.rs | 2 + .../src/object/internal_methods/namespace.rs | 308 ++++ boa_engine/src/object/jsobject.rs | 15 +- boa_engine/src/object/mod.rs | 44 + boa_engine/src/realm.rs | 21 +- boa_gc/src/cell.rs | 1 + boa_gc/src/pointers/ephemeron.rs | 2 +- boa_gc/src/pointers/gc.rs | 2 +- .../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 ++- 31 files changed, 3351 insertions(+), 253 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.rs create mode 100644 boa_engine/src/object/internal_methods/namespace.rs diff --git a/.gitignore b/.gitignore index 56396d40502..0de391828ae 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/boa_ast/src/declaration/export.rs b/boa_ast/src/declaration/export.rs index b174a82b4d3..94b0dad0916 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 `OrdinaryExportEntry`. + #[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 `ReExportEntry`. + #[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 0420c738395..cdb4b215f61 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 25542d61927..1d427359f4f 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-modulerequests + #[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 [`ImportEntries`][spec]. + /// + /// Gets the list of import entries of this module. + /// + /// [spec]: https://tc39.es/ecma262/#sec-static-semantics-modulerequests + #[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 b5be81642a5..8ac824dd088 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::{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, default_value_os_t = PathBuf::from("."), requires = "mod")] + modpath: 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 = context.parse_module(Source::from_bytes(&buffer), None)?; + + 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.modpath) + .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 d079d5e7eda..26fedf0cf31 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 8e14f56b155..252f238332e 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 1d451fd8a55..86d7d667b63 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 6e2de81a97b..6326a3c799e 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 879ce8dd015..24cdf849007 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 { diff --git a/boa_engine/src/context/mod.rs b/boa_engine/src/context/mod.rs index 9280c9eee0f..4db9351ecf0 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::{Module, ModuleLoader, SimpleModuleLoader, SourceTextModule}, 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,15 +209,24 @@ impl<'host> Context<'host> { Ok(result) } - /// Parse the given source script. + /// Abstract operation [`ParseModule ( sourceText, realm, hostDefined )`][spec] + /// + /// Parses the given source as a module. + /// + /// [spec]: https://tc39.es/ecma262/#sec-parsemodule pub fn parse_module( &mut self, src: Source<'_, R>, - ) -> Result { + realm: Option, + ) -> JsResult { 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) + let module = parser.parse_module(&mut self.interner)?; + Ok(SourceTextModule::new( + module, + realm.unwrap_or_else(|| self.realm.clone()), + )) } /// Compile the script AST into a `CodeBlock` ready to be executed by the VM. @@ -271,81 +245,6 @@ 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 @@ -634,6 +533,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 +786,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 +799,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 +828,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 +864,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 +878,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 +890,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 +903,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 +935,7 @@ impl<'icu, 'hooks, 'queue> ContextBuilder<'icu, 'hooks, 'queue> { 'icu: 'host, 'hooks: 'host, 'queue: 'host, + 'module: 'host, { let root_shape = SharedShape::root(); @@ -1034,6 +965,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 6d38765ae51..37ede1c45cb 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 00000000000..18d7d46550b --- /dev/null +++ b/boa_engine/src/environments/runtime/declarative/module.rs @@ -0,0 +1,110 @@ +use std::cell::Cell; + +use boa_ast::expression::Identifier; +use boa_gc::{Finalize, GcRefCell, Trace}; + +use crate::{module::Module, JsValue}; + +#[derive(Debug, Clone, Copy)] +enum BindingAccessor { + Identifier(Identifier), + Index(usize), +} + +#[derive(Clone, Debug, Trace, Finalize)] +struct IndirectBinding { + module: Module, + #[unsafe_ignore_trace] + accessor: Cell, +} + +#[derive(Clone, Debug, Trace, Finalize)] +enum BindingType { + Direct(Option), + Indirect(IndirectBinding), +} + +#[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") + } + } + } + + /// Sets a reference from this environment to an external 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 636fec61ce4..a537779bb2b 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 9a5c8d36990..bd33d889abf 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; diff --git a/boa_engine/src/module/mod.rs b/boa_engine/src/module/mod.rs new file mode 100644 index 00000000000..819a2cf57d0 --- /dev/null +++ b/boa_engine/src/module/mod.rs @@ -0,0 +1,527 @@ +//! Module related types. + +mod source; +use boa_parser::Source; +use boa_profiler::Profiler; +pub use source::SourceTextModule; + +use boa_ast::expression::Identifier; +use boa_interner::Sym; +use indexmap::IndexMap; +use rustc_hash::{FxHashMap, FxHashSet, FxHasher}; + +use std::cell::{Cell, RefCell}; +use std::hash::Hash; +use std::path::{Path, PathBuf}; +use std::rc::Rc; +use std::{collections::HashSet, hash::BuildHasherDefault}; + +use boa_gc::{Finalize, Gc, GcRefCell, Trace}; + +use crate::object::FunctionObjectBuilder; +use crate::property::{PropertyDescriptor, PropertyKey}; +use crate::{ + builtins::promise::{PromiseCapability, PromiseState}, + environments::DeclarativeEnvironment, + object::{JsObject, JsPromise, ObjectData}, + realm::Realm, + Context, JsError, JsResult, JsString, JsValue, +}; +use crate::{js_string, JsNativeError, JsSymbol, NativeFunction}; + +/// +#[derive(Debug)] +pub enum Referrer { + /// + Module(SourceTextModule), + /// + Realm(Realm), // TODO: script +} + +/// +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 return `()` instead of `JsResult`, leaving the responsibility of calling + /// [`FinishLoadingImportedModule`][finish] to the host, but this simpler API just provides + /// a closures that replaces [`FinishLoadingImportedModule`]. Specifically, the requirements of + /// this hook per the spec are as follows: + /// + /// - 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`. + pub fn new(root: &Path) -> JsResult { + let absolute = root + .canonicalize() + .map_err(|e| JsNativeError::typ().with_message(e.to_string()))?; + Ok(Self { + root: absolute, + module_map: GcRefCell::default(), + }) + } + + /// Inserts a new module onto the module map. + pub fn insert(&self, path: PathBuf, module: Module) { + self.module_map.borrow_mut().insert(path, module); + } + + /// Gets a module from its original path. + 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 path = Path::new(&path); + let path = self.root.join(path); + let path = path + .canonicalize() + .map_err(|e| JsNativeError::typ().with_message(e.to_string()))?; + if let Some(module) = self.get(&path) { + return Ok(module); + } + let source = Source::from_filepath(&path) + .map_err(|err| JsNativeError::typ().with_message(err.to_string()))?; + let module = context.parse_module(source, None)?; + 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(Debug, Clone, Trace, Finalize)] +pub struct Module { + inner: Gc, +} + +#[derive(Trace, Finalize)] +struct Inner { + realm: Realm, + environment: GcRefCell>>, + namespace: GcRefCell>, + kind: ModuleKind, + host_defined: (), +} + +impl std::fmt::Debug for Inner { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("Inner") + .field("realm", &self.realm.addr()) + .field("environment", &self.environment) + .field("namespace", &self.namespace) + .field("kind", &self.kind) + .field("host_defined", &self.host_defined) + .finish() + } +} + +#[derive(Debug, Trace, Finalize)] +enum ModuleKind { + Source(SourceTextModule), + #[allow(unused)] + Synthetic, +} + +#[derive(Debug, Clone)] +struct GraphLoadingState { + capability: PromiseCapability, + loading: Cell, + pending_modules: Cell, + visited: RefCell>, +} + +#[derive(Debug, Clone)] +pub(crate) struct ExportLocator { + module: Module, + binding_name: BindingName, +} + +impl ExportLocator { + pub(crate) const fn module(&self) -> &Module { + &self.module + } + + pub(crate) const fn binding_name(&self) -> BindingName { + self.binding_name + } +} + +#[derive(Debug, Clone, Copy)] +pub(crate) enum BindingName { + Name(Identifier), + Namespace, +} + +#[derive(Debug, Clone, Copy)] +pub(crate) enum ResolveExportError { + NotFound(Sym), + Ambiguous(Sym), +} + +impl Module { + pub(crate) fn realm(&self) -> &Realm { + &self.inner.realm + } + + pub(crate) fn environment(&self) -> Option> { + self.inner.environment.borrow().clone() + } + + fn kind(&self) -> &ModuleKind { + &self.inner.kind + } + + /// + #[allow(clippy::missing_panics_doc)] + pub fn load(&self, context: &mut Context<'_>) -> JsPromise { + match self.kind() { + ModuleKind::Source(_) => SourceTextModule::load(self, context), + ModuleKind::Synthetic => todo!("synthetic.load()"), + } + } + + fn inner_load(&self, state: &Rc, context: &mut Context<'_>) { + assert!(state.loading.get()); + + if let ModuleKind::Source(_) = self.kind() { + SourceTextModule::inner_load(self, state, context); + if !state.loading.get() { + return; + } + } + + assert!(state.pending_modules.get() >= 1); + + state.pending_modules.set(state.pending_modules.get() - 1); + if state.pending_modules.get() == 0 { + state.loading.set(false); + state + .capability + .resolve() + .call(&JsValue::undefined(), &[], context) + .expect("marking a module as loaded should not fail"); + } + } + + fn resume_load( + state: &Rc, + completion: JsResult, + context: &mut Context<'_>, + ) { + if !state.loading.get() { + return; + } + + match completion { + Ok(m) => { + m.inner_load(state, context); + } + Err(err) => { + state.loading.set(false); + state + .capability + .reject() + .call(&JsValue::undefined(), &[err.to_opaque(context)], context) + .expect("cannot fail for the default reject function"); + } + } + } + + fn get_exported_names(&self, export_star_set: &mut Vec) -> FxHashSet { + match self.kind() { + ModuleKind::Source(src) => src.get_exported_names(export_star_set), + ModuleKind::Synthetic => todo!("synthetic.get_exported_names()"), + } + } + + #[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::Source(_) => { + SourceTextModule::resolve_export(self, export_name, resolve_set) + } + ModuleKind::Synthetic => todo!("synthetic.resolve_export()"), + } + } + + /// + #[allow(clippy::missing_panics_doc)] + pub fn link(&self, context: &mut Context<'_>) -> JsResult<()> { + match self.kind() { + ModuleKind::Source(_) => SourceTextModule::link(self, context), + ModuleKind::Synthetic => todo!("synthetic.link()"), + } + } + + fn inner_link( + &self, + stack: &mut Vec, + index: usize, + context: &mut Context<'_>, + ) -> JsResult { + match self.kind() { + ModuleKind::Source(_) => SourceTextModule::inner_link(self, stack, index, context), + #[allow(unreachable_code)] + ModuleKind::Synthetic => { + todo!("synthetic.load()"); + Ok(index) + } + } + } + + pub(crate) fn get_namespace(&self, context: &mut Context<'_>) -> JsObject { + if let Some(obj) = self.inner.namespace.borrow().clone() { + return obj; + } + + let exported_names = self.get_exported_names(&mut Vec::default()); + + let unambiguous_names = exported_names + .into_iter() + .filter_map(|name| { + self.resolve_export(name, &mut HashSet::default()) + .ok() + .map(|_| name) + }) + .collect(); + + let namespace = ModuleNamespace::create(self.clone(), unambiguous_names, context); + + *self.inner.namespace.borrow_mut() = Some(namespace.clone()); + + namespace + } + + /// + #[allow(clippy::missing_panics_doc)] + pub fn evaluate(&self, context: &mut Context<'_>) -> JsPromise { + match self.kind() { + ModuleKind::Source(src) => src.evaluate(context), + ModuleKind::Synthetic => todo!("synthetic.evaluate()"), + } + } + + fn inner_evaluate( + &self, + stack: &mut Vec, + index: usize, + context: &mut Context<'_>, + ) -> JsResult { + match self.kind() { + ModuleKind::Source(src) => src.inner_evaluate(stack, index, None, context), + #[allow(unused, clippy::diverging_sub_expression)] + ModuleKind::Synthetic => { + let promise: JsPromise = todo!("module.Evaluate()"); + let state = promise.state()?; + match state { + PromiseState::Pending => { + unreachable!("b. Assert: promise.[[PromiseState]] is not pending.") + } + PromiseState::Fulfilled(_) => Ok(index), + PromiseState::Rejected(err) => Err(JsError::from_opaque(err)), + } + } + } + } + + /// Loads, links and evaluates the given module `src` by compiling down to bytecode, then + /// returning a promise that will resolve when the module executes. + /// + /// # Examples + /// ```ignore + /// # use boa_engine::{Context, Source}; + /// let loader: &ModuleLoader = &ModuleLoader::new(Path::new(".")); + /// let mut context = Context::builder().module_loader(loader).build().unwrap(); + /// + /// let source = Source::from_bytes("1 + 3"); + /// + /// let module = context.parse_module(source, None).unwrap(); + /// + /// loader.insert(Path::new("./main.mjs").canonicalize().unwrap(), module.clone()); + /// + /// let promise = module.load_link_evaluate(context).unwrap(); + /// + /// context.run_jobs(); + /// + /// assert!(matches!(promise.state(), PromiseState::Fulfilled(JsValue::undefined()))); + /// ``` + 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) + } +} + +impl PartialEq for Module { + 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 { + fn hash(&self, state: &mut H) { + std::ptr::hash(self.inner.as_ref(), state); + } +} + +/// An object containing the exports of a module. +#[derive(Debug, Trace, Finalize)] +pub struct ModuleNamespace { + module: Module, + #[unsafe_ignore_trace] + exports: IndexMap>, +} + +impl ModuleNamespace { + pub(crate) fn create(module: Module, names: Vec, context: &mut Context<'_>) -> JsObject { + let mut exports = names + .into_iter() + .map(|sym| { + ( + context + .interner() + .resolve_expect(sym) + .into_common::(false), + sym, + ) + }) + .collect::>(); + exports.sort_keys(); + + let namespace = JsObject::from_proto_and_data_with_shared_shape( + context.root_shape(), + None, + ObjectData::module_namespace(ModuleNamespace { module, exports }), + ); + + namespace.borrow_mut().properties_mut().insert( + &PropertyKey::Symbol(JsSymbol::to_string_tag()), + PropertyDescriptor::builder() + .value(js_string!("Module")) + .writable(false) + .enumerable(false) + .configurable(false) + .build(), + ); + namespace + } + + pub(crate) const fn exports(&self) -> &IndexMap> { + &self.exports + } + + 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 00000000000..b250d0ad70b --- /dev/null +++ b/boa_engine/src/module/source.rs @@ -0,0 +1,1445 @@ +use std::{ + cell::{Cell, RefCell}, + collections::{HashMap, 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, GcRef, GcRefCell, GcRefMut, 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, ExportLocator, GraphLoadingState, Module, Referrer, ResolveExportError}; + +#[derive(Clone, Copy, Debug, Finalize)] +pub(super) struct DfsInfo { + dfs_index: usize, + dfs_ancestor_index: usize, +} + +unsafe impl Trace for DfsInfo { + empty_trace!(); +} + +#[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, + }, +} + +unsafe impl Trace for Status { + custom_trace!(this, { + match this { + Status::Unlinked | Status::Linking { .. } | Status::Linked { .. } => {} + Status::PreLinked { context, .. } => mark(context), + Status::Evaluating { + top_level_capability, + cycle_root, + context, + .. + } + | Status::EvaluatingAsync { + top_level_capability, + cycle_root, + context, + .. + } => { + 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 { + 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), + } + } + + 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), + } + } + + 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(), + } + } + + const fn evaluation_error(&self) -> Option<&JsError> { + match &self { + Status::Evaluated { error, .. } => error.as_ref(), + _ => None, + } + } + + 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, + } + } + + fn transition(&mut self, f: F) + where + F: FnOnce(Status) -> Status, + { + *self = f(std::mem::take(self)); + } +} + +#[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); + }); +} + +#[derive(Finalize)] +struct SourceTextModuleData { + code: ModuleItemList, + status: Status, + requested_modules: FxHashSet, + loaded_modules: FxHashMap, + has_tla: bool, + async_parent_modules: Vec, + import_meta: Option, + import_entries: Vec, + local_export_entries: Vec, + indirect_export_entries: Vec, + star_export_entries: Vec, +} + +impl std::fmt::Debug for SourceTextModuleData { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("SourceTextModuleData") + .field("code", &"ModuleItemList") + .field("status", &self.status) + .field("requested_modules", &self.requested_modules) + .field("loaded_modules", &self.loaded_modules) + .field("has_tla", &self.has_tla) + .field("async_parent_modules", &self.async_parent_modules) + .field("import_meta", &self.import_meta) + .field("import_entries", &self.import_entries) + .field("local_export_entries", &self.local_export_entries) + .field("indirect_export_entries", &self.indirect_export_entries) + .field("star_export_entries", &self.star_export_entries) + .finish() + } +} + +unsafe impl Trace for SourceTextModuleData { + custom_trace!(this, { + mark(&this.status); + for module in this.loaded_modules.values() { + mark(module); + } + mark(&this.async_parent_modules); + mark(&this.import_meta); + }); +} + +/// +#[derive(Clone, Trace, Finalize)] +pub struct SourceTextModule(Gc>); + +impl std::fmt::Debug for SourceTextModule { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let limiter = RecursionLimiter::new(&*self.0); + + if !limiter.visited && !limiter.live { + f.debug_tuple("SourceTextModule").field(&self.0).finish() + } else { + f.debug_tuple("SourceTextModule").field(&"").finish() + } + } +} + +impl SourceTextModule { + #[allow(clippy::new_ret_no_self)] + pub(crate) fn new(code: ModuleItemList, realm: Realm) -> Module { + let requested_modules = code.requests(); + let import_entries = code.import_entries(); + + let mut indirect_export_entries = Vec::new(); + let mut local_export_entries = Vec::new(); + let mut star_export_entries = Vec::new(); + for ee in code.export_entries() { + match ee { + ExportEntry::Ordinary(entry) => { + 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, + }) + { + indirect_export_entries.push(IndirectExportEntry::new( + module, + ReExportImportName::Name(import), + entry.export_name(), + )); + } else { + local_export_entries.push(entry); + } + } + ExportEntry::StarReExport { module_request } => { + star_export_entries.push(module_request); + } + ExportEntry::ReExport(entry) => indirect_export_entries.push(entry), + } + } + + let has_tla = contains(&code, ContainsSymbol::AwaitExpression); + + let src = SourceTextModule(Gc::new(GcRefCell::new(SourceTextModuleData { + code, + requested_modules, + has_tla, + import_entries, + local_export_entries, + indirect_export_entries, + star_export_entries, + status: Status::Unlinked, + loaded_modules: HashMap::default(), + async_parent_modules: Vec::default(), + import_meta: None, + }))); + + Module { + inner: Gc::new(super::Inner { + realm, + environment: GcRefCell::new(None), + namespace: GcRefCell::new(None), + kind: ModuleKind::Source(src), + host_defined: (), + }), + } + } + + pub(super) fn load(module: &Module, context: &mut Context<'_>) -> JsPromise { + let pc = PromiseCapability::new( + &context.intrinsics().constructors().promise().constructor(), + context, + ) + .expect("capability creation must always succeed when using the `%Promise%` intrinsic"); + + module.inner_load( + &Rc::new(GraphLoadingState { + capability: pc.clone(), + loading: Cell::new(true), + pending_modules: Cell::new(1), + visited: RefCell::default(), + }), + context, + ); + + JsPromise::from_object(pc.promise().clone()) + .expect("promise created from the %Promise% intrinsic is always native") + } + + pub(super) fn inner_load( + module: &Module, + state: &Rc, + context: &mut Context<'_>, + ) { + let ModuleKind::Source(src) = module.kind() else { + unreachable!("must only be called for `SourceTextModule`s"); + }; + + if matches!(src.borrow().status, Status::Unlinked) + && state.visited.borrow_mut().insert(src.clone()) + { + let requested = src.borrow().requested_modules.clone(); + state + .pending_modules + .set(state.pending_modules.get() + requested.len()); + for required in requested { + let loaded = src.borrow().loaded_modules.get(&required).cloned(); + if let Some(loaded) = loaded { + loaded.inner_load(state, context); + } else { + let name_specifier: JsString = context + .interner() + .resolve_expect(required) + .into_common(false); + let src = src.clone(); + let state = state.clone(); + context.module_loader().load_imported_module( + Referrer::Module(src.clone()), + name_specifier, + Box::new(move |completion, ctx| { + if let Ok(loaded) = &completion { + let mut src = src.borrow_mut(); + let entry = src + .loaded_modules + .entry(required) + .or_insert_with(|| loaded.clone()); + debug_assert_eq!(entry, loaded); + } + + Module::resume_load(&state, completion, ctx); + }), + context, + ); + } + if !state.loading.get() { + return; + } + } + } + } + + pub(super) fn get_exported_names( + &self, + export_star_set: &mut Vec, + ) -> FxHashSet { + if export_star_set.contains(self) { + return FxHashSet::default(); + } + + export_star_set.push(self.clone()); + + let module = self.borrow(); + let mut exported_names = FxHashSet::default(); + + for e in &module.local_export_entries { + exported_names.insert(e.export_name()); + } + + for e in &module.indirect_export_entries { + exported_names.insert(e.export_name()); + } + + for e in &module.star_export_entries { + let requested_module = module.loaded_modules[e].clone(); + + for n in requested_module.get_exported_names(export_star_set) { + if n != Sym::DEFAULT { + exported_names.insert(n); + } + } + } + + exported_names + } + + #[allow(clippy::mutable_key_type)] + pub(super) fn resolve_export( + module: &Module, + export_name: Sym, + resolve_set: &mut FxHashSet<(Module, Sym)>, + ) -> Result { + let ModuleKind::Source(src) = module.kind() else { + unreachable!("must only be called for `SourceTextModule`s"); + }; + + if resolve_set.contains(&(module.clone(), export_name)) { + return Err(ResolveExportError::NotFound(export_name)); + } + + resolve_set.insert((module.clone(), export_name)); + let src = src.borrow(); + + for e in &src.local_export_entries { + if export_name == e.export_name() { + return Ok(ExportLocator { + module: module.clone(), + binding_name: BindingName::Name(e.local_name()), + }); + } + } + + for e in &src.indirect_export_entries { + if export_name == e.export_name() { + let imported_module = &src.loaded_modules[&e.module_request()]; + match e.import_name() { + ReExportImportName::Star => { + return Ok(ExportLocator { + module: imported_module.clone(), + binding_name: BindingName::Namespace, + }) + } + ReExportImportName::Name(_) => { + return imported_module.resolve_export(export_name, resolve_set); + } + } + } + } + + if export_name == Sym::DEFAULT { + return Err(ResolveExportError::NotFound(export_name)); + } + + let mut star_resolution: Option = None; + + for e in &src.star_export_entries { + let imported_module = &src.loaded_modules[e]; + let resolution = match imported_module.resolve_export(export_name, resolve_set) { + Ok(resolution) => resolution, + Err(e @ ResolveExportError::Ambiguous(_)) => return Err(e), + Err(ResolveExportError::NotFound(_)) => continue, + }; + + if let Some(star_resolution) = &star_resolution { + // 1. Assert: There is more than one * import that includes the requested name. + if resolution.module != star_resolution.module { + return Err(ResolveExportError::Ambiguous(export_name)); + } + match (resolution.binding_name, star_resolution.binding_name) { + (BindingName::Namespace, BindingName::Name(_)) + | (BindingName::Name(_), BindingName::Namespace) => { + return Err(ResolveExportError::Ambiguous(export_name)); + } + (BindingName::Name(res), BindingName::Name(star)) if res != star => { + return Err(ResolveExportError::Ambiguous(export_name)); + } + _ => {} + } + } else { + star_resolution = Some(resolution); + } + } + + star_resolution.ok_or(ResolveExportError::NotFound(export_name)) + } + + pub(super) fn link(module: &Module, context: &mut Context<'_>) -> JsResult<()> { + let ModuleKind::Source(src) = module.kind() else { + unreachable!("must only be called for `SourceTextModule`s"); + }; + assert!(matches!( + src.borrow().status, + Status::Unlinked + | Status::Linked { .. } + | Status::EvaluatingAsync { .. } + | Status::Evaluated { .. } + )); + + let mut stack = Vec::new(); + + if let Err(err) = Self::inner_link(module, &mut stack, 0, context) { + for source in stack { + assert!(matches!(source.borrow().status, Status::Linking { .. })); + source.borrow_mut().status = Status::Unlinked; + } + assert!(matches!(src.borrow().status, Status::Unlinked)); + return Err(err); + } + + assert!(matches!( + src.borrow().status, + Status::Linked { .. } | Status::EvaluatingAsync { .. } | Status::Evaluated { .. } + )); + assert!(stack.is_empty()); + + Ok(()) + } + + pub(super) fn inner_link( + module: &Module, + stack: &mut Vec, + mut index: usize, + context: &mut Context<'_>, + ) -> JsResult { + let ModuleKind::Source(src) = module.kind() else { + unreachable!("must only be called for `SourceTextModule`s"); + }; + if matches!( + src.borrow().status, + Status::Linking { .. } + | Status::PreLinked { .. } + | Status::Linked { .. } + | Status::EvaluatingAsync { .. } + | Status::Evaluated { .. } + ) { + return Ok(index); + } + + assert!(matches!(src.borrow().status, Status::Unlinked)); + + { + let mut module = src.borrow_mut(); + module.status = Status::Linking { + info: DfsInfo { + dfs_index: index, + dfs_ancestor_index: index, + }, + }; + } + + index += 1; + + stack.push(src.clone()); + + let requested = src.borrow().requested_modules.clone(); + for required in requested { + let required_module = src.borrow().loaded_modules[&required].clone(); + + index = required_module.inner_link(stack, index, context)?; + if let ModuleKind::Source(required_module) = required_module.kind() { + debug_assert!(match required_module.borrow().status { + Status::PreLinked { .. } + | Status::Linked { .. } + | Status::EvaluatingAsync { .. } + | Status::Evaluated { .. } => true, + Status::Linking { .. } if stack.contains(required_module) => true, + _ => false, + }); + + let req_index = if let Status::Linking { + info: + DfsInfo { + dfs_ancestor_index, .. + }, + } = &required_module.borrow().status + { + Some(*dfs_ancestor_index) + } else { + None + }; + + if let Some(req_index) = req_index { + let mut module = src.borrow_mut(); + + let DfsInfo { + dfs_ancestor_index, .. + } = module + .status + .dfs_info_mut() + .expect("should be on the linking state"); + *dfs_ancestor_index = usize::min(*dfs_ancestor_index, req_index); + } + } + } + Self::initialize_environment(module, context)?; + debug_assert_eq!(stack.iter().filter(|module| *module == src).count(), 1); + debug_assert!({ + let DfsInfo { + dfs_ancestor_index, + dfs_index, + } = src + .borrow() + .status + .dfs_info() + .copied() + .expect("should be linking"); + dfs_ancestor_index <= dfs_index + }); + + let info = src.borrow().status.dfs_info().copied(); + match info { + Some(info) if info.dfs_ancestor_index == info.dfs_index => loop { + let last = stack.pop().expect("should have at least one element"); + last.borrow_mut() + .status + .transition(|current| match current { + Status::PreLinked { info, context } => Status::Linked { info, context }, + _ => { + unreachable!( + "can only transition to `Linked` from the `PreLinked` state" + ) + } + }); + if &last == src { + break; + } + }, + _ => {} + } + + Ok(index) + } + + pub(super) fn evaluate(&self, context: &mut Context<'_>) -> JsPromise { + let (module, promise) = { + let this = self.borrow(); + match &this.status { + Status::Unlinked + | Status::Linking { .. } + | Status::PreLinked { .. } + | Status::Evaluating { .. } => { + unreachable!() + } + Status::Linked { .. } => (self.clone(), None), + 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") + }), + ), + } + }; + + if let Some(promise) = promise { + return promise; + } + + let mut stack = Vec::new(); + + let capability = PromiseCapability::new( + &context.intrinsics().constructors().promise().constructor(), + context, + ) + .expect("capability creation must always succeed when using the `%Promise%` intrinsic"); + + let result = module.inner_evaluate(&mut stack, 0, Some(capability.clone()), context); + + match result { + Ok(_) => { + assert!(match &module.borrow().status { + Status::EvaluatingAsync { .. } => true, + Status::Evaluated { error, .. } if error.is_none() => true, + _ => false, + }); + + if matches!(&module.borrow().status, Status::Evaluated { .. }) { + capability + .resolve() + .call(&JsValue::undefined(), &[], context) + .expect("cannot fail for the default resolve function"); + } + + assert!(stack.is_empty()); + } + Err(err) => { + for m in stack { + m.borrow_mut().status. + transition(|current| match current { + Status::Evaluating { + top_level_capability, + cycle_root, + .. + } + | Status::EvaluatingAsync { + top_level_capability, + cycle_root, + .. + } => Status::Evaluated { + top_level_capability, + cycle_root, + error: Some(err.clone()), + }, + _ => panic!( + "can only transition to `Evaluated` from the `Evaluating` or `EvaluatingAsync states" + ), + }); + } + assert!( + matches!(&self.borrow().status, Status::Evaluated { error, .. } if error.is_some()) + ); + + capability + .reject() + .call(&JsValue::undefined(), &[err.to_opaque(context)], context) + .expect("cannot fail for the default reject function"); + } + } + + JsPromise::from_object(capability.promise().clone()) + .expect("promise created from the %Promise% intrinsic is always native") + } + + pub(super) fn inner_evaluate( + &self, + stack: &mut Vec, + mut index: usize, + capability: Option, + context: &mut Context<'_>, + ) -> JsResult { + match &self.borrow_mut().status { + Status::Evaluating { .. } | Status::EvaluatingAsync { .. } => return Ok(index), + Status::Evaluated { error, .. } => return error.clone().map_or(Ok(index), Err), + Status::Linked { .. } => { + // evaluate a linked module + } + _ => unreachable!( + "2. Assert: module.[[Status]] is one of linked, evaluating-async, or evaluated." + ), + } + + let this = self.clone(); + self.borrow_mut().status.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`. "), + }); + + index += 1; + + let mut pending_async_dependencies = 0; + stack.push(self.clone()); + + let requested = self.borrow().requested_modules.clone(); + for required in requested { + let required_module = self.borrow().loaded_modules[&required].clone(); + index = required_module.inner_evaluate(stack, index, context)?; + + if let ModuleKind::Source(required_module) = required_module.kind() { + debug_assert!(match required_module.borrow().status { + Status::EvaluatingAsync { .. } | Status::Evaluated { .. } => true, + Status::Evaluating { .. } if stack.contains(required_module) => true, + _ => false, + }); + + let (required_module, async_eval, req_info) = match &required_module.borrow().status { + Status::Evaluating { + info, + async_eval_index, + .. + } => { + (required_module.clone(), async_eval_index.is_some(), Some(*info)) + } + Status::EvaluatingAsync { cycle_root, .. } + | Status::Evaluated { cycle_root, .. } => { + match &cycle_root.borrow().status { + Status::EvaluatingAsync { .. } => (cycle_root.clone(), true, None), + 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 this = self.borrow_mut(); + let info = this + .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); + } + + if async_eval { + pending_async_dependencies += 1; + required_module + .borrow_mut() + .async_parent_modules + .push(self.clone()); + } + } + } + + if pending_async_dependencies > 0 || self.borrow().has_tla { + { + let Status::Evaluating { async_eval_index, .. } = &mut self.borrow_mut().status else { + unreachable!("self should still be in the evaluating state") + }; + + *async_eval_index = Some(get_async_eval_index()?); + } + + if pending_async_dependencies == 0 { + self.execute_async(context); + } + } else { + self.execute(None, context)?; + } + + let dfs_info = self.borrow().status.dfs_info().copied().expect( + "haven't transitioned from the `Evaluating` state, so it should have its dfs info", + ); + + debug_assert_eq!(stack.iter().filter(|m| *m == self).count(), 1); + assert!(dfs_info.dfs_ancestor_index <= dfs_info.dfs_index); + + if dfs_info.dfs_ancestor_index == dfs_info.dfs_index { + loop { + let required_module = stack + .pop() + .expect("should at least have `self` in the stack"); + let is_self = self == &required_module; + + required_module.borrow_mut().status.transition(|current| match current { + Status::Evaluating { + top_level_capability, + cycle_root, + async_eval_index, + context, + .. + } => if let Some(async_eval_index) = async_eval_index { + Status::EvaluatingAsync { + top_level_capability, + cycle_root: if is_self { + cycle_root + } else { + self.clone() + }, + async_eval_index, + pending_async_dependencies, + context + } + } else { + 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" + ) + } + ); + + if is_self { + break; + } + } + } + + Ok(index) + } + + fn execute_async(&self, context: &mut Context<'_>) { + assert!(matches!( + self.borrow().status, + Status::Evaluating { .. } | Status::EvaluatingAsync { .. } + )); + assert!(self.borrow().has_tla); + + let capability = PromiseCapability::new( + &context.intrinsics().constructors().promise().constructor(), + context, + ) + .expect("cannot fail for the %Promise% intrinsic"); + + let on_fulfilled = FunctionObjectBuilder::new( + context, + NativeFunction::from_copy_closure_with_captures( + |_, _, module, context| { + async_module_execution_fulfilled(module, context); + Ok(JsValue::undefined()) + }, + self.clone(), + ), + ) + .build(); + + 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()); + async_module_execution_rejected(module, &error, context); + Ok(JsValue::undefined()) + }, + self.clone(), + ), + ) + .build(); + + Promise::perform_promise_then( + capability.promise(), + Some(on_fulfilled), + Some(on_rejected), + None, + context, + ); + + self.execute(Some(capability), context) + .expect("async modules cannot directly throw"); + } + + #[allow(clippy::mutable_key_type)] + fn gather_available_ancestors(&self, exec_list: &mut FxHashSet) { + for m in &self.borrow().async_parent_modules { + if !exec_list.contains(m) + && m.borrow() + .status + .cycle_root() + .map_or(false, |cr| cr.borrow().status.evaluation_error().is_none()) + { + let (deps, has_tla) = { + let m = &mut m.borrow_mut(); + let Status::EvaluatingAsync { pending_async_dependencies, .. } = &mut m.status else { + unreachable!("i. Assert: m.[[Status]] is evaluating-async."); + }; + + *pending_async_dependencies -= 1; + (*pending_async_dependencies, m.has_tla) + }; + + if deps == 0 { + exec_list.insert(m.clone()); + if !has_tla { + m.gather_available_ancestors(exec_list); + } + } + } + } + } + + fn initialize_environment(module: &Module, context: &mut Context<'_>) -> JsResult<()> { + #[derive(Debug)] + enum ImportBinding { + Namespace { + locator: BindingLocator, + module: Module, + }, + Single { + locator: BindingLocator, + export_locator: ExportLocator, + }, + } + + let ModuleKind::Source(src) = module.kind() else { + unreachable!("must only be called for `SourceTextModule`s"); + }; + + { + let src = src.borrow(); + for e in &src.indirect_export_entries { + module + .resolve_export(e.export_name(), &mut HashSet::default()) + .map_err(|err| match err { + ResolveExportError::NotFound(name) => { + JsNativeError::syntax().with_message(format!( + "could not find export `{}`", + context.interner().resolve_expect(name) + )) + } + ResolveExportError::Ambiguous(name) => JsNativeError::syntax() + .with_message(format!( + "could not resolve ambiguous export `{}`", + context.interner().resolve_expect(name) + )), + })?; + } + } + + let mut realm = module.realm().clone(); + 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 = { + let src = src.borrow(); + for entry in &src.import_entries { + let imported_module = &src.loaded_modules[&entry.module_request()]; + + if let ImportName::Name(name) = entry.import_name() { + let resolution = imported_module + .resolve_export(name, &mut HashSet::default()) + .map_err(|err| match err { + ResolveExportError::NotFound(name) => JsNativeError::syntax() + .with_message(format!( + "could not find export `{}`", + compiler.interner().resolve_expect(name) + )), + ResolveExportError::Ambiguous(name) => JsNativeError::syntax() + .with_message(format!( + "could not resolve ambiguous export `{}`", + compiler.interner().resolve_expect(name) + )), + })?; + compiler.create_immutable_binding(entry.local_name(), true); + let locator = compiler.initialize_immutable_binding(entry.local_name()); + if let BindingName::Name(_) = resolution.binding_name { + imports.push(ImportBinding::Single { + locator, + export_locator: resolution, + }); + } else { + imports.push(ImportBinding::Namespace { + locator, + module: imported_module.clone(), + }); + } + } else { + compiler.create_immutable_binding(entry.local_name(), true); + let locator = compiler.initialize_immutable_binding(entry.local_name()); + imports.push(ImportBinding::Namespace { + locator, + module: imported_module.clone(), + }); + } + } + + let var_declarations = var_scoped_declarations(&src.code); + 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(&src.code); + 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(&src.code); + + Gc::new(compiler.finish()) + }; + + let mut envs = EnvironmentStack::new(global_env); + envs.push_module(module_compile_env); + + std::mem::swap(&mut context.vm.environments, &mut envs); + let stack = std::mem::take(&mut context.vm.stack); + let active_function = context.vm.active_function.take(); + context.swap_realm(&mut realm); + + // initialize import bindings + for import in imports { + match import { + ImportBinding::Namespace { locator, module } => { + let namespace = module.get_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.get_namespace(context); + context.vm.environments.put_lexical_value( + locator.environment_index(), + locator.binding_index(), + namespace.into(), + ); + } + }, + } + } + + 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()); + *module.inner.environment.borrow_mut() = envs.current().as_declarative().cloned(); + + src.borrow_mut().status.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" + ), + }); + + Ok(()) + } + + fn execute( + &self, + capability: Option, + context: &mut Context<'_>, + ) -> JsResult<()> { + let SourceTextContext { + codeblock, + mut environments, + mut realm, + } = match &self.borrow().status { + 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; + + std::mem::swap(&mut context.vm.environments, &mut environments); + let stack = std::mem::take(&mut context.vm.stack); + let function = context.vm.active_function.take(); + context.swap_realm(&mut realm); + context.vm.push_frame(callframe); + + 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(); + + if let CompletionRecord::Throw(err) = result { + Err(err) + } else { + Ok(()) + } + } + + #[track_caller] + fn borrow(&self) -> GcRef<'_, SourceTextModuleData> { + GcRefCell::borrow(&self.0) + } + + #[track_caller] + fn borrow_mut(&self) -> GcRefMut<'_, SourceTextModuleData> { + GcRefCell::borrow_mut(&self.0) + } +} + +/// [`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<'_>) { + if let Status::Evaluated { error, .. } = &module.borrow().status { + assert!(error.is_some()); + return; + } + + module + .borrow_mut() + .status + .transition(|status| match status { + Status::EvaluatingAsync { + top_level_capability, + cycle_root, + .. + } => Status::Evaluated { + top_level_capability, + cycle_root, + error: None, + }, + _ => unreachable!(), + }); + + if let Some(cap) = module.borrow().status.top_level_capability() { + debug_assert_eq!(module.borrow().status.cycle_root(), Some(module)); + + cap.resolve() + .call(&JsValue::undefined(), &[], context) + .expect("default `resolve` function cannot fail"); + } + + let mut ancestors = FxHashSet::default(); + + module.gather_available_ancestors(&mut ancestors); + + let mut ancestors = ancestors.into_iter().collect::>(); + + ancestors.sort_by_cached_key(|m| { + let Status::EvaluatingAsync { async_eval_index, .. } = &m.borrow().status else { + unreachable!("GatherAvailableAncestors: i. Assert: m.[[Status]] is evaluating-async."); + }; + + *async_eval_index + }); + + for m in ancestors { + if let Status::Evaluated { error, .. } = &m.borrow().status { + assert!(error.is_some()); + continue; + } + + let has_tla = m.borrow().has_tla; + if has_tla { + m.execute_async(context); + } else { + let result = m.execute(None, context); + + if let Err(e) = result { + async_module_execution_rejected(module, &e, context); + } + + m.borrow_mut().status.transition(|status| match status { + Status::EvaluatingAsync { + top_level_capability, + cycle_root, + .. + } => Status::Evaluated { + top_level_capability, + cycle_root, + error: None, + }, + _ => unreachable!(), + }); + + if let Some(cap) = m.borrow().status.top_level_capability() { + debug_assert_eq!(m.borrow().status.cycle_root(), Some(&m)); + + cap.resolve() + .call(&JsValue::undefined(), &[], context) + .expect("default `resolve` function cannot fail"); + } + } + } +} + +/// [`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<'_>, +) { + if let Status::Evaluated { error, .. } = &module.borrow().status { + assert!(error.is_some()); + return; + } + + module + .borrow_mut() + .status + .transition(|status| match status { + Status::EvaluatingAsync { + top_level_capability, + cycle_root, + .. + } => Status::Evaluated { + top_level_capability, + cycle_root, + error: Some(error.clone()), + }, + _ => unreachable!(), + }); + + let module_b = module.borrow(); + for m in &module_b.async_parent_modules { + async_module_execution_rejected(m, error, context); + } + + if let Some(cap) = module_b.status.top_level_capability() { + debug_assert_eq!(module_b.status.cycle_root(), Some(module)); + + cap.reject() + .call(&JsValue::undefined(), &[error.to_opaque(context)], context) + .expect("default `reject` function cannot fail"); + } +} + +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() + }) +} + +impl PartialEq for SourceTextModule { + fn eq(&self, other: &Self) -> bool { + std::ptr::eq(self.0.as_ref(), other.0.as_ref()) + } +} + +impl Eq for SourceTextModule {} + +impl Hash for SourceTextModule { + fn hash(&self, state: &mut H) { + std::ptr::hash(self.0.as_ref(), state); + } +} diff --git a/boa_engine/src/object/internal_methods/immutable.rs b/boa_engine/src/object/internal_methods/immutable.rs new file mode 100644 index 00000000000..62511cdc13f --- /dev/null +++ b/boa_engine/src/object/internal_methods/immutable.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 [**Module Namespace Exotic Objects**][spec]. +/// +/// [spec]: https://tc39.es/ecma262/#sec-module-namespace-exotic-objects +pub(crate) static IMMUTABLE_PROTOTYPE_EXOTIC_INTERNAL_METHODS: InternalObjectMethods = + InternalObjectMethods { + __set_prototype_of__: 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 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 2660b4c444b..b594205196d 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; pub(super) mod integer_indexed; +pub(super) mod namespace; pub(super) mod proxy; pub(super) mod string; diff --git a/boa_engine/src/object/internal_methods/namespace.rs b/boa_engine/src/object/internal_methods/namespace.rs new file mode 100644 index 00000000000..1430c108cf9 --- /dev/null +++ b/boa_engine/src/object/internal_methods/namespace.rs @@ -0,0 +1,308 @@ +use std::collections::HashSet; + +use crate::{ + module::BindingName, + object::{JsObject, JsPrototype}, + property::{PropertyDescriptor, PropertyKey}, + Context, JsNativeError, JsResult, JsValue, +}; + +use super::{ + immutable, 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__: get_prototype_of, + __set_prototype_of__: set_prototype_of, + __is_extensible__: is_extensible, + __prevent_extensions__: prevent_extensions, + __get_own_property__: get_own_property, + __define_own_property__: define_own_property, + __has_property__: has_property, + __get__: get, + __set__: set, + __delete__: delete, + __own_property_keys__: own_property_keys, + ..ORDINARY_INTERNAL_METHODS + }; + +/// [`[[GetPrototypeOf]] ( )`][spec]. +/// +/// [spec]: https://tc39.es/ecma262/#sec-module-namespace-exotic-objects-getprototypeof +#[allow(clippy::unnecessary_wraps)] +fn 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 set_prototype_of(obj: &JsObject, val: JsPrototype, context: &mut Context<'_>) -> JsResult { + // 1. Return ! SetImmutablePrototype(O, V). + Ok(immutable::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 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 prevent_extensions(_: &JsObject, _: &mut Context<'_>) -> JsResult { + Ok(true) +} + +/// [`[[GetOwnProperty]] ( P )`][spec] +/// +/// [spec]: https://tc39.es/ecma262/#sec-module-namespace-exotic-objects-getownproperty-p +fn 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 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 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 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.get_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 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 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 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 9ad7cd52f6a..059d84f93e8 100644 --- a/boa_engine/src/object/jsobject.rs +++ b/boa_engine/src/object/jsobject.rs @@ -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,8 @@ 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 obj: *const _ = &*self.inner; + 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 @@ -1097,7 +1098,7 @@ impl Debug for JsObject { if !limiter.visited && !limiter.live { f.debug_struct("JsObject") .field("vtable", &ptr) - .field("object", &self.inner.object) + .field("object", &obj) .finish() } else { f.write_str("{ ... }") diff --git a/boa_engine/src/object/mod.rs b/boa_engine/src/object/mod.rs index a9b8a1f57fa..555127af538 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::IMMUTABLE_PROTOTYPE_EXOTIC_INTERNAL_METHODS, integer_indexed::INTEGER_INDEXED_EXOTIC_INTERNAL_METHODS, + 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 ee2a871c6ea..f7772899721 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_gc/src/cell.rs b/boa_gc/src/cell.rs index faf5a6d3922..ce7d6d2cae7 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 e7804700147..c63da65fd72 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 dda97c30cf4..61327dfb580 100644 --- a/boa_gc/src/pointers/gc.rs +++ b/boa_gc/src/pointers/gc.rs @@ -94,7 +94,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_parser/src/parser/statement/declaration/export.rs b/boa_parser/src/parser/statement/declaration/export.rs index bd1cddfbccf..9d30090697c 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 6824188d56b..0f4f426c0c3 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 33d67c1a7de..302e1a3d0a0 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 70f846dbc7c..33809428fae 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 c226dfcd23e..5345f42e4a6 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::{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 context.parse_module(source, None) { + 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 context.parse_module(source, None) { 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 context.parse_module(source, None) { + 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 context.parse_module(source, None) { + 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. From d76401c1296720d144806faf22239af172dac7b8 Mon Sep 17 00:00:00 2001 From: jedel1043 Date: Tue, 9 May 2023 19:49:20 -0600 Subject: [PATCH 02/18] Fix recursion bug --- boa_engine/src/object/jsobject.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/boa_engine/src/object/jsobject.rs b/boa_engine/src/object/jsobject.rs index 059d84f93e8..0e74fe6ae9a 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 { From d72015996773d4e7b20dc61be229fd771636ae53 Mon Sep 17 00:00:00 2001 From: jedel1043 Date: Tue, 9 May 2023 19:50:29 -0600 Subject: [PATCH 03/18] Re-enable JsObject's debug printing --- boa_engine/src/object/jsobject.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/boa_engine/src/object/jsobject.rs b/boa_engine/src/object/jsobject.rs index 0e74fe6ae9a..df62ce1b068 100644 --- a/boa_engine/src/object/jsobject.rs +++ b/boa_engine/src/object/jsobject.rs @@ -1086,7 +1086,6 @@ impl RecursionLimiter { impl Debug for JsObject { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> std::fmt::Result { let ptr: *const _ = self.vtable(); - let obj: *const _ = &*self.inner; let limiter = RecursionLimiter::new(self.as_ref()); // Typically, using `!limiter.live` would be good enough here. @@ -1098,7 +1097,7 @@ impl Debug for JsObject { if !limiter.visited && !limiter.live { f.debug_struct("JsObject") .field("vtable", &ptr) - .field("object", &obj) + .field("object", &self.inner.object) .finish() } else { f.write_str("{ ... }") From 9099ed5035c5e7724a5c8b93022ac6e60fae7610 Mon Sep 17 00:00:00 2001 From: jedel1043 Date: Wed, 10 May 2023 22:15:17 -0600 Subject: [PATCH 04/18] Modify API and document some methods --- boa_cli/src/main.rs | 4 +- boa_engine/src/context/mod.rs | 22 +---- boa_engine/src/lib.rs | 2 + boa_engine/src/module/mod.rs | 155 +++++++++++++++++++++----------- boa_engine/src/module/source.rs | 134 ++++++++++++++------------- boa_tester/src/exec/mod.rs | 10 +-- 6 files changed, 178 insertions(+), 149 deletions(-) diff --git a/boa_cli/src/main.rs b/boa_cli/src/main.rs index 8ac824dd088..1e4b7e673b0 100644 --- a/boa_cli/src/main.rs +++ b/boa_cli/src/main.rs @@ -67,7 +67,7 @@ use boa_engine::{ builtins::promise::PromiseState, context::ContextBuilder, job::{FutureJob, JobQueue, NativeJob}, - module::{ModuleLoader, SimpleModuleLoader}, + module::{Module, ModuleLoader, SimpleModuleLoader}, optimizer::OptimizerOptions, property::Attribute, vm::flowgraph::{Direction, Graph}, @@ -306,7 +306,7 @@ fn evaluate_files( } } else if args.module { let result = (|| { - let module = context.parse_module(Source::from_bytes(&buffer), None)?; + let module = Module::parse(Source::from_bytes(&buffer), None, context)?; loader.insert( file.canonicalize() diff --git a/boa_engine/src/context/mod.rs b/boa_engine/src/context/mod.rs index 4db9351ecf0..4f86c37ff96 100644 --- a/boa_engine/src/context/mod.rs +++ b/boa_engine/src/context/mod.rs @@ -21,7 +21,7 @@ use crate::{ bytecompiler::ByteCompiler, class::{Class, ClassBuilder}, job::{JobQueue, NativeJob, SimpleJobQueue}, - module::{Module, ModuleLoader, SimpleModuleLoader, SourceTextModule}, + module::{ModuleLoader, SimpleModuleLoader}, native_function::NativeFunction, object::{shape::SharedShape, FunctionObjectBuilder, JsObject}, optimizer::{Optimizer, OptimizerOptions, OptimizerStatistics}, @@ -209,26 +209,6 @@ impl<'host> Context<'host> { Ok(result) } - /// Abstract operation [`ParseModule ( sourceText, realm, hostDefined )`][spec] - /// - /// Parses the given source as a module. - /// - /// [spec]: https://tc39.es/ecma262/#sec-parsemodule - pub fn parse_module( - &mut self, - src: Source<'_, R>, - realm: Option, - ) -> JsResult { - let _timer = Profiler::global().start_event("Module parsing", "Main"); - let mut parser = Parser::new(src); - parser.set_identifier(self.next_parser_identifier()); - let module = parser.parse_module(&mut self.interner)?; - Ok(SourceTextModule::new( - module, - realm.unwrap_or_else(|| self.realm.clone()), - )) - } - /// 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"); diff --git a/boa_engine/src/lib.rs b/boa_engine/src/lib.rs index bd33d889abf..1206387ca4d 100644 --- a/boa_engine/src/lib.rs +++ b/boa_engine/src/lib.rs @@ -152,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, @@ -167,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 index 819a2cf57d0..3051f64b7eb 100644 --- a/boa_engine/src/module/mod.rs +++ b/boa_engine/src/module/mod.rs @@ -1,7 +1,12 @@ -//! Module related types. +//! Boa's implementation of the ECMAScript's module system. +//! +//! More information: +//! - [ECMAScript reference][spec] +//! +//! [spec]: https://tc39.es/ecma262/#sec-modules mod source; -use boa_parser::Source; +use boa_parser::{Parser, Source}; use boa_profiler::Profiler; pub use source::SourceTextModule; @@ -12,6 +17,7 @@ use rustc_hash::{FxHashMap, FxHashSet, FxHasher}; 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}; @@ -29,11 +35,11 @@ use crate::{ }; use crate::{js_string, JsNativeError, JsSymbol, NativeFunction}; -/// +/// The referrer from which a load request of a module originates. #[derive(Debug)] pub enum Referrer { /// - Module(SourceTextModule), + Module(Module), /// Realm(Realm), // TODO: script } @@ -43,10 +49,10 @@ 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 return `()` instead of `JsResult`, leaving the responsibility of calling - /// [`FinishLoadingImportedModule`][finish] to the host, but this simpler API just provides - /// a closures that replaces [`FinishLoadingImportedModule`]. Specifically, the requirements of - /// this hook per the spec are as follows: + /// 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 @@ -144,7 +150,7 @@ impl ModuleLoader for SimpleModuleLoader { } let source = Source::from_filepath(&path) .map_err(|err| JsNativeError::typ().with_message(err.to_string()))?; - let module = context.parse_module(source, None)?; + let module = Module::parse(source, None, context)?; self.insert(path, module.clone()); Ok(module) })(); @@ -156,11 +162,23 @@ impl ModuleLoader for SimpleModuleLoader { /// ECMAScript's [**Abstract module record**][spec]. /// /// [spec]: https://tc39.es/ecma262/#sec-abstract-module-records -#[derive(Debug, Clone, Trace, Finalize)] +#[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, @@ -170,39 +188,29 @@ struct Inner { host_defined: (), } -impl std::fmt::Debug for Inner { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.debug_struct("Inner") - .field("realm", &self.realm.addr()) - .field("environment", &self.environment) - .field("namespace", &self.namespace) - .field("kind", &self.kind) - .field("host_defined", &self.host_defined) - .finish() - } -} - +/// #[derive(Debug, Trace, Finalize)] -enum ModuleKind { - Source(SourceTextModule), +#[non_exhaustive] +pub enum ModuleKind { + /// + SourceText(SourceTextModule), + /// #[allow(unused)] Synthetic, } -#[derive(Debug, Clone)] -struct GraphLoadingState { - capability: PromiseCapability, - loading: Cell, - pending_modules: Cell, - visited: RefCell>, -} - #[derive(Debug, Clone)] pub(crate) struct ExportLocator { module: Module, binding_name: BindingName, } +#[derive(Debug, Clone, Copy)] +pub(crate) enum BindingName { + Name(Identifier), + Namespace, +} + impl ExportLocator { pub(crate) const fn module(&self) -> &Module { &self.module @@ -213,36 +221,72 @@ impl ExportLocator { } } -#[derive(Debug, Clone, Copy)] -pub(crate) enum BindingName { - Name(Identifier), - Namespace, +#[derive(Debug, Clone)] +struct GraphLoadingState { + capability: PromiseCapability, + loading: Cell, + pending_modules: Cell, + visited: RefCell>, } #[derive(Debug, Clone, Copy)] pub(crate) enum ResolveExportError { - NotFound(Sym), - Ambiguous(Sym), + NotFound, + Ambiguous, } impl Module { - pub(crate) fn realm(&self) -> &Realm { - &self.inner.realm + /// 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 + 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())?; + + Ok(Module { + inner: Gc::new(Inner { + realm: realm.unwrap_or_else(|| context.realm().clone()), + environment: GcRefCell::default(), + namespace: GcRefCell::default(), + kind: ModuleKind::SourceText(SourceTextModule::new(module)), + host_defined: (), + }), + }) } - pub(crate) fn environment(&self) -> Option> { - self.inner.environment.borrow().clone() + /// Gets the realm of this `Module`. + pub fn realm(&self) -> &Realm { + &self.inner.realm } - fn kind(&self) -> &ModuleKind { + /// Gets the kind of this `Module`. + pub 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 operation [`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)] pub fn load(&self, context: &mut Context<'_>) -> JsPromise { match self.kind() { - ModuleKind::Source(_) => SourceTextModule::load(self, context), + ModuleKind::SourceText(_) => SourceTextModule::load(self, context), ModuleKind::Synthetic => todo!("synthetic.load()"), } } @@ -250,7 +294,7 @@ impl Module { fn inner_load(&self, state: &Rc, context: &mut Context<'_>) { assert!(state.loading.get()); - if let ModuleKind::Source(_) = self.kind() { + if let ModuleKind::SourceText(_) = self.kind() { SourceTextModule::inner_load(self, state, context); if !state.loading.get() { return; @@ -296,7 +340,7 @@ impl Module { fn get_exported_names(&self, export_star_set: &mut Vec) -> FxHashSet { match self.kind() { - ModuleKind::Source(src) => src.get_exported_names(export_star_set), + ModuleKind::SourceText(src) => src.get_exported_names(export_star_set), ModuleKind::Synthetic => todo!("synthetic.get_exported_names()"), } } @@ -308,7 +352,7 @@ impl Module { resolve_set: &mut FxHashSet<(Module, Sym)>, ) -> Result { match self.kind() { - ModuleKind::Source(_) => { + ModuleKind::SourceText(_) => { SourceTextModule::resolve_export(self, export_name, resolve_set) } ModuleKind::Synthetic => todo!("synthetic.resolve_export()"), @@ -319,7 +363,7 @@ impl Module { #[allow(clippy::missing_panics_doc)] pub fn link(&self, context: &mut Context<'_>) -> JsResult<()> { match self.kind() { - ModuleKind::Source(_) => SourceTextModule::link(self, context), + ModuleKind::SourceText(_) => SourceTextModule::link(self, context), ModuleKind::Synthetic => todo!("synthetic.link()"), } } @@ -331,7 +375,7 @@ impl Module { context: &mut Context<'_>, ) -> JsResult { match self.kind() { - ModuleKind::Source(_) => SourceTextModule::inner_link(self, stack, index, context), + ModuleKind::SourceText(_) => SourceTextModule::inner_link(self, stack, index, context), #[allow(unreachable_code)] ModuleKind::Synthetic => { todo!("synthetic.load()"); @@ -367,7 +411,7 @@ impl Module { #[allow(clippy::missing_panics_doc)] pub fn evaluate(&self, context: &mut Context<'_>) -> JsPromise { match self.kind() { - ModuleKind::Source(src) => src.evaluate(context), + ModuleKind::SourceText(src) => src.evaluate(context), ModuleKind::Synthetic => todo!("synthetic.evaluate()"), } } @@ -379,7 +423,7 @@ impl Module { context: &mut Context<'_>, ) -> JsResult { match self.kind() { - ModuleKind::Source(src) => src.inner_evaluate(stack, index, None, context), + ModuleKind::SourceText(src) => src.inner_evaluate(stack, index, None, context), #[allow(unused, clippy::diverging_sub_expression)] ModuleKind::Synthetic => { let promise: JsPromise = todo!("module.Evaluate()"); @@ -395,8 +439,8 @@ impl Module { } } - /// Loads, links and evaluates the given module `src` by compiling down to bytecode, then - /// returning a promise that will resolve when the module executes. + /// Loads, links and evaluates this module, returning a promise that will resolve after the module + /// finishes its lifecycle. /// /// # Examples /// ```ignore @@ -484,6 +528,9 @@ pub struct ModuleNamespace { } 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 { let mut exports = names .into_iter() @@ -517,10 +564,12 @@ impl ModuleNamespace { namespace } + /// Gets the export names of the `ModuleNamespace` object. pub(crate) const fn exports(&self) -> &IndexMap> { &self.exports } + /// Gest the module associated with this `ModuleNamespace` 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 index b250d0ad70b..92e590d3d90 100644 --- a/boa_engine/src/module/source.rs +++ b/boa_engine/src/module/source.rs @@ -265,8 +265,8 @@ impl std::fmt::Debug for SourceTextModule { } impl SourceTextModule { - #[allow(clippy::new_ret_no_self)] - pub(crate) fn new(code: ModuleItemList, realm: Realm) -> Module { + /// Creates a new `SourceTextModule` from a parsed `ModuleItemList`. + pub(super) fn new(code: ModuleItemList) -> Self { let requested_modules = code.requests(); let import_entries = code.import_entries(); @@ -302,7 +302,7 @@ impl SourceTextModule { let has_tla = contains(&code, ContainsSymbol::AwaitExpression); - let src = SourceTextModule(Gc::new(GcRefCell::new(SourceTextModuleData { + SourceTextModule(Gc::new(GcRefCell::new(SourceTextModuleData { code, requested_modules, has_tla, @@ -314,19 +314,15 @@ impl SourceTextModule { loaded_modules: HashMap::default(), async_parent_modules: Vec::default(), import_meta: None, - }))); - - Module { - inner: Gc::new(super::Inner { - realm, - environment: GcRefCell::new(None), - namespace: GcRefCell::new(None), - kind: ModuleKind::Source(src), - host_defined: (), - }), - } + }))) } + /// Abstract operation [`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 pub(super) fn load(module: &Module, context: &mut Context<'_>) -> JsPromise { let pc = PromiseCapability::new( &context.intrinsics().constructors().promise().constructor(), @@ -353,7 +349,7 @@ impl SourceTextModule { state: &Rc, context: &mut Context<'_>, ) { - let ModuleKind::Source(src) = module.kind() else { + let ModuleKind::SourceText(src) = module.kind() else { unreachable!("must only be called for `SourceTextModule`s"); }; @@ -376,7 +372,7 @@ impl SourceTextModule { let src = src.clone(); let state = state.clone(); context.module_loader().load_imported_module( - Referrer::Module(src.clone()), + Referrer::Module(module.clone()), name_specifier, Box::new(move |completion, ctx| { if let Ok(loaded) = &completion { @@ -440,12 +436,12 @@ impl SourceTextModule { export_name: Sym, resolve_set: &mut FxHashSet<(Module, Sym)>, ) -> Result { - let ModuleKind::Source(src) = module.kind() else { + let ModuleKind::SourceText(src) = module.kind() else { unreachable!("must only be called for `SourceTextModule`s"); }; if resolve_set.contains(&(module.clone(), export_name)) { - return Err(ResolveExportError::NotFound(export_name)); + return Err(ResolveExportError::NotFound); } resolve_set.insert((module.clone(), export_name)); @@ -463,22 +459,20 @@ impl SourceTextModule { for e in &src.indirect_export_entries { if export_name == e.export_name() { let imported_module = &src.loaded_modules[&e.module_request()]; - match e.import_name() { - ReExportImportName::Star => { - return Ok(ExportLocator { - module: imported_module.clone(), - binding_name: BindingName::Namespace, - }) - } + return match e.import_name() { + ReExportImportName::Star => Ok(ExportLocator { + module: imported_module.clone(), + binding_name: BindingName::Namespace, + }), ReExportImportName::Name(_) => { - return imported_module.resolve_export(export_name, resolve_set); + imported_module.resolve_export(export_name, resolve_set) } - } + }; } } if export_name == Sym::DEFAULT { - return Err(ResolveExportError::NotFound(export_name)); + return Err(ResolveExportError::NotFound); } let mut star_resolution: Option = None; @@ -487,22 +481,22 @@ impl SourceTextModule { let imported_module = &src.loaded_modules[e]; let resolution = match imported_module.resolve_export(export_name, resolve_set) { Ok(resolution) => resolution, - Err(e @ ResolveExportError::Ambiguous(_)) => return Err(e), - Err(ResolveExportError::NotFound(_)) => continue, + Err(e @ ResolveExportError::Ambiguous) => return Err(e), + Err(ResolveExportError::NotFound) => continue, }; if let Some(star_resolution) = &star_resolution { // 1. Assert: There is more than one * import that includes the requested name. if resolution.module != star_resolution.module { - return Err(ResolveExportError::Ambiguous(export_name)); + return Err(ResolveExportError::Ambiguous); } match (resolution.binding_name, star_resolution.binding_name) { (BindingName::Namespace, BindingName::Name(_)) | (BindingName::Name(_), BindingName::Namespace) => { - return Err(ResolveExportError::Ambiguous(export_name)); + return Err(ResolveExportError::Ambiguous); } (BindingName::Name(res), BindingName::Name(star)) if res != star => { - return Err(ResolveExportError::Ambiguous(export_name)); + return Err(ResolveExportError::Ambiguous); } _ => {} } @@ -511,14 +505,14 @@ impl SourceTextModule { } } - star_resolution.ok_or(ResolveExportError::NotFound(export_name)) + star_resolution.ok_or(ResolveExportError::NotFound) } pub(super) fn link(module: &Module, context: &mut Context<'_>) -> JsResult<()> { - let ModuleKind::Source(src) = module.kind() else { + let ModuleKind::SourceText(src) = module.kind() else { unreachable!("must only be called for `SourceTextModule`s"); }; - assert!(matches!( + debug_assert!(matches!( src.borrow().status, Status::Unlinked | Status::Linked { .. } @@ -529,15 +523,15 @@ impl SourceTextModule { let mut stack = Vec::new(); if let Err(err) = Self::inner_link(module, &mut stack, 0, context) { - for source in stack { - assert!(matches!(source.borrow().status, Status::Linking { .. })); - source.borrow_mut().status = Status::Unlinked; + for m in stack { + debug_assert!(matches!(m.borrow().status, Status::Linking { .. })); + m.borrow_mut().status = Status::Unlinked; } assert!(matches!(src.borrow().status, Status::Unlinked)); return Err(err); } - assert!(matches!( + debug_assert!(matches!( src.borrow().status, Status::Linked { .. } | Status::EvaluatingAsync { .. } | Status::Evaluated { .. } )); @@ -552,7 +546,7 @@ impl SourceTextModule { mut index: usize, context: &mut Context<'_>, ) -> JsResult { - let ModuleKind::Source(src) = module.kind() else { + let ModuleKind::SourceText(src) = module.kind() else { unreachable!("must only be called for `SourceTextModule`s"); }; if matches!( @@ -566,7 +560,7 @@ impl SourceTextModule { return Ok(index); } - assert!(matches!(src.borrow().status, Status::Unlinked)); + debug_assert!(matches!(src.borrow().status, Status::Unlinked)); { let mut module = src.borrow_mut(); @@ -587,7 +581,7 @@ impl SourceTextModule { let required_module = src.borrow().loaded_modules[&required].clone(); index = required_module.inner_link(stack, index, context)?; - if let ModuleKind::Source(required_module) = required_module.kind() { + if let ModuleKind::SourceText(required_module) = required_module.kind() { debug_assert!(match required_module.borrow().status { Status::PreLinked { .. } | Status::Linked { .. } @@ -597,7 +591,7 @@ impl SourceTextModule { _ => false, }); - let req_index = if let Status::Linking { + let required_index = if let Status::Linking { info: DfsInfo { dfs_ancestor_index, .. @@ -609,7 +603,7 @@ impl SourceTextModule { None }; - if let Some(req_index) = req_index { + if let Some(required_index) = required_index { let mut module = src.borrow_mut(); let DfsInfo { @@ -618,7 +612,7 @@ impl SourceTextModule { .status .dfs_info_mut() .expect("should be on the linking state"); - *dfs_ancestor_index = usize::min(*dfs_ancestor_index, req_index); + *dfs_ancestor_index = usize::min(*dfs_ancestor_index, required_index); } } } @@ -803,7 +797,7 @@ impl SourceTextModule { let required_module = self.borrow().loaded_modules[&required].clone(); index = required_module.inner_evaluate(stack, index, context)?; - if let ModuleKind::Source(required_module) = required_module.kind() { + if let ModuleKind::SourceText(required_module) = required_module.kind() { debug_assert!(match required_module.borrow().status { Status::EvaluatingAsync { .. } | Status::Evaluated { .. } => true, Status::Evaluating { .. } if stack.contains(required_module) => true, @@ -1017,7 +1011,7 @@ impl SourceTextModule { }, } - let ModuleKind::Source(src) = module.kind() else { + let ModuleKind::SourceText(src) = module.kind() else { unreachable!("must only be called for `SourceTextModule`s"); }; @@ -1027,17 +1021,18 @@ impl SourceTextModule { module .resolve_export(e.export_name(), &mut HashSet::default()) .map_err(|err| match err { - ResolveExportError::NotFound(name) => { + ResolveExportError::NotFound => { JsNativeError::syntax().with_message(format!( "could not find export `{}`", - context.interner().resolve_expect(name) + context.interner().resolve_expect(e.export_name()) )) } - ResolveExportError::Ambiguous(name) => JsNativeError::syntax() - .with_message(format!( + ResolveExportError::Ambiguous => { + JsNativeError::syntax().with_message(format!( "could not resolve ambiguous export `{}`", - context.interner().resolve_expect(name) - )), + context.interner().resolve_expect(e.export_name()) + )) + } })?; } } @@ -1060,22 +1055,25 @@ impl SourceTextModule { let imported_module = &src.loaded_modules[&entry.module_request()]; if let ImportName::Name(name) = entry.import_name() { - let resolution = imported_module - .resolve_export(name, &mut HashSet::default()) - .map_err(|err| match err { - ResolveExportError::NotFound(name) => JsNativeError::syntax() - .with_message(format!( - "could not find export `{}`", - compiler.interner().resolve_expect(name) - )), - ResolveExportError::Ambiguous(name) => JsNativeError::syntax() - .with_message(format!( - "could not resolve ambiguous export `{}`", - compiler.interner().resolve_expect(name) - )), - })?; + let resolution = + imported_module + .resolve_export(name, &mut HashSet::default()) + .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) + )), + })?; + compiler.create_immutable_binding(entry.local_name(), true); let locator = compiler.initialize_immutable_binding(entry.local_name()); + if let BindingName::Name(_) = resolution.binding_name { imports.push(ImportBinding::Single { locator, diff --git a/boa_tester/src/exec/mod.rs b/boa_tester/src/exec/mod.rs index 5345f42e4a6..1b065267168 100644 --- a/boa_tester/src/exec/mod.rs +++ b/boa_tester/src/exec/mod.rs @@ -8,7 +8,7 @@ use crate::{ }; use boa_engine::{ builtins::promise::PromiseState, - module::{ModuleLoader, SimpleModuleLoader}, + module::{Module, ModuleLoader, SimpleModuleLoader}, native_function::NativeFunction, object::FunctionObjectBuilder, optimizer::OptimizerOptions, @@ -236,7 +236,7 @@ impl Test { // TODO: timeout let value = if self.is_module() { - let module = match context.parse_module(source, None) { + let module = match Module::parse(source, None, context) { Ok(module) => module, Err(err) => return (false, format!("Uncaught {err}")), }; @@ -306,7 +306,7 @@ impl Test { context.set_optimizer_options(OptimizerOptions::OPTIMIZE_ALL); if self.is_module() { - match context.parse_module(source, None) { + match Module::parse(source, None, context) { Ok(_) => (false, "ModuleItemList parsing should fail".to_owned()), Err(e) => (true, format!("Uncaught {e}")), } @@ -332,7 +332,7 @@ impl Test { .build() .expect("cannot fail with default global object"); - let module = match context.parse_module(source, None) { + let module = match Module::parse(source, None, context) { Ok(module) => module, Err(err) => return (false, format!("Uncaught {err}")), }; @@ -396,7 +396,7 @@ impl Test { return (false, e); } let error = if self.is_module() { - let module = match context.parse_module(source, None) { + let module = match Module::parse(source, None, context) { Ok(module) => module, Err(e) => return (false, format!("Uncaught {e}")), }; From 581afa08f51aaea2768c0b4faf72b1967d0025dc Mon Sep 17 00:00:00 2001 From: jedel1043 Date: Thu, 11 May 2023 19:00:08 -0600 Subject: [PATCH 05/18] Add missing documentation --- boa_engine/src/context/mod.rs | 3 +- boa_engine/src/module/mod.rs | 258 +++++-- boa_engine/src/module/source.rs | 661 +++++++++++++++--- .../src/object/internal_methods/namespace.rs | 2 +- boa_examples/scripts/modules/operations.mjs | 21 + boa_examples/scripts/modules/trig.mjs | 11 + boa_examples/src/bin/modules.rs | 120 ++++ 7 files changed, 915 insertions(+), 161 deletions(-) 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/boa_engine/src/context/mod.rs b/boa_engine/src/context/mod.rs index 4f86c37ff96..8bedb552058 100644 --- a/boa_engine/src/context/mod.rs +++ b/boa_engine/src/context/mod.rs @@ -230,8 +230,7 @@ impl<'host> Context<'host> { /// 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. diff --git a/boa_engine/src/module/mod.rs b/boa_engine/src/module/mod.rs index 3051f64b7eb..d9d86447d45 100644 --- a/boa_engine/src/module/mod.rs +++ b/boa_engine/src/module/mod.rs @@ -1,19 +1,26 @@ //! Boa's implementation of the ECMAScript's module system. //! +//! This module contains the [`Module`] type, which represents an [**Abstract Module Record**][module]. +//! 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`]. +//! +//! Additionally, 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. There's a default [`SimpleModuleLoader`] implementation that just loads modules +//! relative to a root path, which should hopefully cover most simple usecases. +//! //! More information: //! - [ECMAScript reference][spec] //! //! [spec]: https://tc39.es/ecma262/#sec-modules +//! [module]: https://tc39.es/ecma262/#sec-abstract-module-records mod source; -use boa_parser::{Parser, Source}; -use boa_profiler::Profiler; -pub use source::SourceTextModule; - -use boa_ast::expression::Identifier; -use boa_interner::Sym; -use indexmap::IndexMap; -use rustc_hash::{FxHashMap, FxHashSet, FxHasher}; +use source::SourceTextModule; use std::cell::{Cell, RefCell}; use std::hash::Hash; @@ -22,7 +29,14 @@ 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::property::{PropertyDescriptor, PropertyKey}; @@ -38,19 +52,22 @@ use crate::{js_string, JsNativeError, JsSymbol, 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`]. + /// a closure that replaces `FinishLoadingImportedModule`. /// /// # Requirements /// @@ -106,11 +123,14 @@ pub struct SimpleModuleLoader { } impl SimpleModuleLoader { - /// Creates a new `SimpleModuleLoader`. - pub fn new(root: &Path) -> JsResult { - let absolute = root - .canonicalize() - .map_err(|e| JsNativeError::typ().with_message(e.to_string()))?; + /// 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(), @@ -140,17 +160,29 @@ impl ModuleLoader for SimpleModuleLoader { let path = specifier .to_std_string() .map_err(|err| JsNativeError::typ().with_message(err.to_string()))?; - let path = Path::new(&path); - let path = self.root.join(path); - let path = path - .canonicalize() - .map_err(|e| JsNativeError::typ().with_message(e.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(err.to_string()))?; - let module = Module::parse(source, None, context)?; + 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) })(); @@ -188,34 +220,44 @@ struct Inner { host_defined: (), } -/// +/// The kind of a [`Module`]. #[derive(Debug, Trace, Finalize)] -#[non_exhaustive] -pub enum ModuleKind { - /// +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 ExportLocator { +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 ExportLocator { +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 } @@ -268,7 +310,7 @@ impl Module { } /// Gets the kind of this `Module`. - pub fn kind(&self) -> &ModuleKind { + pub(crate) fn kind(&self) -> &ModuleKind { &self.inner.kind } @@ -277,7 +319,7 @@ impl Module { self.inner.environment.borrow().clone() } - /// Abstract operation [`LoadRequestedModules ( [ hostDefined ] )`][spec]. + /// 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. @@ -291,29 +333,48 @@ impl Module { } } + /// 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(_) = self.kind() { + // continues on `inner_load SourceTextModule::inner_load(self, 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 operation [`ContinueModuleLoading ( state, moduleCompletion )`][spec]. + /// + /// [spec]: https://tc39.es/ecma262/#sec-ContinueModuleLoading fn resume_load( state: &Rc, completion: JsResult, @@ -338,6 +399,15 @@ impl Module { } } + /// 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), @@ -345,12 +415,23 @@ impl Module { } } + /// 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 { + ) -> Result { match self.kind() { ModuleKind::SourceText(_) => { SourceTextModule::resolve_export(self, export_name, resolve_set) @@ -359,7 +440,16 @@ impl Module { } } + /// 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)] pub fn link(&self, context: &mut Context<'_>) -> JsResult<()> { match self.kind() { @@ -368,6 +458,9 @@ impl Module { } } + /// Abstract operation [`InnerModuleLinking ( module, stack, index )`][spec]. + /// + /// [spec]: https://tc39.es/ecma262/#sec-InnerModuleLinking fn inner_link( &self, stack: &mut Vec, @@ -377,37 +470,28 @@ impl Module { match self.kind() { ModuleKind::SourceText(_) => SourceTextModule::inner_link(self, stack, index, context), #[allow(unreachable_code)] + // If module is not a Cyclic Module Record, then ModuleKind::Synthetic => { - todo!("synthetic.load()"); + // a. Perform ? module.Link(). + todo!("synthetic.link()"); + // b. Return index. Ok(index) } } } - pub(crate) fn get_namespace(&self, context: &mut Context<'_>) -> JsObject { - if let Some(obj) = self.inner.namespace.borrow().clone() { - return obj; - } - - let exported_names = self.get_exported_names(&mut Vec::default()); - - let unambiguous_names = exported_names - .into_iter() - .filter_map(|name| { - self.resolve_export(name, &mut HashSet::default()) - .ok() - .map(|_| name) - }) - .collect(); - - let namespace = ModuleNamespace::create(self.clone(), unambiguous_names, context); - - *self.inner.namespace.borrow_mut() = Some(namespace.clone()); - - namespace - } - + /// 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)] pub fn evaluate(&self, context: &mut Context<'_>) -> JsPromise { match self.kind() { @@ -416,6 +500,9 @@ impl Module { } } + /// Abstract operation [`InnerModuleLinking ( module, stack, index )`][spec]. + /// + /// [spec]: https://tc39.es/ecma262/#sec-InnerModuleLinking fn inner_evaluate( &self, stack: &mut Vec, @@ -424,15 +511,20 @@ impl Module { ) -> 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)), } } @@ -443,23 +535,28 @@ impl Module { /// finishes its lifecycle. /// /// # Examples - /// ```ignore - /// # use boa_engine::{Context, Source}; - /// let loader: &ModuleLoader = &ModuleLoader::new(Path::new(".")); - /// let mut context = Context::builder().module_loader(loader).build().unwrap(); + /// ``` + /// # 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 = context.parse_module(source, None).unwrap(); + /// let module = Module::parse(source, None, context).unwrap(); /// - /// loader.insert(Path::new("./main.mjs").canonicalize().unwrap(), module.clone()); + /// loader.insert(Path::new("main.mjs").to_path_buf(), module.clone()); /// /// let promise = module.load_link_evaluate(context).unwrap(); /// /// context.run_jobs(); /// - /// assert!(matches!(promise.state(), PromiseState::Fulfilled(JsValue::undefined()))); + /// assert_eq!(promise.state().unwrap(), PromiseState::Fulfilled(JsValue::undefined())); /// ``` + #[allow(clippy::drop_copy)] pub fn load_link_evaluate(&self, context: &mut Context<'_>) -> JsResult { let main_timer = Profiler::global().start_event("Module evaluation", "Main"); @@ -503,6 +600,33 @@ impl Module { 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 { + self.inner + .namespace + .borrow_mut() + .get_or_insert_with(|| { + let exported_names = self.get_exported_names(&mut Vec::default()); + + let unambiguous_names = exported_names + .into_iter() + .filter_map(|name| { + self.resolve_export(name, &mut HashSet::default()) + .ok() + .map(|_| name) + }) + .collect(); + + ModuleNamespace::create(self.clone(), unambiguous_names, context) + }) + .clone() + } } impl PartialEq for Module { @@ -519,7 +643,9 @@ impl Hash for Module { } } -/// An object containing the exports of a module. +/// 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, diff --git a/boa_engine/src/module/source.rs b/boa_engine/src/module/source.rs index 92e590d3d90..c025971ad00 100644 --- a/boa_engine/src/module/source.rs +++ b/boa_engine/src/module/source.rs @@ -31,18 +31,29 @@ use crate::{ Context, JsArgs, JsError, JsNativeError, JsObject, JsResult, JsString, JsValue, NativeFunction, }; -use super::{BindingName, ExportLocator, GraphLoadingState, Module, Referrer, ResolveExportError}; +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] @@ -79,6 +90,9 @@ enum Status { }, } +// SAFETY: This must be synced with `Status` to mark any new data added that needs to be traced. +// This implementation is necessary to be able to transition from one state to another by destructuring, +// which saves us some unnecessary clones. unsafe impl Trace for Status { custom_trace!(this, { match this { @@ -114,6 +128,8 @@ unsafe impl Trace for Status { } 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, @@ -124,6 +140,8 @@ impl Status { } } + /// 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, @@ -134,6 +152,8 @@ impl Status { } } + /// 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 @@ -155,6 +175,7 @@ impl Status { } } + /// 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(), @@ -162,6 +183,7 @@ impl Status { } } + /// 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, .. } @@ -171,6 +193,8 @@ impl Status { } } + /// 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, @@ -179,6 +203,10 @@ impl Status { } } +/// 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, @@ -204,8 +232,26 @@ unsafe impl Trace for SourceTextContext { }); } +/// 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(Gc>); + +impl std::fmt::Debug for SourceTextModule { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let limiter = RecursionLimiter::new(&*self.0); + + if !limiter.visited && !limiter.live { + f.debug_tuple("SourceTextModule").field(&self.0).finish() + } else { + f.debug_tuple("SourceTextModule").field(&"").finish() + } + } +} + #[derive(Finalize)] -struct SourceTextModuleData { +struct Inner { code: ModuleItemList, status: Status, requested_modules: FxHashSet, @@ -219,7 +265,7 @@ struct SourceTextModuleData { star_export_entries: Vec, } -impl std::fmt::Debug for SourceTextModuleData { +impl std::fmt::Debug for Inner { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.debug_struct("SourceTextModuleData") .field("code", &"ModuleItemList") @@ -237,7 +283,7 @@ impl std::fmt::Debug for SourceTextModuleData { } } -unsafe impl Trace for SourceTextModuleData { +unsafe impl Trace for Inner { custom_trace!(this, { mark(&this.status); for module in this.loaded_modules.values() { @@ -248,34 +294,35 @@ unsafe impl Trace for SourceTextModuleData { }); } -/// -#[derive(Clone, Trace, Finalize)] -pub struct SourceTextModule(Gc>); - -impl std::fmt::Debug for SourceTextModule { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - let limiter = RecursionLimiter::new(&*self.0); - - if !limiter.visited && !limiter.live { - f.debug_tuple("SourceTextModule").field(&self.0).finish() - } else { - f.debug_tuple("SourceTextModule").field(&"").finish() - } - } -} - impl SourceTextModule { /// 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() => { @@ -284,25 +331,55 @@ impl SourceTextModule { _ => 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); - SourceTextModule(Gc::new(GcRefCell::new(SourceTextModuleData { + // 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. + SourceTextModule(Gc::new(GcRefCell::new(Inner { code, requested_modules, has_tla, @@ -317,20 +394,24 @@ impl SourceTextModule { }))) } - /// Abstract operation [`LoadRequestedModules ( [ hostDefined ] )`][spec]. + /// Concrete 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 + /// [spec]: https://tc39.es/ecma262/#sec-LoadRequestedModules pub(super) fn load(module: &Module, context: &mut Context<'_>) -> JsPromise { + // 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). module.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), @@ -340,10 +421,14 @@ impl SourceTextModule { context, ); + // 5. Return pc.[[Promise]]. JsPromise::from_object(pc.promise().clone()) .expect("promise created from the %Promise% intrinsic is always native") } + /// Abstract operation [`InnerModuleLoading`][spec]. + /// + /// [spec]: https://tc39.es/ecma262/#sec-InnerModuleLoading pub(super) fn inner_load( module: &Module, state: &Rc, @@ -353,18 +438,31 @@ impl SourceTextModule { unreachable!("must only be called for `SourceTextModule`s"); }; + // 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!(src.borrow().status, Status::Unlinked) && state.visited.borrow_mut().insert(src.clone()) { + // b. Let requestedModulesCount be the number of elements in module.[[RequestedModules]]. let requested = src.borrow().requested_modules.clone(); + // 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 = src.borrow().loaded_modules.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) @@ -389,6 +487,7 @@ impl SourceTextModule { context, ); } + // iii. If state.[[IsLoading]] is false, return unused. if !state.loading.get() { return; } @@ -396,74 +495,122 @@ impl SourceTextModule { } } + /// 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()); let module = self.borrow(); + // 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 &module.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 &module.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 &module.star_export_entries { + // a. Let requestedModule be GetImportedModule(module, e.[[ModuleRequest]]). let requested_module = module.loaded_modules[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( module: &Module, export_name: Sym, resolve_set: &mut FxHashSet<(Module, Sym)>, - ) -> Result { + ) -> Result { + // 1. Assert: module.[[Status]] is not new. + // 2. If resolveSet is not present, set resolveSet to a new empty List. let ModuleKind::SourceText(src) = module.kind() else { - unreachable!("must only be called for `SourceTextModule`s"); - }; + unreachable!("must only be called for `SourceTextModule`s"); + }; + // 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(&(module.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((module.clone(), export_name)); let src = src.borrow(); + // 5. For each ExportEntry Record e of module.[[LocalExportEntries]], do for e in &src.local_export_entries { + // a. If SameValue(exportName, e.[[ExportName]]) is true, then if export_name == e.export_name() { - return Ok(ExportLocator { + // i. Assert: module provides the direct binding for this export. + // ii. Return ResolvedBinding Record { [[Module]]: module, [[BindingName]]: e.[[LocalName]] }. + return Ok(ResolvedBinding { module: module.clone(), binding_name: BindingName::Name(e.local_name()), }); } } + // 6. For each ExportEntry Record e of module.[[IndirectExportEntries]], do for e in &src.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 = &src.loaded_modules[&e.module_request()]; return match e.import_name() { - ReExportImportName::Star => Ok(ExportLocator { + // 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.clone(), 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) } @@ -471,47 +618,75 @@ impl SourceTextModule { } } + // 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); } - let mut star_resolution: Option = None; + // 8. Let starResolution be null. + let mut star_resolution: Option = None; + // 9. For each ExportEntry Record e of module.[[StarExportEntries]], do for e in &src.star_export_entries { + // a. Let importedModule be GetImportedModule(module, e.[[ModuleRequest]]). let imported_module = &src.loaded_modules[e]; + // 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 { - // 1. Assert: There is more than one * import that includes the requested name. + // 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(module: &Module, context: &mut Context<'_>) -> JsResult<()> { let ModuleKind::SourceText(src) = module.kind() else { unreachable!("must only be called for `SourceTextModule`s"); }; + + // 1. Assert: module.[[Status]] is one of unlinked, linked, evaluating-async, or evaluated. debug_assert!(matches!( src.borrow().status, Status::Unlinked @@ -520,26 +695,40 @@ impl SourceTextModule { | 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(module, &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.borrow().status, Status::Linking { .. })); + // ii. Set m.[[Status]] to unlinked. m.borrow_mut().status = Status::Unlinked; } + // b. Assert: module.[[Status]] is unlinked. assert!(matches!(src.borrow().status, Status::Unlinked)); + // c. Return ? result. return Err(err); } + // 5. Assert: module.[[Status]] is one of linked, evaluating-async, or evaluated. debug_assert!(matches!( src.borrow().status, 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( module: &Module, stack: &mut Vec, @@ -549,6 +738,8 @@ impl SourceTextModule { let ModuleKind::SourceText(src) = module.kind() else { unreachable!("must only be called for `SourceTextModule`s"); }; + + // 2. If module.[[Status]] is one of linking, linked, evaluating-async, or evaluated, then if matches!( src.borrow().status, Status::Linking { .. } @@ -557,13 +748,18 @@ impl SourceTextModule { | Status::EvaluatingAsync { .. } | Status::Evaluated { .. } ) { + // a. Return index. return Ok(index); } + // 3. Assert: module.[[Status]] is unlinked. debug_assert!(matches!(src.borrow().status, Status::Unlinked)); { let mut module = src.borrow_mut(); + // 4. Set module.[[Status]] to linking. + // 5. Set module.[[DFSIndex]] to index. + // 6. Set module.[[DFSAncestorIndex]] to index. module.status = Status::Linking { info: DfsInfo { dfs_index: index, @@ -572,16 +768,25 @@ impl SourceTextModule { }; } + // 7. Set index to index + 1. index += 1; + // 8. Append module to stack. stack.push(src.clone()); + // 9. For each String required of module.[[RequestedModules]], do + let requested = src.borrow().requested_modules.clone(); for required in requested { + // a. Let requiredModule be GetImportedModule(module, required). let required_module = src.borrow().loaded_modules[&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.borrow().status { Status::PreLinked { .. } | Status::Linked { .. } @@ -591,6 +796,7 @@ impl SourceTextModule { _ => false, }); + // iii. If requiredModule.[[Status]] is linking, then let required_index = if let Status::Linking { info: DfsInfo { @@ -598,6 +804,9 @@ impl SourceTextModule { }, } = &required_module.borrow().status { + // 1. Set module.[[DFSAncestorIndex]] to min(module.[[DFSAncestorIndex]], + // requiredModule.[[DFSAncestorIndex]]). + Some(*dfs_ancestor_index) } else { None @@ -616,8 +825,11 @@ impl SourceTextModule { } } } + // 10. Perform ? module.InitializeEnvironment(). Self::initialize_environment(module, context)?; + // 11. Assert: module occurs exactly once in stack. debug_assert_eq!(stack.iter().filter(|module| *module == src).count(), 1); + // 12. Assert: module.[[DFSAncestorIndex]] ≤ module.[[DFSIndex]]. debug_assert!({ let DfsInfo { dfs_ancestor_index, @@ -633,8 +845,16 @@ impl SourceTextModule { let info = src.borrow().status.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.borrow_mut() .status .transition(|current| match current { @@ -645,6 +865,8 @@ impl SourceTextModule { ) } }); + + // v. If requiredModule and module are the same Module Record, set done to true. if &last == src { break; } @@ -652,10 +874,15 @@ impl SourceTextModule { _ => {} } + // 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) = { let this = self.borrow(); match &this.status { @@ -663,9 +890,10 @@ impl SourceTextModule { | Status::Linking { .. } | Status::PreLinked { .. } | Status::Evaluating { .. } => { - unreachable!() + 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, @@ -685,49 +913,67 @@ impl SourceTextModule { } }; + // 4. If module.[[TopLevelCapability]] is not empty, then + // a. Return module.[[TopLevelCapability]].[[Promise]]. if let Some(promise) = 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.borrow().status { 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.borrow().status, 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 { + // i. Assert: m.[[Status]] is evaluating. + // ii. Set m.[[Status]] to evaluated. + // iii. Set m.[[EvaluationError]] to result. m.borrow_mut().status. transition(|current| match current { - Status::Evaluating { - top_level_capability, - cycle_root, - .. - } - | Status::EvaluatingAsync { - top_level_capability, - cycle_root, + Status::Evaluating { + top_level_capability, + cycle_root, + .. + } + | Status::EvaluatingAsync { + top_level_capability, + cycle_root, .. } => Status::Evaluated { top_level_capability, @@ -737,12 +983,15 @@ impl SourceTextModule { _ => 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!(&self.borrow().status, 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) @@ -750,10 +999,14 @@ impl SourceTextModule { } } + // 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, @@ -761,10 +1014,35 @@ impl SourceTextModule { 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.borrow_mut().status { + // 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!( @@ -773,6 +1051,10 @@ impl SourceTextModule { } 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.borrow_mut().status.transition(|status| match status { Status::Linked { context, .. } => Status::Evaluating { context, @@ -787,17 +1069,25 @@ impl SourceTextModule { _ => 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 let requested = self.borrow().requested_modules.clone(); for required in requested { + // a. Let requiredModule be GetImportedModule(module, required). let required_module = self.borrow().loaded_modules[&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.borrow().status { Status::EvaluatingAsync { .. } | Status::Evaluated { .. } => true, Status::Evaluating { .. } if stack.contains(required_module) => true, @@ -805,17 +1095,23 @@ impl SourceTextModule { }); let (required_module, async_eval, req_info) = match &required_module.borrow().status { + // 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.borrow().status { 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."), @@ -834,8 +1130,11 @@ impl SourceTextModule { 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 .borrow_mut() .async_parent_modules @@ -844,19 +1143,26 @@ impl SourceTextModule { } } + // 12. If module.[[PendingAsyncDependencies]] > 0 or module.[[HasTLA]] is true, then if pending_async_dependencies > 0 || self.borrow().has_tla { + // a. Assert: module.[[AsyncEvaluation]] is false and was never previously set to true. { let Status::Evaluating { async_eval_index, .. } = &mut self.borrow_mut().status 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)?; } @@ -864,26 +1170,36 @@ impl SourceTextModule { "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.borrow_mut().status.transition(|current| match current { - Status::Evaluating { + 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 { @@ -894,6 +1210,7 @@ impl SourceTextModule { context } } else { + // iv. If requiredModule.[[AsyncEvaluation]] is false, set requiredModule.[[Status]] to evaluated. Status::Evaluated { top_level_capability, cycle_root: if is_self { @@ -910,33 +1227,45 @@ impl SourceTextModule { } ); + // 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<'_>) { - assert!(matches!( + // 1. Assert: module.[[Status]] is either evaluating or evaluating-async. + debug_assert!(matches!( self.borrow().status, Status::Evaluating { .. } | Status::EvaluatingAsync { .. } )); - assert!(self.borrow().has_tla); + // 2. Assert: module.[[HasTLA]] is true. + debug_assert!(self.borrow().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(), @@ -944,12 +1273,16 @@ impl SourceTextModule { ) .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(), @@ -957,6 +1290,7 @@ impl SourceTextModule { ) .build(); + // 8. Perform PerformPromiseThen(capability.[[Promise]], onFulfilled, onRejected). Promise::perform_promise_then( capability.promise(), Some(on_fulfilled), @@ -965,13 +1299,21 @@ impl SourceTextModule { 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.borrow().async_parent_modules { + // a. If execList does not contain m and m.[[CycleRoot]].[[EvaluationError]] is empty, then + // 2. Return unused. if !exec_list.contains(m) && m.borrow() .status @@ -980,16 +1322,25 @@ impl SourceTextModule { { let (deps, has_tla) = { let m = &mut m.borrow_mut(); + // 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.status 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.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); } @@ -998,6 +1349,9 @@ impl SourceTextModule { } } + /// Abstract operation [`InitializeEnvironment ( )`][spec]. + /// + /// [spec]: https://tc39.es/ecma262/#sec-source-text-module-record-initialize-environment fn initialize_environment(module: &Module, context: &mut Context<'_>) -> JsResult<()> { #[derive(Debug)] enum ImportBinding { @@ -1007,7 +1361,7 @@ impl SourceTextModule { }, Single { locator: BindingLocator, - export_locator: ExportLocator, + export_locator: ResolvedBinding, }, } @@ -1017,9 +1371,12 @@ impl SourceTextModule { { let src = src.borrow(); + // 1. For each ExportEntry Record e of module.[[IndirectExportEntries]], do for e in &src.indirect_export_entries { + // a. Let resolution be module.ResolveExport(e.[[ExportName]]). module .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!( @@ -1034,10 +1391,17 @@ impl SourceTextModule { )) } })?; + // 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 = module.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( @@ -1050,14 +1414,19 @@ impl SourceTextModule { let mut imports = Vec::new(); let codeblock = { + // 7. For each ImportEntry Record in of module.[[ImportEntries]], do let src = src.borrow(); for entry in &src.import_entries { + // a. Let importedModule be GetImportedModule(module, in.[[ModuleRequest]]). let imported_module = &src.loaded_modules[&entry.module_request()]; 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!( @@ -1071,23 +1440,36 @@ impl SourceTextModule { )), })?; + // 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.clone(), }); } } 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(), @@ -1095,59 +1477,89 @@ impl SourceTextModule { } } + // 18. Let code be module.[[ECMAScriptCode]]. + // 19. Let varDeclarations be the VarScopedDeclarations of code. let var_declarations = var_scoped_declarations(&src.code); + // 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(&src.code); + // 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); } @@ -1160,19 +1572,29 @@ impl SourceTextModule { 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. - // initialize import bindings + // deferred initialization of import bindings for import in imports { match import { ImportBinding::Namespace { locator, module } => { - let namespace = module.get_namespace(context); + // i. Let namespace be GetModuleNamespace(importedModule). + let namespace = module.namespace(context); context.vm.environments.put_lexical_value( locator.environment_index(), locator.binding_index(), @@ -1193,7 +1615,7 @@ impl SourceTextModule { .expect("last environment should be the module env") .set_indirect(locator.binding_index(), export_locator.module, name), BindingName::Namespace => { - let namespace = export_locator.module.get_namespace(context); + let namespace = export_locator.module.namespace(context); context.vm.environments.put_lexical_value( locator.environment_index(), locator.binding_index(), @@ -1204,6 +1626,7 @@ impl SourceTextModule { } } + // 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; @@ -1212,6 +1635,7 @@ impl SourceTextModule { debug_assert!(envs.current().as_declarative().is_some()); *module.inner.environment.borrow_mut() = envs.current().as_declarative().cloned(); + // 16. Set module.[[Context]] to moduleContext. src.borrow_mut().status.transition(|state| match state { Status::Linking { info } => Status::PreLinked { info, @@ -1226,14 +1650,19 @@ impl SourceTextModule { ), }); + // 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, @@ -1248,12 +1677,28 @@ impl SourceTextModule { 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); @@ -1262,34 +1707,47 @@ impl SourceTextModule { 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(()) } } + /// Borrows the inner data of the script module. #[track_caller] - fn borrow(&self) -> GcRef<'_, SourceTextModuleData> { + fn borrow(&self) -> GcRef<'_, Inner> { GcRefCell::borrow(&self.0) } + /// Mutably borrows the inner data of the script module. #[track_caller] - fn borrow_mut(&self) -> GcRefMut<'_, SourceTextModuleData> { + fn borrow_mut(&self) -> GcRefMut<'_, Inner> { GcRefCell::borrow_mut(&self.0) } } -/// [`AsyncModuleExecutionFulfilled ( module )`][spec] +/// 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.borrow().status { + // 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 .borrow_mut() .status @@ -1306,69 +1764,91 @@ fn async_module_execution_fulfilled(module: &SourceTextModule, context: &mut Con _ => unreachable!(), }); + // 7. If module.[[TopLevelCapability]] is not empty, then if let Some(cap) = module.borrow().status.top_level_capability() { + // a. Assert: module.[[CycleRoot]] is module. debug_assert_eq!(module.borrow().status.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.borrow().status else { - unreachable!("GatherAvailableAncestors: i. Assert: m.[[Status]] is evaluating-async."); - }; + 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.borrow().status { + // i. Assert: m.[[EvaluationError]] is not empty. assert!(error.is_some()); continue; } + // b. Else if m.[[HasTLA]] is true, then let has_tla = m.borrow().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); - } - - m.borrow_mut().status.transition(|status| match status { - Status::EvaluatingAsync { - top_level_capability, - cycle_root, - .. - } => Status::Evaluated { - top_level_capability, - cycle_root, - error: None, - }, - _ => unreachable!(), - }); + } else { + // iii. Else, + // 1. Set m.[[Status]] to evaluated. + m.borrow_mut().status.transition(|status| match status { + Status::EvaluatingAsync { + top_level_capability, + cycle_root, + .. + } => Status::Evaluated { + top_level_capability, + cycle_root, + error: None, + }, + _ => unreachable!(), + }); - if let Some(cap) = m.borrow().status.top_level_capability() { - debug_assert_eq!(m.borrow().status.cycle_root(), Some(&m)); + // 2. If m.[[TopLevelCapability]] is not empty, then + if let Some(cap) = m.borrow().status.top_level_capability() { + // a. Assert: m.[[CycleRoot]] is m. + debug_assert_eq!(m.borrow().status.cycle_root(), Some(&m)); - cap.resolve() - .call(&JsValue::undefined(), &[], context) - .expect("default `resolve` function cannot fail"); + // b. Perform ! Call(m.[[TopLevelCapability]].[[Resolve]], undefined, « undefined »). + cap.resolve() + .call(&JsValue::undefined(), &[], context) + .expect("default `resolve` function cannot fail"); + } } } } + // 13. Return unused. } -/// [`AsyncModuleExecutionRejected ( module, error )`][spec] +/// Abstract operation [`AsyncModuleExecutionRejected ( module, error )`][spec]. /// /// [spec]: https://tc39.es/ecma262/#sec-async-module-execution-rejected fn async_module_execution_rejected( @@ -1376,11 +1856,19 @@ fn async_module_execution_rejected( error: &JsError, context: &mut Context<'_>, ) { + // 1. If module.[[Status]] is evaluated, then if let Status::Evaluated { error, .. } = &module.borrow().status { + // 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 .borrow_mut() .status @@ -1397,35 +1885,24 @@ fn async_module_execution_rejected( _ => unreachable!(), }); + // 7. For each Cyclic Module Record m of module.[[AsyncParentModules]], do let module_b = module.borrow(); for m in &module_b.async_parent_modules { + // a. Perform AsyncModuleExecutionRejected(m, error). async_module_execution_rejected(m, error, context); } + // 8. If module.[[TopLevelCapability]] is not empty, then if let Some(cap) = module_b.status.top_level_capability() { + // a. Assert: module.[[CycleRoot]] is module. debug_assert_eq!(module_b.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"); } -} - -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() - }) + // 9. Return unused. } impl PartialEq for SourceTextModule { diff --git a/boa_engine/src/object/internal_methods/namespace.rs b/boa_engine/src/object/internal_methods/namespace.rs index 1430c108cf9..a4990598e6a 100644 --- a/boa_engine/src/object/internal_methods/namespace.rs +++ b/boa_engine/src/object/internal_methods/namespace.rs @@ -240,7 +240,7 @@ fn get( } else { // 9. If binding.[[BindingName]] is namespace, then // a. Return GetModuleNamespace(targetModule). - Ok(target_module.get_namespace(context).into()) + Ok(target_module.namespace(context).into()) } } diff --git a/boa_examples/scripts/modules/operations.mjs b/boa_examples/scripts/modules/operations.mjs new file mode 100644 index 00000000000..1c1b2feb5cd --- /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 }; \ No newline at end of file diff --git a/boa_examples/scripts/modules/trig.mjs b/boa_examples/scripts/modules/trig.mjs new file mode 100644 index 00000000000..200a5fed1a6 --- /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 }; \ No newline at end of file diff --git a/boa_examples/src/bin/modules.rs b/boa_examples/src/bin/modules.rs new file mode 100644 index 00000000000..bab89cd8dea --- /dev/null +++ b/boa_examples/src/bin/modules.rs @@ -0,0 +1,120 @@ +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 + .load(context) + .then( + Some( + FunctionObjectBuilder::new( + context, + NativeFunction::from_copy_closure_with_captures( + |_, _, module, context| { + module.link(context)?; + Ok(JsValue::undefined()) + }, + module.clone(), + ), + ) + .build(), + ), + None, + context, + )? + .then( + Some( + FunctionObjectBuilder::new( + context, + NativeFunction::from_copy_closure_with_captures( + |_, _, 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(()) +} From 0a4807df7bd43885d4b9ef538cadcb4a9aed6995 Mon Sep 17 00:00:00 2001 From: jedel1043 Date: Thu, 11 May 2023 19:04:36 -0600 Subject: [PATCH 06/18] Add newline to module scripts --- boa_examples/scripts/modules/operations.mjs | 2 +- boa_examples/scripts/modules/trig.mjs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/boa_examples/scripts/modules/operations.mjs b/boa_examples/scripts/modules/operations.mjs index 1c1b2feb5cd..a5c45b20ab1 100644 --- a/boa_examples/scripts/modules/operations.mjs +++ b/boa_examples/scripts/modules/operations.mjs @@ -18,4 +18,4 @@ function sqrt(a) { return Math.sqrt(a); } -export { sum, sub, mult, div, sqrt }; \ No newline at end of file +export { sum, sub, mult, div, sqrt }; diff --git a/boa_examples/scripts/modules/trig.mjs b/boa_examples/scripts/modules/trig.mjs index 200a5fed1a6..09638135113 100644 --- a/boa_examples/scripts/modules/trig.mjs +++ b/boa_examples/scripts/modules/trig.mjs @@ -8,4 +8,4 @@ function pyth(a, b) { return sqrt(a2b2); } -export { pyth }; \ No newline at end of file +export { pyth }; From cbaaf624b25c38f9ccb7923734e3bb5cc94c33a6 Mon Sep 17 00:00:00 2001 From: jedel1043 Date: Thu, 11 May 2023 19:08:37 -0600 Subject: [PATCH 07/18] npx prettier --- boa_examples/scripts/modules/operations.mjs | 10 +++++----- boa_examples/scripts/modules/trig.mjs | 8 ++++---- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/boa_examples/scripts/modules/operations.mjs b/boa_examples/scripts/modules/operations.mjs index a5c45b20ab1..036fb789f75 100644 --- a/boa_examples/scripts/modules/operations.mjs +++ b/boa_examples/scripts/modules/operations.mjs @@ -1,21 +1,21 @@ function sum(a, b) { - return a + b; + return a + b; } function sub(a, b) { - return a - b; + return a - b; } function mult(a, b) { - return a * b; + return a * b; } function div(a, b) { - return a / b; + return a / b; } function sqrt(a) { - return Math.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 index 09638135113..8407c566f1c 100644 --- a/boa_examples/scripts/modules/trig.mjs +++ b/boa_examples/scripts/modules/trig.mjs @@ -1,11 +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); + let a2 = mult(a, a); + let b2 = mult(b, b); + let a2b2 = sum(a2, b2); - return sqrt(a2b2); + return sqrt(a2b2); } export { pyth }; From a96708cb108b25e2ec4caceccd44f5fae79db234 Mon Sep 17 00:00:00 2001 From: jedel1043 Date: Sat, 13 May 2023 23:16:27 -0600 Subject: [PATCH 08/18] Apply reviews --- boa_ast/src/module_item_list/mod.rs | 8 +-- boa_engine/src/context/intrinsics.rs | 15 +++++ .../runtime/declarative/module.rs | 12 +++- boa_engine/src/module/mod.rs | 55 +++++++++++++------ 4 files changed, 69 insertions(+), 21 deletions(-) diff --git a/boa_ast/src/module_item_list/mod.rs b/boa_ast/src/module_item_list/mod.rs index 1d427359f4f..cbb622b996b 100644 --- a/boa_ast/src/module_item_list/mod.rs +++ b/boa_ast/src/module_item_list/mod.rs @@ -237,7 +237,7 @@ impl ModuleItemList { /// /// Gets the list of import entries of this module. /// - /// [spec]: https://tc39.es/ecma262/#sec-static-semantics-modulerequests + /// [spec]: https://tc39.es/ecma262/#sec-static-semantics-importentries #[inline] #[must_use] pub fn import_entries(&self) -> Vec { @@ -298,11 +298,11 @@ impl ModuleItemList { entries } - /// Operation [`ImportEntries`][spec]. + /// Operation [`ExportEntries`][spec]. /// - /// Gets the list of import entries of this module. + /// Gets the list of export entries of this module. /// - /// [spec]: https://tc39.es/ecma262/#sec-static-semantics-modulerequests + /// [spec]: https://tc39.es/ecma262/#sec-static-semantics-exportentries #[inline] #[must_use] pub fn export_entries(&self) -> Vec { diff --git a/boa_engine/src/context/intrinsics.rs b/boa_engine/src/context/intrinsics.rs index 24cdf849007..9d2ff4e0b1c 100644 --- a/boa_engine/src/context/intrinsics.rs +++ b/boa_engine/src/context/intrinsics.rs @@ -999,6 +999,8 @@ pub(crate) struct ObjectTemplates { function_without_proto: ObjectTemplate, function_with_prototype_without_proto: ObjectTemplate, + + namespace: ObjectTemplate, } impl ObjectTemplates { @@ -1104,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, @@ -1121,6 +1126,7 @@ impl ObjectTemplates { async_function, function_without_proto, function_with_prototype_without_proto, + namespace, } } @@ -1288,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/environments/runtime/declarative/module.rs b/boa_engine/src/environments/runtime/declarative/module.rs index 18d7d46550b..b21b668a163 100644 --- a/boa_engine/src/environments/runtime/declarative/module.rs +++ b/boa_engine/src/environments/runtime/declarative/module.rs @@ -5,12 +5,14 @@ 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, @@ -18,12 +20,20 @@ struct IndirectBinding { 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>, @@ -88,7 +98,7 @@ impl ModuleEnvironment { } } - /// Sets a reference from this environment to an external environment binding. + /// Creates an indirect binding reference to another environment binding. /// /// # Panics /// diff --git a/boa_engine/src/module/mod.rs b/boa_engine/src/module/mod.rs index d9d86447d45..2487189e438 100644 --- a/boa_engine/src/module/mod.rs +++ b/boa_engine/src/module/mod.rs @@ -39,7 +39,6 @@ use boa_parser::{Parser, Source}; use boa_profiler::Profiler; use crate::object::FunctionObjectBuilder; -use crate::property::{PropertyDescriptor, PropertyKey}; use crate::{ builtins::promise::{PromiseCapability, PromiseState}, environments::DeclarativeEnvironment, @@ -47,7 +46,7 @@ use crate::{ realm::Realm, Context, JsError, JsResult, JsString, JsValue, }; -use crate::{js_string, JsNativeError, JsSymbol, NativeFunction}; +use crate::{js_string, JsNativeError, NativeFunction}; /// The referrer from which a load request of a module originates. #[derive(Debug)] @@ -138,11 +137,13 @@ impl SimpleModuleLoader { } /// 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() } @@ -283,6 +284,7 @@ impl Module { /// 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, @@ -305,6 +307,7 @@ impl Module { } /// Gets the realm of this `Module`. + #[inline] pub fn realm(&self) -> &Realm { &self.inner.realm } @@ -326,6 +329,7 @@ impl Module { /// /// [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(_) => SourceTextModule::load(self, context), @@ -451,6 +455,7 @@ impl Module { /// /// [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(_) => SourceTextModule::link(self, context), @@ -493,6 +498,7 @@ impl Module { /// /// [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), @@ -557,6 +563,7 @@ impl Module { /// 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"); @@ -608,21 +615,31 @@ impl Module { /// [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 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). + // 4. Return namespace. ModuleNamespace::create(self.clone(), unambiguous_names, context) }) .clone() @@ -630,6 +647,7 @@ impl Module { } impl PartialEq for Module { + #[inline] fn eq(&self, other: &Self) -> bool { std::ptr::eq(self.inner.as_ref(), other.inner.as_ref()) } @@ -638,6 +656,7 @@ impl PartialEq for Module { impl Eq for Module {} impl Hash for Module { + #[inline] fn hash(&self, state: &mut H) { std::ptr::hash(self.inner.as_ref(), state); } @@ -658,6 +677,10 @@ impl ModuleNamespace { /// /// [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| { @@ -672,30 +695,30 @@ impl ModuleNamespace { .collect::>(); exports.sort_keys(); - let namespace = JsObject::from_proto_and_data_with_shared_shape( - context.root_shape(), - None, + // 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()], ); - namespace.borrow_mut().properties_mut().insert( - &PropertyKey::Symbol(JsSymbol::to_string_tag()), - PropertyDescriptor::builder() - .value(js_string!("Module")) - .writable(false) - .enumerable(false) - .configurable(false) - .build(), - ); + // 9. Set module.[[Namespace]] to M. + // Ignored because this is done by `Module::namespace` + + // 10. Return M. namespace } - /// Gets the export names of the `ModuleNamespace` object. + /// Gets the export names of the Module Namespace object. pub(crate) const fn exports(&self) -> &IndexMap> { &self.exports } - /// Gest the module associated with this `ModuleNamespace` object. + /// Gest the module associated with this Module Namespace object. pub(crate) const fn module(&self) -> &Module { &self.module } From 2665ffc5daf68c9e219f50ed609712603ec70112 Mon Sep 17 00:00:00 2001 From: jedel1043 Date: Mon, 15 May 2023 15:32:50 -0600 Subject: [PATCH 09/18] Add reference to parent struct on source module --- boa_engine/src/module/mod.rs | 78 ++++----- boa_engine/src/module/source.rs | 229 +++++++++++++------------- boa_gc/src/internals/ephemeron_box.rs | 19 +++ boa_gc/src/internals/gc_box.rs | 4 +- boa_gc/src/internals/mod.rs | 1 + boa_gc/src/lib.rs | 108 +++++------- boa_gc/src/pointers/ephemeron.rs | 13 +- boa_gc/src/pointers/gc.rs | 71 +++++++- boa_gc/src/pointers/weak.rs | 5 + boa_gc/src/test/mod.rs | 21 +-- 10 files changed, 312 insertions(+), 237 deletions(-) diff --git a/boa_engine/src/module/mod.rs b/boa_engine/src/module/mod.rs index 2487189e438..1720fd143df 100644 --- a/boa_engine/src/module/mod.rs +++ b/boa_engine/src/module/mod.rs @@ -296,11 +296,11 @@ impl Module { let module = parser.parse_module(context.interner_mut())?; Ok(Module { - inner: Gc::new(Inner { + inner: Gc::new_cyclic(|this| Inner { realm: realm.unwrap_or_else(|| context.realm().clone()), environment: GcRefCell::default(), namespace: GcRefCell::default(), - kind: ModuleKind::SourceText(SourceTextModule::new(module)), + kind: ModuleKind::SourceText(SourceTextModule::new(module, this.clone())), host_defined: (), }), }) @@ -332,7 +332,40 @@ impl Module { #[inline] pub fn load(&self, context: &mut Context<'_>) -> JsPromise { match self.kind() { - ModuleKind::SourceText(_) => SourceTextModule::load(self, context), + 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()"), } } @@ -344,9 +377,9 @@ impl Module { // 1. Assert: state.[[IsLoading]] is true. assert!(state.loading.get()); - if let ModuleKind::SourceText(_) = self.kind() { + if let ModuleKind::SourceText(src) = self.kind() { // continues on `inner_load - SourceTextModule::inner_load(self, state, context); + src.inner_load(state, context); if !state.loading.get() { return; } @@ -376,33 +409,6 @@ impl Module { // 6. Return unused. } - /// Abstract operation [`ContinueModuleLoading ( state, moduleCompletion )`][spec]. - /// - /// [spec]: https://tc39.es/ecma262/#sec-ContinueModuleLoading - fn resume_load( - state: &Rc, - completion: JsResult, - context: &mut Context<'_>, - ) { - if !state.loading.get() { - return; - } - - match completion { - Ok(m) => { - m.inner_load(state, context); - } - Err(err) => { - state.loading.set(false); - state - .capability - .reject() - .call(&JsValue::undefined(), &[err.to_opaque(context)], context) - .expect("cannot fail for the default reject function"); - } - } - } - /// Abstract method [`GetExportedNames([exportStarSet])`][spec]. /// /// Returns a list of all the names exported from this module. @@ -437,9 +443,7 @@ impl Module { resolve_set: &mut FxHashSet<(Module, Sym)>, ) -> Result { match self.kind() { - ModuleKind::SourceText(_) => { - SourceTextModule::resolve_export(self, export_name, resolve_set) - } + ModuleKind::SourceText(src) => src.resolve_export(export_name, resolve_set), ModuleKind::Synthetic => todo!("synthetic.resolve_export()"), } } @@ -458,7 +462,7 @@ impl Module { #[inline] pub fn link(&self, context: &mut Context<'_>) -> JsResult<()> { match self.kind() { - ModuleKind::SourceText(_) => SourceTextModule::link(self, context), + ModuleKind::SourceText(src) => src.link(context), ModuleKind::Synthetic => todo!("synthetic.link()"), } } @@ -473,7 +477,7 @@ impl Module { context: &mut Context<'_>, ) -> JsResult { match self.kind() { - ModuleKind::SourceText(_) => SourceTextModule::inner_link(self, stack, index, context), + ModuleKind::SourceText(src) => src.inner_link(stack, index, context), #[allow(unreachable_code)] // If module is not a Cyclic Module Record, then ModuleKind::Synthetic => { diff --git a/boa_engine/src/module/source.rs b/boa_engine/src/module/source.rs index c025971ad00..cb7d07ee3f3 100644 --- a/boa_engine/src/module/source.rs +++ b/boa_engine/src/module/source.rs @@ -1,5 +1,5 @@ use std::{ - cell::{Cell, RefCell}, + cell::Cell, collections::{HashMap, HashSet}, hash::Hash, rc::Rc, @@ -16,7 +16,7 @@ use boa_ast::{ }, Declaration, ModuleItemList, }; -use boa_gc::{custom_trace, empty_trace, Finalize, Gc, GcRef, GcRefCell, GcRefMut, Trace}; +use boa_gc::{custom_trace, empty_trace, Finalize, Gc, GcRef, GcRefCell, GcRefMut, Trace, WeakGc}; use boa_interner::Sym; use rustc_hash::{FxHashMap, FxHashSet}; @@ -252,6 +252,7 @@ impl std::fmt::Debug for SourceTextModule { #[derive(Finalize)] struct Inner { + parent: WeakGc, code: ModuleItemList, status: Status, requested_modules: FxHashSet, @@ -285,6 +286,7 @@ impl std::fmt::Debug for Inner { unsafe impl Trace for Inner { custom_trace!(this, { + mark(&this.parent); mark(&this.status); for module in this.loaded_modules.values() { mark(module); @@ -295,12 +297,23 @@ unsafe impl Trace for Inner { } impl SourceTextModule { + /// Gets the parent module of this source module. + fn parent(&self) -> Module { + Module { + inner: self + .borrow() + .parent + .upgrade() + .expect("source text cannot be dropped before parent module"), + } + } + /// 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 { + pub(super) fn new(code: ModuleItemList, parent: WeakGc) -> Self { // 3. Let requestedModules be the ModuleRequests of body. let requested_modules = code.requests(); // 4. Let importEntries be ImportEntries of body. @@ -380,6 +393,7 @@ impl SourceTextModule { // }. // Most of this can be ignored, since `Status` takes care of the remaining state. SourceTextModule(Gc::new(GcRefCell::new(Inner { + parent, code, requested_modules, has_tla, @@ -394,58 +408,18 @@ impl SourceTextModule { }))) } - /// Concrete method [`LoadRequestedModules ( [ hostDefined ] )`][spec]. - /// - /// [spec]: https://tc39.es/ecma262/#sec-LoadRequestedModules - pub(super) fn load(module: &Module, context: &mut Context<'_>) -> JsPromise { - // 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). - module.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") - } - /// Abstract operation [`InnerModuleLoading`][spec]. /// /// [spec]: https://tc39.es/ecma262/#sec-InnerModuleLoading - pub(super) fn inner_load( - module: &Module, - state: &Rc, - context: &mut Context<'_>, - ) { - let ModuleKind::SourceText(src) = module.kind() else { - unreachable!("must only be called for `SourceTextModule`s"); - }; - + 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!(src.borrow().status, Status::Unlinked) - && state.visited.borrow_mut().insert(src.clone()) + if matches!(self.borrow().status, Status::Unlinked) + && state.visited.borrow_mut().insert(self.clone()) { // b. Let requestedModulesCount be the number of elements in module.[[RequestedModules]]. - let requested = src.borrow().requested_modules.clone(); + let requested = self.borrow().requested_modules.clone(); // c. Set state.[[PendingModulesCount]] to state.[[PendingModulesCount]] + requestedModulesCount. state .pending_modules @@ -453,7 +427,7 @@ impl SourceTextModule { // 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 = src.borrow().loaded_modules.get(&required).cloned(); + let loaded = self.borrow().loaded_modules.get(&required).cloned(); if let Some(loaded) = loaded { // 1. Let record be that Record. // 2. Perform InnerModuleLoading(state, record.[[Module]]). @@ -467,12 +441,12 @@ impl SourceTextModule { .interner() .resolve_expect(required) .into_common(false); - let src = src.clone(); + let src = self.clone(); let state = state.clone(); context.module_loader().load_imported_module( - Referrer::Module(module.clone()), + Referrer::Module(self.parent()), name_specifier, - Box::new(move |completion, ctx| { + Box::new(move |completion, context| { if let Ok(loaded) = &completion { let mut src = src.borrow_mut(); let entry = src @@ -482,7 +456,40 @@ impl SourceTextModule { debug_assert_eq!(entry, loaded); } - Module::resume_load(&state, completion, ctx); + // 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, ); @@ -559,27 +566,24 @@ impl SourceTextModule { /// [spec]: https://tc39.es/ecma262/#sec-resolveexport #[allow(clippy::mutable_key_type)] pub(super) fn resolve_export( - module: &Module, + &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. - let ModuleKind::SourceText(src) = module.kind() else { - unreachable!("must only be called for `SourceTextModule`s"); - }; - // 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(&(module.clone(), export_name)) { + 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((module.clone(), export_name)); - let src = src.borrow(); + resolve_set.insert((parent.clone(), export_name)); + let src = self.borrow(); // 5. For each ExportEntry Record e of module.[[LocalExportEntries]], do for e in &src.local_export_entries { @@ -588,7 +592,7 @@ impl SourceTextModule { // i. Assert: module provides the direct binding for this export. // ii. Return ResolvedBinding Record { [[Module]]: module, [[BindingName]]: e.[[LocalName]] }. return Ok(ResolvedBinding { - module: module.clone(), + module: parent, binding_name: BindingName::Name(e.local_name()), }); } @@ -681,14 +685,10 @@ impl SourceTextModule { /// Concrete method [`Link ( )`][spec]. /// /// [spec]: https://tc39.es/ecma262/#sec-moduledeclarationlinking - pub(super) fn link(module: &Module, context: &mut Context<'_>) -> JsResult<()> { - let ModuleKind::SourceText(src) = module.kind() else { - unreachable!("must only be called for `SourceTextModule`s"); - }; - + 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!( - src.borrow().status, + self.borrow().status, Status::Unlinked | Status::Linked { .. } | Status::EvaluatingAsync { .. } @@ -700,7 +700,7 @@ impl SourceTextModule { // 3. Let result be Completion(InnerModuleLinking(module, stack, 0)). // 4. If result is an abrupt completion, then - if let Err(err) = Self::inner_link(module, &mut stack, 0, context) { + 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. @@ -709,14 +709,14 @@ impl SourceTextModule { m.borrow_mut().status = Status::Unlinked; } // b. Assert: module.[[Status]] is unlinked. - assert!(matches!(src.borrow().status, Status::Unlinked)); + assert!(matches!(self.borrow().status, Status::Unlinked)); // c. Return ? result. return Err(err); } // 5. Assert: module.[[Status]] is one of linked, evaluating-async, or evaluated. debug_assert!(matches!( - src.borrow().status, + self.borrow().status, Status::Linked { .. } | Status::EvaluatingAsync { .. } | Status::Evaluated { .. } )); // 6. Assert: stack is empty. @@ -730,18 +730,14 @@ impl SourceTextModule { /// /// [spec]: https://tc39.es/ecma262/#sec-InnerModuleLinking pub(super) fn inner_link( - module: &Module, + &self, stack: &mut Vec, mut index: usize, context: &mut Context<'_>, ) -> JsResult { - let ModuleKind::SourceText(src) = module.kind() else { - unreachable!("must only be called for `SourceTextModule`s"); - }; - // 2. If module.[[Status]] is one of linking, linked, evaluating-async, or evaluated, then if matches!( - src.borrow().status, + self.borrow().status, Status::Linking { .. } | Status::PreLinked { .. } | Status::Linked { .. } @@ -753,10 +749,10 @@ impl SourceTextModule { } // 3. Assert: module.[[Status]] is unlinked. - debug_assert!(matches!(src.borrow().status, Status::Unlinked)); + debug_assert!(matches!(self.borrow().status, Status::Unlinked)); { - let mut module = src.borrow_mut(); + let mut module = self.borrow_mut(); // 4. Set module.[[Status]] to linking. // 5. Set module.[[DFSIndex]] to index. // 6. Set module.[[DFSAncestorIndex]] to index. @@ -772,14 +768,14 @@ impl SourceTextModule { index += 1; // 8. Append module to stack. - stack.push(src.clone()); + stack.push(self.clone()); // 9. For each String required of module.[[RequestedModules]], do - let requested = src.borrow().requested_modules.clone(); + let requested = self.borrow().requested_modules.clone(); for required in requested { // a. Let requiredModule be GetImportedModule(module, required). - let required_module = src.borrow().loaded_modules[&required].clone(); + let required_module = self.borrow().loaded_modules[&required].clone(); // b. Set index to ? InnerModuleLinking(requiredModule, stack, index). index = required_module.inner_link(stack, index, context)?; @@ -813,7 +809,7 @@ impl SourceTextModule { }; if let Some(required_index) = required_index { - let mut module = src.borrow_mut(); + let mut module = self.borrow_mut(); let DfsInfo { dfs_ancestor_index, .. @@ -825,16 +821,18 @@ impl SourceTextModule { } } } + // 10. Perform ? module.InitializeEnvironment(). - Self::initialize_environment(module, context)?; + self.initialize_environment(context)?; + // 11. Assert: module occurs exactly once in stack. - debug_assert_eq!(stack.iter().filter(|module| *module == src).count(), 1); + 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, - } = src + } = self .borrow() .status .dfs_info() @@ -843,7 +841,7 @@ impl SourceTextModule { dfs_ancestor_index <= dfs_index }); - let info = src.borrow().status.dfs_info().copied(); + let info = self.borrow().status.dfs_info().copied(); match info { // 13. If module.[[DFSAncestorIndex]] = module.[[DFSIndex]], then @@ -867,7 +865,7 @@ impl SourceTextModule { }); // v. If requiredModule and module are the same Module Record, set done to true. - if &last == src { + if &last == self { break; } }, @@ -914,8 +912,8 @@ impl SourceTextModule { }; // 4. If module.[[TopLevelCapability]] is not empty, then - // a. Return module.[[TopLevelCapability]].[[Promise]]. if let Some(promise) = promise { + // a. Return module.[[TopLevelCapability]].[[Promise]]. return promise; } @@ -959,39 +957,39 @@ impl SourceTextModule { } // 9. If result is an abrupt completion, then Err(err) => { - // a. For each Cyclic Module Record m of stack, do + // a. For each Cyclic Module Record m of stack, do for m in stack { - // i. Assert: m.[[Status]] is evaluating. - // ii. Set m.[[Status]] to evaluated. - // iii. Set m.[[EvaluationError]] to result. - m.borrow_mut().status. - transition(|current| match current { + m.borrow_mut().status.transition(|current| match current { + // i. Assert: m.[[Status]] is evaluating. Status::Evaluating { top_level_capability, cycle_root, .. - } - | Status::EvaluatingAsync { + } | Status::EvaluatingAsync { top_level_capability, cycle_root, - .. - } => Status::Evaluated { + .. + } => { + // 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" - ), - }); + } + }, + _ => panic!( + "can only transition to `Evaluated` from the `Evaluating` or `EvaluatingAsync states" + ), + }); } - // b. Assert: module.[[Status]] is evaluated. - // c. Assert: module.[[EvaluationError]] is result. + // b. Assert: module.[[Status]] is evaluated. + // c. Assert: module.[[EvaluationError]] is result. assert!( matches!(&self.borrow().status, Status::Evaluated { error, .. } if error.is_some()) ); - // d. Perform ! Call(capability.[[Reject]], undefined, « result.[[Value]] »). + // d. Perform ! Call(capability.[[Reject]], undefined, « result.[[Value]] »). capability .reject() .call(&JsValue::undefined(), &[err.to_opaque(context)], context) @@ -1352,7 +1350,8 @@ impl SourceTextModule { /// Abstract operation [`InitializeEnvironment ( )`][spec]. /// /// [spec]: https://tc39.es/ecma262/#sec-source-text-module-record-initialize-environment - fn initialize_environment(module: &Module, context: &mut Context<'_>) -> JsResult<()> { + fn initialize_environment(&self, context: &mut Context<'_>) -> JsResult<()> { + let parent = self.parent(); #[derive(Debug)] enum ImportBinding { Namespace { @@ -1365,16 +1364,12 @@ impl SourceTextModule { }, } - let ModuleKind::SourceText(src) = module.kind() else { - unreachable!("must only be called for `SourceTextModule`s"); - }; - { - let src = src.borrow(); + let src = self.borrow(); // 1. For each ExportEntry Record e of module.[[IndirectExportEntries]], do for e in &src.indirect_export_entries { // a. Let resolution be module.ResolveExport(e.[[ExportName]]). - module + 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 { @@ -1398,7 +1393,7 @@ impl SourceTextModule { // 2. Assert: All named exports from module are resolvable. // 3. Let realm be module.[[Realm]]. // 4. Assert: realm is not undefined. - let mut realm = module.realm().clone(); + let mut realm = parent.realm().clone(); // 5. Let env be NewModuleEnvironment(realm.[[GlobalEnv]]). // 6. Set module.[[Environment]] to env. @@ -1415,7 +1410,7 @@ impl SourceTextModule { let codeblock = { // 7. For each ImportEntry Record in of module.[[ImportEntries]], do - let src = src.borrow(); + let src = self.borrow(); for entry in &src.import_entries { // a. Let importedModule be GetImportedModule(module, in.[[ModuleRequest]]). let imported_module = &src.loaded_modules[&entry.module_request()]; @@ -1633,10 +1628,10 @@ impl SourceTextModule { context.swap_realm(&mut realm); debug_assert!(envs.current().as_declarative().is_some()); - *module.inner.environment.borrow_mut() = envs.current().as_declarative().cloned(); + *parent.inner.environment.borrow_mut() = envs.current().as_declarative().cloned(); // 16. Set module.[[Context]] to moduleContext. - src.borrow_mut().status.transition(|state| match state { + self.borrow_mut().status.transition(|state| match state { Status::Linking { info } => Status::PreLinked { info, context: SourceTextContext { diff --git a/boa_gc/src/internals/ephemeron_box.rs b/boa_gc/src/internals/ephemeron_box.rs index 68cbb0af976..a42316146ac 100644 --- a/boa_gc/src/internals/ephemeron_box.rs +++ b/boa_gc/src/internals/ephemeron_box.rs @@ -117,6 +117,25 @@ impl EphemeronBox { } } + pub(crate) fn new_empty() -> Self { + Self { + header: EphemeronBoxHeader::new(), + data: Cell::new(None), + } + } + + pub(crate) fn init(&self, key: &Gc, value: V) { + let data = Box::into_raw(Box::new(Data { + key: key.inner_ptr(), + value, + })); + + // SAFETY: `Box::into_raw` must always return a non-null pointer. + let data = unsafe { NonNull::new_unchecked(data) }; + + self.data.set(Some(data)); + } + /// Returns `true` if the two references refer to the same `GcBox`. pub(crate) fn ptr_eq(this: &Self, other: &Self) -> bool { // Use .header to ignore fat pointer vtables, to work around diff --git a/boa_gc/src/internals/gc_box.rs b/boa_gc/src/internals/gc_box.rs index e5e5dd1b92e..61e61f19195 100644 --- a/boa_gc/src/internals/gc_box.rs +++ b/boa_gc/src/internals/gc_box.rs @@ -85,9 +85,9 @@ impl fmt::Debug for GcBoxHeader { /// A garbage collected allocation. #[derive(Debug)] -pub struct GcBox { +pub struct GcBox { pub(crate) header: GcBoxHeader, - value: T, + pub(crate) value: T, } impl GcBox { diff --git a/boa_gc/src/internals/mod.rs b/boa_gc/src/internals/mod.rs index 964b526e84e..919531a706a 100644 --- a/boa_gc/src/internals/mod.rs +++ b/boa_gc/src/internals/mod.rs @@ -3,6 +3,7 @@ mod gc_box; mod weak_map_box; pub(crate) use self::ephemeron_box::{EphemeronBox, ErasedEphemeronBox}; +pub(crate) use self::gc_box::GcBoxHeader; pub(crate) use self::weak_map_box::{ErasedWeakMapBox, WeakMapBox}; pub use self::gc_box::GcBox; diff --git a/boa_gc/src/lib.rs b/boa_gc/src/lib.rs index fb6e9b53fbb..8e1e9eacb8c 100644 --- a/boa_gc/src/lib.rs +++ b/boa_gc/src/lib.rs @@ -78,12 +78,7 @@ pub(crate) mod internals; use boa_profiler::Profiler; use internals::{EphemeronBox, ErasedEphemeronBox, ErasedWeakMapBox, WeakMapBox}; -use std::{ - cell::{Cell, RefCell}, - collections::HashMap, - mem, - ptr::NonNull, -}; +use std::{cell::Cell, collections::HashMap, mem, ptr::NonNull}; pub use crate::trace::{Finalize, Trace}; pub use boa_macros::{Finalize, Trace}; @@ -96,36 +91,27 @@ type EphemeronPointer = NonNull; type ErasedWeakMapBoxPointer = NonNull; thread_local!(static GC_DROPPING: Cell = Cell::new(false)); -thread_local!(static BOA_GC: RefCell = RefCell::new( BoaGc { - config: GcConfig::default(), +thread_local!(static BOA_GC: BoaGc = BoaGc { + config: GcConfig { + threshold: Cell::new(1024), + used_space_percentage: Cell::new(80) + }, runtime: GcRuntimeData::default(), strong_start: Cell::new(None), weak_start: Cell::new(None), weak_map_start: Cell::new(None), -})); +}); -#[derive(Debug, Clone, Copy)] +#[derive(Debug, Clone)] struct GcConfig { - threshold: usize, - used_space_percentage: usize, + threshold: Cell, + used_space_percentage: Cell, } -// Setting the defaults to an arbitrary value currently. -// -// TODO: Add a configure later -impl Default for GcConfig { - fn default() -> Self { - Self { - threshold: 1024, - used_space_percentage: 80, - } - } -} - -#[derive(Default, Debug, Clone, Copy)] +#[derive(Default, Debug, Clone)] struct GcRuntimeData { - collections: usize, - bytes_allocated: usize, + collections: Cell, + bytes_allocated: Cell, } #[derive(Debug)] @@ -182,17 +168,17 @@ impl Allocator { fn alloc_gc(value: GcBox) -> NonNull> { let _timer = Profiler::global().start_event("New GcBox", "BoaAlloc"); let element_size = mem::size_of_val::>(&value); - BOA_GC.with(|st| { - let mut gc = st.borrow_mut(); - - Self::manage_state(&mut gc); + BOA_GC.with(|gc| { + Self::manage_state(gc); value.header.next.set(gc.strong_start.take()); // Safety: value cannot be a null pointer, since `Box` cannot return null pointers. let ptr = unsafe { NonNull::new_unchecked(Box::into_raw(Box::new(value))) }; let erased: NonNull> = ptr; gc.strong_start.set(Some(erased)); - gc.runtime.bytes_allocated += element_size; + gc.runtime + .bytes_allocated + .set(gc.runtime.bytes_allocated.get() + element_size); ptr }) @@ -203,18 +189,17 @@ impl Allocator { ) -> NonNull> { let _timer = Profiler::global().start_event("New EphemeronBox", "BoaAlloc"); let element_size = mem::size_of_val::>(&value); - BOA_GC.with(|st| { - let mut gc = st.borrow_mut(); - - Self::manage_state(&mut gc); + BOA_GC.with(|gc| { + Self::manage_state(&gc); value.header.next.set(gc.weak_start.take()); // Safety: value cannot be a null pointer, since `Box` cannot return null pointers. let ptr = unsafe { NonNull::new_unchecked(Box::into_raw(Box::new(value))) }; let erased: NonNull = ptr; gc.weak_start.set(Some(erased)); - gc.runtime.bytes_allocated += element_size; - + gc.runtime + .bytes_allocated + .set(gc.runtime.bytes_allocated.get() + element_size); ptr }) } @@ -227,9 +212,7 @@ impl Allocator { }; let weak = WeakGc::new(&weak_map.inner); - BOA_GC.with(|st| { - let gc = st.borrow_mut(); - + BOA_GC.with(|gc| { let weak_box = WeakMapBox { map: weak, next: Cell::new(gc.weak_map_start.take()), @@ -245,15 +228,18 @@ impl Allocator { }) } - fn manage_state(gc: &mut BoaGc) { - if gc.runtime.bytes_allocated > gc.config.threshold { + fn manage_state(gc: &BoaGc) { + let bytes_allocated = gc.runtime.bytes_allocated.get(); + let threshold = gc.config.threshold.get(); + let used_space_percentage = gc.config.used_space_percentage.get(); + + if bytes_allocated > threshold { Collector::collect(gc); - if gc.runtime.bytes_allocated - > gc.config.threshold / 100 * gc.config.used_space_percentage - { - gc.config.threshold = - gc.runtime.bytes_allocated / gc.config.used_space_percentage * 100; + if bytes_allocated > threshold / 100 * used_space_percentage { + gc.config + .threshold + .set(bytes_allocated / used_space_percentage * 100); } } } @@ -280,9 +266,9 @@ struct Collector; impl Collector { /// Run a collection on the full heap. - fn collect(gc: &mut BoaGc) { + fn collect(gc: &BoaGc) { let _timer = Profiler::global().start_event("Gc Full Collection", "gc"); - gc.runtime.collections += 1; + gc.runtime.collections.set(gc.runtime.collections.get() + 1); let unreachables = Self::mark_heap(&gc.strong_start, &gc.weak_start, &gc.weak_map_start); // Only finalize if there are any unreachable nodes. @@ -300,7 +286,7 @@ impl Collector { Self::sweep( &gc.strong_start, &gc.weak_start, - &mut gc.runtime.bytes_allocated, + &gc.runtime.bytes_allocated, ); } @@ -450,7 +436,7 @@ impl Collector { unsafe fn sweep( mut strong: &Cell>>>, mut weak: &Cell>>, - total_allocated: &mut usize, + total_allocated: &Cell, ) { let _timer = Profiler::global().start_event("Gc Sweeping", "gc"); let _guard = DropGuard::new(); @@ -466,7 +452,7 @@ impl Collector { // The caller must ensure all pointers were allocated by `Box::into_raw(Box::new(..))`. let unmarked_node = unsafe { Box::from_raw(node.as_ptr()) }; let unallocated_bytes = mem::size_of_val(&*unmarked_node); - *total_allocated -= unallocated_bytes; + total_allocated.set(total_allocated.get() - unallocated_bytes); strong.set(unmarked_node.header.next.take()); } } @@ -483,7 +469,7 @@ impl Collector { // The caller must ensure all pointers were allocated by `Box::into_raw(Box::new(..))`. let unmarked_eph = unsafe { Box::from_raw(eph.as_ptr()) }; let unallocated_bytes = mem::size_of_val(&*unmarked_eph); - *total_allocated -= unallocated_bytes; + total_allocated.set(total_allocated.get() - unallocated_bytes); weak.set(unmarked_eph.header().next.take()); } } @@ -527,11 +513,9 @@ impl Collector { /// Forcefully runs a garbage collection of all unaccessible nodes. pub fn force_collect() { - BOA_GC.with(|current| { - let mut gc = current.borrow_mut(); - - if gc.runtime.bytes_allocated > 0 { - Collector::collect(&mut gc); + BOA_GC.with(|gc| { + if gc.runtime.bytes_allocated.get() > 0 { + Collector::collect(&gc); } }); } @@ -543,9 +527,5 @@ mod test; #[cfg(test)] #[must_use] pub fn has_weak_maps() -> bool { - BOA_GC.with(|current| { - let gc = current.borrow(); - - gc.weak_map_start.get().is_some() - }) + BOA_GC.with(|gc| gc.weak_map_start.get().is_some()) } diff --git a/boa_gc/src/pointers/ephemeron.rs b/boa_gc/src/pointers/ephemeron.rs index c63da65fd72..c3c45c10aa0 100644 --- a/boa_gc/src/pointers/ephemeron.rs +++ b/boa_gc/src/pointers/ephemeron.rs @@ -60,6 +60,17 @@ impl Ephemeron { EphemeronBox::ptr_eq(this.inner(), other.inner()) } + /// Creates a new `Ephemeron` that has no data + pub(crate) fn new_empty() -> Self { + unsafe { + Self { + inner_ptr: Cell::new(Rootable::new_unchecked(Allocator::alloc_ephemeron( + EphemeronBox::new_empty(), + ))), + } + } + } + fn is_rooted(&self) -> bool { self.inner_ptr.get().is_rooted() } @@ -77,7 +88,7 @@ impl Ephemeron { self.inner_ptr.get().as_ptr() } - fn inner(&self) -> &EphemeronBox { + pub(crate) fn inner(&self) -> &EphemeronBox { // SAFETY: Please see Gc::inner_ptr() unsafe { self.inner_ptr().as_ref() } } diff --git a/boa_gc/src/pointers/gc.rs b/boa_gc/src/pointers/gc.rs index 61327dfb580..a80ed8e24a8 100644 --- a/boa_gc/src/pointers/gc.rs +++ b/boa_gc/src/pointers/gc.rs @@ -1,8 +1,8 @@ use crate::{ finalizer_safe, - internals::GcBox, + internals::{GcBox, GcBoxHeader}, trace::{Finalize, Trace}, - Allocator, + Allocator, Ephemeron, WeakGc, BOA_GC, }; use std::{ cell::Cell, @@ -10,8 +10,9 @@ use std::{ fmt::{self, Debug, Display}, hash::{Hash, Hasher}, marker::PhantomData, + mem, ops::Deref, - ptr::NonNull, + ptr::{self, NonNull}, rc::Rc, }; @@ -30,6 +31,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() } @@ -42,6 +44,69 @@ impl Gc { } } + /// Constructs a new `Gc` while giving you a `WeakGc` to the allocation, + /// to allow you to construct a `T` which holds a weak pointer to itself. + /// + /// Since the new `Gc` is not fully-constructed until `Gc::new_cyclic` + /// returns, calling [`WeakGc::upgrade`] on the weak reference inside the closure will + /// fail and result in a `None` value. + pub fn new_cyclic(data_fn: F) -> Gc + where + F: FnOnce(&WeakGc) -> T, + { + // Create GcBox and allocate it to heap. + let inner_ptr = BOA_GC.with(|gc| { + Allocator::manage_state(&gc); + + let header = GcBoxHeader::new(); + + header.next.set(gc.strong_start.take()); + + // Safety: value cannot be a null pointer, since `Box` cannot return null pointers. + unsafe { + NonNull::new_unchecked(Box::into_raw(Box::new(GcBox { + header, + value: mem::MaybeUninit::::uninit(), + }))) + } + }); + + let init_ptr: NonNull> = inner_ptr.cast(); + + let weak = WeakGc::from(Ephemeron::new_empty()); + + let data = data_fn(&weak); + + // SAFETY: `inner_ptr` has been allocated above, so making writes to `init_ptr` is safe. + let strong = unsafe { + let inner = init_ptr.as_ptr(); + ptr::write(ptr::addr_of_mut!((*inner).value), data); + + // `strong` must be a valid value that implements [`Trace`]. + (*inner).value.unroot(); + + // `init_ptr` is initialized and its contents are unrooted, making this operation safe. + Self::from_raw(init_ptr) + }; + + BOA_GC.with(|gc| { + let erased: NonNull> = init_ptr; + + gc.strong_start.set(Some(erased)); + gc.runtime + .bytes_allocated + .set(gc.runtime.bytes_allocated.get() + mem::size_of::>()); + }); + + // SAFETY: `init_ptr` is initialized and its contents are unrooted, making this operation + // safe. + unsafe { + weak.inner().inner().init(&strong, Self::from_raw(init_ptr)); + } + + strong + } + /// Consumes the `Gc`, returning a wrapped raw pointer. /// /// To avoid a memory leak, the pointer must be converted back to a `Gc` using [`Gc::from_raw`]. diff --git a/boa_gc/src/pointers/weak.rs b/boa_gc/src/pointers/weak.rs index edeadd7add3..939a6827d2d 100644 --- a/boa_gc/src/pointers/weak.rs +++ b/boa_gc/src/pointers/weak.rs @@ -23,6 +23,11 @@ impl WeakGc { pub fn upgrade(&self) -> Option> { self.inner.value() } + + /// Gets the inner ephemeron of the weak gc + pub(crate) fn inner(&self) -> &Ephemeron> { + &self.inner + } } impl Clone for WeakGc { diff --git a/boa_gc/src/test/mod.rs b/boa_gc/src/test/mod.rs index fbe6f7939b2..4ff372c6359 100644 --- a/boa_gc/src/test/mod.rs +++ b/boa_gc/src/test/mod.rs @@ -9,32 +9,27 @@ struct Harness; impl Harness { fn assert_collections(o: usize) { - BOA_GC.with(|current| { - let gc = current.borrow(); - assert_eq!(gc.runtime.collections, o); + BOA_GC.with(|gc| { + assert_eq!(gc.runtime.collections.get(), o); }); } fn assert_empty_gc() { - BOA_GC.with(|current| { - let gc = current.borrow(); - + BOA_GC.with(|gc| { assert!(gc.strong_start.get().is_none()); - assert!(gc.runtime.bytes_allocated == 0); + assert_eq!(gc.runtime.bytes_allocated.get(), 0); }); } fn assert_bytes_allocated() { - BOA_GC.with(|current| { - let gc = current.borrow(); - assert!(gc.runtime.bytes_allocated > 0); + BOA_GC.with(|gc| { + assert!(gc.runtime.bytes_allocated.get() > 0); }); } fn assert_exact_bytes_allocated(bytes: usize) { - BOA_GC.with(|current| { - let gc = current.borrow(); - assert_eq!(gc.runtime.bytes_allocated, bytes); + BOA_GC.with(|gc| { + assert_eq!(gc.runtime.bytes_allocated.get(), bytes); }); } } From 3c81871575fcbfc07bba3517f5904706758f7293 Mon Sep 17 00:00:00 2001 From: jedel1043 Date: Mon, 15 May 2023 15:40:26 -0600 Subject: [PATCH 10/18] Document more steps on the example --- boa_ast/src/declaration/export.rs | 4 ++-- boa_examples/src/bin/modules.rs | 11 +++++++++++ 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/boa_ast/src/declaration/export.rs b/boa_ast/src/declaration/export.rs index 94b0dad0916..89c4bea40b4 100644 --- a/boa_ast/src/declaration/export.rs +++ b/boa_ast/src/declaration/export.rs @@ -264,7 +264,7 @@ pub struct LocalExportEntry { } impl LocalExportEntry { - /// Creates a new `OrdinaryExportEntry`. + /// Creates a new `LocalExportEntry`. #[must_use] pub const fn new(local_name: Identifier, export_name: Sym) -> Self { Self { @@ -295,7 +295,7 @@ pub struct IndirectExportEntry { } impl IndirectExportEntry { - /// Creates a new `ReExportEntry`. + /// Creates a new `IndirectExportEntry`. #[must_use] pub const fn new( module_request: Sym, diff --git a/boa_examples/src/bin/modules.rs b/boa_examples/src/bin/modules.rs index bab89cd8dea..f1ea129e80c 100644 --- a/boa_examples/src/bin/modules.rs +++ b/boa_examples/src/bin/modules.rs @@ -50,6 +50,9 @@ fn main() -> Result<(), Box> { // // 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( @@ -57,6 +60,10 @@ fn main() -> Result<(), Box> { 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()) }, @@ -73,6 +80,10 @@ fn main() -> Result<(), Box> { 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(), ), From e1a729b706cdfc1aba1568eaa584e2c9c768819f Mon Sep 17 00:00:00 2001 From: jedel1043 Date: Mon, 15 May 2023 15:52:16 -0600 Subject: [PATCH 11/18] cargo clippy --- boa_engine/src/module/source.rs | 3 ++- boa_gc/src/lib.rs | 4 ++-- boa_gc/src/pointers/ephemeron.rs | 2 ++ boa_gc/src/pointers/gc.rs | 4 ++-- boa_gc/src/pointers/weak.rs | 2 +- 5 files changed, 9 insertions(+), 6 deletions(-) diff --git a/boa_engine/src/module/source.rs b/boa_engine/src/module/source.rs index cb7d07ee3f3..1e480f9e107 100644 --- a/boa_engine/src/module/source.rs +++ b/boa_engine/src/module/source.rs @@ -1351,7 +1351,6 @@ impl SourceTextModule { /// /// [spec]: https://tc39.es/ecma262/#sec-source-text-module-record-initialize-environment fn initialize_environment(&self, context: &mut Context<'_>) -> JsResult<()> { - let parent = self.parent(); #[derive(Debug)] enum ImportBinding { Namespace { @@ -1364,6 +1363,8 @@ impl SourceTextModule { }, } + let parent = self.parent(); + { let src = self.borrow(); // 1. For each ExportEntry Record e of module.[[IndirectExportEntries]], do diff --git a/boa_gc/src/lib.rs b/boa_gc/src/lib.rs index 8e1e9eacb8c..4f10670fa40 100644 --- a/boa_gc/src/lib.rs +++ b/boa_gc/src/lib.rs @@ -190,7 +190,7 @@ impl Allocator { let _timer = Profiler::global().start_event("New EphemeronBox", "BoaAlloc"); let element_size = mem::size_of_val::>(&value); BOA_GC.with(|gc| { - Self::manage_state(&gc); + Self::manage_state(gc); value.header.next.set(gc.weak_start.take()); // Safety: value cannot be a null pointer, since `Box` cannot return null pointers. let ptr = unsafe { NonNull::new_unchecked(Box::into_raw(Box::new(value))) }; @@ -515,7 +515,7 @@ impl Collector { pub fn force_collect() { BOA_GC.with(|gc| { if gc.runtime.bytes_allocated.get() > 0 { - Collector::collect(&gc); + Collector::collect(gc); } }); } diff --git a/boa_gc/src/pointers/ephemeron.rs b/boa_gc/src/pointers/ephemeron.rs index c3c45c10aa0..8015077979e 100644 --- a/boa_gc/src/pointers/ephemeron.rs +++ b/boa_gc/src/pointers/ephemeron.rs @@ -62,6 +62,8 @@ impl Ephemeron { /// Creates a new `Ephemeron` that has no data pub(crate) fn new_empty() -> Self { + // SAFETY: `Allocator` can only return non-null pointers, making this + // safe. unsafe { Self { inner_ptr: Cell::new(Rootable::new_unchecked(Allocator::alloc_ephemeron( diff --git a/boa_gc/src/pointers/gc.rs b/boa_gc/src/pointers/gc.rs index a80ed8e24a8..9dda7b5b1e2 100644 --- a/boa_gc/src/pointers/gc.rs +++ b/boa_gc/src/pointers/gc.rs @@ -50,13 +50,13 @@ impl Gc { /// Since the new `Gc` is not fully-constructed until `Gc::new_cyclic` /// returns, calling [`WeakGc::upgrade`] on the weak reference inside the closure will /// fail and result in a `None` value. - pub fn new_cyclic(data_fn: F) -> Gc + pub fn new_cyclic(data_fn: F) -> Self where F: FnOnce(&WeakGc) -> T, { // Create GcBox and allocate it to heap. let inner_ptr = BOA_GC.with(|gc| { - Allocator::manage_state(&gc); + Allocator::manage_state(gc); let header = GcBoxHeader::new(); diff --git a/boa_gc/src/pointers/weak.rs b/boa_gc/src/pointers/weak.rs index 939a6827d2d..c6cbdfb06f3 100644 --- a/boa_gc/src/pointers/weak.rs +++ b/boa_gc/src/pointers/weak.rs @@ -25,7 +25,7 @@ impl WeakGc { } /// Gets the inner ephemeron of the weak gc - pub(crate) fn inner(&self) -> &Ephemeron> { + pub(crate) const fn inner(&self) -> &Ephemeron> { &self.inner } } From 0b6b082667a7bf0dae9535237b1bfdd4d88a5963 Mon Sep 17 00:00:00 2001 From: jedel1043 Date: Mon, 15 May 2023 18:27:48 -0600 Subject: [PATCH 12/18] Revert `BoaGc` changes --- boa_gc/src/lib.rs | 108 ++++++++++++++++++++++---------------- boa_gc/src/pointers/gc.rs | 13 ++--- boa_gc/src/test/mod.rs | 21 +++++--- 3 files changed, 84 insertions(+), 58 deletions(-) diff --git a/boa_gc/src/lib.rs b/boa_gc/src/lib.rs index 4f10670fa40..fb6e9b53fbb 100644 --- a/boa_gc/src/lib.rs +++ b/boa_gc/src/lib.rs @@ -78,7 +78,12 @@ pub(crate) mod internals; use boa_profiler::Profiler; use internals::{EphemeronBox, ErasedEphemeronBox, ErasedWeakMapBox, WeakMapBox}; -use std::{cell::Cell, collections::HashMap, mem, ptr::NonNull}; +use std::{ + cell::{Cell, RefCell}, + collections::HashMap, + mem, + ptr::NonNull, +}; pub use crate::trace::{Finalize, Trace}; pub use boa_macros::{Finalize, Trace}; @@ -91,27 +96,36 @@ type EphemeronPointer = NonNull; type ErasedWeakMapBoxPointer = NonNull; thread_local!(static GC_DROPPING: Cell = Cell::new(false)); -thread_local!(static BOA_GC: BoaGc = BoaGc { - config: GcConfig { - threshold: Cell::new(1024), - used_space_percentage: Cell::new(80) - }, +thread_local!(static BOA_GC: RefCell = RefCell::new( BoaGc { + config: GcConfig::default(), runtime: GcRuntimeData::default(), strong_start: Cell::new(None), weak_start: Cell::new(None), weak_map_start: Cell::new(None), -}); +})); -#[derive(Debug, Clone)] +#[derive(Debug, Clone, Copy)] struct GcConfig { - threshold: Cell, - used_space_percentage: Cell, + threshold: usize, + used_space_percentage: usize, } -#[derive(Default, Debug, Clone)] +// Setting the defaults to an arbitrary value currently. +// +// TODO: Add a configure later +impl Default for GcConfig { + fn default() -> Self { + Self { + threshold: 1024, + used_space_percentage: 80, + } + } +} + +#[derive(Default, Debug, Clone, Copy)] struct GcRuntimeData { - collections: Cell, - bytes_allocated: Cell, + collections: usize, + bytes_allocated: usize, } #[derive(Debug)] @@ -168,17 +182,17 @@ impl Allocator { fn alloc_gc(value: GcBox) -> NonNull> { let _timer = Profiler::global().start_event("New GcBox", "BoaAlloc"); let element_size = mem::size_of_val::>(&value); - BOA_GC.with(|gc| { - Self::manage_state(gc); + BOA_GC.with(|st| { + let mut gc = st.borrow_mut(); + + Self::manage_state(&mut gc); value.header.next.set(gc.strong_start.take()); // Safety: value cannot be a null pointer, since `Box` cannot return null pointers. let ptr = unsafe { NonNull::new_unchecked(Box::into_raw(Box::new(value))) }; let erased: NonNull> = ptr; gc.strong_start.set(Some(erased)); - gc.runtime - .bytes_allocated - .set(gc.runtime.bytes_allocated.get() + element_size); + gc.runtime.bytes_allocated += element_size; ptr }) @@ -189,17 +203,18 @@ impl Allocator { ) -> NonNull> { let _timer = Profiler::global().start_event("New EphemeronBox", "BoaAlloc"); let element_size = mem::size_of_val::>(&value); - BOA_GC.with(|gc| { - Self::manage_state(gc); + BOA_GC.with(|st| { + let mut gc = st.borrow_mut(); + + Self::manage_state(&mut gc); value.header.next.set(gc.weak_start.take()); // Safety: value cannot be a null pointer, since `Box` cannot return null pointers. let ptr = unsafe { NonNull::new_unchecked(Box::into_raw(Box::new(value))) }; let erased: NonNull = ptr; gc.weak_start.set(Some(erased)); - gc.runtime - .bytes_allocated - .set(gc.runtime.bytes_allocated.get() + element_size); + gc.runtime.bytes_allocated += element_size; + ptr }) } @@ -212,7 +227,9 @@ impl Allocator { }; let weak = WeakGc::new(&weak_map.inner); - BOA_GC.with(|gc| { + BOA_GC.with(|st| { + let gc = st.borrow_mut(); + let weak_box = WeakMapBox { map: weak, next: Cell::new(gc.weak_map_start.take()), @@ -228,18 +245,15 @@ impl Allocator { }) } - fn manage_state(gc: &BoaGc) { - let bytes_allocated = gc.runtime.bytes_allocated.get(); - let threshold = gc.config.threshold.get(); - let used_space_percentage = gc.config.used_space_percentage.get(); - - if bytes_allocated > threshold { + fn manage_state(gc: &mut BoaGc) { + if gc.runtime.bytes_allocated > gc.config.threshold { Collector::collect(gc); - if bytes_allocated > threshold / 100 * used_space_percentage { - gc.config - .threshold - .set(bytes_allocated / used_space_percentage * 100); + if gc.runtime.bytes_allocated + > gc.config.threshold / 100 * gc.config.used_space_percentage + { + gc.config.threshold = + gc.runtime.bytes_allocated / gc.config.used_space_percentage * 100; } } } @@ -266,9 +280,9 @@ struct Collector; impl Collector { /// Run a collection on the full heap. - fn collect(gc: &BoaGc) { + fn collect(gc: &mut BoaGc) { let _timer = Profiler::global().start_event("Gc Full Collection", "gc"); - gc.runtime.collections.set(gc.runtime.collections.get() + 1); + gc.runtime.collections += 1; let unreachables = Self::mark_heap(&gc.strong_start, &gc.weak_start, &gc.weak_map_start); // Only finalize if there are any unreachable nodes. @@ -286,7 +300,7 @@ impl Collector { Self::sweep( &gc.strong_start, &gc.weak_start, - &gc.runtime.bytes_allocated, + &mut gc.runtime.bytes_allocated, ); } @@ -436,7 +450,7 @@ impl Collector { unsafe fn sweep( mut strong: &Cell>>>, mut weak: &Cell>>, - total_allocated: &Cell, + total_allocated: &mut usize, ) { let _timer = Profiler::global().start_event("Gc Sweeping", "gc"); let _guard = DropGuard::new(); @@ -452,7 +466,7 @@ impl Collector { // The caller must ensure all pointers were allocated by `Box::into_raw(Box::new(..))`. let unmarked_node = unsafe { Box::from_raw(node.as_ptr()) }; let unallocated_bytes = mem::size_of_val(&*unmarked_node); - total_allocated.set(total_allocated.get() - unallocated_bytes); + *total_allocated -= unallocated_bytes; strong.set(unmarked_node.header.next.take()); } } @@ -469,7 +483,7 @@ impl Collector { // The caller must ensure all pointers were allocated by `Box::into_raw(Box::new(..))`. let unmarked_eph = unsafe { Box::from_raw(eph.as_ptr()) }; let unallocated_bytes = mem::size_of_val(&*unmarked_eph); - total_allocated.set(total_allocated.get() - unallocated_bytes); + *total_allocated -= unallocated_bytes; weak.set(unmarked_eph.header().next.take()); } } @@ -513,9 +527,11 @@ impl Collector { /// Forcefully runs a garbage collection of all unaccessible nodes. pub fn force_collect() { - BOA_GC.with(|gc| { - if gc.runtime.bytes_allocated.get() > 0 { - Collector::collect(gc); + BOA_GC.with(|current| { + let mut gc = current.borrow_mut(); + + if gc.runtime.bytes_allocated > 0 { + Collector::collect(&mut gc); } }); } @@ -527,5 +543,9 @@ mod test; #[cfg(test)] #[must_use] pub fn has_weak_maps() -> bool { - BOA_GC.with(|gc| gc.weak_map_start.get().is_some()) + BOA_GC.with(|current| { + let gc = current.borrow(); + + gc.weak_map_start.get().is_some() + }) } diff --git a/boa_gc/src/pointers/gc.rs b/boa_gc/src/pointers/gc.rs index 9dda7b5b1e2..915ff653d1f 100644 --- a/boa_gc/src/pointers/gc.rs +++ b/boa_gc/src/pointers/gc.rs @@ -55,8 +55,10 @@ impl Gc { F: FnOnce(&WeakGc) -> T, { // Create GcBox and allocate it to heap. - let inner_ptr = BOA_GC.with(|gc| { - Allocator::manage_state(gc); + let inner_ptr = BOA_GC.with(|st| { + let mut gc = st.borrow_mut(); + + Allocator::manage_state(&mut gc); let header = GcBoxHeader::new(); @@ -89,13 +91,12 @@ impl Gc { Self::from_raw(init_ptr) }; - BOA_GC.with(|gc| { + BOA_GC.with(|st| { + let mut gc = st.borrow_mut(); let erased: NonNull> = init_ptr; gc.strong_start.set(Some(erased)); - gc.runtime - .bytes_allocated - .set(gc.runtime.bytes_allocated.get() + mem::size_of::>()); + gc.runtime.bytes_allocated += mem::size_of::>(); }); // SAFETY: `init_ptr` is initialized and its contents are unrooted, making this operation diff --git a/boa_gc/src/test/mod.rs b/boa_gc/src/test/mod.rs index 4ff372c6359..fbe6f7939b2 100644 --- a/boa_gc/src/test/mod.rs +++ b/boa_gc/src/test/mod.rs @@ -9,27 +9,32 @@ struct Harness; impl Harness { fn assert_collections(o: usize) { - BOA_GC.with(|gc| { - assert_eq!(gc.runtime.collections.get(), o); + BOA_GC.with(|current| { + let gc = current.borrow(); + assert_eq!(gc.runtime.collections, o); }); } fn assert_empty_gc() { - BOA_GC.with(|gc| { + BOA_GC.with(|current| { + let gc = current.borrow(); + assert!(gc.strong_start.get().is_none()); - assert_eq!(gc.runtime.bytes_allocated.get(), 0); + assert!(gc.runtime.bytes_allocated == 0); }); } fn assert_bytes_allocated() { - BOA_GC.with(|gc| { - assert!(gc.runtime.bytes_allocated.get() > 0); + BOA_GC.with(|current| { + let gc = current.borrow(); + assert!(gc.runtime.bytes_allocated > 0); }); } fn assert_exact_bytes_allocated(bytes: usize) { - BOA_GC.with(|gc| { - assert_eq!(gc.runtime.bytes_allocated.get(), bytes); + BOA_GC.with(|current| { + let gc = current.borrow(); + assert_eq!(gc.runtime.bytes_allocated, bytes); }); } } From e62d2aaad0e56bf714e511663f041bd5600d5a99 Mon Sep 17 00:00:00 2001 From: jedel1043 Date: Mon, 15 May 2023 21:23:22 -0600 Subject: [PATCH 13/18] Lower `GcRefCell` to `Inner` of `SourceTextModule` --- Cargo.lock | 1 + boa_engine/src/module/source.rs | 431 ++++++++++++++++---------------- boa_interner/Cargo.toml | 1 + boa_interner/src/sym.rs | 7 +- 4 files changed, 219 insertions(+), 221 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 6f8ba2d27ee..4aec6623a38 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_engine/src/module/source.rs b/boa_engine/src/module/source.rs index 1e480f9e107..e3f1aa7f336 100644 --- a/boa_engine/src/module/source.rs +++ b/boa_engine/src/module/source.rs @@ -1,9 +1,4 @@ -use std::{ - cell::Cell, - collections::{HashMap, HashSet}, - hash::Hash, - rc::Rc, -}; +use std::{cell::Cell, collections::HashSet, hash::Hash, rc::Rc}; use boa_ast::{ declaration::{ @@ -16,7 +11,7 @@ use boa_ast::{ }, Declaration, ModuleItemList, }; -use boa_gc::{custom_trace, empty_trace, Finalize, Gc, GcRef, GcRefCell, GcRefMut, Trace, WeakGc}; +use boa_gc::{custom_trace, empty_trace, Finalize, Gc, GcRefCell, Trace, WeakGc}; use boa_interner::Sym; use rustc_hash::{FxHashMap, FxHashSet}; @@ -236,72 +231,64 @@ unsafe impl Trace for SourceTextContext { /// /// [spec]: https://tc39.es/ecma262/#sec-source-text-module-records #[derive(Clone, Trace, Finalize)] -pub(crate) struct SourceTextModule(Gc>); +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.0); + let limiter = RecursionLimiter::new(&*self.inner); if !limiter.visited && !limiter.live { - f.debug_tuple("SourceTextModule").field(&self.0).finish() + f.debug_struct("SourceTextModule") + .field("code", &self.inner.code) + .field("status", &self.inner.status) + .field("requested_modules", &self.inner.requested_modules) + .field("loaded_modules", &self.inner.loaded_modules) + .field("has_tla", &self.inner.has_tla) + .field("async_parent_modules", &self.inner.async_parent_modules) + .field("import_meta", &self.inner.import_meta) + .field("import_entries", &self.inner.import_entries) + .field("local_export_entries", &self.inner.local_export_entries) + .field( + "indirect_export_entries", + &self.inner.indirect_export_entries, + ) + .field("star_export_entries", &self.inner.star_export_entries) + .finish() } else { - f.debug_tuple("SourceTextModule").field(&"").finish() + f.write_str("{ ... }") } } } -#[derive(Finalize)] +#[derive(Trace, Finalize)] struct Inner { parent: WeakGc, - code: ModuleItemList, - status: Status, + status: GcRefCell, + loaded_modules: GcRefCell>, + async_parent_modules: GcRefCell>, + import_meta: GcRefCell>, requested_modules: FxHashSet, - loaded_modules: FxHashMap, has_tla: bool, - async_parent_modules: Vec, - import_meta: Option, + #[unsafe_ignore_trace] + code: ModuleItemList, + #[unsafe_ignore_trace] import_entries: Vec, + #[unsafe_ignore_trace] local_export_entries: Vec, + #[unsafe_ignore_trace] indirect_export_entries: Vec, + #[unsafe_ignore_trace] star_export_entries: Vec, } -impl std::fmt::Debug for Inner { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.debug_struct("SourceTextModuleData") - .field("code", &"ModuleItemList") - .field("status", &self.status) - .field("requested_modules", &self.requested_modules) - .field("loaded_modules", &self.loaded_modules) - .field("has_tla", &self.has_tla) - .field("async_parent_modules", &self.async_parent_modules) - .field("import_meta", &self.import_meta) - .field("import_entries", &self.import_entries) - .field("local_export_entries", &self.local_export_entries) - .field("indirect_export_entries", &self.indirect_export_entries) - .field("star_export_entries", &self.star_export_entries) - .finish() - } -} - -unsafe impl Trace for Inner { - custom_trace!(this, { - mark(&this.parent); - mark(&this.status); - for module in this.loaded_modules.values() { - mark(module); - } - mark(&this.async_parent_modules); - mark(&this.import_meta); - }); -} - impl SourceTextModule { /// Gets the parent module of this source module. fn parent(&self) -> Module { Module { inner: self - .borrow() + .inner .parent .upgrade() .expect("source text cannot be dropped before parent module"), @@ -392,20 +379,22 @@ impl SourceTextModule { // [[DFSIndex]]: empty, [[DFSAncestorIndex]]: empty // }. // Most of this can be ignored, since `Status` takes care of the remaining state. - SourceTextModule(Gc::new(GcRefCell::new(Inner { - parent, - code, - requested_modules, - has_tla, - import_entries, - local_export_entries, - indirect_export_entries, - star_export_entries, - status: Status::Unlinked, - loaded_modules: HashMap::default(), - async_parent_modules: Vec::default(), - import_meta: None, - }))) + Self { + inner: Gc::new(Inner { + parent, + code, + requested_modules, + has_tla, + import_entries, + local_export_entries, + indirect_export_entries, + star_export_entries, + status: GcRefCell::default(), + loaded_modules: GcRefCell::default(), + async_parent_modules: GcRefCell::default(), + import_meta: GcRefCell::default(), + }), + } } /// Abstract operation [`InnerModuleLoading`][spec]. @@ -415,19 +404,19 @@ impl SourceTextModule { // 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.borrow().status, Status::Unlinked) + 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.borrow().requested_modules.clone(); + let requested = &self.inner.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 { + for &required in requested { // i. If module.[[LoadedModules]] contains a Record whose [[Specifier]] is required, then - let loaded = self.borrow().loaded_modules.get(&required).cloned(); + 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]]). @@ -447,15 +436,26 @@ impl SourceTextModule { 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 { - let mut src = src.borrow_mut(); - let entry = src - .loaded_modules + // 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 @@ -522,28 +522,27 @@ impl SourceTextModule { // 4. Append module to exportStarSet. export_star_set.push(self.clone()); - let module = self.borrow(); // 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 &module.local_export_entries { + for e in &self.inner.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 &module.indirect_export_entries { + for e in &self.inner.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 &module.star_export_entries { + for e in &self.inner.star_export_entries { // a. Let requestedModule be GetImportedModule(module, e.[[ModuleRequest]]). - let requested_module = module.loaded_modules[e].clone(); + 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 @@ -583,10 +582,9 @@ impl SourceTextModule { // 4. Append the Record { [[Module]]: module, [[ExportName]]: exportName } to resolveSet. resolve_set.insert((parent.clone(), export_name)); - let src = self.borrow(); // 5. For each ExportEntry Record e of module.[[LocalExportEntries]], do - for e in &src.local_export_entries { + for e in &self.inner.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. @@ -599,17 +597,18 @@ impl SourceTextModule { } // 6. For each ExportEntry Record e of module.[[IndirectExportEntries]], do - for e in &src.indirect_export_entries { + for e in &self.inner.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 = &src.loaded_modules[&e.module_request()]; + 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.clone(), + module: imported_module, binding_name: BindingName::Namespace, }), // iii. Else, @@ -634,9 +633,9 @@ impl SourceTextModule { let mut star_resolution: Option = None; // 9. For each ExportEntry Record e of module.[[StarExportEntries]], do - for e in &src.star_export_entries { + for e in &self.inner.star_export_entries { // a. Let importedModule be GetImportedModule(module, e.[[ModuleRequest]]). - let imported_module = &src.loaded_modules[e]; + 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 @@ -688,7 +687,7 @@ impl SourceTextModule { 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.borrow().status, + &*self.inner.status.borrow(), Status::Unlinked | Status::Linked { .. } | Status::EvaluatingAsync { .. } @@ -704,19 +703,19 @@ impl SourceTextModule { // a. For each Cyclic Module Record m of stack, do for m in stack { // i. Assert: m.[[Status]] is linking. - debug_assert!(matches!(m.borrow().status, Status::Linking { .. })); + debug_assert!(matches!(&*m.inner.status.borrow(), Status::Linking { .. })); // ii. Set m.[[Status]] to unlinked. - m.borrow_mut().status = Status::Unlinked; + *m.inner.status.borrow_mut() = Status::Unlinked; } // b. Assert: module.[[Status]] is unlinked. - assert!(matches!(self.borrow().status, Status::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.borrow().status, + &*self.inner.status.borrow(), Status::Linked { .. } | Status::EvaluatingAsync { .. } | Status::Evaluated { .. } )); // 6. Assert: stack is empty. @@ -737,7 +736,7 @@ impl SourceTextModule { ) -> JsResult { // 2. If module.[[Status]] is one of linking, linked, evaluating-async, or evaluated, then if matches!( - self.borrow().status, + &*self.inner.status.borrow(), Status::Linking { .. } | Status::PreLinked { .. } | Status::Linked { .. } @@ -749,20 +748,17 @@ impl SourceTextModule { } // 3. Assert: module.[[Status]] is unlinked. - debug_assert!(matches!(self.borrow().status, Status::Unlinked)); - - { - let mut module = self.borrow_mut(); - // 4. Set module.[[Status]] to linking. - // 5. Set module.[[DFSIndex]] to index. - // 6. Set module.[[DFSAncestorIndex]] to index. - module.status = Status::Linking { - info: DfsInfo { - dfs_index: index, - dfs_ancestor_index: index, - }, - }; - } + 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; @@ -772,10 +768,9 @@ impl SourceTextModule { // 9. For each String required of module.[[RequestedModules]], do - let requested = self.borrow().requested_modules.clone(); - for required in requested { + for required in &self.inner.requested_modules { // a. Let requiredModule be GetImportedModule(module, required). - let required_module = self.borrow().loaded_modules[&required].clone(); + 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)?; @@ -783,7 +778,7 @@ impl SourceTextModule { 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.borrow().status { + debug_assert!(match &*required_module.inner.status.borrow() { Status::PreLinked { .. } | Status::Linked { .. } | Status::EvaluatingAsync { .. } @@ -798,7 +793,7 @@ impl SourceTextModule { DfsInfo { dfs_ancestor_index, .. }, - } = &required_module.borrow().status + } = &*required_module.inner.status.borrow() { // 1. Set module.[[DFSAncestorIndex]] to min(module.[[DFSAncestorIndex]], // requiredModule.[[DFSAncestorIndex]]). @@ -809,12 +804,11 @@ impl SourceTextModule { }; if let Some(required_index) = required_index { - let mut module = self.borrow_mut(); + let mut status = self.inner.status.borrow_mut(); let DfsInfo { dfs_ancestor_index, .. - } = module - .status + } = status .dfs_info_mut() .expect("should be on the linking state"); *dfs_ancestor_index = usize::min(*dfs_ancestor_index, required_index); @@ -833,15 +827,16 @@ impl SourceTextModule { dfs_ancestor_index, dfs_index, } = self - .borrow() + .inner .status + .borrow() .dfs_info() .copied() .expect("should be linking"); dfs_ancestor_index <= dfs_index }); - let info = self.borrow().status.dfs_info().copied(); + let info = self.inner.status.borrow().dfs_info().copied(); match info { // 13. If module.[[DFSAncestorIndex]] = module.[[DFSIndex]], then @@ -853,8 +848,9 @@ impl SourceTextModule { // 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.borrow_mut() + last.inner .status + .borrow_mut() .transition(|current| match current { Status::PreLinked { info, context } => Status::Linked { info, context }, _ => { @@ -882,8 +878,7 @@ impl SourceTextModule { 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) = { - let this = self.borrow(); - match &this.status { + match &*self.inner.status.borrow() { Status::Unlinked | Status::Linking { .. } | Status::PreLinked { .. } @@ -935,7 +930,7 @@ impl SourceTextModule { Ok(_) => { // 10. Else, // a. Assert: module.[[Status]] is either evaluating-async or evaluated. - assert!(match &module.borrow().status { + assert!(match &*module.inner.status.borrow() { Status::EvaluatingAsync { .. } => true, // b. Assert: module.[[EvaluationError]] is empty. Status::Evaluated { error, .. } if error.is_none() => true, @@ -943,7 +938,7 @@ impl SourceTextModule { }); // c. If module.[[AsyncEvaluation]] is false, then - if matches!(&module.borrow().status, Status::Evaluated { .. }) { + if matches!(&*module.inner.status.borrow(), Status::Evaluated { .. }) { // i. Assert: module.[[Status]] is evaluated. // ii. Perform ! Call(capability.[[Resolve]], undefined, « undefined »). capability @@ -959,7 +954,7 @@ impl SourceTextModule { Err(err) => { // a. For each Cyclic Module Record m of stack, do for m in stack { - m.borrow_mut().status.transition(|current| match current { + m.inner.status.borrow_mut().transition(|current| match current { // i. Assert: m.[[Status]] is evaluating. Status::Evaluating { top_level_capability, @@ -986,7 +981,7 @@ impl SourceTextModule { // b. Assert: module.[[Status]] is evaluated. // c. Assert: module.[[EvaluationError]] is result. assert!( - matches!(&self.borrow().status, Status::Evaluated { error, .. } if error.is_some()) + matches!(&*module.inner.status.borrow(), Status::Evaluated { error, .. } if error.is_some()) ); // d. Perform ! Call(capability.[[Reject]], undefined, « result.[[Value]] »). @@ -1033,7 +1028,7 @@ impl SourceTextModule { } // 2. If module.[[Status]] is either evaluating-async or evaluated, then - match &self.borrow_mut().status { + 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. @@ -1053,19 +1048,22 @@ impl SourceTextModule { // 6. Set module.[[DFSIndex]] to index. // 7. Set module.[[DFSAncestorIndex]] to index. // 8. Set module.[[PendingAsyncDependencies]] to 0. - self.borrow_mut().status.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, + 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, }, - async_eval_index: None, - }, - _ => unreachable!("already asserted that this state is `Linked`. "), - }); + _ => unreachable!("already asserted that this state is `Linked`. "), + }); // 9. Set index to index + 1. index += 1; @@ -1075,10 +1073,9 @@ impl SourceTextModule { stack.push(self.clone()); // 11. For each String required of module.[[RequestedModules]], do - let requested = self.borrow().requested_modules.clone(); - for required in requested { + for &required in &self.inner.requested_modules { // a. Let requiredModule be GetImportedModule(module, required). - let required_module = self.borrow().loaded_modules[&required].clone(); + 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)?; @@ -1086,13 +1083,13 @@ impl SourceTextModule { 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.borrow().status { + 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.borrow().status { + let (required_module, async_eval, req_info) = match &*required_module.inner.status.borrow() { // iii. If requiredModule.[[Status]] is evaluating, then Status::Evaluating { info, @@ -1107,7 +1104,7 @@ impl SourceTextModule { | Status::Evaluated { cycle_root, .. } => { // 1. Set requiredModule to requiredModule.[[CycleRoot]]. // 2. Assert: requiredModule.[[Status]] is either evaluating-async or evaluated. - match &cycle_root.borrow().status { + 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()), @@ -1119,9 +1116,8 @@ impl SourceTextModule { }; if let Some(req_info) = req_info { - let mut this = self.borrow_mut(); - let info = this - .status + 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 = @@ -1134,18 +1130,19 @@ impl SourceTextModule { pending_async_dependencies += 1; // 2. Append module to requiredModule.[[AsyncParentModules]]. required_module - .borrow_mut() + .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.borrow().has_tla { + if pending_async_dependencies > 0 || self.inner.has_tla { // a. Assert: module.[[AsyncEvaluation]] is false and was never previously set to true. { - let Status::Evaluating { async_eval_index, .. } = &mut self.borrow_mut().status else { + let Status::Evaluating { async_eval_index, .. } = &mut *self.inner.status.borrow_mut() else { unreachable!("self should still be in the evaluating state") }; @@ -1164,7 +1161,7 @@ impl SourceTextModule { self.execute(None, context)?; } - let dfs_info = self.borrow().status.dfs_info().copied().expect( + 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", ); @@ -1186,7 +1183,7 @@ impl SourceTextModule { let is_self = self == &required_module; // iii. Assert: requiredModule is a Cyclic Module Record. - required_module.borrow_mut().status.transition(|current| match current { + required_module.inner.status.borrow_mut().transition(|current| match current { Status::Evaluating { top_level_capability, cycle_root, @@ -1242,11 +1239,11 @@ impl SourceTextModule { fn execute_async(&self, context: &mut Context<'_>) { // 1. Assert: module.[[Status]] is either evaluating or evaluating-async. debug_assert!(matches!( - self.borrow().status, + &*self.inner.status.borrow(), Status::Evaluating { .. } | Status::EvaluatingAsync { .. } )); // 2. Assert: module.[[HasTLA]] is true. - debug_assert!(self.borrow().has_tla); + debug_assert!(self.inner.has_tla); // 3. Let capability be ! NewPromiseCapability(%Promise%). let capability = PromiseCapability::new( @@ -1309,21 +1306,19 @@ impl SourceTextModule { #[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.borrow().async_parent_modules { + for m in &*self.inner.async_parent_modules.borrow() { // a. If execList does not contain m and m.[[CycleRoot]].[[EvaluationError]] is empty, then // 2. Return unused. if !exec_list.contains(m) - && m.borrow() - .status - .cycle_root() - .map_or(false, |cr| cr.borrow().status.evaluation_error().is_none()) + && m.inner.status.borrow().cycle_root().map_or(false, |cr| { + cr.inner.status.borrow().evaluation_error().is_none() + }) { let (deps, has_tla) = { - let m = &mut m.borrow_mut(); // 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.status else { + 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. @@ -1331,7 +1326,7 @@ impl SourceTextModule { // v. Set m.[[PendingAsyncDependencies]] to m.[[PendingAsyncDependencies]] - 1. *pending_async_dependencies -= 1; - (*pending_async_dependencies, m.has_tla) + (*pending_async_dependencies, m.inner.has_tla) }; // vi. If m.[[PendingAsyncDependencies]] = 0, then @@ -1366,9 +1361,8 @@ impl SourceTextModule { let parent = self.parent(); { - let src = self.borrow(); // 1. For each ExportEntry Record e of module.[[IndirectExportEntries]], do - for e in &src.indirect_export_entries { + for e in &self.inner.indirect_export_entries { // a. Let resolution be module.ResolveExport(e.[[ExportName]]). parent .resolve_export(e.export_name(), &mut HashSet::default()) @@ -1411,10 +1405,10 @@ impl SourceTextModule { let codeblock = { // 7. For each ImportEntry Record in of module.[[ImportEntries]], do - let src = self.borrow(); - for entry in &src.import_entries { + for entry in &self.inner.import_entries { // a. Let importedModule be GetImportedModule(module, in.[[ModuleRequest]]). - let imported_module = &src.loaded_modules[&entry.module_request()]; + let imported_module = + self.inner.loaded_modules.borrow()[&entry.module_request()].clone(); if let ImportName::Name(name) = entry.import_name() { // c. Else, @@ -1454,7 +1448,7 @@ impl SourceTextModule { // deferred to initialization below imports.push(ImportBinding::Namespace { locator, - module: imported_module.clone(), + module: imported_module, }); } } else { @@ -1475,7 +1469,7 @@ impl SourceTextModule { // 18. Let code be module.[[ECMAScriptCode]]. // 19. Let varDeclarations be the VarScopedDeclarations of code. - let var_declarations = var_scoped_declarations(&src.code); + let var_declarations = var_scoped_declarations(&self.inner.code); // 20. Let declaredVarNames be a new empty List. let mut declared_var_names = Vec::new(); // 21. For each element d of varDeclarations, do @@ -1499,7 +1493,7 @@ impl SourceTextModule { // 22. Let lexDeclarations be the LexicallyScopedDeclarations of code. // 23. Let privateEnv be null. - let lex_declarations = lexically_scoped_declarations(&src.code); + let lex_declarations = lexically_scoped_declarations(&self.inner.code); // 24. For each element d of lexDeclarations, do for declaration in lex_declarations { match &declaration { @@ -1563,7 +1557,7 @@ impl SourceTextModule { } } - compiler.compile_module_item_list(&src.code); + compiler.compile_module_item_list(&self.inner.code); Gc::new(compiler.finish()) }; @@ -1632,19 +1626,22 @@ impl SourceTextModule { *parent.inner.environment.borrow_mut() = envs.current().as_declarative().cloned(); // 16. Set module.[[Context]] to moduleContext. - self.borrow_mut().status.transition(|state| match state { - Status::Linking { info } => Status::PreLinked { - info, - context: SourceTextContext { - codeblock, - environments: envs, - realm, + 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" - ), - }); + _ => unreachable!( + "should only transition to the `PreLinked` state from the `Linking` state" + ), + }); // 26. Return unused. Ok(()) @@ -1663,7 +1660,7 @@ impl SourceTextModule { codeblock, mut environments, mut realm, - } = match &self.borrow().status { + } = match &*self.inner.status.borrow() { Status::Evaluating { context, .. } | Status::EvaluatingAsync { context, .. } => { context.clone() } @@ -1712,18 +1709,6 @@ impl SourceTextModule { Ok(()) } } - - /// Borrows the inner data of the script module. - #[track_caller] - fn borrow(&self) -> GcRef<'_, Inner> { - GcRefCell::borrow(&self.0) - } - - /// Mutably borrows the inner data of the script module. - #[track_caller] - fn borrow_mut(&self) -> GcRefMut<'_, Inner> { - GcRefCell::borrow_mut(&self.0) - } } /// Abstract operation [`AsyncModuleExecutionFulfilled ( module )`][spec]. @@ -1732,7 +1717,7 @@ impl SourceTextModule { #[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.borrow().status { + if let Status::Evaluated { error, .. } = &*module.inner.status.borrow() { // a. Assert: module.[[EvaluationError]] is not empty. assert!(error.is_some()); // b. Return unused. @@ -1745,8 +1730,9 @@ fn async_module_execution_fulfilled(module: &SourceTextModule, context: &mut Con // 5. Set module.[[AsyncEvaluation]] to false. // 6. Set module.[[Status]] to evaluated. module - .borrow_mut() + .inner .status + .borrow_mut() .transition(|status| match status { Status::EvaluatingAsync { top_level_capability, @@ -1761,9 +1747,9 @@ fn async_module_execution_fulfilled(module: &SourceTextModule, context: &mut Con }); // 7. If module.[[TopLevelCapability]] is not empty, then - if let Some(cap) = module.borrow().status.top_level_capability() { + if let Some(cap) = module.inner.status.borrow().top_level_capability() { // a. Assert: module.[[CycleRoot]] is module. - debug_assert_eq!(module.borrow().status.cycle_root(), Some(module)); + debug_assert_eq!(module.inner.status.borrow().cycle_root(), Some(module)); // b. Perform ! Call(module.[[TopLevelCapability]].[[Resolve]], undefined, « undefined »). cap.resolve() @@ -1782,9 +1768,9 @@ fn async_module_execution_fulfilled(module: &SourceTextModule, context: &mut Con // 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.borrow().status else { - unreachable!("GatherAvailableAncestors: i. Assert: m.[[Status]] is evaluating-async."); - }; + let Status::EvaluatingAsync { async_eval_index, .. } = &*m.inner.status.borrow() else { + unreachable!("GatherAvailableAncestors: i. Assert: m.[[Status]] is evaluating-async."); + }; *async_eval_index }); @@ -1792,14 +1778,14 @@ fn async_module_execution_fulfilled(module: &SourceTextModule, context: &mut Con // 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.borrow().status { + 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.borrow().has_tla; + let has_tla = m.inner.has_tla; if has_tla { // i. Perform ExecuteAsyncModule(m). m.execute_async(context); @@ -1815,23 +1801,27 @@ fn async_module_execution_fulfilled(module: &SourceTextModule, context: &mut Con } else { // iii. Else, // 1. Set m.[[Status]] to evaluated. - m.borrow_mut().status.transition(|status| match status { - Status::EvaluatingAsync { - top_level_capability, - cycle_root, - .. - } => Status::Evaluated { - top_level_capability, - cycle_root, - error: None, - }, - _ => unreachable!(), - }); + 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) = m.borrow().status.top_level_capability() { + if let Some(cap) = status.top_level_capability() { // a. Assert: m.[[CycleRoot]] is m. - debug_assert_eq!(m.borrow().status.cycle_root(), Some(&m)); + debug_assert_eq!(status.cycle_root(), Some(&m)); // b. Perform ! Call(m.[[TopLevelCapability]].[[Resolve]], undefined, « undefined »). cap.resolve() @@ -1853,7 +1843,7 @@ fn async_module_execution_rejected( context: &mut Context<'_>, ) { // 1. If module.[[Status]] is evaluated, then - if let Status::Evaluated { error, .. } = &module.borrow().status { + if let Status::Evaluated { error, .. } = &*module.inner.status.borrow() { // a. Assert: module.[[EvaluationError]] is not empty. assert!(error.is_some()); // b. Return unused. @@ -1866,8 +1856,9 @@ fn async_module_execution_rejected( // 5. Set module.[[EvaluationError]] to ThrowCompletion(error). // 6. Set module.[[Status]] to evaluated. module - .borrow_mut() + .inner .status + .borrow_mut() .transition(|status| match status { Status::EvaluatingAsync { top_level_capability, @@ -1882,16 +1873,16 @@ fn async_module_execution_rejected( }); // 7. For each Cyclic Module Record m of module.[[AsyncParentModules]], do - let module_b = module.borrow(); - for m in &module_b.async_parent_modules { + 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) = module_b.status.top_level_capability() { + if let Some(cap) = status.top_level_capability() { // a. Assert: module.[[CycleRoot]] is module. - debug_assert_eq!(module_b.status.cycle_root(), Some(module)); + debug_assert_eq!(status.cycle_root(), Some(module)); // b. Perform ! Call(module.[[TopLevelCapability]].[[Reject]], undefined, « error »). cap.reject() @@ -1903,7 +1894,7 @@ fn async_module_execution_rejected( impl PartialEq for SourceTextModule { fn eq(&self, other: &Self) -> bool { - std::ptr::eq(self.0.as_ref(), other.0.as_ref()) + std::ptr::eq(self.inner.as_ref(), other.inner.as_ref()) } } @@ -1911,6 +1902,6 @@ impl Eq for SourceTextModule {} impl Hash for SourceTextModule { fn hash(&self, state: &mut H) { - std::ptr::hash(self.0.as_ref(), state); + std::ptr::hash(self.inner.as_ref(), state); } } diff --git a/boa_interner/Cargo.toml b/boa_interner/Cargo.toml index 461adf37abe..23f099b5364 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 a29db7b3dac..0920e2fe76f 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,15 @@ 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, } +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 { From 2ffab4c3a2dfb45a24012a25b5ddfe6d9628cc74 Mon Sep 17 00:00:00 2001 From: jedel1043 Date: Mon, 15 May 2023 23:02:56 -0600 Subject: [PATCH 14/18] Replace weak ref to module with strong ref --- boa_engine/src/module/mod.rs | 14 +++- boa_engine/src/module/source.rs | 104 +++++++++++++------------- boa_gc/src/internals/ephemeron_box.rs | 19 ----- boa_gc/src/internals/mod.rs | 1 - boa_gc/src/pointers/ephemeron.rs | 13 ---- boa_gc/src/pointers/gc.rs | 71 +----------------- boa_gc/src/pointers/weak.rs | 5 -- 7 files changed, 63 insertions(+), 164 deletions(-) diff --git a/boa_engine/src/module/mod.rs b/boa_engine/src/module/mod.rs index 1720fd143df..ec30b846d01 100644 --- a/boa_engine/src/module/mod.rs +++ b/boa_engine/src/module/mod.rs @@ -295,15 +295,21 @@ impl Module { parser.set_identifier(context.next_parser_identifier()); let module = parser.parse_module(context.interner_mut())?; - Ok(Module { - inner: Gc::new_cyclic(|this| Inner { + 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(SourceTextModule::new(module, this.clone())), + kind: ModuleKind::SourceText(src.clone()), host_defined: (), }), - }) + }; + + src.set_parent(module.clone()); + + Ok(module) } /// Gets the realm of this `Module`. diff --git a/boa_engine/src/module/source.rs b/boa_engine/src/module/source.rs index e3f1aa7f336..7a222c80af8 100644 --- a/boa_engine/src/module/source.rs +++ b/boa_engine/src/module/source.rs @@ -11,7 +11,7 @@ use boa_ast::{ }, Declaration, ModuleItemList, }; -use boa_gc::{custom_trace, empty_trace, Finalize, Gc, GcRefCell, Trace, WeakGc}; +use boa_gc::{custom_trace, empty_trace, Finalize, Gc, GcRefCell, Trace}; use boa_interner::Sym; use rustc_hash::{FxHashMap, FxHashSet}; @@ -241,21 +241,11 @@ impl std::fmt::Debug for SourceTextModule { if !limiter.visited && !limiter.live { f.debug_struct("SourceTextModule") - .field("code", &self.inner.code) .field("status", &self.inner.status) - .field("requested_modules", &self.inner.requested_modules) .field("loaded_modules", &self.inner.loaded_modules) - .field("has_tla", &self.inner.has_tla) .field("async_parent_modules", &self.inner.async_parent_modules) .field("import_meta", &self.inner.import_meta) - .field("import_entries", &self.inner.import_entries) - .field("local_export_entries", &self.inner.local_export_entries) - .field( - "indirect_export_entries", - &self.inner.indirect_export_entries, - ) - .field("star_export_entries", &self.inner.star_export_entries) - .finish() + .finish_non_exhaustive() } else { f.write_str("{ ... }") } @@ -264,35 +254,39 @@ impl std::fmt::Debug for SourceTextModule { #[derive(Trace, Finalize)] struct Inner { - parent: WeakGc, + parent: GcRefCell>, status: GcRefCell, loaded_modules: GcRefCell>, async_parent_modules: GcRefCell>, import_meta: GcRefCell>, - requested_modules: FxHashSet, - has_tla: bool, - #[unsafe_ignore_trace] - code: ModuleItemList, #[unsafe_ignore_trace] + code: ModuleCode, +} + +#[derive(Debug)] +struct ModuleCode { + has_tla: bool, + requested_modules: FxHashSet, + node: ModuleItemList, import_entries: Vec, - #[unsafe_ignore_trace] local_export_entries: Vec, - #[unsafe_ignore_trace] indirect_export_entries: Vec, - #[unsafe_ignore_trace] 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 { - Module { - inner: self - .inner - .parent - .upgrade() - .expect("source text cannot be dropped before parent module"), - } + self.inner + .parent + .borrow() + .clone() + .expect("parent module must be initialized") } /// Creates a new `SourceTextModule` from a parsed `ModuleItemList`. @@ -300,7 +294,7 @@ impl SourceTextModule { /// Contains part of the abstract operation [`ParseModule`][parse]. /// /// [parse]: https://tc39.es/ecma262/#sec-parsemodule - pub(super) fn new(code: ModuleItemList, parent: WeakGc) -> Self { + 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. @@ -381,18 +375,20 @@ impl SourceTextModule { // Most of this can be ignored, since `Status` takes care of the remaining state. Self { inner: Gc::new(Inner { - parent, - code, - requested_modules, - has_tla, - import_entries, - local_export_entries, - indirect_export_entries, - star_export_entries, + 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, + }, }), } } @@ -408,7 +404,7 @@ impl SourceTextModule { && state.visited.borrow_mut().insert(self.clone()) { // b. Let requestedModulesCount be the number of elements in module.[[RequestedModules]]. - let requested = &self.inner.requested_modules; + let requested = &self.inner.code.requested_modules; // c. Set state.[[PendingModulesCount]] to state.[[PendingModulesCount]] + requestedModulesCount. state .pending_modules @@ -526,21 +522,21 @@ impl SourceTextModule { let mut exported_names = FxHashSet::default(); // 6. For each ExportEntry Record e of module.[[LocalExportEntries]], do - for e in &self.inner.local_export_entries { + 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.indirect_export_entries { + 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.star_export_entries { + 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(); @@ -584,7 +580,7 @@ impl SourceTextModule { resolve_set.insert((parent.clone(), export_name)); // 5. For each ExportEntry Record e of module.[[LocalExportEntries]], do - for e in &self.inner.local_export_entries { + 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. @@ -597,7 +593,7 @@ impl SourceTextModule { } // 6. For each ExportEntry Record e of module.[[IndirectExportEntries]], do - for e in &self.inner.indirect_export_entries { + 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]]). @@ -633,7 +629,7 @@ impl SourceTextModule { let mut star_resolution: Option = None; // 9. For each ExportEntry Record e of module.[[StarExportEntries]], do - for e in &self.inner.star_export_entries { + 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). @@ -768,7 +764,7 @@ impl SourceTextModule { // 9. For each String required of module.[[RequestedModules]], do - for required in &self.inner.requested_modules { + 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(); @@ -1073,7 +1069,7 @@ impl SourceTextModule { stack.push(self.clone()); // 11. For each String required of module.[[RequestedModules]], do - for &required in &self.inner.requested_modules { + 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). @@ -1139,7 +1135,7 @@ impl SourceTextModule { } // 12. If module.[[PendingAsyncDependencies]] > 0 or module.[[HasTLA]] is true, then - if pending_async_dependencies > 0 || self.inner.has_tla { + 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 { @@ -1243,7 +1239,7 @@ impl SourceTextModule { Status::Evaluating { .. } | Status::EvaluatingAsync { .. } )); // 2. Assert: module.[[HasTLA]] is true. - debug_assert!(self.inner.has_tla); + debug_assert!(self.inner.code.has_tla); // 3. Let capability be ! NewPromiseCapability(%Promise%). let capability = PromiseCapability::new( @@ -1326,7 +1322,7 @@ impl SourceTextModule { // v. Set m.[[PendingAsyncDependencies]] to m.[[PendingAsyncDependencies]] - 1. *pending_async_dependencies -= 1; - (*pending_async_dependencies, m.inner.has_tla) + (*pending_async_dependencies, m.inner.code.has_tla) }; // vi. If m.[[PendingAsyncDependencies]] = 0, then @@ -1362,7 +1358,7 @@ impl SourceTextModule { { // 1. For each ExportEntry Record e of module.[[IndirectExportEntries]], do - for e in &self.inner.indirect_export_entries { + 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()) @@ -1405,7 +1401,7 @@ impl SourceTextModule { let codeblock = { // 7. For each ImportEntry Record in of module.[[ImportEntries]], do - for entry in &self.inner.import_entries { + 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(); @@ -1469,7 +1465,7 @@ impl SourceTextModule { // 18. Let code be module.[[ECMAScriptCode]]. // 19. Let varDeclarations be the VarScopedDeclarations of code. - let var_declarations = var_scoped_declarations(&self.inner.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 @@ -1493,7 +1489,7 @@ impl SourceTextModule { // 22. Let lexDeclarations be the LexicallyScopedDeclarations of code. // 23. Let privateEnv be null. - let lex_declarations = lexically_scoped_declarations(&self.inner.code); + 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 { @@ -1557,7 +1553,7 @@ impl SourceTextModule { } } - compiler.compile_module_item_list(&self.inner.code); + compiler.compile_module_item_list(&self.inner.code.node); Gc::new(compiler.finish()) }; @@ -1785,7 +1781,7 @@ fn async_module_execution_fulfilled(module: &SourceTextModule, context: &mut Con } // b. Else if m.[[HasTLA]] is true, then - let has_tla = m.inner.has_tla; + let has_tla = m.inner.code.has_tla; if has_tla { // i. Perform ExecuteAsyncModule(m). m.execute_async(context); diff --git a/boa_gc/src/internals/ephemeron_box.rs b/boa_gc/src/internals/ephemeron_box.rs index a42316146ac..68cbb0af976 100644 --- a/boa_gc/src/internals/ephemeron_box.rs +++ b/boa_gc/src/internals/ephemeron_box.rs @@ -117,25 +117,6 @@ impl EphemeronBox { } } - pub(crate) fn new_empty() -> Self { - Self { - header: EphemeronBoxHeader::new(), - data: Cell::new(None), - } - } - - pub(crate) fn init(&self, key: &Gc, value: V) { - let data = Box::into_raw(Box::new(Data { - key: key.inner_ptr(), - value, - })); - - // SAFETY: `Box::into_raw` must always return a non-null pointer. - let data = unsafe { NonNull::new_unchecked(data) }; - - self.data.set(Some(data)); - } - /// Returns `true` if the two references refer to the same `GcBox`. pub(crate) fn ptr_eq(this: &Self, other: &Self) -> bool { // Use .header to ignore fat pointer vtables, to work around diff --git a/boa_gc/src/internals/mod.rs b/boa_gc/src/internals/mod.rs index 919531a706a..964b526e84e 100644 --- a/boa_gc/src/internals/mod.rs +++ b/boa_gc/src/internals/mod.rs @@ -3,7 +3,6 @@ mod gc_box; mod weak_map_box; pub(crate) use self::ephemeron_box::{EphemeronBox, ErasedEphemeronBox}; -pub(crate) use self::gc_box::GcBoxHeader; pub(crate) use self::weak_map_box::{ErasedWeakMapBox, WeakMapBox}; pub use self::gc_box::GcBox; diff --git a/boa_gc/src/pointers/ephemeron.rs b/boa_gc/src/pointers/ephemeron.rs index 8015077979e..cce6d363afa 100644 --- a/boa_gc/src/pointers/ephemeron.rs +++ b/boa_gc/src/pointers/ephemeron.rs @@ -60,19 +60,6 @@ impl Ephemeron { EphemeronBox::ptr_eq(this.inner(), other.inner()) } - /// Creates a new `Ephemeron` that has no data - pub(crate) fn new_empty() -> Self { - // SAFETY: `Allocator` can only return non-null pointers, making this - // safe. - unsafe { - Self { - inner_ptr: Cell::new(Rootable::new_unchecked(Allocator::alloc_ephemeron( - EphemeronBox::new_empty(), - ))), - } - } - } - fn is_rooted(&self) -> bool { self.inner_ptr.get().is_rooted() } diff --git a/boa_gc/src/pointers/gc.rs b/boa_gc/src/pointers/gc.rs index 915ff653d1f..4cc308294df 100644 --- a/boa_gc/src/pointers/gc.rs +++ b/boa_gc/src/pointers/gc.rs @@ -1,8 +1,8 @@ use crate::{ finalizer_safe, - internals::{GcBox, GcBoxHeader}, + internals::GcBox, trace::{Finalize, Trace}, - Allocator, Ephemeron, WeakGc, BOA_GC, + Allocator, }; use std::{ cell::Cell, @@ -10,9 +10,8 @@ use std::{ fmt::{self, Debug, Display}, hash::{Hash, Hasher}, marker::PhantomData, - mem, ops::Deref, - ptr::{self, NonNull}, + ptr::NonNull, rc::Rc, }; @@ -44,70 +43,6 @@ impl Gc { } } - /// Constructs a new `Gc` while giving you a `WeakGc` to the allocation, - /// to allow you to construct a `T` which holds a weak pointer to itself. - /// - /// Since the new `Gc` is not fully-constructed until `Gc::new_cyclic` - /// returns, calling [`WeakGc::upgrade`] on the weak reference inside the closure will - /// fail and result in a `None` value. - pub fn new_cyclic(data_fn: F) -> Self - where - F: FnOnce(&WeakGc) -> T, - { - // Create GcBox and allocate it to heap. - let inner_ptr = BOA_GC.with(|st| { - let mut gc = st.borrow_mut(); - - Allocator::manage_state(&mut gc); - - let header = GcBoxHeader::new(); - - header.next.set(gc.strong_start.take()); - - // Safety: value cannot be a null pointer, since `Box` cannot return null pointers. - unsafe { - NonNull::new_unchecked(Box::into_raw(Box::new(GcBox { - header, - value: mem::MaybeUninit::::uninit(), - }))) - } - }); - - let init_ptr: NonNull> = inner_ptr.cast(); - - let weak = WeakGc::from(Ephemeron::new_empty()); - - let data = data_fn(&weak); - - // SAFETY: `inner_ptr` has been allocated above, so making writes to `init_ptr` is safe. - let strong = unsafe { - let inner = init_ptr.as_ptr(); - ptr::write(ptr::addr_of_mut!((*inner).value), data); - - // `strong` must be a valid value that implements [`Trace`]. - (*inner).value.unroot(); - - // `init_ptr` is initialized and its contents are unrooted, making this operation safe. - Self::from_raw(init_ptr) - }; - - BOA_GC.with(|st| { - let mut gc = st.borrow_mut(); - let erased: NonNull> = init_ptr; - - gc.strong_start.set(Some(erased)); - gc.runtime.bytes_allocated += mem::size_of::>(); - }); - - // SAFETY: `init_ptr` is initialized and its contents are unrooted, making this operation - // safe. - unsafe { - weak.inner().inner().init(&strong, Self::from_raw(init_ptr)); - } - - strong - } - /// Consumes the `Gc`, returning a wrapped raw pointer. /// /// To avoid a memory leak, the pointer must be converted back to a `Gc` using [`Gc::from_raw`]. diff --git a/boa_gc/src/pointers/weak.rs b/boa_gc/src/pointers/weak.rs index c6cbdfb06f3..edeadd7add3 100644 --- a/boa_gc/src/pointers/weak.rs +++ b/boa_gc/src/pointers/weak.rs @@ -23,11 +23,6 @@ impl WeakGc { pub fn upgrade(&self) -> Option> { self.inner.value() } - - /// Gets the inner ephemeron of the weak gc - pub(crate) const fn inner(&self) -> &Ephemeron> { - &self.inner - } } impl Clone for WeakGc { From c722df3d089511fb4412f335c938d5f4b16b607f Mon Sep 17 00:00:00 2001 From: jedel1043 Date: Tue, 16 May 2023 22:52:23 -0600 Subject: [PATCH 15/18] Apply review --- .../{immutable.rs => immutable_prototype.rs} | 8 +-- boa_engine/src/object/internal_methods/mod.rs | 4 +- .../{namespace.rs => module_namespace.rs} | 69 ++++++++++++------- boa_engine/src/object/mod.rs | 4 +- boa_interner/src/sym.rs | 2 + 5 files changed, 55 insertions(+), 32 deletions(-) rename boa_engine/src/object/internal_methods/{immutable.rs => immutable_prototype.rs} (76%) rename boa_engine/src/object/internal_methods/{namespace.rs => module_namespace.rs} (83%) diff --git a/boa_engine/src/object/internal_methods/immutable.rs b/boa_engine/src/object/internal_methods/immutable_prototype.rs similarity index 76% rename from boa_engine/src/object/internal_methods/immutable.rs rename to boa_engine/src/object/internal_methods/immutable_prototype.rs index 62511cdc13f..f9cf56c7410 100644 --- a/boa_engine/src/object/internal_methods/immutable.rs +++ b/boa_engine/src/object/internal_methods/immutable_prototype.rs @@ -5,12 +5,12 @@ use crate::{ use super::{InternalObjectMethods, ORDINARY_INTERNAL_METHODS}; -/// Definitions of the internal object methods for [**Module Namespace Exotic Objects**][spec]. +/// Definitions of the internal object methods for [**Immutable Prototype Exotic Objects**][spec]. /// -/// [spec]: https://tc39.es/ecma262/#sec-module-namespace-exotic-objects +/// [spec]: https://tc39.es/ecma262/#sec-immutable-prototype-exotic-objects pub(crate) static IMMUTABLE_PROTOTYPE_EXOTIC_INTERNAL_METHODS: InternalObjectMethods = InternalObjectMethods { - __set_prototype_of__: set_prototype_of, + __set_prototype_of__: immutable_prototype_exotic_set_prototype_of, ..ORDINARY_INTERNAL_METHODS }; @@ -18,7 +18,7 @@ pub(crate) static IMMUTABLE_PROTOTYPE_EXOTIC_INTERNAL_METHODS: InternalObjectMet /// /// [spec]: https://tc39.es/ecma262/#sec-immutable-prototype-exotic-objects-setprototypeof-v #[allow(clippy::needless_pass_by_value)] -pub(crate) fn set_prototype_of( +pub(crate) fn immutable_prototype_exotic_set_prototype_of( obj: &JsObject, val: JsPrototype, context: &mut Context<'_>, diff --git a/boa_engine/src/object/internal_methods/mod.rs b/boa_engine/src/object/internal_methods/mod.rs index b594205196d..976f2824226 100644 --- a/boa_engine/src/object/internal_methods/mod.rs +++ b/boa_engine/src/object/internal_methods/mod.rs @@ -19,9 +19,9 @@ pub(super) mod arguments; pub(super) mod array; pub(super) mod bound_function; pub(super) mod function; -pub(super) mod immutable; +pub(super) mod immutable_prototype; pub(super) mod integer_indexed; -pub(super) mod namespace; +pub(super) mod module_namespace; pub(super) mod proxy; pub(super) mod string; diff --git a/boa_engine/src/object/internal_methods/namespace.rs b/boa_engine/src/object/internal_methods/module_namespace.rs similarity index 83% rename from boa_engine/src/object/internal_methods/namespace.rs rename to boa_engine/src/object/internal_methods/module_namespace.rs index a4990598e6a..ceb3e53b607 100644 --- a/boa_engine/src/object/internal_methods/namespace.rs +++ b/boa_engine/src/object/internal_methods/module_namespace.rs @@ -8,7 +8,7 @@ use crate::{ }; use super::{ - immutable, ordinary_define_own_property, ordinary_delete, ordinary_get, + 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, }; @@ -18,17 +18,17 @@ use super::{ /// [spec]: https://tc39.es/ecma262/#sec-module-namespace-exotic-objects pub(crate) static MODULE_NAMESPACE_EXOTIC_INTERNAL_METHODS: InternalObjectMethods = InternalObjectMethods { - __get_prototype_of__: get_prototype_of, - __set_prototype_of__: set_prototype_of, - __is_extensible__: is_extensible, - __prevent_extensions__: prevent_extensions, - __get_own_property__: get_own_property, - __define_own_property__: define_own_property, - __has_property__: has_property, - __get__: get, - __set__: set, - __delete__: delete, - __own_property_keys__: own_property_keys, + __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 }; @@ -36,7 +36,10 @@ pub(crate) static MODULE_NAMESPACE_EXOTIC_INTERNAL_METHODS: InternalObjectMethod /// /// [spec]: https://tc39.es/ecma262/#sec-module-namespace-exotic-objects-getprototypeof #[allow(clippy::unnecessary_wraps)] -fn get_prototype_of(_: &JsObject, _: &mut Context<'_>) -> JsResult { +fn module_namespace_exotic_get_prototype_of( + _: &JsObject, + _: &mut Context<'_>, +) -> JsResult { // 1. Return null. Ok(None) } @@ -45,16 +48,23 @@ fn get_prototype_of(_: &JsObject, _: &mut Context<'_>) -> JsResult /// /// [spec]: https://tc39.es/ecma262/#sec-module-namespace-exotic-objects-setprototypeof-v #[allow(clippy::unnecessary_wraps)] -fn set_prototype_of(obj: &JsObject, val: JsPrototype, context: &mut Context<'_>) -> JsResult { +fn module_namespace_exotic_set_prototype_of( + obj: &JsObject, + val: JsPrototype, + context: &mut Context<'_>, +) -> JsResult { // 1. Return ! SetImmutablePrototype(O, V). - Ok(immutable::set_prototype_of(obj, val, context).expect("this must not fail per the spec")) + 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 is_extensible(_: &JsObject, _: &mut Context<'_>) -> JsResult { +fn module_namespace_exotic_is_extensible(_: &JsObject, _: &mut Context<'_>) -> JsResult { // 1. Return false. Ok(false) } @@ -63,14 +73,14 @@ fn is_extensible(_: &JsObject, _: &mut Context<'_>) -> JsResult { /// /// [spec]: https://tc39.es/ecma262/#sec-module-namespace-exotic-objects-preventextensions #[allow(clippy::unnecessary_wraps)] -fn prevent_extensions(_: &JsObject, _: &mut Context<'_>) -> JsResult { +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 get_own_property( +fn module_namespace_exotic_get_own_property( obj: &JsObject, key: &PropertyKey, context: &mut Context<'_>, @@ -113,7 +123,7 @@ fn get_own_property( /// [`[[DefineOwnProperty]] ( P, Desc )`][spec] /// /// [spec]: https://tc39.es/ecma262/#sec-module-namespace-exotic-objects-defineownproperty-p-desc -fn define_own_property( +fn module_namespace_exotic_define_own_property( obj: &JsObject, key: &PropertyKey, desc: PropertyDescriptor, @@ -150,7 +160,11 @@ fn define_own_property( /// [`[[HasProperty]] ( P )`][spec] /// /// [spec]: https://tc39.es/ecma262/#sec-module-namespace-exotic-objects-hasproperty-p -fn has_property(obj: &JsObject, key: &PropertyKey, context: &mut Context<'_>) -> JsResult { +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), @@ -174,7 +188,7 @@ fn has_property(obj: &JsObject, key: &PropertyKey, context: &mut Context<'_>) -> /// [`[[Get]] ( P, Receiver )`][spec] /// /// [spec]: https://tc39.es/ecma262/#sec-module-namespace-exotic-objects-get-p-receiver -fn get( +fn module_namespace_exotic_get( obj: &JsObject, key: &PropertyKey, receiver: JsValue, @@ -248,7 +262,7 @@ fn get( /// /// [spec]: https://tc39.es/ecma262/#sec-module-namespace-exotic-objects-set-p-v-receiver #[allow(clippy::needless_pass_by_value, clippy::unnecessary_wraps)] -fn set( +fn module_namespace_exotic_set( _obj: &JsObject, _key: PropertyKey, _value: JsValue, @@ -262,7 +276,11 @@ fn set( /// [`[[Delete]] ( P )`][spec]. /// /// [spec]: https://tc39.es/ecma262/#sec-module-namespace-exotic-objects-delete-p -fn delete(obj: &JsObject, key: &PropertyKey, context: &mut Context<'_>) -> JsResult { +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 { @@ -287,7 +305,10 @@ fn delete(obj: &JsObject, key: &PropertyKey, context: &mut Context<'_>) -> JsRes /// [`[[OwnPropertyKeys]] ( )`][spec]. /// /// [spec]: https://tc39.es/ecma262/#sec-module-namespace-exotic-objects-ownpropertykeys -fn own_property_keys(obj: &JsObject, context: &mut Context<'_>) -> JsResult> { +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)?; diff --git a/boa_engine/src/object/mod.rs b/boa_engine/src/object/mod.rs index 555127af538..39033caeedc 100644 --- a/boa_engine/src/object/mod.rs +++ b/boa_engine/src/object/mod.rs @@ -16,9 +16,9 @@ use self::{ BOUND_CONSTRUCTOR_EXOTIC_INTERNAL_METHODS, BOUND_FUNCTION_EXOTIC_INTERNAL_METHODS, }, function::{CONSTRUCTOR_INTERNAL_METHODS, FUNCTION_INTERNAL_METHODS}, - immutable::IMMUTABLE_PROTOTYPE_EXOTIC_INTERNAL_METHODS, + immutable_prototype::IMMUTABLE_PROTOTYPE_EXOTIC_INTERNAL_METHODS, integer_indexed::INTEGER_INDEXED_EXOTIC_INTERNAL_METHODS, - namespace::MODULE_NAMESPACE_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, diff --git a/boa_interner/src/sym.rs b/boa_interner/src/sym.rs index 0920e2fe76f..3129f60c3a5 100644 --- a/boa_interner/src/sym.rs +++ b/boa_interner/src/sym.rs @@ -18,6 +18,8 @@ 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!(); } From 88715c653e16b782db73a1f7c212b61682fa28bb Mon Sep 17 00:00:00 2001 From: jedel1043 Date: Tue, 16 May 2023 23:24:01 -0600 Subject: [PATCH 16/18] Clarify reasoning of manual `Trace` impl --- boa_engine/src/module/mod.rs | 18 +- boa_engine/src/module/source.rs | 414 ++++++++++++++++---------------- 2 files changed, 221 insertions(+), 211 deletions(-) diff --git a/boa_engine/src/module/mod.rs b/boa_engine/src/module/mod.rs index ec30b846d01..19d8e255dac 100644 --- a/boa_engine/src/module/mod.rs +++ b/boa_engine/src/module/mod.rs @@ -399,13 +399,13 @@ impl Module { // 5. If state.[[PendingModulesCount]] = 0, then if state.pending_modules.get() == 0 { - // a. Set state.[[IsLoading]] to false. + // 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. + // 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 »). + // c. Perform ! Call(state.[[PromiseCapability]].[[Resolve]], undefined, « undefined »). state .capability .resolve() @@ -530,17 +530,17 @@ impl Module { // 1. If module is not a Cyclic Module Record, then #[allow(unused, clippy::diverging_sub_expression)] ModuleKind::Synthetic => { - // a. Let promise be ! module.Evaluate(). + // 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. + // d. Return index. PromiseState::Fulfilled(_) => Ok(index), - // c. If promise.[[PromiseState]] is rejected, then - // i. Return ThrowCompletion(promise.[[PromiseResult]]). + // c. If promise.[[PromiseState]] is rejected, then + // i. Return ThrowCompletion(promise.[[PromiseResult]]). PromiseState::Rejected(err) => Err(JsError::from_opaque(err)), } } @@ -628,6 +628,7 @@ impl Module { // 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() @@ -649,7 +650,6 @@ impl Module { .collect(); // d. Set namespace to ModuleNamespaceCreate(module, unambiguousNames). - // 4. Return namespace. ModuleNamespace::create(self.clone(), unambiguous_names, context) }) .clone() diff --git a/boa_engine/src/module/source.rs b/boa_engine/src/module/source.rs index 7a222c80af8..b0fc490ca35 100644 --- a/boa_engine/src/module/source.rs +++ b/boa_engine/src/module/source.rs @@ -86,24 +86,32 @@ enum Status { } // SAFETY: This must be synced with `Status` to mark any new data added that needs to be traced. -// This implementation is necessary to be able to transition from one state to another by destructuring, -// which saves us some unnecessary clones. +// `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 { .. } | Status::Linked { .. } => {} - Status::PreLinked { context, .. } => mark(context), + 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); @@ -325,11 +333,11 @@ impl SourceTextModule { _ => 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. + // 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), @@ -337,22 +345,22 @@ impl SourceTextModule { )); } else { // i. If importedBoundNames does not contain ee.[[LocalName]], then - // 1. Append ee to localExportEntries. + // 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. + // 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. + // i. Assert: ee.[[ExportName]] is null. + // ii. Append ee to starExportEntries. star_export_entries.push(module_request); } // c. Else, - // i. Append ee to indirectExportEntries. + // i. Append ee to indirectExportEntries. ExportEntry::ReExport(entry) => indirect_export_entries.push(entry), } } @@ -411,17 +419,17 @@ impl SourceTextModule { .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 + // 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]]). + // 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. + // 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) @@ -439,13 +447,13 @@ impl SourceTextModule { 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]]. + // 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]]. + // i. Assert: That Record's [[Module]] is result.[[Value]]. debug_assert_eq!(entry, loaded); } @@ -490,7 +498,7 @@ impl SourceTextModule { context, ); } - // iii. If state.[[IsLoading]] is false, return unused. + // iii. If state.[[IsLoading]] is false, return unused. if !state.loading.get() { return; } @@ -510,8 +518,8 @@ impl SourceTextModule { // 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. + // a. Assert: We've reached the starting point of an export * circularity. + // b. Return a new empty List. return FxHashSet::default(); } @@ -523,30 +531,30 @@ impl SourceTextModule { // 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. + // 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. + // 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]]). + // 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 + // 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 + // 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. + // 1. If exportedNames does not contain n, then + // a. Append n to exportedNames. exported_names.insert(n); } } @@ -569,10 +577,10 @@ impl SourceTextModule { // 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 + // 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. + // i. Assert: This is a circular import request. + // ii. Return null. return Err(ResolveExportError::NotFound); } @@ -581,10 +589,10 @@ impl SourceTextModule { // 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 + // 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]] }. + // 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()), @@ -594,22 +602,22 @@ impl SourceTextModule { // 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 + // a. If SameValue(exportName, e.[[ExportName]]) is true, then if export_name == e.export_name() { - // i. Let importedModule be GetImportedModule(module, e.[[ModuleRequest]]). + // 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 }. + // 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). + // 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) } @@ -619,9 +627,9 @@ impl SourceTextModule { // 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. + // 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); } @@ -630,45 +638,45 @@ impl SourceTextModule { // 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]]). + // 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). + // 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 + // d. If resolution is not null, then Ok(resolution) => resolution, - // c. If resolution is ambiguous, return ambiguous. + // 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. + // 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. + // 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. + // 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. + // 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. + // ii. If starResolution is null, then + // 1. Set starResolution to resolution. star_resolution = Some(resolution); } } @@ -696,16 +704,16 @@ impl SourceTextModule { // 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 + // a. For each Cyclic Module Record m of stack, do for m in stack { - // i. Assert: m.[[Status]] is linking. + // i. Assert: m.[[Status]] is linking. debug_assert!(matches!(&*m.inner.status.borrow(), Status::Linking { .. })); - // ii. Set m.[[Status]] to unlinked. + // ii. Set m.[[Status]] to unlinked. *m.inner.status.borrow_mut() = Status::Unlinked; } - // b. Assert: module.[[Status]] is unlinked. + // b. Assert: module.[[Status]] is unlinked. assert!(matches!(&*self.inner.status.borrow(), Status::Unlinked)); - // c. Return ? result. + // c. Return ? result. return Err(err); } @@ -739,7 +747,7 @@ impl SourceTextModule { | Status::EvaluatingAsync { .. } | Status::Evaluated { .. } ) { - // a. Return index. + // a. Return index. return Ok(index); } @@ -765,15 +773,15 @@ impl SourceTextModule { // 9. For each String required of module.[[RequestedModules]], do for required in &self.inner.code.requested_modules { - // a. Let requiredModule be GetImportedModule(module, required). + // 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). + // 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 + // 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. + // 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 { .. } @@ -783,7 +791,7 @@ impl SourceTextModule { _ => false, }); - // iii. If requiredModule.[[Status]] is linking, then + // iii. If requiredModule.[[Status]] is linking, then let required_index = if let Status::Linking { info: DfsInfo { @@ -791,8 +799,8 @@ impl SourceTextModule { }, } = &*required_module.inner.status.borrow() { - // 1. Set module.[[DFSAncestorIndex]] to min(module.[[DFSAncestorIndex]], - // requiredModule.[[DFSAncestorIndex]]). + // 1. Set module.[[DFSAncestorIndex]] to min(module.[[DFSAncestorIndex]], + // requiredModule.[[DFSAncestorIndex]]). Some(*dfs_ancestor_index) } else { @@ -839,11 +847,11 @@ impl SourceTextModule { // 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. + // 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. + // iv. Set requiredModule.[[Status]] to linked. last.inner .status .borrow_mut() @@ -856,7 +864,7 @@ impl SourceTextModule { } }); - // v. If requiredModule and module are the same Module Record, set done to true. + // v. If requiredModule and module are the same Module Record, set done to true. if &last == self { break; } @@ -928,15 +936,15 @@ impl SourceTextModule { // 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. + // 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 »). + // i. Assert: module.[[Status]] is evaluated. + // ii. Perform ! Call(capability.[[Resolve]], undefined, « undefined »). capability .resolve() .call(&JsValue::undefined(), &[], context) @@ -1070,15 +1078,15 @@ impl SourceTextModule { // 11. For each String required of module.[[RequestedModules]], do for &required in &self.inner.code.requested_modules { - // a. Let requiredModule be GetImportedModule(module, required). + // 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). + // 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 + // 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. + // 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, @@ -1086,23 +1094,23 @@ impl SourceTextModule { }); let (required_module, async_eval, req_info) = match &*required_module.inner.status.borrow() { - // iii. If requiredModule.[[Status]] is evaluating, then + // iii. If requiredModule.[[Status]] is evaluating, then Status::Evaluating { info, async_eval_index, .. } => { - // 1. Set module.[[DFSAncestorIndex]] to min(module.[[DFSAncestorIndex]], requiredModule.[[DFSAncestorIndex]]). + // 1. Set module.[[DFSAncestorIndex]] to min(module.[[DFSAncestorIndex]], requiredModule.[[DFSAncestorIndex]]). (required_module.clone(), async_eval_index.is_some(), Some(*info)) } - // iv. Else, + // 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. + // 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]]. + // 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."), @@ -1120,11 +1128,11 @@ impl SourceTextModule { usize::min(info.dfs_ancestor_index, req_info.dfs_ancestor_index); } - // v. If requiredModule.[[AsyncEvaluation]] is true, then + // v. If requiredModule.[[AsyncEvaluation]] is true, then if async_eval { - // 1. Set module.[[PendingAsyncDependencies]] to module.[[PendingAsyncDependencies]] + 1. + // 1. Set module.[[PendingAsyncDependencies]] to module.[[PendingAsyncDependencies]] + 1. pending_async_dependencies += 1; - // 2. Append module to requiredModule.[[AsyncParentModules]]. + // 2. Append module to requiredModule.[[AsyncParentModules]]. required_module .inner .async_parent_modules @@ -1136,14 +1144,14 @@ impl SourceTextModule { // 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. + // 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.) + // 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()?); } @@ -1153,7 +1161,7 @@ impl SourceTextModule { } } else { // 13. Else, - // a. Perform ? module.ExecuteModule(). + // a. Perform ? module.ExecuteModule(). self.execute(None, context)?; } @@ -1168,17 +1176,17 @@ impl SourceTextModule { // 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, + // 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. + // 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. + // iii. Assert: requiredModule is a Cyclic Module Record. required_module.inner.status.borrow_mut().transition(|current| match current { Status::Evaluating { top_level_capability, @@ -1187,10 +1195,10 @@ impl SourceTextModule { context, .. } => if let Some(async_eval_index) = async_eval_index { - // v. Otherwise, set requiredModule.[[Status]] to evaluating-async. + // v. Otherwise, set requiredModule.[[Status]] to evaluating-async. Status::EvaluatingAsync { top_level_capability, - // vii. Set requiredModule.[[CycleRoot]] to module. + // vii. Set requiredModule.[[CycleRoot]] to module. cycle_root: if is_self { cycle_root } else { @@ -1201,7 +1209,7 @@ impl SourceTextModule { context } } else { - // iv. If requiredModule.[[AsyncEvaluation]] is false, set requiredModule.[[Status]] to evaluated. + // iv. If requiredModule.[[AsyncEvaluation]] is false, set requiredModule.[[Status]] to evaluated. Status::Evaluated { top_level_capability, cycle_root: if is_self { @@ -1218,7 +1226,7 @@ impl SourceTextModule { } ); - // vi. If requiredModule and module are the same Module Record, set done to true. + // vi. If requiredModule and module are the same Module Record, set done to true. if is_self { break; } @@ -1271,9 +1279,9 @@ impl SourceTextModule { 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). + // a. Perform AsyncModuleExecutionRejected(module, error). async_module_execution_rejected(module, &error, context); - // b. Return undefined. + // b. Return undefined. Ok(JsValue::undefined()) }, self.clone(), @@ -1303,39 +1311,39 @@ impl SourceTextModule { 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 - // 2. Return unused. + // 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. + // 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. + // iv. Assert: m.[[PendingAsyncDependencies]] > 0. assert!(*pending_async_dependencies > 0); - // v. Set m.[[PendingAsyncDependencies]] to m.[[PendingAsyncDependencies]] - 1. + // 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 + // vi. If m.[[PendingAsyncDependencies]] = 0, then if deps == 0 { - // 1. Append m to execList. + // 1. Append m to execList. exec_list.insert(m.clone()); - // 2. If m.[[HasTLA]] is false, perform GatherAvailableAncestors(m, execList). + // 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]. @@ -1359,10 +1367,10 @@ impl SourceTextModule { { // 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]]). + // 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. + // b. If resolution is either null or ambiguous, throw a SyntaxError exception. .map_err(|err| match err { ResolveExportError::NotFound => { JsNativeError::syntax().with_message(format!( @@ -1377,7 +1385,7 @@ impl SourceTextModule { )) } })?; - // c. Assert: resolution is a ResolvedBinding Record. + // c. Assert: resolution is a ResolvedBinding Record. } } @@ -1402,17 +1410,17 @@ impl SourceTextModule { 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]]). + // 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]]). + // 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. + // ii. If resolution is either null or ambiguous, throw a SyntaxError exception. .map_err(|err| match err { ResolveExportError::NotFound => JsNativeError::syntax() .with_message(format!( @@ -1426,15 +1434,15 @@ impl SourceTextModule { )), })?; - // 2. Perform ! env.CreateImmutableBinding(in.[[LocalName]], true). - // 3. Perform ! env.InitializeBinding(in.[[LocalName]], namespace). + // 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 + // 1. Perform env.CreateImportBinding(in.[[LocalName]], resolution.[[Module]], + // resolution.[[BindingName]]). + // deferred to initialization below imports.push(ImportBinding::Single { locator, export_locator: resolution, @@ -1448,14 +1456,14 @@ impl SourceTextModule { }); } } else { - // b. If in.[[ImportName]] is namespace-object, then - // ii. Perform ! env.CreateImmutableBinding(in.[[LocalName]], true). + // 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). + // 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 + // i. Let namespace be GetModuleNamespace(importedModule). + // deferred to initialization below imports.push(ImportBinding::Namespace { locator, module: imported_module.clone(), @@ -1470,18 +1478,18 @@ impl SourceTextModule { 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 + // 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 + // i. If declaredVarNames does not contain dn, then if !declared_var_names.contains(&name) { - // 1. Perform ! env.CreateMutableBinding(dn, false). + // 1. Perform ! env.CreateMutableBinding(dn, false). compiler.create_mutable_binding(name, false); - // 2. Perform ! env.InitializeBinding(dn, undefined). + // 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. + // 3. Append dn to declaredVarNames. declared_var_names.push(name); } } @@ -1493,59 +1501,59 @@ impl SourceTextModule { // 24. For each element d of lexDeclarations, do for declaration in lex_declarations { match &declaration { - // i. If IsConstantDeclaration of d is true, then + // i. If IsConstantDeclaration of d is true, then Declaration::Lexical(LexicalDeclaration::Const(declaration)) => { - // a. For each element dn of the BoundNames of d, do + // a. For each element dn of the BoundNames of d, do for name in bound_names(declaration) { - // 1. Perform ! env.CreateImmutableBinding(dn, true). + // 1. Perform ! env.CreateImmutableBinding(dn, true). compiler.create_immutable_binding(name, true); } } - // ii. Else, + // ii. Else, Declaration::Lexical(LexicalDeclaration::Let(declaration)) => { - // a. For each element dn of the BoundNames of d, do + // a. For each element dn of the BoundNames of d, do for name in bound_names(declaration) { - // 1. Perform ! env.CreateMutableBinding(dn, false). + // 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 + // 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). + // 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). + // 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). + // 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). + // 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). + // 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); } @@ -1568,8 +1576,10 @@ impl SourceTextModule { // 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); @@ -1579,7 +1589,7 @@ impl SourceTextModule { for import in imports { match import { ImportBinding::Namespace { locator, module } => { - // i. Let namespace be GetModuleNamespace(importedModule). + // i. Let namespace be GetModuleNamespace(importedModule). let namespace = module.namespace(context); context.vm.environments.put_lexical_value( locator.environment_index(), @@ -1680,14 +1690,14 @@ impl SourceTextModule { 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. + // 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). + // 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); @@ -1698,7 +1708,7 @@ impl SourceTextModule { // f. If result is an abrupt completion, then if let CompletionRecord::Throw(err) = result { - // i. Return ? result. + // i. Return ? result. Err(err) } else { // 11. Return unused. @@ -1744,10 +1754,10 @@ fn async_module_execution_fulfilled(module: &SourceTextModule, context: &mut Con // 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. + // 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 »). + // b. Perform ! Call(module.[[TopLevelCapability]].[[Resolve]], undefined, « undefined »). cap.resolve() .call(&JsValue::undefined(), &[], context) .expect("default `resolve` function cannot fail"); @@ -1773,30 +1783,30 @@ fn async_module_execution_fulfilled(module: &SourceTextModule, context: &mut Con // 12. For each Cyclic Module Record m of sortedExecList, do for m in ancestors { - // a. If m.[[Status]] is evaluated, then + // a. If m.[[Status]] is evaluated, then if let Status::Evaluated { error, .. } = &*m.inner.status.borrow() { - // i. Assert: m.[[EvaluationError]] is not empty. + // i. Assert: m.[[EvaluationError]] is not empty. assert!(error.is_some()); continue; } - // b. Else if m.[[HasTLA]] is true, then + // b. Else if m.[[HasTLA]] is true, then let has_tla = m.inner.code.has_tla; if has_tla { - // i. Perform ExecuteAsyncModule(m). + // i. Perform ExecuteAsyncModule(m). m.execute_async(context); } else { - // c. Else, - // i. Let result be m.ExecuteModule(). + // c. Else, + // i. Let result be m.ExecuteModule(). let result = m.execute(None, context); - // ii. If result is an abrupt completion, then + // ii. If result is an abrupt completion, then if let Err(e) = result { - // 1. Perform AsyncModuleExecutionRejected(m, result.[[Value]]). + // 1. Perform AsyncModuleExecutionRejected(m, result.[[Value]]). async_module_execution_rejected(module, &e, context); } else { - // iii. Else, - // 1. Set m.[[Status]] to evaluated. + // iii. Else, + // 1. Set m.[[Status]] to evaluated. m.inner .status .borrow_mut() @@ -1814,12 +1824,12 @@ fn async_module_execution_fulfilled(module: &SourceTextModule, context: &mut Con }); let status = m.inner.status.borrow(); - // 2. If m.[[TopLevelCapability]] is not empty, then + // 2. If m.[[TopLevelCapability]] is not empty, then if let Some(cap) = status.top_level_capability() { - // a. Assert: m.[[CycleRoot]] is m. + // a. Assert: m.[[CycleRoot]] is m. debug_assert_eq!(status.cycle_root(), Some(&m)); - // b. Perform ! Call(m.[[TopLevelCapability]].[[Resolve]], undefined, « undefined »). + // b. Perform ! Call(m.[[TopLevelCapability]].[[Resolve]], undefined, « undefined »). cap.resolve() .call(&JsValue::undefined(), &[], context) .expect("default `resolve` function cannot fail"); @@ -1870,17 +1880,17 @@ fn async_module_execution_rejected( // 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). + // 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. + // a. Assert: module.[[CycleRoot]] is module. debug_assert_eq!(status.cycle_root(), Some(module)); - // b. Perform ! Call(module.[[TopLevelCapability]].[[Reject]], undefined, « error »). + // b. Perform ! Call(module.[[TopLevelCapability]].[[Reject]], undefined, « error »). cap.reject() .call(&JsValue::undefined(), &[error.to_opaque(context)], context) .expect("default `reject` function cannot fail"); From 5aa6d075621cd68cfbf56acfa35b0efc5e244a35 Mon Sep 17 00:00:00 2001 From: jedel1043 Date: Wed, 17 May 2023 11:23:55 -0600 Subject: [PATCH 17/18] Apply review pt. 2 --- boa_cli/src/main.rs | 6 +++--- boa_engine/src/module/mod.rs | 10 ++++++---- 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/boa_cli/src/main.rs b/boa_cli/src/main.rs index 1e4b7e673b0..e7e7210c7a3 100644 --- a/boa_cli/src/main.rs +++ b/boa_cli/src/main.rs @@ -163,8 +163,8 @@ struct Opt { module: bool, /// Root path from where the module resolver will try to load the modules. - #[arg(long, default_value_os_t = PathBuf::from("."), requires = "mod")] - modpath: PathBuf, + #[arg(long, short = 'r', default_value_os_t = PathBuf::from("."), requires = "mod")] + root: PathBuf, } impl Opt { @@ -346,7 +346,7 @@ fn main() -> Result<(), io::Error> { let args = Opt::parse(); let queue: &dyn JobQueue = &Jobs::default(); - let loader = &SimpleModuleLoader::new(&args.modpath) + 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() diff --git a/boa_engine/src/module/mod.rs b/boa_engine/src/module/mod.rs index 19d8e255dac..7c8214c1037 100644 --- a/boa_engine/src/module/mod.rs +++ b/boa_engine/src/module/mod.rs @@ -1,17 +1,19 @@ //! Boa's implementation of the ECMAScript's module system. //! -//! This module contains the [`Module`] type, which represents an [**Abstract Module Record**][module]. +//! 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`]. //! -//! Additionally, the [`ModuleLoader`] trait allows customizing the "load" step on the lifecycle +//! 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. There's a default [`SimpleModuleLoader`] implementation that just loads modules -//! relative to a root path, which should hopefully cover most simple usecases. +//! on loads. //! //! More information: //! - [ECMAScript reference][spec] From b2ec6ad0ecebbf43e4efe5a9e61b4b029d748b9f Mon Sep 17 00:00:00 2001 From: jedel1043 Date: Wed, 17 May 2023 13:51:35 -0600 Subject: [PATCH 18/18] Revert gc changes --- boa_gc/src/internals/gc_box.rs | 4 ++-- boa_gc/src/pointers/ephemeron.rs | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/boa_gc/src/internals/gc_box.rs b/boa_gc/src/internals/gc_box.rs index 61e61f19195..e5e5dd1b92e 100644 --- a/boa_gc/src/internals/gc_box.rs +++ b/boa_gc/src/internals/gc_box.rs @@ -85,9 +85,9 @@ impl fmt::Debug for GcBoxHeader { /// A garbage collected allocation. #[derive(Debug)] -pub struct GcBox { +pub struct GcBox { pub(crate) header: GcBoxHeader, - pub(crate) value: T, + value: T, } impl GcBox { diff --git a/boa_gc/src/pointers/ephemeron.rs b/boa_gc/src/pointers/ephemeron.rs index cce6d363afa..c63da65fd72 100644 --- a/boa_gc/src/pointers/ephemeron.rs +++ b/boa_gc/src/pointers/ephemeron.rs @@ -77,7 +77,7 @@ impl Ephemeron { self.inner_ptr.get().as_ptr() } - pub(crate) fn inner(&self) -> &EphemeronBox { + fn inner(&self) -> &EphemeronBox { // SAFETY: Please see Gc::inner_ptr() unsafe { self.inner_ptr().as_ref() } }