This commit is contained in:
bQUARKz 2026-02-20 12:16:31 +00:00
parent 24d6962c0f
commit d123919b73
Signed by: bquarkz
SSH Key Fingerprint: SHA256:Z7dgqoglWwoK6j6u4QC87OveEq74WOhFN+gitsxtkf8
3 changed files with 102 additions and 29 deletions

View File

@ -305,6 +305,25 @@ impl Heap {
/// Current number of allocated (live) objects. /// Current number of allocated (live) objects.
pub fn len(&self) -> usize { self.objects.iter().filter(|s| s.is_some()).count() } pub fn len(&self) -> usize { self.objects.iter().filter(|s| s.is_some()).count() }
pub fn is_empty(&self) -> bool { self.len() == 0 } pub fn is_empty(&self) -> bool { self.len() == 0 }
/// Enumerate handles of coroutines that are currently suspended (i.e., not running):
/// Ready or Sleeping. These must be treated as GC roots by the runtime so their
/// stacks/frames are scanned during mark.
pub fn suspended_coroutine_handles(&self) -> Vec<HeapRef> {
let mut out = Vec::new();
for (idx, slot) in self.objects.iter().enumerate() {
if let Some(obj) = slot {
if obj.header.kind == ObjectKind::Coroutine {
if let Some(co) = &obj.coroutine {
if matches!(co.state, CoroutineState::Ready | CoroutineState::Sleeping) {
out.push(HeapRef(idx as u32));
}
}
}
}
}
out
}
} }
#[cfg(test)] #[cfg(test)]

View File

@ -1180,6 +1180,10 @@ impl VirtualMachine {
let mut collector = CollectRoots(Vec::new()); let mut collector = CollectRoots(Vec::new());
self.visit_roots(&mut collector); self.visit_roots(&mut collector);
// Add suspended coroutine handles as GC roots so their stacks/frames are scanned
let mut coro_roots = self.heap.suspended_coroutine_handles();
collector.0.append(&mut coro_roots);
// Run mark-sweep // Run mark-sweep
self.heap.mark_from_roots(collector.0); self.heap.mark_from_roots(collector.0);
self.heap.sweep(); self.heap.sweep();
@ -2955,6 +2959,85 @@ mod tests {
assert_eq!(vm.heap.len(), 0, "All short-lived objects must be reclaimed deterministically"); assert_eq!(vm.heap.len(), 0, "All short-lived objects must be reclaimed deterministically");
} }
#[test]
fn test_gc_keeps_objects_captured_by_suspended_coroutines() {
use crate::object::ObjectKind;
use crate::heap::CoroutineState;
// ROM: FRAME_SYNC; HALT (trigger GC at safepoint)
let mut rom = Vec::new();
rom.extend_from_slice(&(OpCode::FrameSync as u16).to_le_bytes());
rom.extend_from_slice(&(OpCode::Halt as u16).to_le_bytes());
let mut vm = new_test_vm(rom.clone(), vec![]);
vm.program.functions = std::sync::Arc::from(vec![prometeu_bytecode::FunctionMeta {
code_offset: 0,
code_len: rom.len() as u32,
..Default::default()
}]);
// Trigger GC at first FRAME_SYNC
vm.gc_alloc_threshold = 1;
// Allocate a heap object and a suspended coroutine that captures it on its stack
let captured = vm.heap.allocate_object(ObjectKind::Bytes, &[0xAA, 0xBB]);
let _coro = vm.heap.allocate_coroutine(
CoroutineState::Ready,
0,
vec![Value::HeapRef(captured)],
vec![],
);
assert_eq!(vm.heap.len(), 2, "object + coroutine must be allocated");
let mut native = MockNative;
let mut ctx = HostContext::new(None);
// FRAME_SYNC: GC runs and should keep both alive via suspended coroutine root
match vm.step(&mut native, &mut ctx) {
Err(LogicalFrameEndingReason::FrameSync) => {}
other => panic!("Expected FrameSync, got {:?}", other),
}
assert!(vm.heap.is_valid(captured), "captured object must remain alive");
assert_eq!(vm.heap.len(), 2, "both coroutine and captured object must survive");
}
#[test]
fn test_gc_collects_finished_coroutine() {
use crate::heap::CoroutineState;
// ROM: FRAME_SYNC; HALT (trigger GC at safepoint)
let mut rom = Vec::new();
rom.extend_from_slice(&(OpCode::FrameSync as u16).to_le_bytes());
rom.extend_from_slice(&(OpCode::Halt as u16).to_le_bytes());
let mut vm = new_test_vm(rom.clone(), vec![]);
vm.program.functions = std::sync::Arc::from(vec![prometeu_bytecode::FunctionMeta {
code_offset: 0,
code_len: rom.len() as u32,
..Default::default()
}]);
vm.gc_alloc_threshold = 1;
// Allocate a finished coroutine with no external references
let finished = vm.heap.allocate_coroutine(CoroutineState::Finished, 0, vec![], vec![]);
assert!(vm.heap.is_valid(finished));
let mut native = MockNative;
let mut ctx = HostContext::new(None);
// FRAME_SYNC: GC should collect the finished coroutine since it's not a root
match vm.step(&mut native, &mut ctx) {
Err(LogicalFrameEndingReason::FrameSync) => {}
other => panic!("Expected FrameSync, got {:?}", other),
}
assert!(!vm.heap.is_valid(finished), "finished coroutine must be collected");
assert_eq!(vm.heap.len(), 0, "no objects should remain");
}
#[test] #[test]
fn test_make_closure_zero_captures() { fn test_make_closure_zero_captures() {
use prometeu_bytecode::{FunctionMeta, Value}; use prometeu_bytecode::{FunctionMeta, Value};

View File

@ -1,32 +1,3 @@
# PR-7.7 — GC Integration
## Briefing
Suspended coroutines must be GC roots.
## Target
GC mark phase must traverse:
* All coroutine stacks
* All coroutine frames
## Checklist
* [ ] GC visits all suspended coroutines.
* [ ] No leaked references.
## Tests
* Coroutine capturing heap object remains alive.
* Finished coroutine collected.
## Junie Rules
You MUST NOT change sweep policy.
---
# PR-7.8 — Verifier Rules # PR-7.8 — Verifier Rules
## Briefing ## Briefing