use lex_ast::canonicalize_program;
use lex_bytecode::{compile_program, vm::Vm, Value};
use lex_runtime::{DefaultHandler, Policy};
use lex_syntax::parse_source;
fn run(src: &str, func: &str, args: Vec<Value>) -> Value {
let prog = parse_source(src).expect("parse");
let stages = canonicalize_program(&prog);
if let Err(errs) = lex_types::check_program(&stages) {
panic!("type errors: {errs:#?}");
}
let bc = compile_program(&stages);
let handler = DefaultHandler::new(Policy::permissive());
let mut vm = Vm::with_handler(&bc, Box::new(handler));
vm.call(func, args).expect("vm")
}
fn type_check(src: &str) -> Result<(), Vec<lex_types::TypeError>> {
let prog = parse_source(src).expect("parse");
let stages = canonicalize_program(&prog);
lex_types::check_program(&stages).map(|_| ())
}
#[test]
fn list_map_with_effectful_closure_type_checks() {
let src = r#"
import "std.list" as list
import "std.time" as time
fn timestamp_each(xs :: List[Int]) -> [time] List[Int] {
list.map(xs, fn (x :: Int) -> [time] Int { x + time.now() })
}
"#;
type_check(src).expect("type-check should accept effectful closure");
}
#[test]
fn list_map_propagated_effect_must_be_declared_on_caller() {
let src = r#"
import "std.list" as list
import "std.time" as time
fn timestamp_each(xs :: List[Int]) -> List[Int] {
list.map(xs, fn (x :: Int) -> [time] Int { x + time.now() })
}
"#;
let errs = type_check(src).expect_err("expected effect leak");
assert!(errs.iter().any(|e| matches!(e,
lex_types::TypeError::EffectNotDeclared { effect, .. } if effect == "time")),
"expected EffectNotDeclared(time); got {errs:#?}");
}
#[test]
fn list_map_with_pure_closure_still_works() {
let src = r#"
import "std.list" as list
fn doubled(xs :: List[Int]) -> List[Int] {
list.map(xs, fn (n :: Int) -> Int { n * 2 })
}
"#;
let r = run(src, "doubled",
vec![Value::List(vec![Value::Int(1), Value::Int(2), Value::Int(3)].into())]);
assert_eq!(r, Value::List(vec![Value::Int(2), Value::Int(4), Value::Int(6)].into()));
}
#[test]
fn list_filter_with_effectful_predicate_type_checks() {
let src = r#"
import "std.list" as list
import "std.time" as time
fn keep_recent(xs :: List[Int]) -> [time] List[Int] {
list.filter(xs, fn (x :: Int) -> [time] Bool { x > time.now() })
}
"#;
type_check(src).expect("filter with [time] predicate should work");
}
#[test]
fn list_fold_with_effectful_combiner_type_checks() {
let src = r#"
import "std.list" as list
import "std.time" as time
fn folded(xs :: List[Int]) -> [time] Int {
list.fold(xs, 0, fn (acc :: Int, x :: Int) -> [time] Int {
acc + x + time.now()
})
}
"#;
type_check(src).expect("fold with [time] combiner should work");
}
#[test]
fn result_map_with_effectful_closure_type_checks() {
let src = r#"
import "std.result" as result
import "std.time" as time
fn stamp(r :: Result[Int, Str]) -> [time] Result[Int, Str] {
result.map(r, fn (n :: Int) -> [time] Int { n + time.now() })
}
"#;
type_check(src).expect("result.map with [time] closure should work");
}
#[test]
fn result_and_then_with_effectful_closure_type_checks() {
let src = r#"
import "std.result" as result
import "std.time" as time
fn chain(r :: Result[Int, Str]) -> [time] Result[Int, Str] {
result.and_then(r, fn (n :: Int) -> [time] Result[Int, Str] {
Ok(n + time.now())
})
}
"#;
type_check(src).expect("result.and_then with [time] closure should work");
}
#[test]
fn option_map_with_effectful_closure_type_checks() {
let src = r#"
import "std.option" as option
import "std.time" as time
fn stamp_opt(o :: Option[Int]) -> [time] Option[Int] {
option.map(o, fn (n :: Int) -> [time] Int { n + time.now() })
}
"#;
type_check(src).expect("option.map with [time] closure should work");
}
#[test]
fn effectful_list_map_runs_end_to_end() {
let src = r#"
import "std.list" as list
import "std.rand" as rand
fn midpoints(xs :: List[Int]) -> [rand] List[Int] {
list.map(xs, fn (hi :: Int) -> [rand] Int { rand.int_in(0, hi) })
}
"#;
let r = run(src, "midpoints",
vec![Value::List(vec![Value::Int(10), Value::Int(100), Value::Int(6)].into())]);
assert_eq!(r, Value::List(vec![Value::Int(5), Value::Int(50), Value::Int(3)].into()));
}