use indexmap::IndexMap;
use lex_ast::canonicalize_program;
use lex_bytecode::{compile_program, vm::Vm, Value};
use lex_runtime::{DefaultHandler, Policy};
use lex_syntax::parse_source;
use lex_trace::{diff_runs, Recorder, TraceNodeKind, TraceTree};
use std::collections::BTreeSet;
fn compile(src: &str) -> lex_bytecode::Program {
let prog = parse_source(src).unwrap();
let stages = canonicalize_program(&prog);
compile_program(&stages)
}
fn allow(effects: &[&str]) -> Policy {
let mut p = Policy::pure();
p.allow_effects = effects.iter().map(|s| s.to_string()).collect::<BTreeSet<_>>();
p
}
fn run_with_recorder(
src: &str,
func: &str,
args: Vec<Value>,
overrides: Option<IndexMap<String, serde_json::Value>>,
policy: Policy,
) -> (TraceTree, Result<Value, lex_bytecode::vm::VmError>) {
let prog = compile(src);
let mut recorder = Recorder::new();
if let Some(o) = overrides { recorder = recorder.with_overrides(o); }
let handle = recorder.handle();
let started = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH).unwrap().as_secs();
let handler = DefaultHandler::new(policy);
let mut vm = Vm::with_handler(&prog, Box::new(handler));
vm.set_tracer(Box::new(recorder));
let result = vm.call(func, args);
let ended = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH).unwrap().as_secs();
let (root_out, root_err) = match &result {
Ok(v) => (Some(value_to_json(v)), None),
Err(e) => (None, Some(format!("{e}"))),
};
let tree = handle.finalize(func, serde_json::Value::Null, root_out, root_err, started, ended);
(tree, result)
}
fn value_to_json(v: &Value) -> serde_json::Value {
use serde_json::Value as J;
match v {
Value::Int(n) => J::from(*n),
Value::Float(f) => J::from(*f),
Value::Bool(b) => J::Bool(*b),
Value::Str(s) => J::String(s.to_string()),
Value::Bytes(b) => J::String(b.iter().map(|b| format!("{:02x}", b)).collect()),
Value::Unit => J::Null,
Value::List(items) => J::Array(items.iter().map(value_to_json).collect()),
Value::Tuple(items) => J::Array(items.iter().map(value_to_json).collect()),
Value::Record { fields, .. } => {
let mut m = serde_json::Map::new();
for (k, v) in fields.iter() { m.insert(k.to_string(), value_to_json(v)); }
J::Object(m)
}
Value::Variant { name, args } => {
let mut m = serde_json::Map::new();
m.insert("$variant".into(), J::String(name.clone()));
m.insert("args".into(), J::Array(args.iter().map(value_to_json).collect()));
J::Object(m)
}
Value::Closure { body_hash, .. } => {
let prefix: String = body_hash.iter().take(4)
.map(|b| format!("{b:02x}")).collect();
J::String(format!("<closure {prefix}>"))
}
Value::F64Array { rows, cols, data } => {
let mut m = serde_json::Map::new();
m.insert("$f64_array".into(), J::Bool(true));
m.insert("rows".into(), J::from(*rows));
m.insert("cols".into(), J::from(*cols));
m.insert("data".into(), J::Array(data.iter().map(|f| J::from(*f)).collect()));
J::Object(m)
}
Value::Map(_) | Value::Set(_) | Value::Deque(_) | Value::Actor(_)
| Value::Ticker(_) | Value::ArrowTable(_)
| Value::StackRecord { .. } | Value::StackTuple { .. }
| Value::ArenaRecord { .. } | Value::ArenaTuple { .. } => {
J::String("<container>".into())
}
}
}
const FACTORIAL: &str = "fn factorial(n :: Int) -> Int { match n { 0 => 1, _ => n * factorial(n - 1) } }\nfn driver(n :: Int) -> Int { factorial(n) }\n";
#[test]
fn pure_run_records_call_tree() {
let (tree, r) = run_with_recorder(FACTORIAL, "driver", vec![Value::Int(3)], None, Policy::pure());
assert_eq!(r.unwrap(), Value::Int(6));
assert_eq!(tree.nodes.len(), 1, "single top-level call");
let factorial_call = &tree.nodes[0];
assert!(matches!(factorial_call.kind, TraceNodeKind::Call));
assert_eq!(factorial_call.target, "factorial");
assert!(factorial_call.output.is_some());
}
#[test]
fn effect_call_appears_in_trace() {
let src = r#"
import "std.io" as io
fn say(line :: Str) -> [io] Nil { io.print(line) }
"#;
let (tree, r) = run_with_recorder(src, "say", vec![Value::Str("hello".into())], None, allow(&["io"]));
assert!(r.is_ok());
fn find_effect(n: &lex_trace::TraceNode) -> Option<&lex_trace::TraceNode> {
if matches!(n.kind, TraceNodeKind::Effect) { return Some(n); }
for c in &n.children {
if let Some(f) = find_effect(c) { return Some(f); }
}
None
}
let effect = tree.nodes.iter().find_map(find_effect)
.expect("io.print effect should appear in trace");
assert_eq!(effect.target, "io.print");
}
#[test]
fn failed_run_identifies_failing_node() {
let src = r#"
import "std.io" as io
fn say(line :: Str) -> [io] Nil { io.print(line) }
"#;
struct FailHandler;
impl lex_bytecode::vm::EffectHandler for FailHandler {
fn dispatch(&mut self, _: &str, _: &str, _: Vec<Value>) -> Result<Value, String> {
Err("simulated failure".into())
}
}
let prog = compile(src);
let recorder = Recorder::new();
let handle = recorder.handle();
let mut vm = Vm::with_handler(&prog, Box::new(FailHandler));
vm.set_tracer(Box::new(recorder));
let result = vm.call("say", vec![Value::Str("x".into())]);
let tree = handle.finalize("say", serde_json::Value::Null, None,
result.as_ref().err().map(|e| format!("{e}")),
0, 0);
assert!(result.is_err());
fn find_err_node(n: &lex_trace::TraceNode) -> Option<&lex_trace::TraceNode> {
if n.error.is_some() { return Some(n); }
for c in &n.children {
if let Some(f) = find_err_node(c) { return Some(f); }
}
None
}
let bad = tree.nodes.iter().find_map(find_err_node)
.expect("trace should record the failing effect node");
assert_eq!(bad.target, "io.print");
assert!(bad.error.as_deref().unwrap_or("").contains("simulated"));
assert!(!bad.node_id.is_empty(), "failing node must carry its NodeId");
}
#[test]
fn replay_with_override_substitutes_effect_output() {
let src = r#"
import "std.io" as io
fn read_then_concat(path :: Str) -> [io] Result[Str, Str] {
match io.read(path) {
Ok(s) => Ok(s),
Err(e) => Err(e),
}
}
"#;
let (tree1, r1) = run_with_recorder(src, "read_then_concat",
vec![Value::Str("/no/such/path".into())], None, allow(&["io"]));
assert!(r1.is_ok(), "io.read returns Err(...) as a value");
fn find_effect(n: &lex_trace::TraceNode) -> Option<&lex_trace::TraceNode> {
if matches!(n.kind, TraceNodeKind::Effect) { return Some(n); }
for c in &n.children {
if let Some(f) = find_effect(c) { return Some(f); }
}
None
}
let effect_node = tree1.nodes.iter().find_map(find_effect).expect("effect");
let effect_node_id = effect_node.node_id.clone();
let mut overrides = IndexMap::new();
let injected = serde_json::json!({"$variant": "Ok", "args": ["REPLACED"]});
overrides.insert(effect_node_id.clone(), injected);
let (tree2, r2) = run_with_recorder(src, "read_then_concat",
vec![Value::Str("/no/such/path".into())], Some(overrides), allow(&["io"]));
let v2 = r2.unwrap();
let expected = Value::Variant { name: "Ok".into(), args: vec![Value::Str("REPLACED".into())] };
assert_eq!(v2, expected, "override must propagate through pattern match");
let div = diff_runs(&tree1, &tree2).expect("traces should diverge");
assert_eq!(div.node_id, effect_node_id);
}
#[test]
fn diff_returns_first_divergence() {
let src = r#"
import "std.io" as io
fn pipe(line :: Str) -> [io] Nil { io.print(line) }
"#;
let (a, _) = run_with_recorder(src, "pipe", vec![Value::Str("one".into())], None, allow(&["io"]));
let (b, _) = run_with_recorder(src, "pipe", vec![Value::Str("one".into())], None, allow(&["io"]));
assert!(diff_runs(&a, &b).is_none(), "identical runs should not diverge");
let (c, _) = run_with_recorder(src, "pipe", vec![Value::Str("two".into())], None, allow(&["io"]));
let div = diff_runs(&a, &c).expect("different args ⇒ divergence");
assert!(!div.node_id.is_empty());
}