mirror of https://github.com/boa-dev/boa.git
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
424 lines
13 KiB
424 lines
13 KiB
2 years ago
|
//! Boa's **`boa_engine`** crate implements ECMAScript's standard library of builtin objects
|
||
2 years ago
|
//! and an ECMAScript context, bytecompiler, and virtual machine for code execution.
|
||
3 years ago
|
//!
|
||
2 years ago
|
//! # Example usage
|
||
|
//!
|
||
|
//! You can find multiple examples of the usage of Boa in the [`boa_examples`][examples] crate. In
|
||
|
//! order to use Boa in your project, you will need to add the `boa_engine` crate to your
|
||
|
//! `Cargo.toml` file. You will need to use a [`Source`] structure to handle the JavaScript code
|
||
|
//! to execute, and a [`Context`] structure to execute the code:
|
||
|
//!
|
||
|
//! ```
|
||
|
//! use boa_engine::{Context, Source};
|
||
|
//!
|
||
2 years ago
|
//! let js_code = r#"
|
||
|
//! let two = 1 + 1;
|
||
|
//! let definitely_not_four = two + "2";
|
||
|
//!
|
||
|
//! definitely_not_four
|
||
|
//! "#;
|
||
2 years ago
|
//!
|
||
|
//! // Instantiate the execution context
|
||
|
//! let mut context = Context::default();
|
||
|
//!
|
||
|
//! // Parse the source code
|
||
2 years ago
|
//! match context.eval(Source::from_bytes(js_code)) {
|
||
2 years ago
|
//! Ok(res) => {
|
||
|
//! println!(
|
||
|
//! "{}",
|
||
|
//! res.to_string(&mut context).unwrap().to_std_string_escaped()
|
||
|
//! );
|
||
|
//! }
|
||
|
//! Err(e) => {
|
||
|
//! // Pretty print the error
|
||
|
//! eprintln!("Uncaught {e}");
|
||
2 years ago
|
//! # panic!("There was an error in boa_engine's introduction example.");
|
||
2 years ago
|
//! }
|
||
|
//! };
|
||
|
//! ```
|
||
|
//!
|
||
3 years ago
|
//! # Crate Features
|
||
2 years ago
|
//!
|
||
3 years ago
|
//! - **serde** - Enables serialization and deserialization of the AST (Abstract Syntax Tree).
|
||
|
//! - **profiler** - Enables profiling with measureme (this is mostly internal).
|
||
3 years ago
|
//! - **intl** - Enables `boa`'s [ECMA-402 Internationalization API][ecma-402] (`Intl` object)
|
||
|
//!
|
||
|
//! [ecma-402]: https://tc39.es/ecma402
|
||
2 years ago
|
//! [examples]: https://github.com/boa-dev/boa/tree/main/boa_examples
|
||
2 years ago
|
#![doc = include_str!("../ABOUT.md")]
|
||
5 years ago
|
#![doc(
|
||
3 years ago
|
html_logo_url = "https://raw.githubusercontent.com/boa-dev/boa/main/assets/logo.svg",
|
||
|
html_favicon_url = "https://raw.githubusercontent.com/boa-dev/boa/main/assets/logo.svg"
|
||
5 years ago
|
)]
|
||
1 year ago
|
#![cfg_attr(test, allow(clippy::needless_raw_string_hashes))] // Makes strings a bit more copy-pastable
|
||
3 years ago
|
#![cfg_attr(not(test), forbid(clippy::unwrap_used))]
|
||
5 years ago
|
#![allow(
|
||
2 years ago
|
// Currently throws a false positive regarding dependencies that are only used in benchmarks.
|
||
|
unused_crate_dependencies,
|
||
3 years ago
|
clippy::module_name_repetitions,
|
||
2 years ago
|
clippy::redundant_pub_crate,
|
||
3 years ago
|
clippy::too_many_lines,
|
||
5 years ago
|
clippy::cognitive_complexity,
|
||
|
clippy::missing_errors_doc,
|
||
5 years ago
|
clippy::let_unit_value,
|
||
2 years ago
|
clippy::option_if_let_else,
|
||
|
|
||
|
// It may be worth to look if we can fix the issues highlighted by these lints.
|
||
|
clippy::cast_possible_truncation,
|
||
|
clippy::cast_sign_loss,
|
||
|
clippy::cast_precision_loss,
|
||
1 year ago
|
clippy::cast_possible_wrap,
|
||
1 year ago
|
|
||
|
// Add temporarily - Needs addressing
|
||
|
clippy::missing_panics_doc,
|
||
5 years ago
|
)]
|
||
6 years ago
|
|
||
1 year ago
|
#[cfg(not(target_has_atomic = "ptr"))]
|
||
|
compile_error!("Boa requires a lock free `AtomicUsize` in order to work properly.");
|
||
|
|
||
1 year ago
|
extern crate self as boa_engine;
|
||
2 years ago
|
extern crate static_assertions as sa;
|
||
|
|
||
3 years ago
|
pub mod bigint;
|
||
5 years ago
|
pub mod builtins;
|
||
3 years ago
|
pub mod bytecompiler;
|
||
4 years ago
|
pub mod class;
|
||
4 years ago
|
pub mod context;
|
||
3 years ago
|
pub mod environments;
|
||
2 years ago
|
pub mod error;
|
||
3 years ago
|
pub mod job;
|
||
2 years ago
|
pub mod module;
|
||
2 years ago
|
pub mod native_function;
|
||
4 years ago
|
pub mod object;
|
||
2 years ago
|
pub mod optimizer;
|
||
4 years ago
|
pub mod property;
|
||
5 years ago
|
pub mod realm;
|
||
2 years ago
|
pub mod script;
|
||
4 years ago
|
pub mod string;
|
||
4 years ago
|
pub mod symbol;
|
||
1 year ago
|
pub mod value;
|
||
|
pub mod vm;
|
||
|
|
||
|
mod host_defined;
|
||
1 year ago
|
mod small_map;
|
||
|
mod sys;
|
||
2 years ago
|
mod tagged;
|
||
1 year ago
|
|
||
3 years ago
|
#[cfg(test)]
|
||
|
mod tests;
|
||
|
|
||
3 years ago
|
/// A convenience module that re-exports the most commonly-used Boa APIs
|
||
|
pub mod prelude {
|
||
2 years ago
|
pub use crate::{
|
||
1 year ago
|
bigint::JsBigInt,
|
||
|
context::Context,
|
||
2 years ago
|
error::{JsError, JsNativeError, JsNativeErrorKind},
|
||
1 year ago
|
host_defined::HostDefined,
|
||
2 years ago
|
module::Module,
|
||
2 years ago
|
native_function::NativeFunction,
|
||
1 year ago
|
object::{JsData, JsObject, NativeObject},
|
||
2 years ago
|
script::Script,
|
||
1 year ago
|
string::JsString,
|
||
|
symbol::JsSymbol,
|
||
|
value::JsValue,
|
||
2 years ago
|
};
|
||
1 year ago
|
pub use boa_gc::{Finalize, Trace};
|
||
|
pub use boa_macros::JsData;
|
||
2 years ago
|
pub use boa_parser::Source;
|
||
3 years ago
|
}
|
||
|
|
||
3 years ago
|
use std::result::Result as StdResult;
|
||
4 years ago
|
|
||
4 years ago
|
// Export things to root level
|
||
4 years ago
|
#[doc(inline)]
|
||
1 year ago
|
pub use prelude::*;
|
||
4 years ago
|
|
||
4 years ago
|
/// The result of a Javascript expression is represented like this so it can succeed (`Ok`) or fail (`Err`)
|
||
2 years ago
|
pub type JsResult<T> = StdResult<T, JsError>;
|
||
4 years ago
|
|
||
2 years ago
|
/// A utility trait to make working with function arguments easier.
|
||
|
pub trait JsArgs {
|
||
|
/// Utility function to `get` a parameter from a `[JsValue]` or default to `JsValue::Undefined`
|
||
|
/// if `get` returns `None`.
|
||
|
///
|
||
|
/// Call this if you are thinking of calling something similar to
|
||
|
/// `args.get(n).cloned().unwrap_or_default()` or
|
||
|
/// `args.get(n).unwrap_or(&undefined)`.
|
||
|
///
|
||
1 year ago
|
/// This returns a reference for efficiency, in case you only need to call methods of `JsValue`.
|
||
2 years ago
|
fn get_or_undefined(&self, index: usize) -> &JsValue;
|
||
|
}
|
||
|
|
||
|
impl JsArgs for [JsValue] {
|
||
|
fn get_or_undefined(&self, index: usize) -> &JsValue {
|
||
|
const UNDEFINED: &JsValue = &JsValue::Undefined;
|
||
|
self.get(index).unwrap_or(UNDEFINED)
|
||
|
}
|
||
|
}
|
||
|
|
||
4 years ago
|
#[cfg(test)]
|
||
2 years ago
|
use std::borrow::Cow;
|
||
|
|
||
|
/// A test action executed in a test function.
|
||
|
#[cfg(test)]
|
||
|
#[derive(Clone)]
|
||
2 years ago
|
struct TestAction(Inner);
|
||
2 years ago
|
|
||
|
#[cfg(test)]
|
||
|
#[derive(Clone)]
|
||
|
enum Inner {
|
||
|
RunHarness,
|
||
|
Run {
|
||
|
source: Cow<'static, str>,
|
||
|
},
|
||
|
InspectContext {
|
||
1 year ago
|
op: fn(&mut Context),
|
||
2 years ago
|
},
|
||
|
Assert {
|
||
|
source: Cow<'static, str>,
|
||
|
},
|
||
|
AssertEq {
|
||
|
source: Cow<'static, str>,
|
||
|
expected: JsValue,
|
||
|
},
|
||
|
AssertWithOp {
|
||
|
source: Cow<'static, str>,
|
||
1 year ago
|
op: fn(JsValue, &mut Context) -> bool,
|
||
2 years ago
|
},
|
||
|
AssertOpaqueError {
|
||
|
source: Cow<'static, str>,
|
||
|
expected: JsValue,
|
||
|
},
|
||
|
AssertNativeError {
|
||
|
source: Cow<'static, str>,
|
||
2 years ago
|
kind: JsNativeErrorKind,
|
||
2 years ago
|
message: &'static str,
|
||
|
},
|
||
|
AssertContext {
|
||
1 year ago
|
op: fn(&mut Context) -> bool,
|
||
2 years ago
|
},
|
||
6 years ago
|
}
|
||
|
|
||
4 years ago
|
#[cfg(test)]
|
||
2 years ago
|
impl TestAction {
|
||
|
/// Evaluates some utility functions used in tests.
|
||
2 years ago
|
const fn run_harness() -> Self {
|
||
2 years ago
|
Self(Inner::RunHarness)
|
||
|
}
|
||
3 years ago
|
|
||
2 years ago
|
/// Runs `source`, panicking if the execution throws.
|
||
2 years ago
|
fn run(source: impl Into<Cow<'static, str>>) -> Self {
|
||
2 years ago
|
Self(Inner::Run {
|
||
|
source: source.into(),
|
||
|
})
|
||
|
}
|
||
4 years ago
|
|
||
2 years ago
|
/// Executes `op` with the currently active context.
|
||
|
///
|
||
|
/// Useful to make custom assertions that must be done from Rust code.
|
||
1 year ago
|
fn inspect_context(op: fn(&mut Context)) -> Self {
|
||
2 years ago
|
Self(Inner::InspectContext { op })
|
||
|
}
|
||
3 years ago
|
|
||
2 years ago
|
/// Asserts that evaluating `source` returns the `true` value.
|
||
2 years ago
|
fn assert(source: impl Into<Cow<'static, str>>) -> Self {
|
||
2 years ago
|
Self(Inner::Assert {
|
||
|
source: source.into(),
|
||
|
})
|
||
|
}
|
||
5 years ago
|
|
||
2 years ago
|
/// Asserts that the script returns `expected` when evaluating `source`.
|
||
2 years ago
|
fn assert_eq(source: impl Into<Cow<'static, str>>, expected: impl Into<JsValue>) -> Self {
|
||
2 years ago
|
Self(Inner::AssertEq {
|
||
|
source: source.into(),
|
||
|
expected: expected.into(),
|
||
|
})
|
||
|
}
|
||
5 years ago
|
|
||
2 years ago
|
/// Asserts that calling `op` with the value obtained from evaluating `source` returns `true`.
|
||
|
///
|
||
|
/// Useful to check properties of the obtained value that cannot be checked from JS code.
|
||
2 years ago
|
fn assert_with_op(
|
||
2 years ago
|
source: impl Into<Cow<'static, str>>,
|
||
1 year ago
|
op: fn(JsValue, &mut Context) -> bool,
|
||
2 years ago
|
) -> Self {
|
||
|
Self(Inner::AssertWithOp {
|
||
|
source: source.into(),
|
||
|
op,
|
||
|
})
|
||
|
}
|
||
|
|
||
|
/// Asserts that evaluating `source` throws the opaque error `value`.
|
||
2 years ago
|
fn assert_opaque_error(
|
||
2 years ago
|
source: impl Into<Cow<'static, str>>,
|
||
|
value: impl Into<JsValue>,
|
||
|
) -> Self {
|
||
|
Self(Inner::AssertOpaqueError {
|
||
|
source: source.into(),
|
||
|
expected: value.into(),
|
||
|
})
|
||
|
}
|
||
|
|
||
|
/// Asserts that evaluating `source` throws a native error of `kind` and `message`.
|
||
2 years ago
|
fn assert_native_error(
|
||
2 years ago
|
source: impl Into<Cow<'static, str>>,
|
||
2 years ago
|
kind: JsNativeErrorKind,
|
||
2 years ago
|
message: &'static str,
|
||
|
) -> Self {
|
||
|
Self(Inner::AssertNativeError {
|
||
|
source: source.into(),
|
||
|
kind,
|
||
|
message,
|
||
|
})
|
||
|
}
|
||
|
|
||
|
/// Asserts that calling `op` with the currently executing context returns `true`.
|
||
1 year ago
|
fn assert_context(op: fn(&mut Context) -> bool) -> Self {
|
||
2 years ago
|
Self(Inner::AssertContext { op })
|
||
4 years ago
|
}
|
||
6 years ago
|
}
|
||
3 years ago
|
|
||
2 years ago
|
/// Executes a list of test actions on a new, default context.
|
||
3 years ago
|
#[cfg(test)]
|
||
2 years ago
|
#[track_caller]
|
||
2 years ago
|
fn run_test_actions(actions: impl IntoIterator<Item = TestAction>) {
|
||
2 years ago
|
let context = &mut Context::default();
|
||
|
run_test_actions_with(actions, context);
|
||
3 years ago
|
}
|
||
|
|
||
2 years ago
|
/// Executes a list of test actions on the provided context.
|
||
3 years ago
|
#[cfg(test)]
|
||
|
#[track_caller]
|
||
1 year ago
|
fn run_test_actions_with(actions: impl IntoIterator<Item = TestAction>, context: &mut Context) {
|
||
2 years ago
|
#[track_caller]
|
||
1 year ago
|
fn forward_val(context: &mut Context, source: &str) -> JsResult<JsValue> {
|
||
2 years ago
|
context.eval(Source::from_bytes(source))
|
||
2 years ago
|
}
|
||
3 years ago
|
|
||
2 years ago
|
#[track_caller]
|
||
|
fn fmt_test(source: &str, test: usize) -> String {
|
||
|
format!(
|
||
|
"\n\nTest case {test}: \n```\n{}\n```",
|
||
|
textwrap::indent(source, " ")
|
||
|
)
|
||
|
}
|
||
|
|
||
|
// Some unwrapping patterns look weird because they're replaceable
|
||
|
// by simpler patterns like `unwrap_or_else` or `unwrap_err
|
||
3 years ago
|
let mut i = 1;
|
||
2 years ago
|
for action in actions.into_iter().map(|a| a.0) {
|
||
3 years ago
|
match action {
|
||
2 years ago
|
Inner::RunHarness => {
|
||
|
// add utility functions for testing
|
||
|
// TODO: extract to a file
|
||
|
forward_val(
|
||
|
context,
|
||
|
r#"
|
||
|
function equals(a, b) {
|
||
|
if (Array.isArray(a) && Array.isArray(b)) {
|
||
|
return arrayEquals(a, b);
|
||
|
}
|
||
|
return a === b;
|
||
|
}
|
||
|
function arrayEquals(a, b) {
|
||
|
return Array.isArray(a) &&
|
||
|
Array.isArray(b) &&
|
||
|
a.length === b.length &&
|
||
|
a.every((val, index) => equals(val, b[index]));
|
||
|
}
|
||
|
"#,
|
||
|
)
|
||
|
.expect("failed to evaluate test harness");
|
||
|
}
|
||
|
Inner::Run { source } => {
|
||
|
if let Err(e) = forward_val(context, &source) {
|
||
|
panic!("{}\nUncaught {e}", fmt_test(&source, i));
|
||
|
}
|
||
|
}
|
||
|
Inner::InspectContext { op } => {
|
||
|
op(context);
|
||
|
}
|
||
|
Inner::Assert { source } => {
|
||
|
let val = match forward_val(context, &source) {
|
||
|
Err(e) => panic!("{}\nUncaught {e}", fmt_test(&source, i)),
|
||
|
Ok(v) => v,
|
||
|
};
|
||
|
let Some(val) = val.as_boolean() else {
|
||
|
panic!(
|
||
|
"{}\nTried to assert with the non-boolean value `{}`",
|
||
|
fmt_test(&source, i),
|
||
|
val.display()
|
||
|
)
|
||
|
};
|
||
|
assert!(val, "{}", fmt_test(&source, i));
|
||
|
i += 1;
|
||
|
}
|
||
|
Inner::AssertEq { source, expected } => {
|
||
|
let val = match forward_val(context, &source) {
|
||
|
Err(e) => panic!("{}\nUncaught {e}", fmt_test(&source, i)),
|
||
|
Ok(v) => v,
|
||
|
};
|
||
|
assert_eq!(val, expected, "{}", fmt_test(&source, i));
|
||
|
i += 1;
|
||
|
}
|
||
|
Inner::AssertWithOp { source, op } => {
|
||
|
let val = match forward_val(context, &source) {
|
||
|
Err(e) => panic!("{}\nUncaught {e}", fmt_test(&source, i)),
|
||
|
Ok(v) => v,
|
||
|
};
|
||
|
assert!(op(val, context), "{}", fmt_test(&source, i));
|
||
|
i += 1;
|
||
3 years ago
|
}
|
||
2 years ago
|
Inner::AssertOpaqueError { source, expected } => {
|
||
|
let err = match forward_val(context, &source) {
|
||
|
Ok(v) => panic!(
|
||
|
"{}\nExpected error, got value `{}`",
|
||
|
fmt_test(&source, i),
|
||
|
v.display()
|
||
|
),
|
||
|
Err(e) => e,
|
||
|
};
|
||
|
let Some(err) = err.as_opaque() else {
|
||
1 year ago
|
panic!(
|
||
|
"{}\nExpected opaque error, got native error `{}`",
|
||
|
fmt_test(&source, i),
|
||
|
err
|
||
|
)
|
||
2 years ago
|
};
|
||
|
|
||
|
assert_eq!(err, &expected, "{}", fmt_test(&source, i));
|
||
|
i += 1;
|
||
|
}
|
||
|
Inner::AssertNativeError {
|
||
|
source,
|
||
|
kind,
|
||
|
message,
|
||
|
} => {
|
||
|
let err = match forward_val(context, &source) {
|
||
|
Ok(v) => panic!(
|
||
|
"{}\nExpected error, got value `{}`",
|
||
|
fmt_test(&source, i),
|
||
|
v.display()
|
||
|
),
|
||
|
Err(e) => e,
|
||
|
};
|
||
|
let native = match err.try_native(context) {
|
||
|
Ok(err) => err,
|
||
|
Err(e) => panic!(
|
||
|
"{}\nCouldn't obtain a native error: {e}",
|
||
|
fmt_test(&source, i)
|
||
|
),
|
||
|
};
|
||
|
|
||
|
assert_eq!(&native.kind, &kind, "{}", fmt_test(&source, i));
|
||
|
assert_eq!(native.message(), message, "{}", fmt_test(&source, i));
|
||
3 years ago
|
i += 1;
|
||
|
}
|
||
2 years ago
|
Inner::AssertContext { op } => {
|
||
|
assert!(op(context), "Test case {i}");
|
||
3 years ago
|
i += 1;
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|