use terni::{Imperfect, Loss};
use crate::oid::Oid;
use super::Lambda;
#[derive(Clone, Debug, PartialEq)]
pub enum ReductionError {
UnboundVariable(Oid),
BudgetExhausted { steps_taken: usize },
NoMatchingArm,
}
#[derive(Clone, Debug, PartialEq, Default)]
pub struct ReductionLoss {
pub steps: usize,
pub budget_remaining: usize,
}
impl Loss for ReductionLoss {
fn zero() -> Self {
ReductionLoss {
steps: 0,
budget_remaining: usize::MAX,
}
}
fn total() -> Self {
ReductionLoss {
steps: usize::MAX,
budget_remaining: 0,
}
}
fn is_zero(&self) -> bool {
self.steps == 0
}
fn combine(self, other: Self) -> Self {
ReductionLoss {
steps: self.steps.saturating_add(other.steps),
budget_remaining: self.budget_remaining.min(other.budget_remaining),
}
}
}
pub fn reduce_bounded<T: Clone + PartialEq>(
term: Lambda<T>,
budget: usize,
) -> Imperfect<Lambda<T>, ReductionError, ReductionLoss> {
let mut current = term;
let mut steps = 0;
loop {
if steps >= budget {
return Imperfect::Failure(
ReductionError::BudgetExhausted { steps_taken: steps },
ReductionLoss {
steps,
budget_remaining: 0,
},
);
}
match try_step(¤t) {
Some(next) => {
current = next;
steps += 1;
}
None => {
let loss = ReductionLoss {
steps,
budget_remaining: budget - steps,
};
if steps == 0 {
return Imperfect::Success(current);
} else {
return Imperfect::Partial(current, loss);
}
}
}
}
}
fn try_step<T: Clone + PartialEq>(term: &Lambda<T>) -> Option<Lambda<T>> {
match term {
Lambda::Apply(app) => {
match app.function.as_ref() {
Lambda::Abs(abs) => {
Some(substitute(&abs.body, &abs.param, &app.argument))
}
_ => {
if let Some(reduced_fn) = try_step(&app.function) {
Some(Lambda::apply(reduced_fn, *app.argument.clone()))
} else if let Some(reduced_arg) = try_step(&app.argument) {
Some(Lambda::apply(*app.function.clone(), reduced_arg))
} else {
None
}
}
}
}
Lambda::Abs(abs) => {
try_step(&abs.body).map(|reduced| Lambda::abs(abs.param.clone(), reduced))
}
Lambda::Case(c) => {
if let Some(reduced) = try_step(&c.scrutinee) {
Some(Lambda::case(reduced, c.arms.clone()))
} else {
None
}
}
Lambda::Bind(_) => None, }
}
fn substitute<T: Clone + PartialEq>(
body: &Lambda<T>,
param: &Oid,
argument: &Lambda<T>,
) -> Lambda<T> {
match body {
Lambda::Bind(b) if b.name == *param => argument.clone(),
Lambda::Bind(_) => body.clone(),
Lambda::Abs(a) if a.param == *param => body.clone(), Lambda::Abs(a) => Lambda::abs(a.param.clone(), substitute(&a.body, param, argument)),
Lambda::Apply(a) => Lambda::apply(
substitute(&a.function, param, argument),
substitute(&a.argument, param, argument),
),
Lambda::Case(c) => Lambda::case(
substitute(&c.scrutinee, param, argument),
c.arms
.iter()
.map(|(p, t)| (p.clone(), substitute(t, param, argument)))
.collect(),
),
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::lambda::Pattern;
use crate::oid::Oid;
#[test]
fn reduction_loss_zero_is_zero() {
assert!(ReductionLoss::zero().is_zero());
}
#[test]
fn reduction_loss_total_is_not_zero() {
assert!(!ReductionLoss::total().is_zero());
}
#[test]
fn reduction_loss_combine_adds_steps() {
let a = ReductionLoss {
steps: 3,
budget_remaining: 10,
};
let b = ReductionLoss {
steps: 5,
budget_remaining: 7,
};
let combined = a.combine(b);
assert_eq!(combined.steps, 8);
assert_eq!(combined.budget_remaining, 7); }
#[test]
fn beta_reduction_identity() {
let x = Oid::hash(b"x");
let id = Lambda::<String>::abs(x.clone(), Lambda::bind(x));
let arg = Lambda::bind(Oid::hash(b"hello"));
let app = Lambda::apply(id, arg.clone());
let result = reduce_bounded(app, 10);
assert_eq!(result.ok(), Some(arg));
}
#[test]
fn beta_reduction_constant() {
let x = Oid::hash(b"x");
let y = Oid::hash(b"y");
let konst =
Lambda::<String>::abs(x.clone(), Lambda::abs(y.clone(), Lambda::bind(x.clone())));
let a = Lambda::bind(Oid::hash(b"a"));
let b = Lambda::bind(Oid::hash(b"b"));
let app = Lambda::apply(Lambda::apply(konst, a.clone()), b);
let result = reduce_bounded(app, 10);
assert_eq!(result.ok(), Some(a));
}
#[test]
fn budget_exhausted_returns_failure() {
let x = Oid::hash(b"x");
let omega = Lambda::<String>::abs(
x.clone(),
Lambda::apply(Lambda::bind(x.clone()), Lambda::bind(x.clone())),
);
let big_omega = Lambda::apply(omega.clone(), omega);
let result = reduce_bounded(big_omega, 5);
assert!(result.is_err());
match result {
Imperfect::Failure(ReductionError::BudgetExhausted { steps_taken }, loss) => {
assert_eq!(steps_taken, 5);
assert_eq!(loss.budget_remaining, 0);
}
other => panic!("expected BudgetExhausted, got {:?}", other),
}
}
#[test]
fn normal_form_zero_budget_is_success() {
let term = Lambda::<String>::bind(Oid::hash(b"x"));
let result = reduce_bounded(term.clone(), 0);
assert!(result.is_err());
}
#[test]
fn shadowed_variable_not_substituted() {
let x = Oid::hash(b"x");
let shadowed =
Lambda::<String>::abs(x.clone(), Lambda::abs(x.clone(), Lambda::bind(x.clone())));
let a = Lambda::bind(Oid::hash(b"a"));
let app = Lambda::apply(shadowed, a);
let result = reduce_bounded(app, 10);
let expected = Lambda::<String>::abs(Oid::hash(b"x"), Lambda::bind(Oid::hash(b"x")));
assert_eq!(result.ok(), Some(expected));
}
#[test]
fn substitute_in_case() {
let x = Oid::hash(b"x");
let body = Lambda::<String>::case(
Lambda::bind(x.clone()),
vec![(Pattern::Any, Lambda::bind(x.clone()))],
);
let abs = Lambda::abs(x, body);
let a = Lambda::bind(Oid::hash(b"a"));
let app = Lambda::apply(abs, a.clone());
let result = reduce_bounded(app, 10);
let expected = Lambda::<String>::case(a.clone(), vec![(Pattern::Any, a)]);
assert_eq!(result.ok(), Some(expected));
}
#[test]
fn reduce_under_abstraction() {
let x = Oid::hash(b"x");
let y = Oid::hash(b"y");
let z = Oid::hash(b"z");
let inner = Lambda::<String>::apply(
Lambda::abs(x.clone(), Lambda::bind(x)),
Lambda::bind(z.clone()),
);
let outer = Lambda::abs(y, inner);
let result = reduce_bounded(outer, 10);
let expected = Lambda::<String>::abs(Oid::hash(b"y"), Lambda::bind(z));
assert_eq!(result.ok(), Some(expected));
}
#[test]
fn partial_result_has_loss() {
let x = Oid::hash(b"x");
let id = Lambda::<String>::abs(x.clone(), Lambda::bind(x));
let arg = Lambda::bind(Oid::hash(b"hello"));
let app = Lambda::apply(id, arg);
let result = reduce_bounded(app, 10);
match result {
Imperfect::Partial(_, loss) => {
assert_eq!(loss.steps, 1);
assert_eq!(loss.budget_remaining, 9);
}
other => panic!("expected Partial, got {:?}", other),
}
}
}