solverforge_core/wasm/expression/
builder.rs

1use super::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 (compiles to i32)
24    pub fn int(value: i64) -> Expression {
25        Expression::IntLiteral { value }
26    }
27
28    /// Create a 64-bit integer literal (compiles directly to i64)
29    /// Use for datetime arithmetic and other i64 operations.
30    pub fn int64(value: i64) -> Expression {
31        Expression::Int64Literal { value }
32    }
33
34    /// Create a boolean literal
35    pub fn bool(value: bool) -> Expression {
36        Expression::BoolLiteral { value }
37    }
38
39    /// Create a null literal
40    pub fn null() -> Expression {
41        Expression::Null
42    }
43
44    // ===== Parameter Access =====
45
46    /// Access a function parameter by index
47    pub fn param(index: u32) -> Expression {
48        Expression::Param { index }
49    }
50
51    // ===== Comparisons =====
52
53    /// Equal comparison (==)
54    pub fn eq(left: Expression, right: Expression) -> Expression {
55        Expression::Eq {
56            left: Box::new(left),
57            right: Box::new(right),
58        }
59    }
60
61    /// Not equal comparison (!=)
62    pub fn ne(left: Expression, right: Expression) -> Expression {
63        Expression::Ne {
64            left: Box::new(left),
65            right: Box::new(right),
66        }
67    }
68
69    /// Less than comparison (<)
70    pub fn lt(left: Expression, right: Expression) -> Expression {
71        Expression::Lt {
72            left: Box::new(left),
73            right: Box::new(right),
74        }
75    }
76
77    /// Less than or equal comparison (<=)
78    pub fn le(left: Expression, right: Expression) -> Expression {
79        Expression::Le {
80            left: Box::new(left),
81            right: Box::new(right),
82        }
83    }
84
85    /// Greater than comparison (>)
86    pub fn gt(left: Expression, right: Expression) -> Expression {
87        Expression::Gt {
88            left: Box::new(left),
89            right: Box::new(right),
90        }
91    }
92
93    /// Greater than or equal comparison (>=)
94    pub fn ge(left: Expression, right: Expression) -> Expression {
95        Expression::Ge {
96            left: Box::new(left),
97            right: Box::new(right),
98        }
99    }
100
101    // ===== i64 Comparisons =====
102
103    /// Equal comparison for i64 (==)
104    pub fn eq64(left: Expression, right: Expression) -> Expression {
105        Expression::Eq64 {
106            left: Box::new(left),
107            right: Box::new(right),
108        }
109    }
110
111    /// Not equal comparison for i64 (!=)
112    pub fn ne64(left: Expression, right: Expression) -> Expression {
113        Expression::Ne64 {
114            left: Box::new(left),
115            right: Box::new(right),
116        }
117    }
118
119    /// Less than comparison for i64 (<)
120    pub fn lt64(left: Expression, right: Expression) -> Expression {
121        Expression::Lt64 {
122            left: Box::new(left),
123            right: Box::new(right),
124        }
125    }
126
127    /// Less than or equal comparison for i64 (<=)
128    pub fn le64(left: Expression, right: Expression) -> Expression {
129        Expression::Le64 {
130            left: Box::new(left),
131            right: Box::new(right),
132        }
133    }
134
135    /// Greater than comparison for i64 (>)
136    pub fn gt64(left: Expression, right: Expression) -> Expression {
137        Expression::Gt64 {
138            left: Box::new(left),
139            right: Box::new(right),
140        }
141    }
142
143    /// Greater than or equal comparison for i64 (>=)
144    pub fn ge64(left: Expression, right: Expression) -> Expression {
145        Expression::Ge64 {
146            left: Box::new(left),
147            right: Box::new(right),
148        }
149    }
150
151    // ===== Logical Operations =====
152
153    /// Logical AND (&&)
154    pub fn and(left: Expression, right: Expression) -> Expression {
155        Expression::And {
156            left: Box::new(left),
157            right: Box::new(right),
158        }
159    }
160
161    /// Logical OR (||)
162    pub fn or(left: Expression, right: Expression) -> Expression {
163        Expression::Or {
164            left: Box::new(left),
165            right: Box::new(right),
166        }
167    }
168
169    /// Logical NOT (!)
170    pub fn not(operand: Expression) -> Expression {
171        Expression::Not {
172            operand: Box::new(operand),
173        }
174    }
175
176    /// Null check (is null)
177    pub fn is_null(operand: Expression) -> Expression {
178        Expression::IsNull {
179            operand: Box::new(operand),
180        }
181    }
182
183    /// Not-null check (is not null)
184    pub fn is_not_null(operand: Expression) -> Expression {
185        Expression::IsNotNull {
186            operand: Box::new(operand),
187        }
188    }
189
190    // ===== Arithmetic Operations =====
191
192    /// Addition (+)
193    pub fn add(left: Expression, right: Expression) -> Expression {
194        Expression::Add {
195            left: Box::new(left),
196            right: Box::new(right),
197        }
198    }
199
200    /// Subtraction (-)
201    pub fn sub(left: Expression, right: Expression) -> Expression {
202        Expression::Sub {
203            left: Box::new(left),
204            right: Box::new(right),
205        }
206    }
207
208    /// Multiplication (*)
209    pub fn mul(left: Expression, right: Expression) -> Expression {
210        Expression::Mul {
211            left: Box::new(left),
212            right: Box::new(right),
213        }
214    }
215
216    /// Division (/)
217    pub fn div(left: Expression, right: Expression) -> Expression {
218        Expression::Div {
219            left: Box::new(left),
220            right: Box::new(right),
221        }
222    }
223
224    // ===== i64 Arithmetic Operations =====
225
226    /// Addition for i64 (+)
227    pub fn add64(left: Expression, right: Expression) -> Expression {
228        Expression::Add64 {
229            left: Box::new(left),
230            right: Box::new(right),
231        }
232    }
233
234    /// Subtraction for i64 (-)
235    pub fn sub64(left: Expression, right: Expression) -> Expression {
236        Expression::Sub64 {
237            left: Box::new(left),
238            right: Box::new(right),
239        }
240    }
241
242    /// Multiplication for i64 (*)
243    pub fn mul64(left: Expression, right: Expression) -> Expression {
244        Expression::Mul64 {
245            left: Box::new(left),
246            right: Box::new(right),
247        }
248    }
249
250    /// Division for i64 (/)
251    pub fn div64(left: Expression, right: Expression) -> Expression {
252        Expression::Div64 {
253            left: Box::new(left),
254            right: Box::new(right),
255        }
256    }
257
258    // ===== Host Function Calls =====
259
260    /// Call a host function
261    pub fn host_call(function_name: impl Into<String>, args: Vec<Expression>) -> Expression {
262        Expression::HostCall {
263            function_name: function_name.into(),
264            args,
265        }
266    }
267
268    // ===== List Operations =====
269
270    /// Check if a list contains an element
271    ///
272    /// Generates a loop that iterates through the list and checks for equality.
273    /// Returns true if the element is found, false otherwise.
274    pub fn list_contains(list: Expression, element: Expression) -> Expression {
275        Expression::ListContains {
276            list: Box::new(list),
277            element: Box::new(element),
278        }
279    }
280
281    // ===== Convenience Methods =====
282
283    /// String equality via host function
284    ///
285    /// Equivalent to: hstringEquals(left, right)
286    pub fn string_equals(left: Expression, right: Expression) -> Expression {
287        Self::host_call("hstringEquals", vec![left, right])
288    }
289
290    /// Check if two time ranges overlap (i32 version)
291    ///
292    /// Equivalent to: start1 < end2 && start2 < end1
293    pub fn ranges_overlap(
294        start1: Expression,
295        end1: Expression,
296        start2: Expression,
297        end2: Expression,
298    ) -> Expression {
299        Self::and(Self::lt(start1, end2), Self::lt(start2, end1))
300    }
301
302    /// Check if two time ranges overlap (i64 version for datetime)
303    ///
304    /// Equivalent to: start1 < end2 && start2 < end1
305    /// Use this for datetime fields stored as i64.
306    pub fn ranges_overlap64(
307        start1: Expression,
308        end1: Expression,
309        start2: Expression,
310        end2: Expression,
311    ) -> Expression {
312        Self::and(Self::lt64(start1, end2), Self::lt64(start2, end1))
313    }
314
315    // ===== Conditional =====
316
317    /// If-then-else conditional expression (produces i32)
318    pub fn if_then_else(
319        condition: Expression,
320        then_branch: Expression,
321        else_branch: Expression,
322    ) -> Expression {
323        Expression::IfThenElse {
324            condition: Box::new(condition),
325            then_branch: Box::new(then_branch),
326            else_branch: Box::new(else_branch),
327        }
328    }
329
330    /// If-then-else conditional expression (produces i64)
331    /// Use for datetime arithmetic where branches return i64 values.
332    pub fn if_then_else64(
333        condition: Expression,
334        then_branch: Expression,
335        else_branch: Expression,
336    ) -> Expression {
337        Expression::IfThenElse64 {
338            condition: Box::new(condition),
339            then_branch: Box::new(then_branch),
340            else_branch: Box::new(else_branch),
341        }
342    }
343
344    // ===== Type Conversions =====
345
346    /// Wrap i64 to i32 (truncate)
347    /// Use to convert i64 arithmetic results to i32 for final comparisons.
348    pub fn i64_to_i32(operand: Expression) -> Expression {
349        Expression::I64ToI32 {
350            operand: Box::new(operand),
351        }
352    }
353
354    /// Extend i32 to i64 (signed)
355    /// Use to convert i32 values (like date epoch days) to i64 for datetime arithmetic.
356    pub fn i32_to_i64(operand: Expression) -> Expression {
357        Expression::I32ToI64 {
358            operand: Box::new(operand),
359        }
360    }
361}
362
363/// Extension trait for chaining field access
364///
365/// Allows for fluent syntax like: `Expr::param(0).get("Class", "field")`
366pub trait FieldAccessExt {
367    /// Access a field on this expression
368    fn get(self, class_name: &str, field_name: &str) -> Expression;
369}
370
371impl FieldAccessExt for Expression {
372    fn get(self, class_name: &str, field_name: &str) -> Expression {
373        Expression::FieldAccess {
374            object: Box::new(self),
375            class_name: class_name.into(),
376            field_name: field_name.into(),
377            field_type: super::WasmFieldType::Object,
378        }
379    }
380}
381
382#[cfg(test)]
383mod tests {
384    use super::*;
385
386    #[test]
387    fn test_int_literal() {
388        let expr = Expr::int(42);
389        assert_eq!(expr, Expression::IntLiteral { value: 42 });
390    }
391
392    #[test]
393    fn test_bool_literal() {
394        let expr = Expr::bool(true);
395        assert_eq!(expr, Expression::BoolLiteral { value: true });
396    }
397
398    #[test]
399    fn test_null() {
400        let expr = Expr::null();
401        assert_eq!(expr, Expression::Null);
402    }
403
404    #[test]
405    fn test_param() {
406        let expr = Expr::param(0);
407        assert_eq!(expr, Expression::Param { index: 0 });
408    }
409
410    #[test]
411    fn test_field_access_chaining() {
412        let expr = Expr::param(0).get("Employee", "name");
413
414        match expr {
415            Expression::FieldAccess {
416                object,
417                class_name,
418                field_name,
419                ..
420            } => {
421                assert_eq!(class_name, "Employee");
422                assert_eq!(field_name, "name");
423                assert_eq!(*object, Expression::Param { index: 0 });
424            }
425            _ => panic!("Expected FieldAccess"),
426        }
427    }
428
429    #[test]
430    fn test_eq() {
431        let expr = Expr::eq(Expr::int(1), Expr::int(2));
432
433        match expr {
434            Expression::Eq { left, right } => {
435                assert_eq!(*left, Expression::IntLiteral { value: 1 });
436                assert_eq!(*right, Expression::IntLiteral { value: 2 });
437            }
438            _ => panic!("Expected Eq"),
439        }
440    }
441
442    #[test]
443    fn test_and() {
444        let expr = Expr::and(Expr::bool(true), Expr::bool(false));
445
446        match expr {
447            Expression::And { left, right } => {
448                assert_eq!(*left, Expression::BoolLiteral { value: true });
449                assert_eq!(*right, Expression::BoolLiteral { value: false });
450            }
451            _ => panic!("Expected And"),
452        }
453    }
454
455    #[test]
456    fn test_is_not_null() {
457        let expr = Expr::is_not_null(Expr::param(0));
458
459        match expr {
460            Expression::IsNotNull { operand } => {
461                assert_eq!(*operand, Expression::Param { index: 0 });
462            }
463            _ => panic!("Expected IsNotNull"),
464        }
465    }
466
467    #[test]
468    fn test_add() {
469        let expr = Expr::add(Expr::int(10), Expr::int(20));
470
471        match expr {
472            Expression::Add { left, right } => {
473                assert_eq!(*left, Expression::IntLiteral { value: 10 });
474                assert_eq!(*right, Expression::IntLiteral { value: 20 });
475            }
476            _ => panic!("Expected Add"),
477        }
478    }
479
480    #[test]
481    fn test_host_call() {
482        let expr = Expr::host_call("test_func", vec![Expr::int(1), Expr::int(2)]);
483
484        match expr {
485            Expression::HostCall {
486                function_name,
487                args,
488            } => {
489                assert_eq!(function_name, "test_func");
490                assert_eq!(args.len(), 2);
491            }
492            _ => panic!("Expected HostCall"),
493        }
494    }
495
496    #[test]
497    fn test_string_equals() {
498        let left = Expr::param(0).get("Employee", "skill");
499        let right = Expr::param(1).get("Shift", "requiredSkill");
500        let expr = Expr::string_equals(left, right);
501
502        match expr {
503            Expression::HostCall {
504                function_name,
505                args,
506            } => {
507                assert_eq!(function_name, "hstringEquals");
508                assert_eq!(args.len(), 2);
509            }
510            _ => panic!("Expected HostCall"),
511        }
512    }
513
514    #[test]
515    fn test_ranges_overlap() {
516        let expr = Expr::ranges_overlap(Expr::int(0), Expr::int(10), Expr::int(5), Expr::int(15));
517
518        match expr {
519            Expression::And { left, right } => {
520                // start1 < end2
521                match *left {
522                    Expression::Lt { .. } => {}
523                    _ => panic!("Expected Lt in left side"),
524                }
525                // start2 < end1
526                match *right {
527                    Expression::Lt { .. } => {}
528                    _ => panic!("Expected Lt in right side"),
529                }
530            }
531            _ => panic!("Expected And"),
532        }
533    }
534
535    #[test]
536    fn test_complex_predicate_builder() {
537        // Build: param(0).employee != null && hstringEquals(employee.skill, shift.requiredSkill)
538        let shift = Expr::param(0);
539        let employee = shift.clone().get("Shift", "employee");
540
541        let predicate = Expr::and(
542            Expr::is_not_null(employee.clone()),
543            Expr::not(Expr::string_equals(
544                employee.get("Employee", "skill"),
545                shift.get("Shift", "requiredSkill"),
546            )),
547        );
548
549        // Should be parseable
550        match predicate {
551            Expression::And { .. } => {}
552            _ => panic!("Expected And at top level"),
553        }
554    }
555
556    #[test]
557    fn test_nested_field_access() {
558        // Build: param(0).shift.employee.name
559        let expr = Expr::param(0)
560            .get("Assignment", "shift")
561            .get("Shift", "employee")
562            .get("Employee", "name");
563
564        match expr {
565            Expression::FieldAccess {
566                class_name,
567                field_name,
568                object,
569                ..
570            } => {
571                assert_eq!(class_name, "Employee");
572                assert_eq!(field_name, "name");
573
574                match *object {
575                    Expression::FieldAccess { .. } => {}
576                    _ => panic!("Expected nested FieldAccess"),
577                }
578            }
579            _ => panic!("Expected FieldAccess"),
580        }
581    }
582
583    #[test]
584    fn test_time_calculation() {
585        // Build: (shift.start / 24)
586        let expr = Expr::div(Expr::param(0).get("Shift", "start"), Expr::int(24));
587
588        match expr {
589            Expression::Div { left, right } => {
590                match *left {
591                    Expression::FieldAccess { .. } => {}
592                    _ => panic!("Expected FieldAccess"),
593                }
594                assert_eq!(*right, Expression::IntLiteral { value: 24 });
595            }
596            _ => panic!("Expected Div"),
597        }
598    }
599
600    #[test]
601    fn test_if_then_else() {
602        // Build: if x > 0 { 1 } else { 0 }
603        let expr = Expr::if_then_else(
604            Expr::gt(Expr::param(0), Expr::int(0)),
605            Expr::int(1),
606            Expr::int(0),
607        );
608
609        match expr {
610            Expression::IfThenElse {
611                condition,
612                then_branch,
613                else_branch,
614            } => {
615                match *condition {
616                    Expression::Gt { .. } => {}
617                    _ => panic!("Expected Gt"),
618                }
619                assert_eq!(*then_branch, Expression::IntLiteral { value: 1 });
620                assert_eq!(*else_branch, Expression::IntLiteral { value: 0 });
621            }
622            _ => panic!("Expected IfThenElse"),
623        }
624    }
625
626    #[test]
627    fn test_nested_if_then_else() {
628        // Build: if x > 0 { if x > 10 { 2 } else { 1 } } else { 0 }
629        let expr = Expr::if_then_else(
630            Expr::gt(Expr::param(0), Expr::int(0)),
631            Expr::if_then_else(
632                Expr::gt(Expr::param(0), Expr::int(10)),
633                Expr::int(2),
634                Expr::int(1),
635            ),
636            Expr::int(0),
637        );
638
639        match expr {
640            Expression::IfThenElse { then_branch, .. } => match *then_branch {
641                Expression::IfThenElse { .. } => {}
642                _ => panic!("Expected nested IfThenElse"),
643            },
644            _ => panic!("Expected IfThenElse"),
645        }
646    }
647
648    #[test]
649    fn test_list_contains() {
650        let list = Expr::param(0).get("Employee", "skills");
651        let element = Expr::param(1).get("Shift", "requiredSkill");
652        let expr = Expr::list_contains(list, element);
653
654        match expr {
655            Expression::ListContains { list, element } => {
656                match *list {
657                    Expression::FieldAccess { field_name, .. } => {
658                        assert_eq!(field_name, "skills");
659                    }
660                    _ => panic!("Expected FieldAccess for list"),
661                }
662                match *element {
663                    Expression::FieldAccess { field_name, .. } => {
664                        assert_eq!(field_name, "requiredSkill");
665                    }
666                    _ => panic!("Expected FieldAccess for element"),
667                }
668            }
669            _ => panic!("Expected ListContains"),
670        }
671    }
672}