From 08e5e461175938c663523ac9d33845a34d5813e3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Juli=C3=A1n=20Espina?= Date: Thu, 19 Jan 2023 10:37:26 +0000 Subject: [PATCH] Make `JsSymbol` thread-safe (#2539) The section about `Symbol` on the [specification](https://tc39.es/ecma262/#sec-ecmascript-language-types-symbol-type) says: > The Symbol type is the set of all non-String values that may be used as the key of an Object property ([6.1.7](https://tc39.es/ecma262/#sec-object-type)). Each possible Symbol value is unique and immutable. Our previous implementation of `JsSymbol` used `Rc` and a thread local `Cell`. However, this meant that two different symbols in two different threads could share the same hash, making symbols not unique. Also, the [GlobalSymbolRegistry](https://tc39.es/ecma262/#table-globalsymbolregistry-record-fields) is meant to be shared by all realms, including realms that are not in the same thread as the main one; this forces us to replace our current thread local global symbol registry with a thread-safe one that uses `DashMap` for concurrent access. However, the global symbol registry uses `JsString`s as keys and values, which forces us to either use `Vec` instead (wasteful and needs to allocate to convert to `JsString` on each access) or make `JsString` thread-safe with an atomic counter. For this reason, I implemented the second option. This PR changes the following: - Makes `JsSymbol` thread-safe by using Arc instead of Rc, and making `SYMBOL_HASH_COUNT` an `AtomicU64`. - ~~Makes `JsString` thread-safe by using `AtomicUsize` instead of `Cell` for its ref count.~~ EDIT: Talked with @jasonwilliams and we decided to use `Box<[u16]>` for the global registry instead, because this won't penalize common usage of `JsString`, which is used a LOT more than `JsSymbol`. - Makes the `GLOBAL_SYMBOL_REGISTRY` truly global, using `DashMap` as our global map that is shared by all threads. - Replaces some thread locals with thread-safe alternatives, such as static arrays and static indices. - Various improvements to all related code for this. --- Cargo.lock | 47 ++ boa_engine/Cargo.toml | 2 + .../src/builtins/array/array_iterator.rs | 4 +- boa_engine/src/builtins/array/mod.rs | 14 +- boa_engine/src/builtins/array_buffer/mod.rs | 6 +- boa_engine/src/builtins/async_function/mod.rs | 4 +- .../src/builtins/async_generator/mod.rs | 4 +- .../builtins/async_generator_function/mod.rs | 4 +- boa_engine/src/builtins/bigint/mod.rs | 4 +- boa_engine/src/builtins/dataview/mod.rs | 4 +- boa_engine/src/builtins/date/mod.rs | 4 +- boa_engine/src/builtins/function/arguments.rs | 6 +- boa_engine/src/builtins/function/mod.rs | 4 +- boa_engine/src/builtins/generator/mod.rs | 4 +- .../src/builtins/generator_function/mod.rs | 4 +- boa_engine/src/builtins/intl/collator/mod.rs | 4 +- .../src/builtins/intl/list_format/mod.rs | 4 +- boa_engine/src/builtins/intl/locale/mod.rs | 4 +- boa_engine/src/builtins/intl/mod.rs | 4 +- boa_engine/src/builtins/iterable/mod.rs | 14 +- boa_engine/src/builtins/json/mod.rs | 4 +- boa_engine/src/builtins/map/map_iterator.rs | 4 +- boa_engine/src/builtins/map/mod.rs | 8 +- boa_engine/src/builtins/math/mod.rs | 6 +- .../src/builtins/object/for_in_iterator.rs | 4 +- boa_engine/src/builtins/object/mod.rs | 4 +- boa_engine/src/builtins/promise/mod.rs | 6 +- boa_engine/src/builtins/reflect/mod.rs | 4 +- boa_engine/src/builtins/regexp/mod.rs | 30 +- .../builtins/regexp/regexp_string_iterator.rs | 4 +- boa_engine/src/builtins/set/mod.rs | 8 +- boa_engine/src/builtins/set/set_iterator.rs | 4 +- boa_engine/src/builtins/string/mod.rs | 24 +- .../src/builtins/string/string_iterator.rs | 4 +- boa_engine/src/builtins/symbol/mod.rs | 124 ++--- boa_engine/src/builtins/typed_array/mod.rs | 14 +- boa_engine/src/builtins/weak/weak_ref.rs | 4 +- boa_engine/src/lib.rs | 1 + boa_engine/src/object/jsobject.rs | 18 +- boa_engine/src/object/operations.rs | 5 +- boa_engine/src/string/common.rs | 223 ++++++--- boa_engine/src/string/mod.rs | 261 +++-------- boa_engine/src/symbol.rs | 443 ++++++++---------- boa_engine/src/tagged.rs | 109 +++++ boa_engine/src/value/mod.rs | 4 +- boa_engine/src/value/operations.rs | 4 +- boa_engine/src/vm/opcode/mod.rs | 33 +- 47 files changed, 788 insertions(+), 714 deletions(-) create mode 100644 boa_engine/src/tagged.rs diff --git a/Cargo.lock b/Cargo.lock index 75b653ad16..da392d2967 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -200,6 +200,7 @@ dependencies = [ "boa_profiler", "chrono", "criterion", + "dashmap", "fast-float", "float-cmp", "icu_calendar", @@ -215,6 +216,7 @@ dependencies = [ "num-bigint", "num-integer", "num-traits", + "num_enum", "once_cell", "rand", "regress", @@ -884,6 +886,19 @@ dependencies = [ "syn", ] +[[package]] +name = "dashmap" +version = "5.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "907076dfda823b0b36d2a1bb5f90c96660a5bbcd7729e10727f07858f22c4edc" +dependencies = [ + "cfg-if 1.0.0", + "hashbrown 0.12.3", + "lock_api", + "once_cell", + "parking_lot_core 0.9.5", +] + [[package]] name = "databake" version = "0.1.2" @@ -2334,6 +2349,27 @@ dependencies = [ "libc", ] +[[package]] +name = "num_enum" +version = "0.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf5395665662ef45796a4ff5486c5d41d29e0c09640af4c5f17fd94ee2c119c9" +dependencies = [ + "num_enum_derive", +] + +[[package]] +name = "num_enum_derive" +version = "0.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b0498641e53dd6ac1a4f22547548caa6864cc4933784319cd1775271c5a46ce" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "num_threads" version = "0.1.6" @@ -2636,6 +2672,17 @@ version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" +[[package]] +name = "proc-macro-crate" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eda0fc3b0fb7c975631757e14d9049da17374063edb6ebbcbc54d880d4fe94e9" +dependencies = [ + "once_cell", + "thiserror", + "toml", +] + [[package]] name = "proc-macro-error" version = "1.0.4" diff --git a/boa_engine/Cargo.toml b/boa_engine/Cargo.toml index 16524ca067..158a1f8109 100644 --- a/boa_engine/Cargo.toml +++ b/boa_engine/Cargo.toml @@ -62,6 +62,8 @@ tap = "1.0.1" sptr = "0.3.2" static_assertions = "1.1.0" thiserror = "1.0.38" +dashmap = "5.4.0" +num_enum = "0.5.7" # intl deps boa_icu_provider = { workspace = true, optional = true } diff --git a/boa_engine/src/builtins/array/array_iterator.rs b/boa_engine/src/builtins/array/array_iterator.rs index 077b2b0804..f28045ab13 100644 --- a/boa_engine/src/builtins/array/array_iterator.rs +++ b/boa_engine/src/builtins/array/array_iterator.rs @@ -10,7 +10,7 @@ use crate::{ error::JsNativeError, object::{JsObject, ObjectData}, property::{PropertyDescriptor, PropertyNameKind}, - symbol::WellKnownSymbols, + symbol::JsSymbol, Context, JsResult, }; use boa_gc::{Finalize, Trace}; @@ -148,7 +148,7 @@ impl ArrayIterator { JsObject::from_proto_and_data(iterator_prototype, ObjectData::ordinary()); make_builtin_fn(Self::next, "next", &array_iterator, 0, context); - let to_string_tag = WellKnownSymbols::to_string_tag(); + let to_string_tag = JsSymbol::to_string_tag(); let to_string_tag_property = PropertyDescriptor::builder() .value("Array Iterator") .writable(false) diff --git a/boa_engine/src/builtins/array/mod.rs b/boa_engine/src/builtins/array/mod.rs index 8d48b77419..6cd68b4af7 100644 --- a/boa_engine/src/builtins/array/mod.rs +++ b/boa_engine/src/builtins/array/mod.rs @@ -32,7 +32,7 @@ use crate::{ FunctionObjectBuilder, JsFunction, JsObject, ObjectData, }, property::{Attribute, PropertyDescriptor, PropertyNameKind}, - symbol::WellKnownSymbols, + symbol::JsSymbol, value::{IntegerOrInfinity, JsValue}, Context, JsResult, }; @@ -48,8 +48,8 @@ impl BuiltIn for Array { fn init(context: &mut Context<'_>) -> Option { let _timer = Profiler::global().start_event(Self::NAME, "init"); - let symbol_iterator = WellKnownSymbols::iterator(); - let symbol_unscopables = WellKnownSymbols::unscopables(); + let symbol_iterator = JsSymbol::iterator(); + let symbol_unscopables = JsSymbol::unscopables(); let get_species = FunctionObjectBuilder::new(context, NativeFunction::from_fn_ptr(Self::get_species)) @@ -68,7 +68,7 @@ impl BuiltIn for Array { .name(Self::NAME) .length(Self::LENGTH) .static_accessor( - WellKnownSymbols::species(), + JsSymbol::species(), Some(get_species), None, Attribute::CONFIGURABLE, @@ -307,7 +307,7 @@ impl Array { }; // 2. Let spreadable be ? Get(O, @@isConcatSpreadable). - let spreadable = o.get(WellKnownSymbols::is_concat_spreadable(), context)?; + let spreadable = o.get(JsSymbol::is_concat_spreadable(), context)?; // 3. If spreadable is not undefined, return ! ToBoolean(spreadable). if !spreadable.is_undefined() { @@ -366,7 +366,7 @@ impl Array { // 5. If Type(C) is Object, then let c = if let Some(c) = c.as_object() { // 5.a. Set C to ? Get(C, @@species). - let c = c.get(WellKnownSymbols::species(), context)?; + let c = c.get(JsSymbol::species(), context)?; // 5.b. If C is null, set C to undefined. if c.is_null_or_undefined() { JsValue::undefined() @@ -428,7 +428,7 @@ impl Array { }; // 4. Let usingIterator be ? GetMethod(items, @@iterator). - let using_iterator = items.get_method(WellKnownSymbols::iterator(), context)?; + let using_iterator = items.get_method(JsSymbol::iterator(), context)?; let Some(using_iterator) = using_iterator else { // 6. NOTE: items is not an Iterable so assume it is an array-like object. diff --git a/boa_engine/src/builtins/array_buffer/mod.rs b/boa_engine/src/builtins/array_buffer/mod.rs index 69120c8298..d0546421a8 100644 --- a/boa_engine/src/builtins/array_buffer/mod.rs +++ b/boa_engine/src/builtins/array_buffer/mod.rs @@ -20,7 +20,7 @@ use crate::{ FunctionObjectBuilder, JsObject, ObjectData, }, property::Attribute, - symbol::WellKnownSymbols, + symbol::JsSymbol, value::{IntegerOrInfinity, Numeric}, Context, JsResult, JsValue, }; @@ -76,7 +76,7 @@ impl BuiltIn for ArrayBuffer { .length(Self::LENGTH) .accessor("byteLength", Some(get_byte_length), None, flag_attributes) .static_accessor( - WellKnownSymbols::species(), + JsSymbol::species(), Some(get_species), None, Attribute::CONFIGURABLE, @@ -84,7 +84,7 @@ impl BuiltIn for ArrayBuffer { .static_method(Self::is_view, "isView", 1) .method(Self::slice, "slice", 2) .property( - WellKnownSymbols::to_string_tag(), + JsSymbol::to_string_tag(), Self::NAME, Attribute::READONLY | Attribute::NON_ENUMERABLE | Attribute::CONFIGURABLE, ) diff --git a/boa_engine/src/builtins/async_function/mod.rs b/boa_engine/src/builtins/async_function/mod.rs index abf7ac04c3..e2d0fae755 100644 --- a/boa_engine/src/builtins/async_function/mod.rs +++ b/boa_engine/src/builtins/async_function/mod.rs @@ -15,7 +15,7 @@ use crate::{ native_function::NativeFunction, object::ObjectData, property::PropertyDescriptor, - symbol::WellKnownSymbols, + symbol::JsSymbol, Context, JsResult, JsValue, }; use boa_profiler::Profiler; @@ -83,7 +83,7 @@ impl BuiltIn for AsyncFunction { .configurable(true); prototype .borrow_mut() - .insert(WellKnownSymbols::to_string_tag(), property); + .insert(JsSymbol::to_string_tag(), property); None } diff --git a/boa_engine/src/builtins/async_generator/mod.rs b/boa_engine/src/builtins/async_generator/mod.rs index b0a5c8089e..5745f67f3d 100644 --- a/boa_engine/src/builtins/async_generator/mod.rs +++ b/boa_engine/src/builtins/async_generator/mod.rs @@ -14,7 +14,7 @@ use crate::{ native_function::NativeFunction, object::{ConstructorBuilder, FunctionObjectBuilder, JsObject, ObjectData}, property::{Attribute, PropertyDescriptor}, - symbol::WellKnownSymbols, + symbol::JsSymbol, value::JsValue, vm::GeneratorResumeKind, Context, JsError, JsResult, @@ -93,7 +93,7 @@ impl BuiltIn for AsyncGenerator { .name(Self::NAME) .length(0) .property( - WellKnownSymbols::to_string_tag(), + JsSymbol::to_string_tag(), Self::NAME, Attribute::READONLY | Attribute::NON_ENUMERABLE | Attribute::CONFIGURABLE, ) diff --git a/boa_engine/src/builtins/async_generator_function/mod.rs b/boa_engine/src/builtins/async_generator_function/mod.rs index 1ac40d3126..3b66706955 100644 --- a/boa_engine/src/builtins/async_generator_function/mod.rs +++ b/boa_engine/src/builtins/async_generator_function/mod.rs @@ -13,7 +13,7 @@ use crate::{ native_function::NativeFunction, object::ObjectData, property::PropertyDescriptor, - symbol::WellKnownSymbols, + symbol::JsSymbol, value::JsValue, Context, JsResult, }; @@ -106,7 +106,7 @@ impl BuiltIn for AsyncGeneratorFunction { .configurable(true); prototype .borrow_mut() - .insert(WellKnownSymbols::to_string_tag(), property); + .insert(JsSymbol::to_string_tag(), property); None } diff --git a/boa_engine/src/builtins/bigint/mod.rs b/boa_engine/src/builtins/bigint/mod.rs index 6f9a5aa989..b3dd59d12c 100644 --- a/boa_engine/src/builtins/bigint/mod.rs +++ b/boa_engine/src/builtins/bigint/mod.rs @@ -17,7 +17,7 @@ use crate::{ error::JsNativeError, object::ConstructorBuilder, property::Attribute, - symbol::WellKnownSymbols, + symbol::JsSymbol, value::{IntegerOrInfinity, PreferredType}, Context, JsBigInt, JsResult, JsValue, }; @@ -38,7 +38,7 @@ impl BuiltIn for BigInt { fn init(context: &mut Context<'_>) -> Option { let _timer = Profiler::global().start_event(Self::NAME, "init"); - let to_string_tag = WellKnownSymbols::to_string_tag(); + let to_string_tag = JsSymbol::to_string_tag(); ConstructorBuilder::with_standard_constructor( context, diff --git a/boa_engine/src/builtins/dataview/mod.rs b/boa_engine/src/builtins/dataview/mod.rs index 33a0b1afff..fcf7738167 100644 --- a/boa_engine/src/builtins/dataview/mod.rs +++ b/boa_engine/src/builtins/dataview/mod.rs @@ -17,7 +17,7 @@ use crate::{ FunctionObjectBuilder, JsObject, ObjectData, }, property::Attribute, - symbol::WellKnownSymbols, + symbol::JsSymbol, value::JsValue, Context, JsResult, }; @@ -84,7 +84,7 @@ impl BuiltIn for DataView { .method(Self::set_uint16, "setUint16", 2) .method(Self::set_uint32, "setUint32", 2) .property( - WellKnownSymbols::to_string_tag(), + JsSymbol::to_string_tag(), Self::NAME, Attribute::READONLY | Attribute::NON_ENUMERABLE | Attribute::CONFIGURABLE, ) diff --git a/boa_engine/src/builtins/date/mod.rs b/boa_engine/src/builtins/date/mod.rs index 497eb10e04..2890737c3a 100644 --- a/boa_engine/src/builtins/date/mod.rs +++ b/boa_engine/src/builtins/date/mod.rs @@ -23,7 +23,7 @@ use crate::{ internal_methods::get_prototype_from_constructor, ConstructorBuilder, JsObject, ObjectData, }, string::utf16, - symbol::WellKnownSymbols, + symbol::JsSymbol, value::{IntegerOrNan, JsValue, PreferredType}, Context, JsError, JsResult, }; @@ -165,7 +165,7 @@ impl BuiltIn for Date { .method(Self::value_of, "valueOf", 0) .method( Self::to_primitive, - (WellKnownSymbols::to_primitive(), "[Symbol.toPrimitive]"), + (JsSymbol::to_primitive(), "[Symbol.toPrimitive]"), 1, ) .build() diff --git a/boa_engine/src/builtins/function/arguments.rs b/boa_engine/src/builtins/function/arguments.rs index 69b97a6e8e..dae9b0d23f 100644 --- a/boa_engine/src/builtins/function/arguments.rs +++ b/boa_engine/src/builtins/function/arguments.rs @@ -2,7 +2,7 @@ use crate::{ environments::DeclarativeEnvironment, object::{JsObject, ObjectData}, property::PropertyDescriptor, - symbol::{self, WellKnownSymbols}, + symbol::{self, JsSymbol}, Context, JsValue, }; use boa_ast::{function::FormalParameterList, operations::bound_names}; @@ -113,7 +113,7 @@ impl Arguments { // [[Configurable]]: true }). let values_function = context.intrinsics().objects().array_prototype_values(); obj.define_property_or_throw( - symbol::WellKnownSymbols::iterator(), + symbol::JsSymbol::iterator(), PropertyDescriptor::builder() .value(values_function) .writable(true) @@ -264,7 +264,7 @@ impl Arguments { // [[Configurable]]: true }). let values_function = context.intrinsics().objects().array_prototype_values(); obj.define_property_or_throw( - WellKnownSymbols::iterator(), + JsSymbol::iterator(), PropertyDescriptor::builder() .value(values_function) .writable(true) diff --git a/boa_engine/src/builtins/function/mod.rs b/boa_engine/src/builtins/function/mod.rs index fe018aed30..cdb035daf3 100644 --- a/boa_engine/src/builtins/function/mod.rs +++ b/boa_engine/src/builtins/function/mod.rs @@ -23,7 +23,7 @@ use crate::{ object::{ConstructorBuilder, FunctionObjectBuilder, JsFunction, PrivateElement}, property::{Attribute, PropertyDescriptor, PropertyKey}, string::utf16, - symbol::WellKnownSymbols, + symbol::JsSymbol, value::IntegerOrInfinity, Context, JsResult, JsString, JsValue, }; @@ -879,7 +879,7 @@ impl BuiltIn for BuiltInFunctionObject { .constructor(false) .build_function_prototype(&function_prototype); - let symbol_has_instance = WellKnownSymbols::has_instance(); + let symbol_has_instance = JsSymbol::has_instance(); let has_instance = FunctionObjectBuilder::new(context, NativeFunction::from_fn_ptr(Self::has_instance)) diff --git a/boa_engine/src/builtins/generator/mod.rs b/boa_engine/src/builtins/generator/mod.rs index 7008f65ed7..bdbecb1a84 100644 --- a/boa_engine/src/builtins/generator/mod.rs +++ b/boa_engine/src/builtins/generator/mod.rs @@ -15,7 +15,7 @@ use crate::{ error::JsNativeError, object::{ConstructorBuilder, JsObject, ObjectData}, property::{Attribute, PropertyDescriptor}, - symbol::WellKnownSymbols, + symbol::JsSymbol, value::JsValue, vm::{CallFrame, GeneratorResumeKind, ReturnType}, Context, JsError, JsResult, @@ -81,7 +81,7 @@ impl BuiltIn for Generator { .name(Self::NAME) .length(Self::LENGTH) .property( - WellKnownSymbols::to_string_tag(), + JsSymbol::to_string_tag(), Self::NAME, Attribute::READONLY | Attribute::NON_ENUMERABLE | Attribute::CONFIGURABLE, ) diff --git a/boa_engine/src/builtins/generator_function/mod.rs b/boa_engine/src/builtins/generator_function/mod.rs index 845538b5d0..637a9cd81a 100644 --- a/boa_engine/src/builtins/generator_function/mod.rs +++ b/boa_engine/src/builtins/generator_function/mod.rs @@ -18,7 +18,7 @@ use crate::{ native_function::NativeFunction, object::ObjectData, property::PropertyDescriptor, - symbol::WellKnownSymbols, + symbol::JsSymbol, value::JsValue, Context, JsResult, }; @@ -105,7 +105,7 @@ impl BuiltIn for GeneratorFunction { .configurable(true); prototype .borrow_mut() - .insert(WellKnownSymbols::to_string_tag(), property); + .insert(JsSymbol::to_string_tag(), property); None } diff --git a/boa_engine/src/builtins/intl/collator/mod.rs b/boa_engine/src/builtins/intl/collator/mod.rs index 2bd3f30d63..8fcca3d15e 100644 --- a/boa_engine/src/builtins/intl/collator/mod.rs +++ b/boa_engine/src/builtins/intl/collator/mod.rs @@ -20,7 +20,7 @@ use crate::{ FunctionObjectBuilder, JsFunction, JsObject, ObjectData, }, property::Attribute, - symbol::WellKnownSymbols, + symbol::JsSymbol, Context, JsNativeError, JsResult, JsValue, }; @@ -176,7 +176,7 @@ impl BuiltIn for Collator { .length(Self::LENGTH) .static_method(Self::supported_locales_of, "supportedLocalesOf", 1) .property( - WellKnownSymbols::to_string_tag(), + JsSymbol::to_string_tag(), "Intl.Collator", Attribute::CONFIGURABLE, ) diff --git a/boa_engine/src/builtins/intl/list_format/mod.rs b/boa_engine/src/builtins/intl/list_format/mod.rs index 30863570fe..42c5f20c4f 100644 --- a/boa_engine/src/builtins/intl/list_format/mod.rs +++ b/boa_engine/src/builtins/intl/list_format/mod.rs @@ -13,7 +13,7 @@ use crate::{ internal_methods::get_prototype_from_constructor, ConstructorBuilder, JsObject, ObjectData, }, property::Attribute, - symbol::WellKnownSymbols, + symbol::JsSymbol, Context, JsNativeError, JsResult, JsString, JsValue, }; @@ -64,7 +64,7 @@ impl BuiltIn for ListFormat { .length(Self::LENGTH) .static_method(Self::supported_locales_of, "supportedLocalesOf", 1) .property( - WellKnownSymbols::to_string_tag(), + JsSymbol::to_string_tag(), "Intl.ListFormat", Attribute::CONFIGURABLE, ) diff --git a/boa_engine/src/builtins/intl/locale/mod.rs b/boa_engine/src/builtins/intl/locale/mod.rs index e87b3203f0..3815278aaa 100644 --- a/boa_engine/src/builtins/intl/locale/mod.rs +++ b/boa_engine/src/builtins/intl/locale/mod.rs @@ -26,7 +26,7 @@ use crate::{ FunctionObjectBuilder, JsObject, ObjectData, }, property::Attribute, - symbol::WellKnownSymbols, + symbol::JsSymbol, Context, JsNativeError, JsResult, JsString, JsValue, }; @@ -109,7 +109,7 @@ impl BuiltIn for Locale { .name(Self::NAME) .length(Self::LENGTH) .property( - WellKnownSymbols::to_string_tag(), + JsSymbol::to_string_tag(), "Intl.Locale", Attribute::CONFIGURABLE, ) diff --git a/boa_engine/src/builtins/intl/mod.rs b/boa_engine/src/builtins/intl/mod.rs index 57b9a293d9..2132ac214a 100644 --- a/boa_engine/src/builtins/intl/mod.rs +++ b/boa_engine/src/builtins/intl/mod.rs @@ -16,7 +16,7 @@ use crate::{ context::BoaProvider, object::ObjectInitializer, property::Attribute, - symbol::WellKnownSymbols, + symbol::JsSymbol, Context, JsResult, JsValue, }; @@ -57,7 +57,7 @@ impl BuiltIn for Intl { ObjectInitializer::new(context) .property( - WellKnownSymbols::to_string_tag(), + JsSymbol::to_string_tag(), Self::NAME, Attribute::READONLY | Attribute::NON_ENUMERABLE | Attribute::CONFIGURABLE, ) diff --git a/boa_engine/src/builtins/iterable/mod.rs b/boa_engine/src/builtins/iterable/mod.rs index fdd85b2747..0dae4bbaee 100644 --- a/boa_engine/src/builtins/iterable/mod.rs +++ b/boa_engine/src/builtins/iterable/mod.rs @@ -10,7 +10,7 @@ use crate::{ }, error::JsNativeError, object::{JsObject, ObjectInitializer}, - symbol::WellKnownSymbols, + symbol::JsSymbol, Context, JsResult, JsValue, }; use async_from_sync_iterator::create_async_from_sync_iterator_prototype; @@ -181,14 +181,12 @@ impl JsValue { // a. If hint is async, then if hint == IteratorHint::Async { // i. Set method to ? GetMethod(obj, @@asyncIterator). - if let Some(method) = - self.get_method(WellKnownSymbols::async_iterator(), context)? - { + if let Some(method) = self.get_method(JsSymbol::async_iterator(), context)? { Some(method) } else { // ii. If method is undefined, then // 1. Let syncMethod be ? GetMethod(obj, @@iterator). - let sync_method = self.get_method(WellKnownSymbols::iterator(), context)?; + let sync_method = self.get_method(JsSymbol::iterator(), context)?; // 2. Let syncIteratorRecord be ? GetIterator(obj, sync, syncMethod). let sync_iterator_record = @@ -199,7 +197,7 @@ impl JsValue { } } else { // b. Otherwise, set method to ? GetMethod(obj, @@iterator). - self.get_method(WellKnownSymbols::iterator(), context)? + self.get_method(JsSymbol::iterator(), context)? } } .ok_or_else(|| { @@ -239,7 +237,7 @@ impl JsValue { fn create_iterator_prototype(context: &mut Context<'_>) -> JsObject { let _timer = Profiler::global().start_event("Iterator Prototype", "init"); - let symbol_iterator = WellKnownSymbols::iterator(); + let symbol_iterator = JsSymbol::iterator(); let iterator_prototype = ObjectInitializer::new(context) .function( |v, _, _| Ok(v.clone()), @@ -563,7 +561,7 @@ pub(crate) use if_abrupt_close_iterator; fn create_async_iterator_prototype(context: &mut Context<'_>) -> JsObject { let _timer = Profiler::global().start_event("AsyncIteratorPrototype", "init"); - let symbol_iterator = WellKnownSymbols::async_iterator(); + let symbol_iterator = JsSymbol::async_iterator(); let iterator_prototype = ObjectInitializer::new(context) .function( |v, _, _| Ok(v.clone()), diff --git a/boa_engine/src/builtins/json/mod.rs b/boa_engine/src/builtins/json/mod.rs index 9b224d3e4e..832cee8cd8 100644 --- a/boa_engine/src/builtins/json/mod.rs +++ b/boa_engine/src/builtins/json/mod.rs @@ -26,7 +26,7 @@ use crate::{ object::{JsObject, ObjectInitializer, RecursionLimiter}, property::{Attribute, PropertyNameKind}, string::{utf16, CodePoint}, - symbol::WellKnownSymbols, + symbol::JsSymbol, value::IntegerOrInfinity, Context, JsResult, JsString, JsValue, }; @@ -141,7 +141,7 @@ impl BuiltIn for Json { fn init(context: &mut Context<'_>) -> Option { let _timer = Profiler::global().start_event(Self::NAME, "init"); - let to_string_tag = WellKnownSymbols::to_string_tag(); + let to_string_tag = JsSymbol::to_string_tag(); let attribute = Attribute::READONLY | Attribute::NON_ENUMERABLE | Attribute::CONFIGURABLE; ObjectInitializer::new(context) diff --git a/boa_engine/src/builtins/map/map_iterator.rs b/boa_engine/src/builtins/map/map_iterator.rs index 6044da6d57..ad5a47f497 100644 --- a/boa_engine/src/builtins/map/map_iterator.rs +++ b/boa_engine/src/builtins/map/map_iterator.rs @@ -11,7 +11,7 @@ use crate::{ error::JsNativeError, object::{JsObject, ObjectData}, property::{PropertyDescriptor, PropertyNameKind}, - symbol::WellKnownSymbols, + symbol::JsSymbol, Context, JsResult, }; use boa_gc::{Finalize, Trace}; @@ -147,7 +147,7 @@ impl MapIterator { JsObject::from_proto_and_data(iterator_prototype, ObjectData::ordinary()); make_builtin_fn(Self::next, "next", &map_iterator, 0, context); - let to_string_tag = WellKnownSymbols::to_string_tag(); + let to_string_tag = JsSymbol::to_string_tag(); let to_string_tag_property = PropertyDescriptor::builder() .value("Map Iterator") .writable(false) diff --git a/boa_engine/src/builtins/map/mod.rs b/boa_engine/src/builtins/map/mod.rs index 1f9e6388e9..81ad8592b9 100644 --- a/boa_engine/src/builtins/map/mod.rs +++ b/boa_engine/src/builtins/map/mod.rs @@ -22,7 +22,7 @@ use crate::{ FunctionObjectBuilder, JsObject, ObjectData, }, property::{Attribute, PropertyNameKind}, - symbol::WellKnownSymbols, + symbol::JsSymbol, Context, JsResult, JsValue, }; use boa_profiler::Profiler; @@ -71,7 +71,7 @@ impl BuiltIn for Map { .name(Self::NAME) .length(Self::LENGTH) .static_accessor( - WellKnownSymbols::species(), + JsSymbol::species(), Some(get_species), None, Attribute::CONFIGURABLE, @@ -82,12 +82,12 @@ impl BuiltIn for Map { Attribute::WRITABLE | Attribute::NON_ENUMERABLE | Attribute::CONFIGURABLE, ) .property( - WellKnownSymbols::iterator(), + JsSymbol::iterator(), entries_function, Attribute::WRITABLE | Attribute::NON_ENUMERABLE | Attribute::CONFIGURABLE, ) .property( - WellKnownSymbols::to_string_tag(), + JsSymbol::to_string_tag(), Self::NAME, Attribute::READONLY | Attribute::NON_ENUMERABLE | Attribute::CONFIGURABLE, ) diff --git a/boa_engine/src/builtins/math/mod.rs b/boa_engine/src/builtins/math/mod.rs index beb4d78b66..8f23f04f4c 100644 --- a/boa_engine/src/builtins/math/mod.rs +++ b/boa_engine/src/builtins/math/mod.rs @@ -13,8 +13,8 @@ use super::JsArgs; use crate::{ - builtins::BuiltIn, object::ObjectInitializer, property::Attribute, symbol::WellKnownSymbols, - Context, JsResult, JsValue, + builtins::BuiltIn, object::ObjectInitializer, property::Attribute, symbol::JsSymbol, Context, + JsResult, JsValue, }; use boa_profiler::Profiler; use tap::{Conv, Pipe}; @@ -33,7 +33,7 @@ impl BuiltIn for Math { let _timer = Profiler::global().start_event(Self::NAME, "init"); let attribute = Attribute::READONLY | Attribute::NON_ENUMERABLE | Attribute::PERMANENT; - let string_tag = WellKnownSymbols::to_string_tag(); + let string_tag = JsSymbol::to_string_tag(); ObjectInitializer::new(context) .property("E", std::f64::consts::E, attribute) .property("LN10", std::f64::consts::LN_10, attribute) diff --git a/boa_engine/src/builtins/object/for_in_iterator.rs b/boa_engine/src/builtins/object/for_in_iterator.rs index 0ae865c4e0..7cb11388d7 100644 --- a/boa_engine/src/builtins/object/for_in_iterator.rs +++ b/boa_engine/src/builtins/object/for_in_iterator.rs @@ -11,7 +11,7 @@ use crate::{ object::{JsObject, ObjectData}, property::PropertyDescriptor, property::PropertyKey, - symbol::WellKnownSymbols, + symbol::JsSymbol, Context, JsResult, JsString, JsValue, }; use boa_gc::{Finalize, Trace}; @@ -148,7 +148,7 @@ impl ForInIterator { JsObject::from_proto_and_data(iterator_prototype, ObjectData::ordinary()); make_builtin_fn(Self::next, "next", &for_in_iterator, 0, context); - let to_string_tag = WellKnownSymbols::to_string_tag(); + let to_string_tag = JsSymbol::to_string_tag(); let to_string_tag_property = PropertyDescriptor::builder() .value("For In Iterator") .writable(false) diff --git a/boa_engine/src/builtins/object/mod.rs b/boa_engine/src/builtins/object/mod.rs index d98ee1300e..67fe1fccb2 100644 --- a/boa_engine/src/builtins/object/mod.rs +++ b/boa_engine/src/builtins/object/mod.rs @@ -28,7 +28,7 @@ use crate::{ }, property::{Attribute, PropertyDescriptor, PropertyKey, PropertyNameKind}, string::utf16, - symbol::WellKnownSymbols, + symbol::JsSymbol, value::JsValue, Context, JsResult, JsString, }; @@ -822,7 +822,7 @@ impl Object { }; // 15. Let tag be ? Get(O, @@toStringTag). - let tag = o.get(WellKnownSymbols::to_string_tag(), context)?; + let tag = o.get(JsSymbol::to_string_tag(), context)?; // 16. If Type(tag) is not String, set tag to builtinTag. let tag_str = tag.as_string().map_or(builtin_tag, JsString::deref); diff --git a/boa_engine/src/builtins/promise/mod.rs b/boa_engine/src/builtins/promise/mod.rs index d9a997197f..0387d46641 100644 --- a/boa_engine/src/builtins/promise/mod.rs +++ b/boa_engine/src/builtins/promise/mod.rs @@ -15,7 +15,7 @@ use crate::{ FunctionObjectBuilder, JsFunction, JsObject, ObjectData, }, property::{Attribute, PropertyDescriptorBuilder}, - symbol::WellKnownSymbols, + symbol::JsSymbol, value::JsValue, Context, JsError, JsResult, }; @@ -274,7 +274,7 @@ impl BuiltIn for Promise { .static_method(Self::reject, "reject", 1) .static_method(Self::resolve, "resolve", 1) .static_accessor( - WellKnownSymbols::species(), + JsSymbol::species(), Some(get_species), None, Attribute::CONFIGURABLE, @@ -284,7 +284,7 @@ impl BuiltIn for Promise { .method(Self::finally, "finally", 1) // .property( - WellKnownSymbols::to_string_tag(), + JsSymbol::to_string_tag(), Self::NAME, Attribute::READONLY | Attribute::NON_ENUMERABLE | Attribute::CONFIGURABLE, ) diff --git a/boa_engine/src/builtins/reflect/mod.rs b/boa_engine/src/builtins/reflect/mod.rs index 8c5181afd7..af8c7d00aa 100644 --- a/boa_engine/src/builtins/reflect/mod.rs +++ b/boa_engine/src/builtins/reflect/mod.rs @@ -16,7 +16,7 @@ use crate::{ error::JsNativeError, object::ObjectInitializer, property::Attribute, - symbol::WellKnownSymbols, + symbol::JsSymbol, Context, JsResult, JsValue, }; use boa_profiler::Profiler; @@ -35,7 +35,7 @@ impl BuiltIn for Reflect { fn init(context: &mut Context<'_>) -> Option { let _timer = Profiler::global().start_event(Self::NAME, "init"); - let to_string_tag = WellKnownSymbols::to_string_tag(); + let to_string_tag = JsSymbol::to_string_tag(); ObjectInitializer::new(context) .function(Self::apply, "apply", 3) diff --git a/boa_engine/src/builtins/regexp/mod.rs b/boa_engine/src/builtins/regexp/mod.rs index 68e2eaddf9..169b5f81ae 100644 --- a/boa_engine/src/builtins/regexp/mod.rs +++ b/boa_engine/src/builtins/regexp/mod.rs @@ -25,7 +25,7 @@ use crate::{ }, property::{Attribute, PropertyDescriptorBuilder}, string::{utf16, CodePoint}, - symbol::WellKnownSymbols, + symbol::JsSymbol, value::JsValue, Context, JsResult, JsString, }; @@ -115,7 +115,7 @@ impl BuiltIn for RegExp { .name(Self::NAME) .length(Self::LENGTH) .static_accessor( - WellKnownSymbols::species(), + JsSymbol::species(), Some(get_species), None, Attribute::CONFIGURABLE, @@ -124,31 +124,15 @@ impl BuiltIn for RegExp { .method(Self::test, "test", 1) .method(Self::exec, "exec", 1) .method(Self::to_string, "toString", 0) - .method( - Self::r#match, - (WellKnownSymbols::r#match(), "[Symbol.match]"), - 1, - ) + .method(Self::r#match, (JsSymbol::r#match(), "[Symbol.match]"), 1) .method( Self::match_all, - (WellKnownSymbols::match_all(), "[Symbol.matchAll]"), - 1, - ) - .method( - Self::replace, - (WellKnownSymbols::replace(), "[Symbol.replace]"), - 2, - ) - .method( - Self::search, - (WellKnownSymbols::search(), "[Symbol.search]"), + (JsSymbol::match_all(), "[Symbol.matchAll]"), 1, ) - .method( - Self::split, - (WellKnownSymbols::split(), "[Symbol.split]"), - 2, - ) + .method(Self::replace, (JsSymbol::replace(), "[Symbol.replace]"), 2) + .method(Self::search, (JsSymbol::search(), "[Symbol.search]"), 1) + .method(Self::split, (JsSymbol::split(), "[Symbol.split]"), 2) .accessor("hasIndices", Some(get_has_indices), None, flag_attributes) .accessor("global", Some(get_global), None, flag_attributes) .accessor("ignoreCase", Some(get_ignore_case), None, flag_attributes) diff --git a/boa_engine/src/builtins/regexp/regexp_string_iterator.rs b/boa_engine/src/builtins/regexp/regexp_string_iterator.rs index 513dbd01c7..4f8e2a8f0d 100644 --- a/boa_engine/src/builtins/regexp/regexp_string_iterator.rs +++ b/boa_engine/src/builtins/regexp/regexp_string_iterator.rs @@ -15,7 +15,7 @@ use crate::{ error::JsNativeError, object::{JsObject, ObjectData}, property::PropertyDescriptor, - symbol::WellKnownSymbols, + symbol::JsSymbol, Context, JsResult, JsString, JsValue, }; use boa_gc::{Finalize, Trace}; @@ -172,7 +172,7 @@ impl RegExpStringIterator { let result = JsObject::from_proto_and_data(iterator_prototype, ObjectData::ordinary()); make_builtin_fn(Self::next, "next", &result, 0, context); - let to_string_tag = WellKnownSymbols::to_string_tag(); + let to_string_tag = JsSymbol::to_string_tag(); let to_string_tag_property = PropertyDescriptor::builder() .value("RegExp String Iterator") .writable(false) diff --git a/boa_engine/src/builtins/set/mod.rs b/boa_engine/src/builtins/set/mod.rs index 228f9bbc87..1f401e2a80 100644 --- a/boa_engine/src/builtins/set/mod.rs +++ b/boa_engine/src/builtins/set/mod.rs @@ -22,7 +22,7 @@ use crate::{ FunctionObjectBuilder, JsObject, ObjectData, }, property::{Attribute, PropertyNameKind}, - symbol::WellKnownSymbols, + symbol::JsSymbol, Context, JsResult, JsValue, }; use boa_profiler::Profiler; @@ -54,9 +54,9 @@ impl BuiltIn for Set { .name("get size") .build(); - let iterator_symbol = WellKnownSymbols::iterator(); + let iterator_symbol = JsSymbol::iterator(); - let to_string_tag = WellKnownSymbols::to_string_tag(); + let to_string_tag = JsSymbol::to_string_tag(); let values_function = FunctionObjectBuilder::new(context, NativeFunction::from_fn_ptr(Self::values)) @@ -73,7 +73,7 @@ impl BuiltIn for Set { .name(Self::NAME) .length(Self::LENGTH) .static_accessor( - WellKnownSymbols::species(), + JsSymbol::species(), Some(get_species), None, Attribute::CONFIGURABLE, diff --git a/boa_engine/src/builtins/set/set_iterator.rs b/boa_engine/src/builtins/set/set_iterator.rs index 05f6c1efb2..9cf347a80a 100644 --- a/boa_engine/src/builtins/set/set_iterator.rs +++ b/boa_engine/src/builtins/set/set_iterator.rs @@ -10,7 +10,7 @@ use crate::{ error::JsNativeError, object::{JsObject, ObjectData}, property::{PropertyDescriptor, PropertyNameKind}, - symbol::WellKnownSymbols, + symbol::JsSymbol, Context, JsResult, }; use boa_gc::{Finalize, Trace}; @@ -154,7 +154,7 @@ impl SetIterator { JsObject::from_proto_and_data(iterator_prototype, ObjectData::ordinary()); make_builtin_fn(Self::next, "next", &set_iterator, 0, context); - let to_string_tag = WellKnownSymbols::to_string_tag(); + let to_string_tag = JsSymbol::to_string_tag(); let to_string_tag_property = PropertyDescriptor::builder() .value("Set Iterator") .writable(false) diff --git a/boa_engine/src/builtins/string/mod.rs b/boa_engine/src/builtins/string/mod.rs index 55723ca9ad..055957f4c5 100644 --- a/boa_engine/src/builtins/string/mod.rs +++ b/boa_engine/src/builtins/string/mod.rs @@ -25,7 +25,7 @@ use crate::{ property::{Attribute, PropertyDescriptor}, string::utf16, string::{CodePoint, Utf16Trim}, - symbol::WellKnownSymbols, + symbol::JsSymbol, value::IntegerOrInfinity, Context, JsResult, JsString, JsValue, }; @@ -69,7 +69,7 @@ impl BuiltIn for String { fn init(context: &mut Context<'_>) -> Option { let _timer = Profiler::global().start_event(Self::NAME, "init"); - let symbol_iterator = WellKnownSymbols::iterator(); + let symbol_iterator = JsSymbol::iterator(); let attribute = Attribute::READONLY | Attribute::NON_ENUMERABLE | Attribute::PERMANENT; ConstructorBuilder::with_standard_constructor( @@ -930,7 +930,7 @@ impl String { // 2. If searchValue is neither undefined nor null, then if !search_value.is_null_or_undefined() { // a. Let replacer be ? GetMethod(searchValue, @@replace). - let replacer = search_value.get_method(WellKnownSymbols::replace(), context)?; + let replacer = search_value.get_method(JsSymbol::replace(), context)?; // b. If replacer is not undefined, then if let Some(replacer) = replacer { @@ -1053,7 +1053,7 @@ impl String { } // c. Let replacer be ? GetMethod(searchValue, @@replace). - let replacer = search_value.get_method(WellKnownSymbols::replace(), context)?; + let replacer = search_value.get_method(JsSymbol::replace(), context)?; // d. If replacer is not undefined, then if let Some(replacer) = replacer { @@ -1358,7 +1358,7 @@ impl String { let regexp = args.get_or_undefined(0); if !regexp.is_null_or_undefined() { // a. Let matcher be ? GetMethod(regexp, @@match). - let matcher = regexp.get_method(WellKnownSymbols::r#match(), context)?; + let matcher = regexp.get_method(JsSymbol::r#match(), context)?; // b. If matcher is not undefined, then if let Some(matcher) = matcher { // i. Return ? Call(matcher, regexp, « O »). @@ -1373,7 +1373,7 @@ impl String { let rx = RegExp::create(regexp, &JsValue::Undefined, context)?; // 5. Return ? Invoke(rx, @@match, « S »). - rx.invoke(WellKnownSymbols::r#match(), &[JsValue::new(s)], context) + rx.invoke(JsSymbol::r#match(), &[JsValue::new(s)], context) } /// Abstract operation `StringPad ( O, maxLength, fillString, placement )`. @@ -1828,7 +1828,7 @@ impl String { // 2. If separator is neither undefined nor null, then if !separator.is_null_or_undefined() { // a. Let splitter be ? GetMethod(separator, @@split). - let splitter = separator.get_method(WellKnownSymbols::split(), context)?; + let splitter = separator.get_method(JsSymbol::split(), context)?; // b. If splitter is not undefined, then if let Some(splitter) = splitter { // i. Return ? Call(splitter, separator, « O, limit »). @@ -1986,7 +1986,7 @@ impl String { } } // c. Let matcher be ? GetMethod(regexp, @@matchAll). - let matcher = regexp.get_method(WellKnownSymbols::match_all(), context)?; + let matcher = regexp.get_method(JsSymbol::match_all(), context)?; // d. If matcher is not undefined, then if let Some(matcher) = matcher { return matcher.call(regexp, &[o.clone()], context); @@ -2000,7 +2000,7 @@ impl String { let rx = RegExp::create(regexp, &JsValue::new(js_string!("g")), context)?; // 5. Return ? Invoke(rx, @@matchAll, « S »). - rx.invoke(WellKnownSymbols::match_all(), &[JsValue::new(s)], context) + rx.invoke(JsSymbol::match_all(), &[JsValue::new(s)], context) } /// `String.prototype.normalize( [ form ] )` @@ -2127,7 +2127,7 @@ impl String { let regexp = args.get_or_undefined(0); if !regexp.is_null_or_undefined() { // a. Let searcher be ? GetMethod(regexp, @@search). - let searcher = regexp.get_method(WellKnownSymbols::search(), context)?; + let searcher = regexp.get_method(JsSymbol::search(), context)?; // b. If searcher is not undefined, then if let Some(searcher) = searcher { // i. Return ? Call(searcher, regexp, « O »). @@ -2142,7 +2142,7 @@ impl String { let rx = RegExp::create(regexp, &JsValue::Undefined, context)?; // 5. Return ? Invoke(rx, @@search, « string »). - rx.invoke(WellKnownSymbols::search(), &[JsValue::new(string)], context) + rx.invoke(JsSymbol::search(), &[JsValue::new(string)], context) } pub(crate) fn iterator( @@ -2364,7 +2364,7 @@ fn is_reg_exp(argument: &JsValue, context: &mut Context<'_>) -> JsResult { } fn is_reg_exp_object(argument: &JsObject, context: &mut Context<'_>) -> JsResult { // 2. Let matcher be ? Get(argument, @@match). - let matcher = argument.get(WellKnownSymbols::r#match(), context)?; + let matcher = argument.get(JsSymbol::r#match(), context)?; // 3. If matcher is not undefined, return ! ToBoolean(matcher). if !matcher.is_undefined() { diff --git a/boa_engine/src/builtins/string/string_iterator.rs b/boa_engine/src/builtins/string/string_iterator.rs index 79dc146c33..8bdcc71a40 100644 --- a/boa_engine/src/builtins/string/string_iterator.rs +++ b/boa_engine/src/builtins/string/string_iterator.rs @@ -10,7 +10,7 @@ use crate::{ error::JsNativeError, object::{JsObject, ObjectData}, property::PropertyDescriptor, - symbol::WellKnownSymbols, + symbol::JsSymbol, Context, JsResult, JsValue, }; use boa_gc::{Finalize, Trace}; @@ -98,7 +98,7 @@ impl StringIterator { JsObject::from_proto_and_data(iterator_prototype, ObjectData::ordinary()); make_builtin_fn(Self::next, "next", &array_iterator, 0, context); - let to_string_tag = WellKnownSymbols::to_string_tag(); + let to_string_tag = JsSymbol::to_string_tag(); let to_string_tag_property = PropertyDescriptor::builder() .value("String Iterator") .writable(false) diff --git a/boa_engine/src/builtins/symbol/mod.rs b/boa_engine/src/builtins/symbol/mod.rs index 6f85fe9e97..783b993b45 100644 --- a/boa_engine/src/builtins/symbol/mod.rs +++ b/boa_engine/src/builtins/symbol/mod.rs @@ -18,53 +18,67 @@ #[cfg(test)] mod tests; +use std::hash::BuildHasherDefault; + use super::JsArgs; use crate::{ builtins::BuiltIn, error::JsNativeError, + js_string, native_function::NativeFunction, object::{ConstructorBuilder, FunctionObjectBuilder}, property::Attribute, - symbol::{JsSymbol, WellKnownSymbols}, + symbol::JsSymbol, value::JsValue, Context, JsResult, JsString, }; use boa_profiler::Profiler; -use rustc_hash::FxHashMap; -use std::cell::RefCell; +use dashmap::DashMap; +use once_cell::sync::Lazy; +use rustc_hash::FxHasher; use tap::{Conv, Pipe}; -thread_local! { - static GLOBAL_SYMBOL_REGISTRY: RefCell = RefCell::new(GlobalSymbolRegistry::new()); -} +static GLOBAL_SYMBOL_REGISTRY: Lazy = Lazy::new(GlobalSymbolRegistry::new); + +type FxDashMap = DashMap>; +// We previously used `JsString` instead of `Box<[u16]>` for this, but since the glocal symbol +// registry needed to be global, we had to either make `JsString` thread-safe or directly store +// its info into the registry. `JsSymbol` is already a pretty niche feature of JS, and we expect only +// advanced users to utilize it. On the other hand, almost every JS programmer uses `JsString`s, and +// the first option would impact performance for all `JsString`s in general. For those reasons, we +// opted for the second option, but we should try to optimize this in the future. struct GlobalSymbolRegistry { - keys: FxHashMap, - symbols: FxHashMap, + keys: FxDashMap, JsSymbol>, + symbols: FxDashMap>, } impl GlobalSymbolRegistry { fn new() -> Self { Self { - keys: FxHashMap::default(), - symbols: FxHashMap::default(), + keys: FxDashMap::default(), + symbols: FxDashMap::default(), } } - fn get_or_insert_key(&mut self, key: JsString) -> JsSymbol { - if let Some(symbol) = self.keys.get(&key) { - return symbol.clone(); + fn get_or_create_symbol(&self, key: &JsString) -> JsResult { + let slice = &**key; + if let Some(symbol) = self.keys.get(slice) { + return Ok(symbol.clone()); } - let symbol = JsSymbol::new(Some(key.clone())); - self.keys.insert(key.clone(), symbol.clone()); - self.symbols.insert(symbol.clone(), key); - symbol + let symbol = JsSymbol::new(Some(key.clone())).ok_or_else(|| { + JsNativeError::range() + .with_message("reached the maximum number of symbols that can be created") + })?; + self.keys.insert(slice.into(), symbol.clone()); + self.symbols.insert(symbol.clone(), slice.into()); + Ok(symbol) } - fn get_symbol(&self, sym: &JsSymbol) -> Option { + fn get_key(&self, sym: &JsSymbol) -> Option { if let Some(key) = self.symbols.get(sym) { - return Some(key.clone()); + return Some(js_string!(&**key)); } None @@ -81,19 +95,19 @@ impl BuiltIn for Symbol { fn init(context: &mut Context<'_>) -> Option { let _timer = Profiler::global().start_event(Self::NAME, "init"); - let symbol_async_iterator = WellKnownSymbols::async_iterator(); - let symbol_has_instance = WellKnownSymbols::has_instance(); - let symbol_is_concat_spreadable = WellKnownSymbols::is_concat_spreadable(); - let symbol_iterator = WellKnownSymbols::iterator(); - let symbol_match = WellKnownSymbols::r#match(); - let symbol_match_all = WellKnownSymbols::match_all(); - let symbol_replace = WellKnownSymbols::replace(); - let symbol_search = WellKnownSymbols::search(); - let symbol_species = WellKnownSymbols::species(); - let symbol_split = WellKnownSymbols::split(); - let symbol_to_primitive = WellKnownSymbols::to_primitive(); - let symbol_to_string_tag = WellKnownSymbols::to_string_tag(); - let symbol_unscopables = WellKnownSymbols::unscopables(); + let symbol_async_iterator = JsSymbol::async_iterator(); + let symbol_has_instance = JsSymbol::has_instance(); + let symbol_is_concat_spreadable = JsSymbol::is_concat_spreadable(); + let symbol_iterator = JsSymbol::iterator(); + let symbol_match = JsSymbol::r#match(); + let symbol_match_all = JsSymbol::match_all(); + let symbol_replace = JsSymbol::replace(); + let symbol_search = JsSymbol::search(); + let symbol_species = JsSymbol::species(); + let symbol_split = JsSymbol::split(); + let symbol_to_primitive = JsSymbol::to_primitive(); + let symbol_to_string_tag = JsSymbol::to_string_tag(); + let symbol_unscopables = JsSymbol::unscopables(); let attribute = Attribute::READONLY | Attribute::NON_ENUMERABLE | Attribute::PERMANENT; @@ -193,7 +207,12 @@ impl Symbol { }; // 4. Return a new unique Symbol value whose [[Description]] value is descString. - Ok(JsSymbol::new(description).into()) + Ok(JsSymbol::new(description) + .ok_or_else(|| { + JsNativeError::range() + .with_message("reached the maximum number of symbols that can be created") + })? + .into()) } fn this_symbol_value(value: &JsValue) -> JsResult { @@ -300,12 +319,9 @@ impl Symbol { // 4. Let newSymbol be a new unique Symbol value whose [[Description]] value is stringKey. // 5. Append the Record { [[Key]]: stringKey, [[Symbol]]: newSymbol } to the GlobalSymbolRegistry List. // 6. Return newSymbol. - Ok(GLOBAL_SYMBOL_REGISTRY - .with(move |registry| { - let mut registry = registry.borrow_mut(); - registry.get_or_insert_key(string_key) - }) - .into()) + GLOBAL_SYMBOL_REGISTRY + .get_or_create_symbol(&string_key) + .map(JsValue::from) } /// `Symbol.keyFor( sym )` @@ -318,24 +334,20 @@ impl Symbol { /// [spec]: https://tc39.es/ecma262/#sec-symbol.prototype.keyfor /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Symbol/keyFor pub(crate) fn key_for(_: &JsValue, args: &[JsValue], _: &mut Context<'_>) -> JsResult { - let sym = args.get_or_undefined(0); // 1. If Type(sym) is not Symbol, throw a TypeError exception. - if let Some(sym) = sym.as_symbol() { - // 2. For each element e of the GlobalSymbolRegistry List (see 20.4.2.2), do - // a. If SameValue(e.[[Symbol]], sym) is true, return e.[[Key]]. - // 3. Assert: GlobalSymbolRegistry does not currently contain an entry for sym. - // 4. Return undefined. - let symbol = GLOBAL_SYMBOL_REGISTRY.with(move |registry| { - let registry = registry.borrow(); - registry.get_symbol(&sym) - }); - - Ok(symbol.map(JsValue::from).unwrap_or_default()) - } else { - Err(JsNativeError::typ() - .with_message("Symbol.keyFor: sym is not a symbol") - .into()) - } + let sym = args.get_or_undefined(0).as_symbol().ok_or_else(|| { + JsNativeError::typ().with_message("Symbol.keyFor: sym is not a symbol") + })?; + + // 2. For each element e of the GlobalSymbolRegistry List (see 20.4.2.2), do + // a. If SameValue(e.[[Symbol]], sym) is true, return e.[[Key]]. + // 3. Assert: GlobalSymbolRegistry does not currently contain an entry for sym. + // 4. Return undefined. + + Ok(GLOBAL_SYMBOL_REGISTRY + .get_key(&sym) + .map(JsValue::from) + .unwrap_or_default()) } /// `Symbol.prototype [ @@toPrimitive ]` diff --git a/boa_engine/src/builtins/typed_array/mod.rs b/boa_engine/src/builtins/typed_array/mod.rs index 12dce54308..2db07f685f 100644 --- a/boa_engine/src/builtins/typed_array/mod.rs +++ b/boa_engine/src/builtins/typed_array/mod.rs @@ -28,7 +28,7 @@ use crate::{ FunctionObjectBuilder, JsObject, ObjectData, }, property::{Attribute, PropertyNameKind}, - symbol::WellKnownSymbols, + symbol::JsSymbol, value::{IntegerOrInfinity, JsValue}, Context, JsResult, }; @@ -87,7 +87,7 @@ macro_rules! typed_array { .name(Self::NAME) .length(Self::LENGTH) .static_accessor( - WellKnownSymbols::species(), + JsSymbol::species(), Some(get_species), None, Attribute::CONFIGURABLE, @@ -197,7 +197,7 @@ macro_rules! typed_array { let first_argument_v = JsValue::from(first_argument.clone()); let using_iterator = - first_argument_v.get_method(WellKnownSymbols::replace(), context)?; + first_argument_v.get_method(JsSymbol::replace(), context)?; // 3. If usingIterator is not undefined, then if let Some(using_iterator) = using_iterator { @@ -305,7 +305,7 @@ impl BuiltIn for TypedArray { .name(Self::NAME) .length(Self::LENGTH) .static_accessor( - WellKnownSymbols::species(), + JsSymbol::species(), Some(get_species), None, Attribute::CONFIGURABLE, @@ -316,7 +316,7 @@ impl BuiltIn for TypedArray { Attribute::WRITABLE | Attribute::NON_ENUMERABLE | Attribute::PERMANENT, ) .property( - WellKnownSymbols::iterator(), + JsSymbol::iterator(), values_function, Attribute::WRITABLE | Attribute::NON_ENUMERABLE | Attribute::CONFIGURABLE, ) @@ -345,7 +345,7 @@ impl BuiltIn for TypedArray { Attribute::CONFIGURABLE | Attribute::NON_ENUMERABLE, ) .accessor( - WellKnownSymbols::to_string_tag(), + JsSymbol::to_string_tag(), Some(get_to_string_tag), None, Attribute::CONFIGURABLE | Attribute::NON_ENUMERABLE, @@ -441,7 +441,7 @@ impl TypedArray { // 5. Let usingIterator be ? GetMethod(source, @@iterator). let source = args.get_or_undefined(0); - let using_iterator = source.get_method(WellKnownSymbols::iterator(), context)?; + let using_iterator = source.get_method(JsSymbol::iterator(), context)?; let this_arg = args.get_or_undefined(2); diff --git a/boa_engine/src/builtins/weak/weak_ref.rs b/boa_engine/src/builtins/weak/weak_ref.rs index 9db9b2be81..f6fcfcc03d 100644 --- a/boa_engine/src/builtins/weak/weak_ref.rs +++ b/boa_engine/src/builtins/weak/weak_ref.rs @@ -9,7 +9,7 @@ use crate::{ internal_methods::get_prototype_from_constructor, ConstructorBuilder, JsObject, ObjectData, }, property::Attribute, - symbol::WellKnownSymbols, + symbol::JsSymbol, Context, JsNativeError, JsResult, JsValue, }; @@ -41,7 +41,7 @@ impl BuiltIn for WeakRef { .name(Self::NAME) .length(Self::LENGTH) .property( - WellKnownSymbols::to_string_tag(), + JsSymbol::to_string_tag(), "WeakRef", Attribute::CONFIGURABLE, ) diff --git a/boa_engine/src/lib.rs b/boa_engine/src/lib.rs index 7f2a8604ad..c8303cbae6 100644 --- a/boa_engine/src/lib.rs +++ b/boa_engine/src/lib.rs @@ -124,6 +124,7 @@ pub mod symbol; pub mod value; pub mod vm; +pub(crate) mod tagged; #[cfg(test)] mod tests; diff --git a/boa_engine/src/object/jsobject.rs b/boa_engine/src/object/jsobject.rs index a700bc5566..cbccfea182 100644 --- a/boa_engine/src/object/jsobject.rs +++ b/boa_engine/src/object/jsobject.rs @@ -840,10 +840,10 @@ impl Drop for RecursionLimiter { fn drop(&mut self) { if self.top_level { // When the top level of the graph is dropped, we can free the entire map for the next traversal. - Self::SEEN.with(|hm| hm.borrow_mut().clear()); + SEEN.with(|hm| hm.borrow_mut().clear()); } else if !self.live { // This was the first RL for this object to become live, so it's no longer live now that it's dropped. - Self::SEEN.with(|hm| { + SEEN.with(|hm| { hm.borrow_mut() .insert(self.ptr, RecursionValueState::Visited) }); @@ -851,13 +851,13 @@ impl Drop for RecursionLimiter { } } -impl RecursionLimiter { - thread_local! { - /// The map of pointers to `JsObject` that have been visited during the current `Debug::fmt` graph, - /// and the current state of their RecursionLimiter (dropped or live -- see `RecursionValueState`) - static SEEN: RefCell> = RefCell::new(HashMap::new()); - } +thread_local! { + /// The map of pointers to `JsObject` that have been visited during the current `Debug::fmt` graph, + /// and the current state of their RecursionLimiter (dropped or live -- see `RecursionValueState`) + static SEEN: RefCell> = RefCell::new(HashMap::new()); +} +impl RecursionLimiter { /// Determines if the specified `JsObject` has been visited, and returns a struct that will free it when dropped. /// /// This is done by maintaining a thread-local hashset containing the pointers of `JsObject` values that have been @@ -867,7 +867,7 @@ impl RecursionLimiter { // We shouldn't have to worry too much about this being moved during Debug::fmt. #[allow(trivial_casts)] let ptr = (o.as_ref() as *const _) as usize; - let (top_level, visited, live) = Self::SEEN.with(|hm| { + let (top_level, visited, live) = SEEN.with(|hm| { let mut hm = hm.borrow_mut(); let top_level = hm.is_empty(); let old_state = hm.insert(ptr, RecursionValueState::Live); diff --git a/boa_engine/src/object/operations.rs b/boa_engine/src/object/operations.rs index 2039bfa66a..20718bbd7d 100644 --- a/boa_engine/src/object/operations.rs +++ b/boa_engine/src/object/operations.rs @@ -4,9 +4,8 @@ use crate::{ error::JsNativeError, object::{JsObject, PrivateElement}, property::{PropertyDescriptor, PropertyDescriptorBuilder, PropertyKey, PropertyNameKind}, - symbol::WellKnownSymbols, value::Type, - Context, JsResult, JsValue, + Context, JsResult, JsSymbol, JsValue, }; use boa_ast::function::PrivateName; @@ -512,7 +511,7 @@ impl JsObject { })?; // 5. Let S be ? Get(C, @@species). - let s = c.get(WellKnownSymbols::species(), context)?; + let s = c.get(JsSymbol::species(), context)?; // 6. If S is either undefined or null, return defaultConstructor. if s.is_null_or_undefined() { diff --git a/boa_engine/src/string/common.rs b/boa_engine/src/string/common.rs index 5382fae330..50a3b711e8 100644 --- a/boa_engine/src/string/common.rs +++ b/boa_engine/src/string/common.rs @@ -1,15 +1,145 @@ use std::hash::BuildHasherDefault; +use crate::tagged::Tagged; + +use super::JsString; use boa_macros::utf16; use rustc_hash::{FxHashMap, FxHasher}; -use super::{JsString, TaggedJsString}; +macro_rules! well_known_statics { + ( $( $(#[$attr:meta])* ($name:ident, $string:literal) ),+$(,)? ) => { + $( + $(#[$attr])* pub(crate) const fn $name() -> JsString { + JsString { + ptr: Tagged::from_tag( + Self::find_index(utf16!($string)), + ), + } + } + )+ + }; +} /// List of commonly used strings in Javascript code. /// -/// Any string defined here is used as a static [`JsString`] instead of allocating on the heap. -pub(super) const COMMON_STRINGS: &[&[u16]] = &[ - // Empty string +/// Any strings defined here are used as a static [`JsString`] instead of allocating on the heap. +#[derive(Debug)] +pub(crate) struct StaticJsStrings; + +impl StaticJsStrings { + // useful to search at compile time a certain string in the array + const fn find_index(candidate: &[u16]) -> usize { + const fn const_eq(lhs: &[u16], rhs: &[u16]) -> bool { + if lhs.len() != rhs.len() { + return false; + } + + let mut i = 0; + while i < lhs.len() { + if lhs[i] != rhs[i] { + return false; + } + i += 1; + } + true + } + let mut i = 0; + while i < RAW_STATICS.len() { + let s = RAW_STATICS[i]; + if const_eq(s, candidate) { + return i; + } + i += 1; + } + panic!("couldn't find the required string on the common string array"); + } + + /// Gets the `JsString` corresponding to `string`, or `None` if the string + /// doesn't exist inside the static array. + pub(crate) fn get_string(string: &[u16]) -> Option { + if string.len() > MAX_STATIC_LENGTH { + return None; + } + + let index = RAW_STATICS_CACHE.with(|map| map.get(string).copied())?; + + Some(JsString { + ptr: Tagged::from_tag(index), + }) + } + + /// Gets the `&[u16]` slice corresponding to the provided index, or `None` if the index + /// provided exceeds the size of the static array. + pub(crate) fn get(index: usize) -> Option<&'static [u16]> { + RAW_STATICS.get(index).copied() + } + + well_known_statics! { + /// Gets the empty string (`""`) `JsString`. + (empty_string, ""), + /// Gets the static `JsString` for `"Symbol.asyncIterator"`. + (symbol_async_iterator, "Symbol.asyncIterator"), + /// Gets the static `JsString` for `"Symbol.hasInstance"`. + (symbol_has_instance, "Symbol.hasInstance"), + /// Gets the static `JsString` for `"Symbol.isConcatSpreadable"`. + (symbol_is_concat_spreadable, "Symbol.isConcatSpreadable"), + /// Gets the static `JsString` for `"Symbol.iterator"`. + (symbol_iterator, "Symbol.iterator"), + /// Gets the static `JsString` for `"Symbol.match"`. + (symbol_match, "Symbol.match"), + /// Gets the static `JsString` for `"Symbol.matchAll"`. + (symbol_match_all, "Symbol.matchAll"), + /// Gets the static `JsString` for `"Symbol.replace"`. + (symbol_replace, "Symbol.replace"), + /// Gets the static `JsString` for `"Symbol.search"`. + (symbol_search, "Symbol.search"), + /// Gets the static `JsString` for `"Symbol.species"`. + (symbol_species, "Symbol.species"), + /// Gets the static `JsString` for `"Symbol.split"`. + (symbol_split, "Symbol.split"), + /// Gets the static `JsString` for `"Symbol.toPrimitive"`. + (symbol_to_primitive, "Symbol.toPrimitive"), + /// Gets the static `JsString` for `"Symbol.toStringTag"`. + (symbol_to_string_tag, "Symbol.toStringTag"), + /// Gets the static `JsString` for `"Symbol.unscopables"`. + (symbol_unscopables, "Symbol.unscopables"), + } +} + +static MAX_STATIC_LENGTH: usize = { + let mut max = 0; + let mut i = 0; + while i < RAW_STATICS.len() { + let len = RAW_STATICS[i].len(); + if len > max { + max = len; + } + i += 1; + } + max +}; + +thread_local! { + /// Map from a string inside [`RAW_STATICS`] to its corresponding static index on `RAW_STATICS`. + static RAW_STATICS_CACHE: FxHashMap<&'static [u16], usize> = { + let mut constants = FxHashMap::with_capacity_and_hasher( + RAW_STATICS.len(), + BuildHasherDefault::::default(), + ); + + for (idx, &s) in RAW_STATICS.iter().enumerate() { + constants.insert(s, idx); + } + + constants + }; +} + +/// Array of raw static strings that aren't reference counted. +/// +/// The macro `static_strings` automatically sorts the array of strings, making it faster +/// for searches by using `binary_search`. +const RAW_STATICS: &[&[u16]] = &[ utf16!(""), // Misc utf16!(","), @@ -80,7 +210,6 @@ pub(super) const COMMON_STRINGS: &[&[u16]] = &[ utf16!("isArray"), utf16!("of"), utf16!("copyWithin"), - utf16!("entries"), utf16!("every"), utf16!("fill"), utf16!("filter"), @@ -116,8 +245,6 @@ pub(super) const COMMON_STRINGS: &[&[u16]] = &[ utf16!("endsWith"), utf16!("fromCharCode"), utf16!("fromCodePoint"), - utf16!("includes"), - utf16!("indexOf"), utf16!("lastIndexOf"), utf16!("match"), utf16!("matchAll"), @@ -129,7 +256,6 @@ pub(super) const COMMON_STRINGS: &[&[u16]] = &[ utf16!("replace"), utf16!("replaceAll"), utf16!("search"), - utf16!("slice"), utf16!("split"), utf16!("startsWith"), utf16!("substr"), @@ -148,7 +274,6 @@ pub(super) const COMMON_STRINGS: &[&[u16]] = &[ utf16!("parseFloat"), utf16!("isFinite"), utf16!("isNaN"), - utf16!("parseInt"), utf16!("EPSILON"), utf16!("MAX_SAFE_INTEGER"), utf16!("MIN_SAFE_INTEGER"), @@ -196,19 +321,8 @@ pub(super) const COMMON_STRINGS: &[&[u16]] = &[ utf16!("asyncIterator"), utf16!("hasInstance"), utf16!("species"), - utf16!("Symbol.species"), utf16!("unscopables"), utf16!("iterator"), - utf16!("Symbol.iterator"), - utf16!("Symbol.match"), - utf16!("[Symbol.match]"), - utf16!("Symbol.matchAll"), - utf16!("Symbol.replace"), - utf16!("[Symbol.replace]"), - utf16!("Symbol.search"), - utf16!("[Symbol.search]"), - utf16!("Symbol.split"), - utf16!("[Symbol.split]"), utf16!("toStringTag"), utf16!("toPrimitive"), utf16!("get description"), @@ -455,46 +569,31 @@ pub(super) const COMMON_STRINGS: &[&[u16]] = &[ utf16!("Z"), utf16!("_"), utf16!("$"), + // Well known symbols + utf16!("Symbol.asyncIterator"), + utf16!("[Symbol.asyncIterator]"), + utf16!("Symbol.hasInstance"), + utf16!("[Symbol.hasInstance]"), + utf16!("Symbol.isConcatSpreadable"), + utf16!("[Symbol.isConcatSpreadable]"), + utf16!("Symbol.iterator"), + utf16!("[Symbol.iterator]"), + utf16!("Symbol.match"), + utf16!("[Symbol.match]"), + utf16!("Symbol.matchAll"), + utf16!("[Symbol.matchAll]"), + utf16!("Symbol.replace"), + utf16!("[Symbol.replace]"), + utf16!("Symbol.search"), + utf16!("[Symbol.search]"), + utf16!("Symbol.species"), + utf16!("[Symbol.species]"), + utf16!("Symbol.split"), + utf16!("[Symbol.split]"), + utf16!("Symbol.toPrimitive"), + utf16!("[Symbol.toPrimitive]"), + utf16!("Symbol.toStringTag"), + utf16!("[Symbol.toStringTag]"), + utf16!("Symbol.unscopables"), + utf16!("[Symbol.unscopables]"), ]; - -/// The maximum length of a string within [`COMMON_STRINGS`]. -/// -/// This is useful to skip checks for strings with lengths > `MAX_COMMON_STRING_LENGTH` and directly -/// allocate on the heap. -pub(super) const MAX_COMMON_STRING_LENGTH: usize = { - let mut max = 0; - let mut i = 0; - while i < COMMON_STRINGS.len() { - let len = COMMON_STRINGS[i].len(); - if len > max { - max = len; - } - i += 1; - } - max -}; - -thread_local! { - /// Map from a string inside [`COMMON_STRINGS`] to its corresponding static [`JsString`]. - pub(super) static COMMON_STRINGS_CACHE: FxHashMap<&'static [u16], JsString> = { - let mut constants = FxHashMap::with_capacity_and_hasher( - COMMON_STRINGS.len(), - BuildHasherDefault::::default(), - ); - - for (idx, &s) in COMMON_STRINGS.iter().enumerate() { - // Safety: - // As we're just building a cache of `JsString` indices to access the stored - // `COMMON_STRINGS`, this cannot generate invalid `TaggedJsString`s, since `idx` is - // always a valid index in `COMMON_STRINGS`. - let v = unsafe { - JsString { - ptr: TaggedJsString::new_static(idx), - } - }; - constants.insert(s, v); - } - - constants - }; -} diff --git a/boa_engine/src/string/mod.rs b/boa_engine/src/string/mod.rs index 72bdede031..1783bcfa9d 100644 --- a/boa_engine/src/string/mod.rs +++ b/boa_engine/src/string/mod.rs @@ -21,9 +21,13 @@ // the same names from the unstable functions of the `std::ptr` module. #![allow(unstable_name_collisions)] -mod common; +pub(crate) mod common; -use crate::{builtins::string::is_trimmable_whitespace, JsBigInt}; +use crate::{ + builtins::string::is_trimmable_whitespace, + tagged::{Tagged, UnwrappedTagged}, + JsBigInt, +}; use boa_gc::{empty_trace, Finalize, Trace}; pub use boa_macros::utf16; @@ -34,12 +38,13 @@ use std::{ convert::Infallible, hash::{Hash, Hasher}, ops::{Deref, Index}, - ptr::{self, NonNull}, + process::abort, + ptr::{self, addr_of, addr_of_mut, NonNull}, slice::SliceIndex, str::FromStr, }; -use self::common::{COMMON_STRINGS, COMMON_STRINGS_CACHE, MAX_COMMON_STRING_LENGTH}; +use self::common::StaticJsStrings; fn alloc_overflow() -> ! { panic!("detected overflow during string allocation") @@ -185,98 +190,11 @@ struct RawJsString { const DATA_OFFSET: usize = std::mem::size_of::(); -/// This struct uses a technique called tagged pointer to benefit from the fact that newly allocated -/// pointers are always word aligned on 64-bits platforms, making it impossible to have a LSB equal -/// to 1. More details about this technique on the article of Wikipedia about [tagged pointers][tagged_wp]. -/// -/// # Representation -/// -/// If the LSB of the internal [`NonNull`] is set (1), then the pointer address represents -/// an index value for [`COMMON_STRINGS`], where the remaining MSBs store the index. -/// Otherwise, the whole pointer represents the address of a heap allocated [`RawJsString`]. -/// -/// It uses [`NonNull`], which guarantees that [`TaggedJsString`] (and subsequently [`JsString`]) can -/// use the "null pointer optimization" to optimize the size of [`Option`]. -/// -/// # Provenance -/// -/// This struct stores a [`NonNull`] instead of a [`NonZeroUsize`][std::num::NonZeroUsize] -/// in order to preserve the provenance of our valid heap pointers. -/// On the other hand, all index values are just casted to invalid pointers, because we don't need to -/// preserve the provenance of [`usize`] indices. -/// -/// [tagged_wp]: https://en.wikipedia.org/wiki/Tagged_pointer -#[repr(transparent)] -#[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)] -struct TaggedJsString(NonNull); - -impl TaggedJsString { - /// Creates a new [`TaggedJsString`] from a pointer to a valid [`RawJsString`]. - /// - /// # Safety - /// - /// `inner` must point to a valid instance of [`RawJsString`], which should be deallocated only - /// by [`JsString`]. - const unsafe fn new_heap(inner: NonNull) -> Self { - Self(inner) - } - - /// Creates a new static [`TaggedJsString`] from the index of an element inside - /// [`COMMON_STRINGS`]. - /// - /// # Safety - /// - /// `idx` must be a valid index on [`COMMON_STRINGS`]. - const unsafe fn new_static(idx: usize) -> Self { - // SAFETY: - // The operation `(idx << 1) | 1` sets the least significant bit to 1, meaning any pointer - // (valid or invalid) created using this address cannot be null. - unsafe { Self(NonNull::new_unchecked(sptr::invalid_mut((idx << 1) | 1))) } - } - - /// Checks if [`TaggedJsString`] contains an index for [`COMMON_STRINGS`]. - fn is_static(self) -> bool { - (self.0.as_ptr() as usize) & 1 == 1 - } - - /// Returns a reference to a string stored on the heap, without checking if its internal pointer - /// is valid. - /// - /// # Safety - /// - /// `self` must be a heap allocated [`RawJsString`]. - const unsafe fn get_heap_unchecked(self) -> NonNull { - self.0 - } - - /// Returns the string inside [`COMMON_STRINGS`] corresponding to the index inside - /// [`TaggedJsString`], without checking its validity. - /// - /// # Safety - /// - /// `self` must not be a pointer to a heap allocated [`RawJsString`], and it must be a valid - /// index inside [`COMMON_STRINGS`]. - unsafe fn get_static_unchecked(self) -> &'static [u16] { - // SAFETY: - // The caller must ensure `self` is a valid index inside `COMMON_STRINGS`. - unsafe { COMMON_STRINGS.get_unchecked((self.0.as_ptr() as usize) >> 1) } - } -} - -/// Enum representing either a pointer to a heap allocated [`RawJsString`] or a static reference to -/// a [`\[u16\]`][slice] inside [`COMMON_STRINGS`]. -enum JsStringPtrKind { - // A string allocated on the heap. - Heap(NonNull), - // A static string slice. - Static(&'static [u16]), -} - /// A UTF-16–encoded, reference counted, immutable string. /// -/// This is pretty similar to a [Rc][std::rc::Rc]\<[\[u16\]][slice]\>, but without -/// the length metadata associated with the `Rc` fat pointer. Instead, the length of -/// every string is stored on the heap, along with its reference counter and its data. +/// This is pretty similar to a [Rc][std::rc::Rc]\<[\[u16\]][slice]\>, but without the +/// length metadata associated with the `Rc` fat pointer. Instead, the length of every string is +/// stored on the heap, along with its reference counter and its data. /// /// We define some commonly used string constants in an interner. For these strings, we don't allocate /// memory on the heap to reduce the overhead of memory allocation and reference counting. @@ -287,7 +205,7 @@ enum JsStringPtrKind { /// \[u16\]'s methods. #[derive(Finalize)] pub struct JsString { - ptr: TaggedJsString, + ptr: Tagged, } // JsString should always be pointer sized. @@ -327,7 +245,7 @@ impl JsString { let string = { // SAFETY: `allocate_inner` guarantees that `ptr` is a valid pointer. - let mut data = unsafe { ptr.as_ptr().cast::().add(DATA_OFFSET).cast() }; + let mut data = unsafe { addr_of_mut!((*ptr.as_ptr()).data).cast() }; for string in strings { let count = string.len(); // SAFETY: @@ -347,17 +265,11 @@ impl JsString { } Self { // Safety: We already know it's a valid heap pointer. - ptr: unsafe { TaggedJsString::new_heap(ptr) }, + ptr: unsafe { Tagged::from_ptr(ptr.as_ptr()) }, } }; - if string.len() <= MAX_COMMON_STRING_LENGTH { - if let Some(constant) = COMMON_STRINGS_CACHE.with(|c| c.get(&string[..]).cloned()) { - return constant; - } - } - - string + StaticJsStrings::get_string(&string[..]).unwrap_or(string) } /// Decodes a [`JsString`] into a [`String`], replacing invalid data with its escaped representation @@ -541,19 +453,6 @@ impl JsString { JsBigInt::from_string(self.to_std_string().ok().as_ref()?) } - /// Returns the inner pointer data, unwrapping its tagged data if the pointer contains a static - /// index for [`COMMON_STRINGS`]. - fn ptr(&self) -> JsStringPtrKind { - // Check the first bit to 1. - if self.ptr.is_static() { - // Safety: We already checked. - JsStringPtrKind::Static(unsafe { self.ptr.get_static_unchecked() }) - } else { - // Safety: We already checked. - JsStringPtrKind::Heap(unsafe { self.ptr.get_heap_unchecked() }) - } - } - /// Allocates a new [`RawJsString`] with an internal capacity of `str_len` chars. /// /// # Panics @@ -616,8 +515,10 @@ impl JsString { // `[u16; str_len]`, the memory of the array must be in the `usize` // range for the allocation to succeed. unsafe { - let data = (*inner).data.as_ptr(); - ptr::eq(inner.cast::().add(offset).cast(), data) + ptr::eq( + inner.cast::().add(offset).cast(), + (*inner).data.as_mut_ptr(), + ) } }); @@ -630,7 +531,7 @@ impl JsString { let ptr = Self::allocate_inner(count); // SAFETY: `allocate_inner` guarantees that `ptr` is a valid pointer. - let data = unsafe { ptr.as_ptr().cast::().add(DATA_OFFSET) }; + let data = unsafe { addr_of_mut!((*ptr.as_ptr()).data) }; // SAFETY: // - We read `count = data.len()` elements from `data`, which is within the bounds of the slice. // - `allocate_inner` must allocate at least `count` elements, which allows us to safely @@ -643,8 +544,8 @@ impl JsString { ptr::copy_nonoverlapping(string.as_ptr(), data.cast(), count); } Self { - // Safety: We already know it's a valid heap pointer. - ptr: unsafe { TaggedJsString::new_heap(ptr) }, + // Safety: `allocate_inner` guarantees `ptr` is a valid heap pointer. + ptr: Tagged::from_non_null(ptr), } } } @@ -664,10 +565,14 @@ impl Borrow<[u16]> for JsString { impl Clone for JsString { #[inline] fn clone(&self) -> Self { - if let JsStringPtrKind::Heap(inner) = self.ptr() { + if let UnwrappedTagged::Ptr(inner) = self.ptr.unwrap() { // SAFETY: The reference count of `JsString` guarantees that `raw` is always valid. - let inner = unsafe { &mut *inner.as_ptr() }; - inner.refcount.set(inner.refcount.get() + 1); + let inner = unsafe { inner.as_ref() }; + let strong = inner.refcount.get().wrapping_add(1); + if strong == 0 { + abort() + } + inner.refcount.set(strong); } Self { ptr: self.ptr } } @@ -676,42 +581,37 @@ impl Clone for JsString { impl Default for JsString { #[inline] fn default() -> Self { - sa::const_assert!(!COMMON_STRINGS.is_empty()); - // Safety: - // `COMMON_STRINGS` must not be empty for this to be safe. - // The static assertion above verifies this. - unsafe { - Self { - ptr: TaggedJsString::new_static(0), - } - } + StaticJsStrings::empty_string() } } impl Drop for JsString { fn drop(&mut self) { - if let JsStringPtrKind::Heap(raw) = self.ptr() { + if let UnwrappedTagged::Ptr(raw) = self.ptr.unwrap() { + // See https://doc.rust-lang.org/src/alloc/sync.rs.html#1672 for details. + // SAFETY: The reference count of `JsString` guarantees that `raw` is always valid. - let inner = unsafe { &mut *raw.as_ptr() }; + let inner = unsafe { raw.as_ref() }; inner.refcount.set(inner.refcount.get() - 1); - if inner.refcount.get() == 0 { - // SAFETY: - // All the checks for the validity of the layout have already been made on `alloc_inner`, - // so we can skip the unwrap. - let layout = unsafe { - Layout::for_value(inner) - .extend(Layout::array::(inner.len).unwrap_unchecked()) - .unwrap_unchecked() - .0 - .pad_to_align() - }; - - // Safety: - // If refcount is 0 and we call drop, that means this is the last `JsString` which - // points to this memory allocation, so deallocating it is safe. - unsafe { - dealloc(raw.as_ptr().cast(), layout); - } + if inner.refcount.get() != 0 { + return; + } + + // SAFETY: + // All the checks for the validity of the layout have already been made on `alloc_inner`, + // so we can skip the unwrap. + let layout = unsafe { + Layout::for_value(inner) + .extend(Layout::array::(inner.len).unwrap_unchecked()) + .unwrap_unchecked() + .0 + .pad_to_align() + }; + // Safety: + // If refcount is 0 and we call drop, that means this is the last `JsString` which + // points to this memory allocation, so deallocating it is safe. + unsafe { + dealloc(raw.as_ptr().cast(), layout); } } } @@ -735,8 +635,8 @@ impl Deref for JsString { type Target = [u16]; fn deref(&self) -> &Self::Target { - match self.ptr() { - JsStringPtrKind::Heap(h) => { + match self.ptr.unwrap() { + UnwrappedTagged::Ptr(h) => { // SAFETY: // - The `RawJsString` type has all the necessary information to reconstruct a valid // slice (length and starting pointer). @@ -747,13 +647,15 @@ impl Deref for JsString { // - The lifetime of `&Self::Target` is shorter than the lifetime of `self`, as seen // by its signature, so this doesn't outlive `self`. unsafe { - std::slice::from_raw_parts( - h.as_ptr().cast::().add(DATA_OFFSET).cast(), - h.as_ref().len, - ) + let h = h.as_ptr(); + std::slice::from_raw_parts(addr_of!((*h).data).cast(), (*h).len) } } - JsStringPtrKind::Static(s) => s, + UnwrappedTagged::Tag(index) => { + // SAFETY: all static strings are valid indices on `STATIC_JS_STRINGS`, so `get` should always + // return `Some`. + unsafe { StaticJsStrings::get(index).unwrap_unchecked() } + } } } } @@ -762,12 +664,7 @@ impl Eq for JsString {} impl From<&[u16]> for JsString { fn from(s: &[u16]) -> Self { - if s.len() <= MAX_COMMON_STRING_LENGTH { - if let Some(constant) = COMMON_STRINGS_CACHE.with(|c| c.get(s).cloned()) { - return constant; - } - } - Self::from_slice_skip_interning(s) + StaticJsStrings::get_string(s).unwrap_or_else(|| Self::from_slice_skip_interning(s)) } } @@ -822,10 +719,6 @@ impl Ord for JsString { impl PartialEq for JsString { fn eq(&self, other: &Self) -> bool { - if self.ptr == other.ptr { - return true; - } - self[..] == other[..] } } @@ -950,19 +843,21 @@ impl ToStringEscaped for [u16] { } #[cfg(test)] mod tests { + use crate::tagged::UnwrappedTagged; + use super::utf16; - use super::{JsString, JsStringPtrKind}; + use super::JsString; impl JsString { /// Gets the number of `JsString`s which point to this allocation. fn refcount(&self) -> Option { - match self.ptr() { - JsStringPtrKind::Heap(inner) => { + match self.ptr.unwrap() { + UnwrappedTagged::Ptr(inner) => { // SAFETY: The reference count of `JsString` guarantees that `inner` is always valid. let inner = unsafe { inner.as_ref() }; Some(inner.refcount.get()) } - JsStringPtrKind::Static(_inner) => None, + UnwrappedTagged::Tag(_inner) => None, } } } @@ -1016,13 +911,13 @@ mod tests { let x = js_string!("Hello"); let y = x.clone(); - assert!(!x.ptr.is_static()); + assert!(!x.ptr.is_tagged()); - assert_eq!(x.ptr, y.ptr); + assert_eq!(x.ptr.addr(), y.ptr.addr()); let z = js_string!("Hello"); - assert_ne!(x.ptr, z.ptr); - assert_ne!(y.ptr, z.ptr); + assert_ne!(x.ptr.addr(), z.ptr.addr()); + assert_ne!(y.ptr.addr(), z.ptr.addr()); } #[test] @@ -1030,13 +925,13 @@ mod tests { let x = js_string!(); let y = x.clone(); - assert!(x.ptr.is_static()); + assert!(x.ptr.is_tagged()); - assert_eq!(x.ptr, y.ptr); + assert_eq!(x.ptr.addr(), y.ptr.addr()); let z = js_string!(); - assert_eq!(x.ptr, z.ptr); - assert_eq!(y.ptr, z.ptr); + assert_eq!(x.ptr.addr(), z.ptr.addr()); + assert_eq!(y.ptr.addr(), z.ptr.addr()); } #[test] diff --git a/boa_engine/src/symbol.rs b/boa_engine/src/symbol.rs index 20192c2b92..dcbc974000 100644 --- a/boa_engine/src/symbol.rs +++ b/boa_engine/src/symbol.rs @@ -15,240 +15,97 @@ //! [spec]: https://tc39.es/ecma262/#sec-symbol-value //! [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Symbol -use crate::{js_string, string::utf16, JsString}; +#![deny( + unsafe_op_in_unsafe_fn, + clippy::undocumented_unsafe_blocks, + clippy::missing_safety_doc +)] + +use crate::{ + js_string, + string::{common::StaticJsStrings, utf16}, + tagged::{Tagged, UnwrappedTagged}, + JsString, +}; use boa_gc::{empty_trace, Finalize, Trace}; + +use num_enum::{IntoPrimitive, TryFromPrimitive}; + use std::{ - cell::Cell, hash::{Hash, Hasher}, - rc::Rc, + sync::{ + atomic::{AtomicU64, Ordering}, + Arc, + }, }; -/// A structure that contains the JavaScript well known symbols. -/// -/// # Examples -/// ``` -/// # use boa_engine::symbol::WellKnownSymbols; -/// -/// let iterator = WellKnownSymbols::iterator(); -/// assert_eq!(iterator.description().unwrap().to_std_string_escaped(), "Symbol.iterator"); -/// ``` -/// This is equivalent to `let iterator = Symbol.iterator` in JavaScript. -#[derive(Debug, Clone)] -pub struct WellKnownSymbols { - async_iterator: JsSymbol, - has_instance: JsSymbol, - is_concat_spreadable: JsSymbol, - iterator: JsSymbol, - r#match: JsSymbol, - match_all: JsSymbol, - replace: JsSymbol, - search: JsSymbol, - species: JsSymbol, - split: JsSymbol, - to_primitive: JsSymbol, - to_string_tag: JsSymbol, - unscopables: JsSymbol, -} - /// Reserved number of symbols. /// /// This is where the well known symbol live /// and internal engine symbols. -const RESERVED_SYMBOL_HASHES: u64 = 128; - -thread_local! { - /// Cached well known symbols - static WELL_KNOW_SYMBOLS: WellKnownSymbols = WellKnownSymbols::new(); +const RESERVED_SYMBOL_HASHES: u64 = 127; + +fn get_id() -> Option { + // Symbol hash. + // + // For now this is an incremented u64 number. + static SYMBOL_HASH_COUNT: AtomicU64 = AtomicU64::new(RESERVED_SYMBOL_HASHES + 1); + + SYMBOL_HASH_COUNT + .fetch_update(Ordering::Relaxed, Ordering::Relaxed, |value| { + value.checked_add(1) + }) + .ok() +} - /// Symbol hash. - /// - /// For now this is an incremented u64 number. - static SYMBOL_HASH_COUNT: Cell = Cell::new(RESERVED_SYMBOL_HASHES); +/// List of well known symbols. +#[derive(Debug, Clone, Copy, TryFromPrimitive, IntoPrimitive)] +#[repr(u8)] +enum WellKnown { + AsyncIterator, + HasInstance, + IsConcatSpreadable, + Iterator, + Match, + MatchAll, + Replace, + Search, + Species, + Split, + ToPrimitive, + ToStringTag, + Unscopables, } -impl WellKnownSymbols { - /// Create the well known symbols. - fn new() -> Self { - let mut count = 0; - - let async_iterator = JsSymbol::with_hash(count, Some("Symbol.asyncIterator".into())); - count += 1; - let has_instance = JsSymbol::with_hash(count, Some("Symbol.hasInstance".into())); - count += 1; - let is_concat_spreadable = - JsSymbol::with_hash(count, Some("Symbol.isConcatSpreadable".into())); - count += 1; - let iterator = JsSymbol::with_hash(count, Some("Symbol.iterator".into())); - count += 1; - let match_ = JsSymbol::with_hash(count, Some("Symbol.match".into())); - count += 1; - let match_all = JsSymbol::with_hash(count, Some("Symbol.matchAll".into())); - count += 1; - let replace = JsSymbol::with_hash(count, Some("Symbol.replace".into())); - count += 1; - let search = JsSymbol::with_hash(count, Some("Symbol.search".into())); - count += 1; - let species = JsSymbol::with_hash(count, Some("Symbol.species".into())); - count += 1; - let split = JsSymbol::with_hash(count, Some("Symbol.split".into())); - count += 1; - let to_primitive = JsSymbol::with_hash(count, Some("Symbol.toPrimitive".into())); - count += 1; - let to_string_tag = JsSymbol::with_hash(count, Some("Symbol.toStringTag".into())); - count += 1; - let unscopables = JsSymbol::with_hash(count, Some("Symbol.unscopables".into())); - - Self { - async_iterator, - has_instance, - is_concat_spreadable, - iterator, - r#match: match_, - match_all, - replace, - search, - species, - split, - to_primitive, - to_string_tag, - unscopables, +impl WellKnown { + const fn description(self) -> JsString { + match self { + WellKnown::AsyncIterator => StaticJsStrings::symbol_async_iterator(), + WellKnown::HasInstance => StaticJsStrings::symbol_has_instance(), + WellKnown::IsConcatSpreadable => StaticJsStrings::symbol_is_concat_spreadable(), + WellKnown::Iterator => StaticJsStrings::symbol_iterator(), + WellKnown::Match => StaticJsStrings::symbol_match(), + WellKnown::MatchAll => StaticJsStrings::symbol_match_all(), + WellKnown::Replace => StaticJsStrings::symbol_replace(), + WellKnown::Search => StaticJsStrings::symbol_search(), + WellKnown::Species => StaticJsStrings::symbol_species(), + WellKnown::Split => StaticJsStrings::symbol_split(), + WellKnown::ToPrimitive => StaticJsStrings::symbol_to_primitive(), + WellKnown::ToStringTag => StaticJsStrings::symbol_to_string_tag(), + WellKnown::Unscopables => StaticJsStrings::symbol_unscopables(), } } - /// The `Symbol.asyncIterator` well known symbol. - /// - /// A method that returns the default `AsyncIterator` for an object. - /// Called by the semantics of the `for-await-of` statement. - #[inline] - #[must_use] - pub fn async_iterator() -> JsSymbol { - WELL_KNOW_SYMBOLS.with(|symbols| symbols.async_iterator.clone()) - } - - /// The `Symbol.hasInstance` well known symbol. - /// - /// A method that determines if a `constructor` object - /// recognizes an object as one of the `constructor`'s instances. - /// Called by the semantics of the instanceof operator. - #[inline] - #[must_use] - pub fn has_instance() -> JsSymbol { - WELL_KNOW_SYMBOLS.with(|symbols| symbols.has_instance.clone()) - } - - /// The `Symbol.isConcatSpreadable` well known symbol. - /// - /// A Boolean valued property that if `true` indicates that - /// an object should be flattened to its array elements - /// by `Array.prototype.concat`. - #[inline] - #[must_use] - pub fn is_concat_spreadable() -> JsSymbol { - WELL_KNOW_SYMBOLS.with(|symbols| symbols.is_concat_spreadable.clone()) - } - - /// The `Symbol.iterator` well known symbol. - /// - /// A method that returns the default Iterator for an object. - /// Called by the semantics of the `for-of` statement. - #[inline] - #[must_use] - pub fn iterator() -> JsSymbol { - WELL_KNOW_SYMBOLS.with(|symbols| symbols.iterator.clone()) - } - - /// The `Symbol.match` well known symbol. - /// - /// A regular expression method that matches the regular expression - /// against a string. Called by the `String.prototype.match` method. - #[inline] - #[must_use] - pub fn r#match() -> JsSymbol { - WELL_KNOW_SYMBOLS.with(|symbols| symbols.r#match.clone()) - } - - /// The `Symbol.matchAll` well known symbol. - /// - /// A regular expression method that returns an iterator, that yields - /// matches of the regular expression against a string. - /// Called by the `String.prototype.matchAll` method. - #[inline] - #[must_use] - pub fn match_all() -> JsSymbol { - WELL_KNOW_SYMBOLS.with(|symbols| symbols.match_all.clone()) - } - - /// The `Symbol.replace` well known symbol. - /// - /// A regular expression method that replaces matched substrings - /// of a string. Called by the `String.prototype.replace` method. - #[inline] - #[must_use] - pub fn replace() -> JsSymbol { - WELL_KNOW_SYMBOLS.with(|symbols| symbols.replace.clone()) - } - - /// The `Symbol.search` well known symbol. - /// - /// A regular expression method that returns the index within a - /// string that matches the regular expression. - /// Called by the `String.prototype.search` method. - #[inline] - #[must_use] - pub fn search() -> JsSymbol { - WELL_KNOW_SYMBOLS.with(|symbols| symbols.search.clone()) - } - - /// The `Symbol.species` well known symbol. - /// - /// A function valued property that is the `constructor` function - /// that is used to create derived objects. - #[inline] - #[must_use] - pub fn species() -> JsSymbol { - WELL_KNOW_SYMBOLS.with(|symbols| symbols.species.clone()) - } - - /// The `Symbol.split` well known symbol. - /// - /// A regular expression method that splits a string at the indices - /// that match the regular expression. - /// Called by the `String.prototype.split` method. - #[inline] - #[must_use] - pub fn split() -> JsSymbol { - WELL_KNOW_SYMBOLS.with(|symbols| symbols.split.clone()) - } - - /// The `Symbol.toPrimitive` well known symbol. - /// - /// A method that converts an object to a corresponding primitive value. - /// Called by the `ToPrimitive` (`Value::to_primitive`) abstract operation. - #[inline] - #[must_use] - pub fn to_primitive() -> JsSymbol { - WELL_KNOW_SYMBOLS.with(|symbols| symbols.to_primitive.clone()) + const fn hash(self) -> u64 { + self as u64 } - /// The `Symbol.toStringTag` well known symbol. - /// - /// A String valued property that is used in the creation of the default - /// string description of an object. - /// Accessed by the built-in method `Object.prototype.toString`. - #[inline] - #[must_use] - pub fn to_string_tag() -> JsSymbol { - WELL_KNOW_SYMBOLS.with(|symbols| symbols.to_string_tag.clone()) + const fn tag(self) -> usize { + self as usize } - /// The `Symbol.unscopables` well known symbol. - /// - /// An object valued property whose own and inherited property names are property - /// names that are excluded from the `with` environment bindings of the associated object. - #[inline] - #[must_use] - pub fn unscopables() -> JsSymbol { - WELL_KNOW_SYMBOLS.with(|symbols| symbols.unscopables.clone()) + fn from_tag(tag: usize) -> Option { + Self::try_from_primitive(u8::try_from(tag).ok()?).ok() } } @@ -260,46 +117,68 @@ struct Inner { } /// This represents a JavaScript symbol primitive. -#[derive(Debug, Clone, Finalize)] pub struct JsSymbol { - inner: Rc, + repr: Tagged, } +// SAFETY: `JsSymbol` uses `Arc` to do the reference counting, making this type thread-safe. +unsafe impl Send for JsSymbol {} +// SAFETY: `JsSymbol` uses `Arc` to do the reference counting, making this type thread-safe. +unsafe impl Sync for JsSymbol {} + +impl Finalize for JsSymbol {} + // Safety: JsSymbol does not contain any objects which needs to be traced, // so this is safe. unsafe impl Trace for JsSymbol { empty_trace!(); } +macro_rules! well_known_symbols { + ( $( $(#[$attr:meta])* ($name:ident, $variant:path) ),+$(,)? ) => { + $( + $(#[$attr])* pub(crate) const fn $name() -> JsSymbol { + JsSymbol { + repr: Tagged::from_tag($variant.tag()), + } + } + )+ + }; +} + impl JsSymbol { - /// Create a new symbol. + /// Creates a new symbol. + /// + /// Returns `None` if the maximum number of possible symbols has been reached (`u64::MAX`). #[inline] #[must_use] - pub fn new(description: Option) -> Self { - let hash = SYMBOL_HASH_COUNT.with(|count| { - let hash = count.get(); - count.set(hash + 1); - hash - }); - - Self { - inner: Rc::new(Inner { hash, description }), - } - } - - /// Create a new symbol with a specified hash and description. - #[inline] - fn with_hash(hash: u64, description: Option) -> Self { - Self { - inner: Rc::new(Inner { hash, description }), - } + pub fn new(description: Option) -> Option { + let hash = get_id()?; + let arc = Arc::new(Inner { hash, description }); + + Some(Self { + // SAFETY: Pointers returned by `Arc::into_raw` must be non-null. + repr: unsafe { Tagged::from_ptr(Arc::into_raw(arc).cast_mut()) }, + }) } /// Returns the `Symbol`s description. #[inline] #[must_use] pub fn description(&self) -> Option { - self.inner.description.clone() + match self.repr.unwrap() { + UnwrappedTagged::Ptr(ptr) => { + // SAFETY: `ptr` comes from `Arc`, which ensures the validity of the pointer + // as long as we corrently call `Arc::from_raw` on `Drop`. + unsafe { ptr.as_ref().description.clone() } + } + UnwrappedTagged::Tag(tag) => { + // SAFETY: All tagged reprs always come from `WellKnown` itself, making + // this operation always safe. + let wk = unsafe { WellKnown::from_tag(tag).unwrap_unchecked() }; + Some(wk.description()) + } + } } /// Returns the `Symbol`s hash. @@ -308,7 +187,18 @@ impl JsSymbol { #[inline] #[must_use] pub fn hash(&self) -> u64 { - self.inner.hash + match self.repr.unwrap() { + UnwrappedTagged::Ptr(ptr) => { + // SAFETY: `ptr` comes from `Arc`, which ensures the validity of the pointer + // as long as we correctly call `Arc::from_raw` on `Drop`. + unsafe { ptr.as_ref().hash } + } + UnwrappedTagged::Tag(tag) => { + // SAFETY: All tagged reprs always come from `WellKnown` itself, making + // this operation always safe. + unsafe { WellKnown::from_tag(tag).unwrap_unchecked().hash() } + } + } } /// Abstract operation `SymbolDescriptiveString ( sym )` @@ -319,17 +209,82 @@ impl JsSymbol { /// [spec]: https://tc39.es/ecma262/#sec-symboldescriptivestring #[must_use] pub fn descriptive_string(&self) -> JsString { - self.inner.description.as_ref().map_or_else( + self.description().as_ref().map_or_else( || js_string!("Symbol()"), |desc| js_string!(utf16!("Symbol("), desc, utf16!(")")), ) } + + well_known_symbols! { + /// Gets the static `JsSymbol` for `"Symbol.asyncIterator"`. + (async_iterator, WellKnown::AsyncIterator), + /// Gets the static `JsSymbol` for `"Symbol.hasInstance"`. + (has_instance, WellKnown::HasInstance), + /// Gets the static `JsSymbol` for `"Symbol.isConcatSpreadable"`. + (is_concat_spreadable, WellKnown::IsConcatSpreadable), + /// Gets the static `JsSymbol` for `"Symbol.iterator"`. + (iterator, WellKnown::Iterator), + /// Gets the static `JsSymbol` for `"Symbol.match"`. + (r#match, WellKnown::Match), + /// Gets the static `JsSymbol` for `"Symbol.matchAll"`. + (match_all, WellKnown::MatchAll), + /// Gets the static `JsSymbol` for `"Symbol.replace"`. + (replace, WellKnown::Replace), + /// Gets the static `JsSymbol` for `"Symbol.search"`. + (search, WellKnown::Search), + /// Gets the static `JsSymbol` for `"Symbol.species"`. + (species, WellKnown::Species), + /// Gets the static `JsSymbol` for `"Symbol.split"`. + (split, WellKnown::Split), + /// Gets the static `JsSymbol` for `"Symbol.toPrimitive"`. + (to_primitive, WellKnown::ToPrimitive), + /// Gets the static `JsSymbol` for `"Symbol.toStringTag"`. + (to_string_tag, WellKnown::ToStringTag), + /// Gets the static `JsSymbol` for `"Symbol.unscopables"`. + (unscopables, WellKnown::Unscopables), + } +} + +impl Clone for JsSymbol { + fn clone(&self) -> Self { + if let UnwrappedTagged::Ptr(ptr) = self.repr.unwrap() { + // SAFETY: the pointer returned by `self.repr` must be a valid pointer + // that came from an `Arc::into_raw` call. + unsafe { + let arc = Arc::from_raw(ptr.as_ptr().cast_const()); + // Don't need the Arc since `self` is already a copyable pointer, just need to + // trigger the `clone` impl. + std::mem::forget(arc.clone()); + std::mem::forget(arc); + } + } + Self { repr: self.repr } + } +} + +impl Drop for JsSymbol { + fn drop(&mut self) { + if let UnwrappedTagged::Ptr(ptr) = self.repr.unwrap() { + // SAFETY: the pointer returned by `self.repr` must be a valid pointer + // that came from an `Arc::into_raw` call. + unsafe { drop(Arc::from_raw(ptr.as_ptr().cast_const())) } + } + } +} + +impl std::fmt::Debug for JsSymbol { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("JsSymbol") + .field("hash", &self.hash()) + .field("description", &self.description()) + .finish() + } } impl std::fmt::Display for JsSymbol { #[inline] fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match &self.inner.description { + match &self.description() { Some(desc) => write!(f, "Symbol({})", desc.to_std_string_escaped()), None => write!(f, "Symbol()"), } @@ -341,26 +296,26 @@ impl Eq for JsSymbol {} impl PartialEq for JsSymbol { #[inline] fn eq(&self, other: &Self) -> bool { - self.inner.hash == other.inner.hash + self.hash() == other.hash() } } impl PartialOrd for JsSymbol { #[inline] fn partial_cmp(&self, other: &Self) -> Option { - self.inner.hash.partial_cmp(&other.inner.hash) + self.hash().partial_cmp(&other.hash()) } } impl Ord for JsSymbol { #[inline] fn cmp(&self, other: &Self) -> std::cmp::Ordering { - self.inner.hash.cmp(&other.inner.hash) + self.hash().cmp(&other.hash()) } } impl Hash for JsSymbol { fn hash(&self, state: &mut H) { - self.inner.hash.hash(state); + self.hash().hash(state); } } diff --git a/boa_engine/src/tagged.rs b/boa_engine/src/tagged.rs new file mode 100644 index 0000000000..28b8af4005 --- /dev/null +++ b/boa_engine/src/tagged.rs @@ -0,0 +1,109 @@ +// Remove when/if https://github.com/rust-lang/rust/issues/95228 stabilizes. +// Right now this allows us to use the stable polyfill from the `sptr` crate, which uses +// the same names from the unstable functions of the `std::ptr` module. +#![allow(unstable_name_collisions)] + +use sptr::Strict; +use std::ptr::NonNull; + +/// A pointer that can be tagged with an `usize`. +/// +/// Only pointers with a minimum alignment of 2-bytes are valid, and the tag must have its most +/// significant bit (MSB) unset. In other words, the tag must fit inside `usize::BITS - 1` bits. +/// +/// # Representation +/// +/// If the least significant bit (LSB) of the internal [`NonNull`] is set (1), then the pointer +/// address represents a tag where the remaining bits store the tag. Otherwise, the whole pointer +/// represents the pointer itself. +/// +/// It uses [`NonNull`], which guarantees that [`Tagged`] can use the "null pointer optimization" +/// to optimize the size of [`Option`]. +/// +/// # Provenance +/// +/// This struct stores a [`NonNull`] instead of a [`NonZeroUsize`][std::num::NonZeroUsize] +/// in order to preserve the provenance of our valid heap pointers. +/// On the other hand, all index values are just casted to invalid pointers, because we don't need to +/// preserve the provenance of [`usize`] indices. +/// +/// [tagged_wp]: https://en.wikipedia.org/wiki/Tagged_pointer +#[derive(Debug)] +pub(crate) struct Tagged(NonNull); + +impl Clone for Tagged { + fn clone(&self) -> Self { + Self(self.0) + } +} + +impl Copy for Tagged {} + +impl Tagged { + /// Creates a new, tagged `Tagged` pointer from an integer. + /// + /// # Requirements + /// + /// - `T` must have an alignment of at least 2. + /// - `tag` must fit inside `usize::BITS - 1` bits + pub(crate) const fn from_tag(tag: usize) -> Tagged { + debug_assert!(std::mem::align_of::() >= 2); + let addr = (tag << 1) | 1; + // SAFETY: `addr` is never zero, since we always set its LSB to 1 + unsafe { Tagged(NonNull::new_unchecked(sptr::invalid_mut(addr))) } + } + + /// Creates a new `Tagged` pointer from a raw pointer. + /// + /// # Requirements + /// + /// - `T` must have an alignment of at least 2. + /// + /// # Safety + /// + /// - `T` must be non null. + pub(crate) const unsafe fn from_ptr(ptr: *mut T) -> Tagged { + debug_assert!(std::mem::align_of::() >= 2); + // SAFETY: the caller must ensure the invariants hold. + unsafe { Tagged(NonNull::new_unchecked(ptr)) } + } + + /// Creates a new `Tagged` pointer from a `NonNull` pointer. + /// + /// # Requirements + /// + /// - `T` must have an alignment of at least 2. + pub(crate) const fn from_non_null(ptr: NonNull) -> Tagged { + debug_assert!(std::mem::align_of::() >= 2); + Tagged(ptr) + } + + /// Unwraps the `Tagged` pointer. + pub(crate) fn unwrap(self) -> UnwrappedTagged { + let addr = self.0.as_ptr().addr(); + if addr & 1 == 0 { + UnwrappedTagged::Ptr(self.0) + } else { + UnwrappedTagged::Tag(addr >> 1) + } + } + + /// Gets the address of the inner pointer. + #[allow(unused)] + pub(crate) fn addr(self) -> usize { + self.0.as_ptr().addr() + } + + /// Returns `true` if `self ` is a tagged pointer. + #[allow(unused)] + pub(crate) fn is_tagged(self) -> bool { + self.0.as_ptr().addr() & 1 > 0 + } +} + +/// The unwrapped value of a [`Tagged`] pointer. +#[derive(Debug, Clone, Copy)] +pub(crate) enum UnwrappedTagged { + Ptr(NonNull), + Tag(usize), +} diff --git a/boa_engine/src/value/mod.rs b/boa_engine/src/value/mod.rs index 621f793c74..ff3c7bfc9a 100644 --- a/boa_engine/src/value/mod.rs +++ b/boa_engine/src/value/mod.rs @@ -24,7 +24,7 @@ use crate::{ js_string, object::{JsObject, ObjectData}, property::{PropertyDescriptor, PropertyKey}, - symbol::{JsSymbol, WellKnownSymbols}, + symbol::JsSymbol, Context, JsBigInt, JsResult, JsString, }; use boa_gc::{custom_trace, Finalize, Trace}; @@ -360,7 +360,7 @@ impl JsValue { // 2. If Type(input) is Object, then if let Some(input) = self.as_object() { // a. Let exoticToPrim be ? GetMethod(input, @@toPrimitive). - let exotic_to_prim = input.get_method(WellKnownSymbols::to_primitive(), context)?; + let exotic_to_prim = input.get_method(JsSymbol::to_primitive(), context)?; // b. If exoticToPrim is not undefined, then if let Some(exotic_to_prim) = exotic_to_prim { diff --git a/boa_engine/src/value/operations.rs b/boa_engine/src/value/operations.rs index 403c1d3e7c..597b36dc55 100644 --- a/boa_engine/src/value/operations.rs +++ b/boa_engine/src/value/operations.rs @@ -5,7 +5,7 @@ use crate::{ }, error::JsNativeError, js_string, - value::{Numeric, PreferredType, WellKnownSymbols}, + value::{JsSymbol, Numeric, PreferredType}, Context, JsBigInt, JsResult, JsValue, }; @@ -427,7 +427,7 @@ impl JsValue { } // 2. Let instOfHandler be ? GetMethod(target, @@hasInstance). - match target.get_method(WellKnownSymbols::has_instance(), context)? { + match target.get_method(JsSymbol::has_instance(), context)? { // 3. If instOfHandler is not undefined, then Some(instance_of_handler) => { // a. Return ! ToBoolean(? Call(instOfHandler, target, « V »)). diff --git a/boa_engine/src/vm/opcode/mod.rs b/boa_engine/src/vm/opcode/mod.rs index 56c3dd35f7..d8ceb8883d 100644 --- a/boa_engine/src/vm/opcode/mod.rs +++ b/boa_engine/src/vm/opcode/mod.rs @@ -1,6 +1,8 @@ /// The opcodes of the vm. use crate::{vm::ShouldExit, Context, JsResult}; +use num_enum::TryFromPrimitive; + // Operation modules mod await_stm; mod binary_ops; @@ -171,7 +173,7 @@ pub(crate) trait Operation { } generate_impl! { - #[derive(Debug, Clone, Copy)] + #[derive(Debug, Clone, Copy, TryFromPrimitive)] #[repr(u8)] pub enum Opcode { /// Pop the top value from the stack. @@ -1506,35 +1508,6 @@ generate_impl! { } } -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub struct InvalidOpcodeError { - value: u8, -} - -impl std::fmt::Display for InvalidOpcodeError { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "invalid opcode: {:#04x}", self.value) - } -} - -impl std::error::Error for InvalidOpcodeError {} - -impl TryFrom for Opcode { - type Error = InvalidOpcodeError; - - fn try_from(value: u8) -> Result { - if value > Self::Nop as u8 { - return Err(InvalidOpcodeError { value }); - } - - // Safety: we already checked if it is in the Opcode range, - // so this is safe. - let opcode = unsafe { Self::from_raw(value) }; - - Ok(opcode) - } -} - /// Specific opcodes for bindings. /// /// This separate enum exists to make matching exhaustive where needed.