Browse Source

Boa Gc implementation draft (#2394)

<!---
Thank you for contributing to Boa! Please fill out the template below, and remove or add any
information as you feel necessary.
--->

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.
pull/2439/head
Kevin 2 years ago
parent
commit
98e6dd36cb
  1. 149
      Cargo.lock
  2. 1
      boa_engine/Cargo.toml
  3. 6
      boa_engine/src/builtins/async_generator/mod.rs
  4. 10
      boa_engine/src/builtins/function/mod.rs
  5. 4
      boa_engine/src/builtins/generator/mod.rs
  6. 4
      boa_engine/src/builtins/promise/mod.rs
  7. 4
      boa_engine/src/bytecompiler/mod.rs
  8. 8
      boa_engine/src/environments/compile.rs
  9. 2
      boa_engine/src/environments/runtime.rs
  10. 2
      boa_engine/src/job.rs
  11. 14
      boa_engine/src/object/jsobject.rs
  12. 6
      boa_engine/src/realm.rs
  13. 4
      boa_engine/src/string/mod.rs
  14. 4
      boa_engine/src/symbol.rs
  15. 8
      boa_engine/src/vm/code_block.rs
  16. 1
      boa_examples/Cargo.toml
  17. 3
      boa_gc/Cargo.toml
  18. 594
      boa_gc/src/cell.rs
  19. 146
      boa_gc/src/internals/ephemeron_box.rs
  20. 195
      boa_gc/src/internals/gc_box.rs
  21. 5
      boa_gc/src/internals/mod.rs
  22. 385
      boa_gc/src/lib.rs
  23. 125
      boa_gc/src/pointers/ephemeron.rs
  24. 275
      boa_gc/src/pointers/gc.rs
  25. 9
      boa_gc/src/pointers/mod.rs
  26. 49
      boa_gc/src/pointers/weak.rs
  27. 31
      boa_gc/src/test/allocation.rs
  28. 15
      boa_gc/src/test/cell.rs
  29. 37
      boa_gc/src/test/mod.rs
  30. 133
      boa_gc/src/test/weak.rs
  31. 450
      boa_gc/src/trace.rs
  32. 3
      boa_macros/Cargo.toml
  33. 101
      boa_macros/src/lib.rs
  34. 1
      boa_tester/Cargo.toml
  35. 6
      boa_tester/src/exec/mod.rs

149
Cargo.lock generated

@ -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",

1
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"

6
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<Gc<Cell<GeneratorContext>>>,
pub(crate) context: Option<Gc<GcCell<GeneratorContext>>>,
/// The `[[AsyncGeneratorQueue]]` internal slot.
pub(crate) queue: VecDeque<AsyncGeneratorRequest>,
@ -511,7 +511,7 @@ impl AsyncGenerator {
pub(crate) fn resume(
generator: &JsObject,
state: AsyncGeneratorState,
generator_context: &Gc<Cell<GeneratorContext>>,
generator_context: &Gc<GcCell<GeneratorContext>>,
completion: (JsResult<JsValue>, bool),
context: &mut Context,
) {

10
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<boa_gc::Cell<Box<dyn NativeObject>>>);
pub struct Captures(Gc<GcCell<Box<dyn NativeObject>>>);
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 NativeObject>, dyn Any> {
pub fn as_mut_any(&self) -> boa_gc::GcCellRefMut<'_, Box<dyn NativeObject>, dyn Any> {
RefMut::map(self.0.borrow_mut(), |data| data.deref_mut().as_mut_any())
}
}

4
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<Gc<Cell<GeneratorContext>>>,
pub(crate) context: Option<Gc<GcCell<GeneratorContext>>>,
}
impl BuiltIn for Generator {

4
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(),
}));

4
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<boa_gc::Cell<CompileTimeEnvironment>>,
environment: Gc<GcCell<CompileTimeEnvironment>>,
) -> usize {
let index = self.code_block.compile_environments.len();
self.code_block.compile_environments.push(environment);

8
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<Gc<Cell<Self>>>,
outer: Option<Gc<GcCell<Self>>>,
environment_index: usize,
#[unsafe_ignore_trace]
bindings: FxHashMap<Identifier, CompileTimeBinding>,
@ -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<Cell<CompileTimeEnvironment>>) {
) -> (usize, Gc<GcCell<CompileTimeEnvironment>>) {
let current_env_borrow = self.realm.compile_env.borrow();
if let Some(outer) = &current_env_borrow.outer {
let outer_clone = outer.clone();

2
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;

2
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
///

14
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<boa_gc::Cell<Object>>,
inner: Gc<GcCell<Object>>,
}
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<boa_gc::Cell<Object>> for JsObject {
impl AsRef<GcCell<Object>> for JsObject {
#[inline]
fn as_ref(&self) -> &boa_gc::Cell<Object> {
fn as_ref(&self) -> &GcCell<Object> {
&self.inner
}
}

6
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<Cell<CompileTimeEnvironment>>,
pub(crate) compile_env: Gc<GcCell<CompileTimeEnvironment>>,
}
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,

4
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 {

4
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 {

8
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<BindingLocator>,
/// Compile time environments in this function.
pub(crate) compile_environments: Vec<Gc<Cell<CompileTimeEnvironment>>>,
pub(crate) compile_environments: Vec<Gc<GcCell<CompileTimeEnvironment>>>,
/// 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,

1
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"

3
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 }

594
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<T>`.
pub struct GcCell<T: ?Sized + 'static> {
pub(crate) flags: Cell<BorrowFlag>,
pub(crate) cell: UnsafeCell<T>,
}
impl<T: Trace> GcCell<T> {
/// 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<T: Trace + ?Sized> GcCell<T> {
/// 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<GcCellRef<'_, T>, 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<GcCellRefMut<'_, T>, 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<T> 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<T> already borrowed", f)
}
}
impl<T: Trace + ?Sized> Finalize for GcCell<T> {}
// 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<T: Trace + ?Sized> Trace for GcCell<T> {
#[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<T>`.
pub struct GcCellRef<'a, T: ?Sized + 'static> {
pub(crate) flags: &'a Cell<BorrowFlag>,
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<U, F>(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<U, V, F>(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<T>`.
pub struct GcCellRefMut<'a, T: Trace + ?Sized + 'static, U: ?Sized = T> {
pub(crate) gc_cell: &'a GcCell<T>,
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<V, F>(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<T> tracks it's `BorrowState` is `Writing`
unsafe impl<T: ?Sized + Send> Send for GcCell<T> {}
impl<T: Trace + Clone> Clone for GcCell<T> {
#[inline]
fn clone(&self) -> Self {
Self::new(self.borrow().clone())
}
}
impl<T: Trace + Default> Default for GcCell<T> {
#[inline]
fn default() -> Self {
Self::new(Default::default())
}
}
#[allow(clippy::inline_always)]
impl<T: Trace + ?Sized + PartialEq> PartialEq for GcCell<T> {
#[inline(always)]
fn eq(&self, other: &Self) -> bool {
*self.borrow() == *other.borrow()
}
}
impl<T: Trace + ?Sized + Eq> Eq for GcCell<T> {}
#[allow(clippy::inline_always)]
impl<T: Trace + ?Sized + PartialOrd> PartialOrd for GcCell<T> {
#[inline(always)]
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
(*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<T: Trace + ?Sized + Ord> Ord for GcCell<T> {
#[inline]
fn cmp(&self, other: &Self) -> Ordering {
(*self.borrow()).cmp(&*other.borrow())
}
}
impl<T: Trace + ?Sized + Debug> Debug for GcCell<T> {
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", &"<borrowed>")
.finish(),
}
}
}

146
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<K: Trace + ?Sized + 'static, V: Trace + ?Sized + 'static> {
key: Cell<Option<NonNull<GcBox<K>>>>,
value: V,
}
impl<K: Trace + ?Sized, V: Trace> EphemeronBox<K, V> {
pub(crate) fn new(key: &Gc<K>, value: V) -> Self {
Self {
key: Cell::new(Some(key.inner_ptr())),
value,
}
}
}
impl<K: Trace + ?Sized, V: Trace + ?Sized> EphemeronBox<K, V> {
/// 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<K>> {
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<K>> {
// 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<K: Trace + ?Sized, V: Trace + ?Sized> Finalize for EphemeronBox<K, V> {
#[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<K: Trace + ?Sized, V: Trace + ?Sized> Trace for EphemeronBox<K, V> {
#[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);
}
}

195
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<usize>,
pub(crate) next: Cell<Option<NonNull<GcBox<dyn Trace>>>>,
}
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<T: Trace + ?Sized + 'static> {
pub(crate) header: GcBoxHeader,
pub(crate) value: T,
}
impl<T: Trace> GcBox<T> {
/// 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<T: Trace + ?Sized> GcBox<T> {
/// 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()
}
}

5
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;

385
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<GcBox<dyn Trace>>;
thread_local!(static EPHEMERON_QUEUE: Cell<Option<Vec<GcPointer>>> = Cell::new(None));
thread_local!(static GC_DROPPING: Cell<bool> = Cell::new(false));
thread_local!(static BOA_GC: RefCell<BoaGc> = 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<Option<GcPointer>>,
}
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<T>` 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<T: Trace>(value: GcBox<T>) -> NonNull<GcBox<T>> {
let _timer = Profiler::global().start_event("New Pointer", "BoaAlloc");
let element_size = mem::size_of_val::<GcBox<T>>(&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<T>
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<Option<NonNull<GcBox<dyn Trace>>>>) -> Vec<NonNull<GcBox<dyn Trace>>> {
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<NonNull<GcBox<dyn Trace>>>,
) -> Vec<NonNull<GcBox<dyn Trace>>> {
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<NonNull<GcBox<dyn Trace>>>) {
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<Option<NonNull<GcBox<dyn Trace>>>>,
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::<GcBox<_>>(&*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;

125
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<K: Trace + ?Sized + 'static, V: Trace + 'static> {
inner_ptr: Cell<NonNull<GcBox<EphemeronBox<K, V>>>>,
}
impl<K: Trace + ?Sized, V: Trace> Ephemeron<K, V> {
/// Creates a new `Ephemeron`.
pub fn new(key: &Gc<K>, value: V) -> Self {
Self {
inner_ptr: Cell::new(Allocator::allocate(GcBox::new_weak(EphemeronBox::new(
key, value,
)))),
}
}
}
impl<K: Trace + ?Sized, V: Trace> Ephemeron<K, V> {
#[inline]
fn inner_ptr(&self) -> NonNull<GcBox<EphemeronBox<K, V>>> {
self.inner_ptr.get()
}
#[inline]
fn inner(&self) -> &GcBox<EphemeronBox<K, V>> {
// SAFETY: GcBox<EphemeronBox<K,V>> 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<Gc<K>> {
// 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<K: Trace, V: Trace> Finalize for Ephemeron<K, V> {}
// SAFETY: Ephemerons trace implementation is standard for everything except `Trace::weak_trace()`,
// which pushes the GcBox<EphemeronBox<_>> onto the EphemeronQueue
unsafe impl<K: Trace, V: Trace> Trace for Ephemeron<K, V> {
#[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<K: Trace + ?Sized, V: Trace> Clone for Ephemeron<K, V> {
#[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<K: Trace + ?Sized, V: Trace> Drop for Ephemeron<K, V> {
#[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();
}
}
}

275
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<T: ?Sized, U>(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::<u8>());
}
ptr
}
/// A garbage-collected pointer type over an immutable value.
pub struct Gc<T: Trace + ?Sized + 'static> {
pub(crate) inner_ptr: Cell<NonNull<GcBox<T>>>,
pub(crate) marker: PhantomData<Rc<T>>,
}
impl<T: Trace> Gc<T> {
/// Constructs a new `Gc<T>` 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<T: Trace + ?Sized> Gc<T> {
/// 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<GcBox<T>>) -> 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<T: ?Sized + Trace>(
ptr: NonNull<GcBox<T>>,
) -> NonNull<GcBox<T>> {
let ptr = ptr.as_ptr();
let data = ptr.cast::<u8>();
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<T: Trace + ?Sized> Gc<T> {
fn rooted(&self) -> bool {
self.inner_ptr.get().as_ptr().cast::<u8>() as usize & 1 != 0
}
pub(crate) fn set_root(&self) {
let ptr = self.inner_ptr.get().as_ptr();
let data = ptr.cast::<u8>();
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<GcBox<T>> {
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<T> {
// SAFETY: Please see Gc::inner_ptr()
unsafe { self.inner_ptr().as_ref() }
}
}
impl<T: Trace + ?Sized> Finalize for Gc<T> {}
// 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<T: Trace + ?Sized> Trace for Gc<T> {
#[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<T>");
// 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<T>");
// 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<T: Trace + ?Sized> Clone for Gc<T> {
#[inline]
fn clone(&self) -> Self {
Self::from_ptr(self.inner_ptr())
}
}
impl<T: Trace + ?Sized> Deref for Gc<T> {
type Target = T;
#[inline]
fn deref(&self) -> &T {
self.inner().value()
}
}
impl<T: Trace + ?Sized> Drop for Gc<T> {
#[inline]
fn drop(&mut self) {
// If this pointer was a root, we should unroot it.
if self.rooted() {
self.inner().unroot_inner();
}
}
}
impl<T: Trace + Default> Default for Gc<T> {
#[inline]
fn default() -> Self {
Self::new(Default::default())
}
}
#[allow(clippy::inline_always)]
impl<T: Trace + ?Sized + PartialEq> PartialEq for Gc<T> {
#[inline(always)]
fn eq(&self, other: &Self) -> bool {
**self == **other
}
}
impl<T: Trace + ?Sized + Eq> Eq for Gc<T> {}
#[allow(clippy::inline_always)]
impl<T: Trace + ?Sized + PartialOrd> PartialOrd for Gc<T> {
#[inline(always)]
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
(**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<T: Trace + ?Sized + Ord> Ord for Gc<T> {
#[inline]
fn cmp(&self, other: &Self) -> Ordering {
(**self).cmp(&**other)
}
}
impl<T: Trace + ?Sized + Hash> Hash for Gc<T> {
fn hash<H: Hasher>(&self, state: &mut H) {
(**self).hash(state);
}
}
impl<T: Trace + ?Sized + Display> Display for Gc<T> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
Display::fmt(&**self, f)
}
}
impl<T: Trace + ?Sized + Debug> Debug for Gc<T> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
Debug::fmt(&**self, f)
}
}
impl<T: Trace + ?Sized> fmt::Pointer for Gc<T> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
fmt::Pointer::fmt(&self.inner(), f)
}
}
impl<T: Trace + ?Sized> std::borrow::Borrow<T> for Gc<T> {
fn borrow(&self) -> &T {
self
}
}
impl<T: Trace + ?Sized> AsRef<T> for Gc<T> {
fn as_ref(&self) -> &T {
self
}
}

9
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;

49
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<T: Trace + ?Sized + 'static> {
inner: Ephemeron<T, ()>,
}
impl<T: Trace + ?Sized> WeakGc<T> {
/// Creates a new weak pointer for a garbage collected value.
pub fn new(value: &Gc<T>) -> Self {
Self {
inner: Ephemeron::new(value, ()),
}
}
}
impl<T: Trace + ?Sized> WeakGc<T> {
#[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<Gc<T>> {
self.inner.upgrade_key()
}
}
impl<T: Trace + ?Sized> Clone for WeakGc<T> {
#[inline]
fn clone(&self) -> Self {
Self {
inner: self.inner.clone(),
}
}
}
impl<T: Trace + ?Sized> From<Ephemeron<T, ()>> for WeakGc<T> {
fn from(inner: Ephemeron<T, ()>) -> Self {
Self { inner }
}
}

31
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();
});
}

15
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);
}
});
}

37
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();
}

133
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")
);
});
}

450
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<T: $crate::Trace + ?Sized>(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<T: $crate::Trace + ?Sized>(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<T: $crate::Trace + ?Sized>(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<T: $crate::Trace + ?Sized>(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<T: $crate::Trace + ?Sized>(it: &T) {
$crate::Trace::run_finalizer(it);
}
$crate::Finalize::finalize(self);
let $this = self;
$body
}
};
}
impl<T: ?Sized> Finalize for &'static T {}
// SAFETY: 'static references don't need to be traced, since they live indefinitely.
unsafe impl<T: ?Sized> 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<str>,
Rc<str>,
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<T: Trace, const N: usize> Finalize for [T; N] {}
// SAFETY:
// All elements inside the array are correctly marked.
unsafe impl<T: Trace, const N: usize> Trace for [T; N] {
custom_trace!(this, {
for v in this {
mark(v);
}
});
}
macro_rules! fn_finalize_trace_one {
($ty:ty $(,$args:ident)*) => {
impl<Ret $(,$args)*> Finalize for $ty {}
// SAFETY:
// Function pointers don't have inner nodes that need to be marked.
unsafe impl<Ret $(,$args)*> 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<T: Trace + ?Sized> Finalize for Box<T> {}
// SAFETY: The inner value of the `Box` is correctly marked.
unsafe impl<T: Trace + ?Sized> Trace for Box<T> {
custom_trace!(this, {
mark(&**this);
});
}
impl<T: Trace> Finalize for Box<[T]> {}
// SAFETY: All the inner elements of the `Box` array are correctly marked.
unsafe impl<T: Trace> Trace for Box<[T]> {
custom_trace!(this, {
for e in this.iter() {
mark(e);
}
});
}
impl<T: Trace> Finalize for Vec<T> {}
// SAFETY: All the inner elements of the `Vec` are correctly marked.
unsafe impl<T: Trace> Trace for Vec<T> {
custom_trace!(this, {
for e in this {
mark(e);
}
});
}
impl<T: Trace> Finalize for Option<T> {}
// SAFETY: The inner value of the `Option` is correctly marked.
unsafe impl<T: Trace> Trace for Option<T> {
custom_trace!(this, {
if let Some(ref v) = *this {
mark(v);
}
});
}
impl<T: Trace, E: Trace> Finalize for Result<T, E> {}
// SAFETY: Both inner values of the `Result` are correctly marked.
unsafe impl<T: Trace, E: Trace> Trace for Result<T, E> {
custom_trace!(this, {
match *this {
Ok(ref v) => mark(v),
Err(ref v) => mark(v),
}
});
}
impl<T: Ord + Trace> Finalize for BinaryHeap<T> {}
// SAFETY: All the elements of the `BinaryHeap` are correctly marked.
unsafe impl<T: Ord + Trace> Trace for BinaryHeap<T> {
custom_trace!(this, {
for v in this.iter() {
mark(v);
}
});
}
impl<K: Trace, V: Trace> Finalize for BTreeMap<K, V> {}
// SAFETY: All the elements of the `BTreeMap` are correctly marked.
unsafe impl<K: Trace, V: Trace> Trace for BTreeMap<K, V> {
custom_trace!(this, {
for (k, v) in this {
mark(k);
mark(v);
}
});
}
impl<T: Trace> Finalize for BTreeSet<T> {}
// SAFETY: All the elements of the `BTreeSet` are correctly marked.
unsafe impl<T: Trace> Trace for BTreeSet<T> {
custom_trace!(this, {
for v in this {
mark(v);
}
});
}
impl<K: Eq + Hash + Trace, V: Trace, S: BuildHasher> Finalize for HashMap<K, V, S> {}
// SAFETY: All the elements of the `HashMap` are correctly marked.
unsafe impl<K: Eq + Hash + Trace, V: Trace, S: BuildHasher> Trace for HashMap<K, V, S> {
custom_trace!(this, {
for (k, v) in this.iter() {
mark(k);
mark(v);
}
});
}
impl<T: Eq + Hash + Trace, S: BuildHasher> Finalize for HashSet<T, S> {}
// SAFETY: All the elements of the `HashSet` are correctly marked.
unsafe impl<T: Eq + Hash + Trace, S: BuildHasher> Trace for HashSet<T, S> {
custom_trace!(this, {
for v in this.iter() {
mark(v);
}
});
}
impl<T: Eq + Hash + Trace> Finalize for LinkedList<T> {}
// SAFETY: All the elements of the `LinkedList` are correctly marked.
unsafe impl<T: Eq + Hash + Trace> Trace for LinkedList<T> {
custom_trace!(this, {
for v in this.iter() {
mark(v);
}
});
}
impl<T> Finalize for PhantomData<T> {}
// SAFETY: A `PhantomData` doesn't have inner data that needs to be marked.
unsafe impl<T> Trace for PhantomData<T> {
empty_trace!();
}
impl<T: Trace> Finalize for VecDeque<T> {}
// SAFETY: All the elements of the `VecDeque` are correctly marked.
unsafe impl<T: Trace> Trace for VecDeque<T> {
custom_trace!(this, {
for v in this.iter() {
mark(v);
}
});
}
impl<T: ToOwned + Trace + ?Sized> 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<T: ToOwned + Trace + ?Sized> Trace for Cow<'static, T>
where
T::Owned: Trace,
{
custom_trace!(this, {
if let Cow::Owned(ref v) = this {
mark(v);
}
});
}

3
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"

101
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<T: ::boa_gc::Trace + ?Sized>(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<T: ::boa_gc::Trace + ?Sized>(it: &T) {
unsafe {
::boa_gc::Trace::weak_trace(it)
}
}
match *self { #trace_body }
}
#[inline]
unsafe fn root(&self) {
#[allow(dead_code)]
#[inline]
fn mark<T: ::boa_gc::Trace + ?Sized>(it: &T) {
unsafe {
::boa_gc::Trace::root(it);
}
}
match *self { #trace_body }
}
#[inline]
unsafe fn unroot(&self) {
#[allow(dead_code)]
#[inline]
fn mark<T: ::boa_gc::Trace + ?Sized>(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<T: ::boa_gc::Trace + ?Sized>(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!())
}

1
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"

6
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<Cell<Result<(), String>>>,
inner: Gc<GcCell<Result<(), String>>>,
}
impl Default for AsyncResult {
fn default() -> Self {
Self {
inner: Gc::new(Cell::new(Ok(()))),
inner: Gc::new(GcCell::new(Ok(()))),
}
}
}

Loading…
Cancel
Save