Skip to main content

· 20 min read

Summary

Boa v0.18 is now available! After 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 79.36% to 85.03% in the official ECMAScript Test Suite (Test262). This means we now pass 3,550 more tests than in the previous version. Moreover, our amount of ignored tests decreased from 9,496 to 1,391 thanks to all the new builtins we have implemented for this release.

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

You probably noticed that something seems different... This release marks a major update to the design of our website, and the introduction of our new logo! We'd like to thank @ZackMitkin for being the one that started the work on this nifty redesign, and @kelbazz for designing the logo. We're planning to add some additional pages to learn more about the APIs that Boa exposes. Additionally, expect some more blog posts from us in the future! We would like to write about how to use certain APIs, design challenges that we encountered while developing the engine, and internal implementation details. Subscribe to our RSS feed if you're interested in staying up to date!

This big release was partly possible thanks to those who have supported us. Thanks to funds we've received we have been able to renew our domain name, remunerate members of the team who have worked on the features released, and discuss the possibility of using dedicated servers for benchmarking. 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 if you want to contribute some code instead.

Highlights

We're on test262.fyi

Thanks to the amazing work of CanadaHonk, Boa is now listed on test262.fyi! This is a daily runner of the official test262 test suite that runs a nightly build of Boa, along with other engines, and updates the results every day. This is using the tip of the main branch of Boa alongside the latest test262 changes pushed to their main branch.

This is a great achievement for us and we are very proud to be listed alongside other major JavaScript engines. It should be much easier for users to compare Boa's conformance tests with other engines.

Temporal

A lot of work has been put over the past few months on the Temporal API. The Temporal API is a new set of built-in objects and functions that is designed to be a more modern replacement for the Date object, providing a more feature-rich and flexible API for working with dates and times.

It is currently a stage 3 proposal and we are working alongside the TC39 champions to put together a solid implementation. Since Temporal is such an extensive specification, we have done most of the work outside of Boa so that it can be used in other projects. This work can be found in the temporal_rs repository.

We hope to release a full blog post on Temporal in the future, but for now, let's look at a couple small examples of Temporal.

In JavaScript:

// JavaScript's Temporal built-in object.

// For example, you can customize you're own calendar implementations!
class CustomCalendar extends Temporal.Calendar {
constructor() {
super("iso8601");
}
inLeapYear(dateLike) {
messageInACalendar = "It's a message in a Calendar!";
return dateLike.daysInYear === 366;
}
}

let messageInACalendar;
// Construct the CustomCalendar.
const calendar = new CustomCalendar();

const boaReleaseDay = new Temporal.PlainDate(2024, 3, 7, calendar);
const leap = boaReleaseDay.inLeapYear;

messageInACalendar;
// Outputs: "It's a message in a Calendar!"

In Rust:

// Rust's `temporal_rs` crate
use temporal_rs::{components::{calendar::CalendarSlot, Date}, options::ArithmeticOverflow };
use std::str::FromStr;

// Create a Calendar slot value from a string
let calendar = CalendarSlot::<()>::from_str("iso8601").unwrap();

// Create a date. The date can be made to either reject or constrain the input.
let date = Date::<()>::new(2024, 3, 7, calendar, ArithmeticOverflow::Reject).unwrap();

assert_eq!(date.iso_year(), date.year().unwrap());

Please note that Temporal is still an experimental feature, and while a lot of progress has been made, there is still more work to be completed until it is production ready.

If you're interested in learning more or want to contribute to the native Rust implementation of Temporal, feel free to check out temporal_rs's issues!

RegExp

Over the past 7 months there has been some effort poured into an improved implementation of RegExp. This includes:

  • Support for RegExp.prototype.hasIndices (Thanks to @dirkdev98!).
  • Support for Unicode sets, aka the v flag.
  • Support for UTF-16 text searches.
  • General fixes around RegExp(), RegExp.toString() and RegExp.match().

Here is a table showing the progress of RegExp between v0.17 and v0.18:

Test262v0.17 (July 2023)v0.18 (Feb 2024)
Total1,9151,920
Pass1,0711,878
Fail1322
Skipped71240

That's a whopping 807 more tests passed!

We only have two failing tests left and both are caused by the lack of Unicode 15.1 support. The remaining skipped tests are all related to stage 3 proposals.

Shared Array Buffer + Atomics

The SharedArrayBuffer and Atomics builtins have been implemented in this release. This means embedders can now orchestrate Contexts running on separate threads to execute shared work between them.

The Atomics builtin object contains several static methods that allow executing atomic operations on shared memory. In addition to that, it also contains the wait() and notify() methods, which offers the same functionality as Linux futexes for JS's worker threads:

// On the main thread
const sab = new SharedArrayBuffer(1024);
const int32 = new Int32Array(sab);
send(worker1, int32);
send(worker2, int32);

// On worker1
int32 = receive();
Atomics.wait(int32, 0, 0);
console.log(int32[0]); // 123

// On worker2
int32 = receive();
console.log(int32[0]); // 0
Atomics.store(int32, 0, 123);
Atomics.notify(int32, 0, 1);

Intl updates

We're keeping the good progress on our Intl implementation, and now we have the Intl.PluralRules builtin and (a first prototype of) the Intl.NumberFormat builtin in place.

As mentioned by the Mozilla docs:

Languages use different patterns for expressing both plural numbers of items (cardinal numbers) and for expressing the order of items (ordinal numbers). English has two forms for expressing cardinal numbers: one for the singular "item" (1 hour, 1 dog, 1 fish) and the other for zero or any other number of "items" (0 hours, 2 lemmings, 100000.5 fish), while Chinese has only one form, and Arabic has six! Similarly, English has four forms for expressing ordinal numbers: "th", "st", "nd", "rd", giving the sequence: 0th, 1st, 2nd, 3rd, 4th, 5th, ..., 21st, 22nd, 23rd, 24th, 25th, and so on, while both Chinese and Arabic only have one form for ordinal numbers.

This variation between languages makes it really hard to properly localize a cardinal or ordinal number. To fix this, the CLDR (Common Locale Data Repository) project has been collecting information about the "plural category" of certain numeric patterns on many languages, and Intl.PluralRules objects are the builtin objects that enable obtaining this information in an easy way:

const pr = new Intl.PluralRules("en-US", { type: "ordinal" });

const suffixes = new Map([
["one", "st"],
["two", "nd"],
["few", "rd"],
["other", "th"],
]);

const getSuffix = (n) => {
return suffixes.get(pr.select(n));
};

console.log(getSuffix(0)); // "th"
console.log(getSuffix(1)); // "st"
console.log(getSuffix(2)); // "nd"
console.log(getSuffix(3)); // "rd"
console.log(getSuffix(4)); // "th"

console.log(getSuffix(21)); // "st"
console.log(getSuffix(42)); // "nd"
console.log(getSuffix(73)); // "th"

On the same vein, Intl.NumberFormat objects can format numbers in a language-sensitive way:

const nf = new Intl.NumberFormat("bn", {
useGrouping: "min2",
minimumSignificantDigits: 3,
maximumSignificantDigits: 7,
});

console.log(nf.format(10003.1234)); // ১০,০০৩.১২

However, we need to mention that Intl.NumberFormat is NOT feature complete at the moment, since it only allows formatting numbers in the standard notation with no currencies or units. We're still working on adding the missing features, but we hope that this initial prototype is at least useful for some use cases.

Builtins updates

While this new release is filled with shiny new features and APIs, it should be noted that the ECMAScript 262 specification is constantly evolving, which is why there are also a lot of small changes and additions to existing builtins that keep Boa updated to the latest revisions of the specification.

All examples were taken from the Mozilla Web Docs.

findLast and findLastIndex on TypedArray

function isPrime(element) {
if (element % 2 === 0 || element < 2) {
return false;
}
for (let factor = 3; factor <= Math.sqrt(element); factor += 2) {
if (element % factor === 0) {
return false;
}
}
return true;
}

let uint8 = new Uint8Array([4, 6, 8, 12]);
console.log(uint8.findLast(isPrime)); // undefined (no primes in array)
uint8 = new Uint8Array([4, 5, 7, 8, 9, 11, 12]);
console.log(uint8.findLast(isPrime)); // 11

String.prototype.isWellFormed and String.prototype.toWellFormed

const illFormed = "https://example.com/search?q=\uD800";

try {
encodeURI(illFormed);
} catch (e) {
console.log(e); // URIError: URI malformed
}

if (illFormed.isWellFormed()) {
console.log(encodeURI(illFormed));
} else {
console.warn("Ill-formed strings encountered."); // Ill-formed strings encountered.
}

Change Array by copy

const months = ["Mar", "Jan", "Feb", "Dec"];
const sortedMonths = months.toSorted();
console.log(sortedMonths); // ['Dec', 'Feb', 'Jan', 'Mar']
console.log(months); // ['Mar', 'Jan', 'Feb', 'Dec']

const values = [1, 10, 21, 2];
const sortedValues = values.toSorted((a, b) => a - b);
console.log(sortedValues); // [1, 2, 10, 21]
console.log(values); // [1, 10, 21, 2]

Grouping functions

const array = [1, 2, 3, 4, 5];

// `Object.groupBy` groups items by arbitrary key.
// In this case, we're grouping by even/odd keys
Object.groupBy(array, (num, index) => {
return num % 2 === 0 ? "even" : "odd";
});
// => { odd: [1, 3, 5], even: [2, 4] }

// `Map.groupBy` returns items in a Map, and is useful for grouping
// using an object key.
const odd = { odd: true };
const even = { even: true };
Map.groupBy(array, (num, index) => {
return num % 2 === 0 ? even : odd;
});
// => Map { {odd: true}: [1, 3, 5], {even: true}: [2, 4] }

Resizable buffers

const buffer = new ArrayBuffer(8, { maxByteLength: 16 });

console.log(buffer.byteLength); // 8

buffer.resize(12);

console.log(buffer.byteLength); // 12

Transferrable buffers

const buffer = new ArrayBuffer(8, { maxByteLength: 16 });
const view = new Uint8Array(buffer);
view[1] = 2;
view[7] = 4;

// Copy the buffer to a smaller size
const buffer2 = buffer.transfer(4);
console.log(buffer2.byteLength); // 4
console.log(buffer2.maxByteLength); // 16
const view2 = new Uint8Array(buffer2);
console.log(view2[1]); // 2
console.log(view2[7]); // undefined
buffer2.resize(8);
console.log(view2[7]); // 0

// Copy the buffer to a larger size within maxByteLength
const buffer3 = buffer2.transfer(12);
console.log(buffer3.byteLength); // 12

// Copy the buffer to a larger size than maxByteLength
buffer3.transfer(20); // RangeError: Invalid array buffer length

APIs updates

Experimental features

Some of you might have noticed that the previous section contained a builtin addition that isn't technically a "spec addition", but a "proposal for a spec addition". To clarify, the ArrayBuffer.prototype.transfer and friends proposal is, at the time of the publication of this post, still at stage 3 on the TC39 Process. Generally, stages 3 and below need to be gated by implementors; this avoids exposing experimental APIs to users.

Mirroring this general idea, we introduced a new experimental feature for the boa_engine crate. Enabling this feature will make it possible to test future proposals for the ECMAScript specification using Boa, but we do not recommend enabling the feature in production environments.

We're still trying to find a way to enable experimental features in a more granular way, since the current flag allows enabling either all or no experimental features; definitely not ideal. So, expect some API changes in the future around this. But for now, have fun testing the new proposals!

[[HostDefined]] fields

In this version, we introduced a new API to attach custom data to realms, scripts and modules. The HostDefined struct is a more composable way of attaching custom data. Instead of attaching only a single type casted to an Any, you can insert many types to the HostDefined map, and every separate type will have its own value stored inside the map.

// Example snippet taken from https://github.com/boa-dev/boa/blob/main/examples/src/bin/host_defined.rs
// Check that file for a more extensive example.

/// Custom host-defined struct that has some state, and can be shared between JavaScript and rust.
#[derive(Default, Trace, Finalize, JsData)]
struct CustomHostDefinedStruct {
#[unsafe_ignore_trace]
counter: usize,
}

// We create a new `Context` to create a new Javascript executor.
let mut context = Context::default();

// Get the realm from the context.
let realm = context.realm().clone();

// Insert a default CustomHostDefinedStruct.
realm
.host_defined_mut()
.insert_default::<CustomHostDefinedStruct>();

assert!(realm.host_defined().has::<CustomHostDefinedStruct>());

// Get the [[HostDefined]] field from the realm and downcast it to our concrete type.
let host_defined = realm.host_defined();
let Some(host_defined) = host_defined.get::<CustomHostDefinedStruct>() else {
return Err(JsNativeError::typ()
.with_message("Realm does not have HostDefined field")
.into());
};

// Assert that the [[HostDefined]] field is in it's initial state.
assert_eq!(host_defined.counter, 0);

Class redesign + API enhancements

There were some small improvements made to our Class trait API, including a way to cache custom Class implementors into the Context itself for easy access to the constructor and prototype objects. This is part of an ongoing effort about improving the APIs around the Class trait.

// An example of what this new API allows
// Assume there's already a `Person` struct that implements `Class`.

let mut context = Context::default();
context
.register_global_class::<Person>()
.expect("the Person builtin shouldn't exist");

// Previously, the line below had to be done manually using something like
// let prototype = context
// .global_object()
// .get(js_string!("Person"), context)
// .unwrap()
// .as_object()
// .cloned()
// .unwrap()
// .get(js_string("prototype"), context)
// .unwrap()
// .as_object()
// .cloned()
// .unwrap();
// Yeah... it's a handful.
let prototype = context.get_global_class::<Person>().unwrap().prototype();

Runtime limits

We added new APIs to limit the execution of the engine at runtime! This new API has some limitations such as being unable to track limits inside native Rust functions, and we're still working on offering more options for other runtime limits such as heap size limits, but we hope this is at least useful for some users.

// Snippet taken from https://github.com/boa-dev/boa/blob/main/examples/src/bin/runtime_limits.rs
// Check that file for the full example.
// Create the JavaScript context.
let mut context = Context::default();

// Set the context's runtime limit on loops to 10 iterations.
context.runtime_limits_mut().set_loop_iteration_limit(10);

// Here we exceed the limit by 1 iteration and a `RuntimeLimit` error is thrown.
//
// This error cannot be caught in JavaScript, it can only be caught in Rust code.
let result = context.eval(Source::from_bytes(
r"
try {
for (let i = 0; i < 12; ++i) { }
} catch (e) {

}
",
));
assert!(result.is_err());

Synthetic modules

We added support for creating synthetic modules from Rust code. This allows exposing a set of functions and properties to other modules without having to evaluate Javascript code.

// Taken from https://github.com/boa-dev/boa/blob/main/examples/src/bin/synthetic.rs
// See the file for the full example.

// ...

let sum = FunctionObjectBuilder::new(
context.realm(),
NativeFunction::from_fn_ptr(|_, args, ctx| {
args.get_or_undefined(0).add(args.get_or_undefined(1), ctx)
}),
)
.length(2)
.name(js_string!("sum"))
.build();

// ...

let operations = Module::synthetic(
// Make sure to list all exports beforehand.
&[
js_string!("sum"),
// ...
],
// The initializer is evaluated every time a module imports this synthetic module,
// so we avoid creating duplicate objects by capturing and cloning them instead.
SyntheticModuleInitializer::from_copy_closure_with_captures(
|module, fns, _| {
println!("Running initializer!");
module.set_export(&js_string!("sum"), fns.0.clone().into())?;
// ...
Ok(())
},
(sum, /* ... */),
),
None,
context,
)

loader.insert(
PathBuf::from("./scripts/modules")
.canonicalize()?
.join("operations.mjs"),
operations,
);

// ...

Async eval

Due to popular demand, we added some APIs that allow running scripts in an asynchronous way, making it possible to share some workload between async tasks and the execution of the engine itself. Note that, by the single-threaded nature of JS engines, all futures returned by Boa cannot implement neither Send nor Sync.

let context = &mut Context::default();
let src = Source::from_bytes(r#"
let array = new Array([15, 20, 35, 123, 65, 12]);
array.sort();
console.log(array);
"#);
let src = Script::parse(src, None, context).unwrap();
let task = async move {
let result = src.evaluate_async(context).await.unwrap();
println!("{:?}", result.display());
}
block_on(join!(long_task(), task));

JsErasedError

Don't you hate when you try to ? a Result<T, JsError> and the compiler just complains saying something like

error[E0277]: `Rc<num_bigint::bigint::BigInt>` cannot be sent between threads safely
--> tests/tester/src/main.rs:190:52
|
190 | Context::default().eval(Source::from_bytes(""))?;
| ^ `Rc<num_bigint::bigint::BigInt>` cannot be sent between threads safely
|
= help: within `JsError`, the trait `Send` is not implemented for `Rc<num_bigint::bigint::BigInt>`
= help: the following other types implement trait `FromResidual<R>`:
<Result<T, F> as FromResidual<Yeet<E>>>
<Result<T, F> as FromResidual<Result<Infallible, E>>>

Well, say no more to missing Sends in your daily life! We present to you, JsErasedError!

Jokes aside, using JsError is difficult from an embedder's perspective because JsError can be any arbitrary value, including non-Send values such as JsObject, JsString or JsBigInt. This makes JsError automatically incompatible with libraries like anyhow or eyre that expect only Send errors.

To solve this, we introduced a new JsError::into_erased method which returns a thread-safe version of JsError that is compatible with anyhow, eyre and other error-reporting libraries.

fn main() -> eyre::Result<()> {
let context = &mut Context::default();
let value = context
.eval(Source::from_bytes(""))
.map_err(|err| err.into_erased(context))?; // No compiler errors!
}

Why not call it JsSendError instead of JsErasedError? Well, it is generally not possible to convert a JsError into a JsErasedError without losing some information in the conversion. However, JsSendError gave the appearance of being JsError but Send, which is really not true. JsErasedError, on the other hand, makes it clear the conversion is not lossless. Feel free to ping us if you have a better name for it though!

Optimizations

The following benchmarks below are taken from the v8 benchmark suite. This benchmark is deprecated, but is useful in this context to show the performance improvements between versions.

(higher numbers are better)

Boa VersionRichardsDeltaBlueCryptoRayTraceEarleyBoyerSplayNavierStokesTotal
v0.1629.029.242.110710511115.449.1
v0.1734.339.149.113411914111.956.2
v0.1849.853.952.116115215410291.5

Inline Caching

Thanks to the implementation of Object Shapes in version v0.17, we were able to further improve the performance of the engine by implementing Inline Caching. The concept of Inline Caching is based on the idea that a property access for a variable will usually only be applied to objects of similar Shapes. To picture this, let's examine the following code:

function attach(obj1, obj2) {
obj1.attach = obj2.getHandler();
}

On interpreters that don't implement any kind of caching, the previous code would have to make a property lookup for the getHandler method every time that method is called. This is really inefficient for a simple reason: getHandler could be inside obj2, or it could be inside obj2.prototype, or it could be inside obj2.prototype.prototype... in fact, getHandler could be anywhere on the inheritance chain of obj2!

The easy approach to solve this is to cache the method lookup inside obj2 itself using an associative map of some sorts. This is nice, but also a bit wasteful because we would be allocating a new associative map for all instances of obj2, even if the map is only really used inside attach.

What then? Well, we can apply the "inline" part of an inline cache now! Just allocate an array of all property accesses within the attach function and assign an index to every one of them. Initially, a property access is uninitialized. Once we reach a particular uninitialized property access, it performs the dynamic lookup and changes its corresponding array slot to be a weak reference to the object's shape. If we reach the same property access again, we can retrieve the stored shape and directly access the object's dense storage without doing a property lookup!

However, there's a caveat. If obj2.getHandler is evaluated twice with objects of different shapes, the stored shape would be invalid for the second property access. In this case, we can rollback the access to the uninitialized state and make a manual property lookup once again. This is known as monomorphic inline caching. There's also polymorphic inline caching, which stores several shapes per access instead of rolling back to the uninitialized state.

Currently we do eager monomorphic inline caching, so there is plently of room for improvements that we're planning to do in the future!

Road to 1.0

As Boa is being used by more projects it is important we can provide a stable and reliable API. We don't feel like we're quite there yet, but after a discussion with the team we have decided to aim for a 1.0 release in the near future. This will be a big milestone for us and we hope to have a lot of new features and improvements to show off by then.

We will keep our focus on the public API for those embedding Boa. We will also be working on improving the performance of the engine. If you wanted to offer feedback on the API feel free to reach out to us via Github or Discord.

You can keep an eye on the project to reach 1.0 here. We hopefully don't forsee this project getting much bigger as most issues such as spec conformance or performance are a going-concern.

Conclusion

How can you support Boa?

Boa is an independent JavaScript engine implementing the ECMAScript specification, we rely on the support of the community to keep it going. If you want to support us, you can do so by donating to our open collective. Proceeeds here go towards this very website, the domain name, and remunerating members of the team who have worked on the features released.

If financial contribution is not your strength, you can contribute by asking to be assigned to one of our open issues, and asking for mentoring if you don't know your way around the engine. Our contribution guide should help you here. If you are more used to working with JavaScript or frontend web development, we also welcome help to improve our web presence, either in our website, or in our testing representation page or benchmarks page. You can also contribute to our Criterion benchmark comparison GitHub action.

We are also looking to improve the documentation of the engine, both for developers of the engine itself and for users of the engine. Feel free to contact us in Discord.

Thank You

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

· 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!!

· 12 min read

Introduction

When we develop tools for our users, we sometimes want to give them some form of control over how they work. This is common in games, where we can add scripting for our users to be able to create extensions, or even for business tools, where we allow our customer to change or extend the behaviour of our platform. For those cases, using Rust, a compiled, type safe language can be a challenge, since once a program has been compiled, it's tricky to change or extend it at runtime. Furthermore, many of our users will prefer to use a more common scripting language, such as JavaScript.

This is where Boa enters the scene. Boa is a Javascript engine fully written in Rust. Currently, it can be used in places where you need most of the JavaScript language to work, even though, we would advise to wait to get all our known blocker bugs solved before using this for critical workloads. You can check how conformant we are with the official ECMAScript specification here.

And, before going further, we would like to mention that you can contribute to Boa by solving one of the issues where we need special help, and we now also accept financial contributions in our OpenCollective page.

Note: You can see more examples of integrating Boa in our repository.

Starting from scratch

Let's start a new project running cargo new my_project, and then add boa_engine as one of our dependencies by running cargo add boa_engine -F console in our newly created my_project directory.

Let's start by adding the minimal code needed to get a JavaScript interpreter working in our src/main.rs file:

use boa_engine::Context;

fn main() {
let js_code = "console.log('Hello World from a JS code string!')";

// Instantiate the execution context
let mut context = Context::default();

// Parse the source code
match context.eval(js_code) {
Ok(res) => {
println!("{}", res.to_string(&mut context).unwrap());
}
Err(e) => {
// Pretty print the error
eprintln!("Uncaught {}", e.display());
}
};
}

As you can see in this example, when working with Boa, you will have to use a Context, which will be in charge of initializing all the internals and built-in objects (such as Date, Promise and so on). The Context in Boa is also your go-to place for configuring your interpreter as you wish. You can add custom global functions, objects, and anything you might imagine. It's also one of the arguments you will receive if you create a Rust function and expose it to JavaScript, and with it, you will be able to throw errors, modify the global object and return values to JavaScript.

Talking about values, Boa comes with its built-in JsValue type. This enumeration represents any JavaScript value that can, for example, be assigned to a variable. And, before you ask, you can convert it to and from a serde_json::Value, of course, by using the JsValue::from_json() and JsValue::to_json() methods.

As you can see in those methods, or in the Context::eval() that we used earlier, you will receive a JsResult as a response. This result type will contain a JsValue as its error variant, which means you can return the error back to JavaScript for it to handle it. A JsValue, internally, is a garbage-collected JavaScript value. But, isn't Rust one of the few non-garbage collected languages? Wasn't that a good thing?

The answer is yes, of course, but JavaScript requires a garbage collector. This garbage collector makes sure that all values are freed when they are no longer needed. It also makes a JsValue extremely cheap to clone, independently of its contents.

If you run this example code with cargo run, you will notice that it will print the message sent to console.log(), and it will also print undefined at the end. This last undefined is part of the Ok(res) branch in the match, which prints the result of the execution. In this case, the result of the execution is the result of the last statement, which is the console.log(), and this statement returns undefined.

But, what can you do with Boa?

Let's start with the basics. Of course, you can execute JavaScript code. This code can be any string or directly a byte vector (so you can load files and use them directly). You can use Context::eval() in both cases, as you saw before, and you can also use Context::parse(), which will give you a StatementList that you can use multiple times in Context::compile(), so that you don't need to parse the same code more than once. The compiled source code can also be executed multiple times, since it's CodeBlock is garbage collected, and therefore it can be cheaply cloned. In order to execute a code block you will need to use Context::execute().

This in itself is good enough to provide a simple scripting API for your project, but where Boa really shines is in the ability to inter-operate Rust and JavaScript. Let's start with a simple example: exposing a Rust function to JavaScript. A JavaScript-compatible Rust function must have the NativeFunctionSignature signature:

use boa_engine::{builtins::JsArgs, Context, JsResult, JsValue};

/// Says "hello" using the first argument.
fn say_hello(_this: &JsValue, args: &[JsValue], context: &mut Context) -> JsResult<JsValue> {
let name = args.get_or_undefined(0);

if name.is_undefined() {
println!("Hello World!");
} else {
println!("Hello {}!", name.to_string(context)?);
}

Ok(JsValue::undefined())
}

The JsArgs trait allows you to retrieve a value if the function received it, or set it to the undefined value, if not. Then, in this case, it will convert the name to a JsString before printing it, since we might be receiving an object, a symbol, a boolean... one of the perks of dynamic typing. This will then print the result in th standard output using the common println!() macro in Rust. It will just return an undefined value.

You can register this function in the context by adding this line after the context creation (and before executing any JS) in the main() function:

context.register_global_builtin_function("say_hello", 1, say_hello);

This will register it as a global function, with the say_hello() name, and with a length of 1 (which indicates the number of arguments that it receives by default). You can then try it out by modifying the JavaScript string:

let js_code = r#"say_hello("Rust");"#;

The r#"..."# syntax is a Rust raw string literal.

You can also add any JsValue as a property to the global object by using the Context::register_global_property() function:

use boa_engine::property::Attribute;

context.register_global_property("MY_PROJECT_VERSION", "1.0.0", Attribute::all());

And you can use it in JavaScript:

say_hello(MY_PROJECT_VERSION);

The Attribute of a property indicates if it will be writable (it can be set and modified), enumerable (it can be used in for..in statements) and configurable (its attributes or type of property can be modified).

Integrating a full Rust data structure

Sometimes, adding a function or a single JsValue to the global scope of your JavaScript context is not enough, and you want to enable the full power of Rust with its structures to handle more complex scenarios. This can be achieved using the Class trait. This has to be combined with two other traits, that make any Rust object be garbage-collected: Trace and Finalize, in the boa_gc crate. Luckily those two traits can be derived.

Let's start by implementing a Person type, that will showcase the potential of this API. Let's run cargo add gc boa_gc and add some code:

use boa_gc::{Finalize, Trace};

#[derive(Debug, Trace, Finalize)]
struct Person {
/// The name of the person.
name: String,
/// The age of the person.
age: u8,
}

Then, we will move the say_hello() function to be a static method of Person:

impl Person {
/// Says "hello" using the name and the age of a `Person`.
fn say_hello(this: &JsValue, _args: &[JsValue], context: &mut Context) -> JsResult<JsValue> {
let this = this
.as_object()
.and_then(|obj| obj.downcast_ref::<Self>())
.ok_or_else(|| context.construct_type_error("`this` is not a `Person` object"))?;

println!("Hello {}-year-old {}!", this.age, this.name);

Ok(JsValue::undefined())
}
}

As you can see, this now uses the this parameter of the say_hello() function, which should be a Person, but in JavaScript you can assign methods of some objects to others, so we must make sure that on this invocation, we are indeed working with a Person, and return a TypeError if not.

Now, let's implement the Class trait. This trait requires a NAME constant, which will be the name of the global object property, and a LENGTH for the constructor (the number of arguments, by default 0). Then, it needs a constructor() function, which is a native function that will be called when we do a new Person(), and an init() function, which will be called by the Context when registering the function in the global scope. It will receive a ClassBuilder, which allows you to add a method (both, static and prototype), a property, also for both cases, accessor properties (to use get and set) and property descriptors. You can also get a reference to the Context with the ClasBuilder::context() method, in case you want to do anything fancier.

In this case, the constructor will take care of constructing the Rust Person data structure with the two arguments it receives, and then register the say_hello() method:

use boa_engine::{
builtins::JsArgs,
class::{Class, ClassBuilder},
};

impl Class for Person {
const NAME: &'static str = "Person";
const LENGTH: usize = 2;

// This is what is called when we construct a `Person` with the expression `new Person()`.
fn constructor(_this: &JsValue, args: &[JsValue], context: &mut Context) -> JsResult<Self> {
let name = args.get_or_undefined(0).to_string(context)?;
let age = args.get_or_undefined(1).to_u32(context)?;

if !(0..=150).contains(&age) {
context.throw_range_error(format!("invalid age `{age}`. Must be between 0 and 150"))?;
}

let age = u8::try_from(age).expect("we already checked that it was in range");

let person = Person {
name: name.to_string(),
age,
};

Ok(person)
}

/// Here is where the class is initialized, to be inserted into the global object.
fn init(class: &mut ClassBuilder) -> JsResult<()> {
class.method("say_hello", 0, Self::say_hello);

Ok(())
}
}

In order to register the class, you will need to use the Context::register_global_class() method:

context
.register_global_class::<Person>()
.expect("could not register class");

You can now adapt the JavaScript code:

let person = new Person("John", 28);
person.say_hello();

If you want to access the global object from Rust, you can use Context::global_object(), which will return a JsObject. In this object, you can use the JsObject::get() function to retrieve any property of the global object, such as the MY_PROJECT_VERSION that you defined earlier, or any intrinsic, such as the Date object.

We are now in the process of creating Rust wrappers for all JavaScript intrinsics (#2098). For example, you can create a JsArray from a JsObject to make it much easier to manipulate a JavaScript array from Rust. In the following example, you'll create a new reverseAppend() global function that will receive an array, reverse it, and then append the "My Project" string to it. It will then get the MY_PROJECT_VERSION from the global object, and append it to the array.

use boa_engine::{
builtins::JsArgs, object::JsArray, property::Attribute, Context, JsResult, JsValue,
};

/// Reverses an array and appends the `"My Project"` string and the `MY_PROJECT_VERSION` global
/// property to it.
fn reverse_append(_this: &JsValue, args: &[JsValue], context: &mut Context) -> JsResult<JsValue> {
let arr = args
.get_or_undefined(0)
.as_object()
.ok_or_else(|| context.construct_type_error("argument must be an array"))?;

let arr = JsArray::from_object(arr.clone(), context)?;

let reverse = arr.reverse(context)?;
reverse.push("My Project", context)?;

let global_object = context.global_object().clone();
let version = global_object
.get("MY_PROJECT_VERSION", context)
.unwrap_or_default();

reverse.push(version, context)?;

Ok((*reverse).clone().into())
}

fn main() {
let js_code = r#"
let arr = ['a', 2, 5.4, "Hello"];
reverseAppend(arr);
"#;

// Instantiate the execution context
let mut context = Context::default();

context.register_global_property("MY_PROJECT_VERSION", "1.0.0", Attribute::all());

context.register_global_builtin_function("reverseAppend", 1, reverse_append);

// Parse the source code
match context.eval(js_code) {
Ok(res) => {
println!("{}", res.to_string(&mut context).unwrap());
}
Err(e) => {
// Pretty print the error
eprintln!("Uncaught {}", e.display());
}
};
}

We are looking for contributors to implement the rest of the wrappers, and of course, we offer mentoring!

What's coming next?

Boa's development is ongoing non-stop. The next version, v0.17, is already looking pretty nice, with some great enhancements. For example, @jedel1043 has created new "lazy" errors, that are much easier to create and throw, since they don't need a Context, and also enhance the performance. @nekevss has implemented a new wrapper for RegExp, and @anuvratsingh is working on a Date wrapper. @razican is working on a JavaScript to Rust conversion trait and derive, that will allow you to convert a JsValue to a Rust structure and back really easily:

use boa_derive::TryFromJs;
use boa_engine::{value::TryFromJs, Context, JsResult, JsValue};

#[derive(Debug, TryFromJs)]
#[allow(dead_code)]
struct TestStruct {
inner: bool,
hello: String,
#[boa(from_js_with = "lossy_conversion", hello = "myfriend")]
my_float: i16,
}

fn main() {
let js = r#"
let x = {
inner: false,
hello: "World",
my_float: 2.9,
};

x;
"#;

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

let str = TestStruct::try_from_js(&res, &mut context)
.map_err(|e| e.display().to_string())
.unwrap();

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

/// 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),
_ => context.throw_type_error("cannot convert value to an i16"),
}
}

We love contributions, whether it's a documentation enhancement, fixing or implementing the ECMAScript specification, adding new functionality / APIs or enhancing performance, we would love to get new contributors on board! We are also looking for financial contributors, so feel free to join our OpenCollective.

· 4 min read

Summary

Boa v0.16 is now available! After around 3 months of development, we are very happy to present you the newest 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.

Boa currently supports part of the JavaScript language. In this release, our conformance has grown from 62.29% to 74.53% in the official ECMAScript Test Suite (Test262). The engine now passes 68,612 tests, coming from 56,372 in Boa 0.15 (21.7% increase), and we have closed 9 issues and merged 59 pull requests. You can check the full list of changes here, and the full information on conformance here.

New ECMAScript features

Support for Promises

With this new release Boa ships support for ECMAScript Promises. All tests in the 262 test suite built-ins/Promise pass. Promises enable asynchronous operations to be executed via an internal job queue. Starting with our work on Promises, we have stopped ignoring the 262 tests that are flagged as async. We also have enabled our 262 test runner to work with asynchronous tests. We would like to thank @aaronmunsters for proposing the initial PR for Promises (#1923) that we then could expand on.

// This program will print:
// 1. End of synchronous execution.
// 2. The Promise has been resolved.

let promise = Promise.resolve();

promise.then(() => console.log("2. The Promise has been resolved."));

console.log("1. End of synchronous execution.");

Support for Async/Await

The implementation of Promises has enabled us to implement async functions and the await keyword. The async/await syntax makes it easier to use Promises within javascript. We have implemented async functions, async generators, for await...of loops and the await keyword. While not all tests pass for these features yet, the basic functionality should work.

Dense/Packed JavaScript Arrays

JavaScript Arrays are regular objects whose values are stored as indexed properties. Because arrays have no fixed length, values can be assigned to any possible index without any of the previous indices being used. Due to this behavior indexed properties are stored in a map instead of in a vector, as the vector would allocate a lot of unused memory if a high index is used. We have implemented an optimization to make it possible to use optimized vector storage for array values, as long as indices are not assigned out of order. This has improved the performance of arrays around 45% for arrays without empty slots. To learn more about this optimization take a look at the PR #2167.

Support for URI encoding and decoding functions

This version for Boa ships with support for the built-in URI encoding and decoding functions encodeURI, decodeURI, encodeURIComponent and decodeURIComponent. With the exception of UTF-16 related test, all test in the relevant 262 test suites pass for these functions.

How can you contribute to Boa?

In March, Boa opened financial contributions on its OpenCollective page. If financial contribution is not your strength, you can contribute by asking to be assigned to one of our open issues, and asking for mentoring if you don't know your way around the engine. Our contribution guide should help you here. If you are more used to working with JavaScript or frontend web development, we also welcome help to improve our web presence, either in our website, or in our testing representation page or benchmarks page. You can also contribute to our Criterion benchmark comparison GitHub action.

We are also looking to improve the documentation of the engine, both for developers of the engine itself and for users of the engine. Feel free to contact us in Discord.

Thank You

Last but certainly not least, a big Thank You to all the contributors of this Boa release. We would like to particularly thank our new contributors:

· 5 min read

Summary

Boa v0.15 is now available! After around 3 months of development, we are very happy to present you the newest 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.

Boa currently supports part of the JavaScript language. In this release, our conformance has grown from 49.74% to 62.29% in the official ECMAScript Test Suite (Test262). The engine now passes 56,372 tests, coming from 43,986 in Boa 0.14 (28.1% increase), and we have closed 18 issues and merged 58 pull requests. You can check the full list of changes here, and the full information on conformance here.

New ECMAScript features

While there are only a few big new features in this release, there are a lot of fixes for existing features that should enable many more JavaScript programs to execute correctly. For a detailed list checkout the changelog.

Support for Classes

With this new release boa ships with support for ECMAScript Classes. While not all features are implemented, most basic functionality should work as expected.

class Rectangle {
constructor(height, width) {
this.height = height;
this.width = width;
}

calcArea() {
return this.height * this.width;
}

get area() {
return this.calcArea();
}
}

let r = new Rectangle(2, 4);
r.calcArea(); // 8

Support for eval() and Function()

While eval() is one of the less popular Javascript features, many tests in the ECMAScript Test Suite use it. For a better representation of passing tests and to fully comply with the specification we have implemented it and the similar Function() constructor. We would like to echo the mdn docs and point out to Never use eval()!

eval('console.log("Never use eval()!")');

let f = Function("arg", 'console.log("This is also a very " + arg + " idea!")');
f("bad");

Thanks to @raskad for working on classes, eval() and Function(), among many other things!

Regaining Performance

After moving from an AST based execution to our Virtual Machine we observed performance losses in some of our benchmarks. We found out that the root-cause of these losses was a single format!() call in the hot-path of the VM. Our assumption was that the formatting logic would be optimized out by rustc or llvm, because it was used as an argument to an empty (feature-gated) function. After replacing the format!() call with a &'static str the expected optimization took place and we were back to previous performance levels. For more details check out #1973 for the changes and some discussion about this issue. Thanks to @pdogr for finding the issue and contributing a fix.

Starting work on Internationalization

ECMAScript specifies an Internationalization API through the Intl global object. We have started implementing many internal functions and structures to provide the Intl object in boa. To take advantage of existing work in this space we have started integrating the ICU4X library into boa. Thanks to @NorbertGarfield and @jedel1043 for their continued work on this topic.

Rust Wrappers for Javascript Objects

We have started the implementation of wrappers around built-in Javascript objects like Array. These Rust types make it easy to work with Javascript objects in Rust and provide a type safe abstraction around them. Many of these wrappers are not implemented yet and may be a good place to start contributing to boa. If you are interested, check out the tracking issue.

How can you contribute to Boa?

In March, boa opened financial contributions on its OpenCollective page. Since then, we have to thank Demergent Labs, a company using Boa in the Internet Computer that has decided to sponsor us, and Clemens Koza, who is our first backer! We are already thinking on how we will use these contributions to improve how we develop Boa.

If financial contribution is not your strength, you can contribute by asking to be assigned to one of our open issues, and asking for mentoring if you don't know your way around the engine. Our contribution guide should help you here. If you are more used to working with JavaScript or frontend web development, we also welcome help to improve our web presence, either in our website, or in our testing representation page or benchmarks page. You can also contribute to our Criterion benchmark comparison GitHub action.

We are also looking to improve the documentation of the engine, both for developers of the engine itself and for users of the engine. Feel free to contact us in Discord.

Thank You

Last but certainly not least, a big Thank You to all the contributors of this Boa release. We would like to particularly thank our new contributors:

· 6 min read

Summary

Boa v0.14 is here! After almost 6 months of development, we are very happy to present you the newest 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. Together with this release, we present you: A new way to contribute to Boa, a virtual machine, usable examples and much more.

Boa currently supports part of the JavaScript language. In this release, our conformance has grown from 41.01% to 49.74% in the official ECMAScript Test Suite (Test262). The engine now passes 43,986 tests, coming from 33,192 in Boa 0.13 (32.5% increase), and we have closed 40 issues and merged 137 pull requests. You can check the full list of changes here, and the full information on conformance here.

Boa has moved

First off, some breaking changes! Boa is now boa_engine on crates.io. This should bring it in line with our other packages, and allows us to resolve the casing issue we had with Boa previously. It should just be a case of changing your dependencies in cargo.toml to the new destination. The old Boa crate will no longer receive any updates.

New ECMAScript features

Boa v0.14 ships with support for Object.getOwnPropertyNames, Object.getOwnPropertySymbols, Object.prototype.hasOwnProperty, Array.from, AggregateError, Typed Arrays, Proxy, toLocaleString(), Symbol.prototype.valueOf, Array.prototype.at(), String.fromCharCode, Object.hasOwn, Object.fromEntries, Proxy, Array.prototype.findLast, Array.prototype.findLastIndex, console.trace, String.raw( template, ...substitutions ), and more. There's also been several bug fixes and internal improvements, such as switching to Bors for our CI management.

Virtual Machine

The biggest change in this release has definitely been the switch of the main implementation to a Virtual Machine. The JavaScript code first gets parsed into AST, then compiled into op-codes, and finally executed in the VM. This adds some extra steps, which, for now, have reduced the performance of the engine. We hope to fix some of these degradations over the course of the next few releases so hang tight. The new VM opens the door for many great features in the future (we're looking at you, async/await), and allows some nicer optimizations in the parsing and compilation phases in which we have already started working (such as #1829 and #1849).

This move towards a VM has also brought one great new feature, generator execution (#1790). You can find out more about how to use the VM here.


Boa opens financial contributions

In the past months, due to the big and steady progress on Boa, we have received multiple requests to contribute to Boa financially. From now on, anyone can contribute with either a recurring donation, or a one-time contribution in our OpenCollective page.

What does this mean for Boa?

Being able to receive financial contributions won't directly show a change in Boa development, but depending on how much funding we get, we are already planning next steps. Having more motivated contributors would be the first step. We all want to spend more of our free time in boa, and receiving a financial compensation for it is definitely making the team members eager to continue contributing.

But that is not our only idea. We would like to create a reproducible benchmarking platform. Currently, we just use GitHub actions for benchmarking, which is useful to get some general ideas on how some changes affect the performance of the engine. However, they have a high variability (up to 10%), meaning that we can definitely see changes if they are big, but it's impossible to see differences if changes are minimal (1-2%, for example). Having the infrastructure to do proper performance testing would require using dedicated CPUs, for example, which could be financed using the Boa Open Collective.

On the development side, there are some very big projects that require deep involvement from the team. Priorities include (not neccesarily in order):

  • Async/Await support.
  • Better completion records implementation
  • Performance improvement across the board.
  • Future implementation of Shapes/Hidden Classes.
  • Being able to measure against other engines.
  • Improved conformance of the EcmaScript specification.
  • Integration with ICU4X for Intl and future Temporal implementation.

Implementing a proper API for Rust crates that want to embed Boa as they like, or a C API so that other software, not written in Rust, can use Boa, are also a priority, and implementing all the new built-in objects to achieve 100% conformance with the ECMAScript specification is also a must. All this takes time, and we will need to finance developers working on these big issues.

Can I contribute differently?

If financial contribution is not your strength, you can contribute by asking to be assigned to one of our open issues, and asking for mentoring if you don't know your way around the engine. Our contribution guide should help you here. If you are more used to working with JavaScript or frontend web development, we also welcome help to improve our web presence, either in our website, or in our testing representation page or benchmarks page. You can also contribute to our Criterion benchmark comparison GitHub action.

We are also looking to improve the documentation of the engine, both for developers of the engine itself and for users of the engine. Feel free to contact us in Discord.

Thank You

Last but certainly not least, a big Thank You to all the contributors of Boa. It's not often we list them but we would like to say thank you to the new contributors who joined for the last release.

Thank you New Contributors

· 5 min read

Boa v0.13 is here! Boa is a JavaScript engine written in the Rust programming language. It 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.

We currently support part of the language. In this release, our conformance has grown to 41.97% of the official ECMAScript Test Suite (Test262). We have closed 40 issues and merged 105 pull requests. You can check the full list of changes here.

This release brings some new features, such as support for calling Rust closures from JavaScript to improve better interopability between JS and Rust.

ECMAScript language features

named capture groups are now implemented and enabled.

const RE_DATE = /(?<year>[0-9]{4})-(?<month>[0-9]{2})-(?<day>[0-9]{2})/;

const matchObj = RE_DATE.exec("1999-12-31");
const year = matchObj.groups.year; // 1999
const month = matchObj.groups.month; // 12
const day = matchObj.groups.day; // 31

This release brings support for the relative indexing method String.prototype.at(). This makes it easier to fetch values at the end of an array or string instead of doing str[str.length - 1].

const sentence = "The quick brown fox jumps over the lazy dog.";
let index = 5;
console.log(
`Using an index of ${index} the character returned is ${sentence.at(index)}`,
);
// expected output: "Using an index of 5 the character returned is u"

Other implemented language features include destructoring assignments, replaceAll(), Object.values(), Object.keys(), Object.preventExtensions(), splice(), sort(), spreading objects and more.

Boa API

Calling Rust closures from JavaScript

In addition to registering Rust functions as global JavaScript functions, our API has been expanded to register Rust closures. register_global_closure allows to capture variables in closures that can be called from javascript:

let mut context = Context::new();
let add_value = 1.0;
context.register_global_closure("addOne", 1, move |_, args, _| {
let argument = args.get(0).unwrap().as_number().unwrap();
Ok(JsValue::new(argument + add_value))
})?;
assert_eq!(context.eval("addOne(41)")?, 42.into());

To see the full capabilities of this feature, take a look at our examples. Thanks to @HalidOdat and @jedel1043 for their work on this.

Boa prelude

This release exposes a prelude of useful things that are already imported. This should make it more ergonomic to work with.

use boa::prelude::*; // This would import all the commonly-used things

fn main() {
let number = JSValue::number(3.1415);
let realm = Realm::create();
let mut engine = Interpreter::new(realm);
let result = forward(&mut engine, "Math.PI");
}

Test 262

Test262 is the implementation conformance test suite maintained by TC39. It's used by nearly all engines to measure how conformant they are to the specification. Boa pulls the tests in-tree and runs them against all PRs.

Since v0.12 we have managed to pass 6391 more tests and are 7% more conformant. This has been great progress by all involved and we hope this continues to improve. Most of these new passes have come from refactors across the codebase which have had little impact on performance, in fact, v0.13 is much faster than v0.12.

You can track Boa's conformance to the specification here

Keeping up with Rust

Rust is changing underneath us. This means we need to make sure our code is conforming the latest standards and we're taking advantage of the best optimizations the language can provide. In order to do this we rely on Clippy, this is Rust's in-house linter. We updated our code to respect the latest Rust version and updated formatting. Often a nice bonus of doing this is getting performance improvements for free as Clippy gets more "smarter". Thanks to @neeldug and @RageKnify for the work in this area.

Keeping things in order

Previously object properties were stored using FxHashMap. Despite being very fast it didn't offer any guarantees about the order. Properties need to retain the same order they were entered. In order to achieve this we switched over to IndexMap. Traits in Rust make this easy as IndexMap was designed to be a drop-in replacement for other HashMap implementations by following the same Trait. Thanks to IndexMap offering the possibility to use alternative hashing algorithms, we could continue using the fast FxHasher algorithm from rustc. Performance losses were minimal on some benchmarks and we actually made some gains in others. Thanks to @raskad for their work on this

VM

There is still on-going work to utilise a VM, this is happening alongside how Boa runs today. Although its not exposed yet there has been plenty of refactorings to the VM in order to make it performant. Hopefully we can talk about this in more detail soon.

Opening up the discussion

As of this release Boa has begun to utilise Github's Discussions feature. You can catch more long-ranging projects here

Thank You

This has been the biggest release yet, there have been many features and fixes. We want to thank all the contributors in this release, whether it was features, fixes or raising bugs.

If you're interested in contributing to Boa, we have some "good first issues" and "issues where help is wanted".

· 3 min read

Boa v0.12 is here! Boa is a JavaScript parser, compiler and executor written in the Rust programming language. It 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.

We currently support part of the language. In this release, our conformance has grown to 33.97% of the official ECMAScript Test Suite (Test262). In this release, we have closed 19 issues and merged 69 pull requests. You can check the full list of changes here.

Let's dive into the most relevant changes of this release.

Panic-free

Boa now doesn't panic in any of the Test262 tests. This is a huge milestone, since it means that the engine itself can handle all of the edge cases. At least those proposed by the official ECMAScript test suite. We still recommend using std::panic::catch_unwind() to execute arbitrary code, though.

Conformance increase

In version 0.11 we were passing 31.59% of the Test262 suite. In this version, we have increased that coverage to 33.97%. The increase might not seem much, but we now pass 1,924 new tests.

Our conformance tester has also been improved, and thanks to the new APIs available in the engine, we were able to add complex functionality such as "realm" creation, cross-real symbols and overall, the inclusion of a partially complete $262 object, ready for some testing.

We have also added some useful lists of "fixed", "broken", "ignored" and "fixed panics" tests for each pull request. This will allow us to directly check individual tests on each PR and only re-run the new tests that are failing/panicking in order to properly fix them.

New built-in objects, string improvements and @@ToStringTag

Boa now has support for the Reflect and Set built-in objects. This grows the posibilities of the engine to be used in more real-world projects. On the same lines, we have added support for the GetOwnProperty for string. This means that we can use [{index}] to get the characters of a string. For example, "hello"[0] will return "h".

We have also added the @@ToStringTag well known symbol to most of the built-in objects. You can now run the following code, for example, and get the expected result:

let result = new Map()[Symbol.toStringTag];

console.log(result); // Returns "Map"

Thank you

Everything in this release has been such a huge effort, we want to thank all the contributors in this release, whether it was features, fixes or raising bugs.

If you're interested in contributing to Boa, we have some "good first issues" and "issues where help is wanted".

· 5 min read

Boa has reached a new release. v0.11, our biggest one yet!

Since v0.10 we've closed 77 issues and merged 129 pull requests. The engine has been faster and more compliant to the spec. Below are some of the highlights but please see the changelog for more information.

What is Boa? See the About page for more info.

Test 262

Test262 is the implementation conformance test suite maintained by TC39. It's used by nearly all engines to measure how conformant they are to the specification. Boa pulls the tests in-tree and runs them against all PRs. You can find more about Test262 here.

Since v0.10 we have almost doubled on spec conformance, and reduced panics. A year ago Boa didn't even track itself against Test262 so it was difficult to know how compliant we were to the spec, today not only do we track all changes against Test262 but we can see progress on a PR to PR basis.

Conformance graphConformance graph

Previously many tests failed to run as the test-runner was still being worked on. Those issues have been fixed and our tests jumped from 38k to 78K which is why the graph flips up above. Boa should never panic, however we've had many tests reveal areas where panics happen, this has helped us identify and apply correct fixes, to the point where our panics have gone from hundreds to under 50 (the graph above shows the dark red diminishing).

For live tracking of conformance tests you can check here. Below is a snapshot of the previous version and today.

v0.10:

  • Total tests: 38,706

  • Passed tests: 6,960

  • Ignored tests: 5,748

  • Failed tests: 25,998

  • Conformance: 17.98%

v0.11:

  • Total tests: 78,497

  • Passed tests: 24,550

  • Ignored tests: 15,585

  • Failed tests: 38,362 (24 ⚠)

  • Conformance: 31.28%

Regress

In this release Boa switched from its own implementation (wrapping regex) to the regress engine for regular expressions. Regress is a crate aiming to implement ECMAScript compliant regular expressions and Boa makes use of (and contributes back to) that.

While Regress is not 100% spec compliant this is something which is being worked on, also the switch gave us quite a performance boost in our benchmarks we're seeing almost 6X faster execution.

Conformance graphConformance graph

The above image shows a big drop in the middle of the graph, above fb1b8d5 is where we switched over. Conformance went from 19.01% to 18.99% and introduced some panics, however many of those have since been fixed.

Iterating over bytes

Previously the lexer iterated over unicode chars (u32 code points), this wasn't strictly neccesary for Boa and we have instead changed the lexer to work over bytes (u8). Iterating over bytes rather than chars is inherently much faster, non-ascii UTF8 bytes are all >=128, and we might only really care about those being correct when parsing idents. This is standard practise amongs lexical analyzers and even browsers read source code byte-by-byte, using the rules of the language's syntax to convert the source text into tokens.

This was worked on by @jevancc and his changes have improved performance overall.

Embedding examples

We are still working on what the public API should look like, some of these decisions are driven by feedback and the ever-changing way which Boa works.
Not only you can run Boa against javascript today you can also embed your own objects and classes into the engine before it's ran.

We've offered an example to show how a class can be constructed in Rust then added to the environment before executing your scripts. This should offer a great insight into how you can interop Rust with JavaScript by using Boa.

Below are some of the more recent functions available from the Context object for you to add your own functionality.

// Rust implementation of a function injected into the environment
Context::register_global_function(),
// Rust implementation of a class injected into the environment
Context::register_global_class(),
// Rust implementation of a property injected into the environment
Context::register_global_property()

If there are any examples you would like added, please raise an issue on the main repository.

Generating bytecode

Today Boa walks the tree of the AST, although easy to implement it's not ideal for performance.
We are looking to do code generation which can then be interpreted. This gradual process may happen over many releases until at some point we can switch implementations under the hood. Our steps are:

  • Experiment with VM Path on isolated branch
  • Experiment with generating more simpler instructions
  • Build up parity with current implementation
  • Run test suite over VM path including conformance tests
  • Switch over once performance is steady
  • Optimize code generation and intreperter

Thank You

Everything in this release has been such a huge effort, we want to thank all the contributors in this release, whether it was features, fixes or raising bugs.

If you're interested in contributing to Boa, we have some "good first issues" and "issues where help is wanted".

· 4 min read

Boa is an experimental Javascript lexer, parser and compiler written in Rust. It has support for some of the language, can be embedded in Rust projects fairly easily and also used from the command line. Boa also exists to serve as a Rust implementation of the EcmaScript specification, there will be areas where we can utilise Rust and its fantastic ecosystem to make a fast, concurrent and safe engine.

We have a long way to go, however v0.10 has been the biggest release to date, with 138 issues closed!

We have some highlights, but if you prefer to read the full changelog, you can do that here

Test262

One question we've been asked for a long time is "how conformant are you to the spec?". It's been tough to answer as we've been unable to run against the official test suite.

Test262 is the official ECMAScript Test Suite and exists to provide conformance tests for the latest drafts of the Ecma specification. It is used for all engines, you can even run it in your browser.
Thanks to @Razican in v0.10 we now have a test harness that allows us to run it against Boa at any time.

This is a new crate inside the Boa repository that can parse through all of the tests (roughly 40,000 of them) in under 10 minutes and tell us how conformant we are.

image

Today Boa has 18% conformity to the specification. We'll be keeping an eye on this number over the releases. We expect to achieve around 30% by 0.11 due to some of the fixes we're adding which should pass a few thousand tests.

These are run via Github Actions against PRs and for our main branch so that we can keep track of where we are and if there are regressions.

Built-ins

We've added support for Date, Map and well-known symbols. Supporting Well-known symbols unblocks a lot of work around adding @@iterators to some of our global objects which is coming up in the next release.
Both Math and Number have had their remaining methods implemented.

Lexer

The lexer has been rebuilt from scratch. Just like the old parser it was a single file before looping through and becoming unmaintainable. Today we've reorganised it into separate modules which know how to lex certain areas. The new lexer now supports goal symbols and can now tokenize with the correct context at any time.

Goal Symbols

Our issue with goal symbols is explained by the V8 team here

Previously we weren't distinguishing between the contexts where some input elements are permitted and some are not, so lexing / would yeild a division symbols when it should be a RegularExpressionLiteral for example. This change unblocked us being able to run Test262.

Performance wise it is much faster for larger files. The lexer is far more efficient at streaming tokens to the parser than previously so in some scenarios we have big gains.

You can see all the benchmarks here

Repl syntax highlighting

Syntax highlighting was added to the repl this release thanks to @HalidOdat
Our repl is made possible due to the great work of RustyLine

image

Looking forward

There are plenty of fixes and performance changes still needed, we also hope to experiment with producing Bytecode from our AST in future. Test262 coverage will almost certainly increase, and we are polishing the public API for easier use when embedding into other Rust projects.

Thanks to all those who contributed to 0.10, you can see the names in the full changelog linked above.

You can checkout Boa via Github or on crates.io

· 6 min read

Hello World!

Boa is an experimental Javascript lexer, parser and compiler written in Rust. It has support for some of the language, can be embedded in Rust projects fairly easily and also used from the command line.
Boa also exists to serve as a Rust implementation of the EcmaScript specification, there will be areas where we can utilise Rust and its fantastic ecosystem to make a fast, concurrent and safe engine.

Today we're pleased to announce our latest release, version 0.9.
v0.9 is by far the biggest release we've had since Boa began. You can find the full changes from the changelog. The milestone behind this version was further optimisation and an increase in new features. We can show you how we can identify areas that can be optimised.

Better tooling for profiling

Boa became the first Rust project to make use of measureme, a profiling tool built from the ground up for Rust. This was only used by the Rust team themselves to profile the compiler. We managed to work with the compiler team to get the framework in a good enough state to be used by other projects too, and in this release, we gave it a try.

Measure me lets you profile various areas of your choosing, then you can generate a trace file which can be loaded into Chromium or various other tools for analysis. We took it for a spin (which you'll see in Object Specialization).

Below is an example of our trace, this is using a measureme tool called summarize

+----------------------------+-----------+-----------------+----------+------------+
| Item | Self time | % of total time | Time | Item count |
+----------------------------+-----------+-----------------+----------+------------+
| From<Object> | 1.04ms | 14.776 | 1.04ms | 146 |
+----------------------------+-----------+-----------------+----------+------------+
| new_object | 356.50µs | 5.082 | 533.50µs | 18 |
+----------------------------+-----------+-----------------+----------+------------+
| create_instrinsics | 263.50µs | 3.756 | 6.38ms | 1 |
+----------------------------+-----------+-----------------+----------+------------+
| make_builtin_fn: toString | 218.50µs | 3.114 | 290.50µs | 12 |
+----------------------------+-----------+-----------------+----------+------------+
| String | 81.60µs | 1.163 | 961.60µs | 1 |
+----------------------------+-----------+-----------------+----------+------------+

You can read more about Rust's usage of measureme here.

Object Specialization

In JavaScript internal metadata for objects are stored in internal slots. In Boa we stored internal slots as a hashmap tied to the object, with the keys being strings and the values as JSValues. This meant we needed to constantly unwrap them into a Rust primitive to access the data. Secondly we were restricted as to what type of data we could put in internal slots. For example, lets say we want to implement Set by using the native HashSet as a backing store, this would not be possible.

By changing how internal data is handled for some of our builtin objects and removing a whole bunch of redundant access checks, we managed to speed up the interpreter.

Boa would spend a significant amount of time converting back and forth between JS Values and primitive values. We found a huge amount of time was spent in Value::set_field before any code had even been executed. set_field was slow due to the amount of updating of internal slots as part of setting up. Here you can see realm creation takes roughly 60ms (dev build).

We use Crox to convert our measureme data into a format Chrome's performance tab understands

Before

We refactored many builtins to hold an ObjectData enum variant instead, some of which can hold a value for the type also. This can be used to both identify objects and to use their internal data.
Here is an example of our enum.

/// Defines the different types of objects.
#[derive(Debug, Trace, Finalize, Clone)]
pub enum ObjectData {
Array,
BigInt(RcBigInt),
Boolean(bool),
Function(Function),
String(RcString),
Number(f64),
Symbol(RcSymbol),
Error,
Ordinary,
}

After

This gave us a 70% speedup and reduced startup time by well over half. The realm::create function now runs in 13ms instead of 60ms.

Optimised Type Comparisons

If you've ever called typeof in JavaScript, you get a string value describing the primitive type of it's argument. Boa was doing the same internally for comparing (using the "get_type()" call), however getting the string value from each primitive then comparing them is not very performant.
Now, thanks to @Lan2u we have a rust Type enum which makes comparing more efficient and on average brings another 8% performance boost.

JSValue Refactor

We have completely refactored how JavaScript values are stored.
#498 makes values more lightweight by only GC'ing objects and not the primitives. The primitive scalar values are just Rust primitives which implement the Copy trait, so the overhead of moving these around is much lower.
By decoupling our Value types and GC types we have brought our Value size from 40 bytes => 24 bytes and an 80% reduction in arithmetic operations!

Parser rebuild, better code organisation

Boa was predominantly 3 files. The lexer, parser and interpreter.
The naive implementation of the parser was a single file which had a long match expression for tokens and went through every token figuring out what to do. This did the job but became unmaintainable when adding new features.
We have been breaking the parser up into separate modules, which represent various expressions and statements that conform to the specification. (more about this in a future post).

After all the fixes in this release we've seen on average a 70% improvement, we still have areas where we plan to improve. We are currently rebuilding the lexer so it is more broken up like the parser and interpreter, we will blog about this soon in future.

Roadmap

How much of the specification is covered?

Our next milestone is to tidy up the lexer so it takes into account goal symbols then we plan to start running Test 262, the official ECMAScript Test Suite. It has a lot of tests (over 29272 test files) and will tell us in detail which parts of the specification need work.
There are also large items like classes which are still not covered, however, these should now be easier to implement with parsing broken up.

Public API

#445 looks to improve the public API too so Rust projects can interactive with Boa more easily.
It should be possible today to just use the lexer, parser or the whole execution path.

We hope to add more detail in future on how some parts of Boa work, make sure you stay tuned for any future posts!