Skip to main content

lambda_cat/
eval.rs

1//! Tree-walking interpreter for the lambda calculus.
2//!
3//! Evaluation is purely functional: every reduction step is a non-mutating
4//! transformation of an immutable environment.  Recursion is bounded by a
5//! [`Fuel`] budget so that proptest-generated inputs cannot stall the test
6//! suite.  Fixed points are unfolded by substitution; the captured
7//! environment carries no self-reference, which keeps every value
8//! representable without `Rc`, `Arc`, or interior mutability.
9
10use crate::env::Env;
11use crate::error::Error;
12use crate::syntax::{Expr, VarName};
13use crate::value::Value;
14
15/// A step budget for evaluation.
16///
17/// Spend one unit at every reduction step so that diverging programs
18/// surface as [`Error::FuelExhausted`] rather than stack overflow or
19/// non-termination.
20///
21/// [`Error::FuelExhausted`]: crate::error::Error::FuelExhausted
22#[derive(Debug, Clone, Copy, PartialEq, Eq)]
23pub struct Fuel {
24    limit: u64,
25    remaining: u64,
26}
27
28impl Fuel {
29    /// A fresh budget of `limit` steps.
30    #[must_use]
31    pub fn new(limit: u64) -> Self {
32        Self {
33            limit,
34            remaining: limit,
35        }
36    }
37
38    /// The original budget this `Fuel` was created with.
39    #[must_use]
40    pub fn limit(&self) -> u64 {
41        self.limit
42    }
43
44    /// The number of steps still available.
45    #[must_use]
46    pub fn remaining(&self) -> u64 {
47        self.remaining
48    }
49
50    fn spend(self) -> Result<Self, Error> {
51        match self.remaining {
52            0 => Err(Error::FuelExhausted { limit: self.limit }),
53            n => Ok(Self {
54                limit: self.limit,
55                remaining: n - 1,
56            }),
57        }
58    }
59}
60
61/// Evaluate `expr` against `env` with the given step budget.
62///
63/// Returns the resulting [`Value`] paired with the remaining fuel.  The
64/// returned fuel can be threaded into further evaluations.
65///
66/// # Errors
67///
68/// Returns [`Error::UnboundVariable`] if evaluation references a free
69/// variable not bound in `env`, or [`Error::FuelExhausted`] if reduction
70/// exceeds the budget.
71///
72/// [`Error::UnboundVariable`]: crate::error::Error::UnboundVariable
73/// [`Error::FuelExhausted`]: crate::error::Error::FuelExhausted
74///
75/// # Examples
76///
77/// ```
78/// # fn main() -> Result<(), lambda_cat::error::Error> {
79/// use lambda_cat::env::Env;
80/// use lambda_cat::eval::{eval, Fuel};
81/// use lambda_cat::syntax::Expr;
82///
83/// let id = Expr::lam("x", Expr::var("x"));
84/// let (value, _fuel) = eval(&id, &Env::empty(), Fuel::new(100))?;
85/// assert_eq!(format!("{value}"), "\\x. x");
86/// # Ok(())
87/// # }
88/// ```
89pub fn eval(expr: &Expr, env: &Env, fuel: Fuel) -> Result<(Value, Fuel), Error> {
90    let fuel = fuel.spend()?;
91    step(expr, env, fuel)
92}
93
94fn step(expr: &Expr, env: &Env, fuel: Fuel) -> Result<(Value, Fuel), Error> {
95    match expr {
96        Expr::Var(name) => eval_var(name, env, fuel),
97        Expr::Lam { param, body } => Ok((
98            Value::closure(param.clone(), (**body).clone(), env.clone()),
99            fuel,
100        )),
101        Expr::App { func, arg } => eval_app(func, arg, env, fuel),
102        Expr::Let { name, value, body } => eval_let(name, value, body, env, fuel),
103        Expr::Fix { name, body } => eval_fix(expr, name, body, env, fuel),
104    }
105}
106
107fn eval_var(name: &VarName, env: &Env, fuel: Fuel) -> Result<(Value, Fuel), Error> {
108    env.lookup(name)
109        .cloned()
110        .map(|v| (v, fuel))
111        .ok_or_else(|| Error::UnboundVariable { name: name.clone() })
112}
113
114fn eval_app(func: &Expr, arg: &Expr, env: &Env, fuel: Fuel) -> Result<(Value, Fuel), Error> {
115    let (func_v, fuel) = eval(func, env, fuel)?;
116    let (arg_v, fuel) = eval(arg, env, fuel)?;
117    apply(func_v, arg_v, fuel)
118}
119
120fn eval_let(
121    name: &VarName,
122    value: &Expr,
123    body: &Expr,
124    env: &Env,
125    fuel: Fuel,
126) -> Result<(Value, Fuel), Error> {
127    let (value_v, fuel) = eval(value, env, fuel)?;
128    let new_env = env.extend(name.clone(), value_v);
129    eval(body, &new_env, fuel)
130}
131
132fn eval_fix(
133    whole: &Expr,
134    name: &VarName,
135    body: &Expr,
136    env: &Env,
137    fuel: Fuel,
138) -> Result<(Value, Fuel), Error> {
139    let unfolded = subst(body, name, whole);
140    eval(&unfolded, env, fuel)
141}
142
143fn apply(func: Value, arg: Value, fuel: Fuel) -> Result<(Value, Fuel), Error> {
144    match func {
145        Value::Closure { param, body, env } => {
146            let new_env = env.extend(param, arg);
147            eval(&body, &new_env, fuel)
148        }
149    }
150}
151
152fn subst(expr: &Expr, target: &VarName, replacement: &Expr) -> Expr {
153    match expr {
154        Expr::Var(name) => {
155            if name == target {
156                replacement.clone()
157            } else {
158                Expr::Var(name.clone())
159            }
160        }
161        Expr::Lam { param, body } => Expr::Lam {
162            param: param.clone(),
163            body: Box::new(subst_under_binder(body, param, target, replacement)),
164        },
165        Expr::App { func, arg } => Expr::App {
166            func: Box::new(subst(func, target, replacement)),
167            arg: Box::new(subst(arg, target, replacement)),
168        },
169        Expr::Let { name, value, body } => Expr::Let {
170            name: name.clone(),
171            value: Box::new(subst(value, target, replacement)),
172            body: Box::new(subst_under_binder(body, name, target, replacement)),
173        },
174        Expr::Fix { name, body } => Expr::Fix {
175            name: name.clone(),
176            body: Box::new(subst_under_binder(body, name, target, replacement)),
177        },
178    }
179}
180
181fn subst_under_binder(body: &Expr, binder: &VarName, target: &VarName, replacement: &Expr) -> Expr {
182    if binder == target {
183        body.clone()
184    } else {
185        subst(body, target, replacement)
186    }
187}
188
189#[cfg(test)]
190mod tests {
191    use super::*;
192
193    fn empty() -> Fuel {
194        Fuel::new(10_000)
195    }
196
197    #[test]
198    fn identity_is_closure() -> Result<(), Error> {
199        let id = Expr::lam("x", Expr::var("x"));
200        let (value, _fuel) = eval(&id, &Env::empty(), empty())?;
201        matches!(value, Value::Closure { .. })
202            .then_some(())
203            .ok_or(Error::UnboundVariable {
204                name: VarName::from("not a closure"),
205            })
206    }
207
208    #[test]
209    fn unbound_variable_errors() -> Result<(), Error> {
210        let expr = Expr::var("missing");
211        let result = eval(&expr, &Env::empty(), empty());
212        match result {
213            Err(Error::UnboundVariable { .. }) => Ok(()),
214            Err(other) => Err(other),
215            Ok(_) => Err(Error::UnboundVariable {
216                name: VarName::from("expected unbound error"),
217            }),
218        }
219    }
220}