pr7.9
This commit is contained in:
parent
b48386b154
commit
49c9b018bc
@ -1369,6 +1369,163 @@ mod tests {
|
|||||||
let res = vm.run_budget(100, &mut native, &mut ctx);
|
let res = vm.run_budget(100, &mut native, &mut ctx);
|
||||||
assert!(res.is_ok());
|
assert!(res.is_ok());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_deterministic_pc_and_tick_trace_across_runs() {
|
||||||
|
// Program:
|
||||||
|
// PUSH_I32 1; YIELD; FrameSync;
|
||||||
|
// PUSH_I32 2; YIELD; FrameSync;
|
||||||
|
// PUSH_I32 3; FrameSync; HALT
|
||||||
|
// We collect (pc, tick, stack_height) after each run_budget slice and
|
||||||
|
// compare two independent VMs initialized from the same ROM.
|
||||||
|
let mut rom = Vec::new();
|
||||||
|
// PUSH 1
|
||||||
|
rom.extend_from_slice(&(OpCode::PushI32 as u16).to_le_bytes());
|
||||||
|
rom.extend_from_slice(&1i32.to_le_bytes());
|
||||||
|
// YIELD + FrameSync
|
||||||
|
rom.extend_from_slice(&(OpCode::Yield as u16).to_le_bytes());
|
||||||
|
rom.extend_from_slice(&(OpCode::FrameSync as u16).to_le_bytes());
|
||||||
|
// PUSH 2
|
||||||
|
rom.extend_from_slice(&(OpCode::PushI32 as u16).to_le_bytes());
|
||||||
|
rom.extend_from_slice(&2i32.to_le_bytes());
|
||||||
|
// YIELD + FrameSync
|
||||||
|
rom.extend_from_slice(&(OpCode::Yield as u16).to_le_bytes());
|
||||||
|
rom.extend_from_slice(&(OpCode::FrameSync as u16).to_le_bytes());
|
||||||
|
// PUSH 3 + FrameSync + HALT
|
||||||
|
rom.extend_from_slice(&(OpCode::PushI32 as u16).to_le_bytes());
|
||||||
|
rom.extend_from_slice(&3i32.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 vm1 = new_test_vm(rom.clone(), vec![]);
|
||||||
|
let mut vm2 = new_test_vm(rom.clone(), vec![]);
|
||||||
|
|
||||||
|
let mut native = MockNative;
|
||||||
|
let mut ctx1 = HostContext::new(None);
|
||||||
|
let mut ctx2 = HostContext::new(None);
|
||||||
|
|
||||||
|
let mut trace1 = Vec::new();
|
||||||
|
let mut trace2 = Vec::new();
|
||||||
|
|
||||||
|
// Run both VMs in lockstep slices until both halt
|
||||||
|
for _ in 0..10 {
|
||||||
|
if !vm1.halted {
|
||||||
|
let rep = vm1.run_budget(4, &mut native, &mut ctx1).expect("vm1 ok");
|
||||||
|
trace1.push((vm1.pc, vm1.current_tick, vm1.operand_stack.len(), format!("{:?}", rep.reason)));
|
||||||
|
}
|
||||||
|
if !vm2.halted {
|
||||||
|
let rep = vm2.run_budget(4, &mut native, &mut ctx2).expect("vm2 ok");
|
||||||
|
trace2.push((vm2.pc, vm2.current_tick, vm2.operand_stack.len(), format!("{:?}", rep.reason)));
|
||||||
|
}
|
||||||
|
if vm1.halted && vm2.halted { break; }
|
||||||
|
}
|
||||||
|
|
||||||
|
assert!(vm1.halted && vm2.halted, "Both VMs should reach HALT deterministically");
|
||||||
|
assert_eq!(trace1, trace2, "Per-slice traces must be identical across runs");
|
||||||
|
// Also verify final stack content deterministic
|
||||||
|
assert_eq!(vm1.pop().unwrap(), Value::Int32(3));
|
||||||
|
assert_eq!(vm1.pop().unwrap(), Value::Int32(2));
|
||||||
|
assert_eq!(vm1.pop().unwrap(), Value::Int32(1));
|
||||||
|
assert!(vm1.operand_stack.is_empty());
|
||||||
|
assert_eq!(vm2.pop().unwrap(), Value::Int32(3));
|
||||||
|
assert_eq!(vm2.pop().unwrap(), Value::Int32(2));
|
||||||
|
assert_eq!(vm2.pop().unwrap(), Value::Int32(1));
|
||||||
|
assert!(vm2.operand_stack.is_empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_sleep_wake_determinism_across_runs() {
|
||||||
|
// Program:
|
||||||
|
// SLEEP 2; PUSH 7; FrameSync; 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(&7i32.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 native = MockNative;
|
||||||
|
let mut vm_a = new_test_vm(rom.clone(), vec![]);
|
||||||
|
let mut vm_b = new_test_vm(rom.clone(), vec![]);
|
||||||
|
let mut ctx_a = HostContext::new(None);
|
||||||
|
let mut ctx_b = HostContext::new(None);
|
||||||
|
|
||||||
|
let mut ticks_a = Vec::new();
|
||||||
|
let mut ticks_b = Vec::new();
|
||||||
|
|
||||||
|
// Slice 1
|
||||||
|
let ra1 = vm_a.run_budget(100, &mut native, &mut ctx_a).unwrap();
|
||||||
|
let rb1 = vm_b.run_budget(100, &mut native, &mut ctx_b).unwrap();
|
||||||
|
ticks_a.push((vm_a.pc, vm_a.current_tick, format!("{:?}", ra1.reason)));
|
||||||
|
ticks_b.push((vm_b.pc, vm_b.current_tick, format!("{:?}", rb1.reason)));
|
||||||
|
|
||||||
|
// Slice 2
|
||||||
|
let ra2 = vm_a.run_budget(100, &mut native, &mut ctx_a).unwrap();
|
||||||
|
let rb2 = vm_b.run_budget(100, &mut native, &mut ctx_b).unwrap();
|
||||||
|
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)
|
||||||
|
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)));
|
||||||
|
|
||||||
|
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));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_gc_many_coroutines_and_wake_order_determinism() {
|
||||||
|
use crate::heap::{CoroutineState};
|
||||||
|
use crate::object::ObjectKind;
|
||||||
|
|
||||||
|
// ROM: FrameSync; FrameSync; Halt (two deterministic safepoints back-to-back)
|
||||||
|
let mut rom = Vec::new();
|
||||||
|
rom.extend_from_slice(&(OpCode::FrameSync as u16).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.clone(), vec![]);
|
||||||
|
vm.gc_alloc_threshold = 1; // force GC at first safepoint
|
||||||
|
|
||||||
|
// Allocate many coroutine objects: half Ready, half Sleeping with differing wake ticks.
|
||||||
|
let coro_count = 128u32;
|
||||||
|
for i in 0..coro_count {
|
||||||
|
let state = if i % 2 == 0 { CoroutineState::Ready } else { CoroutineState::Sleeping };
|
||||||
|
let wake = if state == CoroutineState::Sleeping { (i / 2) as u64 } else { 0 };
|
||||||
|
let _c = vm.heap.allocate_coroutine(state, wake, vec![], vec![]);
|
||||||
|
// Also allocate a tiny byte object to increase GC pressure.
|
||||||
|
let _b = vm.heap.allocate_object(ObjectKind::Bytes, &[i as u8]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sanity: allocations present
|
||||||
|
assert!(vm.heap.len() as u32 >= coro_count, "heap should contain coroutine objects and bytes");
|
||||||
|
|
||||||
|
let mut native = MockNative;
|
||||||
|
let mut ctx = HostContext::new(None);
|
||||||
|
|
||||||
|
// Reaching FrameSync should run GC; Ready/Sleeping coroutines are treated as roots, so
|
||||||
|
// only unreferenced byte objects can be reclaimed. We just assert determinism: heap size
|
||||||
|
// should be stable across two consecutive FrameSyncs with no new allocations in between.
|
||||||
|
let before = vm.heap.len();
|
||||||
|
match vm.step(&mut native, &mut ctx) {
|
||||||
|
Err(LogicalFrameEndingReason::FrameSync) => {}
|
||||||
|
other => panic!("Expected FrameSync, got {:?}", other),
|
||||||
|
}
|
||||||
|
let after_first = vm.heap.len();
|
||||||
|
match vm.step(&mut native, &mut ctx) {
|
||||||
|
Err(LogicalFrameEndingReason::FrameSync) => {}
|
||||||
|
other => panic!("Expected FrameSync, got {:?}", other),
|
||||||
|
}
|
||||||
|
let after_second = vm.heap.len();
|
||||||
|
|
||||||
|
// GC effect should be deterministic and idempotent at steady state
|
||||||
|
assert!(after_first <= before);
|
||||||
|
assert_eq!(after_second, after_first);
|
||||||
|
}
|
||||||
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,38 +1,3 @@
|
|||||||
# PR-7.9 — Determinism & Stress Tests
|
|
||||||
|
|
||||||
## Briefing
|
|
||||||
|
|
||||||
Validate deterministic behavior.
|
|
||||||
|
|
||||||
## Target
|
|
||||||
|
|
||||||
Tests must confirm:
|
|
||||||
|
|
||||||
* Same order across runs.
|
|
||||||
* Sleep/wake order stable.
|
|
||||||
* GC works with many coroutines.
|
|
||||||
|
|
||||||
## Checklist
|
|
||||||
|
|
||||||
* [ ] Deterministic order tests.
|
|
||||||
* [ ] Stress test 100+ coroutines.
|
|
||||||
|
|
||||||
## Junie Rules
|
|
||||||
|
|
||||||
Tests must not depend on wall clock or randomness.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Final Definition of Done
|
|
||||||
|
|
||||||
* Cooperative coroutines implemented.
|
|
||||||
* Deterministic scheduling.
|
|
||||||
* No mailbox.
|
|
||||||
* GC and verifier fully integrated.
|
|
||||||
* All tests pass.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
# PR-8 — Tooling & Test Harness (JVM-Grade Discipline)
|
# PR-8 — Tooling & Test Harness (JVM-Grade Discipline)
|
||||||
|
|
||||||
This phase establishes tooling and test discipline required to keep the Prometeu VM at JVM-grade quality.
|
This phase establishes tooling and test discipline required to keep the Prometeu VM at JVM-grade quality.
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user