From 98e6dd36cb28eec2c7c5447c8dbf71d147469180 Mon Sep 17 00:00:00 2001 From: Kevin Date: Mon, 14 Nov 2022 19:59:51 +0000 Subject: [PATCH] Boa Gc implementation draft (#2394) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Not sure if anyone else may be working on something more substantial/in-depth, but I thought I'd post this. 😄 The basic rundown is that this is more of an untested (and in some ways naïve) draft than anything else. It builds rather heavily on `rust-gc`, and tries to keep plenty of the core aspects so as to not break anything too much, and also to minimize overarching changes were it to actually be merged at some point. This implementation does add ~~a generational divide (although a little unoptimized) to the heap,~~ a GcAlloc/Collector struct with methods, and an ephemeron implementation that allows for the WeakPair and WeakGc pointers. --- Cargo.lock | 149 +++-- boa_engine/Cargo.toml | 1 - .../src/builtins/async_generator/mod.rs | 6 +- boa_engine/src/builtins/function/mod.rs | 10 +- boa_engine/src/builtins/generator/mod.rs | 4 +- boa_engine/src/builtins/promise/mod.rs | 4 +- boa_engine/src/bytecompiler/mod.rs | 4 +- boa_engine/src/environments/compile.rs | 8 +- boa_engine/src/environments/runtime.rs | 2 +- boa_engine/src/job.rs | 2 +- boa_engine/src/object/jsobject.rs | 14 +- boa_engine/src/realm.rs | 6 +- boa_engine/src/string/mod.rs | 4 +- boa_engine/src/symbol.rs | 4 +- boa_engine/src/vm/code_block.rs | 8 +- boa_examples/Cargo.toml | 1 - boa_gc/Cargo.toml | 3 +- boa_gc/src/cell.rs | 594 ++++++++++++++++++ boa_gc/src/internals/ephemeron_box.rs | 146 +++++ boa_gc/src/internals/gc_box.rs | 195 ++++++ boa_gc/src/internals/mod.rs | 5 + boa_gc/src/lib.rs | 385 +++++++++++- boa_gc/src/pointers/ephemeron.rs | 125 ++++ boa_gc/src/pointers/gc.rs | 275 ++++++++ boa_gc/src/pointers/mod.rs | 9 + boa_gc/src/pointers/weak.rs | 49 ++ boa_gc/src/test/allocation.rs | 31 + boa_gc/src/test/cell.rs | 15 + boa_gc/src/test/mod.rs | 37 ++ boa_gc/src/test/weak.rs | 133 ++++ boa_gc/src/trace.rs | 450 +++++++++++++ boa_macros/Cargo.toml | 3 +- boa_macros/src/lib.rs | 101 +++ boa_tester/Cargo.toml | 1 - boa_tester/src/exec/mod.rs | 6 +- 35 files changed, 2662 insertions(+), 128 deletions(-) create mode 100644 boa_gc/src/cell.rs create mode 100644 boa_gc/src/internals/ephemeron_box.rs create mode 100644 boa_gc/src/internals/gc_box.rs create mode 100644 boa_gc/src/internals/mod.rs create mode 100644 boa_gc/src/pointers/ephemeron.rs create mode 100644 boa_gc/src/pointers/gc.rs create mode 100644 boa_gc/src/pointers/mod.rs create mode 100644 boa_gc/src/pointers/weak.rs create mode 100644 boa_gc/src/test/allocation.rs create mode 100644 boa_gc/src/test/cell.rs create mode 100644 boa_gc/src/test/mod.rs create mode 100644 boa_gc/src/test/weak.rs create mode 100644 boa_gc/src/trace.rs diff --git a/Cargo.lock b/Cargo.lock index 96f665cea0d..1447e5da805 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 2d6fd774ee2..579e3e83946 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 e6ea7180641..768dfd48a62 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 5cc70deec57..ebd927fd795 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 74eea06fc13..7599dd90742 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 ae4ebef81e8..7bdf64cbc7a 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 33dec83bbc2..8bff029b4e8 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 37aa9a64ec4..1ff6f3faf78 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 3314eb75e51..c06ab965724 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 2a0988b2e57..f24be132a3a 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 d1afe8f99aa..6bcf077f9bd 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 588f01fd16c..ab8cf8dfbd9 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 2b6ea514eb8..2932e76b9b7 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 f7f44488294..c0b69c841f2 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 a8444e20725..02f52e097f2 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 28b1be49752..b6135a7dc82 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 5cca97b53be..ceaa195cdc6 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 00000000000..36607629347 --- /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 00000000000..420b7fb36c6 --- /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 00000000000..eaca4b48f79 --- /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 00000000000..005c000ade5 --- /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 ac24531793c..4b26855ea85 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 00000000000..35702a7b119 --- /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 00000000000..df44a8b327e --- /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 00000000000..5c14182762d --- /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 00000000000..26a1945efa2 --- /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 00000000000..27386999836 --- /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 00000000000..1e82acf1e7c --- /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 00000000000..559fbb8d80f --- /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 00000000000..5f1e33ccc19 --- /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 00000000000..354a0119d40 --- /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 d68aae7e972..048fd0f0d67 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 205905786c9..c1fc87fa6dd 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 2d55ff8aeeb..6f252beb774 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 4310c8219e0..dc948894646 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(()))), } } }