lex-runtime 0.9.2

Effect handler runtime + capability policy for Lex.
Documentation
//! std.flow orchestration: sequential, branch, retry. Spec §11.2.

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")
}

// -- sequential --------------------------------------------------------

#[test]
fn flow_sequential_composes_two_functions() {
    // build a closure that does (x + 1) then * 2
    let src = r#"
import "std.flow" as flow
fn pipeline(x :: Int) -> Int {
  let f := flow.sequential(
    fn (n :: Int) -> Int { n + 1 },
    fn (n :: Int) -> Int { n * 2 }
  )
  f(x)
}
"#;
    assert_eq!(run(src, "pipeline", vec![Value::Int(3)]), Value::Int(8));   // (3+1)*2
    assert_eq!(run(src, "pipeline", vec![Value::Int(0)]), Value::Int(2));   // (0+1)*2
}

#[test]
fn flow_sequential_captures_outer_locals() {
    let src = r#"
import "std.flow" as flow
fn make(k :: Int, x :: Int) -> Int {
  let f := flow.sequential(
    fn (n :: Int) -> Int { n + k },
    fn (n :: Int) -> Int { n * k }
  )
  f(x)
}
"#;
    // (5+3)*3 = 24
    assert_eq!(run(src, "make", vec![Value::Int(3), Value::Int(5)]), Value::Int(24));
}

// -- branch ------------------------------------------------------------

#[test]
fn flow_branch_picks_true_arm() {
    let src = r#"
import "std.flow" as flow
fn pos_or_zero(x :: Int) -> Int {
  let f := flow.branch(
    fn (n :: Int) -> Bool { n > 0 },
    fn (n :: Int) -> Int { n },
    fn (n :: Int) -> Int { 0 }
  )
  f(x)
}
"#;
    assert_eq!(run(src, "pos_or_zero", vec![Value::Int(7)]), Value::Int(7));
    assert_eq!(run(src, "pos_or_zero", vec![Value::Int(-3)]), Value::Int(0));
    assert_eq!(run(src, "pos_or_zero", vec![Value::Int(0)]), Value::Int(0));
}

#[test]
fn flow_branch_with_string_arms() {
    let src = r#"
import "std.flow" as flow
fn name(b :: Bool) -> Str {
  let f := flow.branch(
    fn (b :: Bool) -> Bool { b },
    fn (b :: Bool) -> Str { "yes" },
    fn (b :: Bool) -> Str { "no" }
  )
  f(b)
}
"#;
    assert_eq!(run(src, "name", vec![Value::Bool(true)]), Value::Str("yes".into()));
    assert_eq!(run(src, "name", vec![Value::Bool(false)]), Value::Str("no".into()));
}

// -- retry -------------------------------------------------------------

#[test]
fn flow_retry_returns_first_ok() {
    let src = r#"
import "std.flow" as flow
fn always_ok(x :: Int) -> Result[Int, Str] {
  let f := flow.retry(
    fn (n :: Int) -> Result[Int, Str] { Ok(n + 1) },
    3
  )
  f(x)
}
"#;
    assert_eq!(
        run(src, "always_ok", vec![Value::Int(10)]),
        Value::Variant { name: "Ok".into(), args: vec![Value::Int(11)] }
    );
}

#[test]
fn flow_retry_propagates_final_err() {
    // The closure always errors. Retry returns the last Err.
    let src = r#"
import "std.flow" as flow
fn always_err(x :: Int) -> Result[Int, Str] {
  let f := flow.retry(
    fn (n :: Int) -> Result[Int, Str] { Err("nope") },
    3
  )
  f(x)
}
"#;
    assert_eq!(
        run(src, "always_err", vec![Value::Int(0)]),
        Value::Variant { name: "Err".into(), args: vec![Value::Str("nope".into())] }
    );
}

#[test]
fn flow_retry_only_runs_max_times() {
    // Use list.fold to count attempts via a Lex-side counter — actually
    // simpler: just verify max=1 returns the first attempt's result.
    let src = r#"
import "std.flow" as flow
fn one_shot(x :: Int) -> Result[Int, Str] {
  let f := flow.retry(
    fn (n :: Int) -> Result[Int, Str] { Err("only once") },
    1
  )
  f(x)
}
"#;
    assert_eq!(
        run(src, "one_shot", vec![Value::Int(0)]),
        Value::Variant { name: "Err".into(), args: vec![Value::Str("only once".into())] }
    );
}

// -- composition -------------------------------------------------------

#[test]
fn flow_branch_composed_with_sequential() {
    let src = r#"
import "std.flow" as flow
fn sign_and_double(x :: Int) -> Int {
  let plus := flow.sequential(
    fn (n :: Int) -> Int { n },
    fn (n :: Int) -> Int { n * 2 }
  )
  let minus := flow.sequential(
    fn (n :: Int) -> Int { 0 - n },
    fn (n :: Int) -> Int { n * 2 }
  )
  let pick := flow.branch(
    fn (n :: Int) -> Bool { n >= 0 },
    plus,
    minus
  )
  pick(x)
}
"#;
    // plus: (3) -> 3*2 = 6
    assert_eq!(run(src, "sign_and_double", vec![Value::Int(3)]), Value::Int(6));
    // minus: (-3) -> -(-3)*2 = 6
    assert_eq!(run(src, "sign_and_double", vec![Value::Int(-3)]), Value::Int(6));
}

// -- parallel ---------------------------------------------------------

#[test]
fn flow_parallel_returns_tuple_of_both_results() {
    // Two 0-arg closures; flow.parallel returns a closure that returns
    // their results as a 2-tuple.
    let src = r#"
import "std.flow" as flow
fn run_pair() -> Tuple[Int, Int] {
  let p := flow.parallel(
    fn () -> Int { 7 },
    fn () -> Int { 11 }
  )
  p()
}
"#;
    let r = run(src, "run_pair", vec![]);
    assert_eq!(r, Value::Tuple(vec![Value::Int(7), Value::Int(11)]));
}

#[test]
fn flow_parallel_handles_heterogeneous_return_types() {
    // Type variables A and B unify independently — first closure returns
    // Int, second returns Str; the resulting tuple is Tuple[Int, Str].
    let src = r#"
import "std.flow" as flow
fn pair() -> Tuple[Int, Str] {
  let p := flow.parallel(
    fn () -> Int { 42 },
    fn () -> Str { "hello" }
  )
  p()
}
"#;
    let r = run(src, "pair", vec![]);
    assert_eq!(r, Value::Tuple(vec![Value::Int(42), Value::Str("hello".into())]));
}