dampen_core/expr/
eval.rs

1//! Expression evaluator for binding expressions
2
3use crate::binding::{BindingValue, UiBindable};
4use crate::expr::error::{BindingError, BindingErrorKind};
5use crate::expr::{
6    BinaryOp, BinaryOpExpr, ConditionalExpr, Expr, FieldAccessExpr, LiteralExpr, MethodCallExpr,
7    SharedFieldAccessExpr, UnaryOp, UnaryOpExpr,
8};
9
10/// Evaluate an expression against a model
11pub fn evaluate_expr(expr: &Expr, model: &dyn UiBindable) -> Result<BindingValue, BindingError> {
12    match expr {
13        Expr::FieldAccess(field_expr) => evaluate_field_access(field_expr, model),
14        Expr::SharedFieldAccess(_) => {
15            // SharedFieldAccess requires shared context - use evaluate_expr_with_shared instead
16            Err(BindingError {
17                kind: BindingErrorKind::InvalidOperation,
18                message: "Shared field access requires shared context. Use evaluate_expr_with_shared instead.".to_string(),
19                span: crate::ir::span::Span::new(0, 0, 0, 0),
20                suggestion: None,
21            })
22        }
23        Expr::MethodCall(method_expr) => evaluate_method_call(method_expr, model),
24        Expr::BinaryOp(binary_expr) => evaluate_binary_op(binary_expr, model),
25        Expr::UnaryOp(unary_expr) => evaluate_unary_op(unary_expr, model),
26        Expr::Conditional(conditional_expr) => evaluate_conditional(conditional_expr, model),
27        Expr::Literal(literal_expr) => Ok(evaluate_literal(literal_expr)),
28    }
29}
30
31/// Evaluate field access: `counter` or `user.name`
32fn evaluate_field_access(
33    field_expr: &FieldAccessExpr,
34    model: &dyn UiBindable,
35) -> Result<BindingValue, BindingError> {
36    let path: Vec<&str> = field_expr.path.iter().map(|s| s.as_str()).collect();
37
38    model.get_field(&path).ok_or_else(|| {
39        let field_name = field_expr.path.join(".");
40        BindingError {
41            kind: BindingErrorKind::UnknownField,
42            message: format!("Field '{}' not found", field_name),
43            span: crate::ir::span::Span::new(0, 0, 0, 0), // Will be set by caller
44            suggestion: None,
45        }
46    })
47}
48
49/// Evaluate method call: `items.len()` or `name.to_uppercase()`
50fn evaluate_method_call(
51    method_expr: &MethodCallExpr,
52    model: &dyn UiBindable,
53) -> Result<BindingValue, BindingError> {
54    let receiver = evaluate_expr(&method_expr.receiver, model)?;
55    let method = &method_expr.method;
56
57    // Evaluate arguments (currently unused but evaluated for future compatibility)
58    let _args: Vec<BindingValue> = method_expr
59        .args
60        .iter()
61        .map(|arg| evaluate_expr(arg, model))
62        .collect::<Result<Vec<_>, _>>()?;
63
64    match (receiver.clone(), method.as_str()) {
65        // String methods
66        (BindingValue::String(s), "len") => Ok(BindingValue::Integer(s.len() as i64)),
67        (BindingValue::String(s), "to_uppercase") => Ok(BindingValue::String(s.to_uppercase())),
68        (BindingValue::String(s), "to_lowercase") => Ok(BindingValue::String(s.to_lowercase())),
69        (BindingValue::String(s), "trim") => Ok(BindingValue::String(s.trim().to_string())),
70
71        // List methods
72        (BindingValue::List(l), "len") => Ok(BindingValue::Integer(l.len() as i64)),
73        (BindingValue::List(l), "is_empty") => Ok(BindingValue::Bool(l.is_empty())),
74
75        // Integer methods
76        (BindingValue::Integer(i), "to_string") => Ok(BindingValue::String(i.to_string())),
77
78        // Float methods
79        (BindingValue::Float(f), "to_string") => Ok(BindingValue::String(f.to_string())),
80        (BindingValue::Float(f), "round") => Ok(BindingValue::Float(f.round())),
81        (BindingValue::Float(f), "floor") => Ok(BindingValue::Float(f.floor())),
82        (BindingValue::Float(f), "ceil") => Ok(BindingValue::Float(f.ceil())),
83
84        // Bool methods
85        (BindingValue::Bool(b), "to_string") => Ok(BindingValue::String(b.to_string())),
86
87        _ => Err(BindingError {
88            kind: BindingErrorKind::UnknownMethod,
89            message: format!("Method '{}' not supported on {:?}", method, receiver),
90            span: crate::ir::span::Span::new(0, 0, 0, 0),
91            suggestion: None,
92        }),
93    }
94}
95
96/// Evaluate binary operation: `a + b`, `x > 0`, etc.
97fn evaluate_binary_op(
98    binary_expr: &BinaryOpExpr,
99    model: &dyn UiBindable,
100) -> Result<BindingValue, BindingError> {
101    let left = evaluate_expr(&binary_expr.left, model)?;
102    let right = evaluate_expr(&binary_expr.right, model)?;
103
104    match binary_expr.op {
105        // Arithmetic
106        BinaryOp::Add => match (left, right) {
107            (BindingValue::Integer(a), BindingValue::Integer(b)) => {
108                Ok(BindingValue::Integer(a + b))
109            }
110            (BindingValue::Float(a), BindingValue::Float(b)) => Ok(BindingValue::Float(a + b)),
111            (BindingValue::String(a), BindingValue::String(b)) => Ok(BindingValue::String(a + &b)),
112            _ => Err(BindingError {
113                kind: BindingErrorKind::InvalidOperation,
114                message: "Cannot add these types".to_string(),
115                span: crate::ir::span::Span::new(0, 0, 0, 0),
116                suggestion: None,
117            }),
118        },
119        BinaryOp::Sub => match (left, right) {
120            (BindingValue::Integer(a), BindingValue::Integer(b)) => {
121                Ok(BindingValue::Integer(a - b))
122            }
123            (BindingValue::Float(a), BindingValue::Float(b)) => Ok(BindingValue::Float(a - b)),
124            _ => Err(BindingError {
125                kind: BindingErrorKind::InvalidOperation,
126                message: "Cannot subtract these types".to_string(),
127                span: crate::ir::span::Span::new(0, 0, 0, 0),
128                suggestion: None,
129            }),
130        },
131        BinaryOp::Mul => match (left, right) {
132            (BindingValue::Integer(a), BindingValue::Integer(b)) => {
133                Ok(BindingValue::Integer(a * b))
134            }
135            (BindingValue::Float(a), BindingValue::Float(b)) => Ok(BindingValue::Float(a * b)),
136            _ => Err(BindingError {
137                kind: BindingErrorKind::InvalidOperation,
138                message: "Cannot multiply these types".to_string(),
139                span: crate::ir::span::Span::new(0, 0, 0, 0),
140                suggestion: None,
141            }),
142        },
143        BinaryOp::Div => match (left, right) {
144            (BindingValue::Integer(a), BindingValue::Integer(b)) if b != 0 => {
145                Ok(BindingValue::Integer(a / b))
146            }
147            (BindingValue::Float(a), BindingValue::Float(b)) if b != 0.0 => {
148                Ok(BindingValue::Float(a / b))
149            }
150            _ => Err(BindingError {
151                kind: BindingErrorKind::InvalidOperation,
152                message: "Cannot divide these types or division by zero".to_string(),
153                span: crate::ir::span::Span::new(0, 0, 0, 0),
154                suggestion: None,
155            }),
156        },
157
158        // Comparison
159        BinaryOp::Eq => Ok(BindingValue::Bool(left == right)),
160        BinaryOp::Ne => Ok(BindingValue::Bool(left != right)),
161        BinaryOp::Lt => Ok(BindingValue::Bool(compare_values(&left, &right, |a, b| {
162            a < b
163        }))),
164        BinaryOp::Le => Ok(BindingValue::Bool(compare_values(&left, &right, |a, b| {
165            a <= b
166        }))),
167        BinaryOp::Gt => Ok(BindingValue::Bool(compare_values(&left, &right, |a, b| {
168            a > b
169        }))),
170        BinaryOp::Ge => Ok(BindingValue::Bool(compare_values(&left, &right, |a, b| {
171            a >= b
172        }))),
173
174        // Logical
175        BinaryOp::And => {
176            let left_bool = left.to_bool();
177            let right_bool = right.to_bool();
178            Ok(BindingValue::Bool(left_bool && right_bool))
179        }
180        BinaryOp::Or => {
181            let left_bool = left.to_bool();
182            let right_bool = right.to_bool();
183            Ok(BindingValue::Bool(left_bool || right_bool))
184        }
185    }
186}
187
188/// Helper for comparison operations
189fn compare_values<F>(left: &BindingValue, right: &BindingValue, cmp: F) -> bool
190where
191    F: Fn(f64, f64) -> bool,
192{
193    match (left, right) {
194        (BindingValue::Integer(a), BindingValue::Integer(b)) => cmp(*a as f64, *b as f64),
195        (BindingValue::Float(a), BindingValue::Float(b)) => cmp(*a, *b),
196        (BindingValue::String(a), BindingValue::String(b)) => cmp(a.len() as f64, b.len() as f64),
197        (BindingValue::List(a), BindingValue::List(b)) => cmp(a.len() as f64, b.len() as f64),
198        _ => false,
199    }
200}
201
202/// Evaluate unary operation: `!valid` or `-offset`
203fn evaluate_unary_op(
204    unary_expr: &UnaryOpExpr,
205    model: &dyn UiBindable,
206) -> Result<BindingValue, BindingError> {
207    let operand = evaluate_expr(&unary_expr.operand, model)?;
208
209    match unary_expr.op {
210        UnaryOp::Not => Ok(BindingValue::Bool(!operand.to_bool())),
211        UnaryOp::Neg => match operand {
212            BindingValue::Integer(i) => Ok(BindingValue::Integer(-i)),
213            BindingValue::Float(f) => Ok(BindingValue::Float(-f)),
214            _ => Err(BindingError {
215                kind: BindingErrorKind::InvalidOperation,
216                message: "Cannot negate this type".to_string(),
217                span: crate::ir::span::Span::new(0, 0, 0, 0),
218                suggestion: None,
219            }),
220        },
221    }
222}
223
224/// Evaluate conditional: `if condition then a else b`
225fn evaluate_conditional(
226    conditional_expr: &ConditionalExpr,
227    model: &dyn UiBindable,
228) -> Result<BindingValue, BindingError> {
229    let condition = evaluate_expr(&conditional_expr.condition, model)?;
230
231    if condition.to_bool() {
232        evaluate_expr(&conditional_expr.then_branch, model)
233    } else {
234        evaluate_expr(&conditional_expr.else_branch, model)
235    }
236}
237
238/// Evaluate literal value
239fn evaluate_literal(literal_expr: &LiteralExpr) -> BindingValue {
240    match literal_expr {
241        LiteralExpr::String(s) => BindingValue::String(s.clone()),
242        LiteralExpr::Integer(i) => BindingValue::Integer(*i),
243        LiteralExpr::Float(f) => BindingValue::Float(*f),
244        LiteralExpr::Bool(b) => BindingValue::Bool(*b),
245    }
246}
247
248/// Evaluate a binding expression with span information
249pub fn evaluate_binding_expr(
250    binding_expr: &crate::expr::BindingExpr,
251    model: &dyn UiBindable,
252) -> Result<BindingValue, BindingError> {
253    match evaluate_expr(&binding_expr.expr, model) {
254        Ok(result) => Ok(result),
255        Err(mut err) => {
256            err.span = binding_expr.span;
257            Err(err)
258        }
259    }
260}
261
262/// Evaluate formatted string with interpolation: `"Total: {count}"`
263pub fn evaluate_formatted(
264    parts: &[crate::ir::InterpolatedPart],
265    model: &dyn UiBindable,
266) -> Result<String, BindingError> {
267    let mut result = String::new();
268
269    for part in parts {
270        match part {
271            crate::ir::InterpolatedPart::Literal(literal) => {
272                result.push_str(literal);
273            }
274            crate::ir::InterpolatedPart::Binding(binding_expr) => {
275                let value = evaluate_binding_expr(binding_expr, model)?;
276                result.push_str(&value.to_display_string());
277            }
278        }
279    }
280
281    Ok(result)
282}
283
284// ==================== Shared-Aware Evaluation ====================
285
286/// Evaluate an expression with access to both local model and shared context
287///
288/// This is the preferred method when shared state bindings (`{shared.field}`) are used.
289/// Falls back to model-only evaluation for expressions that don't use shared state.
290///
291/// # Arguments
292///
293/// * `expr` - The expression to evaluate
294/// * `model` - The local view model implementing `UiBindable`
295/// * `shared` - Optional shared context implementing `UiBindable`
296///
297/// # Returns
298///
299/// The evaluated `BindingValue` or an error if evaluation fails.
300pub fn evaluate_expr_with_shared(
301    expr: &Expr,
302    model: &dyn UiBindable,
303    shared: Option<&dyn UiBindable>,
304) -> Result<BindingValue, BindingError> {
305    match expr {
306        Expr::FieldAccess(field_expr) => evaluate_field_access(field_expr, model),
307        Expr::SharedFieldAccess(shared_expr) => evaluate_shared_field_access(shared_expr, shared),
308        Expr::MethodCall(method_expr) => {
309            evaluate_method_call_with_shared(method_expr, model, shared)
310        }
311        Expr::BinaryOp(binary_expr) => evaluate_binary_op_with_shared(binary_expr, model, shared),
312        Expr::UnaryOp(unary_expr) => evaluate_unary_op_with_shared(unary_expr, model, shared),
313        Expr::Conditional(conditional_expr) => {
314            evaluate_conditional_with_shared(conditional_expr, model, shared)
315        }
316        Expr::Literal(literal_expr) => Ok(evaluate_literal(literal_expr)),
317    }
318}
319
320/// Evaluate shared field access: `shared.theme` or `shared.user.preferences`
321fn evaluate_shared_field_access(
322    shared_expr: &SharedFieldAccessExpr,
323    shared: Option<&dyn UiBindable>,
324) -> Result<BindingValue, BindingError> {
325    let Some(shared_ctx) = shared else {
326        // No shared context provided - return empty string (graceful degradation)
327        return Ok(BindingValue::String(String::new()));
328    };
329
330    let path: Vec<&str> = shared_expr.path.iter().map(|s| s.as_str()).collect();
331
332    shared_ctx.get_field(&path).ok_or_else(|| {
333        let field_name = format!("shared.{}", shared_expr.path.join("."));
334        BindingError {
335            kind: BindingErrorKind::UnknownField,
336            message: format!("Shared field '{}' not found", field_name),
337            span: crate::ir::span::Span::new(0, 0, 0, 0),
338            suggestion: None,
339        }
340    })
341}
342
343/// Evaluate method call with shared context support
344fn evaluate_method_call_with_shared(
345    method_expr: &MethodCallExpr,
346    model: &dyn UiBindable,
347    shared: Option<&dyn UiBindable>,
348) -> Result<BindingValue, BindingError> {
349    let receiver = evaluate_expr_with_shared(&method_expr.receiver, model, shared)?;
350    let method = &method_expr.method;
351
352    // Evaluate arguments with shared context
353    let _args: Vec<BindingValue> = method_expr
354        .args
355        .iter()
356        .map(|arg| evaluate_expr_with_shared(arg, model, shared))
357        .collect::<Result<Vec<_>, _>>()?;
358
359    match (receiver.clone(), method.as_str()) {
360        // String methods
361        (BindingValue::String(s), "len") => Ok(BindingValue::Integer(s.len() as i64)),
362        (BindingValue::String(s), "to_uppercase") => Ok(BindingValue::String(s.to_uppercase())),
363        (BindingValue::String(s), "to_lowercase") => Ok(BindingValue::String(s.to_lowercase())),
364        (BindingValue::String(s), "trim") => Ok(BindingValue::String(s.trim().to_string())),
365
366        // List methods
367        (BindingValue::List(l), "len") => Ok(BindingValue::Integer(l.len() as i64)),
368        (BindingValue::List(l), "is_empty") => Ok(BindingValue::Bool(l.is_empty())),
369
370        // Integer methods
371        (BindingValue::Integer(i), "to_string") => Ok(BindingValue::String(i.to_string())),
372
373        // Float methods
374        (BindingValue::Float(f), "to_string") => Ok(BindingValue::String(f.to_string())),
375        (BindingValue::Float(f), "round") => Ok(BindingValue::Float(f.round())),
376        (BindingValue::Float(f), "floor") => Ok(BindingValue::Float(f.floor())),
377        (BindingValue::Float(f), "ceil") => Ok(BindingValue::Float(f.ceil())),
378
379        // Bool methods
380        (BindingValue::Bool(b), "to_string") => Ok(BindingValue::String(b.to_string())),
381
382        _ => Err(BindingError {
383            kind: BindingErrorKind::UnknownMethod,
384            message: format!("Method '{}' not supported on {:?}", method, receiver),
385            span: crate::ir::span::Span::new(0, 0, 0, 0),
386            suggestion: None,
387        }),
388    }
389}
390
391/// Evaluate binary operation with shared context support
392fn evaluate_binary_op_with_shared(
393    binary_expr: &BinaryOpExpr,
394    model: &dyn UiBindable,
395    shared: Option<&dyn UiBindable>,
396) -> Result<BindingValue, BindingError> {
397    let left = evaluate_expr_with_shared(&binary_expr.left, model, shared)?;
398    let right = evaluate_expr_with_shared(&binary_expr.right, model, shared)?;
399
400    match binary_expr.op {
401        // Arithmetic
402        BinaryOp::Add => match (left, right) {
403            (BindingValue::Integer(a), BindingValue::Integer(b)) => {
404                Ok(BindingValue::Integer(a + b))
405            }
406            (BindingValue::Float(a), BindingValue::Float(b)) => Ok(BindingValue::Float(a + b)),
407            (BindingValue::String(a), BindingValue::String(b)) => Ok(BindingValue::String(a + &b)),
408            _ => Err(BindingError {
409                kind: BindingErrorKind::InvalidOperation,
410                message: "Cannot add these types".to_string(),
411                span: crate::ir::span::Span::new(0, 0, 0, 0),
412                suggestion: None,
413            }),
414        },
415        BinaryOp::Sub => match (left, right) {
416            (BindingValue::Integer(a), BindingValue::Integer(b)) => {
417                Ok(BindingValue::Integer(a - b))
418            }
419            (BindingValue::Float(a), BindingValue::Float(b)) => Ok(BindingValue::Float(a - b)),
420            _ => Err(BindingError {
421                kind: BindingErrorKind::InvalidOperation,
422                message: "Cannot subtract these types".to_string(),
423                span: crate::ir::span::Span::new(0, 0, 0, 0),
424                suggestion: None,
425            }),
426        },
427        BinaryOp::Mul => match (left, right) {
428            (BindingValue::Integer(a), BindingValue::Integer(b)) => {
429                Ok(BindingValue::Integer(a * b))
430            }
431            (BindingValue::Float(a), BindingValue::Float(b)) => Ok(BindingValue::Float(a * b)),
432            _ => Err(BindingError {
433                kind: BindingErrorKind::InvalidOperation,
434                message: "Cannot multiply these types".to_string(),
435                span: crate::ir::span::Span::new(0, 0, 0, 0),
436                suggestion: None,
437            }),
438        },
439        BinaryOp::Div => match (left, right) {
440            (BindingValue::Integer(a), BindingValue::Integer(b)) if b != 0 => {
441                Ok(BindingValue::Integer(a / b))
442            }
443            (BindingValue::Float(a), BindingValue::Float(b)) if b != 0.0 => {
444                Ok(BindingValue::Float(a / b))
445            }
446            _ => Err(BindingError {
447                kind: BindingErrorKind::InvalidOperation,
448                message: "Cannot divide these types or division by zero".to_string(),
449                span: crate::ir::span::Span::new(0, 0, 0, 0),
450                suggestion: None,
451            }),
452        },
453
454        // Comparison
455        BinaryOp::Eq => Ok(BindingValue::Bool(left == right)),
456        BinaryOp::Ne => Ok(BindingValue::Bool(left != right)),
457        BinaryOp::Lt => Ok(BindingValue::Bool(compare_values(&left, &right, |a, b| {
458            a < b
459        }))),
460        BinaryOp::Le => Ok(BindingValue::Bool(compare_values(&left, &right, |a, b| {
461            a <= b
462        }))),
463        BinaryOp::Gt => Ok(BindingValue::Bool(compare_values(&left, &right, |a, b| {
464            a > b
465        }))),
466        BinaryOp::Ge => Ok(BindingValue::Bool(compare_values(&left, &right, |a, b| {
467            a >= b
468        }))),
469
470        // Logical
471        BinaryOp::And => {
472            let left_bool = left.to_bool();
473            let right_bool = right.to_bool();
474            Ok(BindingValue::Bool(left_bool && right_bool))
475        }
476        BinaryOp::Or => {
477            let left_bool = left.to_bool();
478            let right_bool = right.to_bool();
479            Ok(BindingValue::Bool(left_bool || right_bool))
480        }
481    }
482}
483
484/// Evaluate unary operation with shared context support
485fn evaluate_unary_op_with_shared(
486    unary_expr: &UnaryOpExpr,
487    model: &dyn UiBindable,
488    shared: Option<&dyn UiBindable>,
489) -> Result<BindingValue, BindingError> {
490    let operand = evaluate_expr_with_shared(&unary_expr.operand, model, shared)?;
491
492    match unary_expr.op {
493        UnaryOp::Not => Ok(BindingValue::Bool(!operand.to_bool())),
494        UnaryOp::Neg => match operand {
495            BindingValue::Integer(i) => Ok(BindingValue::Integer(-i)),
496            BindingValue::Float(f) => Ok(BindingValue::Float(-f)),
497            _ => Err(BindingError {
498                kind: BindingErrorKind::InvalidOperation,
499                message: "Cannot negate this type".to_string(),
500                span: crate::ir::span::Span::new(0, 0, 0, 0),
501                suggestion: None,
502            }),
503        },
504    }
505}
506
507/// Evaluate conditional with shared context support
508fn evaluate_conditional_with_shared(
509    conditional_expr: &ConditionalExpr,
510    model: &dyn UiBindable,
511    shared: Option<&dyn UiBindable>,
512) -> Result<BindingValue, BindingError> {
513    let condition = evaluate_expr_with_shared(&conditional_expr.condition, model, shared)?;
514
515    if condition.to_bool() {
516        evaluate_expr_with_shared(&conditional_expr.then_branch, model, shared)
517    } else {
518        evaluate_expr_with_shared(&conditional_expr.else_branch, model, shared)
519    }
520}
521
522/// Evaluate a binding expression with shared context support
523pub fn evaluate_binding_expr_with_shared(
524    binding_expr: &crate::expr::BindingExpr,
525    model: &dyn UiBindable,
526    shared: Option<&dyn UiBindable>,
527) -> Result<BindingValue, BindingError> {
528    match evaluate_expr_with_shared(&binding_expr.expr, model, shared) {
529        Ok(result) => Ok(result),
530        Err(mut err) => {
531            err.span = binding_expr.span;
532            Err(err)
533        }
534    }
535}
536
537/// Evaluate formatted string with interpolation and shared context support
538pub fn evaluate_formatted_with_shared(
539    parts: &[crate::ir::InterpolatedPart],
540    model: &dyn UiBindable,
541    shared: Option<&dyn UiBindable>,
542) -> Result<String, BindingError> {
543    let mut result = String::new();
544
545    for part in parts {
546        match part {
547            crate::ir::InterpolatedPart::Literal(literal) => {
548                result.push_str(literal);
549            }
550            crate::ir::InterpolatedPart::Binding(binding_expr) => {
551                let value = evaluate_binding_expr_with_shared(binding_expr, model, shared)?;
552                result.push_str(&value.to_display_string());
553            }
554        }
555    }
556
557    Ok(result)
558}