solverforge_core/wasm/
expression.rs

1use serde::{Deserialize, Serialize};
2
3/// Rich expression tree for constraint predicates
4///
5/// This enum represents a complete expression language for building constraint predicates.
6/// Expressions are serializable (via serde) for use across FFI boundaries.
7///
8/// # Example
9/// ```
10/// # use solverforge_core::wasm::Expression;
11/// // Build expression: param(0).employee != null
12/// let expr = Expression::IsNotNull {
13///     operand: Box::new(Expression::FieldAccess {
14///         object: Box::new(Expression::Param { index: 0 }),
15///         class_name: "Shift".into(),
16///         field_name: "employee".into(),
17///     })
18/// };
19/// ```
20#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
21#[serde(tag = "kind")]
22pub enum Expression {
23    // ===== Literals =====
24    /// Integer literal (i64)
25    IntLiteral { value: i64 },
26
27    /// Boolean literal
28    BoolLiteral { value: bool },
29
30    /// Null value
31    Null,
32
33    // ===== Parameter Access =====
34    /// Access a function parameter by index
35    /// Example: param(0) refers to the first parameter
36    Param { index: u32 },
37
38    // ===== Field Access =====
39    /// Access a field on an object
40    /// Example: param(0).get("Employee", "name")
41    FieldAccess {
42        object: Box<Expression>,
43        class_name: String,
44        field_name: String,
45    },
46
47    // ===== Comparisons =====
48    /// Equal comparison (==)
49    Eq {
50        left: Box<Expression>,
51        right: Box<Expression>,
52    },
53
54    /// Not equal comparison (!=)
55    Ne {
56        left: Box<Expression>,
57        right: Box<Expression>,
58    },
59
60    /// Less than comparison (<)
61    Lt {
62        left: Box<Expression>,
63        right: Box<Expression>,
64    },
65
66    /// Less than or equal comparison (<=)
67    Le {
68        left: Box<Expression>,
69        right: Box<Expression>,
70    },
71
72    /// Greater than comparison (>)
73    Gt {
74        left: Box<Expression>,
75        right: Box<Expression>,
76    },
77
78    /// Greater than or equal comparison (>=)
79    Ge {
80        left: Box<Expression>,
81        right: Box<Expression>,
82    },
83
84    // ===== Logical Operations =====
85    /// Logical AND (&&)
86    And {
87        left: Box<Expression>,
88        right: Box<Expression>,
89    },
90
91    /// Logical OR (||)
92    Or {
93        left: Box<Expression>,
94        right: Box<Expression>,
95    },
96
97    /// Logical NOT (!)
98    Not { operand: Box<Expression> },
99
100    /// Null check (is null)
101    IsNull { operand: Box<Expression> },
102
103    /// Not-null check (is not null)
104    IsNotNull { operand: Box<Expression> },
105
106    // ===== Arithmetic Operations =====
107    /// Addition (+)
108    Add {
109        left: Box<Expression>,
110        right: Box<Expression>,
111    },
112
113    /// Subtraction (-)
114    Sub {
115        left: Box<Expression>,
116        right: Box<Expression>,
117    },
118
119    /// Multiplication (*)
120    Mul {
121        left: Box<Expression>,
122        right: Box<Expression>,
123    },
124
125    /// Division (/)
126    Div {
127        left: Box<Expression>,
128        right: Box<Expression>,
129    },
130
131    // ===== List Operations =====
132    /// Check if a list contains an element
133    /// Example: list.contains(element)
134    ListContains {
135        list: Box<Expression>,
136        element: Box<Expression>,
137    },
138
139    // ===== Host Function Calls =====
140    /// Call a host-provided function
141    /// Example: hstringEquals(left, right)
142    HostCall {
143        function_name: String,
144        args: Vec<Expression>,
145    },
146
147    // ===== Conditional =====
148    /// If-then-else conditional expression
149    /// Example: if condition { then_branch } else { else_branch }
150    IfThenElse {
151        condition: Box<Expression>,
152        then_branch: Box<Expression>,
153        else_branch: Box<Expression>,
154    },
155}
156
157#[cfg(test)]
158mod tests {
159    use super::*;
160
161    #[test]
162    fn test_int_literal() {
163        let expr = Expression::IntLiteral { value: 42 };
164        assert_eq!(expr, Expression::IntLiteral { value: 42 });
165    }
166
167    #[test]
168    fn test_bool_literal() {
169        let expr = Expression::BoolLiteral { value: true };
170        assert_eq!(expr, Expression::BoolLiteral { value: true });
171    }
172
173    #[test]
174    fn test_null() {
175        let expr = Expression::Null;
176        assert_eq!(expr, Expression::Null);
177    }
178
179    #[test]
180    fn test_param() {
181        let expr = Expression::Param { index: 0 };
182        assert_eq!(expr, Expression::Param { index: 0 });
183    }
184
185    #[test]
186    fn test_field_access() {
187        let expr = Expression::FieldAccess {
188            object: Box::new(Expression::Param { index: 0 }),
189            class_name: "Employee".into(),
190            field_name: "name".into(),
191        };
192
193        match expr {
194            Expression::FieldAccess {
195                object,
196                class_name,
197                field_name,
198            } => {
199                assert_eq!(class_name, "Employee");
200                assert_eq!(field_name, "name");
201                assert_eq!(*object, Expression::Param { index: 0 });
202            }
203            _ => panic!("Expected FieldAccess"),
204        }
205    }
206
207    #[test]
208    fn test_comparison_eq() {
209        let expr = Expression::Eq {
210            left: Box::new(Expression::IntLiteral { value: 1 }),
211            right: Box::new(Expression::IntLiteral { value: 2 }),
212        };
213
214        match expr {
215            Expression::Eq { left, right } => {
216                assert_eq!(*left, Expression::IntLiteral { value: 1 });
217                assert_eq!(*right, Expression::IntLiteral { value: 2 });
218            }
219            _ => panic!("Expected Eq"),
220        }
221    }
222
223    #[test]
224    fn test_serialize_int_literal() {
225        let expr = Expression::IntLiteral { value: 42 };
226        let json = serde_json::to_string(&expr).unwrap();
227        assert!(json.contains("\"kind\":\"IntLiteral\""));
228        assert!(json.contains("\"value\":42"));
229    }
230
231    #[test]
232    fn test_deserialize_int_literal() {
233        let json = r#"{"kind":"IntLiteral","value":42}"#;
234        let expr: Expression = serde_json::from_str(json).unwrap();
235        assert_eq!(expr, Expression::IntLiteral { value: 42 });
236    }
237
238    #[test]
239    fn test_serialize_field_access() {
240        let expr = Expression::FieldAccess {
241            object: Box::new(Expression::Param { index: 0 }),
242            class_name: "Employee".into(),
243            field_name: "name".into(),
244        };
245
246        let json = serde_json::to_string(&expr).unwrap();
247        let deserialized: Expression = serde_json::from_str(&json).unwrap();
248        assert_eq!(expr, deserialized);
249    }
250
251    #[test]
252    fn test_complex_expression() {
253        // Build: param(0).employee != null
254        let expr = Expression::Ne {
255            left: Box::new(Expression::FieldAccess {
256                object: Box::new(Expression::Param { index: 0 }),
257                class_name: "Shift".into(),
258                field_name: "employee".into(),
259            }),
260            right: Box::new(Expression::Null),
261        };
262
263        // Serialize and deserialize
264        let json = serde_json::to_string(&expr).unwrap();
265        let deserialized: Expression = serde_json::from_str(&json).unwrap();
266        assert_eq!(expr, deserialized);
267    }
268
269    // ===== Logical Operations Tests =====
270
271    #[test]
272    fn test_logical_and() {
273        let expr = Expression::And {
274            left: Box::new(Expression::BoolLiteral { value: true }),
275            right: Box::new(Expression::BoolLiteral { value: false }),
276        };
277
278        match expr {
279            Expression::And { left, right } => {
280                assert_eq!(*left, Expression::BoolLiteral { value: true });
281                assert_eq!(*right, Expression::BoolLiteral { value: false });
282            }
283            _ => panic!("Expected And"),
284        }
285    }
286
287    #[test]
288    fn test_logical_or() {
289        let expr = Expression::Or {
290            left: Box::new(Expression::BoolLiteral { value: true }),
291            right: Box::new(Expression::BoolLiteral { value: false }),
292        };
293
294        match expr {
295            Expression::Or { left, right } => {
296                assert_eq!(*left, Expression::BoolLiteral { value: true });
297                assert_eq!(*right, Expression::BoolLiteral { value: false });
298            }
299            _ => panic!("Expected Or"),
300        }
301    }
302
303    #[test]
304    fn test_logical_not() {
305        let expr = Expression::Not {
306            operand: Box::new(Expression::BoolLiteral { value: true }),
307        };
308
309        match expr {
310            Expression::Not { operand } => {
311                assert_eq!(*operand, Expression::BoolLiteral { value: true });
312            }
313            _ => panic!("Expected Not"),
314        }
315    }
316
317    #[test]
318    fn test_is_null() {
319        let expr = Expression::IsNull {
320            operand: Box::new(Expression::Param { index: 0 }),
321        };
322
323        match expr {
324            Expression::IsNull { operand } => {
325                assert_eq!(*operand, Expression::Param { index: 0 });
326            }
327            _ => panic!("Expected IsNull"),
328        }
329    }
330
331    #[test]
332    fn test_is_not_null() {
333        let expr = Expression::IsNotNull {
334            operand: Box::new(Expression::Param { index: 0 }),
335        };
336
337        match expr {
338            Expression::IsNotNull { operand } => {
339                assert_eq!(*operand, Expression::Param { index: 0 });
340            }
341            _ => panic!("Expected IsNotNull"),
342        }
343    }
344
345    #[test]
346    fn test_serialize_logical_and() {
347        let expr = Expression::And {
348            left: Box::new(Expression::BoolLiteral { value: true }),
349            right: Box::new(Expression::BoolLiteral { value: false }),
350        };
351
352        let json = serde_json::to_string(&expr).unwrap();
353        let deserialized: Expression = serde_json::from_str(&json).unwrap();
354        assert_eq!(expr, deserialized);
355    }
356
357    // ===== Arithmetic Operations Tests =====
358
359    #[test]
360    fn test_arithmetic_add() {
361        let expr = Expression::Add {
362            left: Box::new(Expression::IntLiteral { value: 10 }),
363            right: Box::new(Expression::IntLiteral { value: 20 }),
364        };
365
366        match expr {
367            Expression::Add { left, right } => {
368                assert_eq!(*left, Expression::IntLiteral { value: 10 });
369                assert_eq!(*right, Expression::IntLiteral { value: 20 });
370            }
371            _ => panic!("Expected Add"),
372        }
373    }
374
375    #[test]
376    fn test_arithmetic_sub() {
377        let expr = Expression::Sub {
378            left: Box::new(Expression::IntLiteral { value: 30 }),
379            right: Box::new(Expression::IntLiteral { value: 10 }),
380        };
381
382        match expr {
383            Expression::Sub { left, right } => {
384                assert_eq!(*left, Expression::IntLiteral { value: 30 });
385                assert_eq!(*right, Expression::IntLiteral { value: 10 });
386            }
387            _ => panic!("Expected Sub"),
388        }
389    }
390
391    #[test]
392    fn test_arithmetic_mul() {
393        let expr = Expression::Mul {
394            left: Box::new(Expression::IntLiteral { value: 5 }),
395            right: Box::new(Expression::IntLiteral { value: 3 }),
396        };
397
398        match expr {
399            Expression::Mul { left, right } => {
400                assert_eq!(*left, Expression::IntLiteral { value: 5 });
401                assert_eq!(*right, Expression::IntLiteral { value: 3 });
402            }
403            _ => panic!("Expected Mul"),
404        }
405    }
406
407    #[test]
408    fn test_arithmetic_div() {
409        let expr = Expression::Div {
410            left: Box::new(Expression::IntLiteral { value: 100 }),
411            right: Box::new(Expression::IntLiteral { value: 5 }),
412        };
413
414        match expr {
415            Expression::Div { left, right } => {
416                assert_eq!(*left, Expression::IntLiteral { value: 100 });
417                assert_eq!(*right, Expression::IntLiteral { value: 5 });
418            }
419            _ => panic!("Expected Div"),
420        }
421    }
422
423    #[test]
424    fn test_serialize_arithmetic() {
425        let expr = Expression::Add {
426            left: Box::new(Expression::IntLiteral { value: 10 }),
427            right: Box::new(Expression::IntLiteral { value: 20 }),
428        };
429
430        let json = serde_json::to_string(&expr).unwrap();
431        let deserialized: Expression = serde_json::from_str(&json).unwrap();
432        assert_eq!(expr, deserialized);
433    }
434
435    #[test]
436    fn test_complex_logical_expression() {
437        // Build: (param(0).employee != null) && (param(0).skill == "Java")
438        let expr = Expression::And {
439            left: Box::new(Expression::IsNotNull {
440                operand: Box::new(Expression::FieldAccess {
441                    object: Box::new(Expression::Param { index: 0 }),
442                    class_name: "Shift".into(),
443                    field_name: "employee".into(),
444                }),
445            }),
446            right: Box::new(Expression::Eq {
447                left: Box::new(Expression::FieldAccess {
448                    object: Box::new(Expression::Param { index: 0 }),
449                    class_name: "Employee".into(),
450                    field_name: "skill".into(),
451                }),
452                right: Box::new(Expression::IntLiteral { value: 42 }), // Placeholder
453            }),
454        };
455
456        // Serialize and deserialize
457        let json = serde_json::to_string(&expr).unwrap();
458        let deserialized: Expression = serde_json::from_str(&json).unwrap();
459        assert_eq!(expr, deserialized);
460    }
461
462    #[test]
463    fn test_time_calculation_expression() {
464        // Build: (shift.start / 24) to calculate day from hour
465        let expr = Expression::Div {
466            left: Box::new(Expression::FieldAccess {
467                object: Box::new(Expression::Param { index: 0 }),
468                class_name: "Shift".into(),
469                field_name: "start".into(),
470            }),
471            right: Box::new(Expression::IntLiteral { value: 24 }),
472        };
473
474        // Serialize and deserialize
475        let json = serde_json::to_string(&expr).unwrap();
476        let deserialized: Expression = serde_json::from_str(&json).unwrap();
477        assert_eq!(expr, deserialized);
478    }
479
480    // ===== Host Function Call Tests =====
481
482    #[test]
483    fn test_host_call() {
484        let expr = Expression::HostCall {
485            function_name: "hstringEquals".into(),
486            args: vec![
487                Expression::FieldAccess {
488                    object: Box::new(Expression::Param { index: 0 }),
489                    class_name: "Employee".into(),
490                    field_name: "skill".into(),
491                },
492                Expression::FieldAccess {
493                    object: Box::new(Expression::Param { index: 1 }),
494                    class_name: "Shift".into(),
495                    field_name: "requiredSkill".into(),
496                },
497            ],
498        };
499
500        match expr {
501            Expression::HostCall {
502                function_name,
503                args,
504            } => {
505                assert_eq!(function_name, "hstringEquals");
506                assert_eq!(args.len(), 2);
507            }
508            _ => panic!("Expected HostCall"),
509        }
510    }
511
512    #[test]
513    fn test_serialize_host_call() {
514        let expr = Expression::HostCall {
515            function_name: "hstringEquals".into(),
516            args: vec![
517                Expression::IntLiteral { value: 1 },
518                Expression::IntLiteral { value: 2 },
519            ],
520        };
521
522        let json = serde_json::to_string(&expr).unwrap();
523        let deserialized: Expression = serde_json::from_str(&json).unwrap();
524        assert_eq!(expr, deserialized);
525    }
526
527    #[test]
528    fn test_host_call_with_no_args() {
529        let expr = Expression::HostCall {
530            function_name: "hnewList".into(),
531            args: vec![],
532        };
533
534        let json = serde_json::to_string(&expr).unwrap();
535        let deserialized: Expression = serde_json::from_str(&json).unwrap();
536        assert_eq!(expr, deserialized);
537    }
538
539    #[test]
540    fn test_complex_host_call_expression() {
541        // Build: hstringEquals(employee.skill, shift.requiredSkill)
542        // nested in a logical expression: employee != null && hstringEquals(...)
543        let expr = Expression::And {
544            left: Box::new(Expression::IsNotNull {
545                operand: Box::new(Expression::FieldAccess {
546                    object: Box::new(Expression::Param { index: 0 }),
547                    class_name: "Shift".into(),
548                    field_name: "employee".into(),
549                }),
550            }),
551            right: Box::new(Expression::HostCall {
552                function_name: "hstringEquals".into(),
553                args: vec![
554                    Expression::FieldAccess {
555                        object: Box::new(Expression::Param { index: 0 }),
556                        class_name: "Employee".into(),
557                        field_name: "skill".into(),
558                    },
559                    Expression::FieldAccess {
560                        object: Box::new(Expression::Param { index: 1 }),
561                        class_name: "Shift".into(),
562                        field_name: "requiredSkill".into(),
563                    },
564                ],
565            }),
566        };
567
568        // Serialize and deserialize
569        let json = serde_json::to_string(&expr).unwrap();
570        let deserialized: Expression = serde_json::from_str(&json).unwrap();
571        assert_eq!(expr, deserialized);
572    }
573
574    #[test]
575    fn test_list_contains() {
576        let expr = Expression::ListContains {
577            list: Box::new(Expression::FieldAccess {
578                object: Box::new(Expression::Param { index: 0 }),
579                class_name: "Employee".into(),
580                field_name: "skills".into(),
581            }),
582            element: Box::new(Expression::FieldAccess {
583                object: Box::new(Expression::Param { index: 1 }),
584                class_name: "Shift".into(),
585                field_name: "requiredSkill".into(),
586            }),
587        };
588
589        match &expr {
590            Expression::ListContains { list, element } => {
591                assert!(matches!(
592                    **list,
593                    Expression::FieldAccess {
594                        field_name: ref name,
595                        ..
596                    } if name == "skills"
597                ));
598                assert!(matches!(
599                    **element,
600                    Expression::FieldAccess {
601                        field_name: ref name,
602                        ..
603                    } if name == "requiredSkill"
604                ));
605            }
606            _ => panic!("Expected ListContains expression"),
607        }
608
609        // Serialize and deserialize
610        let json = serde_json::to_string(&expr).unwrap();
611        let deserialized: Expression = serde_json::from_str(&json).unwrap();
612        assert_eq!(expr, deserialized);
613    }
614}