Skip to main content

Boa release v0.17

· 12 min read

Summary

Boa v0.17 is now available! This is one of the biggest Boa releases since the project started, and after around 7 months of development, we are very happy to present you the latest release of the Boa JavaScript engine. Boa makes it easy to embed a JS engine in your projects, and you can even use it from WebAssembly. See the about page for more info.

In this release, our conformance has grown from 74.53% to 78.74% in the official ECMAScript Test Suite (Test262). While this might look as a small increase, we now pass 6,079 more tests than in the previous version. In any case, the big changes in this release are not related to conformance, but to huge internal enhancements and new APIs that you will be able to use.

You can check the full list of changes here, and the full information on conformance here.

Moreover, this big release was partly possible thanks to a grant by Lit Protocol. Thanks to this grant, we were able to remunerate 2 team members for their 20h/week work each during three and a half months. If you wish to sponsor Boa, you can do so by donating to our open collective. You can also check easy or good first issues.

Furthermore, we now have a new domain for Boa, boajs.dev.

Highlights

Modules

Boa finally has a module system! This implementation tries to closely follow ECMAScript's Modules specification which includes some useful hooks to customize module loading, making it possible to load modules from several sources, fetch modules from an URL and even asynchronously load and parse them to avoid blocking execution; see the ModuleLoader for more information.

We also implemented a simple loader (currently the default module loader), which should fulfill most of the simpler use cases:

// Creates a new module loader that uses the current directory to resolve module imports.
let loader = &SimpleModuleLoader::new(Path::new(".")).unwrap();

// Need to convert it to either a `&dyn ModuleLoader` or a `Rc<dyn ModuleLoader>` in order
// to pass it to the context.
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();

// `main.mjs` or any of its imports could import `main.mjs` itself, so we
// insert it into the loader for good measure.
loader.insert(Path::new("main.mjs").to_path_buf(), module.clone());

// All modules use promises to signal completion of its lifecycle.
// The utility method `load_link_evaluate` calls `load`, then `link` and
// finally `evaluate`, returning an error if any call fails.
let promise = module.load_link_evaluate(context).unwrap();

// Important to push the job queue forward! Otherwise, the modules won't progress
// on their lifecycle.
context.run_jobs();

// All modules return `undefined` if they're successfully evaluated.
assert_eq!(promise.state().unwrap(), PromiseState::Fulfilled(JsValue::undefined()));

For a more extensive, descriptive example that uses a real directory, you can check out boa_examples.

Spec Version Conformance

Something we get asked a lot is "Do you support ES5 or ES6"? or "How far away are you from supporting ESX"? We're pleased to say we've updated our conformance board to show you how we're doing across ES versions.

Just navigate to our Test262 Dashboard, select "Test Results" on our main branch, and then you can use the dropdown underneath to see how we're doing on each version. ES5 and ES6 are very close, you can see we're only a few tests away from them being fully implemented.

Optimizations

Constant folding optimization

Constant folding expression is a powerful compiler optimization technique that significantly enhances the efficiency and performance of compiled programs. This optimization, now incorporated in the latest release, aims to reduce runtime overhead by evaluating constant expressions at compile-time.

With constant folding expression optimization, the compiler analyzes expressions involving constants and replaces them with their computed results. This process allows the compiler to transform arithmetic operations, comparisons, and logical expressions into simplified forms, removing unnecessary runtime computations. By eliminating these computations, the optimized program benefits from reduced execution time and improved overall performance.

Object Shapes (Hidden classes)

Hidden Classes (called "Shapes" internally to avoid confusion with JavaScript classes) are an alternative way to structure objects that stores the property keys (string or symbol) (i.e. object.propertyName) and its attributes (writable, enumerable, configurable) as transitions from a root shape, and the values as a dense array. This is different from the traditional way of storing properties as a hashmap from property keys to values.

The shapes create a transition tree, where the transitions are property names and prototype changes starting from a root shape (no properties, no prototype).

let o = {} // Shape 1: prototype `Object.prototype` and properties: empty
o.a = 10 // Shape 2: prototype `Object.prototype` and properties: 'a'
o.b = 20 // Shape 3: prototype `Object.prototype` and properties: 'a', 'b'

let o2 = { a: 30; } // Shape 2: prototype `Object.prototype` and properties: 'a'
o2.d = 50 // Shape 4: prototype `Object.prototype` and properties: 'a', 'd' -- fork from shape 2

This separation of property keys and values allows for objects with the same property names to share the same shape, which reduces memory consumption and unlocks the possibility for other optimizations such as inline caching.

Note: When creating objects with the same property keys, it's best to create them in the same order, this ensures that the objects share the same shape.

For a more in depth explanation of how shared shapes work in boa see shapes.md here.

Debug object

The $boa debug object has been implemented for convenient JavaScript debugging using Boa's CLI interface. If you want to use it, you will need to run the Boa CLI / REPL with the --debug-object command line flag.

The $boa debug object is divided into modules, so that you can trigger the garbage collection with $boa.gc.collect(), or get the bytecode of a function by running $boa.function.bytecode(fn_name). You can also trace function invocations, handle compiler optimizations, set runtime limits and inspect object shapes.

You can find all the documentation here.

New APIs

We have added new built-in object wrappers, such as JsPromise, JsRegExp, JsGenerator, JsDate and JsDataView. You can check all of them here.

We also want to present you a new trait that we have developed to make it easier for you to interoperate between Rust and JavaScript: TryFromJs. All built-ins and Rust basic types that exist in JavaScript implement this trait, and it adds a new static method to them that allows you to convert a [JsValue][js_value] into a Rust structure. You can also convert any JsValue to a TryFromJs Rust type with JsValue::try_js_into() function.

let js_str = r#"
let x = /[a-z0-9]@[a-z0-9]/;
x;
"#;

let js = Source::from_bytes(js_str);
let mut context = Context::default();
let res = context.eval(js).unwrap();

let rs_regexp: JsRegExp = res.try_js_into(context).unwrap();

let test_result = rs_regexp.test("hello@domain", context)?;
assert!(test_result);

Moreover, you can derive TryFromJs for any Rust structure, and in the case that you want to manually convert some of the struct attributes, you can override it:

/// Converts the value lossly.
fn lossy_conversion(value: &JsValue, _context: &mut Context) -> JsResult<i16> {
match value {
JsValue::Rational(r) => Ok(r.round() as i16),
JsValue::Integer(i) => Ok(*i as i16),
_ => Err(JsNativeError::typ().with_message("cannot convert value to an i16").into()),
}
}

#[derive(Debug, TryFromJs)]
struct TestStruct {
inner: bool,
hello: String,
// You can override the conversion of an attribute.
#[boa(from_js_with = "lossy_conversion")]
my_float: i16,
}

let js_str = r#"
let x = {
inner: false,
hello: "World",
my_float: 2.9,
};
x;
"#;
let context = &mut Context::default();
let result = context.eval(Source::from_bytes(js_str))?;
let str = TestStruct::try_from_js(&result, context)?;

println!("{str:?}");

Source API

We have introduced a new Source API to Boa. The new API represents JavaScript stored from a path or None if it's coming from a plain string.

This change improves the display of boa_tester to show the path of the tests being run. It also enables hyperlinks to directly jump to the tested file from the VS terminal. This will further help with error displays and debugging in the future.

use boa_engine::{Context, Source};

fn main() {
let js_file_path = "./scripts/helloworld.js";

match Source::from_filepath(Path::new(js_file_path)) {
...

See Boa's examples for more examples on how its used.

Hooks and Job Queues

In this release we have added HostHooks and JobQueue traits to Context. This will allow hosts to implement custom event loops and other host specific functionality. This makes Boa more configurable for users and any future runtimes which need to add a more complex event loop, such as Tokio or Mio.

As a result of this change, Boa's CLI will run all jobs until the queue is empty, even if a Job returns an Err.`

New Builtins

Intl

Boa now has internationalization support! Although we are still working on full compliance with the ecma402 specification, we have a couple of Intl utilities in place:

Internationalization data can be pretty expensive at times: the default data included by Boa is 10.6 MB, which is why we allow customizing the data provider used by the engine with the ContextBuilder::icu_provider hook. For more information on how to generate custom internationalization data, you can check out the data management tutorial from icu4x, the internationalization library used in Boa. Shoutout to the icu4x team, who are the ones that made all of this possible!

Additionally, we added an intl feature flag, which is enabled by default but can be disabled to reduce Boa's binary size.

WeakRef, WeakSet and WeakMap

We've implemented support for weak references to garbage collected objects. This allowed us to implement some builtins like WeakRef, WeakSet and WeakMap. However, garbage collectors are unpredictable! A garbage collector could collect at unexpected moments, extend the lifetime of unreachable objects and even leak, which is why mozilla recommends avoiding using those builtins where possible.

Fuzzing

This release of Boa contains new functionalities in the boa_ast crate to support grammar aware fuzzing. The visitor pattern that is implemented for the AST makes it easy to traverse the AST and either collect information or apply modifications. In addition to the fuzzer, we also use the visitor pattern in multiple syntax directed operations. The AST now implements the Arbitrary trait from the Arbitrary crate to generate inputs for fuzzers. Based on these features we currently have three fuzzers targeting the parser, bytecompiler and vm. The fuzzers have already helped us finding multiple panics that we previously had no tests for.

We want to extend a huge thanks to @addisoncrump as they have contributed not only the fuzzers but also the visitor pattern implementation and the additional bits needed to successfully fuzz Boa.

New Crates

This release of Boa will also mark the release of some new boa crates that contain various aspects of Boa's ECMAScript implementation.

boa_parser

Boa's boa_parser crate contains a lexer and parser that targets the latest ECMAScript language specification.

boa_ast

Boa's boa_ast crate contains an ECMAScript abstract syntax tree implementation of Declaration, Statement, and Expression Parse Nodes.

boa_runtime

Boa's boa_runtime crate contains an example runtime along with basic runtime features and functionality for runtime implementors. Note: this crate will contain any WEB API feature implementations or APIs that are not designated by the ECMAScript specification.

Other internal enhancements

There have also been a various number of other internal enhancements made.

Split Node into Statement / Declaration / Expression

In the last release, Boa's AST used a Node enum to represent both the Statement, Declaration and Expression parse nodes. One of the large internal improvements made for this release was to split Node into Statement, Declaration, and Expression nodes. This refactor involved not only large changes to the AST but also further changes to the bytecompiler and parser. The split also brings us closer in line with the ECMAScript specification.

UTF-16 strings

With this release, Boa's JsStrings are now implemented as utf-16 encoded strings. Along with the new JsString, there are two provided macros: js_string! for creating a new JsString from a &str, and utf16! for creating a utf-16 array literal from a &str.

You can create a utf-16 array literal from any utf-8 str.

const HELLO: &[u16] = utf16!("Hi! :)");

You can create a JsString from a string literal with the js_string macro.

let hw = js_string!("Hello, world!");
assert_eq!(&hw, utf16!("Hello, world!"));

You can also pass any number of &[u16] string values as arguments to create a new JsString.

const NAME: &[u16]  = utf16!("human! ");
let greeting = js_string!("Hello, ");
let msg = js_string!(&greeting, &NAME, utf16!("Nice to meet you!"));

assert_eq!(&msg, utf16!("Hello, human! Nice to meet you!"));

Conclusions

If you reached so far, you probably understand how big this release was, and you can find even more changes in the full changelog. Boa is now becoming a real option for many projects, which shows with the amount of financial support we have received these last months. Nevertheless, going forward, we need your help to get to a 1.0 version. Whether you are good with Rust, JavaScript, documentation or development, we have multiple good first issues, and places where we need help, both in Boa's main repository and others around it.

Once again, big thanks to all the contributors of this release!!