solverforge_core/wasm/
expr_builder.rs

1use crate::wasm::Expression;
2
3/// Fluent builder for constructing expression trees
4///
5/// Provides a convenient API for building complex expressions without
6/// excessive nesting of Box::new() calls.
7///
8/// # Example
9/// ```
10/// use solverforge_core::wasm::{Expression, Expr};
11/// use solverforge_core::wasm::FieldAccessExt;
12///
13/// // Build: param(0).employee != null
14/// let shift = Expr::param(0);
15/// let employee = shift.get("Shift", "employee");
16/// let predicate = Expr::is_not_null(employee);
17/// ```
18pub struct Expr;
19
20impl Expr {
21    // ===== Literals =====
22
23    /// Create an integer literal
24    pub fn int(value: i64) -> Expression {
25        Expression::IntLiteral { value }
26    }
27
28    /// Create a boolean literal
29    pub fn bool(value: bool) -> Expression {
30        Expression::BoolLiteral { value }
31    }
32
33    /// Create a null literal
34    pub fn null() -> Expression {
35        Expression::Null
36    }
37
38    // ===== Parameter Access =====
39
40    /// Access a function parameter by index
41    pub fn param(index: u32) -> Expression {
42        Expression::Param { index }
43    }
44
45    // ===== Comparisons =====
46
47    /// Equal comparison (==)
48    pub fn eq(left: Expression, right: Expression) -> Expression {
49        Expression::Eq {
50            left: Box::new(left),
51            right: Box::new(right),
52        }
53    }
54
55    /// Not equal comparison (!=)
56    pub fn ne(left: Expression, right: Expression) -> Expression {
57        Expression::Ne {
58            left: Box::new(left),
59            right: Box::new(right),
60        }
61    }
62
63    /// Less than comparison (<)
64    pub fn lt(left: Expression, right: Expression) -> Expression {
65        Expression::Lt {
66            left: Box::new(left),
67            right: Box::new(right),
68        }
69    }
70
71    /// Less than or equal comparison (<=)
72    pub fn le(left: Expression, right: Expression) -> Expression {
73        Expression::Le {
74            left: Box::new(left),
75            right: Box::new(right),
76        }
77    }
78
79    /// Greater than comparison (>)
80    pub fn gt(left: Expression, right: Expression) -> Expression {
81        Expression::Gt {
82            left: Box::new(left),
83            right: Box::new(right),
84        }
85    }
86
87    /// Greater than or equal comparison (>=)
88    pub fn ge(left: Expression, right: Expression) -> Expression {
89        Expression::Ge {
90            left: Box::new(left),
91            right: Box::new(right),
92        }
93    }
94
95    // ===== Logical Operations =====
96
97    /// Logical AND (&&)
98    pub fn and(left: Expression, right: Expression) -> Expression {
99        Expression::And {
100            left: Box::new(left),
101            right: Box::new(right),
102        }
103    }
104
105    /// Logical OR (||)
106    pub fn or(left: Expression, right: Expression) -> Expression {
107        Expression::Or {
108            left: Box::new(left),
109            right: Box::new(right),
110        }
111    }
112
113    /// Logical NOT (!)
114    pub fn not(operand: Expression) -> Expression {
115        Expression::Not {
116            operand: Box::new(operand),
117        }
118    }
119
120    /// Null check (is null)
121    pub fn is_null(operand: Expression) -> Expression {
122        Expression::IsNull {
123            operand: Box::new(operand),
124        }
125    }
126
127    /// Not-null check (is not null)
128    pub fn is_not_null(operand: Expression) -> Expression {
129        Expression::IsNotNull {
130            operand: Box::new(operand),
131        }
132    }
133
134    // ===== Arithmetic Operations =====
135
136    /// Addition (+)
137    pub fn add(left: Expression, right: Expression) -> Expression {
138        Expression::Add {
139            left: Box::new(left),
140            right: Box::new(right),
141        }
142    }
143
144    /// Subtraction (-)
145    pub fn sub(left: Expression, right: Expression) -> Expression {
146        Expression::Sub {
147            left: Box::new(left),
148            right: Box::new(right),
149        }
150    }
151
152    /// Multiplication (*)
153    pub fn mul(left: Expression, right: Expression) -> Expression {
154        Expression::Mul {
155            left: Box::new(left),
156            right: Box::new(right),
157        }
158    }
159
160    /// Division (/)
161    pub fn div(left: Expression, right: Expression) -> Expression {
162        Expression::Div {
163            left: Box::new(left),
164            right: Box::new(right),
165        }
166    }
167
168    // ===== Host Function Calls =====
169
170    /// Call a host function
171    pub fn host_call(function_name: impl Into<String>, args: Vec<Expression>) -> Expression {
172        Expression::HostCall {
173            function_name: function_name.into(),
174            args,
175        }
176    }
177
178    // ===== List Operations =====
179
180    /// Check if a list contains an element
181    ///
182    /// Generates a loop that iterates through the list and checks for equality.
183    /// Returns true if the element is found, false otherwise.
184    pub fn list_contains(list: Expression, element: Expression) -> Expression {
185        Expression::ListContains {
186            list: Box::new(list),
187            element: Box::new(element),
188        }
189    }
190
191    // ===== Convenience Methods =====
192
193    /// String equality via host function
194    ///
195    /// Equivalent to: hstringEquals(left, right)
196    pub fn string_equals(left: Expression, right: Expression) -> Expression {
197        Self::host_call("hstringEquals", vec![left, right])
198    }
199
200    /// Check if two time ranges overlap
201    ///
202    /// Equivalent to: start1 < end2 && start2 < end1
203    pub fn ranges_overlap(
204        start1: Expression,
205        end1: Expression,
206        start2: Expression,
207        end2: Expression,
208    ) -> Expression {
209        Self::and(Self::lt(start1, end2), Self::lt(start2, end1))
210    }
211
212    // ===== Conditional =====
213
214    /// If-then-else conditional expression
215    pub fn if_then_else(
216        condition: Expression,
217        then_branch: Expression,
218        else_branch: Expression,
219    ) -> Expression {
220        Expression::IfThenElse {
221            condition: Box::new(condition),
222            then_branch: Box::new(then_branch),
223            else_branch: Box::new(else_branch),
224        }
225    }
226}
227
228/// Extension trait for chaining field access
229///
230/// Allows for fluent syntax like: `Expr::param(0).get("Class", "field")`
231pub trait FieldAccessExt {
232    /// Access a field on this expression
233    fn get(self, class_name: &str, field_name: &str) -> Expression;
234}
235
236impl FieldAccessExt for Expression {
237    fn get(self, class_name: &str, field_name: &str) -> Expression {
238        Expression::FieldAccess {
239            object: Box::new(self),
240            class_name: class_name.into(),
241            field_name: field_name.into(),
242        }
243    }
244}
245
246#[cfg(test)]
247mod tests {
248    use super::*;
249
250    #[test]
251    fn test_int_literal() {
252        let expr = Expr::int(42);
253        assert_eq!(expr, Expression::IntLiteral { value: 42 });
254    }
255
256    #[test]
257    fn test_bool_literal() {
258        let expr = Expr::bool(true);
259        assert_eq!(expr, Expression::BoolLiteral { value: true });
260    }
261
262    #[test]
263    fn test_null() {
264        let expr = Expr::null();
265        assert_eq!(expr, Expression::Null);
266    }
267
268    #[test]
269    fn test_param() {
270        let expr = Expr::param(0);
271        assert_eq!(expr, Expression::Param { index: 0 });
272    }
273
274    #[test]
275    fn test_field_access_chaining() {
276        let expr = Expr::param(0).get("Employee", "name");
277
278        match expr {
279            Expression::FieldAccess {
280                object,
281                class_name,
282                field_name,
283            } => {
284                assert_eq!(class_name, "Employee");
285                assert_eq!(field_name, "name");
286                assert_eq!(*object, Expression::Param { index: 0 });
287            }
288            _ => panic!("Expected FieldAccess"),
289        }
290    }
291
292    #[test]
293    fn test_eq() {
294        let expr = Expr::eq(Expr::int(1), Expr::int(2));
295
296        match expr {
297            Expression::Eq { left, right } => {
298                assert_eq!(*left, Expression::IntLiteral { value: 1 });
299                assert_eq!(*right, Expression::IntLiteral { value: 2 });
300            }
301            _ => panic!("Expected Eq"),
302        }
303    }
304
305    #[test]
306    fn test_and() {
307        let expr = Expr::and(Expr::bool(true), Expr::bool(false));
308
309        match expr {
310            Expression::And { left, right } => {
311                assert_eq!(*left, Expression::BoolLiteral { value: true });
312                assert_eq!(*right, Expression::BoolLiteral { value: false });
313            }
314            _ => panic!("Expected And"),
315        }
316    }
317
318    #[test]
319    fn test_is_not_null() {
320        let expr = Expr::is_not_null(Expr::param(0));
321
322        match expr {
323            Expression::IsNotNull { operand } => {
324                assert_eq!(*operand, Expression::Param { index: 0 });
325            }
326            _ => panic!("Expected IsNotNull"),
327        }
328    }
329
330    #[test]
331    fn test_add() {
332        let expr = Expr::add(Expr::int(10), Expr::int(20));
333
334        match expr {
335            Expression::Add { left, right } => {
336                assert_eq!(*left, Expression::IntLiteral { value: 10 });
337                assert_eq!(*right, Expression::IntLiteral { value: 20 });
338            }
339            _ => panic!("Expected Add"),
340        }
341    }
342
343    #[test]
344    fn test_host_call() {
345        let expr = Expr::host_call("test_func", vec![Expr::int(1), Expr::int(2)]);
346
347        match expr {
348            Expression::HostCall {
349                function_name,
350                args,
351            } => {
352                assert_eq!(function_name, "test_func");
353                assert_eq!(args.len(), 2);
354            }
355            _ => panic!("Expected HostCall"),
356        }
357    }
358
359    #[test]
360    fn test_string_equals() {
361        let left = Expr::param(0).get("Employee", "skill");
362        let right = Expr::param(1).get("Shift", "requiredSkill");
363        let expr = Expr::string_equals(left, right);
364
365        match expr {
366            Expression::HostCall {
367                function_name,
368                args,
369            } => {
370                assert_eq!(function_name, "hstringEquals");
371                assert_eq!(args.len(), 2);
372            }
373            _ => panic!("Expected HostCall"),
374        }
375    }
376
377    #[test]
378    fn test_ranges_overlap() {
379        let expr = Expr::ranges_overlap(Expr::int(0), Expr::int(10), Expr::int(5), Expr::int(15));
380
381        match expr {
382            Expression::And { left, right } => {
383                // start1 < end2
384                match *left {
385                    Expression::Lt { .. } => {}
386                    _ => panic!("Expected Lt in left side"),
387                }
388                // start2 < end1
389                match *right {
390                    Expression::Lt { .. } => {}
391                    _ => panic!("Expected Lt in right side"),
392                }
393            }
394            _ => panic!("Expected And"),
395        }
396    }
397
398    #[test]
399    fn test_complex_predicate_builder() {
400        // Build: param(0).employee != null && hstringEquals(employee.skill, shift.requiredSkill)
401        let shift = Expr::param(0);
402        let employee = shift.clone().get("Shift", "employee");
403
404        let predicate = Expr::and(
405            Expr::is_not_null(employee.clone()),
406            Expr::not(Expr::string_equals(
407                employee.get("Employee", "skill"),
408                shift.get("Shift", "requiredSkill"),
409            )),
410        );
411
412        // Should be parseable
413        match predicate {
414            Expression::And { .. } => {}
415            _ => panic!("Expected And at top level"),
416        }
417    }
418
419    #[test]
420    fn test_nested_field_access() {
421        // Build: param(0).shift.employee.name
422        let expr = Expr::param(0)
423            .get("Assignment", "shift")
424            .get("Shift", "employee")
425            .get("Employee", "name");
426
427        match expr {
428            Expression::FieldAccess {
429                class_name,
430                field_name,
431                object,
432            } => {
433                assert_eq!(class_name, "Employee");
434                assert_eq!(field_name, "name");
435
436                match *object {
437                    Expression::FieldAccess { .. } => {}
438                    _ => panic!("Expected nested FieldAccess"),
439                }
440            }
441            _ => panic!("Expected FieldAccess"),
442        }
443    }
444
445    #[test]
446    fn test_time_calculation() {
447        // Build: (shift.start / 24)
448        let expr = Expr::div(Expr::param(0).get("Shift", "start"), Expr::int(24));
449
450        match expr {
451            Expression::Div { left, right } => {
452                match *left {
453                    Expression::FieldAccess { .. } => {}
454                    _ => panic!("Expected FieldAccess"),
455                }
456                assert_eq!(*right, Expression::IntLiteral { value: 24 });
457            }
458            _ => panic!("Expected Div"),
459        }
460    }
461
462    #[test]
463    fn test_if_then_else() {
464        // Build: if x > 0 { 1 } else { 0 }
465        let expr = Expr::if_then_else(
466            Expr::gt(Expr::param(0), Expr::int(0)),
467            Expr::int(1),
468            Expr::int(0),
469        );
470
471        match expr {
472            Expression::IfThenElse {
473                condition,
474                then_branch,
475                else_branch,
476            } => {
477                match *condition {
478                    Expression::Gt { .. } => {}
479                    _ => panic!("Expected Gt"),
480                }
481                assert_eq!(*then_branch, Expression::IntLiteral { value: 1 });
482                assert_eq!(*else_branch, Expression::IntLiteral { value: 0 });
483            }
484            _ => panic!("Expected IfThenElse"),
485        }
486    }
487
488    #[test]
489    fn test_nested_if_then_else() {
490        // Build: if x > 0 { if x > 10 { 2 } else { 1 } } else { 0 }
491        let expr = Expr::if_then_else(
492            Expr::gt(Expr::param(0), Expr::int(0)),
493            Expr::if_then_else(
494                Expr::gt(Expr::param(0), Expr::int(10)),
495                Expr::int(2),
496                Expr::int(1),
497            ),
498            Expr::int(0),
499        );
500
501        match expr {
502            Expression::IfThenElse { then_branch, .. } => match *then_branch {
503                Expression::IfThenElse { .. } => {}
504                _ => panic!("Expected nested IfThenElse"),
505            },
506            _ => panic!("Expected IfThenElse"),
507        }
508    }
509
510    #[test]
511    fn test_list_contains() {
512        let list = Expr::param(0).get("Employee", "skills");
513        let element = Expr::param(1).get("Shift", "requiredSkill");
514        let expr = Expr::list_contains(list, element);
515
516        match expr {
517            Expression::ListContains { list, element } => {
518                match *list {
519                    Expression::FieldAccess { field_name, .. } => {
520                        assert_eq!(field_name, "skills");
521                    }
522                    _ => panic!("Expected FieldAccess for list"),
523                }
524                match *element {
525                    Expression::FieldAccess { field_name, .. } => {
526                        assert_eq!(field_name, "requiredSkill");
527                    }
528                    _ => panic!("Expected FieldAccess for element"),
529                }
530            }
531            _ => panic!("Expected ListContains"),
532        }
533    }
534}