diff --git a/Cargo.lock b/Cargo.lock index 96f665cea0..1447e5da80 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -112,7 +112,6 @@ dependencies = [ "dyn-clone", "fast-float", "float-cmp", - "gc", "icu_datetime", "icu_locale_canonicalizer", "icu_locid", @@ -148,14 +147,14 @@ dependencies = [ "boa_gc", "boa_interner", "boa_parser", - "gc", ] [[package]] name = "boa_gc" version = "0.16.0" dependencies = [ - "gc", + "boa_macros", + "boa_profiler", "measureme", ] @@ -177,8 +176,10 @@ dependencies = [ name = "boa_macros" version = "0.16.0" dependencies = [ + "proc-macro2", "quote", "syn", + "synstructure", ] [[package]] @@ -218,7 +219,6 @@ dependencies = [ "clap 4.0.23", "colored", "fxhash", - "gc", "once_cell", "rayon", "regex", @@ -245,9 +245,9 @@ dependencies = [ [[package]] name = "bumpalo" -version = "3.11.0" +version = "3.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c1ad822118d20d2c234f427000d5acc36eabe1e29a348c89b63dd60b13f28e5d" +checksum = "572f695136211188308f16ad2ca5c851a712c464060ae6974944458eb83880ba" [[package]] name = "byteorder" @@ -263,9 +263,9 @@ checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5" [[package]] name = "cc" -version = "1.0.73" +version = "1.0.74" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2fff2a6927b3bb87f9595d67196a70493f627687a71d87a0d692242c33f58c11" +checksum = "581f5dba903aac52ea3feb5ec4810848460ee833876f1f9b0fdeab1f19091574" [[package]] name = "cfg-if" @@ -317,9 +317,9 @@ dependencies = [ [[package]] name = "clap" -version = "3.2.22" +version = "3.2.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "86447ad904c7fb335a790c9d7fe3d0d971dc523b8ccd1561a520de9a85302750" +checksum = "71655c45cb9845d3270c9d6df84ebe72b4dad3c2ba3f7023ad47c144e4e473a5" dependencies = [ "bitflags", "clap_lex 0.2.4", @@ -421,7 +421,7 @@ dependencies = [ "atty", "cast", "ciborium", - "clap 3.2.22", + "clap 3.2.23", "criterion-plot", "itertools", "lazy_static", @@ -492,9 +492,9 @@ dependencies = [ [[package]] name = "cxx" -version = "1.0.78" +version = "1.0.80" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19f39818dcfc97d45b03953c1292efc4e80954e1583c4aa770bac1383e2310a4" +checksum = "6b7d4e43b25d3c994662706a1d4fcfc32aaa6afd287502c111b237093bb23f3a" dependencies = [ "cc", "cxxbridge-flags", @@ -504,9 +504,9 @@ dependencies = [ [[package]] name = "cxx-build" -version = "1.0.78" +version = "1.0.80" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e580d70777c116df50c390d1211993f62d40302881e54d4b79727acb83d0199" +checksum = "84f8829ddc213e2c1368e51a2564c552b65a8cb6a28f31e576270ac81d5e5827" dependencies = [ "cc", "codespan-reporting", @@ -519,15 +519,15 @@ dependencies = [ [[package]] name = "cxxbridge-flags" -version = "1.0.78" +version = "1.0.80" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56a46460b88d1cec95112c8c363f0e2c39afdb237f60583b0b36343bf627ea9c" +checksum = "e72537424b474af1460806647c41d4b6d35d09ef7fe031c5c2fa5766047cc56a" [[package]] name = "cxxbridge-macro" -version = "1.0.78" +version = "1.0.80" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "747b608fecf06b0d72d440f27acc99288207324b793be2c17991839f3d4995ea" +checksum = "309e4fb93eed90e1e14bea0da16b209f81813ba9fc7830c20ed151dd7bc0a4d7" dependencies = [ "proc-macro2", "quote", @@ -634,9 +634,9 @@ checksum = "95765f67b4b18863968b4a1bd5bb576f732b29a4a28c7cd84c09fa3e2875f33c" [[package]] name = "fd-lock" -version = "3.0.6" +version = "3.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e11dcc7e4d79a8c89b9ab4c6f5c30b1fc4a83c420792da3542fd31179ed5f517" +checksum = "0c93a581058d957dc4176875aad04f82f81613e6611d64aa1a9c755bdfb16711" dependencies = [ "cfg-if", "rustix", @@ -679,27 +679,6 @@ dependencies = [ "byteorder", ] -[[package]] -name = "gc" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3edaac0f5832202ebc99520cb77c932248010c4645d20be1dc62d6579f5b3752" -dependencies = [ - "gc_derive", -] - -[[package]] -name = "gc_derive" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "60df8444f094ff7885631d80e78eb7d88c3c2361a98daaabb06256e4500db941" -dependencies = [ - "proc-macro2", - "quote", - "syn", - "synstructure", -] - [[package]] name = "getrandom" version = "0.2.8" @@ -742,9 +721,9 @@ dependencies = [ [[package]] name = "iana-time-zone" -version = "0.1.51" +version = "0.1.53" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f5a6ef98976b22b3b7f2f3a806f858cb862044cfa66805aa3ad84cb3d3b785ed" +checksum = "64c122667b287044802d6ce17ee2ddf13207ed924c712de9a66a5814d5b64765" dependencies = [ "android_system_properties", "core-foundation-sys", @@ -756,9 +735,9 @@ dependencies = [ [[package]] name = "iana-time-zone-haiku" -version = "0.1.0" +version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fde6edd6cef363e9359ed3c98ba64590ba9eecba2293eb5a723ab32aee8926aa" +checksum = "0703ae284fc167426161c2e3f1da3ea71d94b21bedbcc9494e92b28e334e3dca" dependencies = [ "cxx", "cxx-build", @@ -914,9 +893,9 @@ dependencies = [ [[package]] name = "io-lifetimes" -version = "0.7.3" +version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ea37f355c05dde75b84bba2d767906ad522e97cd9e2eef2be7a4ab7fb442c06" +checksum = "59ce5ef949d49ee85593fc4d3f3f95ad61657076395cbbce23e2121fc5542074" [[package]] name = "itertools" @@ -971,9 +950,9 @@ checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" [[package]] name = "libc" -version = "0.2.135" +version = "0.2.137" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68783febc7782c6c5cb401fbda4de5a9898be1762314da0bb2c10ced61f18b0c" +checksum = "fc7fcc620a3bff7cdd7a365be3376c97191aeaccc2a603e600951e452615bf89" [[package]] name = "link-cplusplus" @@ -1111,9 +1090,9 @@ dependencies = [ [[package]] name = "num_cpus" -version = "1.13.1" +version = "1.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19e64526ebdee182341572e50e9ad03965aa510cd94427a4549448f285e957a1" +checksum = "f6058e64324c71e02bc2b150e4f3bc8286db6c83092132ffa3f6b1eab0f9def5" dependencies = [ "hermit-abi", "libc", @@ -1133,9 +1112,9 @@ checksum = "0ab1bc2a289d34bd04a330323ac98a1b4bc82c9d9fcb1e66b63caa84da26b575" [[package]] name = "os_str_bytes" -version = "6.3.0" +version = "6.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ff7415e9ae3fff1225851df9e0d9e4e5479f947619774677a63572e55e80eff" +checksum = "3baf96e39c5359d2eb0dd6ccb42c62b91d9678aa68160d261b9e0ccbf9e9dea9" [[package]] name = "parking_lot" @@ -1259,9 +1238,9 @@ checksum = "7c68cb38ed13fd7bc9dd5db8f165b7c8d9c1a315104083a2b10f11354c2af97f" [[package]] name = "ppv-lite86" -version = "0.2.16" +version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eb9f9e6e233e5c4a35559a617bf40a4ec447db2e84c20b55a6f83167b7e57872" +checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" [[package]] name = "proc-macro-error" @@ -1289,9 +1268,9 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.46" +version = "1.0.47" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94e2ef8dbfc347b10c094890f778ee2e36ca9bb4262e86dc99cd217e35f3470b" +checksum = "5ea3d908b0e36316caf9e9e2c4625cdde190a7e6f440d794667ed17a1855e725" dependencies = [ "unicode-ident", ] @@ -1402,9 +1381,9 @@ dependencies = [ [[package]] name = "regex-syntax" -version = "0.6.27" +version = "0.6.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a3f87b73ce11b1619a3c6332f45341e0047173771e8b8b73f87bfeefb7b56244" +checksum = "456c603be3e8d448b072f410900c09faf164fbce2d480456f50eea6e25f9c848" [[package]] name = "regress" @@ -1423,9 +1402,9 @@ checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" [[package]] name = "rustix" -version = "0.35.11" +version = "0.35.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fbb2fda4666def1433b1b05431ab402e42a1084285477222b72d6c564c417cef" +checksum = "727a1a6d65f786ec22df8a81ca3121107f235970dc1705ed681d3e6e8b9cd5f9" dependencies = [ "bitflags", "errno", @@ -1644,9 +1623,9 @@ dependencies = [ [[package]] name = "textwrap" -version = "0.15.1" +version = "0.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "949517c0cf1bf4ee812e2e07e08ab448e3ae0d23472aee8a06c985f0c8815b16" +checksum = "222a222a5bfe1bba4a77b45ec488a741b3cb8872e5e499451fd7d0129c9c7c3d" [[package]] name = "thiserror" @@ -1893,46 +1872,60 @@ checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" [[package]] name = "windows-sys" -version = "0.36.1" +version = "0.42.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea04155a16a59f9eab786fe12a4a450e75cdb175f9e0d80da1e17db09f55b8d2" +checksum = "5a3e1820f08b8513f676f7ab6c1f99ff312fb97b553d30ff4dd86f9f15728aa7" dependencies = [ + "windows_aarch64_gnullvm", "windows_aarch64_msvc", "windows_i686_gnu", "windows_i686_msvc", "windows_x86_64_gnu", + "windows_x86_64_gnullvm", "windows_x86_64_msvc", ] +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.42.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d2aa71f6f0cbe00ae5167d90ef3cfe66527d6f613ca78ac8024c3ccab9a19e" + [[package]] name = "windows_aarch64_msvc" -version = "0.36.1" +version = "0.42.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9bb8c3fd39ade2d67e9874ac4f3db21f0d710bee00fe7cab16949ec184eeaa47" +checksum = "dd0f252f5a35cac83d6311b2e795981f5ee6e67eb1f9a7f64eb4500fbc4dcdb4" [[package]] name = "windows_i686_gnu" -version = "0.36.1" +version = "0.42.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "180e6ccf01daf4c426b846dfc66db1fc518f074baa793aa7d9b9aaeffad6a3b6" +checksum = "fbeae19f6716841636c28d695375df17562ca208b2b7d0dc47635a50ae6c5de7" [[package]] name = "windows_i686_msvc" -version = "0.36.1" +version = "0.42.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2e7917148b2812d1eeafaeb22a97e4813dfa60a3f8f78ebe204bcc88f12f024" +checksum = "84c12f65daa39dd2babe6e442988fc329d6243fdce47d7d2d155b8d874862246" [[package]] name = "windows_x86_64_gnu" -version = "0.36.1" +version = "0.42.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf7b1b21b5362cbc318f686150e5bcea75ecedc74dd157d874d754a2ca44b0ed" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.42.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4dcd171b8776c41b97521e5da127a2d86ad280114807d0b2ab1e462bc764d9e1" +checksum = "09d525d2ba30eeb3297665bd434a54297e4170c7f1a44cad4ef58095b4cd2028" [[package]] name = "windows_x86_64_msvc" -version = "0.36.1" +version = "0.42.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c811ca4a8c853ef420abd8592ba53ddbbac90410fab6903b3e79972a631f7680" +checksum = "f40009d85759725a34da6d89a94e63d7bdc50a862acf0dbc7c8e488f1edcb6f5" [[package]] name = "writeable" @@ -1975,9 +1968,9 @@ dependencies = [ [[package]] name = "zerofrom-derive" -version = "0.1.0" +version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8785f47d6062c1932866147f91297286a9f350b3070e9d9f0b6078e37d623c1a" +checksum = "2e8aa86add9ddbd2409c1ed01e033cd457d79b1b1229b64922c25095c595e829" dependencies = [ "proc-macro2", "quote", diff --git a/boa_engine/Cargo.toml b/boa_engine/Cargo.toml index 2d6fd774ee..579e3e8394 100644 --- a/boa_engine/Cargo.toml +++ b/boa_engine/Cargo.toml @@ -37,7 +37,6 @@ boa_profiler.workspace = true boa_macros.workspace = true boa_ast.workspace = true boa_parser.workspace = true -gc = "0.4.1" serde = { version = "1.0.147", features = ["derive", "rc"] } serde_json = "1.0.87" rand = "0.8.5" diff --git a/boa_engine/src/builtins/async_generator/mod.rs b/boa_engine/src/builtins/async_generator/mod.rs index e6ea718064..768dfd48a6 100644 --- a/boa_engine/src/builtins/async_generator/mod.rs +++ b/boa_engine/src/builtins/async_generator/mod.rs @@ -18,7 +18,7 @@ use crate::{ vm::GeneratorResumeKind, Context, JsError, JsResult, }; -use boa_gc::{Cell, Finalize, Gc, Trace}; +use boa_gc::{Finalize, Gc, GcCell, Trace}; use boa_profiler::Profiler; use std::collections::VecDeque; @@ -56,7 +56,7 @@ pub struct AsyncGenerator { pub(crate) state: AsyncGeneratorState, /// The `[[AsyncGeneratorContext]]` internal slot. - pub(crate) context: Option>>, + pub(crate) context: Option>>, /// The `[[AsyncGeneratorQueue]]` internal slot. pub(crate) queue: VecDeque, @@ -511,7 +511,7 @@ impl AsyncGenerator { pub(crate) fn resume( generator: &JsObject, state: AsyncGeneratorState, - generator_context: &Gc>, + generator_context: &Gc>, completion: (JsResult, bool), context: &mut Context, ) { diff --git a/boa_engine/src/builtins/function/mod.rs b/boa_engine/src/builtins/function/mod.rs index 5cc70deec5..ebd927fd79 100644 --- a/boa_engine/src/builtins/function/mod.rs +++ b/boa_engine/src/builtins/function/mod.rs @@ -34,7 +34,7 @@ use boa_ast::{ operations::{bound_names, contains, lexically_declared_names, ContainsSymbol}, StatementList, }; -use boa_gc::{self, custom_trace, Finalize, Gc, Trace}; +use boa_gc::{self, custom_trace, Finalize, Gc, GcCell, Trace}; use boa_interner::Sym; use boa_parser::Parser; use boa_profiler::Profiler; @@ -178,7 +178,7 @@ unsafe impl Trace for ClassFieldDefinition { /// with `Any::downcast_ref` and `Any::downcast_mut` to recover the original /// type. #[derive(Clone, Debug, Trace, Finalize)] -pub struct Captures(Gc>>); +pub struct Captures(Gc>>); impl Captures { /// Creates a new capture context. @@ -186,7 +186,7 @@ impl Captures { where T: NativeObject, { - Self(Gc::new(boa_gc::Cell::new(Box::new(captures)))) + Self(Gc::new(GcCell::new(Box::new(captures)))) } /// Casts `Captures` to `Any` @@ -194,7 +194,7 @@ impl Captures { /// # Panics /// /// Panics if it's already borrowed as `&mut Any` - pub fn as_any(&self) -> boa_gc::Ref<'_, dyn Any> { + pub fn as_any(&self) -> boa_gc::GcCellRef<'_, dyn Any> { Ref::map(self.0.borrow(), |data| data.deref().as_any()) } @@ -203,7 +203,7 @@ impl Captures { /// # Panics /// /// Panics if it's already borrowed as `&mut Any` - pub fn as_mut_any(&self) -> boa_gc::RefMut<'_, Box, dyn Any> { + pub fn as_mut_any(&self) -> boa_gc::GcCellRefMut<'_, Box, dyn Any> { RefMut::map(self.0.borrow_mut(), |data| data.deref_mut().as_mut_any()) } } diff --git a/boa_engine/src/builtins/generator/mod.rs b/boa_engine/src/builtins/generator/mod.rs index 74eea06fc1..7599dd9074 100644 --- a/boa_engine/src/builtins/generator/mod.rs +++ b/boa_engine/src/builtins/generator/mod.rs @@ -20,7 +20,7 @@ use crate::{ vm::{CallFrame, GeneratorResumeKind, ReturnType}, Context, JsError, JsResult, }; -use boa_gc::{Cell, Finalize, Gc, Trace}; +use boa_gc::{Finalize, Gc, GcCell, Trace}; use boa_profiler::Profiler; /// Indicates the state of a generator. @@ -52,7 +52,7 @@ pub struct Generator { pub(crate) state: GeneratorState, /// The `[[GeneratorContext]]` internal slot. - pub(crate) context: Option>>, + pub(crate) context: Option>>, } impl BuiltIn for Generator { diff --git a/boa_engine/src/builtins/promise/mod.rs b/boa_engine/src/builtins/promise/mod.rs index ae4ebef81e..7bdf64cbc7 100644 --- a/boa_engine/src/builtins/promise/mod.rs +++ b/boa_engine/src/builtins/promise/mod.rs @@ -21,7 +21,7 @@ use crate::{ value::JsValue, Context, JsError, JsResult, }; -use boa_gc::{Cell as GcCell, Finalize, Gc, Trace}; +use boa_gc::{Finalize, Gc, GcCell, Trace}; use boa_profiler::Profiler; use std::{cell::Cell, rc::Rc}; use tap::{Conv, Pipe}; @@ -118,7 +118,7 @@ impl PromiseCapability { // 2. NOTE: C is assumed to be a constructor function that supports the parameter conventions of the Promise constructor (see 27.2.3.1). // 3. Let promiseCapability be the PromiseCapability Record { [[Promise]]: undefined, [[Resolve]]: undefined, [[Reject]]: undefined }. - let promise_capability = Gc::new(boa_gc::Cell::new(RejectResolve { + let promise_capability = Gc::new(GcCell::new(RejectResolve { reject: JsValue::undefined(), resolve: JsValue::undefined(), })); diff --git a/boa_engine/src/bytecompiler/mod.rs b/boa_engine/src/bytecompiler/mod.rs index 33dec83bbc..8bff029b4e 100644 --- a/boa_engine/src/bytecompiler/mod.rs +++ b/boa_engine/src/bytecompiler/mod.rs @@ -30,7 +30,7 @@ use boa_ast::{ }, Declaration, Expression, Statement, StatementList, StatementListItem, }; -use boa_gc::Gc; +use boa_gc::{Gc, GcCell}; use boa_interner::{Interner, Sym}; use rustc_hash::FxHashMap; use std::mem::size_of; @@ -265,7 +265,7 @@ impl<'b> ByteCompiler<'b> { #[inline] fn push_compile_environment( &mut self, - environment: Gc>, + environment: Gc>, ) -> usize { let index = self.code_block.compile_environments.len(); self.code_block.compile_environments.push(environment); diff --git a/boa_engine/src/environments/compile.rs b/boa_engine/src/environments/compile.rs index 37aa9a64ec..1ff6f3faf7 100644 --- a/boa_engine/src/environments/compile.rs +++ b/boa_engine/src/environments/compile.rs @@ -2,7 +2,7 @@ use crate::{ environments::runtime::BindingLocator, property::PropertyDescriptor, Context, JsString, JsValue, }; use boa_ast::expression::Identifier; -use boa_gc::{Cell, Finalize, Gc, Trace}; +use boa_gc::{Finalize, Gc, GcCell, Trace}; use rustc_hash::FxHashMap; @@ -22,7 +22,7 @@ struct CompileTimeBinding { /// A compile time environment also indicates, if it is a function environment. #[derive(Debug, Finalize, Trace)] pub(crate) struct CompileTimeEnvironment { - outer: Option>>, + outer: Option>>, environment_index: usize, #[unsafe_ignore_trace] bindings: FxHashMap, @@ -223,7 +223,7 @@ impl Context { let environment_index = self.realm.compile_env.borrow().environment_index + 1; let outer = self.realm.compile_env.clone(); - self.realm.compile_env = Gc::new(Cell::new(CompileTimeEnvironment { + self.realm.compile_env = Gc::new(GcCell::new(CompileTimeEnvironment { outer: Some(outer), environment_index, bindings: FxHashMap::default(), @@ -241,7 +241,7 @@ impl Context { #[inline] pub(crate) fn pop_compile_time_environment( &mut self, - ) -> (usize, Gc>) { + ) -> (usize, Gc>) { let current_env_borrow = self.realm.compile_env.borrow(); if let Some(outer) = ¤t_env_borrow.outer { let outer_clone = outer.clone(); diff --git a/boa_engine/src/environments/runtime.rs b/boa_engine/src/environments/runtime.rs index 3314eb75e5..c06ab96572 100644 --- a/boa_engine/src/environments/runtime.rs +++ b/boa_engine/src/environments/runtime.rs @@ -3,7 +3,7 @@ use std::cell::Cell; use crate::{ environments::CompileTimeEnvironment, error::JsNativeError, object::JsObject, Context, JsValue, }; -use boa_gc::{Cell as GcCell, Finalize, Gc, Trace}; +use boa_gc::{Finalize, Gc, GcCell, Trace}; use boa_ast::expression::Identifier; use rustc_hash::FxHashSet; diff --git a/boa_engine/src/job.rs b/boa_engine/src/job.rs index 2a0988b2e5..f24be132a3 100644 --- a/boa_engine/src/job.rs +++ b/boa_engine/src/job.rs @@ -1,5 +1,5 @@ use crate::{prelude::JsObject, Context, JsResult, JsValue}; -use gc::{Finalize, Trace}; +use boa_gc::{Finalize, Trace}; /// `JobCallback` records /// diff --git a/boa_engine/src/object/jsobject.rs b/boa_engine/src/object/jsobject.rs index d1afe8f99a..6bcf077f9b 100644 --- a/boa_engine/src/object/jsobject.rs +++ b/boa_engine/src/object/jsobject.rs @@ -10,7 +10,7 @@ use crate::{ value::PreferredType, Context, JsResult, JsValue, }; -use boa_gc::{self, Finalize, Gc, Trace}; +use boa_gc::{self, Finalize, Gc, GcCell, Trace}; use rustc_hash::FxHashMap; use std::{ cell::RefCell, @@ -21,15 +21,15 @@ use std::{ }; /// A wrapper type for an immutably borrowed type T. -pub type Ref<'a, T> = boa_gc::Ref<'a, T>; +pub type Ref<'a, T> = boa_gc::GcCellRef<'a, T>; /// A wrapper type for a mutably borrowed type T. -pub type RefMut<'a, T, U> = boa_gc::RefMut<'a, T, U>; +pub type RefMut<'a, T, U> = boa_gc::GcCellRefMut<'a, T, U>; /// Garbage collected `Object`. #[derive(Trace, Finalize, Clone, Default)] pub struct JsObject { - inner: Gc>, + inner: Gc>, } impl JsObject { @@ -37,7 +37,7 @@ impl JsObject { #[inline] fn from_object(object: Object) -> Self { Self { - inner: Gc::new(boa_gc::Cell::new(object)), + inner: Gc::new(GcCell::new(object)), } } @@ -738,9 +738,9 @@ Cannot both specify accessors and a value or writable attribute", } } -impl AsRef> for JsObject { +impl AsRef> for JsObject { #[inline] - fn as_ref(&self) -> &boa_gc::Cell { + fn as_ref(&self) -> &GcCell { &self.inner } } diff --git a/boa_engine/src/realm.rs b/boa_engine/src/realm.rs index 588f01fd16..ab8cf8dfbd 100644 --- a/boa_engine/src/realm.rs +++ b/boa_engine/src/realm.rs @@ -8,7 +8,7 @@ use crate::{ environments::{CompileTimeEnvironment, DeclarativeEnvironmentStack}, object::{GlobalPropertyMap, JsObject, JsPrototype, ObjectData, PropertyMap}, }; -use boa_gc::{Cell, Gc}; +use boa_gc::{Gc, GcCell}; use boa_profiler::Profiler; /// Representation of a Realm. @@ -21,7 +21,7 @@ pub struct Realm { pub(crate) global_property_map: PropertyMap, pub(crate) global_prototype: JsPrototype, pub(crate) environments: DeclarativeEnvironmentStack, - pub(crate) compile_env: Gc>, + pub(crate) compile_env: Gc>, } impl Realm { @@ -33,7 +33,7 @@ impl Realm { // Allow identification of the global object easily let global_object = JsObject::from_proto_and_data(None, ObjectData::global()); - let global_compile_environment = Gc::new(Cell::new(CompileTimeEnvironment::new_global())); + let global_compile_environment = Gc::new(GcCell::new(CompileTimeEnvironment::new_global())); Self { global_object, diff --git a/boa_engine/src/string/mod.rs b/boa_engine/src/string/mod.rs index 2b6ea514eb..2932e76b9b 100644 --- a/boa_engine/src/string/mod.rs +++ b/boa_engine/src/string/mod.rs @@ -24,7 +24,7 @@ mod common; use crate::{builtins::string::is_trimmable_whitespace, JsBigInt}; -use boa_gc::{unsafe_empty_trace, Finalize, Trace}; +use boa_gc::{empty_trace, Finalize, Trace}; pub use boa_macros::utf16; use std::{ @@ -292,7 +292,7 @@ sa::assert_eq_size!(JsString, *const ()); // Safety: `JsString` does not contain any objects which needs to be traced, so this is safe. unsafe impl Trace for JsString { - unsafe_empty_trace!(); + empty_trace!(); } impl JsString { diff --git a/boa_engine/src/symbol.rs b/boa_engine/src/symbol.rs index f7f4448829..c0b69c841f 100644 --- a/boa_engine/src/symbol.rs +++ b/boa_engine/src/symbol.rs @@ -16,7 +16,7 @@ //! [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Symbol use crate::{js_string, string::utf16, JsString}; -use boa_gc::{unsafe_empty_trace, Finalize, Trace}; +use boa_gc::{empty_trace, Finalize, Trace}; use std::{ cell::Cell, hash::{Hash, Hasher}, @@ -255,7 +255,7 @@ pub struct JsSymbol { // Safety: JsSymbol does not contain any objects which needs to be traced, // so this is safe. unsafe impl Trace for JsSymbol { - unsafe_empty_trace!(); + empty_trace!(); } impl JsSymbol { diff --git a/boa_engine/src/vm/code_block.rs b/boa_engine/src/vm/code_block.rs index a8444e2072..02f52e097f 100644 --- a/boa_engine/src/vm/code_block.rs +++ b/boa_engine/src/vm/code_block.rs @@ -24,7 +24,7 @@ use crate::{ Context, JsResult, JsString, JsValue, }; use boa_ast::{expression::Identifier, function::FormalParameterList}; -use boa_gc::{Cell, Finalize, Gc, Trace}; +use boa_gc::{Finalize, Gc, GcCell, Trace}; use boa_interner::{Interner, Sym, ToInternedString}; use boa_profiler::Profiler; use std::{collections::VecDeque, convert::TryInto, mem::size_of}; @@ -103,7 +103,7 @@ pub struct CodeBlock { pub(crate) arguments_binding: Option, /// Compile time environments in this function. - pub(crate) compile_environments: Vec>>, + pub(crate) compile_environments: Vec>>, /// The `[[IsClassConstructor]]` internal slot. pub(crate) is_class_constructor: bool, @@ -1099,7 +1099,7 @@ impl JsObject { prototype, ObjectData::generator(Generator { state: GeneratorState::SuspendedStart, - context: Some(Gc::new(Cell::new(GeneratorContext { + context: Some(Gc::new(GcCell::new(GeneratorContext { environments, call_frame, stack, @@ -1242,7 +1242,7 @@ impl JsObject { prototype, ObjectData::async_generator(AsyncGenerator { state: AsyncGeneratorState::SuspendedStart, - context: Some(Gc::new(Cell::new(GeneratorContext { + context: Some(Gc::new(GcCell::new(GeneratorContext { environments, call_frame, stack, diff --git a/boa_examples/Cargo.toml b/boa_examples/Cargo.toml index 28b1be4975..b6135a7dc8 100644 --- a/boa_examples/Cargo.toml +++ b/boa_examples/Cargo.toml @@ -17,4 +17,3 @@ boa_ast.workspace = true boa_interner.workspace = true boa_gc.workspace = true boa_parser.workspace = true -gc = "0.4.1" diff --git a/boa_gc/Cargo.toml b/boa_gc/Cargo.toml index 5cca97b53b..ceaa195cdc 100644 --- a/boa_gc/Cargo.toml +++ b/boa_gc/Cargo.toml @@ -11,7 +11,8 @@ repository.workspace = true rust-version.workspace = true [dependencies] -gc = { version = "0.4.1", features = ["derive"] } +boa_profiler.workspace = true +boa_macros.workspace = true # Optional Dependencies measureme = { version = "10.1.0", optional = true } diff --git a/boa_gc/src/cell.rs b/boa_gc/src/cell.rs new file mode 100644 index 0000000000..3660762934 --- /dev/null +++ b/boa_gc/src/cell.rs @@ -0,0 +1,594 @@ +//! A garbage collected cell implementation +use std::cell::{Cell, UnsafeCell}; +use std::cmp::Ordering; +use std::fmt::{self, Debug, Display}; +use std::hash::Hash; +use std::ops::{Deref, DerefMut}; + +use crate::trace::{Finalize, Trace}; + +/// `BorrowFlag` represent the internal state of a `GcCell` and +/// keeps track of the amount of current borrows. +#[derive(Copy, Clone)] +pub(crate) struct BorrowFlag(usize); + +/// `BorrowState` represents the various states of a `BorrowFlag` +/// +/// - Reading: the value is currently being read/borrowed. +/// - Writing: the value is currently being written/borrowed mutably. +/// - Unused: the value is currently unrooted. +#[derive(Copy, Clone, Debug, Eq, PartialEq)] +pub(crate) enum BorrowState { + Reading, + Writing, + Unused, +} + +const ROOT: usize = 1; +const WRITING: usize = !1; +const UNUSED: usize = 0; + +/// The base borrowflag init is rooted, and has no outstanding borrows. +pub(crate) const BORROWFLAG_INIT: BorrowFlag = BorrowFlag(ROOT); + +impl BorrowFlag { + /// Check the current `BorrowState` of `BorrowFlag`. + #[inline] + pub(crate) fn borrowed(self) -> BorrowState { + match self.0 & !ROOT { + UNUSED => BorrowState::Unused, + WRITING => BorrowState::Writing, + _ => BorrowState::Reading, + } + } + + /// Check whether the borrow bit is flagged. + #[inline] + pub(crate) fn rooted(self) -> bool { + self.0 & ROOT > 0 + } + + /// Set the `BorrowFlag`'s state to writing. + #[inline] + pub(crate) fn set_writing(self) -> Self { + // Set every bit other than the root bit, which is preserved + Self(self.0 | WRITING) + } + + /// Remove the root flag on `BorrowFlag` + #[inline] + pub(crate) fn set_unused(self) -> Self { + // Clear every bit other than the root bit, which is preserved + Self(self.0 & ROOT) + } + + /// Increments the counter for a new borrow. + /// + /// # Panic + /// - This method will panic if the current `BorrowState` is writing. + /// - This method will panic after incrementing if the borrow count overflows. + #[inline] + pub(crate) fn add_reading(self) -> Self { + assert!(self.borrowed() != BorrowState::Writing); + // Add 1 to the integer starting at the second binary digit. As our + // borrowstate is not writing, we know that overflow cannot happen, so + // this is equivalent to the following, more complicated, expression: + // + // BorrowFlag((self.0 & ROOT) | (((self.0 >> 1) + 1) << 1)) + let flags = Self(self.0 + 0b10); + + // This will fail if the borrow count overflows, which shouldn't happen, + // but let's be safe + { + assert!(flags.borrowed() == BorrowState::Reading); + } + flags + } + + /// Decrements the counter to remove a borrow. + /// + /// # Panic + /// - This method will panic if the current `BorrowState` is not reading. + #[inline] + pub(crate) fn sub_reading(self) -> Self { + assert!(self.borrowed() == BorrowState::Reading); + // Subtract 1 from the integer starting at the second binary digit. As + // our borrowstate is not writing or unused, we know that overflow or + // undeflow cannot happen, so this is equivalent to the following, more + // complicated, expression: + // + // BorrowFlag((self.0 & ROOT) | (((self.0 >> 1) - 1) << 1)) + Self(self.0 - 0b10) + } + + /// Set the root flag on the `BorrowFlag`. + #[inline] + pub(crate) fn set_rooted(self, rooted: bool) -> Self { + // Preserve the non-root bits + Self((self.0 & !ROOT) | (usize::from(rooted))) + } +} + +impl Debug for BorrowFlag { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("BorrowFlag") + .field("Rooted", &self.rooted()) + .field("State", &self.borrowed()) + .finish() + } +} + +/// A mutable memory location with dynamically checked borrow rules +/// that can be used inside of a garbage-collected pointer. +/// +/// This object is a `RefCell` that can be used inside of a `Gc`. +pub struct GcCell { + pub(crate) flags: Cell, + pub(crate) cell: UnsafeCell, +} + +impl GcCell { + /// Creates a new `GcCell` containing `value`. + #[inline] + pub fn new(value: T) -> Self { + Self { + flags: Cell::new(BORROWFLAG_INIT), + cell: UnsafeCell::new(value), + } + } + + /// Consumes the `GcCell`, returning the wrapped value. + #[inline] + pub fn into_inner(self) -> T { + self.cell.into_inner() + } +} + +impl GcCell { + /// Immutably borrows the wrapped value. + /// + /// The borrow lasts until the returned `GcCellRef` exits scope. + /// Multiple immutable borrows can be taken out at the same time. + /// + /// # Panics + /// + /// Panics if the value is currently mutably borrowed. + #[inline] + pub fn borrow(&self) -> GcCellRef<'_, T> { + match self.try_borrow() { + Ok(value) => value, + Err(e) => panic!("{}", e), + } + } + + /// Mutably borrows the wrapped value. + /// + /// The borrow lasts until the returned `GcCellRefMut` exits scope. + /// The value cannot be borrowed while this borrow is active. + /// + /// # Panics + /// + /// Panics if the value is currently borrowed. + #[inline] + pub fn borrow_mut(&self) -> GcCellRefMut<'_, T> { + match self.try_borrow_mut() { + Ok(value) => value, + Err(e) => panic!("{}", e), + } + } + + /// Immutably borrows the wrapped value, returning an error if the value is currently mutably + /// borrowed. + /// + /// The borrow lasts until the returned `GcCellRef` exits scope. Multiple immutable borrows can be + /// taken out at the same time. + /// + /// This is the non-panicking variant of [`borrow`](#method.borrow). + /// + /// # Errors + /// + /// Returns an `Err` if the value is currently mutably borrowed. + pub fn try_borrow(&self) -> Result, BorrowError> { + if self.flags.get().borrowed() == BorrowState::Writing { + return Err(BorrowError); + } + self.flags.set(self.flags.get().add_reading()); + + // SAFETY: calling value on a rooted value may cause Undefined Behavior + unsafe { + Ok(GcCellRef { + flags: &self.flags, + value: &*self.cell.get(), + }) + } + } + + /// Mutably borrows the wrapped value, returning an error if the value is currently borrowed. + /// + /// The borrow lasts until the returned `GcCellRefMut` exits scope. + /// The value cannot be borrowed while this borrow is active. + /// + /// This is the non-panicking variant of [`borrow_mut`](#method.borrow_mut). + /// + /// # Errors + /// + /// Returns an `Err` if the value is currently borrowed. + pub fn try_borrow_mut(&self) -> Result, BorrowMutError> { + if self.flags.get().borrowed() != BorrowState::Unused { + return Err(BorrowMutError); + } + self.flags.set(self.flags.get().set_writing()); + + // SAFETY: This is safe as the value is rooted if it was not previously rooted, + // so it cannot be dropped. + unsafe { + // Force the val_ref's contents to be rooted for the duration of the + // mutable borrow + if !self.flags.get().rooted() { + (*self.cell.get()).root(); + } + + Ok(GcCellRefMut { + gc_cell: self, + value: &mut *self.cell.get(), + }) + } + } +} + +/// An error returned by [`GcCell::try_borrow`](struct.GcCell.html#method.try_borrow). +#[derive(Debug, Clone, Copy, Eq, PartialEq, Ord, PartialOrd, Default, Hash)] +pub struct BorrowError; + +impl Display for BorrowError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + Display::fmt("GcCell already mutably borrowed", f) + } +} + +/// An error returned by [`GcCell::try_borrow_mut`](struct.GcCell.html#method.try_borrow_mut). +#[derive(Debug, Clone, Copy, Eq, PartialEq, Ord, PartialOrd, Default, Hash)] +pub struct BorrowMutError; + +impl Display for BorrowMutError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + Display::fmt("GcCell already borrowed", f) + } +} + +impl Finalize for GcCell {} + +// SAFETY: GcCell maintains it's own BorrowState and rootedness. GcCell's implementation +// focuses on only continuing Trace based methods while the cell state is not written. +// Implementing a Trace while the cell is being written to or incorrectly implementing Trace +// on GcCell's value may cause Undefined Behavior +unsafe impl Trace for GcCell { + #[inline] + unsafe fn trace(&self) { + match self.flags.get().borrowed() { + BorrowState::Writing => (), + // SAFETY: Please see GcCell's Trace impl Safety note. + _ => unsafe { (*self.cell.get()).trace() }, + } + } + + #[inline] + unsafe fn weak_trace(&self) { + match self.flags.get().borrowed() { + BorrowState::Writing => (), + // SAFETY: Please see GcCell's Trace impl Safety note. + _ => unsafe { (*self.cell.get()).weak_trace() }, + } + } + + unsafe fn root(&self) { + assert!(!self.flags.get().rooted(), "Can't root a GcCell twice!"); + self.flags.set(self.flags.get().set_rooted(true)); + + match self.flags.get().borrowed() { + BorrowState::Writing => (), + // SAFETY: Please see GcCell's Trace impl Safety note. + _ => unsafe { (*self.cell.get()).root() }, + } + } + + #[inline] + unsafe fn unroot(&self) { + assert!(self.flags.get().rooted(), "Can't unroot a GcCell twice!"); + self.flags.set(self.flags.get().set_rooted(false)); + + match self.flags.get().borrowed() { + BorrowState::Writing => (), + // SAFETY: Please see GcCell's Trace impl Safety note. + _ => unsafe { (*self.cell.get()).unroot() }, + } + } + + #[inline] + fn run_finalizer(&self) { + Finalize::finalize(self); + match self.flags.get().borrowed() { + BorrowState::Writing => (), + // SAFETY: Please see GcCell's Trace impl Safety note. + _ => unsafe { (*self.cell.get()).run_finalizer() }, + } + } +} + +/// A wrapper type for an immutably borrowed value from a `GcCell`. +pub struct GcCellRef<'a, T: ?Sized + 'static> { + pub(crate) flags: &'a Cell, + pub(crate) value: &'a T, +} + +impl<'a, T: ?Sized> GcCellRef<'a, T> { + /// Copies a `GcCellRef`. + /// + /// The `GcCell` is already immutably borrowed, so this cannot fail. + /// + /// This is an associated function that needs to be used as + /// `GcCellRef::clone(...)`. A `Clone` implementation or a method + /// would interfere with the use of `c.borrow().clone()` to clone + /// the contents of a `GcCell`. + #[inline] + #[allow(clippy::should_implement_trait)] + #[must_use] + pub fn clone(orig: &GcCellRef<'a, T>) -> GcCellRef<'a, T> { + orig.flags.set(orig.flags.get().add_reading()); + GcCellRef { + flags: orig.flags, + value: orig.value, + } + } + + /// Makes a new `GcCellRef` from a component of the borrowed data. + /// + /// The `GcCell` is already immutably borrowed, so this cannot fail. + /// + /// This is an associated function that needs to be used as `GcCellRef::map(...)`. + /// A method would interfere with methods of the same name on the contents + /// of a `GcCellRef` used through `Deref`. + #[inline] + pub fn map(orig: Self, f: F) -> GcCellRef<'a, U> + where + U: ?Sized, + F: FnOnce(&T) -> &U, + { + let ret = GcCellRef { + flags: orig.flags, + value: f(orig.value), + }; + + // We have to tell the compiler not to call the destructor of GcCellRef, + // because it will update the borrow flags. + std::mem::forget(orig); + + ret + } + + /// Splits a `GcCellRef` into multiple `GcCellRef`s for different components of the borrowed data. + /// + /// The `GcCell` is already immutably borrowed, so this cannot fail. + /// + /// This is an associated function that needs to be used as `GcCellRef::map_split(...)`. + /// A method would interfere with methods of the same name on the contents of a `GcCellRef` used through `Deref`. + #[inline] + pub fn map_split(orig: Self, f: F) -> (GcCellRef<'a, U>, GcCellRef<'a, V>) + where + U: ?Sized, + V: ?Sized, + F: FnOnce(&T) -> (&U, &V), + { + let (a, b) = f(orig.value); + + orig.flags.set(orig.flags.get().add_reading()); + + let ret = ( + GcCellRef { + flags: orig.flags, + value: a, + }, + GcCellRef { + flags: orig.flags, + value: b, + }, + ); + + // We have to tell the compiler not to call the destructor of GcCellRef, + // because it will update the borrow flags. + std::mem::forget(orig); + + ret + } +} + +impl<'a, T: ?Sized> Deref for GcCellRef<'a, T> { + type Target = T; + + #[inline] + fn deref(&self) -> &T { + self.value + } +} + +impl<'a, T: ?Sized> Drop for GcCellRef<'a, T> { + fn drop(&mut self) { + debug_assert!(self.flags.get().borrowed() == BorrowState::Reading); + self.flags.set(self.flags.get().sub_reading()); + } +} + +impl<'a, T: ?Sized + Debug> Debug for GcCellRef<'a, T> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + Debug::fmt(&**self, f) + } +} + +impl<'a, T: ?Sized + Display> Display for GcCellRef<'a, T> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + Display::fmt(&**self, f) + } +} + +/// A wrapper type for a mutably borrowed value from a `GcCell`. +pub struct GcCellRefMut<'a, T: Trace + ?Sized + 'static, U: ?Sized = T> { + pub(crate) gc_cell: &'a GcCell, + pub(crate) value: &'a mut U, +} + +impl<'a, T: Trace + ?Sized, U: ?Sized> GcCellRefMut<'a, T, U> { + /// Makes a new `GcCellRefMut` for a component of the borrowed data, e.g., an enum + /// variant. + /// + /// The `GcCellRefMut` is already mutably borrowed, so this cannot fail. + /// + /// This is an associated function that needs to be used as + /// `GcCellRefMut::map(...)`. A method would interfere with methods of the same + /// name on the contents of a `GcCell` used through `Deref`. + #[inline] + pub fn map(orig: Self, f: F) -> GcCellRefMut<'a, T, V> + where + V: ?Sized, + F: FnOnce(&mut U) -> &mut V, + { + // SAFETY: This is safe as `GcCellRefMut` is already borrowed, so the value is rooted. + let value = unsafe { &mut *(orig.value as *mut U) }; + + let ret = GcCellRefMut { + gc_cell: orig.gc_cell, + value: f(value), + }; + + // We have to tell the compiler not to call the destructor of GcCellRefMut, + // because it will update the borrow flags. + std::mem::forget(orig); + + ret + } +} + +impl<'a, T: Trace + ?Sized, U: ?Sized> Deref for GcCellRefMut<'a, T, U> { + type Target = U; + + #[inline] + fn deref(&self) -> &U { + self.value + } +} + +impl<'a, T: Trace + ?Sized, U: ?Sized> DerefMut for GcCellRefMut<'a, T, U> { + #[inline] + fn deref_mut(&mut self) -> &mut U { + self.value + } +} + +impl<'a, T: Trace + ?Sized, U: ?Sized> Drop for GcCellRefMut<'a, T, U> { + #[inline] + fn drop(&mut self) { + debug_assert!(self.gc_cell.flags.get().borrowed() == BorrowState::Writing); + // Restore the rooted state of the GcCell's contents to the state of the GcCell. + // During the lifetime of the GcCellRefMut, the GcCell's contents are rooted. + if !self.gc_cell.flags.get().rooted() { + // SAFETY: If `GcCell` is no longer rooted, then unroot it. This should be safe + // as the internal `GcBox` should be guaranteed to have at least 1 root. + unsafe { + (*self.gc_cell.cell.get()).unroot(); + } + } + self.gc_cell + .flags + .set(self.gc_cell.flags.get().set_unused()); + } +} + +impl<'a, T: Trace + ?Sized, U: Debug + ?Sized> Debug for GcCellRefMut<'a, T, U> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + Debug::fmt(&**self, f) + } +} + +impl<'a, T: Trace + ?Sized, U: Display + ?Sized> Display for GcCellRefMut<'a, T, U> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + Display::fmt(&**self, f) + } +} + +// SAFETY: GcCell tracks it's `BorrowState` is `Writing` +unsafe impl Send for GcCell {} + +impl Clone for GcCell { + #[inline] + fn clone(&self) -> Self { + Self::new(self.borrow().clone()) + } +} + +impl Default for GcCell { + #[inline] + fn default() -> Self { + Self::new(Default::default()) + } +} + +#[allow(clippy::inline_always)] +impl PartialEq for GcCell { + #[inline(always)] + fn eq(&self, other: &Self) -> bool { + *self.borrow() == *other.borrow() + } +} + +impl Eq for GcCell {} + +#[allow(clippy::inline_always)] +impl PartialOrd for GcCell { + #[inline(always)] + fn partial_cmp(&self, other: &Self) -> Option { + (*self.borrow()).partial_cmp(&*other.borrow()) + } + + #[inline(always)] + fn lt(&self, other: &Self) -> bool { + *self.borrow() < *other.borrow() + } + + #[inline(always)] + fn le(&self, other: &Self) -> bool { + *self.borrow() <= *other.borrow() + } + + #[inline(always)] + fn gt(&self, other: &Self) -> bool { + *self.borrow() > *other.borrow() + } + + #[inline(always)] + fn ge(&self, other: &Self) -> bool { + *self.borrow() >= *other.borrow() + } +} + +impl Ord for GcCell { + #[inline] + fn cmp(&self, other: &Self) -> Ordering { + (*self.borrow()).cmp(&*other.borrow()) + } +} + +impl Debug for GcCell { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self.flags.get().borrowed() { + BorrowState::Unused | BorrowState::Reading => f + .debug_struct("GcCell") + .field("flags", &self.flags.get()) + .field("value", &self.borrow()) + .finish(), + BorrowState::Writing => f + .debug_struct("GcCell") + .field("flags", &self.flags.get()) + .field("value", &"") + .finish(), + } + } +} diff --git a/boa_gc/src/internals/ephemeron_box.rs b/boa_gc/src/internals/ephemeron_box.rs new file mode 100644 index 0000000000..420b7fb36c --- /dev/null +++ b/boa_gc/src/internals/ephemeron_box.rs @@ -0,0 +1,146 @@ +use crate::trace::Trace; +use crate::{finalizer_safe, GcBox}; +use crate::{Finalize, Gc}; +use std::cell::Cell; +use std::ptr::NonNull; + +/// The inner allocation of an [`Ephemeron`][crate::Ephemeron] pointer. +pub(crate) struct EphemeronBox { + key: Cell>>>, + value: V, +} + +impl EphemeronBox { + pub(crate) fn new(key: &Gc, value: V) -> Self { + Self { + key: Cell::new(Some(key.inner_ptr())), + value, + } + } +} + +impl EphemeronBox { + /// Checks if the key pointer is marked by Trace + #[inline] + pub(crate) fn is_marked(&self) -> bool { + if let Some(key) = self.inner_key() { + key.is_marked() + } else { + false + } + } + + /// Returns some pointer to the `key`'s `GcBox` or None + /// # Panics + /// This method will panic if called while the garbage collector is dropping. + #[inline] + pub(crate) fn inner_key_ptr(&self) -> Option<*mut GcBox> { + assert!(finalizer_safe()); + self.key.get().map(NonNull::as_ptr) + } + + /// Returns some reference to `key`'s `GcBox` or None + #[inline] + pub(crate) fn inner_key(&self) -> Option<&GcBox> { + // SAFETY: This is safe as `EphemeronBox::inner_key_ptr()` will + // fetch either a live `GcBox` or None. The value of `key` is set + // to None in the case where `EphemeronBox` and `key`'s `GcBox` + // entered into `Collector::sweep()` as unmarked. + unsafe { + if let Some(inner_key) = self.inner_key_ptr() { + Some(&*inner_key) + } else { + None + } + } + } + + /// Returns a reference to the value of `key`'s `GcBox` + #[inline] + pub(crate) fn key(&self) -> Option<&K> { + if let Some(key_box) = self.inner_key() { + Some(key_box.value()) + } else { + None + } + } + + /// Returns a reference to `value` + #[inline] + pub(crate) fn value(&self) -> &V { + &self.value + } + + /// Calls [`Trace::weak_trace()`][crate::Trace] on key + #[inline] + fn weak_trace_key(&self) { + if let Some(key) = self.inner_key() { + key.weak_trace_inner(); + } + } + + /// Calls [`Trace::weak_trace()`][crate::Trace] on value + #[inline] + fn weak_trace_value(&self) { + // SAFETY: Value is a sized element that must implement trace. The + // operation is safe as EphemeronBox owns value and `Trace::weak_trace` + // must be implemented on it + unsafe { + self.value().weak_trace(); + } + } +} + +// `EphemeronBox`'s Finalize is special in that if it is determined to be unreachable +// and therefore so has the `GcBox` that `key`stores the pointer to, then we set `key` +// to None to guarantee that we do not access freed memory. +impl Finalize for EphemeronBox { + #[inline] + fn finalize(&self) { + self.key.set(None); + } +} + +// SAFETY: EphemeronBox implements primarly two methods of trace `Trace::is_marked_ephemeron` +// to determine whether the key field is stored and `Trace::weak_trace` which continues the `Trace::weak_trace()` +// into `key` and `value`. +unsafe impl Trace for EphemeronBox { + #[inline] + unsafe fn trace(&self) { + /* An ephemeron is never traced with Phase One Trace */ + } + + /// Checks if the `key`'s `GcBox` has been marked by `Trace::trace()` or `Trace::weak_trace`. + #[inline] + fn is_marked_ephemeron(&self) -> bool { + self.is_marked() + } + + /// Checks if this `EphemeronBox` has already been determined reachable. If so, continue to trace + /// value in `key` and `value`. + #[inline] + unsafe fn weak_trace(&self) { + if self.is_marked() { + self.weak_trace_key(); + self.weak_trace_value(); + } + } + + // EphemeronBox does not implement root. + #[inline] + unsafe fn root(&self) {} + + // EphemeronBox does not implement unroot + #[inline] + unsafe fn unroot(&self) {} + + // An `EphemeronBox`'s key is set to None once it has been finalized. + // + // NOTE: while it is possible for the `key`'s pointer value to be + // resurrected, we should still consider the finalize the ephemeron + // box and set the `key` to None. + #[inline] + fn run_finalizer(&self) { + Finalize::finalize(self); + } +} diff --git a/boa_gc/src/internals/gc_box.rs b/boa_gc/src/internals/gc_box.rs new file mode 100644 index 0000000000..eaca4b48f7 --- /dev/null +++ b/boa_gc/src/internals/gc_box.rs @@ -0,0 +1,195 @@ +use crate::Trace; +use std::cell::Cell; +use std::fmt; +use std::ptr::{self, NonNull}; + +// Age and Weak Flags +const MARK_MASK: usize = 1 << (usize::BITS - 2); +const WEAK_MASK: usize = 1 << (usize::BITS - 1); +const ROOTS_MASK: usize = !(MARK_MASK | WEAK_MASK); +const ROOTS_MAX: usize = ROOTS_MASK; + +/// The `GcBoxheader` contains the `GcBox`'s current state for the `Collector`'s +/// Mark/Sweep as well as a pointer to the next node in the heap. +/// +/// These flags include: +/// - Root Count +/// - Mark Flag Bit +/// - Weak Flag Bit +/// +/// The next node is set by the `Allocator` during initialization and by the +/// `Collector` during the sweep phase. +pub(crate) struct GcBoxHeader { + roots: Cell, + pub(crate) next: Cell>>>, +} + +impl GcBoxHeader { + /// Creates a new `GcBoxHeader` with a root of 1 and next set to None. + #[inline] + pub(crate) fn new() -> Self { + Self { + roots: Cell::new(1), + next: Cell::new(None), + } + } + + /// Creates a new `GcBoxHeader` with the Weak bit at 1 and roots of 1. + #[inline] + pub(crate) fn new_weak() -> Self { + // Set weak_flag + Self { + roots: Cell::new(WEAK_MASK | 1), + next: Cell::new(None), + } + } + + /// Returns the `GcBoxHeader`'s current root count + #[inline] + pub(crate) fn roots(&self) -> usize { + self.roots.get() & ROOTS_MASK + } + + /// Increments `GcBoxHeader`'s root count. + #[inline] + pub(crate) fn inc_roots(&self) { + let roots = self.roots.get(); + + if (roots & ROOTS_MASK) < ROOTS_MAX { + self.roots.set(roots + 1); + } else { + // TODO: implement a better way to handle root overload. + panic!("roots counter overflow"); + } + } + + /// Decreases `GcBoxHeader`'s current root count. + #[inline] + pub(crate) fn dec_roots(&self) { + // Underflow check as a stop gap for current issue when dropping. + if self.roots.get() > 0 { + self.roots.set(self.roots.get() - 1); + } + } + + /// Returns a bool for whether `GcBoxHeader`'s mark bit is 1. + #[inline] + pub(crate) fn is_marked(&self) -> bool { + self.roots.get() & MARK_MASK != 0 + } + + /// Sets `GcBoxHeader`'s mark bit to 1. + #[inline] + pub(crate) fn mark(&self) { + self.roots.set(self.roots.get() | MARK_MASK); + } + + /// Sets `GcBoxHeader`'s mark bit to 0. + #[inline] + pub(crate) fn unmark(&self) { + self.roots.set(self.roots.get() & !MARK_MASK); + } + + /// Returns a bool for whether the `GcBoxHeader`'s weak bit is 1. + #[inline] + pub(crate) fn is_ephemeron(&self) -> bool { + self.roots.get() & WEAK_MASK != 0 + } +} + +impl fmt::Debug for GcBoxHeader { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("GcBoxHeader") + .field("Roots", &self.roots()) + .field("Weak", &self.is_ephemeron()) + .field("Marked", &self.is_marked()) + .finish() + } +} + +/// A garbage collected allocation. +#[derive(Debug)] +pub(crate) struct GcBox { + pub(crate) header: GcBoxHeader, + pub(crate) value: T, +} + +impl GcBox { + /// Returns a new `GcBox` with a rooted `GcBoxHeader`. + pub(crate) fn new(value: T) -> Self { + Self { + header: GcBoxHeader::new(), + value, + } + } + + /// Returns a new `GcBox` with a rooted and weak `GcBoxHeader`. + pub(crate) fn new_weak(value: T) -> Self { + Self { + header: GcBoxHeader::new_weak(), + value, + } + } +} + +impl GcBox { + /// Returns `true` if the two references refer to the same `GcBox`. + pub(crate) fn ptr_eq(this: &Self, other: &Self) -> bool { + // Use .header to ignore fat pointer vtables, to work around + // https://github.com/rust-lang/rust/issues/46139 + ptr::eq(&this.header, &other.header) + } + + /// Marks this `GcBox` and marks through its data. + #[inline] + pub(crate) unsafe fn trace_inner(&self) { + if !self.header.is_marked() && !self.header.is_ephemeron() { + self.header.mark(); + // SAFETY: if `GcBox::trace_inner()` has been called, then, + // this box must have been deemed as reachable via tracing + // from a root, which by extension means that value has not + // been dropped either. + unsafe { + self.value.trace(); + } + } + } + + /// Trace inner data + #[inline] + pub(crate) fn weak_trace_inner(&self) { + // SAFETY: if a `GcBox` has `weak_trace_inner` called, then the inner. + // value must have been deemed as reachable. + unsafe { + self.value.weak_trace(); + } + } + + /// Increases the root count on this `GcBox`. + /// + /// Roots prevent the `GcBox` from being destroyed by the garbage collector. + #[inline] + pub(crate) fn root_inner(&self) { + self.header.inc_roots(); + } + + /// Decreases the root count on this `GcBox`. + /// + /// Roots prevent the `GcBox` from being destroyed by the garbage collector. + #[inline] + pub(crate) fn unroot_inner(&self) { + self.header.dec_roots(); + } + + /// Returns a reference to the `GcBox`'s value. + #[inline] + pub(crate) fn value(&self) -> &T { + &self.value + } + + /// Returns a bool for whether the header is marked. + #[inline] + pub(crate) fn is_marked(&self) -> bool { + self.header.is_marked() + } +} diff --git a/boa_gc/src/internals/mod.rs b/boa_gc/src/internals/mod.rs new file mode 100644 index 0000000000..005c000ade --- /dev/null +++ b/boa_gc/src/internals/mod.rs @@ -0,0 +1,5 @@ +mod ephemeron_box; +pub(crate) use ephemeron_box::EphemeronBox; + +mod gc_box; +pub(crate) use gc_box::GcBox; diff --git a/boa_gc/src/lib.rs b/boa_gc/src/lib.rs index ac24531793..4b26855ea8 100644 --- a/boa_gc/src/lib.rs +++ b/boa_gc/src/lib.rs @@ -1,6 +1,383 @@ //! Garbage collector for the Boa JavaScript engine. -pub use gc::{ - custom_trace, finalizer_safe, force_collect, unsafe_empty_trace, Finalize, Gc, GcCell as Cell, - GcCellRef as Ref, GcCellRefMut as RefMut, Trace, -}; +#![warn( + clippy::perf, + clippy::single_match_else, + clippy::dbg_macro, + clippy::doc_markdown, + clippy::wildcard_imports, + clippy::struct_excessive_bools, + clippy::doc_markdown, + clippy::semicolon_if_nothing_returned, + clippy::pedantic +)] +#![deny( + clippy::all, + clippy::cast_lossless, + clippy::redundant_closure_for_method_calls, + clippy::use_self, + clippy::unnested_or_patterns, + clippy::trivially_copy_pass_by_ref, + clippy::needless_pass_by_value, + clippy::match_wildcard_for_single_variants, + clippy::map_unwrap_or, + clippy::undocumented_unsafe_blocks, + clippy::missing_safety_doc, + unsafe_op_in_unsafe_fn, + unused_qualifications, + unused_import_braces, + unused_lifetimes, + unreachable_pub, + trivial_numeric_casts, + rustdoc::broken_intra_doc_links, + missing_debug_implementations, + missing_copy_implementations, + deprecated_in_future, + meta_variable_misuse, + non_ascii_idents, + rust_2018_compatibility, + rust_2018_idioms, + future_incompatible, + nonstandard_style, + missing_docs +)] +#![allow(clippy::let_unit_value, clippy::module_name_repetitions)] + +extern crate self as boa_gc; + +use boa_profiler::Profiler; +use std::cell::{Cell, RefCell}; +use std::mem; +use std::ptr::NonNull; + +mod trace; + +pub(crate) mod internals; + +mod cell; +mod pointers; + +pub use crate::trace::{Finalize, Trace}; +pub use boa_macros::{Finalize, Trace}; +pub use cell::{GcCell, GcCellRef, GcCellRefMut}; +pub use pointers::{Ephemeron, Gc, WeakGc}; + +use internals::GcBox; + +type GcPointer = NonNull>; + +thread_local!(static EPHEMERON_QUEUE: Cell>> = Cell::new(None)); +thread_local!(static GC_DROPPING: Cell = Cell::new(false)); +thread_local!(static BOA_GC: RefCell = RefCell::new( BoaGc { + config: GcConfig::default(), + runtime: GcRuntimeData::default(), + adult_start: Cell::new(None), +})); + +#[derive(Debug, Clone, Copy)] +struct GcConfig { + threshold: usize, + used_space_percentage: usize, +} + +// Setting the defaults to an arbitrary value currently. +// +// TODO: Add a configure later +impl Default for GcConfig { + fn default() -> Self { + Self { + threshold: 1024, + used_space_percentage: 80, + } + } +} + +#[derive(Default, Debug, Clone, Copy)] +struct GcRuntimeData { + collections: usize, + bytes_allocated: usize, +} + +#[derive(Debug)] +struct BoaGc { + config: GcConfig, + runtime: GcRuntimeData, + adult_start: Cell>, +} + +impl Drop for BoaGc { + fn drop(&mut self) { + Collector::dump(self); + } +} + +// Whether or not the thread is currently in the sweep phase of garbage collection. +// During this phase, attempts to dereference a `Gc` pointer will trigger a panic. +/// `DropGuard` flags whether the Collector is currently running `Collector::sweep()` or `Collector::dump()` +/// +/// While the `DropGuard` is active, all `GcBox`s must not be dereferenced or accessed as it could cause Undefined Behavior +#[derive(Debug, Clone)] +struct DropGuard; + +impl DropGuard { + fn new() -> Self { + GC_DROPPING.with(|dropping| dropping.set(true)); + Self + } +} + +impl Drop for DropGuard { + fn drop(&mut self) { + GC_DROPPING.with(|dropping| dropping.set(false)); + } +} + +/// Returns `true` if it is safe for a type to run [`Finalize::finalize`]. +#[must_use] +#[inline] +pub fn finalizer_safe() -> bool { + GC_DROPPING.with(|dropping| !dropping.get()) +} + +/// The Allocator handles allocation of garbage collected values. +/// +/// The allocator can trigger a garbage collection. +#[derive(Debug, Clone, Copy)] +struct Allocator; + +impl Allocator { + /// Allocate a new garbage collected value to the Garbage Collector's heap. + fn allocate(value: GcBox) -> NonNull> { + let _timer = Profiler::global().start_event("New Pointer", "BoaAlloc"); + let element_size = mem::size_of_val::>(&value); + BOA_GC.with(|st| { + let mut gc = st.borrow_mut(); + + Self::manage_state(&mut gc); + value.header.next.set(gc.adult_start.take()); + // Safety: Value Cannot be a null as it must be a GcBox + let ptr = unsafe { NonNull::new_unchecked(Box::into_raw(Box::from(value))) }; + + gc.adult_start.set(Some(ptr)); + gc.runtime.bytes_allocated += element_size; + + ptr + }) + } + + fn manage_state(gc: &mut BoaGc) { + if gc.runtime.bytes_allocated > gc.config.threshold { + Collector::run_full_collection(gc); + + if gc.runtime.bytes_allocated + > gc.config.threshold / 100 * gc.config.used_space_percentage + { + gc.config.threshold = + gc.runtime.bytes_allocated / gc.config.used_space_percentage * 100; + } + } + } +} + +// This collector currently functions in four main phases +// +// Mark -> Finalize -> Mark -> Sweep +// +// Mark nodes as reachable then finalize the unreachable nodes. A remark phase +// then needs to be retriggered as finalization can potentially resurrect dead +// nodes. +// +// A better approach in a more concurrent structure may be to reorder. +// +// Mark -> Sweep -> Finalize +struct Collector; + +impl Collector { + /// Run a collection on the full heap. + fn run_full_collection(gc: &mut BoaGc) { + let _timer = Profiler::global().start_event("Gc Full Collection", "gc"); + gc.runtime.collections += 1; + let unreachable_adults = Self::mark_heap(&gc.adult_start); + + // Check if any unreachable nodes were found and finalize + if !unreachable_adults.is_empty() { + // SAFETY: Please see `Collector::finalize()` + unsafe { Self::finalize(unreachable_adults) }; + } + + let _final_unreachable_adults = Self::mark_heap(&gc.adult_start); + + // SAFETY: Please see `Collector::sweep()` + unsafe { + Self::sweep(&gc.adult_start, &mut gc.runtime.bytes_allocated); + } + } + + /// Walk the heap and mark any nodes deemed reachable + fn mark_heap(head: &Cell>>>) -> Vec>> { + let _timer = Profiler::global().start_event("Gc Marking", "gc"); + // Walk the list, tracing and marking the nodes + let mut finalize = Vec::new(); + let mut ephemeron_queue = Vec::new(); + let mut mark_head = head; + while let Some(node) = mark_head.get() { + // SAFETY: node must be valid as it is coming directly from the heap. + let node_ref = unsafe { node.as_ref() }; + if node_ref.header.is_ephemeron() { + ephemeron_queue.push(node); + } else if node_ref.header.roots() > 0 { + // SAFETY: the reference to node must be valid as it is rooted. Passing + // invalid references can result in Undefined Behavior + unsafe { + node_ref.trace_inner(); + } + } else { + finalize.push(node); + } + mark_head = &node_ref.header.next; + } + + // Ephemeron Evaluation + if !ephemeron_queue.is_empty() { + ephemeron_queue = Self::mark_ephemerons(ephemeron_queue); + } + + // Any left over nodes in the ephemeron queue at this point are + // unreachable and need to be notified/finalized. + finalize.extend(ephemeron_queue); + + finalize + } + + // Tracing Ephemerons/Weak is always requires tracing the inner nodes in case it ends up marking unmarked node + // + // Time complexity should be something like O(nd) where d is the longest chain of epehemerons + /// Mark any ephemerons that are deemed live and trace their fields. + fn mark_ephemerons( + initial_queue: Vec>>, + ) -> Vec>> { + let mut ephemeron_queue = initial_queue; + loop { + // iterate through ephemeron queue, sorting nodes by whether they + // are reachable or unreachable + let (reachable, other): (Vec<_>, Vec<_>) = + ephemeron_queue.into_iter().partition(|node| { + // SAFETY: Any node on the eph_queue or the heap must be non null + let node = unsafe { node.as_ref() }; + if node.value.is_marked_ephemeron() { + node.header.mark(); + true + } else { + node.header.roots() > 0 + } + }); + // Replace the old queue with the unreachable + ephemeron_queue = other; + + // If reachable nodes is not empty, trace values. If it is empty, + // break from the loop + if reachable.is_empty() { + break; + } + EPHEMERON_QUEUE.with(|state| state.set(Some(Vec::new()))); + // iterate through reachable nodes and trace their values, + // enqueuing any ephemeron that is found during the trace + for node in reachable { + // TODO: deal with fetch ephemeron_queue + // SAFETY: Node must be a valid pointer or else it would not be deemed reachable. + unsafe { + node.as_ref().weak_trace_inner(); + } + } + + EPHEMERON_QUEUE.with(|st| { + if let Some(found_nodes) = st.take() { + ephemeron_queue.extend(found_nodes); + } + }); + } + ephemeron_queue + } + + /// # Safety + /// + /// Passing a vec with invalid pointers will result in Undefined Behaviour. + unsafe fn finalize(finalize_vec: Vec>>) { + let _timer = Profiler::global().start_event("Gc Finalization", "gc"); + for node in finalize_vec { + // We double check that the unreachable nodes are actually unreachable + // prior to finalization as they could have been marked by a different + // trace after initially being added to the queue + // + // SAFETY: The caller must ensure all pointers inside `finalize_vec` are valid. + let node = unsafe { node.as_ref() }; + if !node.header.is_marked() { + Trace::run_finalizer(&node.value); + } + } + } + + /// # Safety + /// + /// - Providing an invalid pointer in the `heap_start` or in any of the headers of each + /// node will result in Undefined Behaviour. + /// - Providing a list of pointers that weren't allocated by `Box::into_raw(Box::new(..))` + /// will result in Undefined Behaviour. + unsafe fn sweep( + heap_start: &Cell>>>, + total_allocated: &mut usize, + ) { + let _timer = Profiler::global().start_event("Gc Sweeping", "gc"); + let _guard = DropGuard::new(); + + let mut sweep_head = heap_start; + while let Some(node) = sweep_head.get() { + // SAFETY: The caller must ensure the validity of every node of `heap_start`. + let node_ref = unsafe { node.as_ref() }; + if node_ref.is_marked() { + node_ref.header.unmark(); + sweep_head = &node_ref.header.next; + } else if node_ref.header.is_ephemeron() && node_ref.header.roots() > 0 { + // Keep the ephemeron box's alive if rooted, but note that it's pointer is no longer safe + Trace::run_finalizer(&node_ref.value); + sweep_head = &node_ref.header.next; + } else { + // SAFETY: The algorithm ensures only unmarked/unreachable pointers are dropped. + // The caller must ensure all pointers were allocated by `Box::into_raw(Box::new(..))`. + let unmarked_node = unsafe { Box::from_raw(node.as_ptr()) }; + let unallocated_bytes = mem::size_of_val::>(&*unmarked_node); + *total_allocated -= unallocated_bytes; + sweep_head.set(unmarked_node.header.next.take()); + } + } + } + + // Clean up the heap when BoaGc is dropped + fn dump(gc: &mut BoaGc) { + // Not initializing a dropguard since this should only be invoked when BOA_GC is being dropped. + let _guard = DropGuard::new(); + + let sweep_head = &gc.adult_start; + while let Some(node) = sweep_head.get() { + // SAFETY: + // The `Allocator` must always ensure its start node is a valid, non-null pointer that + // was allocated by `Box::from_raw(Box::new(..))`. + let unmarked_node = unsafe { Box::from_raw(node.as_ptr()) }; + sweep_head.set(unmarked_node.header.next.take()); + } + } +} + +/// Forcefully runs a garbage collection of all unaccessible nodes. +pub fn force_collect() { + BOA_GC.with(|current| { + let mut gc = current.borrow_mut(); + + if gc.runtime.bytes_allocated > 0 { + Collector::run_full_collection(&mut gc); + } + }); +} + +#[cfg(test)] +mod test; diff --git a/boa_gc/src/pointers/ephemeron.rs b/boa_gc/src/pointers/ephemeron.rs new file mode 100644 index 0000000000..35702a7b11 --- /dev/null +++ b/boa_gc/src/pointers/ephemeron.rs @@ -0,0 +1,125 @@ +use crate::{ + finalizer_safe, + internals::EphemeronBox, + trace::{Finalize, Trace}, + Allocator, Gc, GcBox, EPHEMERON_QUEUE, +}; +use std::cell::Cell; +use std::ptr::NonNull; + +#[derive(Debug)] +/// A key-value pair where the value becomes unaccesible when the key is garbage collected. +/// +/// See Racket's explanation on [**ephemerons**][eph] for a brief overview or read Barry Hayes' +/// [_Ephemerons_: a new finalization mechanism][acm]. +/// +/// +/// [eph]: https://docs.racket-lang.org/reference/ephemerons.html +/// [acm]: https://dl.acm.org/doi/10.1145/263700.263733 +pub struct Ephemeron { + inner_ptr: Cell>>>, +} + +impl Ephemeron { + /// Creates a new `Ephemeron`. + pub fn new(key: &Gc, value: V) -> Self { + Self { + inner_ptr: Cell::new(Allocator::allocate(GcBox::new_weak(EphemeronBox::new( + key, value, + )))), + } + } +} + +impl Ephemeron { + #[inline] + fn inner_ptr(&self) -> NonNull>> { + self.inner_ptr.get() + } + + #[inline] + fn inner(&self) -> &GcBox> { + // SAFETY: GcBox> must live until it is unrooted by Drop + unsafe { &*self.inner_ptr().as_ptr() } + } + + #[inline] + /// Gets the weak key of this `Ephemeron`, or `None` if the key was already garbage + /// collected. + pub fn key(&self) -> Option<&K> { + self.inner().value().key() + } + + #[inline] + /// Gets the stored value of this `Ephemeron`. + pub fn value(&self) -> &V { + self.inner().value().value() + } + + #[inline] + /// Gets a `Gc` for the stored key of this `Ephemeron`. + pub fn upgrade_key(&self) -> Option> { + // SAFETY: ptr must be a valid pointer or None would have been returned. + self.inner().value().inner_key_ptr().map(|ptr| unsafe { + let inner_ptr = NonNull::new_unchecked(ptr); + Gc::from_ptr(inner_ptr) + }) + } +} + +impl Finalize for Ephemeron {} + +// SAFETY: Ephemerons trace implementation is standard for everything except `Trace::weak_trace()`, +// which pushes the GcBox> onto the EphemeronQueue +unsafe impl Trace for Ephemeron { + #[inline] + unsafe fn trace(&self) {} + + // Push this Ephemeron's pointer onto the EphemeronQueue + #[inline] + unsafe fn weak_trace(&self) { + EPHEMERON_QUEUE.with(|q| { + let mut queue = q.take().expect("queue is initialized by weak_trace"); + queue.push(self.inner_ptr()); + }); + } + + #[inline] + unsafe fn root(&self) {} + + #[inline] + unsafe fn unroot(&self) {} + + #[inline] + fn run_finalizer(&self) { + Finalize::finalize(self); + } +} + +impl Clone for Ephemeron { + #[inline] + fn clone(&self) -> Self { + // SAFETY: This is safe because the inner_ptr must live as long as it's roots. + // Mismanagement of roots can cause inner_ptr to use after free or Undefined + // Behavior. + unsafe { + let eph = Self { + inner_ptr: Cell::new(NonNull::new_unchecked(self.inner_ptr().as_ptr())), + }; + // Increment the Ephemeron's GcBox roots by 1 + self.inner().root_inner(); + eph + } + } +} + +impl Drop for Ephemeron { + #[inline] + fn drop(&mut self) { + // NOTE: We assert that this drop call is not a + // drop from `Collector::dump` or `Collector::sweep` + if finalizer_safe() { + self.inner().unroot_inner(); + } + } +} diff --git a/boa_gc/src/pointers/gc.rs b/boa_gc/src/pointers/gc.rs new file mode 100644 index 0000000000..df44a8b327 --- /dev/null +++ b/boa_gc/src/pointers/gc.rs @@ -0,0 +1,275 @@ +use std::cell::Cell; +use std::cmp::Ordering; +use std::fmt::{self, Debug, Display}; +use std::hash::{Hash, Hasher}; +use std::marker::PhantomData; +use std::ops::Deref; +use std::ptr::{self, addr_of_mut, NonNull}; +use std::rc::Rc; + +use crate::internals::GcBox; +use crate::trace::{Finalize, Trace}; +use crate::{finalizer_safe, Allocator}; + +// Technically, this function is safe, since we're just modifying the address of a pointer without +// dereferencing it. +pub(crate) fn set_data_ptr(mut ptr: *mut T, data: *mut U) -> *mut T { + // SAFETY: this should be safe as ptr must be a valid nonnull + unsafe { + ptr::write(addr_of_mut!(ptr).cast::<*mut u8>(), data.cast::()); + } + ptr +} + +/// A garbage-collected pointer type over an immutable value. +pub struct Gc { + pub(crate) inner_ptr: Cell>>, + pub(crate) marker: PhantomData>, +} + +impl Gc { + /// Constructs a new `Gc` with the given value. + pub fn new(value: T) -> Self { + // Create GcBox and allocate it to heap. + // + // Note: Allocator can cause Collector to run + let inner_ptr = Allocator::allocate(GcBox::new(value)); + // SAFETY: inner_ptr was just allocated, so it must be a valid value that implements [`Trace`] + unsafe { (*inner_ptr.as_ptr()).value().unroot() } + let gc = Self { + inner_ptr: Cell::new(inner_ptr), + marker: PhantomData, + }; + gc.set_root(); + gc + } +} + +impl Gc { + /// Returns `true` if the two `Gc`s point to the same allocation. + pub fn ptr_eq(this: &Self, other: &Self) -> bool { + GcBox::ptr_eq(this.inner(), other.inner()) + } + + /// Will return a new rooted `Gc` from a `GcBox` pointer + pub(crate) fn from_ptr(ptr: NonNull>) -> Self { + // SAFETY: the value provided as a pointer MUST be a valid GcBox. + unsafe { + ptr.as_ref().root_inner(); + let gc = Self { + inner_ptr: Cell::new(ptr), + marker: PhantomData, + }; + gc.set_root(); + gc + } + } +} + +/// Returns the given pointer with its root bit cleared. +pub(crate) unsafe fn clear_root_bit( + ptr: NonNull>, +) -> NonNull> { + let ptr = ptr.as_ptr(); + let data = ptr.cast::(); + let addr = data as isize; + let ptr = set_data_ptr(ptr, data.wrapping_offset((addr & !1) - addr)); + // SAFETY: ptr must be a non null value + unsafe { NonNull::new_unchecked(ptr) } +} + +impl Gc { + fn rooted(&self) -> bool { + self.inner_ptr.get().as_ptr().cast::() as usize & 1 != 0 + } + + pub(crate) fn set_root(&self) { + let ptr = self.inner_ptr.get().as_ptr(); + let data = ptr.cast::(); + let addr = data as isize; + let ptr = set_data_ptr(ptr, data.wrapping_offset((addr | 1) - addr)); + // SAFETY: ptr must be a non null value. + unsafe { + self.inner_ptr.set(NonNull::new_unchecked(ptr)); + } + } + + fn clear_root(&self) { + // SAFETY: inner_ptr must be a valid non-null pointer to a live GcBox. + unsafe { + self.inner_ptr.set(clear_root_bit(self.inner_ptr.get())); + } + } + + #[inline] + pub(crate) fn inner_ptr(&self) -> NonNull> { + assert!(finalizer_safe()); + // SAFETY: inner_ptr must be a live GcBox. Calling this on a dropped GcBox + // can result in Undefined Behavior. + unsafe { clear_root_bit(self.inner_ptr.get()) } + } + + #[inline] + fn inner(&self) -> &GcBox { + // SAFETY: Please see Gc::inner_ptr() + unsafe { self.inner_ptr().as_ref() } + } +} + +impl Finalize for Gc {} + +// SAFETY: `Gc` maintains it's own rootedness and implements all methods of +// Trace. It is not possible to root an already rooted `Gc` and vice versa. +unsafe impl Trace for Gc { + #[inline] + unsafe fn trace(&self) { + // SAFETY: Inner must be live and allocated GcBox. + unsafe { + self.inner().trace_inner(); + } + } + + #[inline] + unsafe fn weak_trace(&self) { + self.inner().weak_trace_inner(); + } + + #[inline] + unsafe fn root(&self) { + assert!(!self.rooted(), "Can't double-root a Gc"); + // Try to get inner before modifying our state. Inner may be + // inaccessible due to this method being invoked during the sweeping + // phase, and we don't want to modify our state before panicking. + self.inner().root_inner(); + self.set_root(); + } + + #[inline] + unsafe fn unroot(&self) { + assert!(self.rooted(), "Can't double-unroot a Gc"); + // Try to get inner before modifying our state. Inner may be + // inaccessible due to this method being invoked during the sweeping + // phase, and we don't want to modify our state before panicking. + self.inner().unroot_inner(); + self.clear_root(); + } + + #[inline] + fn run_finalizer(&self) { + Finalize::finalize(self); + } +} + +impl Clone for Gc { + #[inline] + fn clone(&self) -> Self { + Self::from_ptr(self.inner_ptr()) + } +} + +impl Deref for Gc { + type Target = T; + + #[inline] + fn deref(&self) -> &T { + self.inner().value() + } +} + +impl Drop for Gc { + #[inline] + fn drop(&mut self) { + // If this pointer was a root, we should unroot it. + if self.rooted() { + self.inner().unroot_inner(); + } + } +} + +impl Default for Gc { + #[inline] + fn default() -> Self { + Self::new(Default::default()) + } +} + +#[allow(clippy::inline_always)] +impl PartialEq for Gc { + #[inline(always)] + fn eq(&self, other: &Self) -> bool { + **self == **other + } +} + +impl Eq for Gc {} + +#[allow(clippy::inline_always)] +impl PartialOrd for Gc { + #[inline(always)] + fn partial_cmp(&self, other: &Self) -> Option { + (**self).partial_cmp(&**other) + } + + #[inline(always)] + fn lt(&self, other: &Self) -> bool { + **self < **other + } + + #[inline(always)] + fn le(&self, other: &Self) -> bool { + **self <= **other + } + + #[inline(always)] + fn gt(&self, other: &Self) -> bool { + **self > **other + } + + #[inline(always)] + fn ge(&self, other: &Self) -> bool { + **self >= **other + } +} + +impl Ord for Gc { + #[inline] + fn cmp(&self, other: &Self) -> Ordering { + (**self).cmp(&**other) + } +} + +impl Hash for Gc { + fn hash(&self, state: &mut H) { + (**self).hash(state); + } +} + +impl Display for Gc { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + Display::fmt(&**self, f) + } +} + +impl Debug for Gc { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + Debug::fmt(&**self, f) + } +} + +impl fmt::Pointer for Gc { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + fmt::Pointer::fmt(&self.inner(), f) + } +} + +impl std::borrow::Borrow for Gc { + fn borrow(&self) -> &T { + self + } +} + +impl AsRef for Gc { + fn as_ref(&self) -> &T { + self + } +} diff --git a/boa_gc/src/pointers/mod.rs b/boa_gc/src/pointers/mod.rs new file mode 100644 index 0000000000..5c14182762 --- /dev/null +++ b/boa_gc/src/pointers/mod.rs @@ -0,0 +1,9 @@ +//! Pointers represents the External types returned by the Boa Garbage Collector + +mod ephemeron; +mod gc; +mod weak; + +pub use ephemeron::Ephemeron; +pub use gc::Gc; +pub use weak::WeakGc; diff --git a/boa_gc/src/pointers/weak.rs b/boa_gc/src/pointers/weak.rs new file mode 100644 index 0000000000..26a1945efa --- /dev/null +++ b/boa_gc/src/pointers/weak.rs @@ -0,0 +1,49 @@ +use crate::{Ephemeron, Finalize, Gc, Trace}; + +/// A weak reference to a [`Gc`]. +/// +/// This type allows keeping references to [`Gc`] managed values without keeping them alive for +/// garbage collections. However, this also means [`WeakGc::value`] can return `None` at any moment. +#[derive(Debug, Trace, Finalize)] +#[repr(transparent)] +pub struct WeakGc { + inner: Ephemeron, +} + +impl WeakGc { + /// Creates a new weak pointer for a garbage collected value. + pub fn new(value: &Gc) -> Self { + Self { + inner: Ephemeron::new(value, ()), + } + } +} + +impl WeakGc { + #[inline] + /// Gets the value of this weak pointer, or `None` if the value was already garbage collected. + pub fn value(&self) -> Option<&T> { + self.inner.key() + } + + #[inline] + /// Upgrade returns a `Gc` pointer for the internal value if valid, or None if the value was already garbage collected. + pub fn upgrade(&self) -> Option> { + self.inner.upgrade_key() + } +} + +impl Clone for WeakGc { + #[inline] + fn clone(&self) -> Self { + Self { + inner: self.inner.clone(), + } + } +} + +impl From> for WeakGc { + fn from(inner: Ephemeron) -> Self { + Self { inner } + } +} diff --git a/boa_gc/src/test/allocation.rs b/boa_gc/src/test/allocation.rs new file mode 100644 index 0000000000..2738699983 --- /dev/null +++ b/boa_gc/src/test/allocation.rs @@ -0,0 +1,31 @@ +use super::{run_test, Harness}; +use crate::{force_collect, Gc, GcCell}; + +#[test] +fn gc_basic_cell_allocation() { + run_test(|| { + let gc_cell = Gc::new(GcCell::new(16_u16)); + + force_collect(); + Harness::assert_collections(1); + Harness::assert_bytes_allocated(); + assert_eq!(*gc_cell.borrow_mut(), 16); + }); +} + +#[test] +fn gc_basic_pointer_alloc() { + run_test(|| { + let gc = Gc::new(16_u8); + + force_collect(); + Harness::assert_collections(1); + Harness::assert_bytes_allocated(); + assert_eq!(*gc, 16); + + drop(gc); + force_collect(); + Harness::assert_collections(2); + Harness::assert_empty_gc(); + }); +} diff --git a/boa_gc/src/test/cell.rs b/boa_gc/src/test/cell.rs new file mode 100644 index 0000000000..1e82acf1e7 --- /dev/null +++ b/boa_gc/src/test/cell.rs @@ -0,0 +1,15 @@ +use boa_gc::{Gc, GcCell}; + +use super::run_test; + +#[test] +fn boa_borrow_mut_test() { + run_test(|| { + let v = Gc::new(GcCell::new(Vec::new())); + + for _ in 1..=259 { + let cell = Gc::new(GcCell::new([0u8; 10])); + v.borrow_mut().push(cell); + } + }); +} diff --git a/boa_gc/src/test/mod.rs b/boa_gc/src/test/mod.rs new file mode 100644 index 0000000000..559fbb8d80 --- /dev/null +++ b/boa_gc/src/test/mod.rs @@ -0,0 +1,37 @@ +use crate::BOA_GC; + +mod allocation; +mod cell; +mod weak; + +struct Harness; + +impl Harness { + fn assert_collections(o: usize) { + BOA_GC.with(|current| { + let gc = current.borrow(); + assert_eq!(gc.runtime.collections, o); + }); + } + + fn assert_empty_gc() { + BOA_GC.with(|current| { + let gc = current.borrow(); + + assert!(gc.adult_start.get().is_none()); + assert!(gc.runtime.bytes_allocated == 0); + }); + } + + fn assert_bytes_allocated() { + BOA_GC.with(|current| { + let gc = current.borrow(); + assert!(gc.runtime.bytes_allocated > 0); + }); + } +} + +fn run_test(test: impl FnOnce() + Send + 'static) { + let handle = std::thread::spawn(test); + handle.join().unwrap(); +} diff --git a/boa_gc/src/test/weak.rs b/boa_gc/src/test/weak.rs new file mode 100644 index 0000000000..5f1e33ccc1 --- /dev/null +++ b/boa_gc/src/test/weak.rs @@ -0,0 +1,133 @@ +use boa_gc::{force_collect, Ephemeron, Gc, WeakGc}; + +use super::run_test; + +#[test] +fn eph_weak_gc_test() { + run_test(|| { + let gc_value = Gc::new(3); + + { + let cloned_gc = gc_value.clone(); + + let weak = WeakGc::new(&cloned_gc); + + assert_eq!(*weak.value().expect("Is live currently"), 3); + drop(cloned_gc); + force_collect(); + assert_eq!(*weak.value().expect("WeakGc is still live here"), 3); + + drop(gc_value); + force_collect(); + + assert!(weak.value().is_none()); + } + }); +} + +#[test] +fn eph_ephemeron_test() { + run_test(|| { + let gc_value = Gc::new(3); + + { + let cloned_gc = gc_value.clone(); + + let ephemeron = Ephemeron::new(&cloned_gc, String::from("Hello World!")); + + assert_eq!(*ephemeron.key().expect("Ephemeron is live"), 3); + assert_eq!(*ephemeron.value(), String::from("Hello World!")); + drop(cloned_gc); + force_collect(); + assert_eq!(*ephemeron.key().expect("Ephemeron is still live here"), 3); + + drop(gc_value); + force_collect(); + + assert!(ephemeron.key().is_none()); + } + }); +} + +#[test] +fn eph_allocation_chains() { + run_test(|| { + let gc_value = Gc::new(String::from("foo")); + + { + let cloned_gc = gc_value.clone(); + let weak = WeakGc::new(&cloned_gc); + let wrap = Gc::new(weak); + + assert_eq!(wrap.value().expect("weak is live"), &String::from("foo")); + + let eph = Ephemeron::new(&wrap, 3); + + drop(cloned_gc); + force_collect(); + + assert_eq!( + eph.key() + .expect("eph is still live") + .value() + .expect("weak is still live"), + &String::from("foo") + ); + + drop(gc_value); + force_collect(); + + assert!(eph.key().expect("eph is still live").value().is_none()); + } + }); +} + +#[test] +fn eph_basic_alloc_dump_test() { + run_test(|| { + let gc_value = Gc::new(String::from("gc here")); + let _gc_two = Gc::new("hmmm"); + + let eph = Ephemeron::new(&gc_value, 4); + let _fourth = Gc::new("tail"); + + assert_eq!(*eph.key().expect("must be live"), String::from("gc here")); + }); +} + +#[test] +fn eph_basic_upgrade_test() { + run_test(|| { + let init_gc = Gc::new(String::from("foo")); + + let weak = WeakGc::new(&init_gc); + + let new_gc = weak.upgrade().expect("Weak is still live"); + + drop(weak); + force_collect(); + + assert_eq!(*init_gc, *new_gc); + }); +} + +#[test] +fn eph_basic_clone_test() { + run_test(|| { + let init_gc = Gc::new(String::from("bar")); + + let weak = WeakGc::new(&init_gc); + + let new_gc = weak.upgrade().expect("Weak is live"); + let new_weak = weak.clone(); + + drop(weak); + force_collect(); + + assert_eq!(*new_gc, *new_weak.value().expect("weak should be live")); + assert_eq!( + *init_gc, + *new_weak.value().expect("weak_should be live still") + ); + }); +} diff --git a/boa_gc/src/trace.rs b/boa_gc/src/trace.rs new file mode 100644 index 0000000000..354a0119d4 --- /dev/null +++ b/boa_gc/src/trace.rs @@ -0,0 +1,450 @@ +use std::borrow::{Cow, ToOwned}; +use std::collections::{BTreeMap, BTreeSet, BinaryHeap, HashMap, HashSet, LinkedList, VecDeque}; +use std::hash::{BuildHasher, Hash}; +use std::marker::PhantomData; +use std::num::{ + NonZeroI128, NonZeroI16, NonZeroI32, NonZeroI64, NonZeroI8, NonZeroIsize, NonZeroU128, + NonZeroU16, NonZeroU32, NonZeroU64, NonZeroU8, NonZeroUsize, +}; +use std::path::{Path, PathBuf}; +use std::rc::Rc; +use std::sync::atomic::{ + AtomicBool, AtomicI16, AtomicI32, AtomicI64, AtomicI8, AtomicIsize, AtomicU16, AtomicU32, + AtomicU64, AtomicU8, AtomicUsize, +}; + +/// Substitute for the [`Drop`] trait for garbage collected types. +pub trait Finalize { + /// Cleanup logic for a type. + fn finalize(&self) {} +} + +/// The Trace trait, which needs to be implemented on garbage-collected objects. +/// +/// # Safety +/// +/// - An incorrect implementation of the trait can result in heap overflows, data corruption, +/// use-after-free, or Undefined Behaviour in general. +/// +/// - Calling any of the functions marked as `unsafe` outside of the context of the garbage collector +/// can result in Undefined Behaviour. +pub unsafe trait Trace: Finalize { + /// Marks all contained `Gc`s. + /// + /// # Safety + /// + /// See [`Trace`]. + unsafe fn trace(&self); + + /// Marks all contained weak references of a `Gc`. + /// + /// # Safety + /// + /// See [`Trace`]. + unsafe fn weak_trace(&self); + + /// Increments the root-count of all contained `Gc`s. + /// + /// # Safety + /// + /// See [`Trace`]. + unsafe fn root(&self); + + /// Decrements the root-count of all contained `Gc`s. + /// + /// # Safety + /// + /// See [`Trace`]. + unsafe fn unroot(&self); + + /// Checks if an ephemeron's key is marked. + #[doc(hidden)] + fn is_marked_ephemeron(&self) -> bool { + false + } + + /// Runs [`Finalize::finalize`] on this object and all + /// contained subobjects. + fn run_finalizer(&self); +} + +/// Utility macro to define an empty implementation of [`Trace`]. +/// +/// Use this for marking types as not containing any `Trace` types. +#[macro_export] +macro_rules! empty_trace { + () => { + #[inline] + unsafe fn trace(&self) {} + #[inline] + unsafe fn weak_trace(&self) {} + #[inline] + unsafe fn root(&self) {} + #[inline] + unsafe fn unroot(&self) {} + #[inline] + fn run_finalizer(&self) { + $crate::Finalize::finalize(self) + } + }; +} + +/// Utility macro to manually implement [`Trace`] on a type. +/// +/// You define a `this` parameter name and pass in a body, which should call `mark` on every +/// traceable element inside the body. The mark implementation will automatically delegate to the +/// correct method on the argument. +/// +/// # Safety +/// +/// Misusing the `mark` function may result in Undefined Behaviour. +#[macro_export] +macro_rules! custom_trace { + ($this:ident, $body:expr) => { + #[inline] + unsafe fn trace(&self) { + #[inline] + fn mark(it: &T) { + // SAFETY: The implementor must ensure that `trace` is correctly implemented. + unsafe { + $crate::Trace::trace(it); + } + } + let $this = self; + $body + } + #[inline] + unsafe fn weak_trace(&self) { + #[inline] + fn mark(it: &T) { + // SAFETY: The implementor must ensure that `weak_trace` is correctly implemented. + unsafe { + $crate::Trace::weak_trace(it); + } + } + let $this = self; + $body + } + #[inline] + unsafe fn root(&self) { + #[inline] + fn mark(it: &T) { + // SAFETY: The implementor must ensure that `root` is correctly implemented. + unsafe { + $crate::Trace::root(it); + } + } + let $this = self; + $body + } + #[inline] + unsafe fn unroot(&self) { + #[inline] + fn mark(it: &T) { + // SAFETY: The implementor must ensure that `unroot` is correctly implemented. + unsafe { + $crate::Trace::unroot(it); + } + } + let $this = self; + $body + } + #[inline] + fn run_finalizer(&self) { + #[inline] + fn mark(it: &T) { + $crate::Trace::run_finalizer(it); + } + $crate::Finalize::finalize(self); + let $this = self; + $body + } + }; +} + +impl Finalize for &'static T {} +// SAFETY: 'static references don't need to be traced, since they live indefinitely. +unsafe impl Trace for &'static T { + empty_trace!(); +} + +macro_rules! simple_empty_finalize_trace { + ($($T:ty),*) => { + $( + impl Finalize for $T {} + + // SAFETY: + // Primitive types and string types don't have inner nodes that need to be marked. + unsafe impl Trace for $T { empty_trace!(); } + )* + } +} + +simple_empty_finalize_trace![ + (), + bool, + isize, + usize, + i8, + u8, + i16, + u16, + i32, + u32, + i64, + u64, + i128, + u128, + f32, + f64, + char, + String, + Box, + Rc, + Path, + PathBuf, + NonZeroIsize, + NonZeroUsize, + NonZeroI8, + NonZeroU8, + NonZeroI16, + NonZeroU16, + NonZeroI32, + NonZeroU32, + NonZeroI64, + NonZeroU64, + NonZeroI128, + NonZeroU128, + AtomicBool, + AtomicIsize, + AtomicUsize, + AtomicI8, + AtomicU8, + AtomicI16, + AtomicU16, + AtomicI32, + AtomicU32, + AtomicI64, + AtomicU64 +]; + +impl Finalize for [T; N] {} +// SAFETY: +// All elements inside the array are correctly marked. +unsafe impl Trace for [T; N] { + custom_trace!(this, { + for v in this { + mark(v); + } + }); +} + +macro_rules! fn_finalize_trace_one { + ($ty:ty $(,$args:ident)*) => { + impl Finalize for $ty {} + // SAFETY: + // Function pointers don't have inner nodes that need to be marked. + unsafe impl Trace for $ty { empty_trace!(); } + } +} +macro_rules! fn_finalize_trace_group { + () => { + fn_finalize_trace_one!(extern "Rust" fn () -> Ret); + fn_finalize_trace_one!(extern "C" fn () -> Ret); + fn_finalize_trace_one!(unsafe extern "Rust" fn () -> Ret); + fn_finalize_trace_one!(unsafe extern "C" fn () -> Ret); + }; + ($($args:ident),*) => { + fn_finalize_trace_one!(extern "Rust" fn ($($args),*) -> Ret, $($args),*); + fn_finalize_trace_one!(extern "C" fn ($($args),*) -> Ret, $($args),*); + fn_finalize_trace_one!(extern "C" fn ($($args),*, ...) -> Ret, $($args),*); + fn_finalize_trace_one!(unsafe extern "Rust" fn ($($args),*) -> Ret, $($args),*); + fn_finalize_trace_one!(unsafe extern "C" fn ($($args),*) -> Ret, $($args),*); + fn_finalize_trace_one!(unsafe extern "C" fn ($($args),*, ...) -> Ret, $($args),*); + } +} + +macro_rules! tuple_finalize_trace { + () => {}; // This case is handled above, by simple_finalize_empty_trace!(). + ($($args:ident),*) => { + impl<$($args),*> Finalize for ($($args,)*) {} + // SAFETY: + // All elements inside the tuple are correctly marked. + unsafe impl<$($args: $crate::Trace),*> Trace for ($($args,)*) { + custom_trace!(this, { + #[allow(non_snake_case, unused_unsafe)] + fn avoid_lints<$($args: $crate::Trace),*>(&($(ref $args,)*): &($($args,)*)) { + // SAFETY: The implementor must ensure a correct implementation. + unsafe { $(mark($args);)* } + } + avoid_lints(this) + }); + } + } +} + +macro_rules! type_arg_tuple_based_finalize_trace_impls { + ($(($($args:ident),*);)*) => { + $( + fn_finalize_trace_group!($($args),*); + tuple_finalize_trace!($($args),*); + )* + } +} + +type_arg_tuple_based_finalize_trace_impls![ + (); + (A); + (A, B); + (A, B, C); + (A, B, C, D); + (A, B, C, D, E); + (A, B, C, D, E, F); + (A, B, C, D, E, F, G); + (A, B, C, D, E, F, G, H); + (A, B, C, D, E, F, G, H, I); + (A, B, C, D, E, F, G, H, I, J); + (A, B, C, D, E, F, G, H, I, J, K); + (A, B, C, D, E, F, G, H, I, J, K, L); +]; + +impl Finalize for Box {} +// SAFETY: The inner value of the `Box` is correctly marked. +unsafe impl Trace for Box { + custom_trace!(this, { + mark(&**this); + }); +} + +impl Finalize for Box<[T]> {} +// SAFETY: All the inner elements of the `Box` array are correctly marked. +unsafe impl Trace for Box<[T]> { + custom_trace!(this, { + for e in this.iter() { + mark(e); + } + }); +} + +impl Finalize for Vec {} +// SAFETY: All the inner elements of the `Vec` are correctly marked. +unsafe impl Trace for Vec { + custom_trace!(this, { + for e in this { + mark(e); + } + }); +} + +impl Finalize for Option {} +// SAFETY: The inner value of the `Option` is correctly marked. +unsafe impl Trace for Option { + custom_trace!(this, { + if let Some(ref v) = *this { + mark(v); + } + }); +} + +impl Finalize for Result {} +// SAFETY: Both inner values of the `Result` are correctly marked. +unsafe impl Trace for Result { + custom_trace!(this, { + match *this { + Ok(ref v) => mark(v), + Err(ref v) => mark(v), + } + }); +} + +impl Finalize for BinaryHeap {} +// SAFETY: All the elements of the `BinaryHeap` are correctly marked. +unsafe impl Trace for BinaryHeap { + custom_trace!(this, { + for v in this.iter() { + mark(v); + } + }); +} + +impl Finalize for BTreeMap {} +// SAFETY: All the elements of the `BTreeMap` are correctly marked. +unsafe impl Trace for BTreeMap { + custom_trace!(this, { + for (k, v) in this { + mark(k); + mark(v); + } + }); +} + +impl Finalize for BTreeSet {} +// SAFETY: All the elements of the `BTreeSet` are correctly marked. +unsafe impl Trace for BTreeSet { + custom_trace!(this, { + for v in this { + mark(v); + } + }); +} + +impl Finalize for HashMap {} +// SAFETY: All the elements of the `HashMap` are correctly marked. +unsafe impl Trace for HashMap { + custom_trace!(this, { + for (k, v) in this.iter() { + mark(k); + mark(v); + } + }); +} + +impl Finalize for HashSet {} +// SAFETY: All the elements of the `HashSet` are correctly marked. +unsafe impl Trace for HashSet { + custom_trace!(this, { + for v in this.iter() { + mark(v); + } + }); +} + +impl Finalize for LinkedList {} +// SAFETY: All the elements of the `LinkedList` are correctly marked. +unsafe impl Trace for LinkedList { + custom_trace!(this, { + for v in this.iter() { + mark(v); + } + }); +} + +impl Finalize for PhantomData {} +// SAFETY: A `PhantomData` doesn't have inner data that needs to be marked. +unsafe impl Trace for PhantomData { + empty_trace!(); +} + +impl Finalize for VecDeque {} +// SAFETY: All the elements of the `VecDeque` are correctly marked. +unsafe impl Trace for VecDeque { + custom_trace!(this, { + for v in this.iter() { + mark(v); + } + }); +} + +impl Finalize for Cow<'static, T> {} +// SAFETY: 'static references don't need to be traced, since they live indefinitely, and the owned +// variant is correctly marked. +unsafe impl Trace for Cow<'static, T> +where + T::Owned: Trace, +{ + custom_trace!(this, { + if let Cow::Owned(ref v) = this { + mark(v); + } + }); +} diff --git a/boa_macros/Cargo.toml b/boa_macros/Cargo.toml index d68aae7e97..048fd0f0d6 100644 --- a/boa_macros/Cargo.toml +++ b/boa_macros/Cargo.toml @@ -15,4 +15,5 @@ proc-macro = true [dependencies] quote = "1.0.21" syn = "1.0.103" - +proc-macro2 = "1.0" +synstructure = "0.12" diff --git a/boa_macros/src/lib.rs b/boa_macros/src/lib.rs index 205905786c..c1fc87fa6d 100644 --- a/boa_macros/src/lib.rs +++ b/boa_macros/src/lib.rs @@ -1,6 +1,7 @@ use proc_macro::TokenStream; use quote::quote; use syn::{parse_macro_input, LitStr}; +use synstructure::{decl_derive, AddBounds, Structure}; /// Construct a utf-16 array literal from a utf-8 [`str`] literal. #[proc_macro] @@ -13,3 +14,103 @@ pub fn utf16(input: TokenStream) -> TokenStream { } .into() } + +decl_derive!([Trace, attributes(unsafe_ignore_trace)] => derive_trace); + +fn derive_trace(mut s: Structure<'_>) -> proc_macro2::TokenStream { + s.filter(|bi| { + !bi.ast() + .attrs + .iter() + .any(|attr| attr.path.is_ident("unsafe_ignore_trace")) + }); + let trace_body = s.each(|bi| quote!(mark(#bi))); + + s.add_bounds(AddBounds::Fields); + let trace_impl = s.unsafe_bound_impl( + quote!(::boa_gc::Trace), + quote! { + #[inline] + unsafe fn trace(&self) { + #[allow(dead_code)] + #[inline] + fn mark(it: &T) { + unsafe { + ::boa_gc::Trace::trace(it); + } + } + match *self { #trace_body } + } + #[inline] + unsafe fn weak_trace(&self) { + #[allow(dead_code, unreachable_code)] + #[inline] + fn mark(it: &T) { + unsafe { + ::boa_gc::Trace::weak_trace(it) + } + } + match *self { #trace_body } + } + #[inline] + unsafe fn root(&self) { + #[allow(dead_code)] + #[inline] + fn mark(it: &T) { + unsafe { + ::boa_gc::Trace::root(it); + } + } + match *self { #trace_body } + } + #[inline] + unsafe fn unroot(&self) { + #[allow(dead_code)] + #[inline] + fn mark(it: &T) { + unsafe { + ::boa_gc::Trace::unroot(it); + } + } + match *self { #trace_body } + } + #[inline] + fn run_finalizer(&self) { + ::boa_gc::Finalize::finalize(self); + #[allow(dead_code)] + #[inline] + fn mark(it: &T) { + unsafe { + ::boa_gc::Trace::run_finalizer(it); + } + } + match *self { #trace_body } + } + }, + ); + + // We also implement drop to prevent unsafe drop implementations on this + // type and encourage people to use Finalize. This implementation will + // call `Finalize::finalize` if it is safe to do so. + let drop_impl = s.unbound_impl( + quote!(::std::ops::Drop), + quote! { + fn drop(&mut self) { + if ::boa_gc::finalizer_safe() { + ::boa_gc::Finalize::finalize(self); + } + } + }, + ); + + quote! { + #trace_impl + #drop_impl + } +} + +decl_derive!([Finalize] => derive_finalize); + +fn derive_finalize(s: Structure<'_>) -> proc_macro2::TokenStream { + s.unbound_impl(quote!(::boa_gc::Finalize), quote!()) +} diff --git a/boa_tester/Cargo.toml b/boa_tester/Cargo.toml index 2d55ff8aee..6f252beb77 100644 --- a/boa_tester/Cargo.toml +++ b/boa_tester/Cargo.toml @@ -25,6 +25,5 @@ regex = "1.7.0" once_cell = "1.16.0" colored = "2.0.0" fxhash = "0.2.1" -gc = { version = "0.4.1", features = ["derive"] } rayon = "1.5.3" anyhow = "1.0.66" diff --git a/boa_tester/src/exec/mod.rs b/boa_tester/src/exec/mod.rs index 4310c8219e..dc94889464 100644 --- a/boa_tester/src/exec/mod.rs +++ b/boa_tester/src/exec/mod.rs @@ -12,7 +12,7 @@ use boa_engine::{ builtins::JsArgs, object::FunctionBuilder, property::Attribute, Context, JsNativeErrorKind, JsResult, JsValue, }; -use boa_gc::{Cell, Finalize, Gc, Trace}; +use boa_gc::{Finalize, Gc, GcCell, Trace}; use boa_parser::Parser; use colored::Colorize; use rayon::prelude::*; @@ -400,13 +400,13 @@ impl Test { /// Object which includes the result of the async operation. #[derive(Debug, Clone, Trace, Finalize)] struct AsyncResult { - inner: Gc>>, + inner: Gc>>, } impl Default for AsyncResult { fn default() -> Self { Self { - inner: Gc::new(Cell::new(Ok(()))), + inner: Gc::new(GcCell::new(Ok(()))), } } }