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