use crate::runtime::interpreter::Value;
use crate::runtime::repl::Repl;
use crate::runtime::replay::{
DeterministicRepl, Divergence, ReplayResult, ResourceUsage, StateCheckpoint, ValidationResult,
};
use anyhow::Result;
use sha2::{Digest, Sha256};
use std::collections::HashMap;
use std::sync::Arc;
pub struct MockTime {
current_ns: u64,
}
impl Default for MockTime {
fn default() -> Self {
Self::new()
}
}
impl MockTime {
pub fn new() -> Self {
Self { current_ns: 0 }
}
pub fn advance(&mut self, ns: u64) {
self.current_ns += ns;
}
pub fn now(&self) -> u64 {
self.current_ns
}
}
pub struct DeterministicRng {
seed: u64,
state: u64,
}
impl DeterministicRng {
pub fn new(seed: u64) -> Self {
Self { seed, state: seed }
}
pub fn next(&mut self) -> u64 {
self.state = self
.state
.wrapping_mul(6_364_136_223_846_793_005)
.wrapping_add(1_442_695_040_888_963_407);
self.state
}
pub fn reset(&mut self) {
self.state = self.seed;
}
}
impl DeterministicRepl for Repl {
fn execute_with_seed(&mut self, input: &str, _seed: u64) -> ReplayResult {
let start_heap = self.estimate_heap_usage();
let start_stack = self.estimate_stack_depth();
let start_time = std::time::Instant::now();
let output = self.process_line(input).map(|_success| {
let s = "success"; if s == "()" {
Value::Nil
} else if s == "true" {
Value::Bool(true)
} else if s == "false" {
Value::Bool(false)
} else if let Ok(n) = s.parse::<i64>() {
Value::Integer(n)
} else if s.starts_with('"') && s.ends_with('"') {
Value::String(Arc::from(&s[1..s.len() - 1]))
} else {
Value::from_string(s.to_string())
}
});
let heap_bytes = self.estimate_heap_usage() - start_heap;
let stack_depth = self.estimate_stack_depth() - start_stack;
let cpu_ns = start_time.elapsed().as_nanos() as u64;
let state_hash = self.compute_state_hash();
ReplayResult {
output,
state_hash,
resource_usage: ResourceUsage {
heap_bytes,
stack_depth,
cpu_ns,
},
}
}
fn checkpoint(&self) -> StateCheckpoint {
let mut bindings = HashMap::new();
let type_environment = HashMap::new();
for (name, value) in self.get_bindings() {
bindings.insert(name.clone(), value.to_string());
}
StateCheckpoint {
bindings,
type_environment,
state_hash: self.compute_state_hash(),
resource_usage: ResourceUsage {
heap_bytes: self.estimate_heap_usage(),
stack_depth: self.estimate_stack_depth(),
cpu_ns: 0, },
}
}
fn restore(&mut self, checkpoint: &StateCheckpoint) -> Result<()> {
self.clear_bindings();
if let Some(evaluator) = self.get_evaluator_mut() {
evaluator.clear_interpreter_variables();
}
for (name, value_str) in &checkpoint.bindings {
let value = if value_str == "nil" {
Value::Nil
} else if value_str == "true" {
Value::Bool(true)
} else if value_str == "false" {
Value::Bool(false)
} else if let Ok(n) = value_str.parse::<i64>() {
Value::Integer(n)
} else if let Ok(f) = value_str.parse::<f64>() {
Value::Float(f)
} else if value_str.starts_with('"') && value_str.ends_with('"') {
let content = &value_str[1..value_str.len() - 1];
Value::from_string(content.to_string())
} else {
Value::from_string(value_str.clone())
};
self.get_bindings_mut().insert(name.clone(), value.clone());
if let Some(evaluator) = self.get_evaluator_mut() {
evaluator.set_variable(name.clone(), value);
}
}
Ok(())
}
fn validate_determinism(&self, other: &Self) -> ValidationResult {
let mut divergences = vec![];
for (name, value) in self.get_bindings() {
match other.get_bindings().get(name) {
Some(other_value) if value == other_value => {
}
Some(other_value) => {
divergences.push(Divergence::State {
expected_hash: format!("{value:?}"),
actual_hash: format!("{other_value:?}"),
});
}
None => {
divergences.push(Divergence::State {
expected_hash: format!("{value:?}"),
actual_hash: "missing".to_string(),
});
}
}
}
for name in other.get_bindings().keys() {
if !self.get_bindings().contains_key(name) {
divergences.push(Divergence::State {
expected_hash: "missing".to_string(),
actual_hash: format!("variable: {name}"),
});
}
}
ValidationResult {
is_deterministic: divergences.is_empty(),
divergences,
}
}
}
impl Repl {
fn estimate_heap_usage(&self) -> usize {
let mut total = 0;
for value in self.get_bindings().values() {
total += std::mem::size_of_val(value);
total += match value {
Value::String(s) => s.len(),
Value::Array(items) => items.len() * std::mem::size_of::<Value>(),
Value::Object(map) => map.len() * (32 + std::mem::size_of::<Value>()),
_ => 0,
};
}
total
}
fn estimate_stack_depth(&self) -> usize {
self.get_bindings().len() / 10 + 1
}
fn compute_state_hash(&self) -> String {
let mut hasher = Sha256::new();
let mut sorted_vars: Vec<_> = self.get_bindings().iter().collect();
sorted_vars.sort_by_key(|(name, _)| name.as_str());
for (name, value) in sorted_vars {
hasher.update(name.as_bytes());
hasher.update(b":");
hasher.update(value.to_string().as_bytes());
hasher.update(b";");
}
format!("{:x}", hasher.finalize())
}
}
#[cfg(test)]
mod tests {
use super::*;
use tempfile::TempDir;
#[test]
fn test_mock_time_new() {
let time = MockTime::new();
assert_eq!(time.now(), 0);
}
#[test]
fn test_mock_time_default() {
let time = MockTime::default();
assert_eq!(time.now(), 0);
}
#[test]
fn test_mock_time_advance() {
let mut time = MockTime::new();
time.advance(1000);
assert_eq!(time.now(), 1000);
time.advance(500);
assert_eq!(time.now(), 1500);
}
#[test]
fn test_deterministic_rng_new() {
let rng = DeterministicRng::new(42);
assert_eq!(rng.seed, 42);
assert_eq!(rng.state, 42);
}
#[test]
fn test_deterministic_rng_next() {
let mut rng = DeterministicRng::new(42);
let first = rng.next();
let second = rng.next();
assert_ne!(first, second);
}
#[test]
fn test_deterministic_rng_reproducible() {
let mut rng1 = DeterministicRng::new(12345);
let mut rng2 = DeterministicRng::new(12345);
for _ in 0..10 {
assert_eq!(rng1.next(), rng2.next());
}
}
#[test]
fn test_deterministic_rng_different_seeds() {
let mut rng1 = DeterministicRng::new(1);
let mut rng2 = DeterministicRng::new(2);
assert_ne!(rng1.next(), rng2.next());
}
#[test]
fn test_mock_time_advance_zero() {
let mut time = MockTime::new();
time.advance(0);
assert_eq!(time.now(), 0);
}
#[test]
fn test_mock_time_advance_large() {
let mut time = MockTime::new();
time.advance(u64::MAX / 2);
assert_eq!(time.now(), u64::MAX / 2);
}
#[test]
fn test_deterministic_rng_many_values() {
let mut rng = DeterministicRng::new(999);
let mut values = Vec::new();
for _ in 0..100 {
values.push(rng.next());
}
let unique: std::collections::HashSet<_> = values.iter().collect();
assert!(unique.len() > 90, "Should generate many unique values");
}
#[test]
fn test_mock_time_multiple_advances() {
let mut time = MockTime::new();
for i in 1..=10 {
time.advance(i);
}
assert_eq!(time.now(), 55);
}
#[test]
fn test_deterministic_rng_reset_multiple() {
let mut rng = DeterministicRng::new(42);
let first_run: Vec<_> = (0..5).map(|_| rng.next()).collect();
rng.reset();
let second_run: Vec<_> = (0..5).map(|_| rng.next()).collect();
rng.reset();
let third_run: Vec<_> = (0..5).map(|_| rng.next()).collect();
assert_eq!(first_run, second_run);
assert_eq!(second_run, third_run);
}
#[test]
fn test_deterministic_rng_zero_seed() {
let mut rng = DeterministicRng::new(0);
let val = rng.next();
assert_ne!(val, 0, "RNG should produce non-zero even with zero seed");
}
#[test]
fn test_mock_time_now_readonly() {
let time = MockTime::new();
let t1 = time.now();
let t2 = time.now();
assert_eq!(t1, t2, "now() should not change state");
}
#[test]
fn test_deterministic_execution() {
let temp_dir1 = TempDir::new().expect("TempDir::new should succeed in test");
let temp_dir2 = TempDir::new().expect("TempDir::new should succeed in test");
let mut repl1 =
Repl::new(temp_dir1.path().to_path_buf()).expect("Repl::new should succeed in test");
let mut repl2 =
Repl::new(temp_dir2.path().to_path_buf()).expect("Repl::new should succeed in test");
let result1 = repl1.execute_with_seed("let x = 42", 12345);
let result2 = repl2.execute_with_seed("let x = 42", 12345);
assert!(result1.output.is_ok());
assert!(result2.output.is_ok());
assert_eq!(result1.state_hash, result2.state_hash);
}
#[test]
fn test_checkpoint_restore() {
let temp_dir = TempDir::new().expect("TempDir::new should succeed in test");
let mut repl =
Repl::new(temp_dir.path().to_path_buf()).expect("Repl::new should succeed in test");
repl.eval("let x = 10")
.expect("eval should succeed in test");
repl.eval("let y = 20")
.expect("eval should succeed in test");
let checkpoint = DeterministicRepl::checkpoint(&repl);
repl.eval("let x = 99")
.expect("eval should succeed in test");
repl.eval("let z = 30")
.expect("eval should succeed in test");
DeterministicRepl::restore(&mut repl, &checkpoint).expect("restore should succeed in test");
assert!(repl.eval("x").is_ok());
assert!(repl.eval("y").is_ok());
assert!(repl.eval("z").is_err());
}
#[test]
fn test_determinism_validation() {
let temp_dir1 = TempDir::new().expect("TempDir::new should succeed in test");
let temp_dir2 = TempDir::new().expect("TempDir::new should succeed in test");
let mut repl1 =
Repl::new(temp_dir1.path().to_path_buf()).expect("Repl::new should succeed in test");
let mut repl2 =
Repl::new(temp_dir2.path().to_path_buf()).expect("Repl::new should succeed in test");
repl1
.eval("let x = 1")
.expect("eval should succeed in test");
repl2
.eval("let x = 1")
.expect("eval should succeed in test");
let validation = repl1.validate_determinism(&repl2);
assert!(validation.is_deterministic);
assert!(validation.divergences.is_empty());
repl1
.eval("let y = 2")
.expect("eval should succeed in test");
repl2
.eval("let y = 3")
.expect("eval should succeed in test");
let validation = repl1.validate_determinism(&repl2);
assert!(!validation.is_deterministic);
assert!(!validation.divergences.is_empty());
}
#[test]
fn test_estimate_heap_usage_actual_calculation() {
let temp_dir = TempDir::new().expect("TempDir::new should succeed in test");
let mut repl =
Repl::new(temp_dir.path().to_path_buf()).expect("Repl::new should succeed in test");
let empty_heap = repl.estimate_heap_usage();
assert_eq!(empty_heap, 0, "Empty REPL should have 0 heap usage");
repl.eval("let s = \"hello world\"")
.expect("eval should succeed in test");
let with_string = repl.estimate_heap_usage();
assert!(
with_string > empty_heap,
"String should increase heap usage from {empty_heap} to {with_string}"
);
repl.eval("let arr = [1, 2, 3, 4, 5]")
.expect("eval should succeed in test");
let with_array = repl.estimate_heap_usage();
assert!(
with_array > with_string,
"Array should increase heap usage from {with_string} to {with_array}"
);
let array_contribution = with_array - with_string;
assert!(
array_contribution >= 5,
"Array contribution {array_contribution} should be at least 5 (items * size)"
);
}
#[test]
fn test_estimate_stack_depth_arithmetic() {
let temp_dir = TempDir::new().expect("TempDir::new should succeed in test");
let mut repl =
Repl::new(temp_dir.path().to_path_buf()).expect("Repl::new should succeed in test");
let initial_depth = repl.estimate_stack_depth();
let initial_bindings = repl.get_bindings().len();
for i in 0..10 {
repl.eval(&format!("let var{i} = {i}"))
.expect("eval should succeed in test");
}
let bindings_after_10 = repl.get_bindings().len();
let depth_after_10 = repl.estimate_stack_depth();
let bindings_added = bindings_after_10 - initial_bindings;
let expected_depth_increase = bindings_added / 10;
let actual_depth_increase = depth_after_10 - initial_depth;
assert_eq!(
actual_depth_increase, expected_depth_increase,
"Stack depth should increase by {expected_depth_increase} (using division), got {actual_depth_increase}"
);
for i in 10..20 {
repl.eval(&format!("let var{i} = {i}"))
.expect("eval should succeed in test");
}
let bindings_after_20 = repl.get_bindings().len();
let depth_after_20 = repl.estimate_stack_depth();
let total_bindings_added = bindings_after_20 - initial_bindings;
let expected_final_depth_increase = total_bindings_added / 10;
let actual_final_depth_increase = depth_after_20 - initial_depth;
assert_eq!(
actual_final_depth_increase, expected_final_depth_increase,
"Stack depth formula should use division (not modulo)"
);
assert!(depth_after_20 > 0, "Stack depth should never be 0");
}
#[test]
fn test_execute_with_seed_resource_tracking() {
let temp_dir = TempDir::new().expect("TempDir::new should succeed in test");
let mut repl =
Repl::new(temp_dir.path().to_path_buf()).expect("Repl::new should succeed in test");
let result1 = repl.execute_with_seed("let x = 10", 12345);
assert!(result1.output.is_ok());
let current_heap = repl.estimate_heap_usage();
assert!(
result1.resource_usage.heap_bytes > 0,
"Heap bytes should be positive, got {}",
result1.resource_usage.heap_bytes
);
assert!(
result1.resource_usage.heap_bytes <= current_heap,
"Heap bytes {0} should be <= current heap {current_heap} (proves subtraction, not addition)",
result1.resource_usage.heap_bytes
);
let before_string = repl.estimate_heap_usage();
let _result2 =
repl.execute_with_seed("let y = \"long string value that uses memory\"", 12345);
let after_string = repl.estimate_heap_usage();
let heap_change = after_string.saturating_sub(before_string);
assert!(
heap_change > 0,
"String variable should increase heap usage"
);
}
#[test]
fn test_execute_with_seed_state_hash_determinism() {
let temp_dir1 = TempDir::new().expect("TempDir::new should succeed in test");
let temp_dir2 = TempDir::new().expect("TempDir::new should succeed in test");
let mut repl1 =
Repl::new(temp_dir1.path().to_path_buf()).expect("Repl::new should succeed in test");
let mut repl2 =
Repl::new(temp_dir2.path().to_path_buf()).expect("Repl::new should succeed in test");
let result1 = repl1.execute_with_seed("let x = 42", 12345);
let result2 = repl2.execute_with_seed("let x = 42", 12345);
assert!(result1.output.is_ok());
assert!(result2.output.is_ok());
assert_eq!(
result1.state_hash, result2.state_hash,
"Deterministic execution should produce same state hash"
);
let result3 = repl1.execute_with_seed("let y = 99", 12345);
assert_ne!(
result1.state_hash, result3.state_hash,
"Different state should produce different hash"
);
}
#[test]
fn test_deterministic_rng_reset() {
let mut rng = DeterministicRng::new(12345);
let first = rng.next();
let second = rng.next();
let third = rng.next();
rng.reset();
let first_after_reset = rng.next();
let second_after_reset = rng.next();
let third_after_reset = rng.next();
assert_eq!(
first, first_after_reset,
"First value after reset should match original"
);
assert_eq!(
second, second_after_reset,
"Second value after reset should match original"
);
assert_eq!(
third, third_after_reset,
"Third value after reset should match original"
);
}
}