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
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:
class CustomCalendar extends Temporal.Calendar {
constructor() {
super("iso8601");
}
inLeapYear(dateLike) {
messageInACalendar = "It's a message in a Calendar!";
return dateLike.daysInYear === 366;
}
}
let messageInACalendar;
const calendar = new CustomCalendar();
const boaReleaseDay = new Temporal.PlainDate(2024, 3, 7, calendar);
const leap = boaReleaseDay.inLeapYear;
messageInACalendar;
In Rust:
use temporal_rs::{components::{calendar::CalendarSlot, Date}, options::ArithmeticOverflow };
use std::str::FromStr;
let calendar = CalendarSlot::<()>::from_str("iso8601").unwrap();
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:
Test262 | v0.17 (July 2023) | v0.18 (Feb 2024) |
---|
Total | 1,915 | 1,920 |
Pass | 1,071 | 1,878 |
Fail | 132 | 2 |
Skipped | 712 | 40 |
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 Context
s 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:
const sab = new SharedArrayBuffer(1024);
const int32 = new Int32Array(sab);
send(worker1, int32);
send(worker2, int32);
int32 = receive();
Atomics.wait(int32, 0, 0);
console.log(int32[0]);
int32 = receive();
console.log(int32[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));
console.log(getSuffix(1));
console.log(getSuffix(2));
console.log(getSuffix(3));
console.log(getSuffix(4));
console.log(getSuffix(21));
console.log(getSuffix(42));
console.log(getSuffix(73));
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.
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));
uint8 = new Uint8Array([4, 5, 7, 8, 9, 11, 12]);
console.log(uint8.findLast(isPrime));
const illFormed = "https://example.com/search?q=\uD800";
try {
encodeURI(illFormed);
} catch (e) {
console.log(e);
}
if (illFormed.isWellFormed()) {
console.log(encodeURI(illFormed));
} else {
console.warn("Ill-formed strings encountered.");
}
const months = ["Mar", "Jan", "Feb", "Dec"];
const sortedMonths = months.toSorted();
console.log(sortedMonths);
console.log(months);
const values = [1, 10, 21, 2];
const sortedValues = values.toSorted((a, b) => a - b);
console.log(sortedValues);
console.log(values);
const array = [1, 2, 3, 4, 5];
Object.groupBy(array, (num, index) => {
return num % 2 === 0 ? "even" : "odd";
});
const odd = { odd: true };
const even = { even: true };
Map.groupBy(array, (num, index) => {
return num % 2 === 0 ? even : odd;
});
const buffer = new ArrayBuffer(8, { maxByteLength: 16 });
console.log(buffer.byteLength);
buffer.resize(12);
console.log(buffer.byteLength);
const buffer = new ArrayBuffer(8, { maxByteLength: 16 });
const view = new Uint8Array(buffer);
view[1] = 2;
view[7] = 4;
const buffer2 = buffer.transfer(4);
console.log(buffer2.byteLength);
console.log(buffer2.maxByteLength);
const view2 = new Uint8Array(buffer2);
console.log(view2[1]);
console.log(view2[7]);
buffer2.resize(8);
console.log(view2[7]);
const buffer3 = buffer2.transfer(12);
console.log(buffer3.byteLength);
buffer3.transfer(20);
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.
#[derive(Default, Trace, Finalize, JsData)]
struct CustomHostDefinedStruct {
#[unsafe_ignore_trace]
counter: usize,
}
let mut context = Context::default();
let realm = context.realm().clone();
realm
.host_defined_mut()
.insert_default::<CustomHostDefinedStruct>();
assert!(realm.host_defined().has::<CustomHostDefinedStruct>());
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_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.
let mut context = Context::default();
context
.register_global_class::<Person>()
.expect("the Person builtin shouldn't exist");
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.
let mut context = Context::default();
context.runtime_limits_mut().set_loop_iteration_limit(10);
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.
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(
&[
js_string!("sum"),
],
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 Send
s 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))?;
}
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 Version | Richards | DeltaBlue | Crypto | RayTrace | EarleyBoyer | Splay | NavierStokes | Total |
---|
v0.16 | 29.0 | 29.2 | 42.1 | 107 | 105 | 111 | 15.4 | 49.1 |
v0.17 | 34.3 | 39.1 | 49.1 | 134 | 119 | 141 | 11.9 | 56.2 |
v0.18 | 49.8 | 53.9 | 52.1 | 161 | 152 | 154 | 102 | 91.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!!