This commit is contained in:
bQUARKz 2026-02-20 12:31:02 +00:00
parent b48386b154
commit 49c9b018bc
Signed by: bquarkz
SSH Key Fingerprint: SHA256:Z7dgqoglWwoK6j6u4QC87OveEq74WOhFN+gitsxtkf8
2 changed files with 157 additions and 35 deletions

View File

@ -1369,6 +1369,163 @@ mod tests {
let res = vm.run_budget(100, &mut native, &mut ctx);
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() {
let mut native = MockNative;
let mut ctx = HostContext::new(None);

View File

@ -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)
This phase establishes tooling and test discipline required to keep the Prometeu VM at JVM-grade quality.