diff --git a/crates/console/prometeu-vm/src/call_frame.rs b/crates/console/prometeu-vm/src/call_frame.rs index ba73d78f..909e0829 100644 --- a/crates/console/prometeu-vm/src/call_frame.rs +++ b/crates/console/prometeu-vm/src/call_frame.rs @@ -1,3 +1,4 @@ +#[derive(Debug, Clone)] pub struct CallFrame { pub return_pc: u32, pub stack_base: usize, diff --git a/crates/console/prometeu-vm/src/heap.rs b/crates/console/prometeu-vm/src/heap.rs index ff3d675b..8cc88205 100644 --- a/crates/console/prometeu-vm/src/heap.rs +++ b/crates/console/prometeu-vm/src/heap.rs @@ -1,4 +1,5 @@ use crate::{ObjectHeader, ObjectKind}; +use crate::call_frame::CallFrame; use prometeu_bytecode::{HeapRef, Value}; /// Internal stored object: header plus opaque payload bytes. @@ -17,6 +18,27 @@ pub struct StoredObject { /// they stay directly GC-visible. The GC must traverse exactly `env_len` /// entries from this slice, in order. pub closure_env: Option>, + /// Optional coroutine data for `ObjectKind::Coroutine`. + pub coroutine: Option, +} + +/// Execution state of a coroutine. +#[derive(Debug, Copy, Clone, Eq, PartialEq)] +pub enum CoroutineState { + Ready, + Running, + Sleeping, + Finished, + Faulted, +} + +/// Stored payload for coroutine objects. +#[derive(Debug, Clone)] +pub struct CoroutineData { + pub state: CoroutineState, + pub wake_tick: u64, + pub stack: Vec, + pub frames: Vec, } /// Simple vector-backed heap. No GC or compaction. @@ -33,7 +55,7 @@ impl Heap { /// Returns an opaque `HeapRef` handle. pub fn allocate_object(&mut self, kind: ObjectKind, payload: &[u8]) -> HeapRef { let header = ObjectHeader::new(kind, payload.len() as u32); - let obj = StoredObject { header, payload: payload.to_vec(), array_elems: None, closure_env: None }; + let obj = StoredObject { header, payload: payload.to_vec(), array_elems: None, closure_env: None, coroutine: None }; let idx = self.objects.len(); // No free-list reuse in this PR: append and keep indices stable. self.objects.push(Some(obj)); @@ -44,7 +66,7 @@ impl Heap { /// `payload_len` stores the element count; raw `payload` bytes are empty. pub fn allocate_array(&mut self, elements: Vec) -> HeapRef { let header = ObjectHeader::new(ObjectKind::Array, elements.len() as u32); - let obj = StoredObject { header, payload: Vec::new(), array_elems: Some(elements), closure_env: None }; + let obj = StoredObject { header, payload: Vec::new(), array_elems: Some(elements), closure_env: None, coroutine: None }; let idx = self.objects.len(); // No free-list reuse in this PR: append and keep indices stable. self.objects.push(Some(obj)); @@ -67,6 +89,29 @@ impl Heap { payload, array_elems: None, closure_env: Some(env_values.to_vec()), + coroutine: None, + }; + let idx = self.objects.len(); + self.objects.push(Some(obj)); + HeapRef(idx as u32) + } + + /// Allocate a new `Coroutine` object with provided initial data. + /// `payload_len` is 0; stack and frames are stored out-of-line for GC visibility. + pub fn allocate_coroutine( + &mut self, + state: CoroutineState, + wake_tick: u64, + stack: Vec, + frames: Vec, + ) -> HeapRef { + let header = ObjectHeader::new(ObjectKind::Coroutine, 0); + let obj = StoredObject { + header, + payload: Vec::new(), + array_elems: None, + closure_env: None, + coroutine: Some(CoroutineData { state, wake_tick, stack, frames }), }; let idx = self.objects.len(); self.objects.push(Some(obj)); @@ -130,6 +175,16 @@ impl Heap { .filter_map(|val| if let Value::HeapRef(h) = val { Some(*h) } else { None }); return Box::new(it); } + ObjectKind::Coroutine => { + if let Some(co) = o.coroutine.as_ref() { + let it = co + .stack + .iter() + .filter_map(|v| if let Value::HeapRef(h) = v { Some(*h) } else { None }); + return Box::new(it); + } + return Box::new(std::iter::empty()); + } _ => return Box::new(std::iter::empty()), } } @@ -212,6 +267,18 @@ impl Heap { } } } + ObjectKind::Coroutine => { + if let Some(co) = obj.coroutine.as_ref() { + for val in co.stack.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); } + } + } + } + } + } _ => {} } } @@ -270,6 +337,42 @@ mod tests { assert_eq!(h3.payload_len, 0); } + #[test] + fn allocate_and_transition_coroutine() { + let mut heap = Heap::new(); + + // Create a coroutine with a small stack containing a HeapRef to verify GC traversal later. + let obj_ref = heap.allocate_object(ObjectKind::Bytes, &[4, 5, 6]); + let coro = heap.allocate_coroutine( + CoroutineState::Ready, + 0, + vec![Value::Int32(1), Value::HeapRef(obj_ref)], + vec![CallFrame { return_pc: 0, stack_base: 0, func_idx: 0 }], + ); + + let hdr = heap.header(coro).unwrap(); + assert_eq!(hdr.kind, ObjectKind::Coroutine); + assert_eq!(hdr.payload_len, 0); + + // Manually mutate state transitions via access to inner data. + { + let slot = heap.objects.get_mut(coro.0 as usize).and_then(|s| s.as_mut()).unwrap(); + let co = slot.coroutine.as_mut().unwrap(); + assert_eq!(co.state, CoroutineState::Ready); + co.state = CoroutineState::Running; + assert_eq!(co.state, CoroutineState::Running); + co.state = CoroutineState::Sleeping; + co.wake_tick = 42; + assert_eq!(co.wake_tick, 42); + co.state = CoroutineState::Finished; + assert_eq!(co.state, CoroutineState::Finished); + } + + // GC should mark the object referenced from the coroutine stack when the coroutine is a root. + heap.mark_from_roots([coro]); + assert!(heap.header(obj_ref).unwrap().is_marked()); + } + #[test] fn mark_reachable_through_array() { let mut heap = Heap::new(); diff --git a/crates/console/prometeu-vm/src/object.rs b/crates/console/prometeu-vm/src/object.rs index 8c8c428e..c1eda211 100644 --- a/crates/console/prometeu-vm/src/object.rs +++ b/crates/console/prometeu-vm/src/object.rs @@ -74,6 +74,15 @@ pub enum ObjectKind { /// User-defined/native host object. Payload shape is host-defined. UserData = 5, + /// Coroutine object: suspended execution context with its own stack/frames. + /// + /// Notes: + /// - Stack/frames are stored in typed fields inside the heap storage + /// (not inside raw `payload` bytes) so the GC can traverse their + /// contained `HeapRef`s directly. + /// - `payload_len` is 0 for this fixed-layout object. + Coroutine = 6, + // Future kinds must be appended here to keep tag numbers stable. } diff --git a/files/TODOs.md b/files/TODOs.md index caa34843..fa63c043 100644 --- a/files/TODOs.md +++ b/files/TODOs.md @@ -1,64 +1,3 @@ -# PR-7 — Coroutines (Cooperative, Deterministic, No Mailbox) - -Coroutines are the **only concurrency model** in the Prometeu VM. - -This phase introduces: - -* Cooperative scheduling -* Deterministic execution order -* SPAWN / YIELD / SLEEP -* Switching only at safepoints (FRAME_SYNC) -* Full GC integration - -No mailbox. No message passing. No preemption. - -Each PR below is self-contained and must compile independently. - ---- - -# PR-7.1 — Coroutine Heap Object - -## Briefing - -A coroutine is a suspended execution context with its own stack and call frames. - -No mailbox is implemented in this phase. - -## Target - -Define `ObjectKind::Coroutine` with: - -* `state: enum { Ready, Running, Sleeping, Finished, Faulted }` -* `wake_tick: u64` -* `stack: Vec` -* `frames: Vec` - -Rules: - -* Allocated in GC heap. -* Addressed via `HeapRef`. -* Stack and frames stored inside the coroutine object. - -## Checklist - -* [ ] Coroutine heap object defined. -* [ ] Stack/frames encapsulated. -* [ ] No RC/HIP remnants. -* [ ] Compiles and tests pass. - -## Tests - -* Allocate coroutine object. -* Validate state transitions manually. - -## Junie Rules - -You MAY extend heap object kinds. -You MUST NOT implement scheduling yet. -If stack representation is unclear, STOP and ask. - ---- - # PR-7.2 — Deterministic Scheduler Core ## Briefing