dampen_core/expr/
eval.rs

1//! Expression evaluator for binding expressions
2//!
3//! This module provides unified evaluation functions that support both
4//! local model access and shared state context, eliminating code duplication.
5
6use crate::binding::{BindingValue, UiBindable};
7use crate::expr::error::{BindingError, BindingErrorKind};
8use crate::expr::{
9    BinaryOp, BinaryOpExpr, ConditionalExpr, Expr, FieldAccessExpr, LiteralExpr, MethodCallExpr,
10    SharedFieldAccessExpr, UnaryOp, UnaryOpExpr,
11};
12
13/// Evaluate an expression against a model
14///
15/// For expressions that use shared state (`{shared.field}`), use
16/// [`evaluate_expr_with_shared`](evaluate_expr_with_shared) instead.
17pub fn evaluate_expr(expr: &Expr, model: &dyn UiBindable) -> Result<BindingValue, BindingError> {
18    evaluate_expr_with_shared(expr, model, None)
19}
20
21/// Evaluate an expression with access to both local model and shared context
22///
23/// This is the preferred method when shared state bindings (`{shared.field}`) are used.
24/// Falls back to model-only evaluation for expressions that don't use shared state.
25///
26/// # Arguments
27///
28/// * `expr` - The expression to evaluate
29/// * `model` - The local view model implementing `UiBindable`
30/// * `shared` - Optional shared context implementing `UiBindable`
31///
32/// # Returns
33///
34/// The evaluated `BindingValue` or an error if evaluation fails.
35pub fn evaluate_expr_with_shared(
36    expr: &Expr,
37    model: &dyn UiBindable,
38    shared: Option<&dyn UiBindable>,
39) -> Result<BindingValue, BindingError> {
40    match expr {
41        Expr::FieldAccess(field_expr) => evaluate_field_access(field_expr, model),
42        Expr::SharedFieldAccess(shared_expr) => evaluate_shared_field_access(shared_expr, shared),
43        Expr::MethodCall(method_expr) => evaluate_method_call(method_expr, model, shared),
44        Expr::BinaryOp(binary_expr) => evaluate_binary_op(binary_expr, model, shared),
45        Expr::UnaryOp(unary_expr) => evaluate_unary_op(unary_expr, model, shared),
46        Expr::Conditional(conditional_expr) => {
47            evaluate_conditional(conditional_expr, model, shared)
48        }
49        Expr::Literal(literal_expr) => Ok(evaluate_literal(literal_expr)),
50    }
51}
52
53/// Evaluate field access: `counter` or `user.name`
54fn evaluate_field_access(
55    field_expr: &FieldAccessExpr,
56    model: &dyn UiBindable,
57) -> Result<BindingValue, BindingError> {
58    let path: Vec<&str> = field_expr.path.iter().map(|s| s.as_str()).collect();
59
60    model.get_field(&path).ok_or_else(|| {
61        let field_name = field_expr.path.join(".");
62        BindingError {
63            kind: BindingErrorKind::UnknownField,
64            message: format!("Field '{}' not found", field_name),
65            span: crate::ir::span::Span::new(0, 0, 0, 0),
66            suggestion: None,
67        }
68    })
69}
70
71/// Evaluate shared field access: `shared.theme` or `shared.user.preferences`
72fn evaluate_shared_field_access(
73    shared_expr: &SharedFieldAccessExpr,
74    shared: Option<&dyn UiBindable>,
75) -> Result<BindingValue, BindingError> {
76    let Some(shared_ctx) = shared else {
77        return Ok(BindingValue::String(String::new()));
78    };
79
80    let path: Vec<&str> = shared_expr.path.iter().map(|s| s.as_str()).collect();
81
82    shared_ctx.get_field(&path).ok_or_else(|| {
83        let field_name = format!("shared.{}", shared_expr.path.join("."));
84        BindingError {
85            kind: BindingErrorKind::UnknownField,
86            message: format!("Shared field '{}' not found", field_name),
87            span: crate::ir::span::Span::new(0, 0, 0, 0),
88            suggestion: None,
89        }
90    })
91}
92
93/// Evaluate method call: `items.len()` or `name.to_uppercase()`
94fn evaluate_method_call(
95    method_expr: &MethodCallExpr,
96    model: &dyn UiBindable,
97    shared: Option<&dyn UiBindable>,
98) -> Result<BindingValue, BindingError> {
99    let receiver = evaluate_expr_with_shared(&method_expr.receiver, model, shared)?;
100    let method = &method_expr.method;
101
102    let _args: Vec<BindingValue> = method_expr
103        .args
104        .iter()
105        .map(|arg| evaluate_expr_with_shared(arg, model, shared))
106        .collect::<Result<Vec<_>, _>>()?;
107
108    evaluate_method(receiver, method)
109}
110
111/// Unified method evaluation for both model and shared contexts
112fn evaluate_method(receiver: BindingValue, method: &str) -> Result<BindingValue, BindingError> {
113    match (receiver.clone(), method) {
114        (BindingValue::String(s), "len") => Ok(BindingValue::Integer(s.len() as i64)),
115        (BindingValue::String(s), "to_uppercase") => Ok(BindingValue::String(s.to_uppercase())),
116        (BindingValue::String(s), "to_lowercase") => Ok(BindingValue::String(s.to_lowercase())),
117        (BindingValue::String(s), "trim") => Ok(BindingValue::String(s.trim().to_string())),
118        (BindingValue::List(l), "len") => Ok(BindingValue::Integer(l.len() as i64)),
119        (BindingValue::List(l), "is_empty") => Ok(BindingValue::Bool(l.is_empty())),
120        (BindingValue::Integer(i), "to_string") => Ok(BindingValue::String(i.to_string())),
121        (BindingValue::Float(f), "to_string") => Ok(BindingValue::String(f.to_string())),
122        (BindingValue::Float(f), "round") => Ok(BindingValue::Float(f.round())),
123        (BindingValue::Float(f), "floor") => Ok(BindingValue::Float(f.floor())),
124        (BindingValue::Float(f), "ceil") => Ok(BindingValue::Float(f.ceil())),
125        (BindingValue::Bool(b), "to_string") => Ok(BindingValue::String(b.to_string())),
126        _ => Err(BindingError {
127            kind: BindingErrorKind::UnknownMethod,
128            message: format!("Method '{}' not supported on {:?}", method, receiver),
129            span: crate::ir::span::Span::new(0, 0, 0, 0),
130            suggestion: None,
131        }),
132    }
133}
134
135/// Evaluate binary operation: `a + b`, `x > 0`, etc.
136fn evaluate_binary_op(
137    binary_expr: &BinaryOpExpr,
138    model: &dyn UiBindable,
139    shared: Option<&dyn UiBindable>,
140) -> Result<BindingValue, BindingError> {
141    let left = evaluate_expr_with_shared(&binary_expr.left, model, shared)?;
142    let right = evaluate_expr_with_shared(&binary_expr.right, model, shared)?;
143
144    match binary_expr.op {
145        BinaryOp::Add => evaluate_add(left, right),
146        BinaryOp::Sub => evaluate_sub(left, right),
147        BinaryOp::Mul => evaluate_mul(left, right),
148        BinaryOp::Div => evaluate_div(left, right),
149        BinaryOp::Eq => Ok(BindingValue::Bool(left == right)),
150        BinaryOp::Ne => Ok(BindingValue::Bool(left != right)),
151        BinaryOp::Lt => Ok(BindingValue::Bool(compare_values(&left, &right, |a, b| {
152            a < b
153        }))),
154        BinaryOp::Le => Ok(BindingValue::Bool(compare_values(&left, &right, |a, b| {
155            a <= b
156        }))),
157        BinaryOp::Gt => Ok(BindingValue::Bool(compare_values(&left, &right, |a, b| {
158            a > b
159        }))),
160        BinaryOp::Ge => Ok(BindingValue::Bool(compare_values(&left, &right, |a, b| {
161            a >= b
162        }))),
163        BinaryOp::And => Ok(BindingValue::Bool(left.to_bool() && right.to_bool())),
164        BinaryOp::Or => Ok(BindingValue::Bool(left.to_bool() || right.to_bool())),
165    }
166}
167
168fn evaluate_add(left: BindingValue, right: BindingValue) -> Result<BindingValue, BindingError> {
169    match (left, right) {
170        (BindingValue::Integer(a), BindingValue::Integer(b)) => Ok(BindingValue::Integer(a + b)),
171        (BindingValue::Float(a), BindingValue::Float(b)) => Ok(BindingValue::Float(a + b)),
172        (BindingValue::String(a), BindingValue::String(b)) => Ok(BindingValue::String(a + &b)),
173        _ => Err(invalid_operation_error("add")),
174    }
175}
176
177fn evaluate_sub(left: BindingValue, right: BindingValue) -> Result<BindingValue, BindingError> {
178    match (left, right) {
179        (BindingValue::Integer(a), BindingValue::Integer(b)) => Ok(BindingValue::Integer(a - b)),
180        (BindingValue::Float(a), BindingValue::Float(b)) => Ok(BindingValue::Float(a - b)),
181        _ => Err(invalid_operation_error("subtract")),
182    }
183}
184
185fn evaluate_mul(left: BindingValue, right: BindingValue) -> Result<BindingValue, BindingError> {
186    match (left, right) {
187        (BindingValue::Integer(a), BindingValue::Integer(b)) => Ok(BindingValue::Integer(a * b)),
188        (BindingValue::Float(a), BindingValue::Float(b)) => Ok(BindingValue::Float(a * b)),
189        _ => Err(invalid_operation_error("multiply")),
190    }
191}
192
193fn evaluate_div(left: BindingValue, right: BindingValue) -> Result<BindingValue, BindingError> {
194    match (left, right) {
195        (BindingValue::Integer(a), BindingValue::Integer(b)) if b != 0 => {
196            Ok(BindingValue::Integer(a / b))
197        }
198        (BindingValue::Float(a), BindingValue::Float(b)) if b != 0.0 => {
199            Ok(BindingValue::Float(a / b))
200        }
201        (BindingValue::Integer(_), BindingValue::Integer(0)) => Err(BindingError {
202            kind: BindingErrorKind::InvalidOperation,
203            message: "Division by zero".to_string(),
204            span: crate::ir::span::Span::new(0, 0, 0, 0),
205            suggestion: None,
206        }),
207        (BindingValue::Float(_), BindingValue::Float(0.0)) => Err(BindingError {
208            kind: BindingErrorKind::InvalidOperation,
209            message: "Division by zero".to_string(),
210            span: crate::ir::span::Span::new(0, 0, 0, 0),
211            suggestion: None,
212        }),
213        _ => Err(invalid_operation_error("divide")),
214    }
215}
216
217fn invalid_operation_error(operation: &str) -> BindingError {
218    BindingError {
219        kind: BindingErrorKind::InvalidOperation,
220        message: format!("Cannot {} these types", operation),
221        span: crate::ir::span::Span::new(0, 0, 0, 0),
222        suggestion: None,
223    }
224}
225
226/// Helper for comparison operations
227///
228/// Compares values by their numeric representation for ordering operations
229/// (`<`, `<=`, `>`, `>=`). Objects (HashMap) are not compared as they have
230/// no natural ordering - such comparisons return `false`.
231fn compare_values<F>(left: &BindingValue, right: &BindingValue, cmp: F) -> bool
232where
233    F: Fn(f64, f64) -> bool,
234{
235    match (left, right) {
236        (BindingValue::Integer(a), BindingValue::Integer(b)) => cmp(*a as f64, *b as f64),
237        (BindingValue::Float(a), BindingValue::Float(b)) => cmp(*a, *b),
238        (BindingValue::String(a), BindingValue::String(b)) => cmp(a.len() as f64, b.len() as f64),
239        (BindingValue::List(a), BindingValue::List(b)) => cmp(a.len() as f64, b.len() as f64),
240        (BindingValue::Object(_), _) | (_, BindingValue::Object(_)) => false,
241        _ => false,
242    }
243}
244
245/// Evaluate unary operation: `!valid` or `-offset`
246fn evaluate_unary_op(
247    unary_expr: &UnaryOpExpr,
248    model: &dyn UiBindable,
249    shared: Option<&dyn UiBindable>,
250) -> Result<BindingValue, BindingError> {
251    let operand = evaluate_expr_with_shared(&unary_expr.operand, model, shared)?;
252
253    match unary_expr.op {
254        UnaryOp::Not => Ok(BindingValue::Bool(!operand.to_bool())),
255        UnaryOp::Neg => match operand {
256            BindingValue::Integer(i) => Ok(BindingValue::Integer(-i)),
257            BindingValue::Float(f) => Ok(BindingValue::Float(-f)),
258            _ => Err(BindingError {
259                kind: BindingErrorKind::InvalidOperation,
260                message: "Cannot negate this type".to_string(),
261                span: crate::ir::span::Span::new(0, 0, 0, 0),
262                suggestion: None,
263            }),
264        },
265    }
266}
267
268/// Evaluate conditional: `if condition then a else b`
269fn evaluate_conditional(
270    conditional_expr: &ConditionalExpr,
271    model: &dyn UiBindable,
272    shared: Option<&dyn UiBindable>,
273) -> Result<BindingValue, BindingError> {
274    let condition = evaluate_expr_with_shared(&conditional_expr.condition, model, shared)?;
275
276    if condition.to_bool() {
277        evaluate_expr_with_shared(&conditional_expr.then_branch, model, shared)
278    } else {
279        evaluate_expr_with_shared(&conditional_expr.else_branch, model, shared)
280    }
281}
282
283/// Evaluate literal value
284fn evaluate_literal(literal_expr: &LiteralExpr) -> BindingValue {
285    match literal_expr {
286        LiteralExpr::String(s) => BindingValue::String(s.clone()),
287        LiteralExpr::Integer(i) => BindingValue::Integer(*i),
288        LiteralExpr::Float(f) => BindingValue::Float(*f),
289        LiteralExpr::Bool(b) => BindingValue::Bool(*b),
290    }
291}
292
293/// Evaluate a binding expression with span information
294pub fn evaluate_binding_expr(
295    binding_expr: &crate::expr::BindingExpr,
296    model: &dyn UiBindable,
297) -> Result<BindingValue, BindingError> {
298    evaluate_binding_expr_with_shared(binding_expr, model, None)
299}
300
301/// Evaluate a binding expression with shared context support
302pub fn evaluate_binding_expr_with_shared(
303    binding_expr: &crate::expr::BindingExpr,
304    model: &dyn UiBindable,
305    shared: Option<&dyn UiBindable>,
306) -> Result<BindingValue, BindingError> {
307    match evaluate_expr_with_shared(&binding_expr.expr, model, shared) {
308        Ok(result) => Ok(result),
309        Err(mut err) => {
310            err.span = binding_expr.span;
311            Err(err)
312        }
313    }
314}
315
316/// Evaluate formatted string with interpolation: `"Total: {count}"`
317pub fn evaluate_formatted(
318    parts: &[crate::ir::InterpolatedPart],
319    model: &dyn UiBindable,
320) -> Result<String, BindingError> {
321    evaluate_formatted_with_shared(parts, model, None)
322}
323
324/// Evaluate formatted string with interpolation and shared context support
325pub fn evaluate_formatted_with_shared(
326    parts: &[crate::ir::InterpolatedPart],
327    model: &dyn UiBindable,
328    shared: Option<&dyn UiBindable>,
329) -> Result<String, BindingError> {
330    let literal_len: usize = parts
331        .iter()
332        .filter_map(|p| match p {
333            crate::ir::InterpolatedPart::Literal(lit) => Some(lit.len()),
334            _ => None,
335        })
336        .sum();
337
338    let mut result = String::with_capacity(literal_len.saturating_mul(2).max(32));
339
340    for part in parts {
341        match part {
342            crate::ir::InterpolatedPart::Literal(literal) => {
343                result.push_str(literal);
344            }
345            crate::ir::InterpolatedPart::Binding(binding_expr) => {
346                let value = evaluate_binding_expr_with_shared(binding_expr, model, shared)?;
347                result.push_str(&value.to_display_string());
348            }
349        }
350    }
351
352    Ok(result)
353}