use lambda_cat::error::Error;
use lambda_cat::eval::Fuel;
use lambda_cat::value::Value;
use lambda_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 identity_via_let() -> Result<(), TestFailure> {
let value = run(r"let id = \x. x in id").run()?;
check_displays(&value, "\\x. x", "identity binding")
}
#[test]
fn k_combinator_first() -> Result<(), TestFailure> {
let source = r"
let id = \x. x in
let one = \x. x x in
(\x. \y. x) id one
";
let value = run(source).run()?;
check_displays(&value, "\\x. x", "K picks first argument")
}
#[test]
fn k_combinator_second() -> Result<(), TestFailure> {
let source = r"
let id = \x. x in
let two = \z. z in
(\x. \y. y) id two
";
let value = run(source).run()?;
check_displays(&value, "\\z. z", "K-second picks second argument")
}
#[test]
fn lexical_scoping_shadowing() -> Result<(), TestFailure> {
let source = r"
let x = \a. a in
let y = \b. b in
(\x. x) y
";
let value = run(source).run()?;
check_displays(&value, "\\b. b", "inner binding shadows outer")
}
#[test]
fn nested_lambda_capture() -> Result<(), TestFailure> {
let source = r"
let outer = \x. \y. x in
let inner_a = \u. u in
let inner_b = \v. v in
outer inner_a inner_b
";
let value = run(source).run()?;
check_displays(&value, "\\u. u", "outer binding survives inner argument")
}
#[test]
fn church_true_picks_first() -> Result<(), TestFailure> {
let source = r"
let tru = \t. \f. t in
let yes = \a. a in
let no = \b. b in
tru yes no
";
let value = run(source).run()?;
check_displays(&value, "\\a. a", "Church true selects then-branch")
}
#[test]
fn church_false_picks_second() -> Result<(), TestFailure> {
let source = r"
let fls = \t. \f. f in
let yes = \a. a in
let no = \b. b in
fls yes no
";
let value = run(source).run()?;
check_displays(&value, "\\b. b", "Church false selects else-branch")
}
#[test]
fn fix_without_recursion_terminates() -> Result<(), TestFailure> {
let source = r"(fix f. \n. n) (\x. x)";
let value = run(source).run()?;
check_displays(
&value,
"\\x. x",
"fix whose body ignores f reduces normally",
)
}
#[test]
fn fix_with_let_inside() -> Result<(), TestFailure> {
let source = r"
let const = fix self. \x. \y. x in
const (\u. u) (\v. v)
";
let value = run(source).run()?;
check_displays(
&value,
"\\u. u",
"fix value behaves as a normal closure when self is unused",
)
}
#[test]
fn unbound_variable_errors() -> Result<(), TestFailure> {
let result = run(r"(\x. y) (\u. u)").run();
check_error_matches(
result,
|e| matches!(e, Error::UnboundVariable { .. }),
"free variable surfaces as UnboundVariable",
)
}
#[test]
fn omega_diverges_into_fuel_exhaustion() -> Result<(), TestFailure> {
let result = run_with_fuel(r"(\x. x x) (\x. x x)", Fuel::new(64)).run();
check_error_matches(
result,
|e| matches!(e, Error::FuelExhausted { .. }),
"omega combinator exhausts a bounded fuel budget",
)
}
#[test]
fn parse_error_propagates() -> Result<(), TestFailure> {
let result = run(r"\x").run();
check_error_matches(
result,
|e| {
matches!(
e,
Error::UnexpectedEnd { .. } | Error::UnexpectedToken { .. }
)
},
"incomplete lambda surfaces as a parse error",
)
}
#[test]
fn default_fuel_handles_small_programs() -> Result<(), TestFailure> {
let source = r"
let id = \x. x in
let k = \x. \y. x in
let s = \x. \y. \z. x z (y z) in
s k k (\q. q)
";
let value = run(source).run()?;
check_displays(&value, "\\q. q", "SKK identity equivalence")?;
(DEFAULT_FUEL == 10_000)
.then_some(())
.ok_or(TestFailure::Assertion {
what: "DEFAULT_FUEL constant",
actual: DEFAULT_FUEL.to_string(),
expected: "10000".to_owned(),
})
}