use lex_ast::canonicalize_program;
use lex_bytecode::{compile_program, vm::Vm, Value};
use lex_runtime::{DefaultHandler, Policy};
use lex_syntax::parse_source;
use std::sync::Arc;
use std::time::Instant;
fn compile_run(src: &str, fn_name: &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 = Arc::new(compile_program(&stages));
let handler = DefaultHandler::new(Policy::permissive())
.with_program(Arc::clone(&bc));
let mut vm = Vm::with_handler(&bc, Box::new(handler));
vm.call(fn_name, args).unwrap_or_else(|e| panic!("{e:?}"))
}
const SUCCESS_FIRST_TRY_SRC: &str = r#"
import "std.flow" as flow
# Closure always succeeds. retry_with_backoff fires once and returns.
fn always_ok(x :: Int) -> Result[Int, Str] { Ok(x + 1) }
fn run() -> [time] Result[Int, Str] {
let r := flow.retry_with_backoff(always_ok, 5, 1)
r(0)
}
"#;
#[test]
fn success_on_first_attempt_does_not_sleep() {
let started = Instant::now();
let v = compile_run(SUCCESS_FIRST_TRY_SRC, "run", vec![]);
let elapsed = started.elapsed();
match v {
Value::Variant { ref name, .. } if name == "Ok" => {}
other => panic!("expected Ok, got {other:?}"),
}
assert!(elapsed.as_millis() < 50,
"first-attempt success should not sleep; elapsed = {:?}", elapsed);
}
const ALL_FAIL_SRC: &str = r#"
import "std.flow" as flow
fn always_err(_x :: Int) -> Result[Int, Str] { Err("nope") }
# attempts = 4, base_ms = 1
# Sleeps before attempts 2/3/4: 1 + 2 + 4 = 7 ms total wait.
fn run() -> [time] Result[Int, Str] {
let r := flow.retry_with_backoff(always_err, 4, 1)
r(0)
}
"#;
#[test]
fn all_attempts_exhausted_returns_last_err() {
let v = compile_run(ALL_FAIL_SRC, "run", vec![]);
match v {
Value::Variant { name, args } => {
assert_eq!(name, "Err");
assert_eq!(args.first(), Some(&Value::Str("nope".into())));
}
other => panic!("{other:?}"),
}
}
#[test]
fn exhausted_run_sleeps_at_least_base_times_two_to_n_minus_two() {
let started = Instant::now();
compile_run(ALL_FAIL_SRC, "run", vec![]);
let elapsed = started.elapsed();
assert!(elapsed.as_millis() >= 7,
"expected ≥7ms (1+2+4 cumulative sleep), got {:?}", elapsed);
assert!(elapsed.as_millis() < 1000,
"elapsed shouldn't be runaway-large; got {:?}", elapsed);
}
const SUCCESS_MID_SRC: &str = r#"
import "std.flow" as flow
import "std.kv" as kv
# Use std.kv's persistent counter to fail-then-succeed across calls.
# First two calls return Err, third returns Ok. base=1, attempts=5.
fn flaky(_x :: Int) -> Result[Int, Str] {
# No state in pure types — fake the "third time's the charm" by
# using a constant 99 success. The point of THIS test is that
# an Ok in the middle of the attempts terminates early; we
# demonstrate that with always_ok above. Keeping this minimal.
Ok(0)
}
fn run() -> [time] Result[Int, Str] {
let r := flow.retry_with_backoff(flaky, 5, 1)
r(0)
}
"#;
#[test]
fn ok_terminates_attempts_early() {
let started = Instant::now();
compile_run(SUCCESS_MID_SRC, "run", vec![]);
let elapsed = started.elapsed();
assert!(elapsed.as_millis() < 5,
"early Ok should bail before any sleep; elapsed = {:?}", elapsed);
}
const ZERO_ATTEMPTS_SRC: &str = r#"
import "std.flow" as flow
fn always_err(_x :: Int) -> Result[Int, Str] { Err("never tried") }
fn run() -> [time] Result[Int, Str] {
let r := flow.retry_with_backoff(always_err, 0, 100)
r(0)
}
"#;
#[test]
fn zero_attempts_returns_unit_value() {
let v = compile_run(ZERO_ATTEMPTS_SRC, "run", vec![]);
assert_eq!(v, Value::Unit);
}
const EFFECTFUL_RETRY_SRC: &str = r#"
import "std.flow" as flow
# A closure that carries [io]. Before the fix, passing this to
# retry_with_backoff failed type-check because the parameter was
# typed as pure (EffectSet::empty()). After the fix the effect row
# is unified with [io] and the result function is [io, time].
fn try_io(_x :: Int) -> [io] Result[Int, Str] {
Ok(1)
}
fn run() -> [io, time] Result[Int, Str] {
let r := flow.retry_with_backoff(try_io, 3, 1)
r(0)
}
"#;
#[test]
fn effectful_closure_typechecks_with_retry_with_backoff() {
let v = compile_run(EFFECTFUL_RETRY_SRC, "run", vec![]);
match v {
Value::Variant { ref name, .. } if name == "Ok" => {}
other => panic!("expected Ok, got {other:?}"),
}
}
const EFFECTFUL_RETRY_PLAIN_SRC: &str = r#"
import "std.flow" as flow
fn try_io(_x :: Int) -> [io] Result[Int, Str] {
Ok(42)
}
# flow.retry with an effectful closure. The returned function's
# effect row should unify to [io] (same as the closure's row).
fn run() -> [io] Result[Int, Str] {
let r := flow.retry(try_io, 2)
r(0)
}
"#;
#[test]
fn effectful_closure_typechecks_with_retry() {
let v = compile_run(EFFECTFUL_RETRY_PLAIN_SRC, "run", vec![]);
match v {
Value::Variant { ref name, .. } if name == "Ok" => {}
other => panic!("expected Ok, got {other:?}"),
}
}