Skip to main content

pepl_eval/
evaluator.rs

1//! Core expression and statement evaluator.
2
3use crate::env::Environment;
4use crate::error::{EvalError, EvalResult};
5use pepl_stdlib::modules::{convert, core, json, list, math, record, string, time, timer};
6use pepl_stdlib::{StdlibModule, Value, ResultValue};
7use pepl_types::ast::*;
8use std::collections::BTreeMap;
9
10/// The core evaluator — walks AST nodes and produces Values.
11pub struct Evaluator {
12    /// Variable environment (scoped).
13    pub env: Environment,
14    /// Gas counter — limits total steps to prevent infinite loops.
15    pub gas: u64,
16    /// Gas limit.
17    pub gas_limit: u64,
18    /// Captured log output from `core.log`.
19    pub log_output: Vec<String>,
20    /// Action names registered in the space (for resolving action references).
21    pub action_names: Vec<String>,
22    /// Mock capability responses (module, function) → response Value.
23    /// Used by the test runner for `with_responses` blocks.
24    pub mock_responses: Vec<(String, String, Value)>,
25}
26
27impl Evaluator {
28    /// Create a new evaluator with the given gas limit.
29    pub fn new(gas_limit: u64) -> Self {
30        Self {
31            env: Environment::new(),
32            gas: 0,
33            gas_limit,
34            log_output: Vec::new(),
35            action_names: Vec::new(),
36            mock_responses: Vec::new(),
37        }
38    }
39
40    /// Consume one unit of gas. Returns error if exhausted.
41    fn tick(&mut self) -> EvalResult<()> {
42        self.gas += 1;
43        if self.gas > self.gas_limit {
44            Err(EvalError::GasExhausted)
45        } else {
46            Ok(())
47        }
48    }
49
50    // ══════════════════════════════════════════════════════════════════════
51    // Expression evaluation
52    // ══════════════════════════════════════════════════════════════════════
53
54    /// Evaluate an expression to a Value.
55    pub fn eval_expr(&mut self, expr: &Expr) -> EvalResult<Value> {
56        self.tick()?;
57        match &expr.kind {
58            ExprKind::NumberLit(n) => Ok(Value::Number(*n)),
59            ExprKind::StringLit(s) => Ok(Value::String(s.clone())),
60            ExprKind::BoolLit(b) => Ok(Value::Bool(*b)),
61            ExprKind::NilLit => Ok(Value::Nil),
62
63            ExprKind::StringInterpolation(parts) => self.eval_string_interpolation(parts),
64            ExprKind::ListLit(elems) => self.eval_list_literal(elems),
65            ExprKind::RecordLit(entries) => self.eval_record_literal(entries),
66
67            ExprKind::Identifier(name) => self.eval_identifier(name),
68
69            ExprKind::Call { name, args } => self.eval_call(&name.name, args),
70            ExprKind::QualifiedCall {
71                module,
72                function,
73                args,
74            } => self.eval_qualified_call(&module.name, &function.name, args),
75            ExprKind::FieldAccess { object, field } => self.eval_field_access(object, &field.name),
76            ExprKind::MethodCall {
77                object,
78                method,
79                args,
80            } => self.eval_method_call(object, &method.name, args),
81
82            ExprKind::Binary { left, op, right } => self.eval_binary(left, *op, right),
83            ExprKind::Unary { op, operand } => self.eval_unary(*op, operand),
84            ExprKind::ResultUnwrap(inner) => self.eval_result_unwrap(inner),
85            ExprKind::NilCoalesce { left, right } => self.eval_nil_coalesce(left, right),
86
87            ExprKind::If(if_expr) => self.eval_if_expr(if_expr),
88            ExprKind::For(for_expr) => self.eval_for_expr(for_expr),
89            ExprKind::Match(match_expr) => self.eval_match_expr(match_expr),
90            ExprKind::Lambda(lambda) => self.eval_lambda(lambda),
91            ExprKind::Paren(inner) => self.eval_expr(inner),
92        }
93    }
94
95    // ── Literals ──────────────────────────────────────────────────────────
96
97    fn eval_string_interpolation(&mut self, parts: &[StringPart]) -> EvalResult<Value> {
98        let mut result = String::new();
99        for part in parts {
100            match part {
101                StringPart::Literal(s) => result.push_str(s),
102                StringPart::Expr(expr) => {
103                    let val = self.eval_expr(expr)?;
104                    result.push_str(&self.value_to_display_string(&val));
105                }
106            }
107        }
108        Ok(Value::String(result))
109    }
110
111    fn eval_list_literal(&mut self, elems: &[Expr]) -> EvalResult<Value> {
112        let mut values = Vec::with_capacity(elems.len());
113        for elem in elems {
114            values.push(self.eval_expr(elem)?);
115        }
116        Ok(Value::List(values))
117    }
118
119    fn eval_record_literal(&mut self, entries: &[RecordEntry]) -> EvalResult<Value> {
120        let mut fields = BTreeMap::new();
121        for entry in entries {
122            match entry {
123                RecordEntry::Field { name, value } => {
124                    let val = self.eval_expr(value)?;
125                    fields.insert(name.name.clone(), val);
126                }
127                RecordEntry::Spread(expr) => {
128                    let val = self.eval_expr(expr)?;
129                    if let Value::Record { fields: rf, .. } = val {
130                        for (k, v) in rf {
131                            fields.insert(k, v);
132                        }
133                    } else {
134                        return Err(EvalError::TypeMismatch(
135                            format!("spread requires record, got {}", val.type_name()),
136                        ));
137                    }
138                }
139            }
140        }
141        Ok(Value::Record {
142            type_name: None,
143            fields,
144        })
145    }
146
147    // ── Identifiers & Calls ──────────────────────────────────────────────
148
149    fn eval_identifier(&self, name: &str) -> EvalResult<Value> {
150        self.env
151            .get(name)
152            .cloned()
153            .ok_or_else(|| EvalError::UndefinedVariable(name.to_string()))
154    }
155
156    /// Evaluate an unqualified call: `func(args)`.
157    /// In PEPL, unqualified calls in action bodies are action dispatches.
158    /// In view/expression context, they resolve to identifiers that are Function values.
159    fn eval_call(&mut self, name: &str, args: &[Expr]) -> EvalResult<Value> {
160        // Check if it's a Function value in scope
161        if let Some(Value::Function(f)) = self.env.get(name).cloned() {
162            let mut arg_vals = Vec::with_capacity(args.len());
163            for arg in args {
164                arg_vals.push(self.eval_expr(arg)?);
165            }
166            return f.0(arg_vals).map_err(|e| EvalError::StdlibError(e.to_string()));
167        }
168        // Otherwise, unknown function
169        Err(EvalError::UnknownFunction(format!(
170            "unknown function '{name}'"
171        )))
172    }
173
174    /// Evaluate a qualified call: `module.function(args)`.
175    pub fn eval_qualified_call(
176        &mut self,
177        module: &str,
178        function: &str,
179        args: &[Expr],
180    ) -> EvalResult<Value> {
181        let mut arg_vals = Vec::with_capacity(args.len());
182        for arg in args {
183            arg_vals.push(self.eval_expr(arg)?);
184        }
185        self.call_stdlib(module, function, arg_vals)
186    }
187
188    fn eval_field_access(&mut self, object: &Expr, field: &str) -> EvalResult<Value> {
189        let obj = self.eval_expr(object)?;
190        match &obj {
191            Value::Record { fields, .. } => fields
192                .get(field)
193                .cloned()
194                .ok_or_else(|| {
195                    EvalError::Runtime(format!("record has no field '{field}'"))
196                }),
197            Value::Nil => Err(EvalError::NilAccess(format!(
198                "cannot access field '{field}' on nil"
199            ))),
200            _ => Err(EvalError::TypeMismatch(format!(
201                "cannot access field '{field}' on {}",
202                obj.type_name()
203            ))),
204        }
205    }
206
207    fn eval_method_call(
208        &mut self,
209        object: &Expr,
210        method: &str,
211        args: &[Expr],
212    ) -> EvalResult<Value> {
213        let obj = self.eval_expr(object)?;
214        let mut all_args = vec![obj];
215        for arg in args {
216            all_args.push(self.eval_expr(arg)?);
217        }
218        // Method calls on lists → list.method, strings → string.method
219        let module = match &all_args[0] {
220            Value::List(_) => "list",
221            Value::String(_) => "string",
222            _ => {
223                return Err(EvalError::TypeMismatch(format!(
224                    "cannot call method '{method}' on {}",
225                    all_args[0].type_name()
226                )));
227            }
228        };
229        self.call_stdlib(module, method, all_args)
230    }
231
232    // ── Operators ────────────────────────────────────────────────────────
233
234    fn eval_binary(&mut self, left: &Expr, op: BinOp, right: &Expr) -> EvalResult<Value> {
235        // Short-circuit for logical operators
236        if op == BinOp::And {
237            let lv = self.eval_expr(left)?;
238            return if !lv.is_truthy() {
239                Ok(Value::Bool(false))
240            } else {
241                let rv = self.eval_expr(right)?;
242                Ok(Value::Bool(rv.is_truthy()))
243            };
244        }
245        if op == BinOp::Or {
246            let lv = self.eval_expr(left)?;
247            return if lv.is_truthy() {
248                Ok(Value::Bool(true))
249            } else {
250                let rv = self.eval_expr(right)?;
251                Ok(Value::Bool(rv.is_truthy()))
252            };
253        }
254
255        let lv = self.eval_expr(left)?;
256        let rv = self.eval_expr(right)?;
257
258        match op {
259            BinOp::Add => self.eval_add(&lv, &rv),
260            BinOp::Sub => self.eval_arith(&lv, &rv, |a, b| a - b, "-"),
261            BinOp::Mul => self.eval_arith(&lv, &rv, |a, b| a * b, "*"),
262            BinOp::Div => {
263                if let (Value::Number(a), Value::Number(b)) = (&lv, &rv) {
264                    if *b == 0.0 {
265                        return Err(EvalError::ArithmeticTrap("division by zero".into()));
266                    }
267                    let result = a / b;
268                    if result.is_nan() || result.is_infinite() {
269                        return Err(EvalError::ArithmeticTrap("division produced NaN/Infinity".into()));
270                    }
271                    Ok(Value::Number(result))
272                } else {
273                    Err(EvalError::TypeMismatch(format!(
274                        "cannot divide {} by {}",
275                        lv.type_name(),
276                        rv.type_name()
277                    )))
278                }
279            }
280            BinOp::Mod => {
281                if let (Value::Number(a), Value::Number(b)) = (&lv, &rv) {
282                    if *b == 0.0 {
283                        return Err(EvalError::ArithmeticTrap("modulo by zero".into()));
284                    }
285                    Ok(Value::Number(a % b))
286                } else {
287                    Err(EvalError::TypeMismatch(format!(
288                        "cannot modulo {} by {}",
289                        lv.type_name(),
290                        rv.type_name()
291                    )))
292                }
293            }
294            BinOp::Eq => Ok(Value::Bool(self.structural_eq(&lv, &rv))),
295            BinOp::NotEq => Ok(Value::Bool(!self.structural_eq(&lv, &rv))),
296            BinOp::Less => self.eval_comparison(&lv, &rv, |a, b| a < b),
297            BinOp::Greater => self.eval_comparison(&lv, &rv, |a, b| a > b),
298            BinOp::LessEq => self.eval_comparison(&lv, &rv, |a, b| a <= b),
299            BinOp::GreaterEq => self.eval_comparison(&lv, &rv, |a, b| a >= b),
300            BinOp::And | BinOp::Or => unreachable!("handled above"),
301        }
302    }
303
304    fn eval_add(&self, lv: &Value, rv: &Value) -> EvalResult<Value> {
305        match (lv, rv) {
306            (Value::Number(a), Value::Number(b)) => {
307                let result = a + b;
308                if result.is_nan() || result.is_infinite() {
309                    Err(EvalError::ArithmeticTrap("addition produced NaN/Infinity".into()))
310                } else {
311                    Ok(Value::Number(result))
312                }
313            }
314            (Value::String(a), Value::String(b)) => {
315                Ok(Value::String(format!("{a}{b}")))
316            }
317            _ => Err(EvalError::TypeMismatch(format!(
318                "cannot add {} and {}",
319                lv.type_name(),
320                rv.type_name()
321            ))),
322        }
323    }
324
325    fn eval_arith(
326        &self,
327        lv: &Value,
328        rv: &Value,
329        op: fn(f64, f64) -> f64,
330        symbol: &str,
331    ) -> EvalResult<Value> {
332        if let (Value::Number(a), Value::Number(b)) = (lv, rv) {
333            let result = op(*a, *b);
334            if result.is_nan() || result.is_infinite() {
335                Err(EvalError::ArithmeticTrap(format!("{symbol} produced NaN/Infinity")))
336            } else {
337                Ok(Value::Number(result))
338            }
339        } else {
340            Err(EvalError::TypeMismatch(format!(
341                "cannot apply '{symbol}' to {} and {}",
342                lv.type_name(),
343                rv.type_name()
344            )))
345        }
346    }
347
348    fn eval_comparison(
349        &self,
350        lv: &Value,
351        rv: &Value,
352        op: fn(f64, f64) -> bool,
353    ) -> EvalResult<Value> {
354        match (lv, rv) {
355            (Value::Number(a), Value::Number(b)) => Ok(Value::Bool(op(*a, *b))),
356            (Value::String(a), Value::String(b)) => Ok(Value::Bool(op(
357                a.len() as f64,
358                b.len() as f64,
359            ))),
360            _ => Err(EvalError::TypeMismatch(format!(
361                "cannot compare {} and {}",
362                lv.type_name(),
363                rv.type_name()
364            ))),
365        }
366    }
367
368    fn eval_unary(&mut self, op: UnaryOp, operand: &Expr) -> EvalResult<Value> {
369        let val = self.eval_expr(operand)?;
370        match op {
371            UnaryOp::Neg => {
372                if let Value::Number(n) = val {
373                    Ok(Value::Number(-n))
374                } else {
375                    Err(EvalError::TypeMismatch(format!(
376                        "cannot negate {}",
377                        val.type_name()
378                    )))
379                }
380            }
381            UnaryOp::Not => Ok(Value::Bool(!val.is_truthy())),
382        }
383    }
384
385    fn eval_result_unwrap(&mut self, inner: &Expr) -> EvalResult<Value> {
386        let val = self.eval_expr(inner)?;
387        match val {
388            Value::Result(r) => match *r {
389                ResultValue::Ok(v) => Ok(v),
390                ResultValue::Err(e) => Err(EvalError::UnwrapError(format!(
391                    "unwrap on Err: {}",
392                    self.value_to_display_string(&e)
393                ))),
394            },
395            _ => Err(EvalError::TypeMismatch(format!(
396                "'?' requires Result, got {}",
397                val.type_name()
398            ))),
399        }
400    }
401
402    fn eval_nil_coalesce(&mut self, left: &Expr, right: &Expr) -> EvalResult<Value> {
403        let lv = self.eval_expr(left)?;
404        if lv == Value::Nil {
405            self.eval_expr(right)
406        } else {
407            Ok(lv)
408        }
409    }
410
411    // ── Control Flow ─────────────────────────────────────────────────────
412
413    pub fn eval_if_expr(&mut self, if_expr: &IfExpr) -> EvalResult<Value> {
414        let cond = self.eval_expr(&if_expr.condition)?;
415        if cond.is_truthy() {
416            self.eval_block(&if_expr.then_block)
417        } else if let Some(else_branch) = &if_expr.else_branch {
418            match else_branch {
419                ElseBranch::ElseIf(elif) => self.eval_if_expr(elif),
420                ElseBranch::Block(block) => self.eval_block(block),
421            }
422        } else {
423            Ok(Value::Nil)
424        }
425    }
426
427    fn eval_for_expr(&mut self, for_expr: &ForExpr) -> EvalResult<Value> {
428        let iterable = self.eval_expr(&for_expr.iterable)?;
429        let items = match iterable {
430            Value::List(items) => items,
431            _ => {
432                return Err(EvalError::TypeMismatch(format!(
433                    "for loop requires list, got {}",
434                    iterable.type_name()
435                )));
436            }
437        };
438
439        self.env.push_scope();
440        let mut last = Value::Nil;
441        for (i, item) in items.iter().enumerate() {
442            self.env.define(&for_expr.item.name, item.clone());
443            if let Some(idx) = &for_expr.index {
444                self.env.define(&idx.name, Value::Number(i as f64));
445            }
446            last = self.eval_block(&for_expr.body)?;
447        }
448        self.env.pop_scope();
449        Ok(last)
450    }
451
452    fn eval_match_expr(&mut self, match_expr: &MatchExpr) -> EvalResult<Value> {
453        let subject = self.eval_expr(&match_expr.subject)?;
454
455        for arm in &match_expr.arms {
456            if let Some(bindings) = self.match_pattern(&arm.pattern, &subject) {
457                self.env.push_scope();
458                for (name, val) in bindings {
459                    self.env.define(&name, val);
460                }
461                let result = self.eval_match_arm_body(&arm.body);
462                self.env.pop_scope();
463                return result;
464            }
465        }
466        // No match — should not happen with exhaustive match, but return Nil
467        Ok(Value::Nil)
468    }
469
470    /// Try to match a pattern against a value.
471    /// Returns Some(bindings) if match, None otherwise.
472    fn match_pattern(&self, pattern: &Pattern, value: &Value) -> Option<Vec<(String, Value)>> {
473        match pattern {
474            Pattern::Wildcard(_) => Some(vec![]),
475            Pattern::Variant { name, bindings } => {
476                // Match against Result variants
477                if let Value::Result(r) = value {
478                    match (name.name.as_str(), r.as_ref()) {
479                        ("Ok", ResultValue::Ok(v)) => {
480                            let mut b = Vec::new();
481                            if let Some(binding) = bindings.first() {
482                                b.push((binding.name.clone(), v.clone()));
483                            }
484                            Some(b)
485                        }
486                        ("Err", ResultValue::Err(v)) => {
487                            let mut b = Vec::new();
488                            if let Some(binding) = bindings.first() {
489                                b.push((binding.name.clone(), v.clone()));
490                            }
491                            Some(b)
492                        }
493                        _ => None,
494                    }
495                }
496                // Match against SumVariant
497                else if let Value::SumVariant {
498                    variant, fields, ..
499                } = value
500                {
501                    if variant == &name.name {
502                        let mut b = Vec::new();
503                        for (binding, field) in bindings.iter().zip(fields.iter()) {
504                            b.push((binding.name.clone(), field.clone()));
505                        }
506                        Some(b)
507                    } else {
508                        None
509                    }
510                }
511                // Match against unit variant names (e.g., `Active`)
512                else if bindings.is_empty() {
513                    // Check if value is a string that equals the variant name
514                    if let Value::String(s) = value {
515                        if s == &name.name {
516                            return Some(vec![]);
517                        }
518                    }
519                    None
520                } else {
521                    None
522                }
523            }
524        }
525    }
526
527    fn eval_match_arm_body(&mut self, body: &MatchArmBody) -> EvalResult<Value> {
528        match body {
529            MatchArmBody::Expr(expr) => self.eval_expr(expr),
530            MatchArmBody::Block(block) => self.eval_block(block),
531        }
532    }
533
534    pub fn eval_lambda(&mut self, lambda: &LambdaExpr) -> EvalResult<Value> {
535        // Capture current environment snapshot for closure
536        let captured_env = self.env.clone();
537        let params: Vec<String> = lambda.params.iter().map(|p| p.name.name.clone()).collect();
538        let body = lambda.body.clone();
539
540        let closure = pepl_stdlib::StdlibFn(std::sync::Arc::new(move |args: Vec<Value>| {
541            // Create a mini evaluator with captured env
542            let mut eval = Evaluator::new(100_000);
543            eval.env = captured_env.clone();
544            eval.env.push_scope();
545            for (param, arg) in params.iter().zip(args.into_iter()) {
546                eval.env.define(param, arg);
547            }
548            let result = eval
549                .eval_block(&body)
550                .map_err(|e| pepl_stdlib::StdlibError::RuntimeError(e.to_string()))?;
551            eval.env.pop_scope();
552            Ok(result)
553        }));
554
555        Ok(Value::Function(closure))
556    }
557
558    // ══════════════════════════════════════════════════════════════════════
559    // Block & Statement execution
560    // ══════════════════════════════════════════════════════════════════════
561
562    /// Execute a block of statements. Returns the value of the last expression, or Nil.
563    pub fn eval_block(&mut self, block: &Block) -> EvalResult<Value> {
564        let mut last = Value::Nil;
565        for stmt in &block.stmts {
566            last = self.eval_stmt(stmt)?;
567        }
568        Ok(last)
569    }
570
571    /// Execute a single statement.
572    pub fn eval_stmt(&mut self, stmt: &Stmt) -> EvalResult<Value> {
573        self.tick()?;
574        match stmt {
575            Stmt::Set(set) => self.eval_set(set),
576            Stmt::Let(binding) => self.eval_let(binding),
577            Stmt::If(if_expr) => self.eval_if_expr(if_expr),
578            Stmt::For(for_expr) => self.eval_for_expr(for_expr),
579            Stmt::Match(match_expr) => self.eval_match_expr(match_expr),
580            Stmt::Return(ret) => {
581                let _ = ret;
582                // Return with Nil value — prior set statements are applied
583                Err(EvalError::Return(Value::Nil))
584            }
585            Stmt::Assert(assert) => self.eval_assert(assert),
586            Stmt::Expr(expr_stmt) => self.eval_expr(&expr_stmt.expr),
587        }
588    }
589
590    fn eval_set(&mut self, set: &SetStmt) -> EvalResult<Value> {
591        let value = self.eval_expr(&set.value)?;
592
593        if set.target.len() == 1 {
594            // Simple: `set x = value`
595            let name = &set.target[0].name;
596            if !self.env.set(name, value) {
597                return Err(EvalError::UndefinedVariable(name.clone()));
598            }
599        } else {
600            // Nested: `set a.b.c = value` → immutable record update
601            self.eval_nested_set(&set.target, value)?;
602        }
603        Ok(Value::Nil)
604    }
605
606    /// Handle `set a.b.c = value` by immutable record reconstruction.
607    fn eval_nested_set(&mut self, target: &[Ident], value: Value) -> EvalResult<()> {
608        let root_name = &target[0].name;
609        let root = self
610            .env
611            .get(root_name)
612            .cloned()
613            .ok_or_else(|| EvalError::UndefinedVariable(root_name.clone()))?;
614
615        let new_root = self.set_nested_field(&root, &target[1..], value)?;
616        self.env.set(root_name, new_root);
617        Ok(())
618    }
619
620    fn set_nested_field(
621        &self,
622        current: &Value,
623        path: &[Ident],
624        value: Value,
625    ) -> EvalResult<Value> {
626        if path.is_empty() {
627            return Ok(value);
628        }
629
630        let field_name = &path[0].name;
631        match current {
632            Value::Record {
633                type_name, fields, ..
634            } => {
635                let mut new_fields = fields.clone();
636                if path.len() == 1 {
637                    new_fields.insert(field_name.clone(), value);
638                } else {
639                    let inner = fields
640                        .get(field_name)
641                        .ok_or_else(|| {
642                            EvalError::Runtime(format!("record has no field '{field_name}'"))
643                        })?;
644                    let new_inner = self.set_nested_field(inner, &path[1..], value)?;
645                    new_fields.insert(field_name.clone(), new_inner);
646                }
647                Ok(Value::Record {
648                    type_name: type_name.clone(),
649                    fields: new_fields,
650                })
651            }
652            _ => Err(EvalError::TypeMismatch(format!(
653                "cannot set field '{}' on {}",
654                field_name,
655                current.type_name()
656            ))),
657        }
658    }
659
660    fn eval_let(&mut self, binding: &LetBinding) -> EvalResult<Value> {
661        let value = self.eval_expr(&binding.value)?;
662        if let Some(name) = &binding.name {
663            self.env.define(&name.name, value);
664        }
665        // Discard binding (let _ = expr)
666        Ok(Value::Nil)
667    }
668
669    fn eval_assert(&mut self, assert: &AssertStmt) -> EvalResult<Value> {
670        let val = self.eval_expr(&assert.condition)?;
671        if !val.is_truthy() {
672            let msg = assert
673                .message
674                .clone()
675                .unwrap_or_else(|| "assertion failed".into());
676            return Err(EvalError::AssertionFailed(msg));
677        }
678        Ok(Value::Nil)
679    }
680
681    // ══════════════════════════════════════════════════════════════════════
682    // Stdlib dispatch
683    // ══════════════════════════════════════════════════════════════════════
684
685    /// Call a stdlib function by module and function name.
686    pub fn call_stdlib(
687        &mut self,
688        module: &str,
689        function: &str,
690        args: Vec<Value>,
691    ) -> EvalResult<Value> {
692        // Special handling for core.log → capture output
693        if module == "core" && function == "log" {
694            if let Some(val) = args.first() {
695                self.log_output
696                    .push(self.value_to_display_string(val));
697            }
698            return Ok(Value::Nil);
699        }
700
701        // Dispatch to the appropriate stdlib module
702        let result = match module {
703            "core" => core::CoreModule.call(function, args),
704            "math" => math::MathModule.call(function, args),
705            "string" => string::StringModule.call(function, args),
706            "list" => list::ListModule.call(function, args),
707            "record" => record::RecordModule.call(function, args),
708            "time" => time::TimeModule.call(function, args),
709            "convert" => convert::ConvertModule.call(function, args),
710            "json" => json::JsonModule.call(function, args),
711            "timer" => timer::TimerModule.call(function, args),
712            // Capability modules — check mock responses first, then return Err for unmocked calls
713            "http" | "storage" | "location" | "notifications" | "clipboard" | "share" => {
714                // Check if there's a mock response available
715                if let Some(response) = self.find_mock_response(module, function) {
716                    Ok(response)
717                } else {
718                    Ok(Value::Result(Box::new(ResultValue::Err(Value::String(
719                        format!("unmocked capability call: {module}.{function}"),
720                    )))))
721                }
722            }
723            _ => {
724                return Err(EvalError::UnknownFunction(format!(
725                    "unknown module '{module}'"
726                )));
727            }
728        };
729
730        result.map_err(|e| EvalError::StdlibError(e.to_string()))
731    }
732
733    // ══════════════════════════════════════════════════════════════════════
734    // Mock response lookup
735    // ══════════════════════════════════════════════════════════════════════
736
737    /// Find a mock response for a capability call.
738    fn find_mock_response(&self, module: &str, function: &str) -> Option<Value> {
739        self.mock_responses
740            .iter()
741            .find(|(m, f, _)| m == module && f == function)
742            .map(|(_, _, v)| v.clone())
743    }
744
745    // ══════════════════════════════════════════════════════════════════════
746    // Structural equality
747    // ══════════════════════════════════════════════════════════════════════
748
749    /// Deep structural equality. NaN != NaN. Functions always false.
750    pub fn structural_eq(&self, a: &Value, b: &Value) -> bool {
751        match (a, b) {
752            (Value::Number(x), Value::Number(y)) => {
753                // NaN != NaN
754                if x.is_nan() || y.is_nan() {
755                    false
756                } else {
757                    x == y
758                }
759            }
760            (Value::String(x), Value::String(y)) => x == y,
761            (Value::Bool(x), Value::Bool(y)) => x == y,
762            (Value::Nil, Value::Nil) => true,
763            (Value::List(x), Value::List(y)) => {
764                x.len() == y.len() && x.iter().zip(y.iter()).all(|(a, b)| self.structural_eq(a, b))
765            }
766            (
767                Value::Record { fields: fa, .. },
768                Value::Record { fields: fb, .. },
769            ) => {
770                fa.len() == fb.len()
771                    && fa
772                        .iter()
773                        .all(|(k, v)| fb.get(k).is_some_and(|v2| self.structural_eq(v, v2)))
774            }
775            (Value::Result(a), Value::Result(b)) => match (a.as_ref(), b.as_ref()) {
776                (ResultValue::Ok(a), ResultValue::Ok(b)) => self.structural_eq(a, b),
777                (ResultValue::Err(a), ResultValue::Err(b)) => self.structural_eq(a, b),
778                _ => false,
779            },
780            (
781                Value::SumVariant {
782                    variant: va,
783                    fields: fa,
784                    ..
785                },
786                Value::SumVariant {
787                    variant: vb,
788                    fields: fb,
789                    ..
790                },
791            ) => {
792                va == vb
793                    && fa.len() == fb.len()
794                    && fa
795                        .iter()
796                        .zip(fb.iter())
797                        .all(|(a, b)| self.structural_eq(a, b))
798            }
799            // Functions never equal
800            (Value::Function(_), _) | (_, Value::Function(_)) => false,
801            _ => false,
802        }
803    }
804
805    // ══════════════════════════════════════════════════════════════════════
806    // Display
807    // ══════════════════════════════════════════════════════════════════════
808
809    /// Convert a Value to its display string (for string interpolation, core.log, etc.)
810    pub fn value_to_display_string(&self, val: &Value) -> String {
811        match val {
812            Value::Number(n) => {
813                if n.fract() == 0.0 && n.is_finite() {
814                    format!("{}", *n as i64)
815                } else {
816                    format!("{n}")
817                }
818            }
819            Value::String(s) => s.clone(),
820            Value::Bool(b) => if *b { "true" } else { "false" }.to_string(),
821            Value::Nil => "nil".to_string(),
822            Value::List(items) => {
823                let parts: Vec<String> = items
824                    .iter()
825                    .map(|v| self.value_to_display_string(v))
826                    .collect();
827                format!("[{}]", parts.join(", "))
828            }
829            Value::Record { fields, .. } => {
830                let parts: Vec<String> = fields
831                    .iter()
832                    .map(|(k, v)| format!("{k}: {}", self.value_to_display_string(v)))
833                    .collect();
834                format!("{{ {} }}", parts.join(", "))
835            }
836            Value::Result(r) => match r.as_ref() {
837                ResultValue::Ok(v) => format!("Ok({})", self.value_to_display_string(v)),
838                ResultValue::Err(v) => format!("Err({})", self.value_to_display_string(v)),
839            },
840            Value::SumVariant {
841                variant, fields, ..
842            } => {
843                if fields.is_empty() {
844                    variant.clone()
845                } else {
846                    let parts: Vec<String> = fields
847                        .iter()
848                        .map(|v| self.value_to_display_string(v))
849                        .collect();
850                    format!("{variant}({})", parts.join(", "))
851                }
852            }
853            Value::Function(_) => "<function>".to_string(),
854            Value::Color { r, g, b, a } => format!("color({r}, {g}, {b}, {a})"),
855        }
856    }
857
858    /// String comparison for ordering (used by string comparison operators).
859    /// Note: PEPL spec says string comparison compares by length, not lexicographic.
860    /// This matches the eval_comparison implementation.
861    pub fn string_comparison(&self, _a: &str, _b: &str) -> std::cmp::Ordering {
862        // Not used in current implementation
863        std::cmp::Ordering::Equal
864    }
865}