pr7.7
This commit is contained in:
parent
24d6962c0f
commit
d123919b73
@ -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)]
|
||||||
|
|||||||
@ -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};
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user