This commit is contained in:
bQUARKz 2026-02-20 11:15:50 +00:00
parent b7e149a1ab
commit 3f50bdaa70
Signed by: bquarkz
SSH Key Fingerprint: SHA256:Z7dgqoglWwoK6j6u4QC87OveEq74WOhFN+gitsxtkf8
4 changed files with 139 additions and 66 deletions

View File

@ -188,6 +188,14 @@ pub enum OpCode {
/// may switch to another ready coroutine. /// may switch to another ready coroutine.
Yield = 0x55, Yield = 0x55,
/// Suspends the current coroutine for a number of logical ticks.
/// Operand: duration_ticks (u32)
/// Semantics:
/// - Set the coroutine wake tick to `current_tick + duration_ticks`.
/// - End the current logical frame (as if reaching FRAME_SYNC).
/// - The coroutine will resume execution on or after the wake tick.
Sleep = 0x56,
// --- 6.8 Peripherals and System --- // --- 6.8 Peripherals and System ---
/// Invokes a system function (Firmware/OS). /// Invokes a system function (Firmware/OS).
/// Operand: syscall_id (u32) /// Operand: syscall_id (u32)
@ -251,6 +259,7 @@ impl TryFrom<u16> for OpCode {
0x53 => Ok(OpCode::CallClosure), 0x53 => Ok(OpCode::CallClosure),
0x54 => Ok(OpCode::Spawn), 0x54 => Ok(OpCode::Spawn),
0x55 => Ok(OpCode::Yield), 0x55 => Ok(OpCode::Yield),
0x56 => Ok(OpCode::Sleep),
0x70 => Ok(OpCode::Syscall), 0x70 => Ok(OpCode::Syscall),
0x80 => Ok(OpCode::FrameSync), 0x80 => Ok(OpCode::FrameSync),
_ => Err(format!("Invalid OpCode: 0x{:04X}", value)), _ => Err(format!("Invalid OpCode: 0x{:04X}", value)),
@ -311,6 +320,7 @@ impl OpCode {
OpCode::CallClosure => 6, OpCode::CallClosure => 6,
OpCode::Spawn => 6, OpCode::Spawn => 6,
OpCode::Yield => 1, OpCode::Yield => 1,
OpCode::Sleep => 1,
OpCode::Syscall => 1, OpCode::Syscall => 1,
OpCode::FrameSync => 1, OpCode::FrameSync => 1,
} }

View File

@ -510,6 +510,19 @@ impl OpCodeSpecExt for OpCode {
// Treated as a safepoint marker for cooperative scheduling // Treated as a safepoint marker for cooperative scheduling
is_safepoint: true, is_safepoint: true,
}, },
OpCode::Sleep => OpcodeSpec {
name: "SLEEP",
// One u32 immediate: duration_ticks
imm_bytes: 4,
pops: 0,
pushes: 0,
is_branch: false,
// Ends execution at safepoint after instruction completes
is_terminator: false,
may_trap: false,
// Considered a safepoint since it forces a frame boundary
is_safepoint: true,
},
OpCode::Syscall => OpcodeSpec { OpCode::Syscall => OpcodeSpec {
name: "SYSCALL", name: "SYSCALL",
imm_bytes: 4, imm_bytes: 4,

View File

@ -87,6 +87,13 @@ pub struct VirtualMachine {
/// Cooperative scheduler: set to true when `YIELD` opcode is executed. /// Cooperative scheduler: set to true when `YIELD` opcode is executed.
/// The runtime/scheduler should only act on this at safepoints (FRAME_SYNC). /// The runtime/scheduler should only act on this at safepoints (FRAME_SYNC).
pub yield_requested: bool, pub yield_requested: bool,
/// Logical tick counter advanced at each FRAME_SYNC boundary.
pub current_tick: u64,
/// If set, the current coroutine is sleeping until this tick (inclusive).
/// While sleeping and before `current_tick >= wake`, the VM will end the
/// logical frame immediately at the start of `step()` and after executing
/// `SLEEP`.
pub sleep_until_tick: Option<u64>,
} }
@ -119,6 +126,8 @@ impl VirtualMachine {
last_gc_live_count: 0, last_gc_live_count: 0,
capabilities: 0, capabilities: 0,
yield_requested: false, yield_requested: false,
current_tick: 0,
sleep_until_tick: None,
} }
} }
@ -140,6 +149,8 @@ impl VirtualMachine {
self.cycles = 0; self.cycles = 0;
self.halted = true; // execution is impossible until a successful load self.halted = true; // execution is impossible until a successful load
self.last_gc_live_count = 0; self.last_gc_live_count = 0;
self.current_tick = 0;
self.sleep_until_tick = None;
// Preserve capabilities across loads; firmware may set them per cart. // Preserve capabilities across loads; firmware may set them per cart.
// Only recognized format is loadable: PBS v0 industrial format // Only recognized format is loadable: PBS v0 industrial format
@ -324,6 +335,16 @@ impl VirtualMachine {
native: &mut dyn NativeInterface, native: &mut dyn NativeInterface,
ctx: &mut HostContext, ctx: &mut HostContext,
) -> Result<(), LogicalFrameEndingReason> { ) -> Result<(), LogicalFrameEndingReason> {
// If the current coroutine is sleeping and hasn't reached its wake tick,
// immediately end the logical frame to respect suspension semantics.
if let Some(wake) = self.sleep_until_tick {
if self.current_tick < wake {
// Consume FRAME_SYNC cost and perform safepoint duties.
self.cycles += OpCode::FrameSync.cycles();
self.handle_safepoint();
return Err(LogicalFrameEndingReason::FrameSync);
}
}
if self.halted || self.pc >= self.program.rom.len() { if self.halted || self.pc >= self.program.rom.len() {
return Ok(()); return Ok(());
} }
@ -423,6 +444,20 @@ impl VirtualMachine {
self.yield_requested = true; self.yield_requested = true;
// Do not end the slice here; we continue executing until a safepoint. // Do not end the slice here; we continue executing until a safepoint.
} }
OpCode::Sleep => {
// Immediate is duration in ticks
let duration = instr
.imm_u32()
.map_err(|e| LogicalFrameEndingReason::Panic(format!("{:?}", e)))? as u64;
let wake = self.current_tick.saturating_add(duration);
self.sleep_until_tick = Some(wake);
// End the logical frame right after the instruction completes
// to ensure no further instructions run until at least next tick.
self.cycles += OpCode::FrameSync.cycles();
self.handle_safepoint();
return Err(LogicalFrameEndingReason::FrameSync);
}
OpCode::MakeClosure => { OpCode::MakeClosure => {
// Immediate carries (fn_id, capture_count) // Immediate carries (fn_id, capture_count)
let (fn_id, cap_count) = instr let (fn_id, cap_count) = instr
@ -1118,32 +1153,7 @@ impl VirtualMachine {
OpCode::FrameSync => { OpCode::FrameSync => {
// Marks the logical end of a frame: consume cycles and signal to the driver // Marks the logical end of a frame: consume cycles and signal to the driver
self.cycles += OpCode::FrameSync.cycles(); self.cycles += OpCode::FrameSync.cycles();
self.handle_safepoint();
// GC Safepoint: only at FRAME_SYNC
if self.gc_alloc_threshold > 0 {
let live_now = self.heap.len();
let since_last = live_now.saturating_sub(self.last_gc_live_count);
if since_last >= self.gc_alloc_threshold {
// Collect GC roots from VM state
struct CollectRoots(Vec<prometeu_bytecode::HeapRef>);
impl crate::roots::RootVisitor for CollectRoots {
fn visit_heap_ref(&mut self, r: prometeu_bytecode::HeapRef) {
self.0.push(r);
}
}
let mut collector = CollectRoots(Vec::new());
self.visit_roots(&mut collector);
// Run mark-sweep
self.heap.mark_from_roots(collector.0);
self.heap.sweep();
// Update baseline for next cycles
self.last_gc_live_count = self.heap.len();
}
}
// Clear cooperative yield request at the safepoint boundary.
self.yield_requested = false;
return Err(LogicalFrameEndingReason::FrameSync); return Err(LogicalFrameEndingReason::FrameSync);
} }
} }
@ -1153,6 +1163,45 @@ impl VirtualMachine {
Ok(()) Ok(())
} }
/// Perform safepoint duties that occur at logical frame boundaries.
/// Runs GC if thresholds are reached, clears cooperative yield flag,
/// and advances the logical tick counter.
fn handle_safepoint(&mut self) {
// GC Safepoint: only at FRAME_SYNC-like boundaries
if self.gc_alloc_threshold > 0 {
let live_now = self.heap.len();
let since_last = live_now.saturating_sub(self.last_gc_live_count);
if since_last >= self.gc_alloc_threshold {
// Collect GC roots from VM state
struct CollectRoots(Vec<prometeu_bytecode::HeapRef>);
impl crate::roots::RootVisitor for CollectRoots {
fn visit_heap_ref(&mut self, r: prometeu_bytecode::HeapRef) { self.0.push(r); }
}
let mut collector = CollectRoots(Vec::new());
self.visit_roots(&mut collector);
// Run mark-sweep
self.heap.mark_from_roots(collector.0);
self.heap.sweep();
// Update baseline for next cycles
self.last_gc_live_count = self.heap.len();
}
}
// Advance logical tick at every frame boundary.
self.current_tick = self.current_tick.wrapping_add(1);
// If we've passed the wake tick, clear the sleep so execution can resume next frame.
if let Some(wake) = self.sleep_until_tick {
if self.current_tick >= wake {
self.sleep_until_tick = None;
}
}
// Clear cooperative yield request at the safepoint boundary.
self.yield_requested = false;
}
pub fn trap( pub fn trap(
&self, &self,
code: u32, code: u32,
@ -1275,6 +1324,47 @@ mod tests {
} }
#[test] #[test]
fn sleep_delays_execution_by_ticks() {
let mut native = MockNative;
let mut ctx = HostContext::new(None);
// Program:
// SLEEP 2
// PUSH_I32 123
// FRAME_SYNC
// HALT
let mut rom = Vec::new();
rom.extend_from_slice(&(OpCode::Sleep as u16).to_le_bytes());
rom.extend_from_slice(&(2u32).to_le_bytes());
rom.extend_from_slice(&(OpCode::PushI32 as u16).to_le_bytes());
rom.extend_from_slice(&123i32.to_le_bytes());
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, vec![]);
// Frame 1: executing SLEEP 2 will force a frame end and advance tick to 1
let rep1 = vm.run_budget(100, &mut native, &mut ctx).expect("run ok");
assert!(matches!(rep1.reason, LogicalFrameEndingReason::FrameSync));
assert!(vm.operand_stack.is_empty());
assert_eq!(vm.current_tick, 1);
// Frame 2: still sleeping (tick 1 < wake 2), immediate FrameSync, tick -> 2
let rep2 = vm.run_budget(100, &mut native, &mut ctx).expect("run ok");
assert!(matches!(rep2.reason, LogicalFrameEndingReason::FrameSync));
assert!(vm.operand_stack.is_empty());
assert_eq!(vm.current_tick, 2);
// Frame 3: wake condition met (current_tick >= wake), execute PUSH_I32 then FRAME_SYNC
let rep3 = vm.run_budget(100, &mut native, &mut ctx).expect("run ok");
assert!(matches!(rep3.reason, LogicalFrameEndingReason::FrameSync));
// Value should now be on the stack
assert_eq!(vm.peek().unwrap(), &Value::Int32(123));
// Next frame should hit HALT without errors
let res = vm.run_budget(100, &mut native, &mut ctx);
assert!(res.is_ok());
}
fn test_arithmetic_chain() { fn test_arithmetic_chain() {
let mut native = MockNative; let mut native = MockNative;
let mut ctx = HostContext::new(None); let mut ctx = HostContext::new(None);

View File

@ -1,43 +1,3 @@
# PR-7.5 — SLEEP Instruction
## Briefing
SLEEP suspends coroutine until a future tick.
## Target
Opcode:
`SLEEP duration_ticks`
Semantics:
* Remove coroutine from ready queue.
* Set wake_tick.
* Add to sleeping list.
At each FRAME_SYNC:
* Check sleeping coroutines.
* Move ready ones to ready_queue.
## Checklist
* [ ] SLEEP implemented.
* [ ] wake_tick respected.
* [ ] Deterministic wake behavior.
## Tests
* Sleep and verify delayed execution.
## Junie Rules
You MAY add tick tracking.
You MUST NOT rely on real wall clock time.
---
# PR-7.6 — Safepoint Integration # PR-7.6 — Safepoint Integration
## Briefing ## Briefing