This commit is contained in:
bQUARKz 2026-02-20 10:17:04 +00:00
parent e894ea1a8c
commit 3fba722b50
Signed by: bquarkz
SSH Key Fingerprint: SHA256:Z7dgqoglWwoK6j6u4QC87OveEq74WOhFN+gitsxtkf8
2 changed files with 93 additions and 37 deletions

View File

@ -11,9 +11,11 @@ pub struct StoredObject {
/// When present, `header.payload_len` must equal `array_elems.len() as u32`.
pub array_elems: Option<Vec<Value>>,
/// Optional captured environment for `ObjectKind::Closure`.
/// `header.payload_len` stores the fixed-size metadata length (8 bytes):
/// [fn_id: u32][env_len: u32].
/// The actual env slots are stored here to remain GC-visible.
/// Invariants for closures:
/// - `header.payload_len == 8` and `payload` bytes are `[fn_id: u32][env_len: u32]` (LE).
/// - The actual `env_len` Value slots are stored here (not in `payload`) so
/// they stay directly GC-visible. The GC must traverse exactly `env_len`
/// entries from this slice, in order.
pub closure_env: Option<Vec<Value>>,
}
@ -94,40 +96,44 @@ impl Heap {
.map(|o| &mut o.header)
}
/// Internal: enumerate inner `HeapRef` children of an object.
fn children_of(&self, r: HeapRef) -> impl Iterator<Item = HeapRef> + '_ {
/// Internal: enumerate inner `HeapRef` children of an object without allocating.
/// Note: This helper is no longer used by GC mark; kept for potential diagnostics.
fn children_of(&self, r: HeapRef) -> Box<dyn Iterator<Item = HeapRef> + '_> {
let idx = r.0 as usize;
self.objects
.get(idx)
.and_then(|slot| slot.as_ref())
.map(|o| match o.header.kind {
if let Some(Some(o)) = self.objects.get(idx) {
match o.header.kind {
ObjectKind::Array => {
// Traverse only Value::HeapRef inside the array.
o.array_elems
.as_ref()
.into_iter()
.flat_map(|v| v.iter())
.filter_map(|val| if let Value::HeapRef(h) = val { Some(*h) } else { None })
.collect::<Vec<_>>()
let it = o
.array_elems
.as_deref()
.into_iter()
.flat_map(|slice| slice.iter())
.filter_map(|val| if let Value::HeapRef(h) = val { Some(*h) } else { None });
return Box::new(it);
}
ObjectKind::Closure => {
// Traverse only Value::HeapRef inside the closure env.
o.closure_env
.as_ref()
.into_iter()
.flat_map(|v| v.iter())
.filter_map(|val| if let Value::HeapRef(h) = val { Some(*h) } else { None })
.collect::<Vec<_>>()
// Read env_len from payload; traverse exactly that many entries.
debug_assert_eq!(o.header.kind, ObjectKind::Closure);
debug_assert_eq!(o.payload.len(), 8, "closure payload metadata must be 8 bytes");
let mut nbytes = [0u8; 4];
nbytes.copy_from_slice(&o.payload[4..8]);
let env_len = u32::from_le_bytes(nbytes) as usize;
let it = o
.closure_env
.as_deref()
.map(|slice| {
debug_assert_eq!(slice.len(), env_len, "closure env length must match encoded env_len");
&slice[..env_len]
})
.into_iter()
.flat_map(|slice| slice.iter())
.filter_map(|val| if let Value::HeapRef(h) = val { Some(*h) } else { None });
return Box::new(it);
}
// These kinds have no inner references in this PR.
ObjectKind::String | ObjectKind::Bytes | ObjectKind::UserData | ObjectKind::Unknown => {
Vec::new().into_iter()
}
})
.into_iter()
.flatten()
_ => return Box::new(std::iter::empty()),
}
}
Box::new(std::iter::empty())
}
/// Read the `fn_id` stored in a closure object. Returns None if kind mismatch or invalid ref.
@ -136,6 +142,7 @@ impl Heap {
let slot = self.objects.get(idx)?.as_ref()?;
if slot.header.kind != ObjectKind::Closure { return None; }
if slot.payload.len() < 8 { return None; }
debug_assert_eq!(slot.header.payload_len, 8);
let mut bytes = [0u8; 4];
bytes.copy_from_slice(&slot.payload[0..4]);
Some(u32::from_le_bytes(bytes))
@ -146,6 +153,14 @@ impl Heap {
let idx = r.0 as usize;
let slot = self.objects.get(idx)?.as_ref()?;
if slot.header.kind != ObjectKind::Closure { return None; }
if slot.payload.len() >= 8 {
let mut nbytes = [0u8; 4];
nbytes.copy_from_slice(&slot.payload[4..8]);
let env_len = u32::from_le_bytes(nbytes) as usize;
if let Some(env) = slot.closure_env.as_deref() {
debug_assert_eq!(env.len(), env_len);
}
}
slot.closure_env.as_deref()
}
@ -164,12 +179,40 @@ impl Heap {
// Set mark bit.
if let Some(h) = self.header_mut(r) { h.set_marked(true); }
// Push children.
for child in self.children_of(r) {
if self.is_valid(child) {
// Check child's mark state cheaply to reduce stack churn.
let marked = self.header(child).map(|h| h.is_marked()).unwrap_or(false);
if !marked { stack.push(child); }
// Push children by scanning payload directly (no intermediate Vec allocs).
let idx = r.0 as usize;
if let Some(Some(obj)) = self.objects.get(idx) {
match obj.header.kind {
ObjectKind::Array => {
if let Some(elems) = obj.array_elems.as_ref() {
for val in elems.iter() {
if let Value::HeapRef(child) = val {
if self.is_valid(*child) {
let marked = self.header(*child).map(|h| h.is_marked()).unwrap_or(false);
if !marked { stack.push(*child); }
}
}
}
}
}
ObjectKind::Closure => {
debug_assert_eq!(obj.payload.len(), 8, "closure payload must be 8 bytes");
let mut nbytes = [0u8; 4];
nbytes.copy_from_slice(&obj.payload[4..8]);
let env_len = u32::from_le_bytes(nbytes) as usize;
if let Some(env) = obj.closure_env.as_ref() {
debug_assert_eq!(env.len(), env_len, "closure env len must match encoded env_len");
for val in env[..env_len].iter() {
if let Value::HeapRef(child) = val {
if self.is_valid(*child) {
let marked = self.header(*child).map(|h| h.is_marked()).unwrap_or(false);
if !marked { stack.push(*child); }
}
}
}
}
}
_ => {}
}
}
}

View File

@ -25,6 +25,13 @@
//! element count (arrays) or byte length (strings). Exact interpretation is
//! defined by each object kind; the GC treats it as an opaque metadata field.
//!
//! Closure-specific note: for `ObjectKind::Closure`, `payload_len` is the
//! fixed size `8` and the payload layout is exactly two little-endian `u32`s:
//! `[fn_id][env_len]`. The captured environment values themselves are NOT in
//! the raw payload; they live in a separate GC-visible area managed by the
//! heap (see `Heap`), and the GC must traverse exactly `env_len` values from
//! that environment slice.
//!
//! Notes:
//! - The GC only relies on `flags` (mark bit) and `kind` to traverse/trace.
//! Actual traversal logic will be implemented elsewhere in future PRs.
@ -52,7 +59,13 @@ pub enum ObjectKind {
/// Homogeneous array of VM values/handles. `payload_len` is element count.
Array = 2,
/// Compiled closure/function value. Fixed-size payload (implementation-defined).
/// Compiled closure/function value.
///
/// Invariants for `payload_len` and payload layout:
/// - `payload_len == 8` (fixed).
/// - payload bytes are `[fn_id: u32][env_len: u32]` (little-endian).
/// - The `env_len` captured values are stored out-of-line in the heap so
/// they remain directly visible to the GC during traversal.
Closure = 3,
/// Byte buffer / blob. `payload_len` is the number of bytes.