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}