Browse Source

Implement module execution (#2922)

* Implement draft of module execution

* Fix recursion bug

* Re-enable JsObject's debug printing

* Modify API and document some methods

* Add missing documentation

* Add newline to module scripts

* npx prettier

* Apply reviews

* Add reference to parent struct on source module

* Document more steps on the example

* cargo clippy

* Revert `BoaGc` changes

* Lower `GcRefCell` to `Inner` of `SourceTextModule`

* Replace weak ref to module with strong ref

* Apply review

* Clarify reasoning of manual `Trace` impl

* Apply review pt. 2

* Revert gc changes
pull/2942/head
José Julián Espina 1 year ago committed by GitHub
parent
commit
06161edcbd
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 4
      .gitignore
  2. 1
      Cargo.lock
  3. 114
      boa_ast/src/declaration/export.rs
  4. 49
      boa_ast/src/declaration/import.rs
  5. 228
      boa_ast/src/module_item_list/mod.rs
  6. 50
      boa_cli/src/main.rs
  7. 14
      boa_engine/src/builtins/json/mod.rs
  8. 13
      boa_engine/src/builtins/promise/mod.rs
  9. 2
      boa_engine/src/bytecompiler/mod.rs
  10. 52
      boa_engine/src/bytecompiler/module.rs
  11. 20
      boa_engine/src/context/intrinsics.rs
  12. 199
      boa_engine/src/context/mod.rs
  13. 23
      boa_engine/src/environments/runtime/declarative/mod.rs
  14. 120
      boa_engine/src/environments/runtime/declarative/module.rs
  15. 22
      boa_engine/src/environments/runtime/mod.rs
  16. 3
      boa_engine/src/lib.rs
  17. 737
      boa_engine/src/module/mod.rs
  18. 1913
      boa_engine/src/module/source.rs
  19. 39
      boa_engine/src/object/internal_methods/immutable_prototype.rs
  20. 2
      boa_engine/src/object/internal_methods/mod.rs
  21. 329
      boa_engine/src/object/internal_methods/module_namespace.rs
  22. 14
      boa_engine/src/object/jsobject.rs
  23. 44
      boa_engine/src/object/mod.rs
  24. 21
      boa_engine/src/realm.rs
  25. 21
      boa_examples/scripts/modules/operations.mjs
  26. 11
      boa_examples/scripts/modules/trig.mjs
  27. 131
      boa_examples/src/bin/modules.rs
  28. 1
      boa_gc/src/cell.rs
  29. 2
      boa_gc/src/pointers/ephemeron.rs
  30. 3
      boa_gc/src/pointers/gc.rs
  31. 1
      boa_interner/Cargo.toml
  32. 9
      boa_interner/src/sym.rs
  33. 6
      boa_parser/src/parser/statement/declaration/export.rs
  34. 4
      boa_parser/src/parser/statement/declaration/import.rs
  35. 13
      boa_parser/src/parser/statement/declaration/mod.rs
  36. 27
      boa_parser/src/parser/statement/mod.rs
  37. 261
      boa_tester/src/exec/mod.rs

4
.gitignore vendored

@ -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

1
Cargo.lock generated

@ -492,6 +492,7 @@ name = "boa_interner"
version = "0.16.0"
dependencies = [
"arbitrary",
"boa_gc",
"boa_macros",
"hashbrown 0.13.2",
"indexmap",

114
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<IndirectExportEntry> for ExportEntry {
fn from(v: IndirectExportEntry) -> Self {
Self::ReExport(v)
}
}
impl From<LocalExportEntry> for ExportEntry {
fn from(v: LocalExportEntry) -> Self {
Self::Ordinary(v)
}
}
/// A local export entry
#[derive(Debug, Clone, Copy)]
pub struct LocalExportEntry {
local_name: Identifier,
export_name: Sym,
}
impl LocalExportEntry {
/// Creates a new `LocalExportEntry`.
#[must_use]
pub const fn new(local_name: Identifier, export_name: Sym) -> Self {
Self {
local_name,
export_name,
}
}
/// Gets the local name of this export entry.
#[must_use]
pub const fn local_name(&self) -> Identifier {
self.local_name
}
/// Gets the export name of this export entry.
#[must_use]
pub const fn export_name(&self) -> Sym {
self.export_name
}
}
/// A reexported export entry.
#[derive(Debug, Clone, Copy)]
pub struct IndirectExportEntry {
module_request: Sym,
import_name: ReExportImportName,
export_name: Sym,
}
impl IndirectExportEntry {
/// Creates a new `IndirectExportEntry`.
#[must_use]
pub const fn new(
module_request: Sym,
import_name: ReExportImportName,
export_name: Sym,
) -> Self {
Self {
module_request,
import_name,
export_name,
}
}
/// Gets the module from where this entry reexports.
#[must_use]
pub const fn module_request(&self) -> Sym {
self.module_request
}
/// Gets the import name of the reexport.
#[must_use]
pub const fn import_name(&self) -> ReExportImportName {
self.import_name
}
/// Gets the public alias of the reexport.
#[must_use]
pub const fn export_name(&self) -> Sym {
self.export_name
}
}

49
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
}
}

228
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<Sym> {
#[derive(Debug)]
struct RequestsVisitor<'vec>(&'vec mut FxHashSet<Sym>);
impl<'ast> Visitor<'ast> for RequestsVisitor<'_> {
type BreakTy = Infallible;
fn visit_statement_list_item(
&mut self,
_: &'ast StatementListItem,
) -> ControlFlow<Self::BreakTy> {
ControlFlow::Continue(())
}
fn visit_module_specifier(
&mut self,
node: &'ast ModuleSpecifier,
) -> ControlFlow<Self::BreakTy> {
self.0.insert(node.sym());
ControlFlow::Continue(())
}
}
let mut requests = FxHashSet::default();
RequestsVisitor(&mut requests).visit_module_item_list(self);
requests
}
/// Operation [`ImportEntries`][spec].
///
/// Gets the list of import entries of this module.
///
/// [spec]: https://tc39.es/ecma262/#sec-static-semantics-importentries
#[inline]
#[must_use]
pub fn import_entries(&self) -> Vec<ImportEntry> {
#[derive(Debug)]
struct ImportEntriesVisitor<'vec>(&'vec mut Vec<ImportEntry>);
impl<'ast> Visitor<'ast> for ImportEntriesVisitor<'_> {
type BreakTy = Infallible;
fn visit_module_item(&mut self, node: &'ast ModuleItem) -> ControlFlow<Self::BreakTy> {
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<Self::BreakTy> {
let module = node.specifier().sym();
if let Some(default) = node.default() {
self.0.push(ImportEntry::new(
module,
ImportName::Name(Sym::DEFAULT),
default,
));
}
match node.kind() {
ImportKind::DefaultOrUnnamed => {}
ImportKind::Namespaced { binding } => {
self.0
.push(ImportEntry::new(module, ImportName::Namespace, *binding));
}
ImportKind::Named { names } => {
for name in &**names {
self.0.push(ImportEntry::new(
module,
ImportName::Name(name.export_name()),
name.binding(),
));
}
}
}
ControlFlow::Continue(())
}
}
let mut entries = Vec::default();
ImportEntriesVisitor(&mut entries).visit_module_item_list(self);
entries
}
/// Operation [`ExportEntries`][spec].
///
/// Gets the list of export entries of this module.
///
/// [spec]: https://tc39.es/ecma262/#sec-static-semantics-exportentries
#[inline]
#[must_use]
pub fn export_entries(&self) -> Vec<ExportEntry> {
#[derive(Debug)]
struct ExportEntriesVisitor<'vec>(&'vec mut Vec<ExportEntry>);
impl<'ast> Visitor<'ast> for ExportEntriesVisitor<'_> {
type BreakTy = Infallible;
fn visit_module_item(&mut self, node: &'ast ModuleItem) -> ControlFlow<Self::BreakTy> {
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<Self::BreakTy> {
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<T> From<T> for ModuleItemList

50
boa_cli/src/main.rs

@ -64,12 +64,14 @@ mod helper;
use boa_ast::StatementList;
use boa_engine::{
builtins::promise::PromiseState,
context::ContextBuilder,
job::{FutureJob, JobQueue, NativeJob},
module::{Module, ModuleLoader, SimpleModuleLoader},
optimizer::OptimizerOptions,
property::Attribute,
vm::flowgraph::{Direction, Graph},
Context, JsResult, Source,
Context, JsNativeError, JsResult, Source,
};
use boa_runtime::Console;
use clap::{Parser, ValueEnum, ValueHint};
@ -155,6 +157,14 @@ struct Opt {
/// Inject debugging object `$boa`.
#[arg(long)]
debug_object: bool,
/// Treats the input files as modules.
#[arg(long, short = 'm', group = "mod")]
module: bool,
/// Root path from where the module resolver will try to load the modules.
#[arg(long, short = 'r', default_value_os_t = PathBuf::from("."), requires = "mod")]
root: PathBuf,
}
impl Opt {
@ -272,7 +282,11 @@ fn generate_flowgraph(
Ok(result)
}
fn evaluate_files(args: &Opt, context: &mut Context<'_>) -> Result<(), io::Error> {
fn evaluate_files(
args: &Opt,
context: &mut Context<'_>,
loader: &SimpleModuleLoader,
) -> Result<(), io::Error> {
for file in &args.files {
let buffer = read(file)?;
@ -290,6 +304,32 @@ fn evaluate_files(args: &Opt, context: &mut Context<'_>) -> Result<(), io::Error
Ok(v) => println!("{v}"),
Err(v) => eprintln!("Uncaught {v}"),
}
} else if args.module {
let result = (|| {
let module = Module::parse(Source::from_bytes(&buffer), None, context)?;
loader.insert(
file.canonicalize()
.map_err(|e| JsNativeError::typ().with_message(e.to_string()))?,
module.clone(),
);
let promise = module.load_link_evaluate(context)?;
context.run_jobs();
promise.state()
})();
match result {
Ok(PromiseState::Pending) => {
eprintln!("module `{}` didn't execute", file.display());
}
Ok(PromiseState::Fulfilled(_)) => {}
Ok(PromiseState::Rejected(err)) => {
eprintln!("Uncaught {}", err.display());
}
Err(err) => eprintln!("Uncaught {err}"),
}
} else {
match context.eval_script(Source::from_bytes(&buffer)) {
Ok(v) => println!("{}", v.display()),
@ -306,8 +346,12 @@ fn main() -> Result<(), io::Error> {
let args = Opt::parse();
let queue: &dyn JobQueue = &Jobs::default();
let loader = &SimpleModuleLoader::new(&args.root)
.map_err(|e| io::Error::new(io::ErrorKind::Other, e.to_string()))?;
let dyn_loader: &dyn ModuleLoader = loader;
let mut context = ContextBuilder::new()
.job_queue(queue)
.module_loader(dyn_loader)
.build()
.expect("cannot fail with default global object");
@ -404,7 +448,7 @@ fn main() -> Result<(), io::Error> {
.save_history(CLI_HISTORY)
.expect("could not save CLI history");
} else {
evaluate_files(&args, &mut context)?;
evaluate_files(&args, &mut context, loader)?;
}
Ok(())

14
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<JsString> {
// 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<JsString> {
// 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<JsObject>,
stack: Vec<JsValue>,
stack: Vec<JsObject>,
indent: JsString,
gap: JsString,
property_list: Option<Vec<JsString>>,

13
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`.

2
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()
}

52
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);
}
}
}
}
}

20
boa_engine/src/context/intrinsics.rs

@ -156,8 +156,11 @@ pub struct StandardConstructors {
impl Default for StandardConstructors {
fn default() -> Self {
Self {
object: StandardConstructor::with_prototype(JsObject::from_proto_and_data(
None,
ObjectData::object_prototype(),
)),
async_generator_function: StandardConstructor::default(),
object: StandardConstructor::default(),
proxy: StandardConstructor::default(),
date: StandardConstructor::default(),
function: StandardConstructor {
@ -996,6 +999,8 @@ pub(crate) struct ObjectTemplates {
function_without_proto: ObjectTemplate,
function_with_prototype_without_proto: ObjectTemplate,
namespace: ObjectTemplate,
}
impl ObjectTemplates {
@ -1101,6 +1106,9 @@ impl ObjectTemplates {
Attribute::WRITABLE | Attribute::CONFIGURABLE | Attribute::ENUMERABLE,
);
let mut namespace = ObjectTemplate::new(root_shape);
namespace.property(JsSymbol::to_string_tag().into(), Attribute::empty());
Self {
iterator_result,
ordinary_object,
@ -1118,6 +1126,7 @@ impl ObjectTemplates {
async_function,
function_without_proto,
function_with_prototype_without_proto,
namespace,
}
}
@ -1285,4 +1294,13 @@ impl ObjectTemplates {
pub(crate) const fn function_with_prototype_without_proto(&self) -> &ObjectTemplate {
&self.function_with_prototype_without_proto
}
/// Cached namespace object template.
///
/// Transitions:
///
/// 1. `@@toStringTag`: (`READONLY`, `NON_ENUMERABLE`, `PERMANENT`)
pub(crate) const fn namespace(&self) -> &ObjectTemplate {
&self.namespace
}
}

199
boa_engine/src/context/mod.rs

@ -14,30 +14,26 @@ pub use maybe_shared::MaybeShared;
#[cfg(not(feature = "intl"))]
pub use std::marker::PhantomData;
use std::{io::Read, rc::Rc};
use std::{io::Read, path::Path, rc::Rc};
use crate::{
builtins,
bytecompiler::{ByteCompiler, NodeKind},
bytecompiler::ByteCompiler,
class::{Class, ClassBuilder},
job::{JobQueue, NativeJob, SimpleJobQueue},
module::{ModuleLoader, SimpleModuleLoader},
native_function::NativeFunction,
object::{shape::SharedShape, FunctionObjectBuilder, JsObject},
optimizer::{Optimizer, OptimizerOptions, OptimizerStatistics},
property::{Attribute, PropertyDescriptor, PropertyKey},
realm::Realm,
vm::{CallFrame, CodeBlock, Opcode, Vm},
vm::{CallFrame, CodeBlock, Vm},
JsResult, JsValue, Source,
};
use boa_ast::{
declaration::LexicalDeclaration,
expression::Identifier,
operations::{bound_names, lexically_scoped_declarations, var_scoped_declarations},
Declaration, ModuleItemList, StatementList,
};
use boa_ast::{expression::Identifier, StatementList};
use boa_gc::Gc;
use boa_interner::{Interner, Sym};
use boa_parser::{Error as ParseError, Parser};
use boa_parser::Parser;
use boa_profiler::Profiler;
use crate::vm::RuntimeLimits;
@ -110,6 +106,8 @@ pub struct Context<'host> {
job_queue: MaybeShared<'host, dyn JobQueue>,
module_loader: MaybeShared<'host, dyn ModuleLoader>,
optimizer_options: OptimizerOptions,
root_shape: SharedShape,
@ -128,6 +126,7 @@ impl std::fmt::Debug for Context<'_> {
.field("strict", &self.strict)
.field("promise_job_queue", &"JobQueue")
.field("hooks", &"HostHooks")
.field("module_loader", &"ModuleLoader")
.field("optimizer_options", &self.optimizer_options);
#[cfg(feature = "intl")]
@ -150,7 +149,7 @@ impl<'host> Context<'host> {
/// Create a new [`ContextBuilder`] to specify the [`Interner`] and/or
/// the icu data provider.
#[must_use]
pub fn builder() -> ContextBuilder<'static, 'static, 'static> {
pub fn builder() -> ContextBuilder<'static, 'static, 'static, 'static> {
ContextBuilder::default()
}
@ -186,37 +185,6 @@ impl<'host> Context<'host> {
result
}
// TODO: remove `ignore` after we implement module execution
/// Evaluates the given module `src` by compiling down to bytecode, then interpreting the
/// bytecode into a value.
///
/// # Examples
/// ```ignore
/// # use boa_engine::{Context, Source};
/// let mut context = Context::default();
///
/// let source = Source::from_bytes("1 + 3");
///
/// let value = context.eval_module(source).unwrap();
///
/// assert!(value.is_number());
/// assert_eq!(value.as_number().unwrap(), 4.0);
/// ```
#[allow(clippy::unit_arg, clippy::drop_copy)]
pub fn eval_module<R: Read>(&mut self, src: Source<'_, R>) -> JsResult<JsValue> {
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<R: Read>(
&mut self,
src: Source<'_, R>,
) -> Result<StatementList, ParseError> {
pub fn parse_script<R: Read>(&mut self, src: Source<'_, R>) -> JsResult<StatementList> {
let _timer = Profiler::global().start_event("Script parsing", "Main");
let mut parser = Parser::new(src);
parser.set_identifier(self.next_parser_identifier());
@ -244,17 +209,6 @@ impl<'host> Context<'host> {
Ok(result)
}
/// Parse the given source script.
pub fn parse_module<R: Read>(
&mut self,
src: Source<'_, R>,
) -> Result<ModuleItemList, ParseError> {
let _timer = Profiler::global().start_event("Module parsing", "Main");
let mut parser = Parser::new(src);
parser.set_identifier(self.next_parser_identifier());
parser.parse_module(&mut self.interner)
}
/// Compile the script AST into a `CodeBlock` ready to be executed by the VM.
pub fn compile_script(&mut self, statement_list: &StatementList) -> JsResult<Gc<CodeBlock>> {
let _timer = Profiler::global().start_event("Script compilation", "Main");
@ -271,88 +225,12 @@ impl<'host> Context<'host> {
Ok(Gc::new(compiler.finish()))
}
/// Compile the module AST into a `CodeBlock` ready to be executed by the VM.
pub fn compile_module(&mut self, statement_list: &ModuleItemList) -> JsResult<Gc<CodeBlock>> {
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<CodeBlock>`, 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<CodeBlock>` returned by the [`Context::compile_script`] or [`Context::compile_module`]
/// functions.
/// `Gc<CodeBlock>` returned by the [`Context::compile_script`] function.
///
/// Note that this won't run any scheduled promise jobs; you need to call [`Context::run_jobs`]
/// on the context or [`JobQueue::run_jobs`] on the provided queue to run them.
@ -634,6 +512,11 @@ impl<'host> Context<'host> {
self.job_queue.clone()
}
/// Gets the module loader.
pub fn module_loader(&self) -> MaybeShared<'host, dyn ModuleLoader> {
self.module_loader.clone()
}
/// Get the [`RuntimeLimits`].
#[inline]
pub const fn runtime_limits(&self) -> RuntimeLimits {
@ -882,10 +765,11 @@ impl<'host> Context<'host> {
doc = "The required data in a valid provider is specified in [`BoaProvider`]"
)]
#[derive(Default)]
pub struct ContextBuilder<'icu, 'hooks, 'queue> {
pub struct ContextBuilder<'icu, 'hooks, 'queue, 'module> {
interner: Option<Interner>,
host_hooks: Option<MaybeShared<'hooks, dyn HostHooks>>,
job_queue: Option<MaybeShared<'queue, dyn JobQueue>>,
module_loader: Option<MaybeShared<'module, dyn ModuleLoader>>,
#[cfg(feature = "intl")]
icu: Option<icu::Icu<'icu>>,
#[cfg(not(feature = "intl"))]
@ -894,17 +778,24 @@ pub struct ContextBuilder<'icu, 'hooks, 'queue> {
instructions_remaining: usize,
}
impl std::fmt::Debug for ContextBuilder<'_, '_, '_> {
impl std::fmt::Debug for ContextBuilder<'_, '_, '_, '_> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
#[derive(Clone, Copy, Debug)]
struct JobQueue;
#[derive(Clone, Copy, Debug)]
struct HostHooks;
#[derive(Clone, Copy, Debug)]
struct ModuleLoader;
let mut out = f.debug_struct("ContextBuilder");
out.field("interner", &self.interner)
.field("host_hooks", &self.host_hooks.as_ref().map(|_| HostHooks))
.field("job_queue", &self.job_queue.as_ref().map(|_| JobQueue));
.field("job_queue", &self.job_queue.as_ref().map(|_| JobQueue))
.field(
"module_loader",
&self.module_loader.as_ref().map(|_| ModuleLoader),
);
#[cfg(feature = "intl")]
out.field("icu", &self.icu);
@ -916,7 +807,7 @@ impl std::fmt::Debug for ContextBuilder<'_, '_, '_> {
}
}
impl<'icu, 'hooks, 'queue> ContextBuilder<'icu, 'hooks, 'queue> {
impl<'icu, 'hooks, 'queue, 'module> ContextBuilder<'icu, 'hooks, 'queue, 'module> {
/// Creates a new [`ContextBuilder`] with a default empty [`Interner`]
/// and a default `BoaProvider` if the `intl` feature is enabled.
#[must_use]
@ -952,7 +843,7 @@ impl<'icu, 'hooks, 'queue> ContextBuilder<'icu, 'hooks, 'queue> {
pub fn icu_provider(
self,
provider: BoaProvider<'_>,
) -> Result<ContextBuilder<'_, 'hooks, 'queue>, IcuError> {
) -> Result<ContextBuilder<'_, 'hooks, 'queue, 'module>, IcuError> {
Ok(ContextBuilder {
icu: Some(icu::Icu::new(provider)?),
..self
@ -966,7 +857,7 @@ impl<'icu, 'hooks, 'queue> ContextBuilder<'icu, 'hooks, 'queue> {
pub fn host_hooks<'new_hooks, H>(
self,
host_hooks: H,
) -> ContextBuilder<'icu, 'new_hooks, 'queue>
) -> ContextBuilder<'icu, 'new_hooks, 'queue, 'module>
where
H: Into<MaybeShared<'new_hooks, dyn HostHooks>>,
{
@ -978,7 +869,10 @@ impl<'icu, 'hooks, 'queue> ContextBuilder<'icu, 'hooks, 'queue> {
/// Initializes the [`JobQueue`] for the context.
#[must_use]
pub fn job_queue<'new_queue, Q>(self, job_queue: Q) -> ContextBuilder<'icu, 'hooks, 'new_queue>
pub fn job_queue<'new_queue, Q>(
self,
job_queue: Q,
) -> ContextBuilder<'icu, 'hooks, 'new_queue, 'module>
where
Q: Into<MaybeShared<'new_queue, dyn JobQueue>>,
{
@ -988,6 +882,21 @@ impl<'icu, 'hooks, 'queue> ContextBuilder<'icu, 'hooks, 'queue> {
}
}
/// Initializes the [`ModuleLoader`] for the context.
#[must_use]
pub fn module_loader<'new_module, M>(
self,
module_loader: M,
) -> ContextBuilder<'icu, 'hooks, 'queue, 'new_module>
where
M: Into<MaybeShared<'new_module, dyn ModuleLoader>>,
{
ContextBuilder {
module_loader: Some(module_loader.into()),
..self
}
}
/// Specifies the number of instructions remaining to the [`Context`].
///
/// This function is only available if the `fuzz` feature is enabled.
@ -1005,6 +914,7 @@ impl<'icu, 'hooks, 'queue> ContextBuilder<'icu, 'hooks, 'queue> {
'icu: 'host,
'hooks: 'host,
'queue: 'host,
'module: 'host,
{
let root_shape = SharedShape::root();
@ -1034,6 +944,13 @@ impl<'icu, 'hooks, 'queue> ContextBuilder<'icu, 'hooks, 'queue> {
let queue: Rc<dyn JobQueue> = Rc::new(SimpleJobQueue::new());
queue.into()
}),
module_loader: self.module_loader.unwrap_or_else(|| {
let loader: Rc<dyn ModuleLoader> = 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,

23
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")
}
}
}
}

120
boa_engine/src/environments/runtime/declarative/module.rs

@ -0,0 +1,120 @@
use std::cell::Cell;
use boa_ast::expression::Identifier;
use boa_gc::{Finalize, GcRefCell, Trace};
use crate::{module::Module, JsValue};
/// Type of accessor used to access an indirect binding.
#[derive(Debug, Clone, Copy)]
enum BindingAccessor {
Identifier(Identifier),
Index(usize),
}
/// An indirect reference to a binding inside an environment.
#[derive(Clone, Debug, Trace, Finalize)]
struct IndirectBinding {
module: Module,
#[unsafe_ignore_trace]
accessor: Cell<BindingAccessor>,
}
/// The type of binding a [`ModuleEnvironment`] can contain.
#[derive(Clone, Debug, Trace, Finalize)]
enum BindingType {
Direct(Option<JsValue>),
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<Vec<BindingType>>,
}
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<JsValue> {
let bindings = self.bindings.borrow();
match &bindings[index] {
BindingType::Direct(v) => v.clone(),
BindingType::Indirect(IndirectBinding { module, accessor }) => {
let env = module.environment()?;
match accessor.get() {
BindingAccessor::Identifier(name) => {
let index = env
.compile_env()
.borrow()
.get_binding(name)
.expect("linking must ensure the binding exists");
let value = env.get(index.binding_index)?;
accessor.set(BindingAccessor::Index(index.binding_index));
Some(value)
}
BindingAccessor::Index(index) => env.get(index),
}
}
}
}
/// Sets the binding value from the environment by index.
///
/// # Panics
///
/// Panics if the binding value is out of range.
#[track_caller]
pub(crate) fn set(&self, index: usize, value: JsValue) {
let mut bindings = self.bindings.borrow_mut();
match &mut bindings[index] {
BindingType::Direct(v) => *v = Some(value),
BindingType::Indirect(_) => {
panic!("cannot modify indirect references to other environments")
}
}
}
/// Creates an indirect binding reference to another environment binding.
///
/// # Panics
///
/// Panics if the binding value is out of range.
#[track_caller]
pub(crate) fn set_indirect(
&self,
index: usize,
target_module: Module,
target_binding: Identifier,
) {
let mut bindings = self.bindings.borrow_mut();
bindings[index] = BindingType::Indirect(IndirectBinding {
module: target_module,
accessor: Cell::new(BindingAccessor::Identifier(target_binding)),
});
}
}

22
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<GcRefCell<CompileTimeEnvironment>>,
) {
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")

3
boa_engine/src/lib.rs

@ -133,6 +133,7 @@ pub mod context;
pub mod environments;
pub mod error;
pub mod job;
pub mod module;
pub mod native_function;
pub mod object;
pub mod optimizer;
@ -151,6 +152,7 @@ pub mod vm;
pub mod prelude {
pub use crate::{
error::{JsError, JsNativeError, JsNativeErrorKind},
module::Module,
native_function::NativeFunction,
object::JsObject,
Context, JsBigInt, JsResult, JsString, JsValue,
@ -166,6 +168,7 @@ pub use crate::{
bigint::JsBigInt,
context::Context,
error::{JsError, JsNativeError, JsNativeErrorKind},
module::Module,
native_function::NativeFunction,
object::JsObject,
string::JsString,

737
boa_engine/src/module/mod.rs

@ -0,0 +1,737 @@
//! Boa's implementation of the ECMAScript's module system.
//!
//! This module contains the [`Module`] type, which represents an [**Abstract Module Record**][module],
//! a [`ModuleLoader`] trait for custom module loader implementations, and [`SimpleModuleLoader`],
//! the default `ModuleLoader` for [`Context`] which can be used for most simple usecases.
//!
//! Every module roughly follows the same lifecycle:
//! - Parse using [`Module::parse`].
//! - Load all its dependencies using [`Module::load`].
//! - Link its dependencies together using [`Module::link`].
//! - Evaluate the module and its dependencies using [`Module::evaluate`].
//!
//! The [`ModuleLoader`] trait allows customizing the "load" step on the lifecycle
//! of a module, which allows doing things like fetching modules from urls, having multiple
//! "modpaths" from where to import modules, or using Rust futures to avoid blocking the main thread
//! on loads.
//!
//! More information:
//! - [ECMAScript reference][spec]
//!
//! [spec]: https://tc39.es/ecma262/#sec-modules
//! [module]: https://tc39.es/ecma262/#sec-abstract-module-records
mod source;
use source::SourceTextModule;
use std::cell::{Cell, RefCell};
use std::hash::Hash;
use std::io::Read;
use std::path::{Path, PathBuf};
use std::rc::Rc;
use std::{collections::HashSet, hash::BuildHasherDefault};
use indexmap::IndexMap;
use rustc_hash::{FxHashMap, FxHashSet, FxHasher};
use boa_ast::expression::Identifier;
use boa_gc::{Finalize, Gc, GcRefCell, Trace};
use boa_interner::Sym;
use boa_parser::{Parser, Source};
use boa_profiler::Profiler;
use crate::object::FunctionObjectBuilder;
use crate::{
builtins::promise::{PromiseCapability, PromiseState},
environments::DeclarativeEnvironment,
object::{JsObject, JsPromise, ObjectData},
realm::Realm,
Context, JsError, JsResult, JsString, JsValue,
};
use crate::{js_string, JsNativeError, NativeFunction};
/// The referrer from which a load request of a module originates.
#[derive(Debug)]
pub enum Referrer {
/// A [**Source Text Module Record**](https://tc39.es/ecma262/#sec-source-text-module-records).
Module(Module),
/// A [**Realm**](https://tc39.es/ecma262/#sec-code-realms).
Realm(Realm), // TODO: script
}
/// Module loading related host hooks.
///
/// This trait allows to customize the behaviour of the engine on module load requests and
/// `import.meta` requests.
pub trait ModuleLoader {
/// Host hook [`HostLoadImportedModule ( referrer, specifier, hostDefined, payload )`][spec].
///
/// This hook allows to customize the module loading functionality of the engine. Technically,
/// this should call the [`FinishLoadingImportedModule`][finish] operation, but this simpler API just provides
/// a closure that replaces `FinishLoadingImportedModule`.
///
/// # Requirements
///
/// - The host environment must perform `FinishLoadingImportedModule(referrer, specifier, payload, result)`,
/// where result is either a normal completion containing the loaded Module Record or a throw
/// completion, either synchronously or asynchronously. This is equivalent to calling the `finish_load`
/// callback.
/// - If this operation is called multiple times with the same `(referrer, specifier)` pair and
/// it performs FinishLoadingImportedModule(referrer, specifier, payload, result) where result
/// is a normal completion, then it must perform
/// `FinishLoadingImportedModule(referrer, specifier, payload, result)` with the same result each
/// time.
/// - The operation must treat payload as an opaque value to be passed through to
/// `FinishLoadingImportedModule`. (can be ignored)
///
/// [spec]: https://tc39.es/ecma262/#sec-HostLoadImportedModule
/// [finish]: https://tc39.es/ecma262/#sec-FinishLoadingImportedModule
#[allow(clippy::type_complexity)]
fn load_imported_module(
&self,
referrer: Referrer,
specifier: JsString,
finish_load: Box<dyn FnOnce(JsResult<Module>, &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<FxHashMap<PathBuf, Module>>,
}
impl SimpleModuleLoader {
/// Creates a new `SimpleModuleLoader` from a root module path.
pub fn new<P: AsRef<Path>>(root: P) -> JsResult<Self> {
let root = root.as_ref();
let absolute = root.canonicalize().map_err(|e| {
JsNativeError::typ()
.with_message(format!("could not set module root `{}`", root.display()))
.with_cause(JsError::from_opaque(js_string!(e.to_string()).into()))
})?;
Ok(Self {
root: absolute,
module_map: GcRefCell::default(),
})
}
/// Inserts a new module onto the module map.
#[inline]
pub fn insert(&self, path: PathBuf, module: Module) {
self.module_map.borrow_mut().insert(path, module);
}
/// Gets a module from its original path.
#[inline]
pub fn get(&self, path: &Path) -> Option<Module> {
self.module_map.borrow().get(path).cloned()
}
}
impl ModuleLoader for SimpleModuleLoader {
fn load_imported_module(
&self,
_referrer: Referrer,
specifier: JsString,
finish_load: Box<dyn FnOnce(JsResult<Module>, &mut Context<'_>)>,
context: &mut Context<'_>,
) {
let result = (|| {
let path = specifier
.to_std_string()
.map_err(|err| JsNativeError::typ().with_message(err.to_string()))?;
let short_path = Path::new(&path);
let path = self.root.join(short_path);
let path = path.canonicalize().map_err(|err| {
JsNativeError::typ()
.with_message(format!(
"could not canonicalize path `{}`",
short_path.display()
))
.with_cause(JsError::from_opaque(js_string!(err.to_string()).into()))
})?;
if let Some(module) = self.get(&path) {
return Ok(module);
}
let source = Source::from_filepath(&path).map_err(|err| {
JsNativeError::typ()
.with_message(format!("could not open file `{}`", short_path.display()))
.with_cause(JsError::from_opaque(js_string!(err.to_string()).into()))
})?;
let module = Module::parse(source, None, context).map_err(|err| {
JsNativeError::error()
.with_message(format!("could not parse module `{}`", short_path.display()))
.with_cause(err)
})?;
self.insert(path, module.clone());
Ok(module)
})();
finish_load(result, context);
}
}
/// ECMAScript's [**Abstract module record**][spec].
///
/// [spec]: https://tc39.es/ecma262/#sec-abstract-module-records
#[derive(Clone, Trace, Finalize)]
pub struct Module {
inner: Gc<Inner>,
}
impl std::fmt::Debug for Module {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Module")
.field("realm", &self.inner.realm.addr())
.field("environment", &self.inner.environment)
.field("namespace", &self.inner.namespace)
.field("kind", &self.inner.kind)
.field("host_defined", &self.inner.host_defined)
.finish()
}
}
#[derive(Trace, Finalize)]
struct Inner {
realm: Realm,
environment: GcRefCell<Option<Gc<DeclarativeEnvironment>>>,
namespace: GcRefCell<Option<JsObject>>,
kind: ModuleKind,
host_defined: (),
}
/// The kind of a [`Module`].
#[derive(Debug, Trace, Finalize)]
pub(crate) enum ModuleKind {
/// A [**Source Text Module Record**](https://tc39.es/ecma262/#sec-source-text-module-records)
SourceText(SourceTextModule),
/// A [**Synthetic Module Record**](https://tc39.es/proposal-json-modules/#sec-synthetic-module-records)
#[allow(unused)]
Synthetic,
}
/// Return value of the [`Module::resolve_export`] operation.
///
/// Indicates how to access a specific export in a module.
#[derive(Debug, Clone)]
pub(crate) struct ResolvedBinding {
module: Module,
binding_name: BindingName,
}
/// The local name of the resolved binding within its containing module.
///
/// Note that a resolved binding can resolve to a single binding inside a module (`export var a = 1"`)
/// or to a whole module namespace (`export * as ns from "mod.js"`).
#[derive(Debug, Clone, Copy)]
pub(crate) enum BindingName {
/// A local binding.
Name(Identifier),
/// The whole namespace of the containing module.
Namespace,
}
impl ResolvedBinding {
/// Gets the module from which the export resolved.
pub(crate) const fn module(&self) -> &Module {
&self.module
}
/// Gets the binding associated with the resolved export.
pub(crate) const fn binding_name(&self) -> BindingName {
self.binding_name
}
}
#[derive(Debug, Clone)]
struct GraphLoadingState {
capability: PromiseCapability,
loading: Cell<bool>,
pending_modules: Cell<usize>,
visited: RefCell<HashSet<SourceTextModule>>,
}
#[derive(Debug, Clone, Copy)]
pub(crate) enum ResolveExportError {
NotFound,
Ambiguous,
}
impl Module {
/// Abstract operation [`ParseModule ( sourceText, realm, hostDefined )`][spec].
///
/// Parses the provided `src` as an ECMAScript module, returning an error if parsing fails.
///
/// [spec]: https://tc39.es/ecma262/#sec-parsemodule
#[inline]
pub fn parse<R: Read>(
src: Source<'_, R>,
realm: Option<Realm>,
context: &mut Context<'_>,
) -> JsResult<Module> {
let _timer = Profiler::global().start_event("Module parsing", "Main");
let mut parser = Parser::new(src);
parser.set_identifier(context.next_parser_identifier());
let module = parser.parse_module(context.interner_mut())?;
let src = SourceTextModule::new(module);
let module = Module {
inner: Gc::new(Inner {
realm: realm.unwrap_or_else(|| context.realm().clone()),
environment: GcRefCell::default(),
namespace: GcRefCell::default(),
kind: ModuleKind::SourceText(src.clone()),
host_defined: (),
}),
};
src.set_parent(module.clone());
Ok(module)
}
/// Gets the realm of this `Module`.
#[inline]
pub fn realm(&self) -> &Realm {
&self.inner.realm
}
/// Gets the kind of this `Module`.
pub(crate) fn kind(&self) -> &ModuleKind {
&self.inner.kind
}
/// Gets the environment of this `Module`.
pub(crate) fn environment(&self) -> Option<Gc<DeclarativeEnvironment>> {
self.inner.environment.borrow().clone()
}
/// Abstract method [`LoadRequestedModules ( [ hostDefined ] )`][spec].
///
/// Prepares the module for linking by loading all its module dependencies. Returns a `JsPromise`
/// that will resolve when the loading process either completes or fails.
///
/// [spec]: https://tc39.es/ecma262/#table-abstract-methods-of-module-records
#[allow(clippy::missing_panics_doc)]
#[inline]
pub fn load(&self, context: &mut Context<'_>) -> JsPromise {
match self.kind() {
ModuleKind::SourceText(_) => {
// Concrete method [`LoadRequestedModules ( [ hostDefined ] )`][spec].
//
// [spec]: https://tc39.es/ecma262/#sec-LoadRequestedModules
// TODO: 1. If hostDefined is not present, let hostDefined be empty.
// 2. Let pc be ! NewPromiseCapability(%Promise%).
let pc = PromiseCapability::new(
&context.intrinsics().constructors().promise().constructor(),
context,
)
.expect(
"capability creation must always succeed when using the `%Promise%` intrinsic",
);
// 4. Perform InnerModuleLoading(state, module).
self.inner_load(
// 3. Let state be the GraphLoadingState Record {
// [[IsLoading]]: true, [[PendingModulesCount]]: 1, [[Visited]]: « »,
// [[PromiseCapability]]: pc, [[HostDefined]]: hostDefined
// }.
&Rc::new(GraphLoadingState {
capability: pc.clone(),
loading: Cell::new(true),
pending_modules: Cell::new(1),
visited: RefCell::default(),
}),
context,
);
// 5. Return pc.[[Promise]].
JsPromise::from_object(pc.promise().clone())
.expect("promise created from the %Promise% intrinsic is always native")
}
ModuleKind::Synthetic => todo!("synthetic.load()"),
}
}
/// Abstract operation [`InnerModuleLoading`][spec].
///
/// [spec]: https://tc39.es/ecma262/#sec-InnerModuleLoading
fn inner_load(&self, state: &Rc<GraphLoadingState>, context: &mut Context<'_>) {
// 1. Assert: state.[[IsLoading]] is true.
assert!(state.loading.get());
if let ModuleKind::SourceText(src) = self.kind() {
// continues on `inner_load
src.inner_load(state, context);
if !state.loading.get() {
return;
}
}
// 3. Assert: state.[[PendingModulesCount]] ≥ 1.
assert!(state.pending_modules.get() >= 1);
// 4. Set state.[[PendingModulesCount]] to state.[[PendingModulesCount]] - 1.
state.pending_modules.set(state.pending_modules.get() - 1);
// 5. If state.[[PendingModulesCount]] = 0, then
if state.pending_modules.get() == 0 {
// a. Set state.[[IsLoading]] to false.
state.loading.set(false);
// b. For each Cyclic Module Record loaded of state.[[Visited]], do
// i. If loaded.[[Status]] is new, set loaded.[[Status]] to unlinked.
// By default, all modules start on `unlinked`.
// c. Perform ! Call(state.[[PromiseCapability]].[[Resolve]], undefined, « undefined »).
state
.capability
.resolve()
.call(&JsValue::undefined(), &[], context)
.expect("marking a module as loaded should not fail");
}
// 6. Return unused.
}
/// Abstract method [`GetExportedNames([exportStarSet])`][spec].
///
/// Returns a list of all the names exported from this module.
///
/// # Note
///
/// This must only be called if the [`JsPromise`] returned by [`Module::load`] has fulfilled.
///
/// [spec]: https://tc39.es/ecma262/#table-abstract-methods-of-module-records
fn get_exported_names(&self, export_star_set: &mut Vec<SourceTextModule>) -> FxHashSet<Sym> {
match self.kind() {
ModuleKind::SourceText(src) => src.get_exported_names(export_star_set),
ModuleKind::Synthetic => todo!("synthetic.get_exported_names()"),
}
}
/// Abstract method [`ResolveExport(exportName [, resolveSet])`][spec].
///
/// Returns the corresponding local binding of a binding exported by this module.
/// The spec requires that this operation must be idempotent; calling this multiple times
/// with the same `export_name` and `resolve_set` should always return the same result.
///
/// # Note
///
/// This must only be called if the [`JsPromise`] returned by [`Module::load`] has fulfilled.
///
/// [spec]: https://tc39.es/ecma262/#table-abstract-methods-of-module-records
#[allow(clippy::mutable_key_type)]
pub(crate) fn resolve_export(
&self,
export_name: Sym,
resolve_set: &mut FxHashSet<(Module, Sym)>,
) -> Result<ResolvedBinding, ResolveExportError> {
match self.kind() {
ModuleKind::SourceText(src) => src.resolve_export(export_name, resolve_set),
ModuleKind::Synthetic => todo!("synthetic.resolve_export()"),
}
}
/// Abstract method [`Link() `][spec].
///
/// Prepares this module for evaluation by resolving all its module dependencies and initializing
/// its environment.
///
/// # Note
///
/// This must only be called if the [`JsPromise`] returned by [`Module::load`] has fulfilled.
///
/// [spec]: https://tc39.es/ecma262/#table-abstract-methods-of-module-records
#[allow(clippy::missing_panics_doc)]
#[inline]
pub fn link(&self, context: &mut Context<'_>) -> JsResult<()> {
match self.kind() {
ModuleKind::SourceText(src) => src.link(context),
ModuleKind::Synthetic => todo!("synthetic.link()"),
}
}
/// Abstract operation [`InnerModuleLinking ( module, stack, index )`][spec].
///
/// [spec]: https://tc39.es/ecma262/#sec-InnerModuleLinking
fn inner_link(
&self,
stack: &mut Vec<SourceTextModule>,
index: usize,
context: &mut Context<'_>,
) -> JsResult<usize> {
match self.kind() {
ModuleKind::SourceText(src) => src.inner_link(stack, index, context),
#[allow(unreachable_code)]
// If module is not a Cyclic Module Record, then
ModuleKind::Synthetic => {
// a. Perform ? module.Link().
todo!("synthetic.link()");
// b. Return index.
Ok(index)
}
}
}
/// Abstract method [`Evaluate()`][spec].
///
/// Evaluates this module, returning a promise for the result of the evaluation of this module
/// and its dependencies.
/// If the promise is rejected, hosts are expected to handle the promise rejection and rethrow
/// the evaluation error.
///
/// # Note
///
/// This must only be called if the [`Module::link`] method finished successfully.
///
/// [spec]: https://tc39.es/ecma262/#table-abstract-methods-of-module-records
#[allow(clippy::missing_panics_doc)]
#[inline]
pub fn evaluate(&self, context: &mut Context<'_>) -> JsPromise {
match self.kind() {
ModuleKind::SourceText(src) => src.evaluate(context),
ModuleKind::Synthetic => todo!("synthetic.evaluate()"),
}
}
/// Abstract operation [`InnerModuleLinking ( module, stack, index )`][spec].
///
/// [spec]: https://tc39.es/ecma262/#sec-InnerModuleLinking
fn inner_evaluate(
&self,
stack: &mut Vec<SourceTextModule>,
index: usize,
context: &mut Context<'_>,
) -> JsResult<usize> {
match self.kind() {
ModuleKind::SourceText(src) => src.inner_evaluate(stack, index, None, context),
// 1. If module is not a Cyclic Module Record, then
#[allow(unused, clippy::diverging_sub_expression)]
ModuleKind::Synthetic => {
// a. Let promise be ! module.Evaluate().
let promise: JsPromise = todo!("module.Evaluate()");
let state = promise.state()?;
match state {
PromiseState::Pending => {
unreachable!("b. Assert: promise.[[PromiseState]] is not pending.")
}
// d. Return index.
PromiseState::Fulfilled(_) => Ok(index),
// c. If promise.[[PromiseState]] is rejected, then
// i. Return ThrowCompletion(promise.[[PromiseResult]]).
PromiseState::Rejected(err) => Err(JsError::from_opaque(err)),
}
}
}
}
/// Loads, links and evaluates this module, returning a promise that will resolve after the module
/// finishes its lifecycle.
///
/// # Examples
/// ```
/// # use std::path::Path;
/// # use boa_engine::{Context, Source, Module, JsValue};
/// # use boa_engine::builtins::promise::PromiseState;
/// # use boa_engine::module::{ModuleLoader, SimpleModuleLoader};
/// let loader = &SimpleModuleLoader::new(Path::new(".")).unwrap();
/// let dyn_loader: &dyn ModuleLoader = loader;
/// let mut context = &mut Context::builder().module_loader(dyn_loader).build().unwrap();
///
/// let source = Source::from_bytes("1 + 3");
///
/// let module = Module::parse(source, None, context).unwrap();
///
/// loader.insert(Path::new("main.mjs").to_path_buf(), module.clone());
///
/// let promise = module.load_link_evaluate(context).unwrap();
///
/// context.run_jobs();
///
/// assert_eq!(promise.state().unwrap(), PromiseState::Fulfilled(JsValue::undefined()));
/// ```
#[allow(clippy::drop_copy)]
#[inline]
pub fn load_link_evaluate(&self, context: &mut Context<'_>) -> JsResult<JsPromise> {
let main_timer = Profiler::global().start_event("Module evaluation", "Main");
let promise = self
.load(context)
.then(
Some(
FunctionObjectBuilder::new(
context,
NativeFunction::from_copy_closure_with_captures(
|_, _, module, context| {
module.link(context)?;
Ok(JsValue::undefined())
},
self.clone(),
),
)
.build(),
),
None,
context,
)?
.then(
Some(
FunctionObjectBuilder::new(
context,
NativeFunction::from_copy_closure_with_captures(
|_, _, module, context| Ok(module.evaluate(context).into()),
self.clone(),
),
)
.build(),
),
None,
context,
)?;
// The main_timer needs to be dropped before the Profiler is.
drop(main_timer);
Profiler::global().drop();
Ok(promise)
}
/// Abstract operation [`GetModuleNamespace ( module )`][spec].
///
/// Gets the [**Module Namespace Object**][ns] that represents this module's exports.
///
/// [spec]: https://tc39.es/ecma262/#sec-getmodulenamespace
/// [ns]: https://tc39.es/ecma262/#sec-module-namespace-exotic-objects
pub fn namespace(&self, context: &mut Context<'_>) -> JsObject {
// 1. Assert: If module is a Cyclic Module Record, then module.[[Status]] is not new or unlinked.
// 2. Let namespace be module.[[Namespace]].
// 3. If namespace is empty, then
// 4. Return namespace.
self.inner
.namespace
.borrow_mut()
.get_or_insert_with(|| {
// a. Let exportedNames be module.GetExportedNames().
let exported_names = self.get_exported_names(&mut Vec::default());
// b. Let unambiguousNames be a new empty List.
let unambiguous_names = exported_names
.into_iter()
// c. For each element name of exportedNames, do
.filter_map(|name| {
// i. Let resolution be module.ResolveExport(name).
// ii. If resolution is a ResolvedBinding Record, append name to unambiguousNames.
self.resolve_export(name, &mut HashSet::default())
.ok()
.map(|_| name)
})
.collect();
// d. Set namespace to ModuleNamespaceCreate(module, unambiguousNames).
ModuleNamespace::create(self.clone(), unambiguous_names, context)
})
.clone()
}
}
impl PartialEq for Module {
#[inline]
fn eq(&self, other: &Self) -> bool {
std::ptr::eq(self.inner.as_ref(), other.inner.as_ref())
}
}
impl Eq for Module {}
impl Hash for Module {
#[inline]
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
std::ptr::hash(self.inner.as_ref(), state);
}
}
/// Module namespace exotic object.
///
/// Exposes the bindings exported by a [`Module`] to be accessed from ECMAScript code.
#[derive(Debug, Trace, Finalize)]
pub struct ModuleNamespace {
module: Module,
#[unsafe_ignore_trace]
exports: IndexMap<JsString, Sym, BuildHasherDefault<FxHasher>>,
}
impl ModuleNamespace {
/// Abstract operation [`ModuleNamespaceCreate ( module, exports )`][spec].
///
/// [spec]: https://tc39.es/ecma262/#sec-modulenamespacecreate
pub(crate) fn create(module: Module, names: Vec<Sym>, context: &mut Context<'_>) -> JsObject {
// 1. Assert: module.[[Namespace]] is empty.
// ignored since this is ensured by `Module::namespace`.
// 6. Let sortedExports be a List whose elements are the elements of exports ordered as if an Array of the same values had been sorted using %Array.prototype.sort% using undefined as comparefn.
let mut exports = names
.into_iter()
.map(|sym| {
(
context
.interner()
.resolve_expect(sym)
.into_common::<JsString>(false),
sym,
)
})
.collect::<IndexMap<_, _, _>>();
exports.sort_keys();
// 2. Let internalSlotsList be the internal slots listed in Table 32.
// 3. Let M be MakeBasicObject(internalSlotsList).
// 4. Set M's essential internal methods to the definitions specified in 10.4.6.
// 5. Set M.[[Module]] to module.
// 7. Set M.[[Exports]] to sortedExports.
// 8. Create own properties of M corresponding to the definitions in 28.3.
let namespace = context.intrinsics().templates().namespace().create(
ObjectData::module_namespace(ModuleNamespace { module, exports }),
vec![js_string!("Module").into()],
);
// 9. Set module.[[Namespace]] to M.
// Ignored because this is done by `Module::namespace`
// 10. Return M.
namespace
}
/// Gets the export names of the Module Namespace object.
pub(crate) const fn exports(&self) -> &IndexMap<JsString, Sym, BuildHasherDefault<FxHasher>> {
&self.exports
}
/// Gest the module associated with this Module Namespace object.
pub(crate) const fn module(&self) -> &Module {
&self.module
}
}

1913
boa_engine/src/module/source.rs

File diff suppressed because it is too large Load Diff

39
boa_engine/src/object/internal_methods/immutable_prototype.rs

@ -0,0 +1,39 @@
use crate::{
object::{JsObject, JsPrototype},
Context, JsResult,
};
use super::{InternalObjectMethods, ORDINARY_INTERNAL_METHODS};
/// Definitions of the internal object methods for [**Immutable Prototype Exotic Objects**][spec].
///
/// [spec]: https://tc39.es/ecma262/#sec-immutable-prototype-exotic-objects
pub(crate) static IMMUTABLE_PROTOTYPE_EXOTIC_INTERNAL_METHODS: InternalObjectMethods =
InternalObjectMethods {
__set_prototype_of__: immutable_prototype_exotic_set_prototype_of,
..ORDINARY_INTERNAL_METHODS
};
/// [`[[SetPrototypeOf]] ( V )`][spec].
///
/// [spec]: https://tc39.es/ecma262/#sec-immutable-prototype-exotic-objects-setprototypeof-v
#[allow(clippy::needless_pass_by_value)]
pub(crate) fn immutable_prototype_exotic_set_prototype_of(
obj: &JsObject,
val: JsPrototype,
context: &mut Context<'_>,
) -> JsResult<bool> {
// 1. Return ? SetImmutablePrototype(O, V).
// inlined since other implementations can just use `set_prototype_of` directly.
// SetImmutablePrototype ( O, V )
// <https://tc39.es/ecma262/#sec-set-immutable-prototype>
// 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)
}

2
boa_engine/src/object/internal_methods/mod.rs

@ -19,7 +19,9 @@ pub(super) mod arguments;
pub(super) mod array;
pub(super) mod bound_function;
pub(super) mod function;
pub(super) mod immutable_prototype;
pub(super) mod integer_indexed;
pub(super) mod module_namespace;
pub(super) mod proxy;
pub(super) mod string;

329
boa_engine/src/object/internal_methods/module_namespace.rs

@ -0,0 +1,329 @@
use std::collections::HashSet;
use crate::{
module::BindingName,
object::{JsObject, JsPrototype},
property::{PropertyDescriptor, PropertyKey},
Context, JsNativeError, JsResult, JsValue,
};
use super::{
immutable_prototype, ordinary_define_own_property, ordinary_delete, ordinary_get,
ordinary_get_own_property, ordinary_has_property, ordinary_own_property_keys,
InternalObjectMethods, ORDINARY_INTERNAL_METHODS,
};
/// Definitions of the internal object methods for [**Module Namespace Exotic Objects**][spec].
///
/// [spec]: https://tc39.es/ecma262/#sec-module-namespace-exotic-objects
pub(crate) static MODULE_NAMESPACE_EXOTIC_INTERNAL_METHODS: InternalObjectMethods =
InternalObjectMethods {
__get_prototype_of__: module_namespace_exotic_get_prototype_of,
__set_prototype_of__: module_namespace_exotic_set_prototype_of,
__is_extensible__: module_namespace_exotic_is_extensible,
__prevent_extensions__: module_namespace_exotic_prevent_extensions,
__get_own_property__: module_namespace_exotic_get_own_property,
__define_own_property__: module_namespace_exotic_define_own_property,
__has_property__: module_namespace_exotic_has_property,
__get__: module_namespace_exotic_get,
__set__: module_namespace_exotic_set,
__delete__: module_namespace_exotic_delete,
__own_property_keys__: module_namespace_exotic_own_property_keys,
..ORDINARY_INTERNAL_METHODS
};
/// [`[[GetPrototypeOf]] ( )`][spec].
///
/// [spec]: https://tc39.es/ecma262/#sec-module-namespace-exotic-objects-getprototypeof
#[allow(clippy::unnecessary_wraps)]
fn module_namespace_exotic_get_prototype_of(
_: &JsObject,
_: &mut Context<'_>,
) -> JsResult<JsPrototype> {
// 1. Return null.
Ok(None)
}
/// [`[[SetPrototypeOf]] ( V )`][spec].
///
/// [spec]: https://tc39.es/ecma262/#sec-module-namespace-exotic-objects-setprototypeof-v
#[allow(clippy::unnecessary_wraps)]
fn module_namespace_exotic_set_prototype_of(
obj: &JsObject,
val: JsPrototype,
context: &mut Context<'_>,
) -> JsResult<bool> {
// 1. Return ! SetImmutablePrototype(O, V).
Ok(
immutable_prototype::immutable_prototype_exotic_set_prototype_of(obj, val, context)
.expect("this must not fail per the spec"),
)
}
/// [`[[IsExtensible]] ( )`][spec].
///
/// [spec]: https://tc39.es/ecma262/#sec-module-namespace-exotic-objects-isextensible
#[allow(clippy::unnecessary_wraps)]
fn module_namespace_exotic_is_extensible(_: &JsObject, _: &mut Context<'_>) -> JsResult<bool> {
// 1. Return false.
Ok(false)
}
/// [`[[PreventExtensions]] ( )`][spec].
///
/// [spec]: https://tc39.es/ecma262/#sec-module-namespace-exotic-objects-preventextensions
#[allow(clippy::unnecessary_wraps)]
fn module_namespace_exotic_prevent_extensions(_: &JsObject, _: &mut Context<'_>) -> JsResult<bool> {
Ok(true)
}
/// [`[[GetOwnProperty]] ( P )`][spec]
///
/// [spec]: https://tc39.es/ecma262/#sec-module-namespace-exotic-objects-getownproperty-p
fn module_namespace_exotic_get_own_property(
obj: &JsObject,
key: &PropertyKey,
context: &mut Context<'_>,
) -> JsResult<Option<PropertyDescriptor>> {
// 1. If P is a Symbol, return OrdinaryGetOwnProperty(O, P).
let key = match key {
PropertyKey::Symbol(_) => return ordinary_get_own_property(obj, key, context),
PropertyKey::Index(_) => return Ok(None),
PropertyKey::String(s) => s,
};
{
let obj = obj.borrow();
let obj = obj
.as_module_namespace()
.expect("internal method can only be called on module namespace objects");
// 2. Let exports be O.[[Exports]].
let exports = obj.exports();
// 3. If exports does not contain P, return undefined.
if !exports.contains_key(key) {
return Ok(None);
}
}
// 4. Let value be ? O.[[Get]](P, O).
let value = obj.get(key.clone(), context)?;
// 5. Return PropertyDescriptor { [[Value]]: value, [[Writable]]: true, [[Enumerable]]: true, [[Configurable]]: false }.
Ok(Some(
PropertyDescriptor::builder()
.value(value)
.writable(true)
.enumerable(true)
.configurable(false)
.build(),
))
}
/// [`[[DefineOwnProperty]] ( P, Desc )`][spec]
///
/// [spec]: https://tc39.es/ecma262/#sec-module-namespace-exotic-objects-defineownproperty-p-desc
fn module_namespace_exotic_define_own_property(
obj: &JsObject,
key: &PropertyKey,
desc: PropertyDescriptor,
context: &mut Context<'_>,
) -> JsResult<bool> {
// 1. If P is a Symbol, return ! OrdinaryDefineOwnProperty(O, P, Desc).
if let PropertyKey::Symbol(_) = key {
return ordinary_define_own_property(obj, key, desc, context);
}
// 2. Let current be ? O.[[GetOwnProperty]](P).
let Some(current) = obj.__get_own_property__(key, context)? else {
// 3. If current is undefined, return false.
return Ok(false);
};
// 4. If Desc has a [[Configurable]] field and Desc.[[Configurable]] is true, return false.
// 5. If Desc has an [[Enumerable]] field and Desc.[[Enumerable]] is false, return false.
// 6. If IsAccessorDescriptor(Desc) is true, return false.
// 7. If Desc has a [[Writable]] field and Desc.[[Writable]] is false, return false.
if desc.configurable() == Some(true)
|| desc.enumerable() == Some(false)
|| desc.is_accessor_descriptor()
|| desc.writable() == Some(false)
{
return Ok(false);
}
// 8. If Desc has a [[Value]] field, return SameValue(Desc.[[Value]], current.[[Value]]).
// 9. Return true.
Ok(desc.value().map_or(true, |v| v == current.expect_value()))
}
/// [`[[HasProperty]] ( P )`][spec]
///
/// [spec]: https://tc39.es/ecma262/#sec-module-namespace-exotic-objects-hasproperty-p
fn module_namespace_exotic_has_property(
obj: &JsObject,
key: &PropertyKey,
context: &mut Context<'_>,
) -> JsResult<bool> {
// 1. If P is a Symbol, return ! OrdinaryHasProperty(O, P).
let key = match key {
PropertyKey::Symbol(_) => return ordinary_has_property(obj, key, context),
PropertyKey::Index(_) => return Ok(false),
PropertyKey::String(s) => s,
};
let obj = obj.borrow();
let obj = obj
.as_module_namespace()
.expect("internal method can only be called on module namespace objects");
// 2. Let exports be O.[[Exports]].
let exports = obj.exports();
// 3. If exports contains P, return true.
// 4. Return false.
Ok(exports.contains_key(key))
}
/// [`[[Get]] ( P, Receiver )`][spec]
///
/// [spec]: https://tc39.es/ecma262/#sec-module-namespace-exotic-objects-get-p-receiver
fn module_namespace_exotic_get(
obj: &JsObject,
key: &PropertyKey,
receiver: JsValue,
context: &mut Context<'_>,
) -> JsResult<JsValue> {
// 1. If P is a Symbol, then
// a. Return ! OrdinaryGet(O, P, Receiver).
let key = match key {
PropertyKey::Symbol(_) => return ordinary_get(obj, key, receiver, context),
PropertyKey::Index(_) => return Ok(JsValue::undefined()),
PropertyKey::String(s) => s,
};
let obj = obj.borrow();
let obj = obj
.as_module_namespace()
.expect("internal method can only be called on module namespace objects");
// 2. Let exports be O.[[Exports]].
let exports = obj.exports();
// 3. If exports does not contain P, return undefined.
let Some(export_name) = exports.get(key).copied() else {
return Ok(JsValue::undefined());
};
// 4. Let m be O.[[Module]].
let m = obj.module();
// 5. Let binding be m.ResolveExport(P).
let binding = m
.resolve_export(export_name, &mut HashSet::default())
.expect("6. Assert: binding is a ResolvedBinding Record.");
// 7. Let targetModule be binding.[[Module]].
// 8. Assert: targetModule is not undefined.
let target_module = binding.module();
// TODO: cache binding resolution instead of doing the whole process on every access.
if let BindingName::Name(name) = binding.binding_name() {
// 10. Let targetEnv be targetModule.[[Environment]].
let Some(env) = target_module.environment() else {
// 11. If targetEnv is empty, throw a ReferenceError exception.
let import = context.interner().resolve_expect(export_name);
return Err(JsNativeError::reference().with_message(
format!("cannot get import `{import}` from an uninitialized module")
).into());
};
let locator = env
.compile_env()
.borrow()
.get_binding(name)
.expect("checked before that the name was reachable");
// 12. Return ? targetEnv.GetBindingValue(binding.[[BindingName]], true).
env.get(locator.binding_index()).ok_or_else(|| {
let import = context.interner().resolve_expect(export_name);
JsNativeError::reference()
.with_message(format!("cannot get uninitialized import `{import}`"))
.into()
})
} else {
// 9. If binding.[[BindingName]] is namespace, then
// a. Return GetModuleNamespace(targetModule).
Ok(target_module.namespace(context).into())
}
}
/// [`[[Set]] ( P, V, Receiver )`][spec].
///
/// [spec]: https://tc39.es/ecma262/#sec-module-namespace-exotic-objects-set-p-v-receiver
#[allow(clippy::needless_pass_by_value, clippy::unnecessary_wraps)]
fn module_namespace_exotic_set(
_obj: &JsObject,
_key: PropertyKey,
_value: JsValue,
_receiver: JsValue,
_context: &mut Context<'_>,
) -> JsResult<bool> {
// 1. Return false.
Ok(false)
}
/// [`[[Delete]] ( P )`][spec].
///
/// [spec]: https://tc39.es/ecma262/#sec-module-namespace-exotic-objects-delete-p
fn module_namespace_exotic_delete(
obj: &JsObject,
key: &PropertyKey,
context: &mut Context<'_>,
) -> JsResult<bool> {
// 1. If P is a Symbol, then
// a. Return ! OrdinaryDelete(O, P).
let key = match key {
PropertyKey::Symbol(_) => return ordinary_delete(obj, key, context),
PropertyKey::Index(_) => return Ok(true),
PropertyKey::String(s) => s,
};
let obj = obj.borrow();
let obj = obj
.as_module_namespace()
.expect("internal method can only be called on module namespace objects");
// 2. Let exports be O.[[Exports]].
let exports = obj.exports();
// 3. If exports contains P, return false.
// 4. Return true.
Ok(!exports.contains_key(key))
}
/// [`[[OwnPropertyKeys]] ( )`][spec].
///
/// [spec]: https://tc39.es/ecma262/#sec-module-namespace-exotic-objects-ownpropertykeys
fn module_namespace_exotic_own_property_keys(
obj: &JsObject,
context: &mut Context<'_>,
) -> JsResult<Vec<PropertyKey>> {
// 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())
}

14
boa_engine/src/object/jsobject.rs

@ -237,7 +237,7 @@ impl JsObject {
// a recursive structure
// We can follow v8 & SpiderMonkey's lead and return a default value for the hint in this situation
// (see https://repl.it/repls/IvoryCircularCertification#index.js)
let recursion_limiter = RecursionLimiter::new(self);
let recursion_limiter = RecursionLimiter::new(self.as_ref());
if recursion_limiter.live {
// we're in a recursive object, bail
return Ok(match hint {
@ -1053,15 +1053,15 @@ thread_local! {
}
impl RecursionLimiter {
/// Determines if the specified `JsObject` has been visited, and returns a struct that will free it when dropped.
/// Determines if the specified `T` has been visited, and returns a struct that will free it when dropped.
///
/// This is done by maintaining a thread-local hashset containing the pointers of `JsObject` values that have been
/// visited. The first `JsObject` visited will clear the hashset, while any others will check if they are contained
/// This is done by maintaining a thread-local hashset containing the pointers of `T` values that have been
/// visited. The first `T` visited will clear the hashset, while any others will check if they are contained
/// by the hashset.
pub fn new(o: &JsObject) -> Self {
pub fn new<T>(o: &T) -> Self {
// We shouldn't have to worry too much about this being moved during Debug::fmt.
#[allow(trivial_casts)]
let ptr = (o.as_ref() as *const _) as usize;
let ptr = (o as *const _) as usize;
let (top_level, visited, live) = SEEN.with(|hm| {
let mut hm = hm.borrow_mut();
let top_level = hm.is_empty();
@ -1086,7 +1086,7 @@ impl RecursionLimiter {
impl Debug for JsObject {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> std::fmt::Result {
let ptr: *const _ = self.vtable();
let limiter = RecursionLimiter::new(self);
let limiter = RecursionLimiter::new(self.as_ref());
// Typically, using `!limiter.live` would be good enough here.
// However, the JS object hierarchy involves quite a bit of repitition, and the sheer amount of data makes

44
boa_engine/src/object/mod.rs

@ -16,7 +16,9 @@ use self::{
BOUND_CONSTRUCTOR_EXOTIC_INTERNAL_METHODS, BOUND_FUNCTION_EXOTIC_INTERNAL_METHODS,
},
function::{CONSTRUCTOR_INTERNAL_METHODS, FUNCTION_INTERNAL_METHODS},
immutable_prototype::IMMUTABLE_PROTOTYPE_EXOTIC_INTERNAL_METHODS,
integer_indexed::INTEGER_INDEXED_EXOTIC_INTERNAL_METHODS,
module_namespace::MODULE_NAMESPACE_EXOTIC_INTERNAL_METHODS,
proxy::{
PROXY_EXOTIC_INTERNAL_METHODS_ALL, PROXY_EXOTIC_INTERNAL_METHODS_BASIC,
PROXY_EXOTIC_INTERNAL_METHODS_WITH_CALL,
@ -55,6 +57,7 @@ use crate::{
DataView, Date, Promise, RegExp,
},
js_string,
module::ModuleNamespace,
native_function::NativeFunction,
property::{Attribute, PropertyDescriptor, PropertyKey},
string::utf16,
@ -307,6 +310,9 @@ pub enum ObjectKind {
/// The `WeakSet` object kind.
WeakSet(boa_gc::WeakMap<VTableObject, ()>),
/// The `ModuleNamespace` object kind.
ModuleNamespace(ModuleNamespace),
/// The `Intl.Collator` object kind.
#[cfg(feature = "intl")]
Collator(Box<Collator>),
@ -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")]

21
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<FxHashMap<u64, JsObject>>,
loaded_modules: GcRefCell<FxHashMap<JsString, Module>>,
}
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<FxHashMap<JsString, Module>> {
&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<JsObject> {
self.inner.template_map.borrow().get(&site).cloned()
}
pub(crate) fn addr(&self) -> *const () {
let ptr: *const _ = &*self.inner;
ptr.cast()
}
}

21
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 };

11
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 };

131
boa_examples/src/bin/modules.rs

@ -0,0 +1,131 @@
use std::{error::Error, path::Path};
use boa_engine::{
builtins::promise::PromiseState,
module::{ModuleLoader, SimpleModuleLoader},
object::FunctionObjectBuilder,
Context, JsError, JsNativeError, JsValue, Module, NativeFunction,
};
use boa_parser::Source;
// This example demonstrates how to use Boa's module API
fn main() -> Result<(), Box<dyn Error>> {
// A simple module that we want to compile from Rust code.
const MODULE_SRC: &str = r#"
import { pyth } from "./trig.mjs";
import * as ops from "./operations.mjs";
export let result = pyth(3, 4);
export function mix(a, b) {
return ops.sum(ops.mult(a, ops.sub(b, a)), 10);
}
"#;
// This can be overriden with any custom implementation of `ModuleLoader`.
let loader = &SimpleModuleLoader::new("./boa_examples/scripts/modules")?;
let dyn_loader: &dyn ModuleLoader = loader;
// Just need to cast to a `ModuleLoader` before passing it to the builder.
let context = &mut Context::builder().module_loader(dyn_loader).build()?;
let source = Source::from_reader(MODULE_SRC.as_bytes(), Some(Path::new("./main.mjs")));
// Can also pass a `Some(realm)` if you need to execute the module in another realm.
let module = Module::parse(source, None, context)?;
// Don't forget to insert the parsed module into the loader itself! Since the root module
// is not automatically inserted by the `ModuleLoader::load_imported_module` impl.
//
// Simulate as if the "fake" module is located in the modules root, just to ensure that
// the loader won't double load in case someone tries to import "./main.mjs".
loader.insert(
Path::new("./boa_examples/scripts/modules")
.canonicalize()?
.join("main.mjs"),
module.clone(),
);
// The lifecycle of the module is tracked using promises, which can be a bit cumbersome
// for simple uses but that use case is better suited by the `Module::load_link_evaluate` method.
// This does the full version for demonstration purposes.
//
// parse -> load -> link -> evaluate
let promise_result = module
// Initial load that recursively loads the module's dependencies.
// This returns a `JsPromise` that will be resolved when loading finishes,
// which allows async loads and async fetches.
.load(context)
.then(
Some(
FunctionObjectBuilder::new(
context,
NativeFunction::from_copy_closure_with_captures(
|_, _, module, context| {
// After loading, link all modules by resolving the imports
// and exports on the full module graph, initializing module
// environments. This returns a plain `Err` since all modules
// must link at the same time.
module.link(context)?;
Ok(JsValue::undefined())
},
module.clone(),
),
)
.build(),
),
None,
context,
)?
.then(
Some(
FunctionObjectBuilder::new(
context,
NativeFunction::from_copy_closure_with_captures(
// Finally, evaluate the root module.
// This returns a `JsPromise` since a module could have
// top-level await statements, which defers module execution to the
// job queue.
|_, _, module, context| Ok(module.evaluate(context).into()),
module.clone(),
),
)
.build(),
),
None,
context,
)?;
// Very important to push forward the job queue after queueing promises.
context.run_jobs();
// Checking if the final promise didn't return an error.
match promise_result.state()? {
PromiseState::Pending => return Err("module didn't execute!".into()),
PromiseState::Fulfilled(v) => {
assert_eq!(v, JsValue::undefined())
}
PromiseState::Rejected(err) => {
return Err(JsError::from_opaque(err).try_native(context)?.into())
}
}
// We can access the full namespace of the module with all its exports.
let namespace = module.namespace(context);
let result = namespace.get("result", context)?;
println!("result = {}", result.display());
assert_eq!(namespace.get("result", context)?, JsValue::from(5));
let mix = namespace
.get("mix", context)?
.as_callable()
.cloned()
.ok_or_else(|| JsNativeError::typ().with_message("mix export wasn't a function!"))?;
let result = mix.call(&JsValue::undefined(), &[5.into(), 10.into()], context)?;
println!("mix(5, 10) = {}", result.display());
assert_eq!(result, 35.into());
Ok(())
}

1
boa_gc/src/cell.rs

@ -161,6 +161,7 @@ impl<T: Trace + ?Sized> GcRefCell<T> {
/// # 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,

2
boa_gc/src/pointers/ephemeron.rs

@ -73,7 +73,7 @@ impl<K: Trace + ?Sized, V: Trace> Ephemeron<K, V> {
}
pub(crate) fn inner_ptr(&self) -> NonNull<EphemeronBox<K, V>> {
assert!(finalizer_safe());
assert!(finalizer_safe() || self.is_rooted());
self.inner_ptr.get().as_ptr()
}

3
boa_gc/src/pointers/gc.rs

@ -30,6 +30,7 @@ impl<T: Trace> Gc<T> {
//
// Note: Allocator can cause Collector to run
let inner_ptr = Allocator::alloc_gc(GcBox::new(value));
// SAFETY: inner_ptr was just allocated, so it must be a valid value that implements [`Trace`]
unsafe { (*inner_ptr.as_ptr()).value().unroot() }
@ -94,7 +95,7 @@ impl<T: Trace + ?Sized> Gc<T> {
}
pub(crate) fn inner_ptr(&self) -> NonNull<GcBox<T>> {
assert!(finalizer_safe());
assert!(finalizer_safe() || self.is_rooted());
self.inner_ptr.get().as_ptr()
}

1
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"

9
boa_interner/src/sym.rs

@ -1,3 +1,4 @@
use boa_gc::{empty_trace, Finalize, Trace};
use boa_macros::static_syms;
use core::num::NonZeroUsize;
@ -12,11 +13,17 @@ use core::num::NonZeroUsize;
)]
#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
#[allow(clippy::unsafe_derive_deserialize)]
#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Finalize)]
pub struct Sym {
value: NonZeroUsize,
}
// SAFETY: `NonZeroUsize` is a constrained `usize`, and all primitive types don't need to be traced
// by the garbage collector.
unsafe impl Trace for Sym {
empty_trace!();
}
impl Sym {
/// Creates a new [`Sym`] from the provided `value`, or returns `None` if `index` is zero.
pub(super) fn new(value: usize) -> Option<Self> {

6
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)?);

4
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)?);

13
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",
)),
}
}
}

27
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),

261
boa_tester/src/exec/mod.rs

@ -7,8 +7,13 @@ use crate::{
TestFlags, TestOutcomeResult, TestResult, TestSuite, VersionedStats,
};
use boa_engine::{
native_function::NativeFunction, object::FunctionObjectBuilder, optimizer::OptimizerOptions,
property::Attribute, Context, JsArgs, JsNativeErrorKind, JsValue, Source,
builtins::promise::PromiseState,
module::{Module, ModuleLoader, SimpleModuleLoader},
native_function::NativeFunction,
object::FunctionObjectBuilder,
optimizer::OptimizerOptions,
property::Attribute,
Context, JsArgs, JsError, JsNativeErrorKind, JsValue, Source,
};
use colored::Colorize;
use fxhash::FxHashSet;
@ -141,12 +146,16 @@ impl Test {
optimizer_options: OptimizerOptions,
) -> Vec<TestResult> {
let mut results = Vec::new();
if self.flags.contains(TestFlags::STRICT) && !self.flags.contains(TestFlags::RAW) {
results.push(self.run_once(harness, true, verbose, optimizer_options));
}
if self.flags.contains(TestFlags::NO_STRICT) || self.flags.contains(TestFlags::RAW) {
if self.flags.contains(TestFlags::MODULE) {
results.push(self.run_once(harness, false, verbose, optimizer_options));
} else {
if self.flags.contains(TestFlags::STRICT) && !self.flags.contains(TestFlags::RAW) {
results.push(self.run_once(harness, true, verbose, optimizer_options));
}
if self.flags.contains(TestFlags::NO_STRICT) || self.flags.contains(TestFlags::RAW) {
results.push(self.run_once(harness, false, verbose, optimizer_options));
}
}
results
@ -209,22 +218,61 @@ impl Test {
let result = std::panic::catch_unwind(|| match self.expected_outcome {
Outcome::Positive => {
let async_result = AsyncResult::default();
let context = &mut Context::default();
let loader = &SimpleModuleLoader::new(
self.path.parent().expect("test should have a parent dir"),
)
.expect("test path should be canonicalizable");
let dyn_loader: &dyn ModuleLoader = loader;
let context = &mut Context::builder()
.module_loader(dyn_loader)
.build()
.expect("cannot fail with default global object");
if let Err(e) = self.set_up_env(harness, context, async_result.clone()) {
return (false, e);
}
context.strict(strict);
context.set_optimizer_options(optimizer_options);
// TODO: timeout
let value = match if self.is_module() {
context.eval_module(source)
let value = if self.is_module() {
let module = match Module::parse(source, None, context) {
Ok(module) => module,
Err(err) => return (false, format!("Uncaught {err}")),
};
loader.insert(
self.path
.canonicalize()
.expect("test path should be canonicalizable"),
module.clone(),
);
let promise = match module.load_link_evaluate(context) {
Ok(promise) => promise,
Err(err) => return (false, format!("Uncaught {err}")),
};
context.run_jobs();
match promise
.state()
.expect("tester can only use builtin promises")
{
PromiseState::Pending => {
return (false, "module should have been executed".to_string())
}
PromiseState::Fulfilled(v) => v,
PromiseState::Rejected(err) => {
return (false, format!("Uncaught {}", err.display()))
}
}
} else {
context.eval_script(source)
} {
Ok(v) => v,
Err(e) => return (false, format!("Uncaught {e}")),
context.strict(strict);
match context.eval_script(source) {
Ok(v) => v,
Err(err) => return (false, format!("Uncaught {err}")),
}
};
context.run_jobs();
@ -254,15 +302,16 @@ impl Test {
);
let context = &mut Context::default();
context.strict(strict);
context.set_optimizer_options(OptimizerOptions::OPTIMIZE_ALL);
if self.is_module() {
match context.parse_module(source) {
match Module::parse(source, None, context) {
Ok(_) => (false, "ModuleItemList parsing should fail".to_owned()),
Err(e) => (true, format!("Uncaught {e}")),
}
} else {
context.strict(strict);
match context.parse_script(source) {
Ok(_) => (false, "StatementList parsing should fail".to_owned()),
Err(e) => (true, format!("Uncaught {e}")),
@ -271,37 +320,134 @@ impl Test {
}
Outcome::Negative {
phase: Phase::Resolution,
error_type: _,
} => (false, "Modules are not implemented yet".to_string()),
error_type,
} => {
let loader = &SimpleModuleLoader::new(
self.path.parent().expect("test should have a parent dir"),
)
.expect("test path should be canonicalizable");
let dyn_loader: &dyn ModuleLoader = loader;
let context = &mut Context::builder()
.module_loader(dyn_loader)
.build()
.expect("cannot fail with default global object");
let module = match Module::parse(source, None, context) {
Ok(module) => module,
Err(err) => return (false, format!("Uncaught {err}")),
};
loader.insert(
self.path
.canonicalize()
.expect("test path should be canonicalizable"),
module.clone(),
);
let promise = module.load(context);
context.run_jobs();
match promise
.state()
.expect("tester can only use builtin promises")
{
PromiseState::Pending => {
return (false, "module didn't try to load".to_string())
}
PromiseState::Fulfilled(_) => {
// Try to link to see if the resolution error shows there.
}
PromiseState::Rejected(err) => {
let err = JsError::from_opaque(err);
return (
is_error_type(&err, error_type, context),
format!("Uncaught {err}"),
);
}
}
if let Err(err) = module.link(context) {
(
is_error_type(&err, error_type, context),
format!("Uncaught {err}"),
)
} else {
(false, "module resolution didn't fail".to_string())
}
}
Outcome::Negative {
phase: Phase::Runtime,
error_type,
} => {
let context = &mut Context::default();
let loader = &SimpleModuleLoader::new(
self.path.parent().expect("test should have a parent dir"),
)
.expect("test path should be canonicalizable");
let dyn_loader: &dyn ModuleLoader = loader;
let context = &mut Context::builder()
.module_loader(dyn_loader)
.build()
.expect("cannot fail with default global object");
context.strict(strict);
context.set_optimizer_options(optimizer_options);
if let Err(e) = self.set_up_env(harness, context, AsyncResult::default()) {
return (false, e);
}
let e = if self.is_module() {
let module = match context.parse_module(source) {
Ok(code) => code,
let error = if self.is_module() {
let module = match Module::parse(source, None, context) {
Ok(module) => module,
Err(e) => return (false, format!("Uncaught {e}")),
};
match context
.compile_module(&module)
.and_then(|code| context.execute(code))
loader.insert(
self.path
.canonicalize()
.expect("test path should be canonicalizable"),
module.clone(),
);
let promise = module.load(context);
context.run_jobs();
match promise
.state()
.expect("tester can only use builtin promises")
{
Ok(_) => return (false, "Module execution should fail".to_owned()),
Err(e) => e,
PromiseState::Pending => {
return (false, "module didn't try to load".to_string())
}
PromiseState::Fulfilled(_) => {}
PromiseState::Rejected(err) => {
return (false, format!("Uncaught {}", err.display()))
}
}
if let Err(err) = module.link(context) {
return (false, format!("Uncaught {err}"));
}
let promise = module.evaluate(context);
match promise
.state()
.expect("tester can only use builtin promises")
{
PromiseState::Pending => {
return (false, "module didn't try to evaluate".to_string())
}
PromiseState::Fulfilled(val) => return (false, val.display().to_string()),
PromiseState::Rejected(err) => JsError::from_opaque(err),
}
} else {
context.strict(strict);
let script = match context.parse_script(source) {
Ok(code) => code,
Err(e) => return (false, format!("Uncaught {e}")),
};
match context
.compile_script(&script)
.and_then(|code| context.execute(code))
@ -311,31 +457,10 @@ impl Test {
}
};
if let Ok(e) = e.try_native(context) {
match &e.kind {
JsNativeErrorKind::Syntax if error_type == ErrorType::SyntaxError => {}
JsNativeErrorKind::Reference if error_type == ErrorType::ReferenceError => {
}
JsNativeErrorKind::Range if error_type == ErrorType::RangeError => {}
JsNativeErrorKind::Type if error_type == ErrorType::TypeError => {}
_ => return (false, format!("Uncaught {e}")),
}
(true, format!("Uncaught {e}"))
} else {
let passed = e
.as_opaque()
.expect("try_native cannot fail if e is not opaque")
.as_object()
.and_then(|o| o.get("constructor", context).ok())
.as_ref()
.and_then(JsValue::as_object)
.and_then(|o| o.get("name", context).ok())
.as_ref()
.and_then(JsValue::as_string)
.map(|s| s == error_type.as_str())
.unwrap_or_default();
(passed, format!("Uncaught {e}"))
}
(
is_error_type(&error, error_type, context),
format!("Uncaught {error}"),
)
}
});
@ -451,6 +576,34 @@ impl Test {
}
}
/// Returns `true` if `error` is a `target_type` error.
fn is_error_type(error: &JsError, target_type: ErrorType, context: &mut Context<'_>) -> bool {
if let Ok(error) = error.try_native(context) {
match &error.kind {
JsNativeErrorKind::Syntax if target_type == ErrorType::SyntaxError => {}
JsNativeErrorKind::Reference if target_type == ErrorType::ReferenceError => {}
JsNativeErrorKind::Range if target_type == ErrorType::RangeError => {}
JsNativeErrorKind::Type if target_type == ErrorType::TypeError => {}
_ => return false,
}
true
} else {
let passed = error
.as_opaque()
.expect("try_native cannot fail if e is not opaque")
.as_object()
.and_then(|o| o.get("constructor", context).ok())
.as_ref()
.and_then(JsValue::as_object)
.and_then(|o| o.get("name", context).ok())
.as_ref()
.and_then(JsValue::as_string)
.map(|s| s == target_type.as_str())
.unwrap_or_default();
passed
}
}
/// Registers the print function in the context.
fn register_print_fn(context: &mut Context<'_>, async_result: AsyncResult) {
// We use `FunctionBuilder` to define a closure with additional captures.

Loading…
Cancel
Save