From d123919b73c892778de2c06ffd54d753be1bdecb Mon Sep 17 00:00:00 2001 From: bQUARKz Date: Fri, 20 Feb 2026 12:16:31 +0000 Subject: [PATCH] pr7.7 --- crates/console/prometeu-vm/src/heap.rs | 19 +++++ .../prometeu-vm/src/virtual_machine.rs | 83 +++++++++++++++++++ files/TODOs.md | 29 ------- 3 files changed, 102 insertions(+), 29 deletions(-) diff --git a/crates/console/prometeu-vm/src/heap.rs b/crates/console/prometeu-vm/src/heap.rs index 8cc88205..7b1e5226 100644 --- a/crates/console/prometeu-vm/src/heap.rs +++ b/crates/console/prometeu-vm/src/heap.rs @@ -305,6 +305,25 @@ impl Heap { /// Current number of allocated (live) objects. pub fn len(&self) -> usize { self.objects.iter().filter(|s| s.is_some()).count() } 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 { + 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)] diff --git a/crates/console/prometeu-vm/src/virtual_machine.rs b/crates/console/prometeu-vm/src/virtual_machine.rs index 6664ed19..f283ced5 100644 --- a/crates/console/prometeu-vm/src/virtual_machine.rs +++ b/crates/console/prometeu-vm/src/virtual_machine.rs @@ -1180,6 +1180,10 @@ impl VirtualMachine { let mut collector = CollectRoots(Vec::new()); 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 self.heap.mark_from_roots(collector.0); self.heap.sweep(); @@ -2955,6 +2959,85 @@ mod tests { 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] fn test_make_closure_zero_captures() { use prometeu_bytecode::{FunctionMeta, Value}; diff --git a/files/TODOs.md b/files/TODOs.md index 33b5c90a..fefaa0fc 100644 --- a/files/TODOs.md +++ b/files/TODOs.md @@ -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 ## Briefing