Browse Source

Implement snapshot Serializer

feature/snapshot
Haled Odat 1 year ago
parent
commit
db0c9162ce
  1. 3
      .gitignore
  2. 7
      boa_cli/src/debug/mod.rs
  3. 29
      boa_cli/src/debug/snapshot.rs
  4. 82
      boa_engine/src/context/intrinsics.rs
  5. 11
      boa_engine/src/context/mod.rs
  6. 2
      boa_engine/src/lib.rs
  7. 10
      boa_engine/src/object/builtins/jsfunction.rs
  8. 12
      boa_engine/src/object/jsobject.rs
  9. 11
      boa_engine/src/object/mod.rs
  10. 10
      boa_engine/src/object/property_map.rs
  11. 20
      boa_engine/src/realm.rs
  12. 273
      boa_engine/src/snapshot/mod.rs
  13. 8
      boa_engine/src/string/mod.rs
  14. 4
      boa_engine/src/symbol.rs
  15. 30
      boa_engine/src/value/mod.rs
  16. 45
      boa_gc/src/lib.rs
  17. 66
      docs/snapshot.md

3
.gitignore vendored

@ -36,3 +36,6 @@ chrome_profiler.json
# e2e test
playwright-report
test-results
# Binary snapshot file
snapshot.bin

7
boa_cli/src/debug/mod.rs

@ -10,6 +10,7 @@ mod object;
mod optimizer;
mod realm;
mod shape;
mod snapshot;
fn create_boa_object(context: &mut Context<'_>) -> JsObject {
let function_module = function::create_object(context);
@ -19,6 +20,7 @@ fn create_boa_object(context: &mut Context<'_>) -> JsObject {
let gc_module = gc::create_object(context);
let realm_module = realm::create_object(context);
let limits_module = limits::create_object(context);
let snapshot_module = snapshot::create_object(context);
ObjectInitializer::new(context)
.property(
@ -56,6 +58,11 @@ fn create_boa_object(context: &mut Context<'_>) -> JsObject {
limits_module,
Attribute::WRITABLE | Attribute::NON_ENUMERABLE | Attribute::CONFIGURABLE,
)
.property(
"snapshot",
snapshot_module,
Attribute::WRITABLE | Attribute::NON_ENUMERABLE | Attribute::CONFIGURABLE,
)
.build()
}

29
boa_cli/src/debug/snapshot.rs

@ -0,0 +1,29 @@
use std::{fs::OpenOptions, io::Write};
use boa_engine::{
object::ObjectInitializer, snapshot::SnapshotSerializer, Context, JsNativeError, JsObject,
JsResult, JsValue, NativeFunction,
};
const SNAPSHOT_PATH: &str = "./snapshot.bin";
fn create(_: &JsValue, _: &[JsValue], context: &mut Context<'_>) -> JsResult<JsValue> {
let Ok(mut file) = OpenOptions::new().write(true).create(true).open(SNAPSHOT_PATH) else {
return Err(JsNativeError::error().with_message("could not create snapshot.bin file").into());
};
let mut serializer = SnapshotSerializer::new();
serializer.serialize(context).unwrap();
file.write_all(serializer.bytes()).unwrap();
file.flush().unwrap();
Ok(JsValue::undefined())
}
pub(super) fn create_object(context: &mut Context<'_>) -> JsObject {
ObjectInitializer::new(context)
.function(NativeFunction::from_fn_ptr(create), "create", 0)
.build()
}

82
boa_engine/src/context/intrinsics.rs

@ -26,6 +26,16 @@ pub struct Intrinsics {
pub(super) templates: ObjectTemplates,
}
impl crate::snapshot::Serialize for Intrinsics {
fn serialize(
&self,
s: &mut crate::snapshot::SnapshotSerializer,
) -> Result<(), crate::snapshot::SnapshotError> {
self.constructors.serialize(s)?;
Ok(())
}
}
impl Intrinsics {
pub(crate) fn new(root_shape: &RootShape) -> Self {
let constructors = StandardConstructors::default();
@ -62,6 +72,17 @@ pub struct StandardConstructor {
prototype: JsObject,
}
impl crate::snapshot::Serialize for StandardConstructor {
fn serialize(
&self,
s: &mut crate::snapshot::SnapshotSerializer,
) -> Result<(), crate::snapshot::SnapshotError> {
self.constructor.serialize(s)?;
self.prototype.serialize(s)?;
Ok(())
}
}
impl Default for StandardConstructor {
fn default() -> Self {
Self {
@ -153,6 +174,67 @@ pub struct StandardConstructors {
segmenter: StandardConstructor,
}
impl crate::snapshot::Serialize for StandardConstructors {
fn serialize(
&self,
s: &mut crate::snapshot::SnapshotSerializer,
) -> Result<(), crate::snapshot::SnapshotError> {
self.object.serialize(s)?;
self.proxy.serialize(s)?;
self.date.serialize(s)?;
self.function.serialize(s)?;
self.async_function.serialize(s)?;
self.generator_function.serialize(s)?;
self.async_generator_function.serialize(s)?;
self.array.serialize(s)?;
self.bigint.serialize(s)?;
self.number.serialize(s)?;
self.boolean.serialize(s)?;
self.string.serialize(s)?;
self.regexp.serialize(s)?;
self.symbol.serialize(s)?;
self.error.serialize(s)?;
self.type_error.serialize(s)?;
self.reference_error.serialize(s)?;
self.range_error.serialize(s)?;
self.syntax_error.serialize(s)?;
self.eval_error.serialize(s)?;
self.uri_error.serialize(s)?;
self.aggregate_error.serialize(s)?;
self.map.serialize(s)?;
self.set.serialize(s)?;
self.typed_array.serialize(s)?;
self.typed_int8_array.serialize(s)?;
self.typed_uint8_array.serialize(s)?;
self.typed_uint8clamped_array.serialize(s)?;
self.typed_int16_array.serialize(s)?;
self.typed_uint16_array.serialize(s)?;
self.typed_int32_array.serialize(s)?;
self.typed_uint32_array.serialize(s)?;
self.typed_bigint64_array.serialize(s)?;
self.typed_biguint64_array.serialize(s)?;
self.typed_float32_array.serialize(s)?;
self.typed_float64_array.serialize(s)?;
self.array_buffer.serialize(s)?;
self.data_view.serialize(s)?;
self.date_time_format.serialize(s)?;
self.promise.serialize(s)?;
self.weak_ref.serialize(s)?;
self.weak_map.serialize(s)?;
self.weak_set.serialize(s)?;
#[cfg(feature = "intl")]
self.collator.serialize(s)?;
#[cfg(feature = "intl")]
self.list_format.serialize(s)?;
#[cfg(feature = "intl")]
self.locale.serialize(s)?;
#[cfg(feature = "intl")]
self.segmenter.serialize(s)?;
Ok(())
}
}
impl Default for StandardConstructors {
fn default() -> Self {
Self {

11
boa_engine/src/context/mod.rs

@ -985,3 +985,14 @@ where
}
}
}
impl crate::snapshot::Serialize for Context<'_> {
fn serialize(
&self,
s: &mut crate::snapshot::SnapshotSerializer,
) -> Result<(), crate::snapshot::SnapshotError> {
s.write_bool(self.strict)?;
self.realm.serialize(s)?;
Ok(())
}
}

2
boa_engine/src/lib.rs

@ -147,6 +147,8 @@ mod tests;
pub mod value;
pub mod vm;
pub mod snapshot;
/// A convenience module that re-exports the most commonly-used Boa APIs
pub mod prelude {
pub use crate::{

10
boa_engine/src/object/builtins/jsfunction.rs

@ -16,6 +16,16 @@ pub struct JsFunction {
inner: JsObject,
}
impl crate::snapshot::Serialize for JsFunction {
fn serialize(
&self,
s: &mut crate::snapshot::SnapshotSerializer,
) -> Result<(), crate::snapshot::SnapshotError> {
self.inner.serialize(s)?;
Ok(())
}
}
impl JsFunction {
/// Creates a new `JsFunction` from an object, without checking if the object is callable.
pub(crate) fn from_object_unchecked(object: JsObject) -> Self {

12
boa_engine/src/object/jsobject.rs

@ -51,6 +51,18 @@ pub struct VTableObject {
vtable: &'static InternalObjectMethods,
}
impl crate::snapshot::Serialize for VTableObject {
fn serialize(
&self,
s: &mut crate::snapshot::SnapshotSerializer,
) -> StdResult<(), crate::snapshot::SnapshotError> {
// TODO: add internal methods to references
self.object.borrow().serialize(s)?;
Ok(())
}
}
impl Default for JsObject {
fn default() -> Self {
let data = ObjectData::ordinary();

11
boa_engine/src/object/mod.rs

@ -142,6 +142,17 @@ pub struct Object {
private_elements: ThinVec<(PrivateName, PrivateElement)>,
}
impl crate::snapshot::Serialize for Object {
fn serialize(
&self,
s: &mut crate::snapshot::SnapshotSerializer,
) -> Result<(), crate::snapshot::SnapshotError> {
s.write_bool(self.extensible)?;
self.properties.serialize(s)?;
Ok(())
}
}
impl Default for Object {
fn default() -> Self {
Self {

10
boa_engine/src/object/property_map.rs

@ -226,6 +226,16 @@ pub struct PropertyMap {
pub(crate) storage: ObjectStorage,
}
impl crate::snapshot::Serialize for PropertyMap {
fn serialize(
&self,
s: &mut crate::snapshot::SnapshotSerializer,
) -> Result<(), crate::snapshot::SnapshotError> {
self.storage.serialize(s)?;
Ok(())
}
}
impl PropertyMap {
/// Create a new [`PropertyMap`].
#[must_use]

20
boa_engine/src/realm.rs

@ -25,6 +25,16 @@ pub struct Realm {
inner: Gc<Inner>,
}
impl crate::snapshot::Serialize for Realm {
fn serialize(
&self,
s: &mut crate::snapshot::SnapshotSerializer,
) -> Result<(), crate::snapshot::SnapshotError> {
self.inner.serialize(s)?;
Ok(())
}
}
impl Eq for Realm {}
impl PartialEq for Realm {
@ -54,6 +64,16 @@ struct Inner {
loaded_modules: GcRefCell<FxHashMap<JsString, Module>>,
}
impl crate::snapshot::Serialize for Inner {
fn serialize(
&self,
s: &mut crate::snapshot::SnapshotSerializer,
) -> Result<(), crate::snapshot::SnapshotError> {
self.intrinsics.serialize(s)?;
Ok(())
}
}
impl Realm {
/// Create a new Realm.
#[inline]

273
boa_engine/src/snapshot/mod.rs

@ -0,0 +1,273 @@
//! TODO: doc
#![allow(missing_debug_implementations)]
#![allow(dead_code)]
use crate::{Context, JsBigInt, JsObject, JsString, JsSymbol};
use indexmap::{IndexMap, IndexSet};
use std::fmt::{Debug, Display};
/// TODO: doc
#[derive(Debug, Clone, Copy)]
pub struct Header {
signature: [u8; 4],
version: u32,
// checksum: u64,
}
/// TODO: doc
pub trait Serialize {
/// Serialize type
fn serialize(&self, s: &mut SnapshotSerializer) -> Result<(), SnapshotError>;
}
impl Serialize for Header {
fn serialize(&self, s: &mut SnapshotSerializer) -> Result<(), SnapshotError> {
s.write_bytes(&self.signature)?;
s.write_u32(self.version)?;
Ok(())
}
}
impl Serialize for JsObject {
fn serialize(&self, s: &mut SnapshotSerializer) -> Result<(), SnapshotError> {
let value = s.objects.insert_full(self.clone()).0;
s.write_u32(value as u32)?;
Ok(())
}
}
/// TODO: doc
pub struct Snapshot {
header: Header,
bytes: Vec<u8>,
}
/// TODO: doc
pub struct SnapshotSerializer {
bytes: Vec<u8>,
objects: IndexSet<JsObject>,
strings: IndexMap<usize, JsString>,
symbols: IndexMap<u64, JsSymbol>,
bigints: IndexSet<JsBigInt>,
external_references: IndexSet<usize>,
}
/// TODO: doc
#[derive(Debug)]
pub enum SnapshotError {
/// Input/output error.
///
/// See: [`std::io::Error`].
Io(std::io::Error),
}
impl Display for SnapshotError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
// FIXME: implement better formatting
<Self as Debug>::fmt(self, f)
}
}
impl std::error::Error for SnapshotError {}
impl From<std::io::Error> for SnapshotError {
fn from(value: std::io::Error) -> Self {
Self::Io(value)
}
}
impl SnapshotSerializer {
/// TODO: doc
pub fn new() -> Self {
Self {
bytes: Vec::new(),
objects: IndexSet::default(),
strings: IndexMap::default(),
symbols: IndexMap::default(),
bigints: IndexSet::default(),
external_references: IndexSet::default(),
}
}
/// TODO: doc
pub fn bytes(&self) -> &[u8] {
&self.bytes
}
/// Serialize the given [`Context`].
pub fn serialize(&mut self, context: &mut Context<'_>) -> Result<(), SnapshotError> {
// Remove any garbage objects before serialization.
boa_gc::force_collect();
// boa_gc::walk_gc_alloc_pointers(|address| {
// });
let header = Header {
signature: *b".boa",
version: 42,
};
header.serialize(self)?;
context.serialize(self)?;
for i in 0..self.objects.len() {
let object = self
.objects
.get_index(i)
.expect("There should be an object")
.clone();
object.inner().serialize(self)?;
}
for i in 0..self.symbols.len() {
let (hash, symbol) = self
.symbols
.get_index(i)
.map(|(hash, symbol)| (*hash, symbol.clone()))
.expect("There should be an object");
self.write_u64(hash)?;
if let Some(desc) = symbol.description() {
self.write_bool(true)?;
desc.serialize(self)?;
} else {
self.write_bool(false)?;
}
}
for i in 0..self.strings.len() {
let string = self
.strings
.get_index(i)
.expect("There should be an string")
.1
.clone();
// string.
string.serialize(self)?;
self.write_bool(string.is_static())?;
self.write_usize(string.len())?;
for elem in string.as_slice() {
self.write_u16(*elem)?;
}
}
Ok(())
}
/// TODO: doc
pub fn write_bool(&mut self, v: bool) -> Result<(), SnapshotError> {
Ok(self.write_u8(if v { 1 } else { 0 })?)
}
/// TODO: doc
pub fn write_u8(&mut self, v: u8) -> Result<(), SnapshotError> {
Ok(self.write_bytes(&[v])?)
}
/// TODO: doc
pub fn write_i8(&mut self, v: i8) -> Result<(), SnapshotError> {
Ok(self.write_bytes(&v.to_le_bytes())?)
}
/// TODO: doc
pub fn write_u16(&mut self, v: u16) -> Result<(), SnapshotError> {
Ok(self.write_bytes(&v.to_le_bytes())?)
}
/// TODO: doc
pub fn write_i16(&mut self, v: i16) -> Result<(), SnapshotError> {
Ok(self.write_bytes(&v.to_le_bytes())?)
}
/// TODO: doc
pub fn write_u32(&mut self, v: u32) -> Result<(), SnapshotError> {
Ok(self.write_bytes(&v.to_le_bytes())?)
}
/// TODO: doc
pub fn write_i32(&mut self, v: i32) -> Result<(), SnapshotError> {
Ok(self.write_bytes(&v.to_le_bytes())?)
}
/// TODO: doc
pub fn write_f32(&mut self, v: f32) -> Result<(), SnapshotError> {
Ok(self.write_bytes(&v.to_le_bytes())?)
}
/// TODO: doc
pub fn write_f64(&mut self, v: f64) -> Result<(), SnapshotError> {
Ok(self.write_bytes(&v.to_le_bytes())?)
}
/// TODO: doc
pub fn write_u64(&mut self, v: u64) -> Result<(), SnapshotError> {
Ok(self.write_bytes(&v.to_le_bytes())?)
}
/// TODO: doc
pub fn write_i64(&mut self, v: i64) -> Result<(), SnapshotError> {
Ok(self.write_bytes(&v.to_le_bytes())?)
}
/// TODO: doc
pub fn write_u128(&mut self, v: u128) -> Result<(), SnapshotError> {
Ok(self.write_bytes(&v.to_le_bytes())?)
}
/// TODO: doc
pub fn write_i128(&mut self, v: i128) -> Result<(), SnapshotError> {
Ok(self.write_bytes(&v.to_le_bytes())?)
}
/// TODO: doc
pub fn write_usize(&mut self, v: usize) -> Result<(), SnapshotError> {
Ok(self.write_bytes(&(v as u64).to_le_bytes())?)
}
/// TODO: doc
pub fn write_isize(&mut self, v: isize) -> Result<(), SnapshotError> {
Ok(self.write_bytes(&(v as i64).to_le_bytes())?)
}
/// TODO: doc
pub fn write_string(&mut self, v: &str) -> Result<(), SnapshotError> {
let asb = v.as_bytes();
self.write_usize(asb.len())?;
self.bytes.extend_from_slice(asb);
Ok(())
}
/// TODO: doc
pub fn write_bytes(&mut self, v: &[u8]) -> Result<(), SnapshotError> {
self.bytes.extend_from_slice(v);
Ok(())
}
}
impl<T: Serialize> Serialize for Vec<T> {
fn serialize(&self, s: &mut SnapshotSerializer) -> Result<(), SnapshotError> {
s.write_usize(self.len())?;
for element in self {
element.serialize(s)?;
}
Ok(())
}
}
impl Serialize for JsString {
fn serialize(&self, s: &mut SnapshotSerializer) -> Result<(), SnapshotError> {
let index = s.strings.insert_full(self.ptr.addr(), self.clone()).0;
s.write_u32(index as u32)?;
Ok(())
}
}
impl Serialize for JsSymbol {
fn serialize(&self, s: &mut SnapshotSerializer) -> Result<(), SnapshotError> {
let index = s.symbols.insert_full(self.hash(), self.clone()).0;
s.write_u32(index as u32)?;
Ok(())
}
}
impl Serialize for JsBigInt {
fn serialize(&self, s: &mut SnapshotSerializer) -> Result<(), SnapshotError> {
let index = s.bigints.insert_full(self.clone()).0;
s.write_u32(index as u32)?;
Ok(())
}
}

8
boa_engine/src/string/mod.rs

@ -176,7 +176,7 @@ impl CodePoint {
/// The raw representation of a [`JsString`] in the heap.
#[repr(C)]
struct RawJsString {
pub(crate) struct RawJsString {
/// The UTF-16 length.
len: usize,
@ -206,7 +206,7 @@ const DATA_OFFSET: usize = std::mem::size_of::<RawJsString>();
/// <code>\[u16\]</code>'s methods.
#[derive(Finalize)]
pub struct JsString {
ptr: Tagged<RawJsString>,
pub(crate) ptr: Tagged<RawJsString>,
}
// JsString should always be pointer sized.
@ -616,6 +616,10 @@ impl JsString {
ptr: Tagged::from_non_null(ptr),
}
}
pub(crate) fn is_static(&self) -> bool {
self.ptr.is_tagged()
}
}
impl AsRef<[u16]> for JsString {

4
boa_engine/src/symbol.rs

@ -111,14 +111,14 @@ impl WellKnown {
/// The inner representation of a JavaScript symbol.
#[derive(Debug, Clone)]
struct Inner {
pub(crate) struct Inner {
hash: u64,
description: Option<JsString>,
}
/// This represents a JavaScript symbol primitive.
pub struct JsSymbol {
repr: Tagged<Inner>,
pub(crate) repr: Tagged<Inner>,
}
// SAFETY: `JsSymbol` uses `Arc` to do the reference counting, making this type thread-safe.

30
boa_engine/src/value/mod.rs

@ -58,6 +58,7 @@ static TWO_E_63: Lazy<BigInt> = Lazy::new(|| {
/// A Javascript value
#[derive(Finalize, Debug, Clone)]
#[repr(u8)]
pub enum JsValue {
/// `null` - A null value, for when a value doesn't exist.
Null,
@ -79,6 +80,35 @@ pub enum JsValue {
Symbol(JsSymbol),
}
impl JsValue {
fn discriminant(&self) -> u8 {
// SAFETY: Because `Self` is marked `repr(u8)`, its layout is a `repr(C)` `union`
// between `repr(C)` structs, each of which has the `u8` discriminant as its first
// field, so we can read the discriminant without offsetting the pointer.
unsafe { *<*const _>::from(self).cast::<u8>() }
}
}
impl crate::snapshot::Serialize for JsValue {
fn serialize(
&self,
s: &mut crate::snapshot::SnapshotSerializer,
) -> Result<(), crate::snapshot::SnapshotError> {
let discriminant = self.discriminant();
s.write_u8(discriminant)?;
match self {
JsValue::Null | JsValue::Undefined => Ok(()),
JsValue::Boolean(value) => s.write_bool(*value),
JsValue::String(string) => string.serialize(s),
JsValue::Rational(rational) => s.write_f64(*rational),
JsValue::Integer(integer) => s.write_i32(*integer),
JsValue::BigInt(bigint) => bigint.serialize(s),
JsValue::Object(object) => object.serialize(s),
JsValue::Symbol(symbol) => symbol.serialize(s),
}
}
}
unsafe impl Trace for JsValue {
custom_trace! {this, {
if let Self::Object(o) = this {

45
boa_gc/src/lib.rs

@ -554,3 +554,48 @@ pub fn has_weak_maps() -> bool {
gc.weak_map_start.get().is_some()
})
}
/// Returns `true` is any weak maps are currently allocated.
#[cfg(test)]
#[must_use]
pub fn walk_gc_alloc_pointers<F>(mut f: F)
where
F: FnMut(usize),
{
BOA_GC.with(|current| {
let gc = current.borrow();
let mut weak_map_head = gc.weak_map_start.get();
while let Some(node) = weak_map_head {
f(node.as_ptr() as *const u8 as usize);
// SAFETY:
// The `Allocator` must always ensure its start node is a valid, non-null pointer that
// was allocated by `Box::from_raw(Box::new(..))`.
let unmarked_node = unsafe { node.as_ref() };
weak_map_head = unmarked_node.next().take();
}
let mut strong_head = gc.strong_start.get();
while let Some(node) = strong_head {
f(node.as_ptr() as *const u8 as usize);
// SAFETY:
// The `Allocator` must always ensure its start node is a valid, non-null pointer that
// was allocated by `Box::from_raw(Box::new(..))`.
let unmarked_node = unsafe { node.as_ref() };
strong_head = unmarked_node.header.next.take();
}
let mut eph_head = gc.weak_start.get();
while let Some(node) = eph_head {
f(node.as_ptr() as *const u8 as usize);
// SAFETY:
// The `Allocator` must always ensure its start node is a valid, non-null pointer that
// was allocated by `Box::from_raw(Box::new(..))`.
let unmarked_node = unsafe { node.as_ref() };
eph_head = unmarked_node.header().next.take();
}
});
}

66
docs/snapshot.md

@ -0,0 +1,66 @@
# Snapshot File format
This docoment describes the binary file format of the boa snapshot files.
## Header
The header composes the first part of the snapshot.
| Field | Description |
| --------------------- | ------------------------------------------------------------------------------------------------------------ |
| signature `: [u8; 4]` | This is used to quickly check if this file is a snapshot file (`.boa`) |
| guid | Guid generated in compile time and backed into the binary, that is used to check if snapshot is compatibile. |
| checksum | Checksum that is used to check that the snapshot is not corrupted. |
## JsString Table
After the `Header` the table containing `JsString`s each entry contains
| static? `: u8` | length: `: usize` | `JsString` elements `: [u16]` |
| -------------- | ----------------- | ----------------------------- |
| 0 | 5 | `'H', 'e', 'l', 'l', 'o'` |
| 1 | - | 3 |
| ... | ... | ... |
If it's a static string then it's elements comprise the index into the `STATIC_STRING`s.
## JsSymbol Table
| `JsSymbol` hash `: u64` | Description (index into `JsString` table) `: usize` |
| ----------------------- | --------------------------------------------------- |
| 200 | 0 |
| ... | ... |
## JsBigInt Table
| Length in bytes `: u64` | Content |
| ----------------------- | ------- |
| 32 | ... |
## Shapes (Hidden classes) Table
### Unique Shapes
| `[[prototype]]` `: u32` (index into `JsObject` table) | property count `: u32` | key-value pairs |
| ----------------------------------------------------- | ---------------------- | --------------- |
| | 0 | |
| | ... | |
### Shared Shapes
| previous `: u32` | flags | transition |
| ---------------- | ----- | ---------- |
| `MAX` (root) | ... | `x` |
## JsObject Table
| length | Content |
| ------ | ------- |
| 200 | ... |
| ... | ... |
## JsValue Encoding
type `: u8` (JsValue discriminant, Boolean, Null, etc) followed by the value if it exits.
If following the `JsValue` is an `JsString`, `JsSymbol`, `JsBigInt`, `JsObject` then the
following value will be an index into the appropriate tables.
Loading…
Cancel
Save