Skip to main content

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;
97/// ECMAScript Abstract Syntax Tree (AST) optimizer.
98pub mod optimizer;
99pub mod property;
100pub mod realm;
101pub mod script;
102pub mod string;
103pub mod symbol;
104pub mod value;
105pub mod vm;
106
107mod host_defined;
108mod sys;
109
110mod spanned_source_text;
111use spanned_source_text::SourceText;
112pub use spanned_source_text::SpannedSourceText;
113
114#[cfg(test)]
115mod tests;
116
117/// A convenience module that re-exports the most commonly-used Boa APIs
118pub mod prelude {
119    pub use crate::{
120        bigint::JsBigInt,
121        context::Context,
122        error::{EngineError, JsError, JsNativeError, JsNativeErrorKind, RuntimeLimitError},
123        host_defined::HostDefined,
124        interop::{IntoJsFunctionCopied, UnsafeIntoJsFunction},
125        module::{IntoJsModule, Module},
126        native_function::NativeFunction,
127        object::{JsData, JsObject, NativeObject},
128        script::Script,
129        string::{JsStr, JsString},
130        symbol::JsSymbol,
131        value::{JsValue, JsVariant, js_object, js_value},
132    };
133    pub use boa_gc::{Finalize, Trace};
134    pub use boa_macros::{JsData, js_str};
135    pub use boa_parser::Source;
136}
137
138#[doc(inline)]
139pub use boa_macros::{boa_class, boa_module, embed_module_inner as __embed_module_inner};
140
141use crate::error::PanicError;
142use std::result::Result as StdResult;
143
144// Export things to root level
145#[doc(inline)]
146pub use prelude::*;
147
148#[doc(inline)]
149pub use boa_parser::Source;
150
151/// The result of a Javascript expression is represented like this so it can succeed (`Ok`) or fail (`Err`)
152pub type JsResult<T> = StdResult<T, JsError>;
153
154/// Create a [`JsResult`] from a Rust value. This trait is used to
155/// convert Rust types to JS types, including [`JsResult`] of
156/// Rust values and [`JsValue`]s.
157///
158/// This trait is implemented for any that can be converted into a [`JsValue`].
159pub trait TryIntoJsResult {
160    /// Try to convert a Rust value into a `JsResult<JsValue>`.
161    ///
162    /// # Errors
163    /// Any parsing errors that may occur during the conversion, or any
164    /// error that happened during the call to a function.
165    fn try_into_js_result(self, context: &mut Context) -> JsResult<JsValue>;
166}
167
168mod try_into_js_result_impls;
169
170/// A utility trait to make working with function arguments easier.
171pub trait JsArgs {
172    /// Utility function to `get` a parameter from a `[JsValue]` or default to
173    /// `JsValue::undefined()` if `get` returns `None`.
174    ///
175    /// Call this if you are thinking of calling something similar to
176    /// `args.get(n).cloned().unwrap_or_default()` or
177    /// `args.get(n).unwrap_or(&undefined)`.
178    ///
179    /// This returns a reference for efficiency, in case you only need to call methods of `JsValue`.
180    fn get_or_undefined(&self, index: usize) -> &JsValue;
181}
182
183impl JsArgs for [JsValue] {
184    fn get_or_undefined(&self, index: usize) -> &JsValue {
185        const UNDEFINED: &JsValue = &JsValue::undefined();
186        self.get(index).unwrap_or(UNDEFINED)
187    }
188}
189
190/// Utility trait to "expect" a `JsResult`, but returning a `PanicError` instead of panicking.
191#[allow(dead_code)]
192pub(crate) trait JsExpect<V> {
193    /// "expects" a `JsResult`, wrapping the error with a `PanicError`.
194    fn js_expect<S: Into<Box<str>>>(self, msg: S) -> StdResult<V, PanicError>;
195}
196
197impl<V> JsExpect<V> for JsResult<V> {
198    fn js_expect<S: Into<Box<str>>>(self, msg: S) -> StdResult<V, PanicError> {
199        self.map_err(|err| PanicError::new(msg).with_source(err))
200    }
201}
202
203impl<V> JsExpect<V> for Option<V> {
204    fn js_expect<S: Into<Box<str>>>(self, msg: S) -> StdResult<V, PanicError> {
205        self.ok_or_else(|| PanicError::new(msg))
206    }
207}
208
209#[cfg(test)]
210use std::{borrow::Cow, pin::Pin};
211
212#[cfg(test)]
213type PinBoxFuture<'a> = Pin<Box<dyn Future<Output = ()> + 'a>>;
214
215/// A test action executed in a test function.
216#[cfg(test)]
217struct TestAction(Inner);
218
219#[cfg(test)]
220enum Inner {
221    RunHarness,
222    Run {
223        source: Cow<'static, str>,
224    },
225    InspectContext {
226        op: fn(&mut Context),
227    },
228    InspectContextAsync {
229        op: Box<dyn for<'a> FnOnce(&'a mut Context) -> PinBoxFuture<'a>>,
230    },
231    Assert {
232        source: Cow<'static, str>,
233    },
234    AssertEq {
235        source: Cow<'static, str>,
236        expected: JsValue,
237    },
238    AssertWithOp {
239        source: Cow<'static, str>,
240        op: fn(JsValue, &mut Context) -> bool,
241    },
242    AssertOpaqueError {
243        source: Cow<'static, str>,
244        expected: JsValue,
245    },
246    AssertNativeError {
247        source: Cow<'static, str>,
248        kind: JsNativeErrorKind,
249        message: &'static str,
250    },
251    AssertContext {
252        op: fn(&mut Context) -> bool,
253    },
254    AssertEngineError {
255        source: Cow<'static, str>,
256        error: EngineError,
257    },
258}
259
260#[cfg(test)]
261impl TestAction {
262    /// Evaluates some utility functions used in tests.
263    const fn run_harness() -> Self {
264        Self(Inner::RunHarness)
265    }
266
267    /// Runs `source`, panicking if the execution throws.
268    fn run(source: impl Into<Cow<'static, str>>) -> Self {
269        Self(Inner::Run {
270            source: source.into(),
271        })
272    }
273
274    /// Executes `op` with the currently active context.
275    ///
276    /// Useful to make custom assertions that must be done from Rust code.
277    fn inspect_context(op: fn(&mut Context)) -> Self {
278        Self(Inner::InspectContext { op })
279    }
280
281    /// Executes `op` with the currently active context in an async environment.
282    pub(crate) fn inspect_context_async(op: impl AsyncFnOnce(&mut Context) + 'static) -> Self {
283        Self(Inner::InspectContextAsync {
284            op: Box::new(move |ctx| Box::pin(op(ctx))),
285        })
286    }
287
288    /// Asserts that evaluating `source` returns the `true` value.
289    fn assert(source: impl Into<Cow<'static, str>>) -> Self {
290        Self(Inner::Assert {
291            source: source.into(),
292        })
293    }
294
295    /// Asserts that the script returns `expected` when evaluating `source`.
296    fn assert_eq(source: impl Into<Cow<'static, str>>, expected: impl Into<JsValue>) -> Self {
297        Self(Inner::AssertEq {
298            source: source.into(),
299            expected: expected.into(),
300        })
301    }
302
303    /// Asserts that calling `op` with the value obtained from evaluating `source` returns `true`.
304    ///
305    /// Useful to check properties of the obtained value that cannot be checked from JS code.
306    fn assert_with_op(
307        source: impl Into<Cow<'static, str>>,
308        op: fn(JsValue, &mut Context) -> bool,
309    ) -> Self {
310        Self(Inner::AssertWithOp {
311            source: source.into(),
312            op,
313        })
314    }
315
316    /// Asserts that evaluating `source` throws the opaque error `value`.
317    fn assert_opaque_error(
318        source: impl Into<Cow<'static, str>>,
319        value: impl Into<JsValue>,
320    ) -> Self {
321        Self(Inner::AssertOpaqueError {
322            source: source.into(),
323            expected: value.into(),
324        })
325    }
326
327    /// Asserts that evaluating `source` throws a native error of `kind` and `message`.
328    fn assert_native_error(
329        source: impl Into<Cow<'static, str>>,
330        kind: JsNativeErrorKind,
331        message: &'static str,
332    ) -> Self {
333        Self(Inner::AssertNativeError {
334            source: source.into(),
335            kind,
336            message,
337        })
338    }
339
340    /// Asserts that evaluating `source` throws a runtime limit error.
341    fn assert_runtime_limit_error(
342        source: impl Into<Cow<'static, str>>,
343        error: RuntimeLimitError,
344    ) -> Self {
345        Self(Inner::AssertEngineError {
346            source: source.into(),
347            error: error.into(),
348        })
349    }
350
351    /// Asserts that calling `op` with the currently executing context returns `true`.
352    fn assert_context(op: fn(&mut Context) -> bool) -> Self {
353        Self(Inner::AssertContext { op })
354    }
355}
356
357/// Executes a list of test actions on a new, default context.
358#[cfg(test)]
359#[track_caller]
360fn run_test_actions(actions: impl IntoIterator<Item = TestAction>) {
361    let mut context = Context::builder();
362    if cfg!(miri) {
363        // Do not use OS APIs when running with Miri to avoid escaping the
364        // isolated sandbox.
365
366        use std::rc::Rc;
367
368        use crate::{context::time::FixedClock, module::IdleModuleLoader};
369
370        context = context
371            .clock(Rc::new(FixedClock::from_millis(65535)))
372            .module_loader(Rc::new(IdleModuleLoader));
373    }
374    run_test_actions_with(actions, &mut context.build().unwrap());
375}
376
377/// Executes a list of test actions on the provided context.
378#[cfg(test)]
379#[track_caller]
380fn run_test_actions_with(actions: impl IntoIterator<Item = TestAction>, context: &mut Context) {
381    #[track_caller]
382    fn forward_val(context: &mut Context, source: &str) -> JsResult<JsValue> {
383        context.eval(Source::from_bytes(source))
384    }
385
386    #[track_caller]
387    fn fmt_test(source: &str, test: usize) -> String {
388        format!(
389            "\n\nTest case {test}: \n```\n{}\n```",
390            textwrap::indent(source, "    ")
391        )
392    }
393
394    // Some unwrapping patterns look weird because they're replaceable
395    // by simpler patterns like `unwrap_or_else` or `unwrap_err
396    let mut i = 1;
397    for action in actions.into_iter().map(|a| a.0) {
398        match action {
399            Inner::RunHarness => {
400                // add utility functions for testing
401                // TODO: extract to a file
402                forward_val(
403                    context,
404                    r#"
405                        function equals(a, b) {
406                            if (Array.isArray(a) && Array.isArray(b)) {
407                                return arrayEquals(a, b);
408                            }
409                            return a === b;
410                        }
411                        function arrayEquals(a, b) {
412                            return Array.isArray(a) &&
413                                Array.isArray(b) &&
414                                a.length === b.length &&
415                                a.every((val, index) => equals(val, b[index]));
416                        }
417                    "#,
418                )
419                .expect("failed to evaluate test harness");
420            }
421            Inner::Run { source } => {
422                if let Err(e) = forward_val(context, &source) {
423                    panic!("{}\nUncaught {e}", fmt_test(&source, i));
424                }
425            }
426            Inner::InspectContext { op } => {
427                op(context);
428            }
429            Inner::InspectContextAsync { op } => futures_lite::future::block_on(op(context)),
430            Inner::Assert { source } => {
431                let val = match forward_val(context, &source) {
432                    Err(e) => panic!("{}\nUncaught {e}", fmt_test(&source, i)),
433                    Ok(v) => v,
434                };
435                let Some(val) = val.as_boolean() else {
436                    panic!(
437                        "{}\nTried to assert with the non-boolean value `{}`",
438                        fmt_test(&source, i),
439                        val.display()
440                    )
441                };
442                assert!(val, "{}", fmt_test(&source, i));
443                i += 1;
444            }
445            Inner::AssertEq { source, expected } => {
446                let val = match forward_val(context, &source) {
447                    Err(e) => panic!("{}\nUncaught {e}", fmt_test(&source, i)),
448                    Ok(v) => v,
449                };
450                assert_eq!(val, expected, "{}", fmt_test(&source, i));
451                i += 1;
452            }
453            Inner::AssertWithOp { source, op } => {
454                let val = match forward_val(context, &source) {
455                    Err(e) => panic!("{}\nUncaught {e}", fmt_test(&source, i)),
456                    Ok(v) => v,
457                };
458                assert!(op(val, context), "{}", fmt_test(&source, i));
459                i += 1;
460            }
461            Inner::AssertOpaqueError { source, expected } => {
462                let err = match forward_val(context, &source) {
463                    Ok(v) => panic!(
464                        "{}\nExpected error, got value `{}`",
465                        fmt_test(&source, i),
466                        v.display()
467                    ),
468                    Err(e) => e,
469                };
470                let Some(err) = err.as_opaque() else {
471                    panic!(
472                        "{}\nExpected opaque error, got native error `{}`",
473                        fmt_test(&source, i),
474                        err
475                    )
476                };
477
478                assert_eq!(err, &expected, "{}", fmt_test(&source, i));
479                i += 1;
480            }
481            Inner::AssertNativeError {
482                source,
483                kind,
484                message,
485            } => {
486                let err = match forward_val(context, &source) {
487                    Ok(v) => panic!(
488                        "{}\nExpected error, got value `{}`",
489                        fmt_test(&source, i),
490                        v.display()
491                    ),
492                    Err(e) => e,
493                };
494                let native = match err.try_native(context) {
495                    Ok(err) => err,
496                    Err(e) => panic!(
497                        "{}\nCouldn't obtain a native error: {e}",
498                        fmt_test(&source, i)
499                    ),
500                };
501
502                assert_eq!(native.kind(), &kind, "{}", fmt_test(&source, i));
503                assert_eq!(native.message(), message, "{}", fmt_test(&source, i));
504                i += 1;
505            }
506            Inner::AssertEngineError { source, error } => {
507                let err = match forward_val(context, &source) {
508                    Ok(v) => panic!(
509                        "{}\nExpected error, got value `{}`",
510                        fmt_test(&source, i),
511                        v.display()
512                    ),
513                    Err(e) => e,
514                };
515
516                let Some(err) = err.as_engine() else {
517                    panic!(
518                        "{}\nExpected a native error, got {err}",
519                        fmt_test(&source, i)
520                    )
521                };
522
523                assert_eq!(*err, error);
524                i += 1;
525            }
526            Inner::AssertContext { op } => {
527                assert!(op(context), "Test case {i}");
528                i += 1;
529            }
530        }
531    }
532}