pr7.5
This commit is contained in:
parent
b7e149a1ab
commit
3f50bdaa70
@ -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,
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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,
|
||||||
|
|||||||
@ -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);
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user