use lambda_throw_cat::error::Error;
use lambda_throw_cat::eval::Fuel;
use lambda_throw_cat::value::Value;
use lambda_throw_cat::{DEFAULT_FUEL, run, run_with_fuel};
#[derive(Debug)]
enum TestFailure {
Interpreter(Error),
Assertion {
what: &'static str,
actual: String,
expected: String,
},
}
impl From<Error> for TestFailure {
fn from(value: Error) -> Self {
Self::Interpreter(value)
}
}
impl std::fmt::Display for TestFailure {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Interpreter(e) => write!(f, "interpreter error: {e}"),
Self::Assertion {
what,
actual,
expected,
} => write!(
f,
"assertion {what:?} failed: expected {expected:?}, got {actual:?}"
),
}
}
}
fn check_displays(value: &Value, expected: &str, what: &'static str) -> Result<(), TestFailure> {
let actual = format!("{value}");
(actual == expected)
.then_some(())
.ok_or(TestFailure::Assertion {
what,
actual,
expected: expected.to_owned(),
})
}
fn check_error_matches<F: FnOnce(&Error) -> bool>(
result: Result<Value, Error>,
predicate: F,
what: &'static str,
) -> Result<(), TestFailure> {
match result {
Err(e) => predicate(&e).then_some(()).ok_or(TestFailure::Assertion {
what,
actual: format!("{e}"),
expected: "matching error variant".to_owned(),
}),
Ok(v) => Err(TestFailure::Assertion {
what,
actual: format!("{v}"),
expected: "an error".to_owned(),
}),
}
}
#[test]
fn try_with_no_throw_returns_body_value() -> Result<(), TestFailure> {
let value = run(r"try (\x. x) catch e. e").run()?;
check_displays(&value, "\\x. x", "try with no throw returns body")
}
#[test]
fn try_catches_immediate_throw() -> Result<(), TestFailure> {
let value = run(r"try throw (\x. x) catch e. e").run()?;
check_displays(&value, "\\x. x", "immediate throw caught and rebound")
}
#[test]
fn catch_can_transform_thrown_value() -> Result<(), TestFailure> {
let value = run(r"try throw (\x. x) catch _ignored. (\y. y)").run()?;
check_displays(&value, "\\y. y", "catch arm yields handler value")
}
#[test]
fn nested_try_inner_catches() -> Result<(), TestFailure> {
let source = r"
try
try throw (\a. a) catch e. e
catch outer. (\never. never)
";
let value = run(source).run()?;
check_displays(&value, "\\a. a", "inner catch handles, outer untouched")
}
#[test]
fn nested_try_outer_catches_inner_handler_throw() -> Result<(), TestFailure> {
let source = r"
try
try throw (\inner. inner) catch e. throw (\outer. outer)
catch o. o
";
let value = run(source).run()?;
check_displays(
&value,
"\\outer. outer",
"outer catch handles inner-handler throw",
)
}
#[test]
fn throw_inside_lambda_body_propagates_at_call_site() -> Result<(), TestFailure> {
let source = r"
let raiser = \dummy. throw (\thrown. thrown) in
try raiser (\unused. unused) catch e. e
";
let value = run(source).run()?;
check_displays(
&value,
"\\thrown. thrown",
"throw inside lambda body propagates",
)
}
#[test]
fn throw_inside_seq_skips_remainder() -> Result<(), TestFailure> {
let source = r"
try
throw (\thrown. thrown) ; (\never. never)
catch e. e
";
let value = run(source).run()?;
check_displays(
&value,
"\\thrown. thrown",
"throw short-circuits the rest of the sequence",
)
}
#[test]
fn heap_mutation_visible_after_catch() -> Result<(), TestFailure> {
let source = r"
let r = ref (\original. original) in
try
r := (\after_store. after_store) ;
throw (\thrown. thrown)
catch _ignored. !r
";
let value = run(source).run()?;
check_displays(
&value,
"\\after_store. after_store",
"pre-throw mutation visible in catch",
)
}
#[test]
fn uncaught_throw_at_top_surfaces_as_uncaught_exception() -> Result<(), TestFailure> {
let result = run(r"throw (\x. x)").run();
check_error_matches(
result,
|e| matches!(e, Error::UncaughtException { .. }),
"uncaught throw becomes UncaughtException at the top",
)
}
#[test]
fn other_errors_are_not_caught() -> Result<(), TestFailure> {
let result = run(r"try unbound_name catch e. e").run();
check_error_matches(
result,
|e| matches!(e, Error::UnboundVariable { .. }),
"try-catch does not swallow non-throw errors",
)
}
#[test]
fn omega_still_exhausts_fuel_inside_try() -> Result<(), TestFailure> {
let result = run_with_fuel(r"try (\x. x x) (\x. x x) catch e. e", Fuel::new(64)).run();
check_error_matches(
result,
|e| matches!(e, Error::FuelExhausted { .. }),
"fuel exhaustion is not catchable",
)
}
#[test]
fn catch_can_rethrow_to_outer_catch() -> Result<(), TestFailure> {
let source = r"
try
try throw (\original. original) catch e. throw e
catch caught. caught
";
let value = run(source).run()?;
check_displays(
&value,
"\\original. original",
"rethrow forwards the same value",
)
}
#[test]
fn default_fuel_constant_visible() -> Result<(), TestFailure> {
(DEFAULT_FUEL == 10_000)
.then_some(())
.ok_or(TestFailure::Assertion {
what: "DEFAULT_FUEL constant",
actual: DEFAULT_FUEL.to_string(),
expected: "10000".to_owned(),
})
}