boa_engine/
lib.rs

1//! Boa's **`boa_engine`** crate implements ECMAScript's standard library of builtin objects
2//! and an ECMAScript context, bytecompiler, and virtual machine for code execution.
3//!
4//! # Example usage
5//!
6//! You can find multiple examples of the usage of Boa in the [`boa_examples`][examples] crate. In
7//! order to use Boa in your project, you will need to add the `boa_engine` crate to your
8//! `Cargo.toml` file. You will need to use a [`Source`] structure to handle the JavaScript code
9//! to execute, and a [`Context`] structure to execute the code:
10//!
11//! ```
12//! use boa_engine::{Context, Source};
13//!
14//! let js_code = r#"
15//!     let two = 1 + 1;
16//!     let definitely_not_four = two + "2";
17//!
18//!     definitely_not_four
19//! "#;
20//!
21//! // Instantiate the execution context
22//! let mut context = Context::default();
23//!
24//! // Parse the source code
25//! match context.eval(Source::from_bytes(js_code)) {
26//!     Ok(res) => {
27//!         println!(
28//!             "{}",
29//!             res.to_string(&mut context).unwrap().to_std_string_escaped()
30//!         );
31//!     }
32//!     Err(e) => {
33//!         // Pretty print the error
34//!         eprintln!("Uncaught {e}");
35//!         # panic!("There was an error in boa_engine's introduction example.");
36//!     }
37//! };
38//! ```
39//!
40//! # Crate Features
41//!
42//!  - **serde** - Enables serialization and deserialization of the AST (Abstract Syntax Tree).
43//!  - **profiler** - Enables profiling with measureme (this is mostly internal).
44//!  - **intl** - Enables `boa`'s [ECMA-402 Internationalization API][ecma-402] (`Intl` object)
45//!
46//! [ecma-402]: https://tc39.es/ecma402
47//! [examples]: https://github.com/boa-dev/boa/tree/main/examples
48#![doc = include_str!("../ABOUT.md")]
49#![doc(
50    html_logo_url = "https://raw.githubusercontent.com/boa-dev/boa/main/assets/logo_black.svg",
51    html_favicon_url = "https://raw.githubusercontent.com/boa-dev/boa/main/assets/logo_black.svg"
52)]
53#![cfg_attr(test, allow(clippy::needless_raw_string_hashes))] // Makes strings a bit more copy-pastable
54#![cfg_attr(not(test), forbid(clippy::unwrap_used))]
55#![allow(
56    // Currently throws a false positive regarding dependencies that are only used in benchmarks.
57    unused_crate_dependencies,
58    clippy::module_name_repetitions,
59    clippy::redundant_pub_crate,
60    clippy::too_many_lines,
61    clippy::cognitive_complexity,
62    clippy::missing_errors_doc,
63    clippy::let_unit_value,
64    clippy::option_if_let_else,
65
66    // It may be worth to look if we can fix the issues highlighted by these lints.
67    clippy::cast_possible_truncation,
68    clippy::cast_sign_loss,
69    clippy::cast_precision_loss,
70    clippy::cast_possible_wrap,
71
72    // Add temporarily - Needs addressing
73    clippy::missing_panics_doc,
74)]
75
76extern crate self as boa_engine;
77#[cfg(not(target_has_atomic = "ptr"))]
78compile_error!("Boa requires a lock free `AtomicUsize` in order to work properly.");
79
80pub use boa_ast as ast;
81pub use boa_gc as gc;
82pub use boa_interner as interner;
83pub use boa_parser as parser;
84
85pub mod bigint;
86pub mod builtins;
87pub mod bytecompiler;
88pub mod class;
89pub mod context;
90pub mod environments;
91pub mod error;
92pub mod interop;
93pub mod job;
94pub mod module;
95pub mod native_function;
96pub mod object;
97pub mod optimizer;
98pub mod property;
99pub mod realm;
100pub mod script;
101pub mod string;
102pub mod symbol;
103pub mod value;
104pub mod vm;
105
106mod host_defined;
107mod sys;
108
109mod spanned_source_text;
110use spanned_source_text::SourceText;
111pub use spanned_source_text::SpannedSourceText;
112
113#[cfg(test)]
114mod tests;
115
116/// A convenience module that re-exports the most commonly-used Boa APIs
117pub mod prelude {
118    pub use crate::{
119        bigint::JsBigInt,
120        context::Context,
121        error::{JsError, JsNativeError, JsNativeErrorKind},
122        host_defined::HostDefined,
123        interop::{IntoJsFunctionCopied, UnsafeIntoJsFunction},
124        module::{IntoJsModule, Module},
125        native_function::NativeFunction,
126        object::{JsData, JsObject, NativeObject},
127        script::Script,
128        string::{JsStr, JsString},
129        symbol::JsSymbol,
130        value::{JsValue, JsVariant, js_object, js_value},
131    };
132    pub use boa_gc::{Finalize, Trace};
133    pub use boa_macros::{JsData, js_str};
134    pub use boa_parser::Source;
135}
136
137#[doc(inline)]
138pub use boa_macros::{boa_class, boa_module, embed_module_inner as __embed_module_inner};
139
140use std::result::Result as StdResult;
141
142// Export things to root level
143#[doc(inline)]
144pub use prelude::*;
145
146#[doc(inline)]
147pub use boa_parser::Source;
148
149/// The result of a Javascript expression is represented like this so it can succeed (`Ok`) or fail (`Err`)
150pub type JsResult<T> = StdResult<T, JsError>;
151
152/// Create a [`JsResult`] from a Rust value. This trait is used to
153/// convert Rust types to JS types, including [`JsResult`] of
154/// Rust values and [`JsValue`]s.
155///
156/// This trait is implemented for any that can be converted into a [`JsValue`].
157pub trait TryIntoJsResult {
158    /// Try to convert a Rust value into a `JsResult<JsValue>`.
159    ///
160    /// # Errors
161    /// Any parsing errors that may occur during the conversion, or any
162    /// error that happened during the call to a function.
163    fn try_into_js_result(self, context: &mut Context) -> JsResult<JsValue>;
164}
165
166mod try_into_js_result_impls;
167
168/// A utility trait to make working with function arguments easier.
169pub trait JsArgs {
170    /// Utility function to `get` a parameter from a `[JsValue]` or default to
171    /// `JsValue::undefined()` if `get` returns `None`.
172    ///
173    /// Call this if you are thinking of calling something similar to
174    /// `args.get(n).cloned().unwrap_or_default()` or
175    /// `args.get(n).unwrap_or(&undefined)`.
176    ///
177    /// This returns a reference for efficiency, in case you only need to call methods of `JsValue`.
178    fn get_or_undefined(&self, index: usize) -> &JsValue;
179}
180
181impl JsArgs for [JsValue] {
182    fn get_or_undefined(&self, index: usize) -> &JsValue {
183        const UNDEFINED: &JsValue = &JsValue::undefined();
184        self.get(index).unwrap_or(UNDEFINED)
185    }
186}
187
188#[cfg(test)]
189use std::borrow::Cow;
190
191/// A test action executed in a test function.
192#[cfg(test)]
193#[derive(Clone)]
194struct TestAction(Inner);
195
196#[cfg(test)]
197#[derive(Clone)]
198enum Inner {
199    RunHarness,
200    Run {
201        source: Cow<'static, str>,
202    },
203    InspectContext {
204        op: fn(&mut Context),
205    },
206    Assert {
207        source: Cow<'static, str>,
208    },
209    AssertEq {
210        source: Cow<'static, str>,
211        expected: JsValue,
212    },
213    AssertWithOp {
214        source: Cow<'static, str>,
215        op: fn(JsValue, &mut Context) -> bool,
216    },
217    AssertOpaqueError {
218        source: Cow<'static, str>,
219        expected: JsValue,
220    },
221    AssertNativeError {
222        source: Cow<'static, str>,
223        kind: JsNativeErrorKind,
224        message: &'static str,
225    },
226    AssertContext {
227        op: fn(&mut Context) -> bool,
228    },
229}
230
231#[cfg(test)]
232impl TestAction {
233    /// Evaluates some utility functions used in tests.
234    const fn run_harness() -> Self {
235        Self(Inner::RunHarness)
236    }
237
238    /// Runs `source`, panicking if the execution throws.
239    fn run(source: impl Into<Cow<'static, str>>) -> Self {
240        Self(Inner::Run {
241            source: source.into(),
242        })
243    }
244
245    /// Executes `op` with the currently active context.
246    ///
247    /// Useful to make custom assertions that must be done from Rust code.
248    fn inspect_context(op: fn(&mut Context)) -> Self {
249        Self(Inner::InspectContext { op })
250    }
251
252    /// Asserts that evaluating `source` returns the `true` value.
253    fn assert(source: impl Into<Cow<'static, str>>) -> Self {
254        Self(Inner::Assert {
255            source: source.into(),
256        })
257    }
258
259    /// Asserts that the script returns `expected` when evaluating `source`.
260    fn assert_eq(source: impl Into<Cow<'static, str>>, expected: impl Into<JsValue>) -> Self {
261        Self(Inner::AssertEq {
262            source: source.into(),
263            expected: expected.into(),
264        })
265    }
266
267    /// Asserts that calling `op` with the value obtained from evaluating `source` returns `true`.
268    ///
269    /// Useful to check properties of the obtained value that cannot be checked from JS code.
270    fn assert_with_op(
271        source: impl Into<Cow<'static, str>>,
272        op: fn(JsValue, &mut Context) -> bool,
273    ) -> Self {
274        Self(Inner::AssertWithOp {
275            source: source.into(),
276            op,
277        })
278    }
279
280    /// Asserts that evaluating `source` throws the opaque error `value`.
281    fn assert_opaque_error(
282        source: impl Into<Cow<'static, str>>,
283        value: impl Into<JsValue>,
284    ) -> Self {
285        Self(Inner::AssertOpaqueError {
286            source: source.into(),
287            expected: value.into(),
288        })
289    }
290
291    /// Asserts that evaluating `source` throws a native error of `kind` and `message`.
292    fn assert_native_error(
293        source: impl Into<Cow<'static, str>>,
294        kind: JsNativeErrorKind,
295        message: &'static str,
296    ) -> Self {
297        Self(Inner::AssertNativeError {
298            source: source.into(),
299            kind,
300            message,
301        })
302    }
303
304    /// Asserts that calling `op` with the currently executing context returns `true`.
305    fn assert_context(op: fn(&mut Context) -> bool) -> Self {
306        Self(Inner::AssertContext { op })
307    }
308}
309
310/// Executes a list of test actions on a new, default context.
311#[cfg(test)]
312#[track_caller]
313fn run_test_actions(actions: impl IntoIterator<Item = TestAction>) {
314    let mut context = Context::builder();
315    if cfg!(miri) {
316        // Do not use OS APIs when running with Miri to avoid escaping the
317        // isolated sandbox.
318
319        use std::rc::Rc;
320
321        use crate::{context::time::FixedClock, module::IdleModuleLoader};
322
323        context = context
324            .clock(Rc::new(FixedClock::from_millis(65535)))
325            .module_loader(Rc::new(IdleModuleLoader));
326    }
327    run_test_actions_with(actions, &mut context.build().unwrap());
328}
329
330/// Executes a list of test actions on the provided context.
331#[cfg(test)]
332#[track_caller]
333fn run_test_actions_with(actions: impl IntoIterator<Item = TestAction>, context: &mut Context) {
334    #[track_caller]
335    fn forward_val(context: &mut Context, source: &str) -> JsResult<JsValue> {
336        context.eval(Source::from_bytes(source))
337    }
338
339    #[track_caller]
340    fn fmt_test(source: &str, test: usize) -> String {
341        format!(
342            "\n\nTest case {test}: \n```\n{}\n```",
343            textwrap::indent(source, "    ")
344        )
345    }
346
347    // Some unwrapping patterns look weird because they're replaceable
348    // by simpler patterns like `unwrap_or_else` or `unwrap_err
349    let mut i = 1;
350    for action in actions.into_iter().map(|a| a.0) {
351        match action {
352            Inner::RunHarness => {
353                // add utility functions for testing
354                // TODO: extract to a file
355                forward_val(
356                    context,
357                    r#"
358                        function equals(a, b) {
359                            if (Array.isArray(a) && Array.isArray(b)) {
360                                return arrayEquals(a, b);
361                            }
362                            return a === b;
363                        }
364                        function arrayEquals(a, b) {
365                            return Array.isArray(a) &&
366                                Array.isArray(b) &&
367                                a.length === b.length &&
368                                a.every((val, index) => equals(val, b[index]));
369                        }
370                    "#,
371                )
372                .expect("failed to evaluate test harness");
373            }
374            Inner::Run { source } => {
375                if let Err(e) = forward_val(context, &source) {
376                    panic!("{}\nUncaught {e}", fmt_test(&source, i));
377                }
378            }
379            Inner::InspectContext { op } => {
380                op(context);
381            }
382            Inner::Assert { source } => {
383                let val = match forward_val(context, &source) {
384                    Err(e) => panic!("{}\nUncaught {e}", fmt_test(&source, i)),
385                    Ok(v) => v,
386                };
387                let Some(val) = val.as_boolean() else {
388                    panic!(
389                        "{}\nTried to assert with the non-boolean value `{}`",
390                        fmt_test(&source, i),
391                        val.display()
392                    )
393                };
394                assert!(val, "{}", fmt_test(&source, i));
395                i += 1;
396            }
397            Inner::AssertEq { source, expected } => {
398                let val = match forward_val(context, &source) {
399                    Err(e) => panic!("{}\nUncaught {e}", fmt_test(&source, i)),
400                    Ok(v) => v,
401                };
402                assert_eq!(val, expected, "{}", fmt_test(&source, i));
403                i += 1;
404            }
405            Inner::AssertWithOp { source, op } => {
406                let val = match forward_val(context, &source) {
407                    Err(e) => panic!("{}\nUncaught {e}", fmt_test(&source, i)),
408                    Ok(v) => v,
409                };
410                assert!(op(val, context), "{}", fmt_test(&source, i));
411                i += 1;
412            }
413            Inner::AssertOpaqueError { source, expected } => {
414                let err = match forward_val(context, &source) {
415                    Ok(v) => panic!(
416                        "{}\nExpected error, got value `{}`",
417                        fmt_test(&source, i),
418                        v.display()
419                    ),
420                    Err(e) => e,
421                };
422                let Some(err) = err.as_opaque() else {
423                    panic!(
424                        "{}\nExpected opaque error, got native error `{}`",
425                        fmt_test(&source, i),
426                        err
427                    )
428                };
429
430                assert_eq!(err, &expected, "{}", fmt_test(&source, i));
431                i += 1;
432            }
433            Inner::AssertNativeError {
434                source,
435                kind,
436                message,
437            } => {
438                let err = match forward_val(context, &source) {
439                    Ok(v) => panic!(
440                        "{}\nExpected error, got value `{}`",
441                        fmt_test(&source, i),
442                        v.display()
443                    ),
444                    Err(e) => e,
445                };
446                let native = match err.try_native(context) {
447                    Ok(err) => err,
448                    Err(e) => panic!(
449                        "{}\nCouldn't obtain a native error: {e}",
450                        fmt_test(&source, i)
451                    ),
452                };
453
454                assert_eq!(&native.kind, &kind, "{}", fmt_test(&source, i));
455                assert_eq!(native.message(), message, "{}", fmt_test(&source, i));
456                i += 1;
457            }
458            Inner::AssertContext { op } => {
459                assert!(op(context), "Test case {i}");
460                i += 1;
461            }
462        }
463    }
464}