From 9b1432f047c06ed7749153cfde186a9fa651f4ad Mon Sep 17 00:00:00 2001 From: TryAngle <45734252+TriedAngle@users.noreply.github.com> Date: Mon, 23 Feb 2026 15:31:53 +0100 Subject: [PATCH 1/3] Add tagged heap overflow chunks for HandleSet capacity --- vm/HANDLESET_SKETCH.md | 168 ++++++++++++++++++++ vm/src/handles.rs | 346 +++++++++++++++++++++++++++++++++++++++++ vm/src/lib.rs | 15 ++ vm/src/special.rs | 1 + 4 files changed, 530 insertions(+) create mode 100644 vm/HANDLESET_SKETCH.md create mode 100644 vm/src/handles.rs diff --git a/vm/HANDLESET_SKETCH.md b/vm/HANDLESET_SKETCH.md new file mode 100644 index 0000000..c8b94c0 --- /dev/null +++ b/vm/HANDLESET_SKETCH.md @@ -0,0 +1,168 @@ +# HandleSet / Handle design sketch for `VM` (revised) + +This revises the initial sketch with the following constraints: + +- `Handle` and `Tagged` stay **separate** types. +- `Handle` has an extra indirection but is **safe to dereference**. +- Handles are **copyable**, but only within the lifetime/scope of their `HandleSet`. +- Clarify overflow policy for stack-allocated sets. +- Clarify whether `next` is sufficient for disjoint handle-set trees. + +## Goals + +- Keep temporary object references safe across allocation + GC points. +- Make handle scopes cheap to create on the stack. +- Support nested scopes linked together. +- Let `VM` expose active handle roots to GC via `RootProvider::visit_roots`. + +## Key type split: `Tagged` vs `Handle` + +- `Tagged` remains a plain typed `Value` wrapper (no rooting). +- `Handle` is a rooted, indirect reference to a slot in a live `HandleSet`. +- They should not be unified. + +`Handle` is for values that must survive safepoints/GC relocation while remaining ergonomic to access. + +## Runtime model + +### 1) VM owns **multiple** handle-set roots + +To support potentially disjoint trees, store root heads in the VM: + +```rust +pub struct VMProxy { + // ...existing fields... + handleset_roots: Vec<*mut HandleSet>, +} +``` + +Why a vector: + +- A single `next` pointer only encodes one linked chain. +- If you allow creating top-level handle sets independently (not strictly nested from one active top), you can form multiple disjoint trees/chains. +- VM root scanning should iterate all root heads. + +### 2) Stack-allocated `HandleSet` + +```rust +const HANDLESET_CAPACITY: usize = 20; + +pub struct HandleSet<'vm> { + vm: &'vm mut VMProxy, + parent: Option<*mut HandleSet<'vm>>, + len: u8, + slots: [Value; HANDLESET_CAPACITY], +} +``` + +Notes: + +- `parent` links a child scope to its parent scope. +- A top-level scope created directly from VM has `parent = None` and is registered in `vm.handleset_roots`. +- A child scope created from an existing scope is linked via `parent = Some(...)` and does not need to be added as a separate VM root head. + +### 3) Copyable, scope-bounded `Handle` + +```rust +#[derive(Clone, Copy)] +pub struct Handle<'hs, T> { + slot: *mut Value, + _scope: PhantomData<&'hs HandleSet<'hs>>, + _type: PhantomData<*const T>, +} +``` + +- `Clone + Copy`: yes. +- `'hs` ties the handle lifetime to the originating handle scope. +- This allows copying handles freely **within scope**, while preventing escape. + +## API shape + +### Creating scopes + +Two entry points: + +1. Top-level scope from VM: + - `let mut hs = vm.new_handleset();` + - registers `hs` in `vm.handleset_roots`. +2. Child scope from existing scope: + - `let mut child = hs.child();` + - links child to parent via `parent`. + +Both are stack values with RAII drop behavior. + +### Inserting handles + +```rust +impl<'vm> HandleSet<'vm> { + pub fn pin<'hs, T>(&'hs mut self, value: Value) -> Handle<'hs, T>; +} +``` + +- Stores value in next free slot. +- Returns copyable handle to that slot. + +### Safe handle operations + +```rust +impl<'hs, T> Handle<'hs, T> { + pub fn get(self) -> Value; + pub fn set(self, value: Value); + + // typed safe accessors + pub fn as_ref(self) -> Option<&'hs T>; + pub fn as_mut(self) -> Option<&'hs mut T>; +} +``` + +Design intent: + +- No `unsafe` required by callers for normal dereference/access. +- Internally, `Handle` may use unsafe pointer operations, but API checks tag/kind and returns `Option` (or `Result`) for invalid type/tag. + +## Overflow policy (stack allocated sets) + +For v1: **no overflow chaining**. + +- Each `HandleSet` has hard capacity 20. +- On overflow, return an error or panic in debug builds. +- Users needing more than 20 simultaneously can explicitly open child scopes. + +Why: + +- True overflow chaining usually implies dynamically allocated extra chunks. +- If we require purely stack allocation, dynamic overflow chunks contradict that model. +- A fixed-capacity + nested-scope strategy is simpler and predictable. + +Possible future option (if desired): allow VM-managed heap chunks for overflow, but that is a different design tradeoff and can be deferred. + +## GC integration + +`VMProxy::visit_roots` additions: + +1. Iterate `vm.handleset_roots`. +2. For each root head, traverse its parent-chain/tree links. +3. For every active set, visit `slots[..len]` with `visitor(&mut slot)`. + +This ensures all values referenced by active handles are traced and fixed up. + +## Safety / invariants + +- Handle sets unregister from VM root list on drop. +- Parent/child scope drop must remain LIFO per chain. +- Handles cannot outlive their scope (`'hs` lifetime). +- VM proxy remains thread-confined while scopes are active. +- `Handle` and `Tagged` remain distinct concepts and APIs. + +## Suggested implementation plan + +1. Add `vm/src/handles.rs` with `HandleSet`, `Handle`, and scope registration. +2. Add `handleset_roots` storage + helper methods on `VMProxy`. +3. Extend `RootProvider::visit_roots` to include all active handle sets. +4. Add tests for: + - handle copyability within scope; + - compile-time prevention of escape; + - nested scope registration/unregistration; + - GC fixup through handle slots. +5. Incrementally migrate GC-sensitive temporary values to handles. + diff --git a/vm/src/handles.rs b/vm/src/handles.rs new file mode 100644 index 0000000..e36025d --- /dev/null +++ b/vm/src/handles.rs @@ -0,0 +1,346 @@ +use core::marker::PhantomData; +use core::ops::{Deref, DerefMut}; +use object::{Tagged, Value}; + +use crate::VMProxy; + +pub const HANDLESET_CAPACITY: usize = 20; +const HEAP_PTR_TAG: usize = 1; + +struct OverflowChunk { + next: *mut OverflowChunk, + len: usize, + slots: [Value; HANDLESET_CAPACITY], +} + +#[inline(always)] +fn is_heap_tagged(ptr: *mut T) -> bool { + (ptr as usize & HEAP_PTR_TAG) != 0 +} + +#[inline(always)] +fn tag_heap_ptr(ptr: *mut T) -> *mut T { + debug_assert_eq!(ptr as usize & HEAP_PTR_TAG, 0, "unaligned pointer"); + ((ptr as usize) | HEAP_PTR_TAG) as *mut T +} + +#[inline(always)] +fn untag_heap_ptr(ptr: *mut T) -> *mut T { + ((ptr as usize) & !HEAP_PTR_TAG) as *mut T +} + +/// Stack-allocated set of GC roots. +/// +/// Each `HandleSet` registers itself on a VM-local linked stack so +/// `VMProxy::visit_roots` can trace all active handle slots. +pub struct HandleSet { + vm: *mut VMProxy, + pub next: *mut HandleSet, + linked: bool, + len: usize, + slots: [Value; HANDLESET_CAPACITY], + overflow_head: *mut OverflowChunk, +} + +/// A copyable, scope-bounded rooted handle. +/// +/// `Handle` is intentionally distinct from `object::Tagged`: this points to a +/// mutable root slot (indirection) while `Tagged` is a direct value wrapper. +pub struct Handle<'scope, T> { + slot: *mut Value, + _scope: PhantomData<&'scope HandleSet>, + _type: PhantomData<*const T>, +} + +impl<'scope, T> Clone for Handle<'scope, T> { + fn clone(&self) -> Self { + *self + } +} + +impl<'scope, T> Copy for Handle<'scope, T> {} + +impl HandleSet { + #[inline(always)] + pub fn new_root(vm: &mut VMProxy) -> Self { + Self { + vm: vm as *mut VMProxy, + next: core::ptr::null_mut(), + linked: false, + len: 0, + slots: [Value::from_i64(0); HANDLESET_CAPACITY], + overflow_head: core::ptr::null_mut(), + } + } + + #[inline(always)] + pub fn child(&mut self) -> Self { + self.ensure_linked(); + Self { + vm: self.vm, + next: core::ptr::null_mut(), + linked: false, + len: 0, + slots: [Value::from_i64(0); HANDLESET_CAPACITY], + overflow_head: core::ptr::null_mut(), + } + } + + #[inline(always)] + fn ensure_linked(&mut self) { + if self.linked { + return; + } + let vm = unsafe { &mut *self.vm }; + self.next = vm.handle_roots_head; + vm.handle_roots_head = self as *mut HandleSet; + self.linked = true; + } + + #[inline(always)] + pub fn pin<'scope, T>( + &'scope mut self, + tagged: Tagged, + ) -> Handle<'scope, T> { + self.pin_value(tagged.value()) + } + + #[inline(always)] + pub fn pin_value<'scope, T>( + &'scope mut self, + value: Value, + ) -> Handle<'scope, T> { + self.ensure_linked(); + let slot = if self.len < HANDLESET_CAPACITY { + let idx = self.len; + self.len += 1; + &mut self.slots[idx] + } else { + self.push_overflow_slot() + }; + *slot = value; + Handle { + slot, + _scope: PhantomData, + _type: PhantomData, + } + } + + fn push_overflow_slot(&mut self) -> &mut Value { + if self.overflow_head.is_null() { + let chunk = Box::new(OverflowChunk { + next: core::ptr::null_mut(), + len: 0, + slots: [Value::from_i64(0); HANDLESET_CAPACITY], + }); + let leaked = Box::into_raw(chunk); + self.overflow_head = tag_heap_ptr(leaked); + } + + let mut tagged = self.overflow_head; + loop { + debug_assert!(is_heap_tagged(tagged)); + let chunk_ptr = untag_heap_ptr(tagged); + let chunk = unsafe { &mut *chunk_ptr }; + if chunk.len < HANDLESET_CAPACITY { + let idx = chunk.len; + chunk.len += 1; + return &mut chunk.slots[idx]; + } + + if chunk.next.is_null() { + let next = Box::new(OverflowChunk { + next: core::ptr::null_mut(), + len: 0, + slots: [Value::from_i64(0); HANDLESET_CAPACITY], + }); + chunk.next = tag_heap_ptr(Box::into_raw(next)); + } + tagged = chunk.next; + } + } + + #[inline(always)] + pub fn visit_roots( + &mut self, + visitor: &mut dyn FnMut(&mut Value), + ) { + for slot in &mut self.slots[..self.len] { + visitor(slot); + } + + let mut tagged = self.overflow_head; + while !tagged.is_null() { + debug_assert!(is_heap_tagged(tagged)); + let chunk_ptr = untag_heap_ptr(tagged); + let chunk = unsafe { &mut *chunk_ptr }; + for slot in &mut chunk.slots[..chunk.len] { + visitor(slot); + } + tagged = chunk.next; + } + } + + fn drop_overflow_chain(&mut self) { + let mut tagged = self.overflow_head; + self.overflow_head = core::ptr::null_mut(); + while !tagged.is_null() { + debug_assert!(is_heap_tagged(tagged)); + let chunk_ptr = untag_heap_ptr(tagged); + let next = unsafe { (*chunk_ptr).next }; + unsafe { + drop(Box::from_raw(chunk_ptr)); + } + tagged = next; + } + } +} + +impl Drop for HandleSet { + fn drop(&mut self) { + self.drop_overflow_chain(); + + if !self.linked { + return; + } + let vm = unsafe { &mut *self.vm }; + let self_ptr = self as *mut HandleSet; + debug_assert_eq!( + vm.handle_roots_head, self_ptr, + "HandleSet drop must follow LIFO scope order" + ); + if vm.handle_roots_head == self_ptr { + vm.handle_roots_head = self.next; + } + } +} + +impl<'scope, T> Handle<'scope, T> { + #[inline(always)] + pub fn value(&self) -> Value { + unsafe { *self.slot } + } + + #[inline(always)] + pub fn set(&self, value: Value) { + unsafe { + *self.slot = value; + } + } +} + +impl<'scope, T> Deref for Handle<'scope, T> { + type Target = T; + + #[inline(always)] + fn deref(&self) -> &Self::Target { + unsafe { (*self.slot).as_ref() } + } +} + +impl<'scope, T> DerefMut for Handle<'scope, T> { + #[inline(always)] + fn deref_mut(&mut self) -> &mut Self::Target { + unsafe { (*self.slot).as_mut() } + } +} + +#[cfg(test)] +mod tests { + use crate::special; + use heap::{HeapSettings, RootProvider}; + use object::{Header, ObjectType, Tagged, Value}; + use std::alloc::Layout; + + #[test] + fn handles_are_copyable_and_dereferenceable() { + let mut vm = special::bootstrap(HeapSettings::default()); + let map_map = vm.special.map_map; + let mut hs = vm.new_handleset(); + + let a: super::Handle<'_, object::Map> = hs.pin(Tagged::from(map_map)); + let b = a; + assert_eq!(a.value(), b.value()); + + let object_type = a.header.object_type(); + assert_eq!(object_type, ObjectType::Map); + } + + #[test] + fn handles_are_updated_when_roots_are_rewritten_on_child_handleset() { + let mut vm = special::bootstrap(HeapSettings::default()); + let layout = Layout::from_size_align(64, 8).expect("valid layout"); + + let vm_ptr: *mut crate::VMProxy = &mut vm; + let old_ptr = unsafe { (*vm_ptr).heap_proxy.allocate(layout, &mut *vm_ptr) }; + unsafe { + old_ptr.as_ptr().write_bytes(0, layout.size()); + *(old_ptr.as_ptr() as *mut Header) = Header::new(ObjectType::Array); + } + + let new_ptr = unsafe { (*vm_ptr).heap_proxy.allocate(layout, &mut *vm_ptr) }; + unsafe { + new_ptr.as_ptr().write_bytes(0, layout.size()); + *(new_ptr.as_ptr() as *mut Header) = Header::new(ObjectType::Array); + } + + let old_value = Value::from_ptr(old_ptr.as_ptr()); + let new_value = Value::from_ptr(new_ptr.as_ptr()); + + let mut hs1 = vm.new_handleset(); + let mut hs2 = hs1.child(); + + let h2: super::Handle<'_, Header> = hs2.pin_value(old_value); + let old_addr_2 = h2.value().ref_bits(); + + vm.visit_roots(&mut |root| { + if *root == old_value { + *root = new_value; + } + }); + + let new_addr_2 = h2.value().ref_bits(); + assert_ne!(old_addr_2, new_addr_2, "inner handle slot should be updated"); + assert_eq!(h2.object_type(), ObjectType::Array); + } + + #[test] + fn handleset_overflow_slots_are_visited_and_rewritten() { + let mut vm = special::bootstrap(HeapSettings::default()); + let layout = Layout::from_size_align(64, 8).expect("valid layout"); + + let vm_ptr: *mut crate::VMProxy = &mut vm; + let old_ptr = unsafe { (*vm_ptr).heap_proxy.allocate(layout, &mut *vm_ptr) }; + unsafe { + old_ptr.as_ptr().write_bytes(0, layout.size()); + *(old_ptr.as_ptr() as *mut Header) = Header::new(ObjectType::Array); + } + + let new_ptr = unsafe { (*vm_ptr).heap_proxy.allocate(layout, &mut *vm_ptr) }; + unsafe { + new_ptr.as_ptr().write_bytes(0, layout.size()); + *(new_ptr.as_ptr() as *mut Header) = Header::new(ObjectType::Array); + } + + let old_value = Value::from_ptr(old_ptr.as_ptr()); + let new_value = Value::from_ptr(new_ptr.as_ptr()); + + let mut hs = vm.new_handleset(); + for i in 0..super::HANDLESET_CAPACITY { + let _ = hs.pin_value::
(Value::from_i64(i as i64)); + } + + let h_overflow: super::Handle<'_, Header> = hs.pin_value(old_value); + let old_addr = h_overflow.value().ref_bits(); + + vm.visit_roots(&mut |root| { + if *root == old_value { + *root = new_value; + } + }); + + let new_addr = h_overflow.value().ref_bits(); + assert_ne!(old_addr, new_addr, "overflow handle slot should be updated"); + assert_eq!(h_overflow.object_type(), ObjectType::Array); + } +} diff --git a/vm/src/lib.rs b/vm/src/lib.rs index d6fd663..3a348bc 100644 --- a/vm/src/lib.rs +++ b/vm/src/lib.rs @@ -3,6 +3,7 @@ pub mod compiler0; pub mod image; pub mod interpreter; pub mod materialize; +pub mod handles; pub mod primitives; pub mod special; pub mod threading; @@ -77,6 +78,7 @@ pub struct VMProxy { #[cfg(debug_assertions)] pub trace_send_name: Option, pub shared: Arc, + pub handle_roots_head: *mut handles::HandleSet, } pub type VM = VMProxy; @@ -163,6 +165,14 @@ impl RootProvider for VMProxy { visitor(v); } } + + unsafe { + let mut cursor = self.handle_roots_head; + while let Some(handleset) = cursor.as_mut() { + handleset.visit_roots(visitor); + cursor = handleset.next; + } + } } } @@ -192,6 +202,7 @@ impl VMProxy { #[cfg(debug_assertions)] trace_send_name: None, shared, + handle_roots_head: core::ptr::null_mut(), } } @@ -246,6 +257,10 @@ impl VMProxy { .expect("ffi callbacks poisoned"); f(&mut callbacks) } + + pub fn new_handleset(&mut self) -> handles::HandleSet { + handles::HandleSet::new_root(self) + } } impl Deref for VMProxy { diff --git a/vm/src/special.rs b/vm/src/special.rs index 6922eb6..29b491c 100644 --- a/vm/src/special.rs +++ b/vm/src/special.rs @@ -3325,6 +3325,7 @@ pub fn bootstrap(settings: HeapSettings) -> VMProxy { #[cfg(debug_assertions)] trace_send_name: None, shared, + handle_roots_head: core::ptr::null_mut(), }; vm.seed_user_module_from_dictionary(); vm From debf8231cbef3ffff17ddd66f9e98b55b469dfa5 Mon Sep 17 00:00:00 2001 From: TryAngle <45734252+TriedAngle@users.noreply.github.com> Date: Mon, 23 Feb 2026 15:38:59 +0100 Subject: [PATCH 2/3] Remove HandleSet sketch document --- vm/HANDLESET_SKETCH.md | 168 ----------------------------------------- 1 file changed, 168 deletions(-) delete mode 100644 vm/HANDLESET_SKETCH.md diff --git a/vm/HANDLESET_SKETCH.md b/vm/HANDLESET_SKETCH.md deleted file mode 100644 index c8b94c0..0000000 --- a/vm/HANDLESET_SKETCH.md +++ /dev/null @@ -1,168 +0,0 @@ -# HandleSet / Handle design sketch for `VM` (revised) - -This revises the initial sketch with the following constraints: - -- `Handle` and `Tagged` stay **separate** types. -- `Handle` has an extra indirection but is **safe to dereference**. -- Handles are **copyable**, but only within the lifetime/scope of their `HandleSet`. -- Clarify overflow policy for stack-allocated sets. -- Clarify whether `next` is sufficient for disjoint handle-set trees. - -## Goals - -- Keep temporary object references safe across allocation + GC points. -- Make handle scopes cheap to create on the stack. -- Support nested scopes linked together. -- Let `VM` expose active handle roots to GC via `RootProvider::visit_roots`. - -## Key type split: `Tagged` vs `Handle` - -- `Tagged` remains a plain typed `Value` wrapper (no rooting). -- `Handle` is a rooted, indirect reference to a slot in a live `HandleSet`. -- They should not be unified. - -`Handle` is for values that must survive safepoints/GC relocation while remaining ergonomic to access. - -## Runtime model - -### 1) VM owns **multiple** handle-set roots - -To support potentially disjoint trees, store root heads in the VM: - -```rust -pub struct VMProxy { - // ...existing fields... - handleset_roots: Vec<*mut HandleSet>, -} -``` - -Why a vector: - -- A single `next` pointer only encodes one linked chain. -- If you allow creating top-level handle sets independently (not strictly nested from one active top), you can form multiple disjoint trees/chains. -- VM root scanning should iterate all root heads. - -### 2) Stack-allocated `HandleSet` - -```rust -const HANDLESET_CAPACITY: usize = 20; - -pub struct HandleSet<'vm> { - vm: &'vm mut VMProxy, - parent: Option<*mut HandleSet<'vm>>, - len: u8, - slots: [Value; HANDLESET_CAPACITY], -} -``` - -Notes: - -- `parent` links a child scope to its parent scope. -- A top-level scope created directly from VM has `parent = None` and is registered in `vm.handleset_roots`. -- A child scope created from an existing scope is linked via `parent = Some(...)` and does not need to be added as a separate VM root head. - -### 3) Copyable, scope-bounded `Handle` - -```rust -#[derive(Clone, Copy)] -pub struct Handle<'hs, T> { - slot: *mut Value, - _scope: PhantomData<&'hs HandleSet<'hs>>, - _type: PhantomData<*const T>, -} -``` - -- `Clone + Copy`: yes. -- `'hs` ties the handle lifetime to the originating handle scope. -- This allows copying handles freely **within scope**, while preventing escape. - -## API shape - -### Creating scopes - -Two entry points: - -1. Top-level scope from VM: - - `let mut hs = vm.new_handleset();` - - registers `hs` in `vm.handleset_roots`. -2. Child scope from existing scope: - - `let mut child = hs.child();` - - links child to parent via `parent`. - -Both are stack values with RAII drop behavior. - -### Inserting handles - -```rust -impl<'vm> HandleSet<'vm> { - pub fn pin<'hs, T>(&'hs mut self, value: Value) -> Handle<'hs, T>; -} -``` - -- Stores value in next free slot. -- Returns copyable handle to that slot. - -### Safe handle operations - -```rust -impl<'hs, T> Handle<'hs, T> { - pub fn get(self) -> Value; - pub fn set(self, value: Value); - - // typed safe accessors - pub fn as_ref(self) -> Option<&'hs T>; - pub fn as_mut(self) -> Option<&'hs mut T>; -} -``` - -Design intent: - -- No `unsafe` required by callers for normal dereference/access. -- Internally, `Handle` may use unsafe pointer operations, but API checks tag/kind and returns `Option` (or `Result`) for invalid type/tag. - -## Overflow policy (stack allocated sets) - -For v1: **no overflow chaining**. - -- Each `HandleSet` has hard capacity 20. -- On overflow, return an error or panic in debug builds. -- Users needing more than 20 simultaneously can explicitly open child scopes. - -Why: - -- True overflow chaining usually implies dynamically allocated extra chunks. -- If we require purely stack allocation, dynamic overflow chunks contradict that model. -- A fixed-capacity + nested-scope strategy is simpler and predictable. - -Possible future option (if desired): allow VM-managed heap chunks for overflow, but that is a different design tradeoff and can be deferred. - -## GC integration - -`VMProxy::visit_roots` additions: - -1. Iterate `vm.handleset_roots`. -2. For each root head, traverse its parent-chain/tree links. -3. For every active set, visit `slots[..len]` with `visitor(&mut slot)`. - -This ensures all values referenced by active handles are traced and fixed up. - -## Safety / invariants - -- Handle sets unregister from VM root list on drop. -- Parent/child scope drop must remain LIFO per chain. -- Handles cannot outlive their scope (`'hs` lifetime). -- VM proxy remains thread-confined while scopes are active. -- `Handle` and `Tagged` remain distinct concepts and APIs. - -## Suggested implementation plan - -1. Add `vm/src/handles.rs` with `HandleSet`, `Handle`, and scope registration. -2. Add `handleset_roots` storage + helper methods on `VMProxy`. -3. Extend `RootProvider::visit_roots` to include all active handle sets. -4. Add tests for: - - handle copyability within scope; - - compile-time prevention of escape; - - nested scope registration/unregistration; - - GC fixup through handle slots. -5. Incrementally migrate GC-sensitive temporary values to handles. - From 0a5aa9049319c7351ec35bbd1ce168cad42465ea Mon Sep 17 00:00:00 2001 From: TryAngle <45734252+TriedAngle@users.noreply.github.com> Date: Mon, 23 Feb 2026 15:52:33 +0100 Subject: [PATCH 3/3] Merge handleset and overflow links via tagged next pointer --- vm/src/handles.rs | 74 ++++++++++++++++++++++++++++++++--------------- vm/src/lib.rs | 2 +- 2 files changed, 51 insertions(+), 25 deletions(-) diff --git a/vm/src/handles.rs b/vm/src/handles.rs index e36025d..fa952cd 100644 --- a/vm/src/handles.rs +++ b/vm/src/handles.rs @@ -8,7 +8,10 @@ pub const HANDLESET_CAPACITY: usize = 20; const HEAP_PTR_TAG: usize = 1; struct OverflowChunk { - next: *mut OverflowChunk, + // Tagged link: + // - tag=1 => next OverflowChunk + // - tag=0 => next HandleSet in VM chain (or null) + next: *mut HandleSet, len: usize, slots: [Value; HANDLESET_CAPACITY], } @@ -35,11 +38,14 @@ fn untag_heap_ptr(ptr: *mut T) -> *mut T { /// `VMProxy::visit_roots` can trace all active handle slots. pub struct HandleSet { vm: *mut VMProxy, + /// Tagged link: + /// - tag=0 => next HandleSet in VM chain (or null) + /// - tag=1 => head OverflowChunk (which itself eventually links to the + /// next HandleSet with tag=0) pub next: *mut HandleSet, linked: bool, len: usize, slots: [Value; HANDLESET_CAPACITY], - overflow_head: *mut OverflowChunk, } /// A copyable, scope-bounded rooted handle. @@ -69,7 +75,6 @@ impl HandleSet { linked: false, len: 0, slots: [Value::from_i64(0); HANDLESET_CAPACITY], - overflow_head: core::ptr::null_mut(), } } @@ -82,7 +87,6 @@ impl HandleSet { linked: false, len: 0, slots: [Value::from_i64(0); HANDLESET_CAPACITY], - overflow_head: core::ptr::null_mut(), } } @@ -127,39 +131,53 @@ impl HandleSet { } fn push_overflow_slot(&mut self) -> &mut Value { - if self.overflow_head.is_null() { + // First overflow chunk steals `self.next`, preserving VM linkage in + // chunk.next. This merges overflow and normal link state into `next`. + if !is_heap_tagged(self.next) { let chunk = Box::new(OverflowChunk { - next: core::ptr::null_mut(), + next: self.next, len: 0, slots: [Value::from_i64(0); HANDLESET_CAPACITY], }); - let leaked = Box::into_raw(chunk); - self.overflow_head = tag_heap_ptr(leaked); + self.next = tag_heap_ptr(Box::into_raw(chunk) as *mut HandleSet); } - let mut tagged = self.overflow_head; + let mut tagged = self.next; loop { debug_assert!(is_heap_tagged(tagged)); - let chunk_ptr = untag_heap_ptr(tagged); + let chunk_ptr = + untag_heap_ptr(tagged as *mut OverflowChunk) as *mut OverflowChunk; let chunk = unsafe { &mut *chunk_ptr }; + if chunk.len < HANDLESET_CAPACITY { let idx = chunk.len; chunk.len += 1; return &mut chunk.slots[idx]; } - if chunk.next.is_null() { + if !is_heap_tagged(chunk.next) { let next = Box::new(OverflowChunk { - next: core::ptr::null_mut(), + next: chunk.next, len: 0, slots: [Value::from_i64(0); HANDLESET_CAPACITY], }); - chunk.next = tag_heap_ptr(Box::into_raw(next)); + chunk.next = tag_heap_ptr(Box::into_raw(next) as *mut HandleSet); } tagged = chunk.next; } } + /// Return the VM chain successor for this handleset. + pub fn vm_next(&self) -> *mut HandleSet { + let mut link = self.next; + while is_heap_tagged(link) { + let chunk_ptr = + untag_heap_ptr(link as *mut OverflowChunk) as *mut OverflowChunk; + link = unsafe { (*chunk_ptr).next }; + } + link + } + #[inline(always)] pub fn visit_roots( &mut self, @@ -169,10 +187,10 @@ impl HandleSet { visitor(slot); } - let mut tagged = self.overflow_head; - while !tagged.is_null() { - debug_assert!(is_heap_tagged(tagged)); - let chunk_ptr = untag_heap_ptr(tagged); + let mut tagged = self.next; + while is_heap_tagged(tagged) { + let chunk_ptr = + untag_heap_ptr(tagged as *mut OverflowChunk) as *mut OverflowChunk; let chunk = unsafe { &mut *chunk_ptr }; for slot in &mut chunk.slots[..chunk.len] { visitor(slot); @@ -181,24 +199,32 @@ impl HandleSet { } } - fn drop_overflow_chain(&mut self) { - let mut tagged = self.overflow_head; - self.overflow_head = core::ptr::null_mut(); - while !tagged.is_null() { - debug_assert!(is_heap_tagged(tagged)); - let chunk_ptr = untag_heap_ptr(tagged); + fn drop_overflow_chain_and_restore_next(&mut self) { + let mut tagged = self.next; + if !is_heap_tagged(tagged) { + return; + } + + let mut vm_next = core::ptr::null_mut(); + while is_heap_tagged(tagged) { + let chunk_ptr = + untag_heap_ptr(tagged as *mut OverflowChunk) as *mut OverflowChunk; let next = unsafe { (*chunk_ptr).next }; + if !is_heap_tagged(next) { + vm_next = next; + } unsafe { drop(Box::from_raw(chunk_ptr)); } tagged = next; } + self.next = vm_next; } } impl Drop for HandleSet { fn drop(&mut self) { - self.drop_overflow_chain(); + self.drop_overflow_chain_and_restore_next(); if !self.linked { return; diff --git a/vm/src/lib.rs b/vm/src/lib.rs index 3a348bc..a176e18 100644 --- a/vm/src/lib.rs +++ b/vm/src/lib.rs @@ -170,7 +170,7 @@ impl RootProvider for VMProxy { let mut cursor = self.handle_roots_head; while let Some(handleset) = cursor.as_mut() { handleset.visit_roots(visitor); - cursor = handleset.next; + cursor = handleset.vm_next(); } } }