From c426e150e3b1675470e9743c360a2d6c5bd117ab Mon Sep 17 00:00:00 2001 From: masnagam Date: Mon, 20 Apr 2026 10:02:41 +0900 Subject: [PATCH 1/3] feat(jsgc-derive): add jsgc-derive --- Cargo.lock | 12 +++ libs/jsgc-derive/Cargo.toml | 26 ++++++ libs/jsgc-derive/README.md | 3 + libs/jsgc-derive/src/lib.rs | 99 ++++++++++++++++++++ libs/jsgc-derive/tests/integration_test.rs | 100 +++++++++++++++++++++ libs/jsgc/src/handle.rs | 25 +++++- libs/jsgc/src/heap.rs | 35 +------- libs/jsgc/src/lib.rs | 5 +- libs/jsgc/src/trace.rs | 50 +++++++++++ libs/jsgc/tests/integration_test.rs | 10 +-- libs/jsruntime/Cargo.toml | 1 + libs/jsruntime/src/lib.rs | 54 +++-------- libs/jsruntime/src/types/capture.rs | 12 +-- libs/jsruntime/src/types/closure.rs | 6 +- libs/jsruntime/src/types/coroutine.rs | 19 +--- libs/jsruntime/src/types/object.rs | 31 +++---- libs/jsruntime/src/types/promise.rs | 28 +++--- libs/jsruntime/src/types/string.rs | 11 +-- libs/jsruntime/src/types/value.rs | 14 +++ 19 files changed, 391 insertions(+), 150 deletions(-) create mode 100644 libs/jsgc-derive/Cargo.toml create mode 100644 libs/jsgc-derive/README.md create mode 100644 libs/jsgc-derive/src/lib.rs create mode 100644 libs/jsgc-derive/tests/integration_test.rs create mode 100644 libs/jsgc/src/trace.rs diff --git a/Cargo.lock b/Cargo.lock index ba536d8f9..f17cafd5b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1215,6 +1215,17 @@ dependencies = [ "rustc-hash", ] +[[package]] +name = "jsgc-derive" +version = "0.0.0" +dependencies = [ + "base", + "jsgc", + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "jsoncmp" version = "0.0.0" @@ -1259,6 +1270,7 @@ dependencies = [ "indexmap", "itertools 0.14.0", "jsgc", + "jsgc-derive", "jsparser", "logging", "paste", diff --git a/libs/jsgc-derive/Cargo.toml b/libs/jsgc-derive/Cargo.toml new file mode 100644 index 000000000..d8518574f --- /dev/null +++ b/libs/jsgc-derive/Cargo.toml @@ -0,0 +1,26 @@ +[package] +name = "jsgc-derive" +authors.workspace = true +edition.workspace = true +homepage.workspace = true +license.workspace = true +publish.workspace = true +repository.workspace = true +rust-version.workspace = true +version.workspace = true + +[dependencies] +base = { path = "../base" } +proc-macro2 = "1.0.106" +quote = "1.0.45" +syn = { version = "2.0.117", features = ["extra-traits"] } + +[dev-dependencies] +jsgc = { path = "../jsgc" } + +[lints] +workspace = true + +[lib] +bench = false +proc-macro = true diff --git a/libs/jsgc-derive/README.md b/libs/jsgc-derive/README.md new file mode 100644 index 000000000..f08eae027 --- /dev/null +++ b/libs/jsgc-derive/README.md @@ -0,0 +1,3 @@ +# jsgc-derive + +> Implementation of `derive(Trace)` diff --git a/libs/jsgc-derive/src/lib.rs b/libs/jsgc-derive/src/lib.rs new file mode 100644 index 000000000..37e880dcc --- /dev/null +++ b/libs/jsgc-derive/src/lib.rs @@ -0,0 +1,99 @@ +#[proc_macro_derive(Trace)] +pub fn derive_trace(input: proc_macro::TokenStream) -> proc_macro::TokenStream { + let input = syn::parse_macro_input!(input as syn::DeriveInput); + do_derive_trace(input).into() +} + +fn do_derive_trace(input: syn::DeriveInput) -> proc_macro2::TokenStream { + let ty_name = &input.ident; + let (impl_generics, ty_generics, where_clause) = input.generics.split_for_impl(); + + let body = trace_fields(&input.data); + + if body.is_empty() { + quote::quote! { + impl #impl_generics jsgc::Trace for #ty_name #ty_generics #where_clause { + #[inline] + fn trace(&self, _visits: &mut jsgc::VisitList) {} + } + } + } else { + quote::quote! { + impl #impl_generics jsgc::Trace for #ty_name #ty_generics #where_clause { + #[inline] + fn trace(&self, visits: &mut jsgc::VisitList) { + #body + } + } + } + } +} + +fn trace_fields(data: &syn::Data) -> proc_macro2::TokenStream { + match *data { + syn::Data::Struct(ref data) => match data.fields { + syn::Fields::Named(ref fields) => { + let recurse = fields.named.iter().map(|f| { + let name = &f.ident; + quote::quote! { + self.#name.trace(visits); + } + }); + quote::quote! { + #(#recurse)* + } + } + syn::Fields::Unnamed(_) => unimplemented!("unnamed fields are not supported yet"), + syn::Fields::Unit => quote::quote!(), + }, + syn::Data::Enum(_) => unimplemented!("enums are not supported yet"), + syn::Data::Union(_) => unreachable!("unions are not sypported"), + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_struct() { + let input: syn::DeriveInput = syn::parse_quote! { + struct Simple { + string: Handle, + object: HandleMut, + } + }; + + let actual = do_derive_trace(input); + + let expected = quote::quote! { + impl jsgc::Trace for Simple { + #[inline] + fn trace(&self, visits: &mut jsgc::VisitList) { + self.string.trace(visits); + self.object.trace(visits); + } + } + }; + + assert_eq!(actual.to_string(), expected.to_string()); + } + + #[test] + fn test_unit() { + let input: syn::DeriveInput = syn::parse_quote! { + struct Unit; + }; + + let actual = do_derive_trace(input); + + let expected = quote::quote! { + impl jsgc::Trace for Unit { + #[inline] + fn trace(&self, _visits: &mut jsgc::VisitList) {} + } + }; + + assert_eq!(actual.to_string(), expected.to_string()); + } +} diff --git a/libs/jsgc-derive/tests/integration_test.rs b/libs/jsgc-derive/tests/integration_test.rs new file mode 100644 index 000000000..df8319d38 --- /dev/null +++ b/libs/jsgc-derive/tests/integration_test.rs @@ -0,0 +1,100 @@ +use jsgc::HandleMut; +use jsgc::Heap; +use jsgc_derive::Trace; + +#[derive(Default, Trace)] +struct Cell { + car: Option>, + cdr: Option>, +} + +#[test] +fn test_collect_garbage() { + let mut heap = Heap::new(); + + macro_rules! cell { + () => { + heap.alloc_mut(Cell::default()) + }; + } + + macro_rules! cons { + ($car:expr, $cdr:expr,) => { + cons!($car, $cdr) + }; + ($car:expr, $cdr:expr) => {{ + let car = Some($car); + let cdr = Some($cdr); + heap.alloc_mut(Cell { car, cdr }) + }}; + } + + macro_rules! gc { + ($roots:expr) => { + heap.collect_garbage(&$roots) + }; + } + + macro_rules! num_objects { + () => { + heap.stats().num_objects + }; + } + + let cell = cell!(); + assert_eq!(num_objects!(), 1); + + let tree = cons!(cons!(cell!(), cell!()), cons!(cell!(), cell!())); + assert_eq!(num_objects!(), 8); + + let ring = { + let mut start = cell!(); + let mut mid = cell!(); + let mut end = cell!(); + start.cdr = Some(mid); + mid.cdr = Some(end); + end.cdr = Some(start); + start + }; + assert_eq!(num_objects!(), 11); + + gc!([cell.as_addr(), tree.as_addr(), ring.as_addr()]); + assert_eq!(num_objects!(), 11); + + gc!([tree.as_addr(), ring.as_addr()]); + assert_eq!(num_objects!(), 10); + + gc!([ring.as_addr()]); + assert_eq!(num_objects!(), 3); + + gc!([]); + assert_eq!(num_objects!(), 0); +} + +#[test] +fn test_unmanaged_tracing_targets() { + let mut heap = Heap::new(); + + let mut root = Cell::default(); // unmanaged + let mut root = HandleMut::from_mut(&mut root); + + // TODO(feat): not ergonomic + heap.add_tracer(root.into()); + + assert_eq!(heap.stats().num_objects, 0); + + root.car = Some(heap.alloc_mut(Cell::default())); + root.cdr = Some(heap.alloc_mut(Cell::default())); + assert_eq!(heap.stats().num_objects, 2); + + heap.collect_garbage(&[root.as_addr()]); + assert_eq!(heap.stats().num_objects, 2); + + // TODO(feat): not ergonomic + heap.remove_tracer(root.into()); + + heap.collect_garbage(&[]); + assert_eq!(heap.stats().num_objects, 0); + + // TODO(feat): UAF... root.[car|cdr] are still accessible. +} diff --git a/libs/jsgc/src/handle.rs b/libs/jsgc/src/handle.rs index e3ed3e518..3858d29a3 100644 --- a/libs/jsgc/src/handle.rs +++ b/libs/jsgc/src/handle.rs @@ -5,7 +5,9 @@ use std::ops::Deref; use std::ops::DerefMut; use std::ptr::NonNull; -use crate::heap::Atom; +use crate::trace::Atom; +use crate::trace::Trace; +use crate::trace::VisitList; /// A data type to hold a non-null pointer to an *immutable* data type managed on the heap memory. /// @@ -98,6 +100,13 @@ where } } +impl Trace for Handle { + #[inline] + fn trace(&self, visits: &mut VisitList) { + visits.push(self.as_addr()); + } +} + /// A data type to hold a non-null pointer to a *mutable* data type managed on the heap memory. /// /// This type treats the pointee type as an opaque type and simply copy the pointer when the value @@ -198,6 +207,13 @@ where } } +impl Trace for HandleMut { + #[inline] + fn trace(&self, visits: &mut VisitList) { + visits.push(self.as_addr()); + } +} + // An *immutable* sequence. #[derive(Debug)] #[repr(C)] @@ -206,6 +222,13 @@ pub struct Seq { pub len: usize, } +impl Trace for Seq { + #[inline] + fn trace(&self, visits: &mut VisitList) { + self.data.trace(visits); + } +} + #[cfg(test)] mod tests { use super::*; diff --git a/libs/jsgc/src/heap.rs b/libs/jsgc/src/heap.rs index 61c1f81cb..2f358af16 100644 --- a/libs/jsgc/src/heap.rs +++ b/libs/jsgc/src/heap.rs @@ -1,5 +1,4 @@ use std::alloc::Layout; -use std::collections::VecDeque; use std::ptr::NonNull; use rustc_hash::FxHashMap; @@ -8,6 +7,9 @@ use rustc_hash::FxHashSet; use crate::handle::Handle; use crate::handle::HandleMut; use crate::handle::Seq; +use crate::trace::Atom; +use crate::trace::Trace; +use crate::trace::VisitList; /// A heap memory managed by GC. pub struct Heap { @@ -263,30 +265,6 @@ impl GcState { } } -/// A list to which reachable objects will be added. -#[derive(Default)] -pub struct VisitList(VecDeque); - -impl VisitList { - /// Appends a handle to the back of the visit list. - pub fn push(&mut self, addr: usize) { - self.0.push_back(addr); - } - - /// Appends handles of an iterator. - pub fn extend(&mut self, iter: I) - where - I: IntoIterator, - { - self.0.extend(iter); - } - - /// Removes the first handle and returns it, or `None` if the visit list is empty. - fn pop(&mut self) -> Option { - self.0.pop_front() - } -} - struct MemoryBlock { layout: Layout, tidy_fn: Option, @@ -332,10 +310,3 @@ impl Tracer { type TidyFn = fn(usize); type TraceFn = fn(usize, &mut VisitList); - -pub trait Trace { - fn trace(&self, visits: &mut VisitList); -} - -pub trait Atom: Copy + Sized {} -impl Atom for u16 {} diff --git a/libs/jsgc/src/lib.rs b/libs/jsgc/src/lib.rs index 1ede22f32..0b0d51cea 100644 --- a/libs/jsgc/src/lib.rs +++ b/libs/jsgc/src/lib.rs @@ -1,10 +1,11 @@ mod handle; mod heap; +mod trace; pub use handle::Handle; pub use handle::HandleMut; pub use handle::Seq; pub use heap::Heap; pub use heap::Stats; -pub use heap::Trace; -pub use heap::VisitList; +pub use trace::Trace; +pub use trace::VisitList; diff --git a/libs/jsgc/src/trace.rs b/libs/jsgc/src/trace.rs new file mode 100644 index 000000000..535f20760 --- /dev/null +++ b/libs/jsgc/src/trace.rs @@ -0,0 +1,50 @@ +use std::collections::VecDeque; + +pub trait Trace { + fn trace(&self, visits: &mut VisitList); +} + +impl Trace for T { + #[inline] + fn trace(&self, _visits: &mut VisitList) {} +} + +impl Trace for Option { + #[inline] + fn trace(&self, visits: &mut VisitList) { + if let Some(v) = self { + v.trace(visits) + } + } +} + +pub trait Atom: Copy + Sized {} + +impl Atom for () {} +impl Atom for bool {} +impl Atom for u16 {} +impl Atom for u32 {} + +/// A list to which reachable objects will be added. +#[derive(Default)] +pub struct VisitList(VecDeque); + +impl VisitList { + /// Appends a handle to the back of the visit list. + pub fn push(&mut self, addr: usize) { + self.0.push_back(addr); + } + + /// Appends handles of an iterator. + pub fn extend(&mut self, iter: I) + where + I: IntoIterator, + { + self.0.extend(iter); + } + + /// Removes the first handle and returns it, or `None` if the visit list is empty. + pub(crate) fn pop(&mut self) -> Option { + self.0.pop_front() + } +} diff --git a/libs/jsgc/tests/integration_test.rs b/libs/jsgc/tests/integration_test.rs index 8332c5db1..cb9edf141 100644 --- a/libs/jsgc/tests/integration_test.rs +++ b/libs/jsgc/tests/integration_test.rs @@ -10,13 +10,9 @@ struct Cell { } impl Trace for Cell { - fn trace(&self, visit_list: &mut VisitList) { - if let Some(car) = self.car { - visit_list.push(car.as_addr()); - } - if let Some(cdr) = self.cdr { - visit_list.push(cdr.as_addr()); - } + fn trace(&self, visits: &mut VisitList) { + self.car.trace(visits); + self.cdr.trace(visits); } } diff --git a/libs/jsruntime/Cargo.toml b/libs/jsruntime/Cargo.toml index 70bf9ea64..ec6482cd7 100644 --- a/libs/jsruntime/Cargo.toml +++ b/libs/jsruntime/Cargo.toml @@ -19,6 +19,7 @@ cranelift-native = "0.130.0" indexmap = "2.13.1" itertools = "0.14.0" jsgc = { path = "../jsgc" } +jsgc-derive = { path = "../jsgc-derive" } jsparser = { path = "../jsparser", features = ["location"] } logging = { path = "../logging" } paste = "1.0.15" diff --git a/libs/jsruntime/src/lib.rs b/libs/jsruntime/src/lib.rs index 2fd85b0b9..76af6e384 100644 --- a/libs/jsruntime/src/lib.rs +++ b/libs/jsruntime/src/lib.rs @@ -528,46 +528,20 @@ impl Drop for Runtime { // TODO(feat): derive(Trace) impl Trace for Runtime { fn trace(&self, visits: &mut jsgc::VisitList) { - visits.push(self.global_object.as_addr()); - if let Some(object) = self.object_prototype { - visits.push(object.as_addr()); - } - if let Some(object) = self.function_prototype { - visits.push(object.as_addr()); - } - if let Some(object) = self.string_prototype { - visits.push(object.as_addr()); - } - if let Some(object) = self.promise_prototype { - visits.push(object.as_addr()); - } - if let Some(object) = self.error_prototype { - visits.push(object.as_addr()); - } - if let Some(object) = self.aggregate_error_prototype { - visits.push(object.as_addr()); - } - if let Some(object) = self.eval_error_prototype { - visits.push(object.as_addr()); - } - if let Some(object) = self.internal_error_prototype { - visits.push(object.as_addr()); - } - if let Some(object) = self.range_error_prototype { - visits.push(object.as_addr()); - } - if let Some(object) = self.reference_error_prototype { - visits.push(object.as_addr()); - } - if let Some(object) = self.syntax_error_prototype { - visits.push(object.as_addr()); - } - if let Some(object) = self.type_error_prototype { - visits.push(object.as_addr()); - } - if let Some(object) = self.uri_error_prototype { - visits.push(object.as_addr()); - } + self.global_object.trace(visits); + self.object_prototype.trace(visits); + self.function_prototype.trace(visits); + self.string_prototype.trace(visits); + self.promise_prototype.trace(visits); + self.error_prototype.trace(visits); + self.aggregate_error_prototype.trace(visits); + self.eval_error_prototype.trace(visits); + self.internal_error_prototype.trace(visits); + self.range_error_prototype.trace(visits); + self.reference_error_prototype.trace(visits); + self.syntax_error_prototype.trace(visits); + self.type_error_prototype.trace(visits); + self.uri_error_prototype.trace(visits); // TODO: tracing X if X implements Trace. } } diff --git a/libs/jsruntime/src/types/capture.rs b/libs/jsruntime/src/types/capture.rs index 945070940..c926cc5b1 100644 --- a/libs/jsruntime/src/types/capture.rs +++ b/libs/jsruntime/src/types/capture.rs @@ -50,15 +50,9 @@ impl std::fmt::Debug for Capture { } impl Trace for Capture { - fn trace(&self, visit_list: &mut VisitList) { - if !self.is_escaped() { - return; - } - - match self.escaped { - Value::String(string) => visit_list.push(string.as_addr()), - Value::Object(object) => visit_list.push(object.as_addr()), - _ => (), + fn trace(&self, visits: &mut VisitList) { + if self.is_escaped() { + self.escaped.trace(visits); } } } diff --git a/libs/jsruntime/src/types/closure.rs b/libs/jsruntime/src/types/closure.rs index b36dd424f..7a2592c2e 100644 --- a/libs/jsruntime/src/types/closure.rs +++ b/libs/jsruntime/src/types/closure.rs @@ -67,7 +67,9 @@ impl std::fmt::Debug for Closure { } impl Trace for Closure { - fn trace(&self, visit_list: &mut VisitList) { - visit_list.extend(self.captures().iter().map(|capture| capture.as_addr())); + fn trace(&self, visits: &mut VisitList) { + for capture in self.captures() { + capture.trace(visits); + } } } diff --git a/libs/jsruntime/src/types/coroutine.rs b/libs/jsruntime/src/types/coroutine.rs index 2e1a97760..5bc19580c 100644 --- a/libs/jsruntime/src/types/coroutine.rs +++ b/libs/jsruntime/src/types/coroutine.rs @@ -85,24 +85,13 @@ impl Coroutine { } impl Trace for Coroutine { - fn trace(&self, visit_list: &mut VisitList) { - visit_list.push(self.closure.as_addr()); - + fn trace(&self, visits: &mut VisitList) { + self.closure.trace(visits); for local in self.locals() { - match local { - Value::String(string) => visit_list.push(string.as_addr()), - Value::Object(object) => visit_list.push(object.as_addr()), - _ => (), - } + local.trace(visits); } - for value in self.scratch_buffer() { - match value { - Value::None => break, - Value::String(string) => visit_list.push(string.as_addr()), - Value::Object(object) => visit_list.push(object.as_addr()), - _ => (), - } + value.trace(visits); } } } diff --git a/libs/jsruntime/src/types/object.rs b/libs/jsruntime/src/types/object.rs index 07069fe4a..3e9b310ff 100644 --- a/libs/jsruntime/src/types/object.rs +++ b/libs/jsruntime/src/types/object.rs @@ -381,26 +381,14 @@ impl Display for Object { } impl Trace for Object { - fn trace(&self, visit_list: &mut VisitList) { - if self.kernel.tracing { - visit_list.push(self.kernel.data); - } - if let Some(prototype) = self.prototype { - visit_list.push(prototype.as_addr()); - } + fn trace(&self, visits: &mut VisitList) { + self.kernel.trace(visits); + self.prototype.trace(visits); for prop in self.properties.values() { - match prop.value() { - Value::String(string) => visit_list.push(string.as_addr()), - Value::Object(object) => visit_list.push(object.as_addr()), - _ => (), - } + prop.value().trace(visits); } for slot in self.slots.iter() { - match slot { - Value::String(string) => visit_list.push(string.as_addr()), - Value::Object(object) => visit_list.push(object.as_addr()), - _ => (), - } + slot.trace(visits); } } } @@ -416,6 +404,15 @@ impl Kernel { const TRACING_OFFSET: usize = std::mem::offset_of!(Self, tracing); } +impl Trace for Kernel { + #[inline] + fn trace(&self, visits: &mut VisitList) { + if self.tracing { + visits.push(self.data); + } + } +} + bitflags! { #[derive(Clone, Copy)] pub struct ObjectFlags: u8 { diff --git a/libs/jsruntime/src/types/promise.rs b/libs/jsruntime/src/types/promise.rs index dc446fb36..04f0197fd 100644 --- a/libs/jsruntime/src/types/promise.rs +++ b/libs/jsruntime/src/types/promise.rs @@ -5,6 +5,7 @@ use crate::types::Coroutine; use crate::types::Object; use crate::types::Value; +#[derive(jsgc_derive::Trace)] pub struct Promise { coroutine: HandleMut, awaiting: Option>, @@ -54,25 +55,20 @@ impl std::fmt::Debug for Promise { } } -impl Trace for Promise { - fn trace(&self, visits: &mut jsgc::VisitList) { - visits.push(self.coroutine.as_addr()); - if let Some(awaiting) = self.awaiting { - visits.push(awaiting.as_addr()); - } - match self.state { - PromiseState::Resolved(Value::String(string)) => visits.push(string.as_addr()), - PromiseState::Resolved(Value::Object(object)) => visits.push(object.as_addr()), - PromiseState::Rejected(Value::String(string)) => visits.push(string.as_addr()), - PromiseState::Rejected(Value::Object(object)) => visits.push(object.as_addr()), - _ => (), - } - } -} - #[derive(Debug)] enum PromiseState { Pending, Resolved(Value), Rejected(Value), } + +// TODO(jsgc-derive): derive(Trace) +impl Trace for PromiseState { + #[inline] + fn trace(&self, visits: &mut jsgc::VisitList) { + match self { + Self::Resolved(v) | Self::Rejected(v) => v.trace(visits), + _ => (), + } + } +} diff --git a/libs/jsruntime/src/types/string.rs b/libs/jsruntime/src/types/string.rs index 503652e3a..5f45f7d3f 100644 --- a/libs/jsruntime/src/types/string.rs +++ b/libs/jsruntime/src/types/string.rs @@ -7,8 +7,7 @@ use itertools::Itertools; use jsgc::Handle; use jsgc::Heap; use jsgc::Seq; -use jsgc::Trace; -use jsgc::VisitList; +use jsgc_derive::Trace; /// An empty string. pub const EMPTY: Handle = Handle::from_ref(&String::EMPTY); @@ -17,7 +16,7 @@ pub const EMPTY: Handle = Handle::from_ref(&String::EMPTY); pub const SPACE: Handle = Handle::from_ref(&String::SPACE); /// A data type representing an **immutable** UTF-16 code units. -#[derive(Clone)] +#[derive(Clone, Trace)] #[repr(C)] pub struct String { /// A pointer to the UTF-16 code unit sequence. @@ -302,12 +301,6 @@ impl std::fmt::Display for String { } } -impl Trace for String { - fn trace(&self, visit_list: &mut VisitList) { - visit_list.push(self.ptr.as_addr()); - } -} - // TODO(refactor) struct CodeUnits<'a> { string: &'a String, diff --git a/libs/jsruntime/src/types/value.rs b/libs/jsruntime/src/types/value.rs index 5c369153c..50bfe1bef 100644 --- a/libs/jsruntime/src/types/value.rs +++ b/libs/jsruntime/src/types/value.rs @@ -2,6 +2,8 @@ use std::ops::Deref; use jsgc::Handle; use jsgc::HandleMut; +use jsgc::Trace; +use jsgc::VisitList; use crate::Error; use crate::logger; @@ -168,3 +170,15 @@ impl std::fmt::Display for Value { } } } + +// TODO(jsgc-derive): derive(Trace) +impl Trace for Value { + #[inline] + fn trace(&self, visits: &mut VisitList) { + match self { + Self::String(string) => string.trace(visits), + Self::Object(object) => object.trace(visits), + _ => (), + } + } +} From 7b5eb840db87c5d7d75c632dbbd972be4366e4e2 Mon Sep 17 00:00:00 2001 From: masnagam Date: Sat, 25 Apr 2026 15:21:25 +0900 Subject: [PATCH 2/3] test(jsgc-derive): add tests --- Cargo.lock | 91 +++++++++++++++++++ libs/jsgc-derive/Cargo.toml | 1 + libs/jsgc-derive/tests/integration_test.rs | 100 --------------------- libs/jsgc-derive/tests/pass/boxed.rs | 14 +++ libs/jsgc-derive/tests/pass/option.rs | 14 +++ libs/jsgc-derive/tests/pass/primitive.rs | 24 +++++ libs/jsgc-derive/tests/pass/ref.rs | 25 ++++++ libs/jsgc-derive/tests/pass/struct.rs | 14 +++ libs/jsgc-derive/tests/pass/unit.rs | 9 ++ libs/jsgc-derive/tests/test_derive.rs | 5 ++ libs/jsgc/src/handle.rs | 20 ++++- libs/jsgc/src/heap.rs | 13 ++- libs/jsgc/src/trace.rs | 50 ++++++++--- 13 files changed, 262 insertions(+), 118 deletions(-) delete mode 100644 libs/jsgc-derive/tests/integration_test.rs create mode 100644 libs/jsgc-derive/tests/pass/boxed.rs create mode 100644 libs/jsgc-derive/tests/pass/option.rs create mode 100644 libs/jsgc-derive/tests/pass/primitive.rs create mode 100644 libs/jsgc-derive/tests/pass/ref.rs create mode 100644 libs/jsgc-derive/tests/pass/struct.rs create mode 100644 libs/jsgc-derive/tests/pass/unit.rs create mode 100644 libs/jsgc-derive/tests/test_derive.rs diff --git a/Cargo.lock b/Cargo.lock index f17cafd5b..e13f42485 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -883,6 +883,12 @@ dependencies = [ "stable_deref_trait", ] +[[package]] +name = "glob" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" + [[package]] name = "half" version = "2.7.1" @@ -1224,6 +1230,7 @@ dependencies = [ "proc-macro2", "quote", "syn", + "trybuild", ] [[package]] @@ -1937,6 +1944,15 @@ dependencies = [ "serde_core", ] +[[package]] +name = "serde_spanned" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6662b5879511e06e8999a8a235d848113e942c9124f211511b16466ee2995f26" +dependencies = [ + "serde_core", +] + [[package]] name = "serde_test" version = "1.0.177" @@ -2081,6 +2097,12 @@ version = "0.13.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "adb6935a6f5c20170eeceb1a3835a49e12e19d792f6dd344ccc76a985ca5a6ca" +[[package]] +name = "target-triple" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "591ef38edfb78ca4771ee32cf494cb8771944bee237a9b91fc9c1424ac4b777b" + [[package]] name = "tempfile" version = "3.27.0" @@ -2094,6 +2116,15 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "termcolor" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755" +dependencies = [ + "winapi-util", +] + [[package]] name = "terminal_size" version = "0.4.4" @@ -2215,6 +2246,45 @@ dependencies = [ "tokio", ] +[[package]] +name = "toml" +version = "1.1.2+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81f3d15e84cbcd896376e6730314d59fb5a87f31e4b038454184435cd57defee" +dependencies = [ + "indexmap", + "serde_core", + "serde_spanned", + "toml_datetime", + "toml_parser", + "toml_writer", + "winnow", +] + +[[package]] +name = "toml_datetime" +version = "1.1.1+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3165f65f62e28e0115a00b2ebdd37eb6f3b641855f9d636d3cd4103767159ad7" +dependencies = [ + "serde_core", +] + +[[package]] +name = "toml_parser" +version = "1.1.2+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2abe9b86193656635d2411dc43050282ca48aa31c2451210f4202550afb7526" +dependencies = [ + "winnow", +] + +[[package]] +name = "toml_writer" +version = "1.1.1+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "756daf9b1013ebe47a8776667b466417e2d4c5679d441c26230efd9ef78692db" + [[package]] name = "tower" version = "0.5.3" @@ -2352,6 +2422,21 @@ dependencies = [ "tracing-serde", ] +[[package]] +name = "trybuild" +version = "1.0.116" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47c635f0191bd3a2941013e5062667100969f8c4e9cd787c14f977265d73616e" +dependencies = [ + "glob", + "serde", + "serde_derive", + "serde_json", + "target-triple", + "termcolor", + "toml", +] + [[package]] name = "typenum" version = "1.19.0" @@ -2825,6 +2910,12 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" +[[package]] +name = "winnow" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ee1708bef14716a11bae175f579062d4554d95be2c6829f518df847b7b3fdd0" + [[package]] name = "wit-bindgen" version = "0.51.0" diff --git a/libs/jsgc-derive/Cargo.toml b/libs/jsgc-derive/Cargo.toml index d8518574f..0466a1f5b 100644 --- a/libs/jsgc-derive/Cargo.toml +++ b/libs/jsgc-derive/Cargo.toml @@ -17,6 +17,7 @@ syn = { version = "2.0.117", features = ["extra-traits"] } [dev-dependencies] jsgc = { path = "../jsgc" } +trybuild = "1.0.116" [lints] workspace = true diff --git a/libs/jsgc-derive/tests/integration_test.rs b/libs/jsgc-derive/tests/integration_test.rs deleted file mode 100644 index df8319d38..000000000 --- a/libs/jsgc-derive/tests/integration_test.rs +++ /dev/null @@ -1,100 +0,0 @@ -use jsgc::HandleMut; -use jsgc::Heap; -use jsgc_derive::Trace; - -#[derive(Default, Trace)] -struct Cell { - car: Option>, - cdr: Option>, -} - -#[test] -fn test_collect_garbage() { - let mut heap = Heap::new(); - - macro_rules! cell { - () => { - heap.alloc_mut(Cell::default()) - }; - } - - macro_rules! cons { - ($car:expr, $cdr:expr,) => { - cons!($car, $cdr) - }; - ($car:expr, $cdr:expr) => {{ - let car = Some($car); - let cdr = Some($cdr); - heap.alloc_mut(Cell { car, cdr }) - }}; - } - - macro_rules! gc { - ($roots:expr) => { - heap.collect_garbage(&$roots) - }; - } - - macro_rules! num_objects { - () => { - heap.stats().num_objects - }; - } - - let cell = cell!(); - assert_eq!(num_objects!(), 1); - - let tree = cons!(cons!(cell!(), cell!()), cons!(cell!(), cell!())); - assert_eq!(num_objects!(), 8); - - let ring = { - let mut start = cell!(); - let mut mid = cell!(); - let mut end = cell!(); - start.cdr = Some(mid); - mid.cdr = Some(end); - end.cdr = Some(start); - start - }; - assert_eq!(num_objects!(), 11); - - gc!([cell.as_addr(), tree.as_addr(), ring.as_addr()]); - assert_eq!(num_objects!(), 11); - - gc!([tree.as_addr(), ring.as_addr()]); - assert_eq!(num_objects!(), 10); - - gc!([ring.as_addr()]); - assert_eq!(num_objects!(), 3); - - gc!([]); - assert_eq!(num_objects!(), 0); -} - -#[test] -fn test_unmanaged_tracing_targets() { - let mut heap = Heap::new(); - - let mut root = Cell::default(); // unmanaged - let mut root = HandleMut::from_mut(&mut root); - - // TODO(feat): not ergonomic - heap.add_tracer(root.into()); - - assert_eq!(heap.stats().num_objects, 0); - - root.car = Some(heap.alloc_mut(Cell::default())); - root.cdr = Some(heap.alloc_mut(Cell::default())); - assert_eq!(heap.stats().num_objects, 2); - - heap.collect_garbage(&[root.as_addr()]); - assert_eq!(heap.stats().num_objects, 2); - - // TODO(feat): not ergonomic - heap.remove_tracer(root.into()); - - heap.collect_garbage(&[]); - assert_eq!(heap.stats().num_objects, 0); - - // TODO(feat): UAF... root.[car|cdr] are still accessible. -} diff --git a/libs/jsgc-derive/tests/pass/boxed.rs b/libs/jsgc-derive/tests/pass/boxed.rs new file mode 100644 index 000000000..07a4283e0 --- /dev/null +++ b/libs/jsgc-derive/tests/pass/boxed.rs @@ -0,0 +1,14 @@ +use jsgc::Handle; +use jsgc_derive::Trace; + +#[derive(Trace)] +struct A { + handle: Handle, +} + +#[derive(Trace)] +struct B { + a: Box, +} + +fn main() {} diff --git a/libs/jsgc-derive/tests/pass/option.rs b/libs/jsgc-derive/tests/pass/option.rs new file mode 100644 index 000000000..84feba8d0 --- /dev/null +++ b/libs/jsgc-derive/tests/pass/option.rs @@ -0,0 +1,14 @@ +use jsgc::Handle; +use jsgc_derive::Trace; + +#[derive(Trace)] +struct A { + handle: Handle, +} + +#[derive(Trace)] +struct B { + a: Option, +} + +fn main() {} diff --git a/libs/jsgc-derive/tests/pass/primitive.rs b/libs/jsgc-derive/tests/pass/primitive.rs new file mode 100644 index 000000000..2af6553b7 --- /dev/null +++ b/libs/jsgc-derive/tests/pass/primitive.rs @@ -0,0 +1,24 @@ +use jsgc_derive::Trace; + +#[derive(Trace)] +struct A { + t_unit: (), + t_bool: bool, + t_char: char, + t_f32: f32, + t_f64: f64, + t_i8: i8, + t_i16: i16, + t_i32: i32, + t_i64: i64, + t_i128: i128, + t_isize: isize, + t_u8: u8, + t_u16: u16, + t_u32: u32, + t_u64: u64, + t_u128: u128, + t_usize: usize, +} + +fn main() {} diff --git a/libs/jsgc-derive/tests/pass/ref.rs b/libs/jsgc-derive/tests/pass/ref.rs new file mode 100644 index 000000000..0fcebc854 --- /dev/null +++ b/libs/jsgc-derive/tests/pass/ref.rs @@ -0,0 +1,25 @@ +use jsgc_derive::Trace; + +#[derive(Trace)] +struct A<'a> { + t_unit: &'a (), + t_bool: &'a bool, + t_char: &'a char, + t_f32: &'a f32, + t_f64: &'a f64, + t_i8: &'a i8, + t_i16: &'a i16, + t_i32: &'a i32, + t_i64: &'a i64, + t_i128: &'a i128, + t_isize: &'a isize, + t_str: &'a str, + t_u8: &'a u8, + t_u16: &'a u16, + t_u32: &'a u32, + t_u64: &'a u64, + t_u128: &'a u128, + t_usize: &'a usize, +} + +fn main() {} diff --git a/libs/jsgc-derive/tests/pass/struct.rs b/libs/jsgc-derive/tests/pass/struct.rs new file mode 100644 index 000000000..4be981c6c --- /dev/null +++ b/libs/jsgc-derive/tests/pass/struct.rs @@ -0,0 +1,14 @@ +use jsgc::Handle; +use jsgc_derive::Trace; + +#[derive(Trace)] +struct A { + handle: Handle, +} + +#[derive(Trace)] +struct B { + a: A, +} + +fn main() {} diff --git a/libs/jsgc-derive/tests/pass/unit.rs b/libs/jsgc-derive/tests/pass/unit.rs new file mode 100644 index 000000000..00f470f16 --- /dev/null +++ b/libs/jsgc-derive/tests/pass/unit.rs @@ -0,0 +1,9 @@ +use jsgc_derive::Trace; + +#[derive(Trace)] +struct A; + +#[derive(Trace)] +struct B {} + +fn main() {} diff --git a/libs/jsgc-derive/tests/test_derive.rs b/libs/jsgc-derive/tests/test_derive.rs new file mode 100644 index 000000000..ce19b1422 --- /dev/null +++ b/libs/jsgc-derive/tests/test_derive.rs @@ -0,0 +1,5 @@ +#[test] +fn test_derive() { + let t = trybuild::TestCases::new(); + t.pass("tests/pass/*.rs"); +} diff --git a/libs/jsgc/src/handle.rs b/libs/jsgc/src/handle.rs index 3858d29a3..336a76eba 100644 --- a/libs/jsgc/src/handle.rs +++ b/libs/jsgc/src/handle.rs @@ -5,7 +5,6 @@ use std::ops::Deref; use std::ops::DerefMut; use std::ptr::NonNull; -use crate::trace::Atom; use crate::trace::Trace; use crate::trace::VisitList; @@ -217,15 +216,28 @@ impl Trace for HandleMut { // An *immutable* sequence. #[derive(Debug)] #[repr(C)] -pub struct Seq { +pub struct Seq { pub data: Handle, pub len: usize, } -impl Trace for Seq { +impl Seq { + pub fn as_slice(&self) -> &[T] { + // SAFETY: `self.data` holds a non-null valid address of an array of `T`. + unsafe { std::slice::from_raw_parts(self.data.as_ptr(), self.len) } + } +} + +impl Trace for Seq +where + T: Trace, +{ #[inline] fn trace(&self, visits: &mut VisitList) { - self.data.trace(visits); + // NOTE: The function body will be empty by optimization if `T::trace()` is empty. + for elem in self.as_slice().iter() { + elem.trace(visits); + } } } diff --git a/libs/jsgc/src/heap.rs b/libs/jsgc/src/heap.rs index 2f358af16..3c5c1115f 100644 --- a/libs/jsgc/src/heap.rs +++ b/libs/jsgc/src/heap.rs @@ -7,7 +7,6 @@ use rustc_hash::FxHashSet; use crate::handle::Handle; use crate::handle::HandleMut; use crate::handle::Seq; -use crate::trace::Atom; use crate::trace::Trace; use crate::trace::VisitList; @@ -36,6 +35,7 @@ impl Heap { where T: Sized + Trace, { + // SAFETY: `ptr` is a valid non-null pointer to `T`. let ptr = unsafe { // TODO(perf): use a dedicated memory pool let ptr = std::alloc::alloc(Layout::new::()) as *mut T; @@ -56,6 +56,7 @@ impl Heap { where T: Sized + Trace, { + // SAFETY: `ptr` is a valid non-null pointer to `T`. let ptr = unsafe { // TODO(perf): use a dedicated memory pool let ptr = std::alloc::alloc(Layout::new::()) as *mut T; @@ -77,6 +78,7 @@ impl Heap { T: Sized + Trace, F: FnOnce(NonNull), { + // SAFETY: `ptr` is a valid non-null pointer to `T`. let ptr = unsafe { // TODO(perf): use a dedicated memory pool NonNull::new(std::alloc::alloc(layout)).unwrap() @@ -89,6 +91,7 @@ impl Heap { self.trace_targets .insert(ptr.addr().get(), Tracer::new::()); + // SAFETY: `ptr` is a valid non-null pointer to `T`. Handle::from_ref(unsafe { ptr.cast::().as_ref() }) } @@ -98,6 +101,7 @@ impl Heap { T: Sized + Trace, F: FnOnce(NonNull), { + // SAFETY: `ptr` is a valid non-null pointer to `T`. let ptr = unsafe { // TODO(perf): use a dedicated memory pool NonNull::new(std::alloc::alloc(layout)).unwrap() @@ -110,17 +114,19 @@ impl Heap { self.trace_targets .insert(ptr.addr().get(), Tracer::new::()); + // SAFETY: `ptr` is a valid non-null pointer to `T`. HandleMut::from_mut(unsafe { ptr.cast::().as_mut() }) } pub fn alloc_seq(&mut self, src: &[T]) -> Seq where - T: Atom, + T: Copy + Sized + Trace, { let len = src.len(); let layout = Layout::array::(len).unwrap(); + // SAFETY: `ptr` is a valid non-null pointer to an array of `T`. let data = unsafe { // TODO(perf): use a dedicated memory pool let ptr = NonNull::new(std::alloc::alloc(layout)).unwrap(); @@ -137,11 +143,12 @@ impl Heap { pub fn alloc_seq_with_init(&mut self, len: usize, init: F) -> Seq where - T: Atom, + T: Trace, F: FnOnce(NonNull), { let layout = Layout::array::(len).unwrap(); + // SAFETY: `ptr` is a valid non-null pointer to an array of `T`. let (ptr, data) = unsafe { // TODO(perf): use a dedicated memory pool let ptr = NonNull::new(std::alloc::alloc(layout)).unwrap(); diff --git a/libs/jsgc/src/trace.rs b/libs/jsgc/src/trace.rs index 535f20760..4d49db952 100644 --- a/libs/jsgc/src/trace.rs +++ b/libs/jsgc/src/trace.rs @@ -4,12 +4,10 @@ pub trait Trace { fn trace(&self, visits: &mut VisitList); } -impl Trace for T { - #[inline] - fn trace(&self, _visits: &mut VisitList) {} -} - -impl Trace for Option { +impl Trace for Option +where + T: Trace, +{ #[inline] fn trace(&self, visits: &mut VisitList) { if let Some(v) = self { @@ -18,12 +16,42 @@ impl Trace for Option { } } -pub trait Atom: Copy + Sized {} +macro_rules! impl_trace_empty { + ($ty:ty) => { + impl Trace for $ty { + #[inline] + fn trace(&self, _visits: &mut VisitList) {} + } + }; + ($ty:ty, $($rest:ty),+) => { + impl_trace_empty! { $ty } + impl_trace_empty! { $($rest),+ } + }; + ($($valiadic:ty,)+) => { + impl_trace_empty! { $($valiadic),+ } + }; +} -impl Atom for () {} -impl Atom for bool {} -impl Atom for u16 {} -impl Atom for u32 {} +impl_trace_empty! { + (), + bool, + char, + f32, + f64, + i8, + i16, + i32, + i64, + i128, + isize, + str, + u8, + u16, + u32, + u64, + u128, + usize, +} /// A list to which reachable objects will be added. #[derive(Default)] From 40d5a63ea2c143ecf2c97a3dee0a4ffad76ff033 Mon Sep 17 00:00:00 2001 From: masnagam Date: Sat, 25 Apr 2026 16:07:20 +0900 Subject: [PATCH 3/3] test(jsgc-derive): add tests --- libs/jsgc-derive/tests/pass/boxed.rs | 1 + libs/jsgc-derive/tests/pass/result.rs | 19 +++++++++++ libs/jsgc-derive/tests/pass/seq.rs | 18 ++++++++++ libs/jsgc/src/trace.rs | 48 ++++++++++++++++++++++++--- 4 files changed, 82 insertions(+), 4 deletions(-) create mode 100644 libs/jsgc-derive/tests/pass/result.rs create mode 100644 libs/jsgc-derive/tests/pass/seq.rs diff --git a/libs/jsgc-derive/tests/pass/boxed.rs b/libs/jsgc-derive/tests/pass/boxed.rs index 07a4283e0..0e51d26e3 100644 --- a/libs/jsgc-derive/tests/pass/boxed.rs +++ b/libs/jsgc-derive/tests/pass/boxed.rs @@ -9,6 +9,7 @@ struct A { #[derive(Trace)] struct B { a: Box, + b: Box, } fn main() {} diff --git a/libs/jsgc-derive/tests/pass/result.rs b/libs/jsgc-derive/tests/pass/result.rs new file mode 100644 index 000000000..792e85013 --- /dev/null +++ b/libs/jsgc-derive/tests/pass/result.rs @@ -0,0 +1,19 @@ +use jsgc::Handle; +use jsgc_derive::Trace; + +#[derive(Trace)] +struct A { + handle: Handle, +} + +#[derive(Trace)] +struct B { + handle: Handle, +} + +#[derive(Trace)] +struct C { + result: Result, +} + +fn main() {} diff --git a/libs/jsgc-derive/tests/pass/seq.rs b/libs/jsgc-derive/tests/pass/seq.rs new file mode 100644 index 000000000..2efc138ad --- /dev/null +++ b/libs/jsgc-derive/tests/pass/seq.rs @@ -0,0 +1,18 @@ +use jsgc::Handle; +use jsgc::Seq; +use jsgc_derive::Trace; + +#[derive(Trace)] +struct A { + handle: Handle, +} + +#[derive(Trace)] +struct B<'a> { + a: [A; 4], + b: &'a [A], + c: Vec, + d: Seq, +} + +fn main() {} diff --git a/libs/jsgc/src/trace.rs b/libs/jsgc/src/trace.rs index 4d49db952..725fbcd81 100644 --- a/libs/jsgc/src/trace.rs +++ b/libs/jsgc/src/trace.rs @@ -4,18 +4,56 @@ pub trait Trace { fn trace(&self, visits: &mut VisitList); } -impl Trace for Option -where - T: Trace, -{ +impl Trace for Option { #[inline] fn trace(&self, visits: &mut VisitList) { + // NOTE: The function body will be empty by optimization if `T::trace()` is empty. if let Some(v) = self { v.trace(visits) } } } +impl Trace for Result { + #[inline] + fn trace(&self, visits: &mut VisitList) { + match self { + Ok(v) => v.trace(visits), + Err(v) => v.trace(visits), + } + } +} + +impl Trace for [T; N] { + #[inline] + fn trace(&self, visits: &mut VisitList) { + // NOTE: The function body will be empty by optimization if `T::trace()` is empty. + for elem in self.iter() { + elem.trace(visits); + } + } +} + +impl Trace for [T] { + #[inline] + fn trace(&self, visits: &mut VisitList) { + // NOTE: The function body will be empty by optimization if `T::trace()` is empty. + for elem in self.iter() { + elem.trace(visits); + } + } +} + +impl Trace for Vec { + #[inline] + fn trace(&self, visits: &mut VisitList) { + // NOTE: The function body will be empty by optimization if `T::trace()` is empty. + for elem in self.iter() { + elem.trace(visits); + } + } +} + macro_rules! impl_trace_empty { ($ty:ty) => { impl Trace for $ty { @@ -51,6 +89,8 @@ impl_trace_empty! { u64, u128, usize, + String, + // TODO: Add other types here } /// A list to which reachable objects will be added.