Skip to main content

haystack_core/expr/
ast.rs

1//! Expression AST types for arithmetic/logic expressions over entity tags.
2
3use crate::kinds::Kind;
4
5/// Maximum nesting depth allowed when parsing expressions.
6pub const MAX_EXPR_DEPTH: usize = 64;
7
8/// Maximum source string length accepted by the parser.
9pub const MAX_EXPR_SOURCE: usize = 4096;
10
11/// Binary operators for arithmetic.
12#[derive(Debug, Clone, PartialEq)]
13pub enum BinOp {
14    Add,
15    Sub,
16    Mul,
17    Div,
18    Mod,
19}
20
21/// Unary operators.
22#[derive(Debug, Clone, PartialEq)]
23pub enum UnOp {
24    Neg,
25    Not,
26}
27
28/// Comparison operators.
29#[derive(Debug, Clone, PartialEq)]
30pub enum CmpOp {
31    Eq,
32    Ne,
33    Lt,
34    Le,
35    Gt,
36    Ge,
37}
38
39/// Logical operators.
40#[derive(Debug, Clone, PartialEq)]
41pub enum LogicOp {
42    And,
43    Or,
44}
45
46/// Expression AST node.
47#[derive(Debug, Clone)]
48pub enum ExprNode {
49    /// A literal value: `72`, `"hello"`, `true`, `null`.
50    Literal(Kind),
51    /// A variable reference: `$tagName`.
52    Variable(String),
53    /// Binary arithmetic: `left op right`.
54    BinaryOp {
55        left: Box<ExprNode>,
56        op: BinOp,
57        right: Box<ExprNode>,
58    },
59    /// Unary operation: `op operand`.
60    UnaryOp { op: UnOp, operand: Box<ExprNode> },
61    /// Comparison: `left op right`.
62    Comparison {
63        left: Box<ExprNode>,
64        op: CmpOp,
65        right: Box<ExprNode>,
66    },
67    /// Logical connective: `left op right`.
68    Logical {
69        left: Box<ExprNode>,
70        op: LogicOp,
71        right: Box<ExprNode>,
72    },
73    /// Function call: `name(args...)`.
74    FnCall { name: String, args: Vec<ExprNode> },
75    /// Conditional: `if cond then then_expr else else_expr`.
76    Conditional {
77        cond: Box<ExprNode>,
78        then_expr: Box<ExprNode>,
79        else_expr: Box<ExprNode>,
80    },
81}
82
83#[cfg(test)]
84mod tests {
85    use super::*;
86    use crate::kinds::Number;
87
88    #[test]
89    fn construct_literal() {
90        let node = ExprNode::Literal(Kind::Number(Number::unitless(42.0)));
91        assert!(matches!(node, ExprNode::Literal(Kind::Number(_))));
92    }
93
94    #[test]
95    fn construct_variable() {
96        let node = ExprNode::Variable("temp".into());
97        if let ExprNode::Variable(name) = &node {
98            assert_eq!(name, "temp");
99        } else {
100            panic!("expected Variable");
101        }
102    }
103
104    #[test]
105    fn construct_binary_op() {
106        let node = ExprNode::BinaryOp {
107            left: Box::new(ExprNode::Literal(Kind::Number(Number::unitless(1.0)))),
108            op: BinOp::Add,
109            right: Box::new(ExprNode::Literal(Kind::Number(Number::unitless(2.0)))),
110        };
111        assert!(matches!(node, ExprNode::BinaryOp { op: BinOp::Add, .. }));
112    }
113
114    #[test]
115    fn construct_unary_op() {
116        let node = ExprNode::UnaryOp {
117            op: UnOp::Neg,
118            operand: Box::new(ExprNode::Literal(Kind::Number(Number::unitless(5.0)))),
119        };
120        assert!(matches!(node, ExprNode::UnaryOp { op: UnOp::Neg, .. }));
121    }
122
123    #[test]
124    fn construct_comparison() {
125        let node = ExprNode::Comparison {
126            left: Box::new(ExprNode::Variable("x".into())),
127            op: CmpOp::Gt,
128            right: Box::new(ExprNode::Literal(Kind::Number(Number::unitless(10.0)))),
129        };
130        assert!(matches!(node, ExprNode::Comparison { op: CmpOp::Gt, .. }));
131    }
132
133    #[test]
134    fn construct_logical() {
135        let node = ExprNode::Logical {
136            left: Box::new(ExprNode::Literal(Kind::Bool(true))),
137            op: LogicOp::And,
138            right: Box::new(ExprNode::Literal(Kind::Bool(false))),
139        };
140        assert!(matches!(
141            node,
142            ExprNode::Logical {
143                op: LogicOp::And,
144                ..
145            }
146        ));
147    }
148
149    #[test]
150    fn construct_fn_call() {
151        let node = ExprNode::FnCall {
152            name: "abs".into(),
153            args: vec![ExprNode::Literal(Kind::Number(Number::unitless(-3.0)))],
154        };
155        if let ExprNode::FnCall { name, args } = &node {
156            assert_eq!(name, "abs");
157            assert_eq!(args.len(), 1);
158        } else {
159            panic!("expected FnCall");
160        }
161    }
162
163    #[test]
164    fn construct_conditional() {
165        let node = ExprNode::Conditional {
166            cond: Box::new(ExprNode::Literal(Kind::Bool(true))),
167            then_expr: Box::new(ExprNode::Literal(Kind::Number(Number::unitless(1.0)))),
168            else_expr: Box::new(ExprNode::Literal(Kind::Number(Number::unitless(0.0)))),
169        };
170        assert!(matches!(node, ExprNode::Conditional { .. }));
171    }
172
173    #[test]
174    fn clone_and_debug() {
175        let node = ExprNode::Literal(Kind::Str("test".into()));
176        let cloned = node.clone();
177        let debug = format!("{:?}", cloned);
178        assert!(debug.contains("Literal"));
179    }
180
181    #[test]
182    fn binop_variants() {
183        let ops = [BinOp::Add, BinOp::Sub, BinOp::Mul, BinOp::Div, BinOp::Mod];
184        for op in &ops {
185            assert_eq!(op.clone(), op.clone());
186        }
187    }
188
189    #[test]
190    fn cmpop_variants() {
191        let ops = [
192            CmpOp::Eq,
193            CmpOp::Ne,
194            CmpOp::Lt,
195            CmpOp::Le,
196            CmpOp::Gt,
197            CmpOp::Ge,
198        ];
199        for op in &ops {
200            assert_eq!(op.clone(), op.clone());
201        }
202    }
203}