Skip to main content

panproto_expr/
eval.rs

1//! Call-by-value expression evaluator with step and depth limits.
2//!
3//! The evaluator is pure, deterministic, and WASM-safe. It tracks a step
4//! counter (decremented on each reduction) and a depth counter (incremented
5//! on each recursive call) to bound computation.
6
7use std::sync::Arc;
8
9use crate::builtin::apply_builtin;
10use crate::env::Env;
11use crate::error::ExprError;
12use crate::expr::{BuiltinOp, Expr, Pattern};
13use crate::literal::Literal;
14
15/// Configuration for the expression evaluator.
16#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
17pub struct EvalConfig {
18    /// Maximum number of reduction steps before aborting.
19    pub max_steps: u64,
20    /// Maximum recursion depth before aborting.
21    pub max_depth: u32,
22    /// Maximum list length for operations that produce lists.
23    pub max_list_len: usize,
24}
25
26impl Default for EvalConfig {
27    fn default() -> Self {
28        Self {
29            max_steps: 100_000,
30            max_depth: 256,
31            max_list_len: 10_000,
32        }
33    }
34}
35
36/// A source of answers for builtins this evaluator cannot compute on its own.
37///
38/// The graph traversal builtins read an instance, which the pure evaluator
39/// does not hold. A resolver plugs that context in at every point a builtin is
40/// applied — not only at the root of the expression — so a graph builtin
41/// nested under a comparison, a binding, a lambda, or a comprehension answers
42/// exactly as it would at the top.
43pub trait BuiltinResolver {
44    /// Whether this resolver answers for `op`. Operations it declines are
45    /// computed by [`apply_builtin`](crate::builtin::apply_builtin).
46    fn handles(&self, op: BuiltinOp) -> bool;
47
48    /// Answer `op` on already-evaluated arguments.
49    ///
50    /// # Errors
51    ///
52    /// Returns [`ExprError`] when the arguments are of the wrong number or
53    /// type, or when the operation fails against the context.
54    fn apply(&self, op: BuiltinOp, args: &[Literal]) -> Result<Literal, ExprError>;
55}
56
57/// The resolver in force when none is supplied: it answers for nothing, so
58/// every builtin is computed purely.
59struct NoResolver;
60
61impl BuiltinResolver for NoResolver {
62    fn handles(&self, _op: BuiltinOp) -> bool {
63        false
64    }
65
66    fn apply(&self, op: BuiltinOp, _args: &[Literal]) -> Result<Literal, ExprError> {
67        Err(ExprError::NoInstanceContext {
68            op: format!("{op:?}"),
69        })
70    }
71}
72
73/// Mutable evaluation state tracking resource consumption.
74struct EvalState<'a> {
75    steps_remaining: u64,
76    max_steps: u64,
77    max_depth: u32,
78    max_list_len: usize,
79    resolver: &'a dyn BuiltinResolver,
80}
81
82impl<'a> EvalState<'a> {
83    const fn new(config: &EvalConfig, resolver: &'a dyn BuiltinResolver) -> Self {
84        Self {
85            steps_remaining: config.max_steps,
86            max_steps: config.max_steps,
87            max_depth: config.max_depth,
88            max_list_len: config.max_list_len,
89            resolver,
90        }
91    }
92
93    const fn tick(&mut self) -> Result<(), ExprError> {
94        if self.steps_remaining == 0 {
95            return Err(ExprError::StepLimitExceeded(self.max_steps));
96        }
97        self.steps_remaining -= 1;
98        Ok(())
99    }
100}
101
102/// Evaluate an expression in the given environment.
103///
104/// # Errors
105///
106/// Returns [`ExprError`] on type mismatches, unbound variables,
107/// step/depth limit exceeded, or runtime errors.
108pub fn eval(expr: &Expr, env: &Env, config: &EvalConfig) -> Result<Literal, ExprError> {
109    eval_with_resolver(expr, env, config, &NoResolver)
110}
111
112/// Evaluate an expression, routing the builtins `resolver` claims through it.
113///
114/// The resolver is consulted wherever a builtin is applied, at any depth, so a
115/// context-dependent builtin behaves the same nested as it does at the root.
116///
117/// # Errors
118///
119/// Returns [`ExprError`] on type mismatches, unbound variables,
120/// step/depth limit exceeded, or runtime errors, including any the resolver
121/// itself reports.
122pub fn eval_with_resolver(
123    expr: &Expr,
124    env: &Env,
125    config: &EvalConfig,
126    resolver: &dyn BuiltinResolver,
127) -> Result<Literal, ExprError> {
128    let mut state = EvalState::new(config, resolver);
129    eval_inner(expr, env, 0, &mut state)
130}
131
132fn eval_inner(
133    expr: &Expr,
134    env: &Env,
135    depth: u32,
136    state: &mut EvalState<'_>,
137) -> Result<Literal, ExprError> {
138    if depth > state.max_depth {
139        return Err(ExprError::DepthExceeded(state.max_depth));
140    }
141    state.tick()?;
142
143    match expr {
144        // The builtins form a scope beneath the environment: a lexical
145        // binding of the same name shadows one, and a free variable naming a
146        // builtin denotes that builtin as a value, curried over its arity.
147        Expr::Var(name) => env.get(name).cloned().map_or_else(
148            || {
149                BuiltinOp::from_name(name)
150                    .map(builtin_as_value)
151                    .ok_or_else(|| ExprError::UnboundVariable(name.to_string()))
152            },
153            Ok,
154        ),
155
156        Expr::Lit(lit) => Ok(lit.clone()),
157
158        Expr::Lam(param, body) => {
159            // Lambdas evaluate to closures over the scope they were written
160            // in, which gives lexical scoping and first-class functions. The
161            // scope is shared, not copied.
162            Ok(Literal::Closure {
163                param: Arc::clone(param),
164                body: body.clone(),
165                env: env.clone(),
166            })
167        }
168
169        Expr::App(func, arg) => eval_app(func, arg, env, depth, state),
170
171        Expr::Record(fields) => {
172            let mut result = Vec::with_capacity(fields.len());
173            for (name, expr) in fields {
174                let val = eval_inner(expr, env, depth + 1, state)?;
175                result.push((Arc::clone(name), val));
176            }
177            Ok(Literal::Record(result))
178        }
179
180        Expr::List(items) => {
181            let mut result = Vec::with_capacity(items.len());
182            for item in items {
183                let val = eval_inner(item, env, depth + 1, state)?;
184                result.push(val);
185            }
186            if result.len() > state.max_list_len {
187                return Err(ExprError::ListLengthExceeded(result.len()));
188            }
189            Ok(Literal::List(result))
190        }
191
192        Expr::Field(expr, field) => {
193            let val = eval_inner(expr, env, depth + 1, state)?;
194            match &val {
195                Literal::Record(fields) => fields
196                    .iter()
197                    .find(|(k, _)| k == field)
198                    .map(|(_, v)| v.clone())
199                    .ok_or_else(|| ExprError::FieldNotFound(field.to_string())),
200                _ => Err(ExprError::TypeError {
201                    expected: "record".into(),
202                    got: val.type_name().into(),
203                }),
204            }
205        }
206
207        Expr::Index(expr, idx_expr) => eval_index(expr, idx_expr, env, depth, state),
208
209        Expr::Match { scrutinee, arms } => eval_match(scrutinee, arms, env, depth, state),
210
211        Expr::Let { name, value, body } => {
212            let val = eval_inner(value, env, depth + 1, state)?;
213            let new_env = env.extend(Arc::clone(name), val);
214            eval_inner(body, &new_env, depth + 1, state)
215        }
216
217        Expr::Builtin(op, args) if args.len() < op.arity() => {
218            // A builtin given fewer arguments than it takes is a function of
219            // the rest, exactly as a partially applied lambda is.
220            eval_partial_builtin(*op, args, env, depth, state)
221        }
222
223        Expr::Builtin(op, args) => {
224            // Special handling for higher-order builtins (Map, Filter, Fold,
225            // FlatMap) and for Range, which needs the list-length budget:
226            // it is the one builtin that can allocate an arbitrarily long
227            // list from a constant-size expression.
228            match op {
229                BuiltinOp::Map => eval_map(args, env, depth, state),
230                BuiltinOp::Filter => eval_filter(args, env, depth, state),
231                BuiltinOp::Fold => eval_fold(args, env, depth, state),
232                BuiltinOp::FlatMap => eval_flat_map(args, env, depth, state),
233                BuiltinOp::Range => eval_range(args, env, depth, state),
234                _ => {
235                    let evaluated: Result<Vec<_>, _> = args
236                        .iter()
237                        .map(|a| eval_inner(a, env, depth + 1, state))
238                        .collect();
239                    let evaluated = evaluated?;
240                    if state.resolver.handles(*op) {
241                        state.resolver.apply(*op, &evaluated)
242                    } else {
243                        apply_builtin(*op, &evaluated)
244                    }
245                }
246            }
247        }
248    }
249}
250
251/// The name a builtin's `n`-th curried parameter binds.
252///
253/// The double underscore keeps these out of the way of surface identifiers,
254/// which the lexer does not admit with that prefix.
255fn builtin_param(index: usize) -> Arc<str> {
256    Arc::from(format!("__builtin_arg{index}"))
257}
258
259/// The function a builtin denotes once `supplied` arguments are in hand: a
260/// closure taking the rest one at a time, in surface order.
261///
262/// Arguments sit in surface order until the call is saturated, so the
263/// permutation into [`Expr::Builtin`] order is applied once, on the complete
264/// list inside the closure's body.
265fn curried_builtin(op: BuiltinOp, mut surface: Vec<Expr>) -> Literal {
266    let arity = op.arity();
267    let first_missing = surface.len();
268    for index in first_missing..arity {
269        surface.push(Expr::Var(builtin_param(index)));
270    }
271
272    let mut body = Expr::Builtin(op, op.surface_args_to_expr_args(surface));
273    // Wrap innermost-last: `\a_k -> ... -> \a_{n-1} -> op a_0 ... a_{n-1}`.
274    for index in (first_missing + 1..arity).rev() {
275        body = Expr::Lam(builtin_param(index), Box::new(body));
276    }
277    Literal::Closure {
278        param: builtin_param(first_missing),
279        body: Box::new(body),
280        env: Env::new(),
281    }
282}
283
284/// The value a builtin's bare name denotes: the builtin curried over its whole
285/// argument list.
286fn builtin_as_value(op: BuiltinOp) -> Literal {
287    curried_builtin(op, Vec::new())
288}
289
290/// Evaluate an under-saturated builtin to the function of its missing
291/// arguments.
292///
293/// The arguments already supplied are evaluated here, at the point the partial
294/// application is written, and carried into the closure as literals; the rest
295/// are abstracted over. Arguments sit in surface order until the call is
296/// saturated, so the permutation into `Expr::Builtin` order is applied once,
297/// on the complete list inside the closure's body.
298fn eval_partial_builtin(
299    op: BuiltinOp,
300    args: &[Expr],
301    env: &Env,
302    depth: u32,
303    state: &mut EvalState<'_>,
304) -> Result<Literal, ExprError> {
305    let mut surface = Vec::with_capacity(op.arity());
306    for arg in args {
307        surface.push(Expr::Lit(eval_inner(arg, env, depth + 1, state)?));
308    }
309    Ok(curried_builtin(op, surface))
310}
311
312/// Evaluate a range: `range(start, stop)` -> `[start, ..., stop]`.
313///
314/// Both bounds are inclusive, following the surface syntax `[a..b]` this
315/// lowers from. A `stop` below `start` yields the empty list rather than
316/// an error, matching the descending-range convention of the Haskell-style
317/// syntax the parser accepts.
318///
319/// The length is checked *before* allocating. Range is the only builtin
320/// that can turn a constant-size expression into an arbitrarily long list,
321/// so computing the length first and rejecting it against `max_list_len`
322/// keeps `[0..9999999999]` from exhausting memory before the budget is
323/// consulted.
324fn eval_range(
325    args: &[Expr],
326    env: &Env,
327    depth: u32,
328    state: &mut EvalState<'_>,
329) -> Result<Literal, ExprError> {
330    if args.len() != 2 {
331        return Err(ExprError::ArityMismatch {
332            op: "Range".into(),
333            expected: 2,
334            got: args.len(),
335        });
336    }
337    let start = match eval_inner(&args[0], env, depth + 1, state)? {
338        Literal::Int(n) => n,
339        other => {
340            return Err(ExprError::TypeError {
341                expected: "int".into(),
342                got: other.type_name().into(),
343            });
344        }
345    };
346    let stop = match eval_inner(&args[1], env, depth + 1, state)? {
347        Literal::Int(n) => n,
348        other => {
349            return Err(ExprError::TypeError {
350                expected: "int".into(),
351                got: other.type_name().into(),
352            });
353        }
354    };
355
356    if stop < start {
357        return Ok(Literal::List(Vec::new()));
358    }
359    // `stop >= start` here, so the difference is non-negative; widen to
360    // i128 so that a range spanning the full i64 domain cannot overflow
361    // while computing its own length.
362    let len = (i128::from(stop) - i128::from(start)) + 1;
363    let max = i128::try_from(state.max_list_len).unwrap_or(i128::MAX);
364    if len > max {
365        return Err(ExprError::ListLengthExceeded(
366            usize::try_from(len).unwrap_or(usize::MAX),
367        ));
368    }
369    let len_usize = usize::try_from(len).unwrap_or(usize::MAX);
370    let mut result = Vec::with_capacity(len_usize);
371    for n in start..=stop {
372        result.push(Literal::Int(n));
373    }
374    Ok(Literal::List(result))
375}
376
377/// Evaluate a function application.
378///
379/// Evaluates both the function and argument, then applies. The function
380/// may be a structural `Lam` node (direct beta reduction) or may evaluate
381/// to a `Literal::Closure` (proper closure application with captured env).
382fn eval_app(
383    func: &Expr,
384    arg: &Expr,
385    env: &Env,
386    depth: u32,
387    state: &mut EvalState<'_>,
388) -> Result<Literal, ExprError> {
389    // Evaluate the function expression to a value.
390    let func_val = eval_inner(func, env, depth + 1, state)?;
391    // Evaluate the argument.
392    let arg_val = eval_inner(arg, env, depth + 1, state)?;
393    // Apply the closure to the argument.
394    apply_closure(&func_val, &arg_val, depth, state)
395}
396
397/// Apply a closure value to an argument value.
398///
399/// Reconstructs the captured environment, binds the parameter, and
400/// evaluates the body. This is the formal beta-reduction step for
401/// the call-by-value evaluation strategy.
402fn apply_closure(
403    func: &Literal,
404    arg: &Literal,
405    depth: u32,
406    state: &mut EvalState<'_>,
407) -> Result<Literal, ExprError> {
408    match func {
409        Literal::Closure { param, body, env } => {
410            // Bind the parameter in the captured scope and evaluate the body
411            // there. This is the beta-reduction step of call-by-value.
412            let closure_env = env.extend(Arc::clone(param), arg.clone());
413            eval_inner(body, &closure_env, depth + 1, state)
414        }
415        _ => Err(ExprError::NotAFunction),
416    }
417}
418
419/// Evaluate an index expression: `expr[idx]`.
420#[allow(
421    clippy::cast_possible_wrap,
422    clippy::cast_possible_truncation,
423    clippy::cast_sign_loss
424)]
425fn eval_index(
426    expr: &Expr,
427    idx_expr: &Expr,
428    env: &Env,
429    depth: u32,
430    state: &mut EvalState<'_>,
431) -> Result<Literal, ExprError> {
432    let val = eval_inner(expr, env, depth + 1, state)?;
433    let idx = eval_inner(idx_expr, env, depth + 1, state)?;
434    match (&val, &idx) {
435        (Literal::List(items), Literal::Int(i)) => {
436            let index = if *i < 0 {
437                (items.len() as i64 + i) as usize
438            } else {
439                *i as usize
440            };
441            items
442                .get(index)
443                .cloned()
444                .ok_or(ExprError::IndexOutOfBounds {
445                    index: *i,
446                    len: items.len(),
447                })
448        }
449        _ => Err(ExprError::TypeError {
450            expected: "(list, int)".into(),
451            got: format!("({}, {})", val.type_name(), idx.type_name()),
452        }),
453    }
454}
455
456/// Evaluate a match expression.
457fn eval_match(
458    scrutinee: &Expr,
459    arms: &[(Pattern, Expr)],
460    env: &Env,
461    depth: u32,
462    state: &mut EvalState<'_>,
463) -> Result<Literal, ExprError> {
464    let val = eval_inner(scrutinee, env, depth + 1, state)?;
465    for (pattern, body) in arms {
466        if let Some(bindings) = match_pattern(pattern, &val) {
467            let mut new_env = env.clone();
468            for (name, bound_val) in bindings {
469                new_env = new_env.extend(name, bound_val);
470            }
471            return eval_inner(body, &new_env, depth + 1, state);
472        }
473    }
474    Err(ExprError::NonExhaustiveMatch)
475}
476
477/// Evaluate `map(list_expr, lambda_expr)`.
478fn eval_map(
479    args: &[Expr],
480    env: &Env,
481    depth: u32,
482    state: &mut EvalState<'_>,
483) -> Result<Literal, ExprError> {
484    if args.len() != 2 {
485        return Err(ExprError::ArityMismatch {
486            op: "Map".into(),
487            expected: 2,
488            got: args.len(),
489        });
490    }
491    let list_val = eval_inner(&args[0], env, depth + 1, state)?;
492    let items = match list_val {
493        Literal::List(items) => items,
494        other => {
495            return Err(ExprError::TypeError {
496                expected: "list".into(),
497                got: other.type_name().into(),
498            });
499        }
500    };
501
502    let func = &args[1];
503    let mut result = Vec::with_capacity(items.len());
504    for item in &items {
505        let val = apply_lambda(func, item, env, depth + 1, state)?;
506        result.push(val);
507    }
508    if result.len() > state.max_list_len {
509        return Err(ExprError::ListLengthExceeded(result.len()));
510    }
511    Ok(Literal::List(result))
512}
513
514/// Evaluate `filter(list_expr, predicate_expr)`.
515fn eval_filter(
516    args: &[Expr],
517    env: &Env,
518    depth: u32,
519    state: &mut EvalState<'_>,
520) -> Result<Literal, ExprError> {
521    if args.len() != 2 {
522        return Err(ExprError::ArityMismatch {
523            op: "Filter".into(),
524            expected: 2,
525            got: args.len(),
526        });
527    }
528    let list_val = eval_inner(&args[0], env, depth + 1, state)?;
529    let items = match list_val {
530        Literal::List(items) => items,
531        other => {
532            return Err(ExprError::TypeError {
533                expected: "list".into(),
534                got: other.type_name().into(),
535            });
536        }
537    };
538
539    let pred = &args[1];
540    let mut result = Vec::new();
541    for item in &items {
542        let keep = apply_lambda(pred, item, env, depth + 1, state)?;
543        match keep {
544            Literal::Bool(true) => result.push(item.clone()),
545            Literal::Bool(false) => {}
546            other => {
547                return Err(ExprError::TypeError {
548                    expected: "bool".into(),
549                    got: other.type_name().into(),
550                });
551            }
552        }
553    }
554    Ok(Literal::List(result))
555}
556
557/// Evaluate `fold(list_expr, init_expr, accumulator_expr)`.
558fn eval_fold(
559    args: &[Expr],
560    env: &Env,
561    depth: u32,
562    state: &mut EvalState<'_>,
563) -> Result<Literal, ExprError> {
564    if args.len() != 3 {
565        return Err(ExprError::ArityMismatch {
566            op: "Fold".into(),
567            expected: 3,
568            got: args.len(),
569        });
570    }
571    let list_val = eval_inner(&args[0], env, depth + 1, state)?;
572    let items = match list_val {
573        Literal::List(items) => items,
574        other => {
575            return Err(ExprError::TypeError {
576                expected: "list".into(),
577                got: other.type_name().into(),
578            });
579        }
580    };
581
582    let mut acc = eval_inner(&args[1], env, depth + 1, state)?;
583    let func = &args[2];
584
585    for item in &items {
586        // func is a curried binary function: λacc. λitem. body
587        // Apply it to acc, then to item.
588        acc = apply_lambda_2(func, &acc, item, env, depth + 1, state)?;
589    }
590    Ok(acc)
591}
592
593/// Evaluate `flat_map(list_expr, lambda_expr)`.
594fn eval_flat_map(
595    args: &[Expr],
596    env: &Env,
597    depth: u32,
598    state: &mut EvalState<'_>,
599) -> Result<Literal, ExprError> {
600    if args.len() != 2 {
601        return Err(ExprError::ArityMismatch {
602            op: "FlatMap".into(),
603            expected: 2,
604            got: args.len(),
605        });
606    }
607    let list_val = eval_inner(&args[0], env, depth + 1, state)?;
608    let items = match list_val {
609        Literal::List(items) => items,
610        other => {
611            return Err(ExprError::TypeError {
612                expected: "list".into(),
613                got: other.type_name().into(),
614            });
615        }
616    };
617
618    let func = &args[1];
619    let mut result = Vec::new();
620    for item in &items {
621        let sub_list = apply_lambda(func, item, env, depth + 1, state)?;
622        match sub_list {
623            Literal::List(sub_items) => result.extend(sub_items),
624            other => {
625                return Err(ExprError::TypeError {
626                    expected: "list".into(),
627                    got: other.type_name().into(),
628                });
629            }
630        }
631        if result.len() > state.max_list_len {
632            return Err(ExprError::ListLengthExceeded(result.len()));
633        }
634    }
635    Ok(Literal::List(result))
636}
637
638/// Evaluate a function expression and apply it to a single argument value.
639///
640/// The function expression is evaluated to produce a closure, then the
641/// closure is applied to the argument via [`apply_closure`].
642fn apply_lambda(
643    func_expr: &Expr,
644    arg: &Literal,
645    env: &Env,
646    depth: u32,
647    state: &mut EvalState<'_>,
648) -> Result<Literal, ExprError> {
649    let func_val = eval_inner(func_expr, env, depth + 1, state)?;
650    apply_closure(&func_val, arg, depth, state)
651}
652
653/// Evaluate a curried binary function and apply it to two arguments.
654///
655/// Evaluates `func_expr` to get a closure, applies to `arg1` to get
656/// a second closure, then applies to `arg2`.
657fn apply_lambda_2(
658    func_expr: &Expr,
659    arg1: &Literal,
660    arg2: &Literal,
661    env: &Env,
662    depth: u32,
663    state: &mut EvalState<'_>,
664) -> Result<Literal, ExprError> {
665    let func_val = eval_inner(func_expr, env, depth + 1, state)?;
666    let partial = apply_closure(&func_val, arg1, depth, state)?;
667    apply_closure(&partial, arg2, depth, state)
668}
669
670/// Try to match a value against a pattern, returning bindings on success.
671fn match_pattern(pattern: &Pattern, value: &Literal) -> Option<Vec<(Arc<str>, Literal)>> {
672    let mut bindings = Vec::new();
673    if match_inner(pattern, value, &mut bindings) {
674        Some(bindings)
675    } else {
676        None
677    }
678}
679
680fn match_inner(
681    pattern: &Pattern,
682    value: &Literal,
683    bindings: &mut Vec<(Arc<str>, Literal)>,
684) -> bool {
685    match pattern {
686        Pattern::Wildcard => true,
687        Pattern::Var(name) => {
688            bindings.push((Arc::clone(name), value.clone()));
689            true
690        }
691        Pattern::Lit(lit) => lit == value,
692        Pattern::Record(field_pats) => {
693            if let Literal::Record(fields) = value {
694                for (pat_name, pat) in field_pats {
695                    let field_val = fields.iter().find(|(k, _)| k == pat_name);
696                    match field_val {
697                        Some((_, v)) => {
698                            if !match_inner(pat, v, bindings) {
699                                return false;
700                            }
701                        }
702                        None => return false,
703                    }
704                }
705                true
706            } else {
707                false
708            }
709        }
710        Pattern::List(item_pats) => {
711            if let Literal::List(items) = value {
712                if items.len() != item_pats.len() {
713                    return false;
714                }
715                for (pat, val) in item_pats.iter().zip(items.iter()) {
716                    if !match_inner(pat, val, bindings) {
717                        return false;
718                    }
719                }
720                true
721            } else {
722                false
723            }
724        }
725        Pattern::Constructor(tag, arg_pats) => {
726            // Constructors match against records with a "$tag" field
727            if let Literal::Record(fields) = value {
728                let tag_field = fields.iter().find(|(k, _)| &**k == "$tag");
729                if let Some((_, Literal::Str(t))) = tag_field {
730                    if t.as_str() != &**tag {
731                        return false;
732                    }
733                    // Match remaining args against "$0", "$1", etc. fields
734                    for (i, pat) in arg_pats.iter().enumerate() {
735                        let key = format!("${i}");
736                        let field_val = fields.iter().find(|(k, _)| k.as_ref() == key.as_str());
737                        match field_val {
738                            Some((_, v)) => {
739                                if !match_inner(pat, v, bindings) {
740                                    return false;
741                                }
742                            }
743                            None => return false,
744                        }
745                    }
746                    true
747                } else {
748                    false
749                }
750            } else {
751                false
752            }
753        }
754    }
755}
756
757#[cfg(test)]
758#[allow(clippy::unwrap_used)]
759mod tests {
760    use super::*;
761
762    fn default_config() -> EvalConfig {
763        EvalConfig::default()
764    }
765
766    #[test]
767    fn eval_literal() {
768        let result = eval(&Expr::Lit(Literal::Int(42)), &Env::new(), &default_config());
769        assert_eq!(result.unwrap(), Literal::Int(42));
770    }
771
772    #[test]
773    fn eval_variable() {
774        let env = Env::new().extend(Arc::from("x"), Literal::Int(10));
775        let result = eval(&Expr::var("x"), &env, &default_config());
776        assert_eq!(result.unwrap(), Literal::Int(10));
777    }
778
779    #[test]
780    fn eval_unbound_variable() {
781        let result = eval(&Expr::var("x"), &Env::new(), &default_config());
782        assert!(matches!(result, Err(ExprError::UnboundVariable(_))));
783    }
784
785    #[test]
786    fn eval_lambda_application() {
787        // (λx. add(x, 1))(41) = 42
788        let expr = Expr::App(
789            Box::new(Expr::lam(
790                "x",
791                Expr::builtin(
792                    BuiltinOp::Add,
793                    vec![Expr::var("x"), Expr::Lit(Literal::Int(1))],
794                ),
795            )),
796            Box::new(Expr::Lit(Literal::Int(41))),
797        );
798        let result = eval(&expr, &Env::new(), &default_config());
799        assert_eq!(result.unwrap(), Literal::Int(42));
800    }
801
802    #[test]
803    fn eval_let_binding() {
804        // let x = 10 in add(x, 5)
805        let expr = Expr::let_in(
806            "x",
807            Expr::Lit(Literal::Int(10)),
808            Expr::builtin(
809                BuiltinOp::Add,
810                vec![Expr::var("x"), Expr::Lit(Literal::Int(5))],
811            ),
812        );
813        let result = eval(&expr, &Env::new(), &default_config());
814        assert_eq!(result.unwrap(), Literal::Int(15));
815    }
816
817    #[test]
818    fn eval_record_and_field() {
819        let expr = Expr::field(
820            Expr::Record(vec![
821                (Arc::from("name"), Expr::Lit(Literal::Str("alice".into()))),
822                (Arc::from("age"), Expr::Lit(Literal::Int(30))),
823            ]),
824            "age",
825        );
826        let result = eval(&expr, &Env::new(), &default_config());
827        assert_eq!(result.unwrap(), Literal::Int(30));
828    }
829
830    #[test]
831    fn eval_list_index() {
832        let expr = Expr::Index(
833            Box::new(Expr::List(vec![
834                Expr::Lit(Literal::Int(10)),
835                Expr::Lit(Literal::Int(20)),
836                Expr::Lit(Literal::Int(30)),
837            ])),
838            Box::new(Expr::Lit(Literal::Int(1))),
839        );
840        let result = eval(&expr, &Env::new(), &default_config());
841        assert_eq!(result.unwrap(), Literal::Int(20));
842    }
843
844    #[test]
845    fn eval_pattern_match() {
846        // match 42 { 0 => "zero", x => concat("num:", int_to_str(x)) }
847        let expr = Expr::Match {
848            scrutinee: Box::new(Expr::Lit(Literal::Int(42))),
849            arms: vec![
850                (
851                    Pattern::Lit(Literal::Int(0)),
852                    Expr::Lit(Literal::Str("zero".into())),
853                ),
854                (
855                    Pattern::Var(Arc::from("x")),
856                    Expr::builtin(
857                        BuiltinOp::Concat,
858                        vec![
859                            Expr::Lit(Literal::Str("num:".into())),
860                            Expr::builtin(BuiltinOp::IntToStr, vec![Expr::var("x")]),
861                        ],
862                    ),
863                ),
864            ],
865        };
866        let result = eval(&expr, &Env::new(), &default_config());
867        assert_eq!(result.unwrap(), Literal::Str("num:42".into()));
868    }
869
870    #[test]
871    fn eval_map() {
872        // map([1, 2, 3], λx. mul(x, 2))
873        let expr = Expr::builtin(
874            BuiltinOp::Map,
875            vec![
876                Expr::List(vec![
877                    Expr::Lit(Literal::Int(1)),
878                    Expr::Lit(Literal::Int(2)),
879                    Expr::Lit(Literal::Int(3)),
880                ]),
881                Expr::lam(
882                    "x",
883                    Expr::builtin(
884                        BuiltinOp::Mul,
885                        vec![Expr::var("x"), Expr::Lit(Literal::Int(2))],
886                    ),
887                ),
888            ],
889        );
890        let result = eval(&expr, &Env::new(), &default_config());
891        assert_eq!(
892            result.unwrap(),
893            Literal::List(vec![Literal::Int(2), Literal::Int(4), Literal::Int(6)])
894        );
895    }
896
897    #[test]
898    fn eval_filter() {
899        // filter([1, 2, 3, 4], λx. gt(x, 2))
900        let expr = Expr::builtin(
901            BuiltinOp::Filter,
902            vec![
903                Expr::List(vec![
904                    Expr::Lit(Literal::Int(1)),
905                    Expr::Lit(Literal::Int(2)),
906                    Expr::Lit(Literal::Int(3)),
907                    Expr::Lit(Literal::Int(4)),
908                ]),
909                Expr::lam(
910                    "x",
911                    Expr::builtin(
912                        BuiltinOp::Gt,
913                        vec![Expr::var("x"), Expr::Lit(Literal::Int(2))],
914                    ),
915                ),
916            ],
917        );
918        let result = eval(&expr, &Env::new(), &default_config());
919        assert_eq!(
920            result.unwrap(),
921            Literal::List(vec![Literal::Int(3), Literal::Int(4)])
922        );
923    }
924
925    #[test]
926    fn eval_fold() {
927        // fold([1, 2, 3], 0, λacc. λx. add(acc, x)) = 6
928        let expr = Expr::builtin(
929            BuiltinOp::Fold,
930            vec![
931                Expr::List(vec![
932                    Expr::Lit(Literal::Int(1)),
933                    Expr::Lit(Literal::Int(2)),
934                    Expr::Lit(Literal::Int(3)),
935                ]),
936                Expr::Lit(Literal::Int(0)),
937                Expr::lam(
938                    "acc",
939                    Expr::lam(
940                        "x",
941                        Expr::builtin(BuiltinOp::Add, vec![Expr::var("acc"), Expr::var("x")]),
942                    ),
943                ),
944            ],
945        );
946        let result = eval(&expr, &Env::new(), &default_config());
947        assert_eq!(result.unwrap(), Literal::Int(6));
948    }
949
950    #[test]
951    fn eval_step_limit() {
952        // A computation that exceeds the step limit
953        let config = EvalConfig {
954            max_steps: 5,
955            ..EvalConfig::default()
956        };
957        // map([1,2,3,4,5,6,7,8,9,10], λx. add(x, 1)); should exceed 5 steps
958        let items: Vec<_> = (1..=10).map(|i| Expr::Lit(Literal::Int(i))).collect();
959        let expr = Expr::builtin(
960            BuiltinOp::Map,
961            vec![
962                Expr::List(items),
963                Expr::lam(
964                    "x",
965                    Expr::builtin(
966                        BuiltinOp::Add,
967                        vec![Expr::var("x"), Expr::Lit(Literal::Int(1))],
968                    ),
969                ),
970            ],
971        );
972        let result = eval(&expr, &Env::new(), &config);
973        assert!(matches!(result, Err(ExprError::StepLimitExceeded(_))));
974    }
975
976    #[test]
977    fn eval_merge_example() {
978        // The merge example from the design doc:
979        // λfirst. λlast. concat(first, concat(" ", last))
980        let merge_fn = Expr::lam(
981            "first",
982            Expr::lam(
983                "last",
984                Expr::builtin(
985                    BuiltinOp::Concat,
986                    vec![
987                        Expr::var("first"),
988                        Expr::builtin(
989                            BuiltinOp::Concat,
990                            vec![Expr::Lit(Literal::Str(" ".into())), Expr::var("last")],
991                        ),
992                    ],
993                ),
994            ),
995        );
996        // Apply: merge_fn("Alice")("Smith")
997        let expr = Expr::App(
998            Box::new(Expr::App(
999                Box::new(merge_fn),
1000                Box::new(Expr::Lit(Literal::Str("Alice".into()))),
1001            )),
1002            Box::new(Expr::Lit(Literal::Str("Smith".into()))),
1003        );
1004        let result = eval(&expr, &Env::new(), &default_config());
1005        assert_eq!(result.unwrap(), Literal::Str("Alice Smith".into()));
1006    }
1007
1008    #[test]
1009    fn eval_split_example() {
1010        // The split example from the design doc:
1011        // λfull. let parts = split(full, " ") in
1012        //   { firstName: head(parts), lastName: join(tail(parts), " ") }
1013        let split_fn = Expr::lam(
1014            "full",
1015            Expr::let_in(
1016                "parts",
1017                Expr::builtin(
1018                    BuiltinOp::Split,
1019                    vec![Expr::var("full"), Expr::Lit(Literal::Str(" ".into()))],
1020                ),
1021                Expr::Record(vec![
1022                    (
1023                        Arc::from("firstName"),
1024                        Expr::builtin(BuiltinOp::Head, vec![Expr::var("parts")]),
1025                    ),
1026                    (
1027                        Arc::from("lastName"),
1028                        Expr::builtin(
1029                            BuiltinOp::Join,
1030                            vec![
1031                                Expr::builtin(BuiltinOp::Tail, vec![Expr::var("parts")]),
1032                                Expr::Lit(Literal::Str(" ".into())),
1033                            ],
1034                        ),
1035                    ),
1036                ]),
1037            ),
1038        );
1039        let expr = Expr::App(
1040            Box::new(split_fn),
1041            Box::new(Expr::Lit(Literal::Str("Alice B Smith".into()))),
1042        );
1043        let result = eval(&expr, &Env::new(), &default_config());
1044        let expected = Literal::Record(vec![
1045            (Arc::from("firstName"), Literal::Str("Alice".into())),
1046            (Arc::from("lastName"), Literal::Str("B Smith".into())),
1047        ]);
1048        assert_eq!(result.unwrap(), expected);
1049    }
1050
1051    #[test]
1052    fn eval_coercion_example() {
1053        // λv. str_to_int(v)
1054        let coerce = Expr::lam(
1055            "v",
1056            Expr::builtin(BuiltinOp::StrToInt, vec![Expr::var("v")]),
1057        );
1058        let expr = Expr::App(
1059            Box::new(coerce),
1060            Box::new(Expr::Lit(Literal::Str("42".into()))),
1061        );
1062        let result = eval(&expr, &Env::new(), &default_config());
1063        assert_eq!(result.unwrap(), Literal::Int(42));
1064    }
1065}