use indexmap::IndexMap;
use lex_ast::canonicalize_program;
use lex_bytecode::{compile_program, Value, Vm, VmError, MAX_CALL_DEPTH};
use lex_syntax::parse_source;
fn compile(src: &str) -> lex_bytecode::Program {
let p = parse_source(src).unwrap();
let stages = canonicalize_program(&p);
compile_program(&stages)
}
#[test]
fn unbounded_recursion_yields_call_stack_overflow_not_segfault() {
let src = "fn deep() -> Int { 1 + deep() }\n";
let p = compile(src);
let handle = std::thread::Builder::new()
.stack_size(512 * 1024)
.spawn(move || {
let mut vm = Vm::new(&p);
vm.call("deep", vec![])
})
.expect("spawn worker thread");
let r = handle.join().expect("worker panicked").expect_err("expected overflow");
match r {
VmError::CallStackOverflow(n) => assert_eq!(n, MAX_CALL_DEPTH),
other => panic!("expected CallStackOverflow, got {other:?}"),
}
}
#[test]
fn modest_recursion_under_cap_still_runs() {
let src = "fn factorial(n :: Int) -> Int { match n { 0 => 1, _ => n * factorial(n - 1) } }\n";
let p = compile(src);
let mut vm = Vm::new(&p);
let r = vm.call("factorial", vec![Value::Int(20)]).unwrap();
assert_eq!(r, Value::Int(2_432_902_008_176_640_000));
}
#[test]
fn example_a_factorial() {
let src = include_str!("../../../examples/a_factorial.lex");
let p = compile(src);
let mut vm = Vm::new(&p);
let r = vm.call("factorial", vec![Value::Int(5)]).unwrap();
assert_eq!(r, Value::Int(120));
let r = vm.call("factorial", vec![Value::Int(0)]).unwrap();
assert_eq!(r, Value::Int(1));
let r = vm.call("factorial", vec![Value::Int(10)]).unwrap();
assert_eq!(r, Value::Int(3628800));
}
#[test]
fn example_d_shape() {
let src = include_str!("../../../examples/d_shape.lex");
let p = compile(src);
let mut vm = Vm::new(&p);
let circle = Value::Variant {
name: "Circle".into(),
args: vec![Value::record_dynamic({
let mut m = IndexMap::new();
m.insert("radius".into(), Value::Float(1.0));
m
})],
};
let r = vm.call("area", vec![circle]).unwrap();
let v = match r { Value::Float(f) => f, other => panic!("expected float, got {other:?}") };
#[allow(clippy::approx_constant)]
let expected_area = 3.14159_f64;
assert!((v - expected_area).abs() < 1e-6, "got {v}");
let rect = Value::Variant {
name: "Rect".into(),
args: vec![Value::record_dynamic({
let mut m = IndexMap::new();
m.insert("width".into(), Value::Float(2.0));
m.insert("height".into(), Value::Float(3.0));
m
})],
};
let r = vm.call("area", vec![rect]).unwrap();
assert_eq!(r, Value::Float(6.0));
}
#[test]
fn bytecode_is_reproducible() {
let src = include_str!("../../../examples/a_factorial.lex");
let p1 = compile(src);
let p2 = compile(src);
assert_eq!(p1, p2);
}
#[test]
fn match_with_literal_int() {
let src = "fn id_or_zero(n :: Int) -> Int {\n match n {\n 0 => 0,\n _ => n,\n }\n}\n";
let p = compile(src);
let mut vm = Vm::new(&p);
assert_eq!(vm.call("id_or_zero", vec![Value::Int(0)]).unwrap(), Value::Int(0));
assert_eq!(vm.call("id_or_zero", vec![Value::Int(7)]).unwrap(), Value::Int(7));
}
#[test]
fn slice3_fuses_two_local_add_and_runs_correctly() {
use lex_bytecode::op::Op;
let src = "fn add_them(a :: Int, b :: Int) -> Int { a + b }\n";
let p = compile(src);
let f = &p.functions[0];
assert!(
matches!(f.code[0], Op::LoadLocalAddLocal { lhs_idx: 0, rhs_idx: 1 }),
"slice 3 did not fire; got {:?}", f.code[0],
);
assert!(matches!(f.code[1], Op::LoadLocal(1)));
assert!(matches!(f.code[2], Op::IntAdd));
assert!(matches!(f.code[3], Op::Return));
let mut vm = Vm::new(&p);
let r = vm.call("add_them", vec![Value::Int(40), Value::Int(2)]).unwrap();
assert_eq!(r, Value::Int(42));
}
#[test]
fn slice4_fuses_two_local_sub_and_runs_correctly() {
use lex_bytecode::op::Op;
let src = "fn sub_them(a :: Int, b :: Int) -> Int { a - b }\n";
let p = compile(src);
let f = &p.functions[0];
assert!(
matches!(f.code[0], Op::LoadLocalSubLocal { lhs_idx: 0, rhs_idx: 1 }),
"slice 4 (sub) did not fire; got {:?}", f.code[0],
);
assert!(matches!(f.code[1], Op::LoadLocal(1)));
assert!(matches!(f.code[2], Op::IntSub));
assert!(matches!(f.code[3], Op::Return));
let mut vm = Vm::new(&p);
let r = vm.call("sub_them", vec![Value::Int(42), Value::Int(7)]).unwrap();
assert_eq!(r, Value::Int(35));
}
#[test]
fn slice4_fuses_two_local_mul_and_runs_correctly() {
use lex_bytecode::op::Op;
let src = "fn mul_them(a :: Int, b :: Int) -> Int { a * b }\n";
let p = compile(src);
let f = &p.functions[0];
assert!(
matches!(f.code[0], Op::LoadLocalMulLocal { lhs_idx: 0, rhs_idx: 1 }),
"slice 4 (mul) did not fire; got {:?}", f.code[0],
);
assert!(matches!(f.code[1], Op::LoadLocal(1)));
assert!(matches!(f.code[2], Op::IntMul));
assert!(matches!(f.code[3], Op::Return));
let mut vm = Vm::new(&p);
let r = vm.call("mul_them", vec![Value::Int(6), Value::Int(7)]).unwrap();
assert_eq!(r, Value::Int(42));
}
#[test]
fn slice4_does_not_fire_across_a_jump_target() {
let src = "
fn pick(flag :: Int, a :: Int, b :: Int) -> Int {
match flag {
0 => a - b,
1 => a * b,
_ => a,
}
}";
let p = compile(src);
let mut vm = Vm::new(&p);
assert_eq!(vm.call("pick", vec![Value::Int(0), Value::Int(10), Value::Int(3)]).unwrap(), Value::Int(7));
assert_eq!(vm.call("pick", vec![Value::Int(1), Value::Int(10), Value::Int(3)]).unwrap(), Value::Int(30));
assert_eq!(vm.call("pick", vec![Value::Int(9), Value::Int(10), Value::Int(3)]).unwrap(), Value::Int(10));
}
#[test]
fn slice5_fuses_pattern_match_arm_test() {
use lex_bytecode::op::Op;
let src = "
fn sum_to(n :: Int, acc :: Int) -> Int {
match n {
0 => acc,
_ => sum_to(n - 1, acc + n),
}
}";
let p = compile(src);
let fused_in_body = p.functions.iter().flat_map(|f| f.code.iter()).any(|op|
matches!(op, Op::LoadLocalEqIntConstJumpIfNot { .. }));
assert!(fused_in_body, "slice 5 did not fire on sum_to's `0 =>` arm test");
let mut vm = Vm::new(&p);
let r = vm.call("sum_to", vec![Value::Int(5), Value::Int(0)]).unwrap();
assert_eq!(r, Value::Int(15));
}
#[test]
fn slice5_runs_correctly_through_branch_not_taken() {
let src = "
fn sum_to(n :: Int, acc :: Int) -> Int {
match n {
0 => acc,
_ => sum_to(n - 1, acc + n),
}
}";
let p = compile(src);
let mut vm = Vm::new(&p);
let r = vm.call("sum_to", vec![Value::Int(0), Value::Int(42)]).unwrap();
assert_eq!(r, Value::Int(42));
}
#[test]
fn slice5_multi_arm_cascade_runs_correctly() {
let src = "
fn classify(n :: Int) -> Int {
match n {
0 => 100,
1 => 200,
2 => 300,
_ => 999,
}
}";
let p = compile(src);
let mut vm = Vm::new(&p);
assert_eq!(vm.call("classify", vec![Value::Int(0)]).unwrap(), Value::Int(100));
assert_eq!(vm.call("classify", vec![Value::Int(1)]).unwrap(), Value::Int(200));
assert_eq!(vm.call("classify", vec![Value::Int(2)]).unwrap(), Value::Int(300));
assert_eq!(vm.call("classify", vec![Value::Int(99)]).unwrap(), Value::Int(999));
}
#[test]
fn slice6_absorbs_match_scrutinee_dance() {
use lex_bytecode::op::Op;
let src = "
fn sum_to(n :: Int, acc :: Int) -> Int {
match n {
0 => acc,
_ => sum_to(n - 1, acc + n),
}
}";
let p = compile(src);
let fused = p.functions.iter().flat_map(|f| f.code.iter()).any(|op|
matches!(op, Op::LoadLocalStoreEqIntConstJumpIfNot { .. }));
assert!(fused, "slice 6 did not fire on sum_to");
let mut vm = Vm::new(&p);
let r = vm.call("sum_to", vec![Value::Int(5), Value::Int(0)]).unwrap();
assert_eq!(r, Value::Int(15));
}
#[test]
fn slice6_writes_dst_so_subsequent_arms_see_scrutinee() {
let src = "
fn classify(n :: Int) -> Int {
match n {
0 => 100,
1 => 200,
2 => 300,
3 => 400,
_ => 999,
}
}";
let p = compile(src);
let mut vm = Vm::new(&p);
assert_eq!(vm.call("classify", vec![Value::Int(0)]).unwrap(), Value::Int(100));
assert_eq!(vm.call("classify", vec![Value::Int(1)]).unwrap(), Value::Int(200));
assert_eq!(vm.call("classify", vec![Value::Int(2)]).unwrap(), Value::Int(300));
assert_eq!(vm.call("classify", vec![Value::Int(3)]).unwrap(), Value::Int(400));
assert_eq!(vm.call("classify", vec![Value::Int(99)]).unwrap(), Value::Int(999));
}
#[test]
fn slice3_does_not_fire_across_a_jump_target() {
let src = "
fn pick(flag :: Int, a :: Int, b :: Int) -> Int {
match flag {
0 => a + b,
_ => a,
}
}";
let p = compile(src);
let mut vm = Vm::new(&p);
assert_eq!(vm.call("pick", vec![Value::Int(0), Value::Int(10), Value::Int(5)]).unwrap(), Value::Int(15));
assert_eq!(vm.call("pick", vec![Value::Int(1), Value::Int(10), Value::Int(5)]).unwrap(), Value::Int(10));
}
#[test]
fn slice7_fuses_load_local_get_field_add() {
use lex_bytecode::op::Op;
let src = "
type R = { x :: Int, y :: Int, z :: Int }
fn sum_fields(r :: R) -> Int { r.x + r.y + r.z }
";
let p = compile(src);
let f = &p.functions[0];
let fused_count = f.code.iter()
.filter(|op| matches!(op, Op::LoadLocalGetFieldAdd { .. }))
.count();
assert!(fused_count >= 2,
"expected ≥2 slice-7 fusions in y+z chain, got {fused_count}: {:?}",
f.code);
let mut vm = Vm::new(&p);
let r = vm.call("sum_fields",
vec![Value::record_dynamic({
let mut m = IndexMap::new();
m.insert("x".into(), Value::Int(10));
m.insert("y".into(), Value::Int(20));
m.insert("z".into(), Value::Int(30));
m
})]).unwrap();
assert_eq!(r, Value::Int(60));
}
#[test]
fn slice7_works_on_stack_records_too() {
let src = r#"
fn inline_sum() -> Int {
let r := { a: 5, b: 11, c: 13 }
r.a + r.b + r.c
}
"#;
let p = compile(src);
use lex_bytecode::op::Op;
let f = &p.functions[0];
assert!(f.code.iter().any(|op| matches!(op, Op::AllocStackRecord { .. })),
"expected escape lowering to fire");
assert!(f.code.iter().any(|op| matches!(op, Op::LoadLocalGetFieldAdd { .. })),
"expected slice 7 fusion to fire");
let mut vm = Vm::new(&p);
assert_eq!(vm.call("inline_sum", vec![]).unwrap(), Value::Int(29));
}
#[test]
fn slice7_does_not_fire_across_a_jump_target() {
let src = r#"
type R = { v :: Int }
fn pick(flag :: Int, r :: R) -> Int {
match flag {
0 => 100 + r.v,
_ => r.v,
}
}
"#;
let p = compile(src);
let mut vm = Vm::new(&p);
let r1 = vm.call("pick", vec![Value::Int(0), Value::record_dynamic({
let mut m = IndexMap::new(); m.insert("v".into(), Value::Int(7)); m
})]).unwrap();
assert_eq!(r1, Value::Int(107));
let r2 = vm.call("pick", vec![Value::Int(1), Value::record_dynamic({
let mut m = IndexMap::new(); m.insert("v".into(), Value::Int(7)); m
})]).unwrap();
assert_eq!(r2, Value::Int(7));
}
#[test]
fn record_field_access() {
let src = "fn xof(r :: Record) -> Int { r.x }\n".replace(
"Record",
"{ x :: Int, y :: Int }",
);
let p = compile(&src);
let mut vm = Vm::new(&p);
let mut m = IndexMap::new();
m.insert("x".into(), Value::Int(11));
m.insert("y".into(), Value::Int(22));
let r = vm.call("xof", vec![Value::record_dynamic(m)]).unwrap();
assert_eq!(r, Value::Int(11));
}
#[test]
fn slice8_fuses_field_sub_and_computes_left_to_right() {
use lex_bytecode::op::Op;
let src = "
type R = { a :: Int, b :: Int, c :: Int }
fn diff(r :: R) -> Int { r.a - r.b - r.c }
";
let p = compile(src);
let n = p.functions[0].code.iter()
.filter(|op| matches!(op, Op::LoadLocalGetFieldSub { .. })).count();
assert!(n >= 2, "expected ≥2 slice-8 sub fusions, got {n}: {:?}",
p.functions[0].code);
let mut vm = Vm::new(&p);
let r = vm.call("diff", vec![Value::record_dynamic({
let mut m = IndexMap::new();
m.insert("a".into(), Value::Int(10));
m.insert("b".into(), Value::Int(3));
m.insert("c".into(), Value::Int(2));
m
})]).unwrap();
assert_eq!(r, Value::Int(5));
}
#[test]
fn slice8_fuses_field_mul() {
use lex_bytecode::op::Op;
let src = "
type R = { a :: Int, b :: Int, c :: Int }
fn prod(r :: R) -> Int { r.a * r.b * r.c }
";
let p = compile(src);
let n = p.functions[0].code.iter()
.filter(|op| matches!(op, Op::LoadLocalGetFieldMul { .. })).count();
assert!(n >= 2, "expected ≥2 slice-8 mul fusions, got {n}: {:?}",
p.functions[0].code);
let mut vm = Vm::new(&p);
let r = vm.call("prod", vec![Value::record_dynamic({
let mut m = IndexMap::new();
m.insert("a".into(), Value::Int(2));
m.insert("b".into(), Value::Int(3));
m.insert("c".into(), Value::Int(5));
m
})]).unwrap();
assert_eq!(r, Value::Int(30));
}
#[test]
fn slice8_mixed_arith_chain() {
let src = "
type R = { a :: Int, b :: Int, c :: Int, d :: Int }
fn mix(r :: R) -> Int { r.a + r.b - r.c * r.d }
";
let p = compile(src);
let mut vm = Vm::new(&p);
let r = vm.call("mix", vec![Value::record_dynamic({
let mut m = IndexMap::new();
m.insert("a".into(), Value::Int(4));
m.insert("b".into(), Value::Int(6));
m.insert("c".into(), Value::Int(3));
m.insert("d".into(), Value::Int(2));
m
})]).unwrap();
assert_eq!(r, Value::Int(4));
}
#[test]
fn slice8_sub_does_not_fire_across_a_jump_target() {
let src = "
type R = { v :: Int }
fn pick(flag :: Int, r :: R) -> Int {
match flag {
0 => 100 - r.v,
_ => r.v,
}
}
";
let p = compile(src);
let mut vm = Vm::new(&p);
let mk = || Value::record_dynamic({
let mut m = IndexMap::new(); m.insert("v".into(), Value::Int(7)); m
});
assert_eq!(vm.call("pick", vec![Value::Int(0), mk()]).unwrap(), Value::Int(93));
assert_eq!(vm.call("pick", vec![Value::Int(1), mk()]).unwrap(), Value::Int(7));
}
#[test]
fn slice9_fuses_bare_field_read() {
use lex_bytecode::op::Op;
let src = "
type R = { x :: Int, y :: Int }
fn getx(r :: R) -> Int { r.x }
";
let p = compile(src);
let f = &p.functions[0];
assert!(matches!(f.code[0], Op::LoadLocalGetField { local_idx: 0, .. }),
"slice 9 did not fire; got {:?}", f.code);
assert!(matches!(f.code[1], Op::GetField { .. }));
let mut vm = Vm::new(&p);
let r = vm.call("getx", vec![Value::record_dynamic({
let mut m = IndexMap::new();
m.insert("x".into(), Value::Int(11));
m.insert("y".into(), Value::Int(22));
m
})]).unwrap();
assert_eq!(r, Value::Int(11));
}
#[test]
fn slice9_fuses_chain_head_alongside_slice7() {
use lex_bytecode::op::Op;
let src = "
type R = { x :: Int, y :: Int }
fn add(r :: R) -> Int { r.x + r.y }
";
let p = compile(src);
let f = &p.functions[0];
let n9 = f.code.iter().filter(|o| matches!(o, Op::LoadLocalGetField { .. })).count();
let n7 = f.code.iter().filter(|o| matches!(o, Op::LoadLocalGetFieldAdd { .. })).count();
assert_eq!(n9, 1, "expected chain-head fused by slice 9: {:?}", f.code);
assert_eq!(n7, 1, "expected `+ r.y` fused by slice 7: {:?}", f.code);
let mut vm = Vm::new(&p);
let r = vm.call("add", vec![Value::record_dynamic({
let mut m = IndexMap::new();
m.insert("x".into(), Value::Int(30));
m.insert("y".into(), Value::Int(12));
m
})]).unwrap();
assert_eq!(r, Value::Int(42));
}
#[test]
fn slice9_reads_field_from_returned_heap_record() {
let src = "
type Resp = { status :: Int, total :: Int }
fn handle(n :: Int) -> Resp { { status: 200, total: n } }
fn drive(n :: Int) -> Int { let r := handle(n) r.total }
";
let p = compile(src);
let mut vm = Vm::new(&p);
assert_eq!(vm.call("drive", vec![Value::Int(99)]).unwrap(), Value::Int(99));
}
#[test]
fn slice9_reads_field_from_stack_record() {
let src = r#"
fn f() -> Int {
let r := { a: 7, b: 9 }
r.b
}
"#;
let p = compile(src);
use lex_bytecode::op::Op;
let f = &p.functions[0];
assert!(f.code.iter().any(|o| matches!(o, Op::AllocStackRecord { .. })),
"expected stack record");
assert!(f.code.iter().any(|o| matches!(o, Op::LoadLocalGetField { .. })),
"expected slice 9 fusion");
let mut vm = Vm::new(&p);
assert_eq!(vm.call("f", vec![]).unwrap(), Value::Int(9));
}
#[test]
fn slice9_does_not_fire_across_a_jump_target() {
let src = "
type R = { v :: Int }
fn pick(flag :: Int, r :: R) -> Int {
match flag {
0 => r.v,
_ => 0,
}
}
";
let p = compile(src);
let mut vm = Vm::new(&p);
let mk = || Value::record_dynamic({
let mut m = IndexMap::new(); m.insert("v".into(), Value::Int(5)); m
});
assert_eq!(vm.call("pick", vec![Value::Int(0), mk()]).unwrap(), Value::Int(5));
assert_eq!(vm.call("pick", vec![Value::Int(1), mk()]).unwrap(), Value::Int(0));
}