Skip to main content

lumen_engine/expr/
eval.rs

1use crate::{
2    error::{ExpressionError, LumenError},
3    expr::{
4        ExpressionContext,
5        ast::{BinaryOp, ExprNode, Expression, ExpressionValue, GlobalVar, UnaryOp},
6        builtins::{evaluate_builtin, evaluate_text_measure_builtin},
7    },
8    node::{PropertyExpression, PropertyValue},
9};
10
11impl Expression {
12    pub fn evaluate(&self, ctx: &ExpressionContext<'_>) -> crate::Result<ExpressionValue> {
13        evaluate_expr(&self.ast, ctx)
14    }
15}
16
17pub fn property_value_to_expression_value(value: &PropertyValue) -> crate::Result<ExpressionValue> {
18    match value {
19        PropertyValue::Float(number) => Ok(ExpressionValue::Number(*number)),
20        PropertyValue::Int(number) => Ok(ExpressionValue::Number(*number as f64)),
21        PropertyValue::Bool(boolean) => Ok(ExpressionValue::Boolean(*boolean)),
22        PropertyValue::String(text) => Ok(ExpressionValue::String(text.clone())),
23        unsupported => Err(LumenError::Expression(ExpressionError::Evaluate {
24            path: None,
25            details: format!(
26                "cannot convert node property `{}` into an expression value",
27                node_property_type_name(unsupported)
28            ),
29        })),
30    }
31}
32
33pub(crate) fn evaluate_expr(
34    expr: &ExprNode,
35    ctx: &ExpressionContext<'_>,
36) -> crate::Result<ExpressionValue> {
37    match expr {
38        ExprNode::Literal(value) => Ok(value.clone()),
39        ExprNode::Unary(op, value) => {
40            let evaluated = evaluate_expr(value, ctx)?;
41            match op {
42                UnaryOp::Neg => Ok(ExpressionValue::Number(-to_number(&evaluated, ctx)?)),
43                UnaryOp::Not => Ok(ExpressionValue::Boolean(!to_boolean(&evaluated))),
44            }
45        }
46        ExprNode::Binary(left, op, right) => {
47            let lhs = evaluate_expr(left, ctx)?;
48            match op {
49                BinaryOp::And => {
50                    if !to_boolean(&lhs) {
51                        return Ok(ExpressionValue::Boolean(false));
52                    }
53                    let rhs = evaluate_expr(right, ctx)?;
54                    Ok(ExpressionValue::Boolean(to_boolean(&rhs)))
55                }
56                BinaryOp::Or => {
57                    if to_boolean(&lhs) {
58                        return Ok(ExpressionValue::Boolean(true));
59                    }
60                    let rhs = evaluate_expr(right, ctx)?;
61                    Ok(ExpressionValue::Boolean(to_boolean(&rhs)))
62                }
63                BinaryOp::Eq => {
64                    let rhs = evaluate_expr(right, ctx)?;
65                    Ok(ExpressionValue::Boolean(lhs == rhs))
66                }
67                BinaryOp::Neq => {
68                    let rhs = evaluate_expr(right, ctx)?;
69                    Ok(ExpressionValue::Boolean(lhs != rhs))
70                }
71                BinaryOp::Add
72                | BinaryOp::Sub
73                | BinaryOp::Mul
74                | BinaryOp::Div
75                | BinaryOp::Mod
76                | BinaryOp::Gt
77                | BinaryOp::Lt
78                | BinaryOp::Gte
79                | BinaryOp::Lte => {
80                    let rhs = evaluate_expr(right, ctx)?;
81                    let lhs_num = to_number(&lhs, ctx)?;
82                    let rhs_num = to_number(&rhs, ctx)?;
83                    match op {
84                        BinaryOp::Add => Ok(ExpressionValue::Number(lhs_num + rhs_num)),
85                        BinaryOp::Sub => Ok(ExpressionValue::Number(lhs_num - rhs_num)),
86                        BinaryOp::Mul => Ok(ExpressionValue::Number(lhs_num * rhs_num)),
87                        BinaryOp::Div => {
88                            if rhs_num.abs() <= f64::EPSILON {
89                                return Err(LumenError::Expression(ExpressionError::Evaluate {
90                                    path: ctx.path.clone(),
91                                    details: "division by zero".to_string(),
92                                }));
93                            }
94                            Ok(ExpressionValue::Number(lhs_num / rhs_num))
95                        }
96                        BinaryOp::Mod => {
97                            if rhs_num.abs() <= f64::EPSILON {
98                                return Err(LumenError::Expression(ExpressionError::Evaluate {
99                                    path: ctx.path.clone(),
100                                    details: "modulo by zero".to_string(),
101                                }));
102                            }
103                            Ok(ExpressionValue::Number(lhs_num % rhs_num))
104                        }
105                        BinaryOp::Gt => Ok(ExpressionValue::Boolean(lhs_num > rhs_num)),
106                        BinaryOp::Lt => Ok(ExpressionValue::Boolean(lhs_num < rhs_num)),
107                        BinaryOp::Gte => Ok(ExpressionValue::Boolean(lhs_num >= rhs_num)),
108                        BinaryOp::Lte => Ok(ExpressionValue::Boolean(lhs_num <= rhs_num)),
109                        _ => unreachable!(),
110                    }
111                }
112            }
113        }
114        ExprNode::Builtin(builtin, args) => {
115            if matches!(
116                builtin,
117                crate::expr::ast::BuiltinFn::TextHeight | crate::expr::ast::BuiltinFn::TextWidth
118            ) {
119                return evaluate_text_measure_builtin(*builtin, args, ctx);
120            }
121            let mut evaluated_args = Vec::with_capacity(args.len());
122            for arg in args {
123                evaluated_args.push(evaluate_expr(arg, ctx)?);
124            }
125            evaluate_builtin(*builtin, &evaluated_args, ctx)
126        }
127        ExprNode::Global(global) => match global {
128            GlobalVar::Frame => Ok(ExpressionValue::Number(f64::from(ctx.frame))),
129            GlobalVar::Time => Ok(ExpressionValue::Number(ctx.time_seconds())),
130            GlobalVar::Fps => Ok(ExpressionValue::Number(f64::from(ctx.fps))),
131            GlobalVar::Width => Ok(ExpressionValue::Number(f64::from(ctx.width))),
132            GlobalVar::Height => Ok(ExpressionValue::Number(f64::from(ctx.height))),
133            GlobalVar::Custom(name) => {
134                Err(LumenError::Expression(ExpressionError::UndefinedVariable {
135                    path: ctx.path.clone(),
136                    name: name.clone(),
137                }))
138            }
139        },
140        ExprNode::SymbolicPath(segments) => {
141            Err(LumenError::Expression(ExpressionError::Evaluate {
142                path: ctx.path.clone(),
143                details: format!(
144                    "unresolved symbolic property reference `{}`",
145                    segments.join(".")
146                ),
147            }))
148        }
149        ExprNode::Node(node_id) => Err(LumenError::Expression(ExpressionError::Evaluate {
150            path: ctx.path.clone(),
151            details: format!(
152                "node reference `{}` can only be used in builtins that accept node references",
153                node_id.0
154            ),
155        })),
156        ExprNode::PropertyValue(node_id, target_path) => {
157            let graph = ctx.graph.ok_or_else(|| {
158                LumenError::Expression(ExpressionError::Evaluate {
159                    path: ctx.path.clone(),
160                    details: format!(
161                        "no graph available to resolve node property reference `{}`",
162                        target_path.0
163                    ),
164                })
165            })?;
166            let node = graph.nodes.get(node_id).ok_or_else(|| {
167                LumenError::Expression(ExpressionError::Evaluate {
168                    path: ctx.path.clone(),
169                    details: format!(
170                        "node `{}` not found for property reference `{}`",
171                        node_id.0, target_path.0
172                    ),
173                })
174            })?;
175            let prop = node
176                .as_property_eval()
177                .get_property(&target_path.0)?
178                .ok_or_else(|| {
179                    LumenError::Expression(ExpressionError::Evaluate {
180                        path: ctx.path.clone(),
181                        details: format!(
182                            "property `{}` not found on node `{}`",
183                            target_path.0, node_id.0
184                        ),
185                    })
186                })?;
187            match &prop {
188                PropertyExpression::Expr(inner_expr) => inner_expr.evaluate(ctx),
189                PropertyExpression::Value(value) => property_value_to_expression_value(value),
190            }
191        }
192        ExprNode::VirtualProperty(id) => Err(LumenError::Expression(ExpressionError::Evaluate {
193            path: ctx.path.clone(),
194            details: format!("unresolved virtual property reference `{}`", id.0),
195        })),
196        ExprNode::Conditional(condition, when_true, when_false) => {
197            let condition = evaluate_expr(condition, ctx)?;
198            if to_boolean(&condition) {
199                evaluate_expr(when_true, ctx)
200            } else {
201                evaluate_expr(when_false, ctx)
202            }
203        }
204    }
205}
206
207fn to_number(value: &ExpressionValue, ctx: &ExpressionContext<'_>) -> crate::Result<f64> {
208    match value {
209        ExpressionValue::Number(number) => Ok(*number),
210        ExpressionValue::Boolean(boolean) => Ok(if *boolean { 1.0 } else { 0.0 }),
211        ExpressionValue::String(text) => text.parse::<f64>().map_err(|_| {
212            LumenError::Expression(ExpressionError::Parse {
213                path: ctx.path.clone(),
214                details: format!("cannot convert `{text}` into f64"),
215            })
216        }),
217    }
218}
219
220fn to_boolean(value: &ExpressionValue) -> bool {
221    match value {
222        ExpressionValue::Boolean(boolean) => *boolean,
223        ExpressionValue::Number(number) => number.abs() > f64::EPSILON,
224        ExpressionValue::String(text) => !text.is_empty(),
225    }
226}
227
228fn node_property_type_name(value: &PropertyValue) -> &'static str {
229    match value {
230        PropertyValue::Float(_) => "float",
231        PropertyValue::Int(_) => "int",
232        PropertyValue::Bool(_) => "bool",
233        PropertyValue::String(_) => "string",
234        PropertyValue::Color(_) => "color",
235        PropertyValue::Paint(_) => "paint",
236        PropertyValue::Vec2(_) => "vec2",
237        PropertyValue::FloatVec(_) => "float[]",
238        PropertyValue::IntVec(_) => "int[]",
239        PropertyValue::StringVec(_) => "string[]",
240    }
241}
242
243#[cfg(test)]
244mod tests {
245    use super::*;
246    use crate::{
247        expr::ast::{BuiltinFn, ExprNode, ExpressionId},
248        graph::Graph,
249        node::{NodeId, NodeKind, PropertyValue, source::text::Text},
250    };
251
252    fn test_context() -> ExpressionContext<'static> {
253        ExpressionContext {
254            frame: 48,
255            fps: 24.0,
256            width: 1920,
257            height: 1080,
258            duration_frames: 240,
259            path: Some("node.opacity".to_string()),
260            graph: None,
261        }
262    }
263
264    #[test]
265    fn evaluates_globals_from_expression_context() {
266        let ctx = test_context();
267
268        assert_eq!(
269            evaluate_expr(&ExprNode::Global(GlobalVar::Time), &ctx).unwrap(),
270            ExpressionValue::Number(2.0)
271        );
272        assert_eq!(
273            evaluate_expr(&ExprNode::Global(GlobalVar::Fps), &ctx).unwrap(),
274            ExpressionValue::Number(24.0)
275        );
276        assert_eq!(
277            evaluate_expr(&ExprNode::Global(GlobalVar::Width), &ctx).unwrap(),
278            ExpressionValue::Number(1920.0)
279        );
280        assert_eq!(
281            evaluate_expr(&ExprNode::Global(GlobalVar::Height), &ctx).unwrap(),
282            ExpressionValue::Number(1080.0)
283        );
284    }
285
286    #[test]
287    fn converts_supported_node_properties() {
288        assert_eq!(
289            property_value_to_expression_value(&PropertyValue::Int(7)).unwrap(),
290            ExpressionValue::Number(7.0)
291        );
292        assert_eq!(
293            property_value_to_expression_value(&PropertyValue::Bool(true)).unwrap(),
294            ExpressionValue::Boolean(true)
295        );
296    }
297
298    #[test]
299    fn rejects_non_scalar_node_properties() {
300        let error = property_value_to_expression_value(&PropertyValue::Color([0, 0, 0, 255]))
301            .unwrap_err()
302            .to_string();
303
304        assert!(error.contains("cannot convert node property `color`"));
305    }
306
307    #[test]
308    fn evaluates_linear_and_step_builtins() {
309        let ctx = test_context();
310        let expression = Expression {
311            id: ExpressionId(1),
312            ast: ExprNode::Builtin(
313                BuiltinFn::Linear,
314                vec![
315                    ExprNode::Literal(ExpressionValue::Number(10.0)),
316                    ExprNode::Literal(ExpressionValue::Number(20.0)),
317                    ExprNode::Literal(ExpressionValue::Number(0.25)),
318                ],
319            ),
320            references: Vec::new(),
321            source: "linear(10, 20, 0.25)".to_string(),
322        };
323        let stepped = Expression {
324            id: ExpressionId(2),
325            ast: ExprNode::Builtin(
326                BuiltinFn::Step,
327                vec![
328                    ExprNode::Literal(ExpressionValue::Number(10.0)),
329                    ExprNode::Literal(ExpressionValue::Number(20.0)),
330                    ExprNode::Literal(ExpressionValue::Number(0.5)),
331                ],
332            ),
333            references: Vec::new(),
334            source: "step(10, 20, 0.5)".to_string(),
335        };
336
337        assert_eq!(
338            expression.evaluate(&ctx).unwrap(),
339            ExpressionValue::Number(12.5)
340        );
341        assert_eq!(
342            stepped.evaluate(&ctx).unwrap(),
343            ExpressionValue::Number(10.0)
344        );
345    }
346
347    #[test]
348    fn text_measure_builtins_use_explicit_text_inputs() {
349        let ctx = test_context();
350
351        let implicit = Expression::parse("text_width(\"Morning, update posted.\")").unwrap();
352        let explicit =
353            Expression::parse("text_width(node(8, \"content\"), 32, 300, \"Roboto\")").unwrap();
354        let implicit_height =
355            Expression::parse("text_height(\"Morning, update posted.\")").unwrap();
356        let explicit_height =
357            Expression::parse("text_height(node(8, \"content\"), 32, 300, \"Roboto\")").unwrap();
358
359        assert!(implicit.evaluate(&ctx).is_ok());
360        assert!(explicit.evaluate(&ctx).is_err());
361        assert!(implicit_height.evaluate(&ctx).is_ok());
362        assert!(explicit_height.evaluate(&ctx).is_err());
363    }
364
365    #[test]
366    fn text_measure_builtins_resolve_text_nodes_from_graph() {
367        let text_id = NodeId::new(8);
368        let mut graph = Graph::new();
369        graph.nodes.insert(
370            text_id,
371            NodeKind::Text(Text {
372                id: text_id,
373                params: crate::node::source::text::TextParamsDelegate {
374                    content: crate::node::Deferred::value("Morning, update posted.".to_string()),
375                    font_family: crate::node::Deferred::value("Roboto".to_string()),
376                    font_size: crate::node::Deferred::value(32.0),
377                    max_width: crate::node::Deferred::value(300.0),
378                    ..Default::default()
379                },
380                ..Text::default()
381            }),
382        );
383        let ctx = ExpressionContext {
384            graph: Some(&graph),
385            ..test_context()
386        };
387
388        let width = Expression::parse("text_width(node(8))")
389            .unwrap()
390            .evaluate(&ctx)
391            .unwrap();
392        let height = Expression::parse("text_height(node(8))")
393            .unwrap()
394            .evaluate(&ctx)
395            .unwrap();
396
397        assert!(matches!(width, ExpressionValue::Number(value) if value > 0.0));
398        assert!(matches!(height, ExpressionValue::Number(value) if value > 32.0));
399    }
400}