pr7.11
This commit is contained in:
parent
f6d33cd8e5
commit
c07a1cc230
@ -52,7 +52,8 @@ impl Scheduler {
|
||||
|
||||
// ---------- Sleeping operations ----------
|
||||
|
||||
/// Put a coroutine to sleep until `wake_tick` (inclusive).
|
||||
/// Put a coroutine to sleep until `wake_tick`.
|
||||
/// A coroutine is woken when `current_tick >= wake_tick`.
|
||||
/// 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 };
|
||||
|
||||
@ -89,7 +89,16 @@ pub struct VirtualMachine {
|
||||
/// Cooperative scheduler: set to true when `YIELD` opcode is executed.
|
||||
/// The runtime/scheduler should only act on this at safepoints (FRAME_SYNC).
|
||||
pub yield_requested: bool,
|
||||
/// If set, the current coroutine requested to sleep until this tick (inclusive).
|
||||
/// Absolute wake tick requested by the currently running coroutine (when it executes `SLEEP`).
|
||||
///
|
||||
/// Canonical rule (authoritative):
|
||||
/// - `SLEEP N` suspends the coroutine for exactly N full scheduler ticks AFTER the current
|
||||
/// `FRAME_SYNC` completes. If `SLEEP` is executed during tick `T`, the coroutine must resume
|
||||
/// in the frame whose end-of-frame tick will be `T + N + 1`.
|
||||
/// - Implementation detail: we compute `wake_tick = current_tick + duration + 1` at the time
|
||||
/// `SLEEP` executes. The scheduler wakes sleeping coroutines when `current_tick >= wake_tick`.
|
||||
///
|
||||
/// This definition is deterministic and eliminates off-by-one ambiguity.
|
||||
pub sleep_requested_until: Option<u64>,
|
||||
/// Logical tick counter advanced at each FRAME_SYNC boundary.
|
||||
pub current_tick: u64,
|
||||
@ -261,14 +270,22 @@ impl VirtualMachine {
|
||||
func_idx,
|
||||
});
|
||||
|
||||
// Initialize the main coroutine to own the current execution context.
|
||||
// State = Running; not enqueued into ready queue.
|
||||
// Initialize the main coroutine object.
|
||||
// IMPORTANT INVARIANT:
|
||||
// - The RUNNING coroutine's authoritative execution state lives in the VM fields
|
||||
// (pc, operand_stack, call_stack).
|
||||
// - The heap-side coroutine object is authoritative ONLY when the coroutine is suspended
|
||||
// (Ready/Sleeping/Finished). While running, its `stack`/`frames` should be empty.
|
||||
//
|
||||
// Therefore we do NOT clone the VM stacks into the heap here. We create the main
|
||||
// coroutine object with empty stack/frames and mark it as Running, and the VM already
|
||||
// holds the live execution context initialized above.
|
||||
let main_href = self.heap.allocate_coroutine(
|
||||
self.pc,
|
||||
CoroutineState::Running,
|
||||
0,
|
||||
self.operand_stack.clone(),
|
||||
self.call_stack.clone(),
|
||||
Vec::new(),
|
||||
Vec::new(),
|
||||
);
|
||||
self.current_coro = Some(main_href);
|
||||
self.scheduler.set_current(self.current_coro);
|
||||
@ -524,11 +541,22 @@ impl VirtualMachine {
|
||||
// Do not end the slice here; we continue executing until a safepoint.
|
||||
}
|
||||
OpCode::Sleep => {
|
||||
// Immediate is duration in ticks
|
||||
// Immediate is duration in ticks.
|
||||
//
|
||||
// Canonical semantics:
|
||||
// SLEEP N => suspend for exactly N full scheduler ticks AFTER the current
|
||||
// FRAME_SYNC completes. If executed at tick T, resume in the frame whose
|
||||
// end-of-frame tick will be T + N + 1.
|
||||
//
|
||||
// Implementation rule:
|
||||
// wake_tick = current_tick + duration + 1
|
||||
let duration = instr
|
||||
.imm_u32()
|
||||
.map_err(|e| LogicalFrameEndingReason::Panic(format!("{:?}", e)))? as u64;
|
||||
let wake = self.current_tick.saturating_add(duration);
|
||||
let wake = self
|
||||
.current_tick
|
||||
.saturating_add(duration)
|
||||
.saturating_add(1);
|
||||
self.sleep_requested_until = Some(wake);
|
||||
|
||||
// End the logical frame right after the instruction completes
|
||||
@ -1521,7 +1549,7 @@ mod tests {
|
||||
assert!(vm.operand_stack.is_empty());
|
||||
assert_eq!(vm.current_tick, 1);
|
||||
|
||||
// Frame 2: still sleeping (tick 1 < wake 2), immediate FrameSync, tick -> 2
|
||||
// Frame 2: still sleeping (tick 1 < wake 3), immediate FrameSync, tick -> 2
|
||||
let rep2 = vm.run_budget(100, &mut native, &mut ctx).expect("run ok");
|
||||
assert!(matches!(rep2.reason, LogicalFrameEndingReason::FrameSync));
|
||||
// In the per-coroutine model, the VM may keep current context intact across idle frames;
|
||||
@ -1529,9 +1557,15 @@ mod tests {
|
||||
assert_eq!(vm.operand_stack.len(), 0);
|
||||
assert_eq!(vm.current_tick, 2);
|
||||
|
||||
// Frame 3: wake condition met (current_tick >= wake), execute PUSH_I32 then FRAME_SYNC
|
||||
// Frame 3: still sleeping (tick 2 < wake 3), immediate FrameSync, tick -> 3
|
||||
let rep3 = vm.run_budget(100, &mut native, &mut ctx).expect("run ok");
|
||||
assert!(matches!(rep3.reason, LogicalFrameEndingReason::FrameSync));
|
||||
assert_eq!(vm.operand_stack.len(), 0);
|
||||
assert_eq!(vm.current_tick, 3);
|
||||
|
||||
// Frame 4: wake condition met (current_tick >= wake), execute PUSH_I32 then FRAME_SYNC
|
||||
let rep4 = vm.run_budget(100, &mut native, &mut ctx).expect("run ok");
|
||||
assert!(matches!(rep4.reason, LogicalFrameEndingReason::FrameSync));
|
||||
// Value should now be on the stack
|
||||
assert_eq!(vm.peek().unwrap(), &Value::Int32(123));
|
||||
|
||||
@ -1636,12 +1670,18 @@ mod tests {
|
||||
ticks_a.push((vm_a.pc, vm_a.current_tick, format!("{:?}", ra2.reason)));
|
||||
ticks_b.push((vm_b.pc, vm_b.current_tick, format!("{:?}", rb2.reason)));
|
||||
|
||||
// Slice 3 (wakes and pushes)
|
||||
// Slice 3
|
||||
let ra3 = vm_a.run_budget(100, &mut native, &mut ctx_a).unwrap();
|
||||
let rb3 = vm_b.run_budget(100, &mut native, &mut ctx_b).unwrap();
|
||||
ticks_a.push((vm_a.pc, vm_a.current_tick, format!("{:?}", ra3.reason)));
|
||||
ticks_b.push((vm_b.pc, vm_b.current_tick, format!("{:?}", rb3.reason)));
|
||||
|
||||
// Slice 4 (wakes and pushes)
|
||||
let ra4 = vm_a.run_budget(100, &mut native, &mut ctx_a).unwrap();
|
||||
let rb4 = vm_b.run_budget(100, &mut native, &mut ctx_b).unwrap();
|
||||
ticks_a.push((vm_a.pc, vm_a.current_tick, format!("{:?}", ra4.reason)));
|
||||
ticks_b.push((vm_b.pc, vm_b.current_tick, format!("{:?}", rb4.reason)));
|
||||
|
||||
assert_eq!(ticks_a, ticks_b, "Sleep/wake slices must match across runs");
|
||||
assert_eq!(vm_a.peek().unwrap(), &Value::Int32(7));
|
||||
assert_eq!(vm_b.peek().unwrap(), &Value::Int32(7));
|
||||
@ -3374,6 +3414,370 @@ mod tests {
|
||||
assert_eq!(vm.heap.len(), 1, "only main coroutine should remain");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_coroutines_strict_alternation_with_yield() {
|
||||
use prometeu_bytecode::FunctionMeta;
|
||||
|
||||
// Build function A: PUSH 1; YIELD; FRAME_SYNC; JMP 0 (loop)
|
||||
let mut fn_a = Vec::new();
|
||||
fn_a.extend_from_slice(&(OpCode::PushI32 as u16).to_le_bytes());
|
||||
fn_a.extend_from_slice(&1i32.to_le_bytes());
|
||||
fn_a.extend_from_slice(&(OpCode::Yield as u16).to_le_bytes());
|
||||
fn_a.extend_from_slice(&(OpCode::FrameSync as u16).to_le_bytes());
|
||||
fn_a.extend_from_slice(&(OpCode::Jmp as u16).to_le_bytes());
|
||||
fn_a.extend_from_slice(&0u32.to_le_bytes());
|
||||
|
||||
// Build function B: PUSH 2; YIELD; FRAME_SYNC; JMP 0 (loop)
|
||||
let mut fn_b = Vec::new();
|
||||
fn_b.extend_from_slice(&(OpCode::PushI32 as u16).to_le_bytes());
|
||||
fn_b.extend_from_slice(&2i32.to_le_bytes());
|
||||
fn_b.extend_from_slice(&(OpCode::Yield as u16).to_le_bytes());
|
||||
fn_b.extend_from_slice(&(OpCode::FrameSync as u16).to_le_bytes());
|
||||
fn_b.extend_from_slice(&(OpCode::Jmp as u16).to_le_bytes());
|
||||
fn_b.extend_from_slice(&0u32.to_le_bytes());
|
||||
|
||||
// Main: SPAWN A; SPAWN B; SLEEP 100; HALT
|
||||
let mut main = Vec::new();
|
||||
main.extend_from_slice(&(OpCode::Spawn as u16).to_le_bytes());
|
||||
main.extend_from_slice(&1u32.to_le_bytes()); // fn A idx
|
||||
main.extend_from_slice(&0u32.to_le_bytes()); // arg count
|
||||
main.extend_from_slice(&(OpCode::Spawn as u16).to_le_bytes());
|
||||
main.extend_from_slice(&2u32.to_le_bytes()); // fn B idx
|
||||
main.extend_from_slice(&0u32.to_le_bytes()); // arg count
|
||||
main.extend_from_slice(&(OpCode::Sleep as u16).to_le_bytes());
|
||||
main.extend_from_slice(&100u32.to_le_bytes());
|
||||
main.extend_from_slice(&(OpCode::Halt as u16).to_le_bytes());
|
||||
|
||||
// Assemble ROM: [main][A][B]
|
||||
let off_main = 0usize;
|
||||
let off_a = main.len();
|
||||
let off_b = off_a + fn_a.len();
|
||||
let mut rom = Vec::with_capacity(main.len() + fn_a.len() + fn_b.len());
|
||||
rom.extend_from_slice(&main);
|
||||
rom.extend_from_slice(&fn_a);
|
||||
rom.extend_from_slice(&fn_b);
|
||||
|
||||
// VM with three functions (0=main, 1=A, 2=B)
|
||||
let mut vm = new_test_vm(rom.clone(), vec![]);
|
||||
vm.program.functions = std::sync::Arc::from(vec![
|
||||
FunctionMeta { code_offset: off_main as u32, code_len: main.len() as u32, param_slots: 0, local_slots: 0, return_slots: 0, max_stack_slots: 8 },
|
||||
FunctionMeta { code_offset: off_a as u32, code_len: fn_a.len() as u32, param_slots: 0, local_slots: 0, return_slots: 0, max_stack_slots: 8 },
|
||||
FunctionMeta { code_offset: off_b as u32, code_len: fn_b.len() as u32, param_slots: 0, local_slots: 0, return_slots: 0, max_stack_slots: 8 },
|
||||
]);
|
||||
|
||||
let mut native = MockNative;
|
||||
let mut ctx = HostContext::new(None);
|
||||
|
||||
// Frame 1: main sleeps; from now on A and B should strictly alternate.
|
||||
let _ = vm.run_budget(100, &mut native, &mut ctx).unwrap();
|
||||
|
||||
// Locate coroutine handles for A (fn_idx=1) and B (fn_idx=2)
|
||||
let mut a_href = None;
|
||||
let mut b_href = None;
|
||||
// Consider currently running coroutine
|
||||
if let Some(cur) = vm.current_coro {
|
||||
if let Some(f) = vm.call_stack.last() {
|
||||
if f.func_idx == 1 { a_href = Some(cur); }
|
||||
if f.func_idx == 2 { b_href = Some(cur); }
|
||||
}
|
||||
}
|
||||
// And also consider suspended (Ready/Sleeping) coroutines
|
||||
for h in vm.heap.suspended_coroutine_handles() {
|
||||
if let Some(co) = vm.heap.coroutine_data(h) {
|
||||
if let Some(f) = co.frames.last() {
|
||||
if f.func_idx == 1 { a_href = Some(h); }
|
||||
if f.func_idx == 2 { b_href = Some(h); }
|
||||
}
|
||||
}
|
||||
}
|
||||
let a_href = a_href.expect("coroutine A not found");
|
||||
let b_href = b_href.expect("coroutine B not found");
|
||||
|
||||
let mut prev_a = vm.heap.coroutine_data(a_href).unwrap().stack.len();
|
||||
let mut prev_b = vm.heap.coroutine_data(b_href).unwrap().stack.len();
|
||||
|
||||
let mut trace = Vec::new();
|
||||
for _ in 0..6 {
|
||||
let _ = vm.run_budget(100, &mut native, &mut ctx).unwrap();
|
||||
let a_now = vm.heap.coroutine_data(a_href).unwrap().stack.len();
|
||||
let b_now = vm.heap.coroutine_data(b_href).unwrap().stack.len();
|
||||
if a_now > prev_a { trace.push(1); }
|
||||
else if b_now > prev_b { trace.push(2); }
|
||||
else { panic!("no coroutine progress detected this frame"); }
|
||||
prev_a = a_now; prev_b = b_now;
|
||||
}
|
||||
|
||||
assert_eq!(trace, vec![1, 2, 1, 2, 1, 2], "Coroutines must strictly alternate under Yield");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_sleep_does_not_stall_others_and_wakes_at_exact_tick() {
|
||||
use prometeu_bytecode::FunctionMeta;
|
||||
|
||||
// Function A: SLEEP N; PUSH 100; YIELD; FRAME_SYNC; HALT
|
||||
let sleep_n: u32 = 3;
|
||||
let mut fn_a = Vec::new();
|
||||
fn_a.extend_from_slice(&(OpCode::Sleep as u16).to_le_bytes());
|
||||
fn_a.extend_from_slice(&sleep_n.to_le_bytes());
|
||||
fn_a.extend_from_slice(&(OpCode::PushI32 as u16).to_le_bytes());
|
||||
fn_a.extend_from_slice(&100i32.to_le_bytes());
|
||||
fn_a.extend_from_slice(&(OpCode::Yield as u16).to_le_bytes());
|
||||
fn_a.extend_from_slice(&(OpCode::FrameSync as u16).to_le_bytes());
|
||||
fn_a.extend_from_slice(&(OpCode::Halt as u16).to_le_bytes());
|
||||
|
||||
// Function B: PUSH 1; YIELD; FRAME_SYNC; JMP 0 (increments every frame)
|
||||
let mut fn_b = Vec::new();
|
||||
fn_b.extend_from_slice(&(OpCode::PushI32 as u16).to_le_bytes());
|
||||
fn_b.extend_from_slice(&1i32.to_le_bytes());
|
||||
fn_b.extend_from_slice(&(OpCode::Yield as u16).to_le_bytes());
|
||||
fn_b.extend_from_slice(&(OpCode::FrameSync as u16).to_le_bytes());
|
||||
fn_b.extend_from_slice(&(OpCode::Jmp as u16).to_le_bytes());
|
||||
fn_b.extend_from_slice(&0u32.to_le_bytes());
|
||||
|
||||
// Main: SPAWN A; SPAWN B; SLEEP big; HALT
|
||||
let mut main = Vec::new();
|
||||
main.extend_from_slice(&(OpCode::Spawn as u16).to_le_bytes());
|
||||
main.extend_from_slice(&1u32.to_le_bytes());
|
||||
main.extend_from_slice(&0u32.to_le_bytes());
|
||||
main.extend_from_slice(&(OpCode::Spawn as u16).to_le_bytes());
|
||||
main.extend_from_slice(&2u32.to_le_bytes());
|
||||
main.extend_from_slice(&0u32.to_le_bytes());
|
||||
main.extend_from_slice(&(OpCode::Sleep as u16).to_le_bytes());
|
||||
main.extend_from_slice(&100u32.to_le_bytes());
|
||||
main.extend_from_slice(&(OpCode::Halt as u16).to_le_bytes());
|
||||
|
||||
let off_main = 0usize;
|
||||
let off_a = main.len();
|
||||
let off_b = off_a + fn_a.len();
|
||||
let mut rom = Vec::new();
|
||||
rom.extend_from_slice(&main);
|
||||
rom.extend_from_slice(&fn_a);
|
||||
rom.extend_from_slice(&fn_b);
|
||||
|
||||
let mut vm = new_test_vm(rom.clone(), vec![]);
|
||||
vm.program.functions = std::sync::Arc::from(vec![
|
||||
FunctionMeta { code_offset: off_main as u32, code_len: main.len() as u32, param_slots: 0, local_slots: 0, return_slots: 0, max_stack_slots: 8 },
|
||||
FunctionMeta { code_offset: off_a as u32, code_len: fn_a.len() as u32, param_slots: 0, local_slots: 0, return_slots: 0, max_stack_slots: 8 },
|
||||
FunctionMeta { code_offset: off_b as u32, code_len: fn_b.len() as u32, param_slots: 0, local_slots: 0, return_slots: 0, max_stack_slots: 8 },
|
||||
]);
|
||||
|
||||
let mut native = MockNative;
|
||||
let mut ctx = HostContext::new(None);
|
||||
|
||||
// Frame 1: main sleeps, tick -> 1
|
||||
let _ = vm.run_budget(100, &mut native, &mut ctx).unwrap();
|
||||
assert_eq!(vm.current_tick, 1);
|
||||
|
||||
// Identify A and B coroutine handles (consider both running and suspended)
|
||||
let mut a_href = None;
|
||||
let mut b_href = None;
|
||||
if let Some(cur) = vm.current_coro {
|
||||
if let Some(f) = vm.call_stack.last() {
|
||||
if f.func_idx == 1 { a_href = Some(cur); }
|
||||
if f.func_idx == 2 { b_href = Some(cur); }
|
||||
}
|
||||
}
|
||||
for h in vm.heap.suspended_coroutine_handles() {
|
||||
if let Some(co) = vm.heap.coroutine_data(h) {
|
||||
if let Some(f) = co.frames.last() {
|
||||
if f.func_idx == 1 { a_href = Some(h); }
|
||||
if f.func_idx == 2 { b_href = Some(h); }
|
||||
}
|
||||
}
|
||||
}
|
||||
let a_href = a_href.expect("A not found");
|
||||
let b_href = b_href.expect("B not found");
|
||||
// Count how many frames B runs while A sleeps using the scheduler's next-to-run handle.
|
||||
// Stop when A is scheduled to run, then execute that frame and record its end-of-frame tick.
|
||||
let mut ones_before = 0usize;
|
||||
let mut woke_at_tick = 0u64;
|
||||
let mut seen_a_once = false;
|
||||
for _ in 0..1000 {
|
||||
if let Some(next) = vm.scheduler.current() {
|
||||
if next == a_href {
|
||||
if seen_a_once {
|
||||
// A has slept before and is about to run again (wake). Run and record.
|
||||
let _ = vm.run_budget(100, &mut native, &mut ctx).unwrap();
|
||||
woke_at_tick = vm.current_tick;
|
||||
break;
|
||||
} else {
|
||||
// First time A runs (to execute SLEEP N); do not count as wake yet.
|
||||
seen_a_once = true;
|
||||
}
|
||||
} else if next == b_href {
|
||||
ones_before += 1;
|
||||
}
|
||||
}
|
||||
let _ = vm.run_budget(100, &mut native, &mut ctx).unwrap();
|
||||
}
|
||||
|
||||
// Canonical semantics: wake_tick = current_tick_at_sleep + N + 1.
|
||||
// The scheduler wakes sleepers at the end of that tick, so the coroutine runs
|
||||
// in the following frame, and we observe its heap stack update at end tick = wake_tick + 1.
|
||||
// A executes SLEEP at its first run (tick 1), so wake_tick = 1 + N + 1, observed tick = +1.
|
||||
let expected_observed_end_tick = 1u64 + sleep_n as u64 + 2u64;
|
||||
assert_eq!(woke_at_tick, expected_observed_end_tick, "A must wake at the exact tick (+1 frame to observe)");
|
||||
// And B must have produced at least N items (one per frame) before A's wake.
|
||||
assert!(ones_before as u64 >= sleep_n as u64, "B must keep running while A sleeps");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_multi_coroutine_determinism_across_runs() {
|
||||
use prometeu_bytecode::FunctionMeta;
|
||||
|
||||
// Reuse alternation program from previous test
|
||||
let mut fn_a = Vec::new();
|
||||
fn_a.extend_from_slice(&(OpCode::PushI32 as u16).to_le_bytes());
|
||||
fn_a.extend_from_slice(&1i32.to_le_bytes());
|
||||
fn_a.extend_from_slice(&(OpCode::Yield as u16).to_le_bytes());
|
||||
fn_a.extend_from_slice(&(OpCode::FrameSync as u16).to_le_bytes());
|
||||
fn_a.extend_from_slice(&(OpCode::Jmp as u16).to_le_bytes());
|
||||
fn_a.extend_from_slice(&0u32.to_le_bytes());
|
||||
|
||||
let mut fn_b = Vec::new();
|
||||
fn_b.extend_from_slice(&(OpCode::PushI32 as u16).to_le_bytes());
|
||||
fn_b.extend_from_slice(&2i32.to_le_bytes());
|
||||
fn_b.extend_from_slice(&(OpCode::Yield as u16).to_le_bytes());
|
||||
fn_b.extend_from_slice(&(OpCode::FrameSync as u16).to_le_bytes());
|
||||
fn_b.extend_from_slice(&(OpCode::Jmp as u16).to_le_bytes());
|
||||
fn_b.extend_from_slice(&0u32.to_le_bytes());
|
||||
|
||||
let mut main = Vec::new();
|
||||
main.extend_from_slice(&(OpCode::Spawn as u16).to_le_bytes());
|
||||
main.extend_from_slice(&1u32.to_le_bytes());
|
||||
main.extend_from_slice(&0u32.to_le_bytes());
|
||||
main.extend_from_slice(&(OpCode::Spawn as u16).to_le_bytes());
|
||||
main.extend_from_slice(&2u32.to_le_bytes());
|
||||
main.extend_from_slice(&0u32.to_le_bytes());
|
||||
main.extend_from_slice(&(OpCode::Sleep as u16).to_le_bytes());
|
||||
main.extend_from_slice(&100u32.to_le_bytes());
|
||||
main.extend_from_slice(&(OpCode::Halt as u16).to_le_bytes());
|
||||
|
||||
let off_main = 0usize;
|
||||
let off_a = main.len();
|
||||
let off_b = off_a + fn_a.len();
|
||||
let mut rom = Vec::new();
|
||||
rom.extend_from_slice(&main);
|
||||
rom.extend_from_slice(&fn_a);
|
||||
rom.extend_from_slice(&fn_b);
|
||||
|
||||
let mut vm1 = new_test_vm(rom.clone(), vec![]);
|
||||
let mut vm2 = new_test_vm(rom.clone(), vec![]);
|
||||
let fm: std::sync::Arc<[prometeu_bytecode::FunctionMeta]> = std::sync::Arc::from(vec![
|
||||
FunctionMeta { code_offset: off_main as u32, code_len: main.len() as u32, param_slots: 0, local_slots: 0, return_slots: 0, max_stack_slots: 8 },
|
||||
FunctionMeta { code_offset: off_a as u32, code_len: fn_a.len() as u32, param_slots: 0, local_slots: 0, return_slots: 0, max_stack_slots: 8 },
|
||||
FunctionMeta { code_offset: off_b as u32, code_len: fn_b.len() as u32, param_slots: 0, local_slots: 0, return_slots: 0, max_stack_slots: 8 },
|
||||
]);
|
||||
vm1.program.functions = fm.clone();
|
||||
vm2.program.functions = fm;
|
||||
|
||||
let mut native = MockNative;
|
||||
let mut c1 = HostContext::new(None);
|
||||
let mut c2 = HostContext::new(None);
|
||||
|
||||
// Burn first frame (main sleeps)
|
||||
let _ = vm1.run_budget(100, &mut native, &mut c1).unwrap();
|
||||
let _ = vm2.run_budget(100, &mut native, &mut c2).unwrap();
|
||||
|
||||
// Discover A/B handles in both VMs
|
||||
let find_ab = |vm: &VirtualMachine| {
|
||||
let mut a = None; let mut b = None;
|
||||
// running
|
||||
if let Some(cur) = vm.current_coro {
|
||||
if let Some(f) = vm.call_stack.last() {
|
||||
if f.func_idx == 1 { a = Some(cur); }
|
||||
if f.func_idx == 2 { b = Some(cur); }
|
||||
}
|
||||
}
|
||||
// suspended
|
||||
for h in vm.heap.suspended_coroutine_handles() {
|
||||
if let Some(co) = vm.heap.coroutine_data(h) {
|
||||
if let Some(f) = co.frames.last() {
|
||||
if f.func_idx == 1 { a = Some(h); }
|
||||
if f.func_idx == 2 { b = Some(h); }
|
||||
}
|
||||
}
|
||||
}
|
||||
(a.expect("A missing"), b.expect("B missing"))
|
||||
};
|
||||
let (a1, b1) = find_ab(&vm1);
|
||||
let (a2, b2) = find_ab(&vm2);
|
||||
|
||||
let mut a1_prev = vm1.heap.coroutine_data(a1).unwrap().stack.len();
|
||||
let mut b1_prev = vm1.heap.coroutine_data(b1).unwrap().stack.len();
|
||||
let mut a2_prev = vm2.heap.coroutine_data(a2).unwrap().stack.len();
|
||||
let mut b2_prev = vm2.heap.coroutine_data(b2).unwrap().stack.len();
|
||||
|
||||
let mut trace1 = Vec::new();
|
||||
let mut trace2 = Vec::new();
|
||||
for _ in 0..8 {
|
||||
let _ = vm1.run_budget(100, &mut native, &mut c1).unwrap();
|
||||
let a_now = vm1.heap.coroutine_data(a1).unwrap().stack.len();
|
||||
let b_now = vm1.heap.coroutine_data(b1).unwrap().stack.len();
|
||||
if a_now > a1_prev { trace1.push(1); } else if b_now > b1_prev { trace1.push(2); } else { panic!("no progress 1"); }
|
||||
a1_prev = a_now; b1_prev = b_now;
|
||||
|
||||
let _ = vm2.run_budget(100, &mut native, &mut c2).unwrap();
|
||||
let a2_now = vm2.heap.coroutine_data(a2).unwrap().stack.len();
|
||||
let b2_now = vm2.heap.coroutine_data(b2).unwrap().stack.len();
|
||||
if a2_now > a2_prev { trace2.push(1); } else if b2_now > b2_prev { trace2.push(2); } else { panic!("no progress 2"); }
|
||||
a2_prev = a2_now; b2_prev = b2_now;
|
||||
}
|
||||
|
||||
assert_eq!(trace1, trace2, "Execution trace (coroutine IDs) must match exactly across runs");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_gc_with_suspended_coroutine_runtime() {
|
||||
use crate::object::ObjectKind;
|
||||
use prometeu_bytecode::FunctionMeta;
|
||||
|
||||
// Function F (idx 1): SLEEP 10; FRAME_SYNC; HALT
|
||||
let mut fn_f = Vec::new();
|
||||
fn_f.extend_from_slice(&(OpCode::Sleep as u16).to_le_bytes());
|
||||
fn_f.extend_from_slice(&10u32.to_le_bytes());
|
||||
fn_f.extend_from_slice(&(OpCode::FrameSync as u16).to_le_bytes());
|
||||
fn_f.extend_from_slice(&(OpCode::Halt as u16).to_le_bytes());
|
||||
|
||||
// Main (idx 0): SPAWN F with 1 argument (the HeapRef we preload); FRAME_SYNC; HALT
|
||||
let mut main = Vec::new();
|
||||
main.extend_from_slice(&(OpCode::Spawn as u16).to_le_bytes());
|
||||
main.extend_from_slice(&1u32.to_le_bytes()); // func idx
|
||||
main.extend_from_slice(&1u32.to_le_bytes()); // arg count = 1
|
||||
main.extend_from_slice(&(OpCode::FrameSync as u16).to_le_bytes());
|
||||
main.extend_from_slice(&(OpCode::Halt as u16).to_le_bytes());
|
||||
|
||||
let off_main = 0usize;
|
||||
let off_f = main.len();
|
||||
let mut rom = Vec::new();
|
||||
rom.extend_from_slice(&main);
|
||||
rom.extend_from_slice(&fn_f);
|
||||
|
||||
let mut vm = new_test_vm(rom.clone(), vec![]);
|
||||
vm.program.functions = std::sync::Arc::from(vec![
|
||||
FunctionMeta { code_offset: off_main as u32, code_len: main.len() as u32, param_slots: 0, local_slots: 0, return_slots: 0, max_stack_slots: 8 },
|
||||
// Function F takes 1 parameter (the HeapRef) which stays on its stack while sleeping
|
||||
FunctionMeta { code_offset: off_f as u32, code_len: fn_f.len() as u32, param_slots: 1, local_slots: 0, return_slots: 0, max_stack_slots: 8 },
|
||||
]);
|
||||
|
||||
// Force GC at first safepoint to stress retention
|
||||
vm.gc_alloc_threshold = 1;
|
||||
|
||||
// Allocate a heap object and preload it onto main's operand stack so SPAWN consumes it as arg.
|
||||
let captured = vm.heap.allocate_object(ObjectKind::Bytes, &[0xAB, 0xCD, 0xEF]);
|
||||
vm.operand_stack.push(Value::HeapRef(captured));
|
||||
|
||||
let mut native = MockNative;
|
||||
let mut ctx = HostContext::new(None);
|
||||
|
||||
// Run main: SPAWN consumes the HeapRef as arg and creates sleeping coroutine; FRAME_SYNC triggers GC
|
||||
let rep = vm.run_budget(100, &mut native, &mut ctx).unwrap();
|
||||
assert!(matches!(rep.reason, LogicalFrameEndingReason::FrameSync));
|
||||
|
||||
// The captured object must remain alive because it is referenced by the sleeping coroutine's stack
|
||||
assert!(vm.heap.is_valid(captured), "captured object must remain alive while coroutine sleeps");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_make_closure_zero_captures() {
|
||||
use prometeu_bytecode::{FunctionMeta, Value};
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user