diff --git a/crates/console/prometeu-vm/src/lib.rs b/crates/console/prometeu-vm/src/lib.rs index 140b9990..3c8505a1 100644 --- a/crates/console/prometeu-vm/src/lib.rs +++ b/crates/console/prometeu-vm/src/lib.rs @@ -6,9 +6,11 @@ pub mod vm_init_error; pub mod object; pub mod heap; pub mod roots; +pub mod scheduler; pub use prometeu_hal::{HostContext, HostReturn, NativeInterface, SyscallId}; pub use virtual_machine::{BudgetReport, LogicalFrameEndingReason, VirtualMachine}; pub use object::{object_flags, ObjectHeader, ObjectKind}; pub use heap::{Heap, StoredObject}; pub use roots::{RootVisitor, visit_value_for_roots}; +pub use scheduler::Scheduler; diff --git a/crates/console/prometeu-vm/src/scheduler.rs b/crates/console/prometeu-vm/src/scheduler.rs new file mode 100644 index 00000000..ec94edfa --- /dev/null +++ b/crates/console/prometeu-vm/src/scheduler.rs @@ -0,0 +1,134 @@ +use std::collections::VecDeque; + +use prometeu_bytecode::HeapRef; + +/// Deterministic cooperative scheduler core. +/// +/// Policy: +/// - FIFO for ready coroutines. +/// - Sleeping coroutines are ordered by `wake_tick` and moved to ready when `wake_tick <= current_tick`. +/// - No randomness, no preemption, no context switching here. +#[derive(Debug, Default)] +pub struct Scheduler { + /// Queue of runnable coroutines (FIFO) + ready_queue: VecDeque, + /// Sleeping list kept sorted by (wake_tick, insertion_order) + sleeping: Vec, + /// Currently selected coroutine (purely informational here) + current: Option, + /// Monotonic counter to preserve deterministic order for same wake_tick + next_seq: u64, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +struct SleepEntry { + wake_tick: u64, + seq: u64, + coro: HeapRef, +} + +impl Scheduler { + pub fn new() -> Self { Self::default() } + + // ---------- Ready queue operations ---------- + + /// Enqueue a coroutine to the ready FIFO. + pub fn enqueue_ready(&mut self, coro: HeapRef) { + self.ready_queue.push_back(coro); + } + + /// Dequeue next ready coroutine (front of FIFO). + pub fn dequeue_next(&mut self) -> Option { + self.ready_queue.pop_front() + } + + pub fn is_ready_empty(&self) -> bool { self.ready_queue.is_empty() } + pub fn ready_len(&self) -> usize { self.ready_queue.len() } + + // ---------- Current tracking (no switching here) ---------- + pub fn set_current(&mut self, coro: Option) { self.current = coro; } + pub fn current(&self) -> Option { self.current } + pub fn clear_current(&mut self) { self.current = None; } + + // ---------- Sleeping operations ---------- + + /// Put a coroutine to sleep until `wake_tick` (inclusive). + /// The sleeping list is kept stably ordered to guarantee determinism. + pub fn sleep_until(&mut self, coro: HeapRef, wake_tick: u64) { + let entry = SleepEntry { wake_tick, seq: self.next_seq, coro }; + self.next_seq = self.next_seq.wrapping_add(1); + + // Binary search insertion point by wake_tick, then by seq to keep total order deterministic + let idx = match self + .sleeping + .binary_search_by(|e| { + if e.wake_tick == entry.wake_tick { + e.seq.cmp(&entry.seq) + } else { + e.wake_tick.cmp(&entry.wake_tick) + } + }) { + Ok(i) => i, // equal element position; insert after to preserve FIFO among equals + Err(i) => i, + }; + self.sleeping.insert(idx, entry); + } + + /// Move all sleeping coroutines with `wake_tick <= current_tick` to ready queue (FIFO by wake order). + pub fn wake_ready(&mut self, current_tick: u64) { + // Find split point where wake_tick > current_tick + let split = self + .sleeping + .partition_point(|e| e.wake_tick <= current_tick); + if split == 0 { return; } + let mut ready_slice: Vec = self.sleeping.drain(0..split).collect(); + // Already in order by (wake_tick, seq); push in that order to preserve determinism + for e in ready_slice.drain(..) { + self.ready_queue.push_back(e.coro); + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + fn hr(id: u32) -> HeapRef { HeapRef(id) } + + #[test] + fn fifo_ready_queue_is_deterministic() { + let mut s = Scheduler::new(); + s.enqueue_ready(hr(1)); + s.enqueue_ready(hr(2)); + s.enqueue_ready(hr(3)); + + assert_eq!(s.ready_len(), 3); + assert_eq!(s.dequeue_next(), Some(hr(1))); + assert_eq!(s.dequeue_next(), Some(hr(2))); + assert_eq!(s.dequeue_next(), Some(hr(3))); + assert_eq!(s.dequeue_next(), None); + } + + #[test] + fn sleeping_wake_is_stable_and_fifo_by_insertion() { + let mut s = Scheduler::new(); + s.sleep_until(hr(10), 5); // first with wake at 5 + s.sleep_until(hr(11), 5); // second with same wake + s.sleep_until(hr(12), 6); // later wake + + // Before tick 5: nothing wakes + s.wake_ready(4); + assert!(s.is_ready_empty()); + + // At tick 5: first two wake in insertion order + s.wake_ready(5); + assert_eq!(s.dequeue_next(), Some(hr(10))); + assert_eq!(s.dequeue_next(), Some(hr(11))); + assert!(s.is_ready_empty()); + + // At tick 6: last one wakes + s.wake_ready(6); + assert_eq!(s.dequeue_next(), Some(hr(12))); + assert!(s.is_ready_empty()); + } +} diff --git a/files/TODOs.md b/files/TODOs.md index fa63c043..754d2384 100644 --- a/files/TODOs.md +++ b/files/TODOs.md @@ -1,41 +1,3 @@ -# PR-7.2 — Deterministic Scheduler Core - -## Briefing - -Implement a cooperative deterministic scheduler. - -## Target - -Scheduler structure inside VM: - -* `ready_queue: VecDeque` -* `sleeping: Vec` (sorted or scanned by wake_tick) -* `current: Option` - -Policy: - -* FIFO for ready coroutines. -* Sleeping coroutines move to ready when `wake_tick <= current_tick`. - -No execution switching yet. - -## Checklist - -* [ ] Scheduler struct exists. -* [ ] Deterministic FIFO behavior. -* [ ] No randomness. - -## Tests - -* Enqueue 3 coroutines and ensure dequeue order stable. - -## Junie Rules - -You MAY add scheduler struct. -You MUST NOT implement SPAWN/YIELD/SLEEP yet. - ---- - # PR-7.3 — SPAWN Instruction ## Briefing