Repository: FalkorDB/falkordb-rs-next-gen
Pull request #54: use tree ds
Author: login: AviAvni
Created: 2025-04-30T07:46:05Z | Merged: 2025-04-30T08:30:22Z



<!-- This is an auto-generated comment: release notes by coderabbit.ai -->

## Summary by CodeRabbit

- **New Features**
  - Improved internal query representation to use a tree-based structure for more flexible and composable query planning and execution.

- **Refactor**
  - Unified function and property access expressions into a generic function invocation format.
  - Simplified and streamlined the handling of string predicates and property access.
  - Updated internal planner and runtime logic to operate on tree-structured query plans.

- **Style**
  - Enhanced formatting of query plans for clearer output.

- **Chores**
  - Updated dependencies to include a new tree structure library.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->


Diff:
diff --git a/Cargo.lock b/Cargo.lock
index 817cfcef..575ae48b 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -84,9 +84,9 @@ checksum = "5c8214115b7bf84099f1309324e63141d4c5d7cc26862f97a0a857dbefe165bd"
 
 [[package]]
 name = "bytemuck"
-version = "1.22.0"
+version = "1.23.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b6b1fc10dbac614ebc03540c9dbd60e83887fda27794998c6528f1782047d540"
+checksum = "9134a6ef01ce4b366b50689c94f82c14bc72bc5d0386829828a2e2752ef7958c"
 
 [[package]]
 name = "byteorder"
@@ -203,6 +203,7 @@ name = "graph"
 version = "0.1.0"
 dependencies = [
  "falkordb-macro",
+ "orx-tree",
  "rand",
  "regex",
  "roaring",
@@ -369,6 +370,85 @@ version = "1.21.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d"
 
+[[package]]
+name = "orx-fixed-vec"
+version = "3.16.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cb192f4a087fac612f646c8f2b9ef15267498a3cfa723773be7ce21109057f07"
+dependencies = [
+ "orx-iterable",
+ "orx-pinned-vec",
+ "orx-pseudo-default",
+]
+
+[[package]]
+name = "orx-iterable"
+version = "1.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dfa2cb3f82a187c68835faac9cf03faaee70b93f4da3b85515ac1b4c6f8a432d"
+dependencies = [
+ "orx-self-or",
+]
+
+[[package]]
+name = "orx-pinned-vec"
+version = "3.16.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2707ef8045673419bec5b92a3073b0066fed6d42b1f1be07aa50201fd3cb4e92"
+dependencies = [
+ "orx-iterable",
+ "orx-pseudo-default",
+]
+
+[[package]]
+name = "orx-pseudo-default"
+version = "2.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "34eaace9ae01f7025804fbca40ec45b87c19ba0328d97195e01c6135897762a8"
+
+[[package]]
+name = "orx-self-or"
+version = "1.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "67a8e35dfe18921e475b9861266fd58a5ecfd681161f242d24a9e2d1e07fbc28"
+
+[[package]]
+name = "orx-selfref-col"
+version = "2.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "13f2cfe8f865954d889b6da4679410bb0a33146631a74d08bdffeb492ba2fd8b"
+dependencies = [
+ "orx-fixed-vec",
+ "orx-pinned-vec",
+ "orx-pseudo-default",
+ "orx-split-vec",
+]
+
+[[package]]
+name = "orx-split-vec"
+version = "3.16.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3122275d0f6f203a27b1e218c2f9db4214ccab15aa7faf99f486e97cc91e0444"
+dependencies = [
+ "orx-iterable",
+ "orx-pinned-vec",
+ "orx-pseudo-default",
+]
+
+[[package]]
+name = "orx-tree"
+version = "1.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d99f0ba3d57b3dd8780b44f50bfce18aefbef4cefe3518c5af610547e50366ad"
+dependencies = [
+ "orx-iterable",
+ "orx-pinned-vec",
+ "orx-pseudo-default",
+ "orx-self-or",
+ "orx-selfref-col",
+ "orx-split-vec",
+]
+
 [[package]]
 name = "peeking_take_while"
 version = "0.1.2"
diff --git a/graph/Cargo.toml b/graph/Cargo.toml
index d37d7b1f..7836d9b9 100644
--- a/graph/Cargo.toml
+++ b/graph/Cargo.toml
@@ -14,4 +14,5 @@ categories = ["data-structures", "database"]
 roaring = "0.10.12"
 regex = "1.11.1"
 falkordb-macro = { path = "../falkordb-macro" }
+orx-tree = "1.5.0"
 rand = "0.9.1"
diff --git a/graph/src/ast.rs b/graph/src/ast.rs
index ac5c7a5f..2fd6d3fd 100644
--- a/graph/src/ast.rs
+++ b/graph/src/ast.rs
@@ -12,8 +12,9 @@ pub enum QueryExprIR {
     String(String),
     Ident(String),
     Parameter(String),
-    Named(String, Box<QueryExprIR>),
     List(Vec<QueryExprIR>),
+    Map(BTreeMap<String, QueryExprIR>),
+    Named(String, Box<QueryExprIR>),
     Or(Vec<QueryExprIR>),
     Xor(Vec<QueryExprIR>),
     And(Vec<QueryExprIR>),
@@ -35,13 +36,7 @@ pub enum QueryExprIR {
     IsNull(Box<QueryExprIR>),
     GetElement(Box<(QueryExprIR, QueryExprIR)>),
     GetElements(Box<(QueryExprIR, Option<QueryExprIR>, Option<QueryExprIR>)>),
-    Property(Box<QueryExprIR>, String),
     FuncInvocation(String, Vec<QueryExprIR>),
-    Map(BTreeMap<String, QueryExprIR>),
-    StartsWith(Box<(QueryExprIR, QueryExprIR)>),
-    EndsWith(Box<(QueryExprIR, QueryExprIR)>),
-    Contains(Box<(QueryExprIR, QueryExprIR)>),
-    RegexMatches(Box<(QueryExprIR, QueryExprIR)>),
 }
 
 impl QueryExprIR {
@@ -98,22 +93,6 @@ impl QueryExprIR {
                 op.0.inner_validate(env)?;
                 op.1.inner_validate(env)
             }
-            Self::StartsWith(op) => {
-                op.0.inner_validate(env)?;
-                op.1.inner_validate(env)
-            }
-            Self::EndsWith(op) => {
-                op.0.inner_validate(env)?;
-                op.1.inner_validate(env)
-            }
-            Self::Contains(op) => {
-                op.0.inner_validate(env)?;
-                op.1.inner_validate(env)
-            }
-            Self::RegexMatches(op) => {
-                op.0.inner_validate(env)?;
-                op.1.inner_validate(env)
-            }
             Self::Map(exprs) => {
                 for expr in exprs.values() {
                     expr.inner_validate(env)?;
@@ -125,9 +104,7 @@ impl QueryExprIR {
                 env.insert(name.to_string());
                 Ok(())
             }
-            Self::Not(expr) | Self::IsNull(expr) | Self::Property(expr, _) | Self::Negate(expr) => {
-                expr.inner_validate(env)
-            }
+            Self::Not(expr) | Self::IsNull(expr) | Self::Negate(expr) => expr.inner_validate(env),
             Self::GetElement(op) => {
                 op.0.inner_validate(env)?;
                 op.1.inner_validate(env)
diff --git a/graph/src/cypher.rs b/graph/src/cypher.rs
index 271f3117..f42e4002 100644
--- a/graph/src/cypher.rs
+++ b/graph/src/cypher.rs
@@ -756,7 +756,10 @@ impl<'a> Parser<'a> {
         while self.lexer.current() == Token::Dot {
             self.lexer.next();
             let ident = self.parse_ident()?;
-            expr = QueryExprIR::Property(Box::new(expr), ident);
+            expr = QueryExprIR::FuncInvocation(
+                "property".to_string(),
+                vec![expr, QueryExprIR::String(ident)],
+            );
         }
 
         Ok(expr)
@@ -840,23 +843,35 @@ impl<'a> Parser<'a> {
                 self.lexer.next();
                 match_token!(self.lexer, With);
                 let rhs = self.parse_add_sub_expr()?;
-                Ok(QueryExprIR::StartsWith(Box::new((lhs, rhs))))
+                Ok(QueryExprIR::FuncInvocation(
+                    "@starts_with".to_string(),
+                    vec![lhs, rhs],
+                ))
             }
             Token::Ends => {
                 self.lexer.next();
                 match_token!(self.lexer, With);
                 let rhs = self.parse_add_sub_expr()?;
-                Ok(QueryExprIR::EndsWith(Box::new((lhs, rhs))))
+                Ok(QueryExprIR::FuncInvocation(
+                    "@ends_with".to_string(),
+                    vec![lhs, rhs],
+                ))
             }
             Token::Contains => {
                 self.lexer.next();
                 let rhs = self.parse_add_sub_expr()?;
-                Ok(QueryExprIR::Contains(Box::new((lhs, rhs))))
+                Ok(QueryExprIR::FuncInvocation(
+                    "@contains".to_string(),
+                    vec![lhs, rhs],
+                ))
             }
             Token::RegexMatches => {
                 self.lexer.next();
                 let rhs = self.parse_add_sub_expr()?;
-                Ok(QueryExprIR::RegexMatches(Box::new((lhs, rhs))))
+                Ok(QueryExprIR::FuncInvocation(
+                    "@regex_matches".to_string(),
+                    vec![lhs, rhs],
+                ))
             }
             _ => Ok(lhs),
         }
diff --git a/graph/src/graph.rs b/graph/src/graph.rs
index bfc0fa99..a17aade3 100644
--- a/graph/src/graph.rs
+++ b/graph/src/graph.rs
@@ -4,6 +4,7 @@ use std::{
     time::{Duration, Instant},
 };
 
+use orx_tree::DynTree;
 use roaring::RoaringTreemap;
 
 use crate::{
@@ -33,7 +34,7 @@ pub struct Graph {
     relationship_types: Vec<String>,
     node_properties: Vec<String>,
     relationship_properties: Vec<String>,
-    cache: Mutex<BTreeMap<String, IR>>,
+    cache: Mutex<BTreeMap<String, DynTree<IR>>>,
 }
 
 impl Graph {
@@ -164,7 +165,7 @@ impl Graph {
             self,
             &mut runtime,
             result_fn,
-            &evaluate,
+            &evaluate.root(),
         )?;
         let run_duration = start.elapsed();
 
@@ -232,7 +233,7 @@ impl Graph {
             self,
             &mut runtime,
             result_fn,
-            &evaluate,
+            &evaluate.root(),
         )?;
         let run_duration = start.elapsed();
 
diff --git a/graph/src/planner.rs b/graph/src/planner.rs
index 2e3758fb..62f31584 100644
--- a/graph/src/planner.rs
+++ b/graph/src/planner.rs
@@ -1,4 +1,6 @@
-use std::{collections::BTreeMap, vec::IntoIter};
+use std::{fmt::Display, vec::IntoIter};
+
+use orx_tree::{DynTree, NodeRef};
 
 use crate::ast::{NodePattern, Pattern, QueryExprIR, QueryIR};
 
@@ -11,44 +13,128 @@ pub enum IR {
     String(String),
     Var(String),
     Parameter(String),
-    List(Vec<IR>),
-    Length(Box<IR>),
-    GetElement(Box<(IR, IR)>),
-    GetElements(Box<(IR, Option<IR>, Option<IR>)>),
-    Range(Box<(IR, IR, IR)>),
-    IsNull(Box<IR>),
-    IsNode(Box<IR>),
-    Or(Vec<IR>),
-    Xor(Vec<IR>),
-    And(Vec<IR>),
-    Not(Box<IR>),
-    Negate(Box<IR>),
-    Eq(Vec<IR>),
-    Neq(Vec<IR>),
-    Lt(Box<(IR, IR)>),
-    Gt(Box<(IR, IR)>),
-    Le(Box<(IR, IR)>),
-    Ge(Box<(IR, IR)>),
-    In(Box<(IR, IR)>),
-    Add(Vec<IR>),
-    Sub(Vec<IR>),
-    Mul(Vec<IR>),
-    Div(Vec<IR>),
-    Pow(Vec<IR>),
-    Modulo(Vec<IR>),
-    FuncInvocation(String, Vec<IR>),
-    Map(BTreeMap<String, IR>),
-    Set(String, Box<IR>),
-    If(Box<(IR, IR)>),
-    For(Box<(IR, IR, IR, IR)>),
-    Return(Box<IR>),
-    Block(Vec<IR>),
+    List,
+    Length,
+    GetElement,
+    GetElements,
+    Range,
+    IsNull,
+    IsNode,
+    Or,
+    Xor,
+    And,
+    Not,
+    Negate,
+    Eq,
+    Neq,
+    Lt,
+    Gt,
+    Le,
+    Ge,
+    In,
+    Add,
+    Sub,
+    Mul,
+    Div,
+    Pow,
+    Modulo,
+    FuncInvocation(String),
+    Map,
+    Set(String),
+    If,
+    For,
+    Return,
+    Block,
+}
+
+impl Display for IR {
+    fn fmt(
+        &self,
+        f: &mut std::fmt::Formatter<'_>,
+    ) -> std::fmt::Result {
+        match self {
+            Self::Null => write!(f, "null"),
+            Self::Bool(x) => write!(f, "{x}"),
+            Self::Integer(x) => write!(f, "{x}"),
+            Self::Float(x) => write!(f, "{x}"),
+            Self::String(x) => write!(f, "\"{x}\""),
+            Self::Var(x) => write!(f, "{x}"),
+            Self::Parameter(x) => write!(f, "${x}"),
+            Self::List => write!(f, "list"),
+            Self::Length => write!(f, "length"),
+            Self::GetElement => write!(f, "get_element"),
+            Self::GetElements => write!(f, "get_elements"),
+            Self::Range => write!(f, "range"),
+            Self::IsNull => write!(f, "is_null"),
+            Self::IsNode => write!(f, "is_node"),
+            Self::Or => write!(f, "or"),
+            Self::Xor => write!(f, "xor"),
+            Self::And => write!(f, "and"),
+            Self::Not => write!(f, "not"),
+            Self::Negate => write!(f, "negate"),
+            Self::Eq => write!(f, "eq"),
+            Self::Neq => write!(f, "neq"),
+            Self::Lt => write!(f, "lt"),
+            Self::Gt => write!(f, "gt"),
+            Self::Le => write!(f, "le"),
+            Self::Ge => write!(f, "ge"),
+            Self::In => write!(f, "in"),
+            Self::Add => write!(f, "+"),
+            Self::Sub => write!(f, "-"),
+            Self::Mul => write!(f, "*"),
+            Self::Div => write!(f, "/"),
+            Self::Pow => write!(f, "^"),
+            Self::Modulo => write!(f, "%"),
+            Self::FuncInvocation(x) => write!(f, "{x}()"),
+            Self::Map => write!(f, "{{}}"),
+            Self::Set(x) => write!(f, "{x} ="),
+            Self::If => write!(f, "if"),
+            Self::For => write!(f, "for"),
+            Self::Return => write!(f, "return"),
+            Self::Block => write!(f, "block"),
+        }
+    }
 }
 
 pub struct Planner {
     var_id: u64,
 }
 
+macro_rules! tree {
+    ($value:expr) => {
+        DynTree::new($value)
+    };
+    ($value:expr, $($child:expr),*) => {
+        {
+            let mut n = DynTree::new($value);
+            let mut root = n.root_mut();
+            $(root.push_child_tree($child);)*
+            n
+        }
+    };
+    ($value:expr ; $iter:expr) => {
+        {
+            let mut n = DynTree::new($value);
+            let mut root = n.root_mut();
+            for child in $iter {
+                root.push_child_tree(child);
+            }
+            n
+        }
+    };
+    ($value:expr => $self:expr => $child:expr) => {
+        {
+            let mut n = DynTree::new($value);
+            let mut root = n.root_mut();
+            for child in $child.into_iter().map(|ir| $self.plan_expr(ir)) {
+                root.push_child_tree(child);
+            }
+            n
+        }
+    };
+    () => {};
+}
+
 impl Default for Planner {
     fn default() -> Self {
         Self::new()
@@ -69,121 +155,92 @@ impl Planner {
 
     #[allow(clippy::only_used_in_recursion)]
     fn plan_expr(
-        &self,
+        &mut self,
         expr: QueryExprIR,
-    ) -> IR {
+    ) -> DynTree<IR> {
         match expr {
-            QueryExprIR::Null => IR::Null,
-            QueryExprIR::Bool(x) => IR::Bool(x),
-            QueryExprIR::Integer(x) => IR::Integer(x),
-            QueryExprIR::Float(x) => IR::Float(x),
-            QueryExprIR::String(x) => IR::String(x),
-            QueryExprIR::Ident(x) => IR::Var(x),
-            QueryExprIR::Parameter(x) => IR::Parameter(x),
-            QueryExprIR::Named(name, expr) => IR::Set(name, Box::new(self.plan_expr(*expr))),
-            QueryExprIR::List(exprs) => {
-                IR::List(exprs.into_iter().map(|ir| self.plan_expr(ir)).collect())
-            }
-            QueryExprIR::Or(exprs) => {
-                IR::Or(exprs.into_iter().map(|ir| self.plan_expr(ir)).collect())
-            }
-            QueryExprIR::Xor(exprs) => {
-                IR::Xor(exprs.into_iter().map(|ir| self.plan_expr(ir)).collect())
-            }
-            QueryExprIR::And(exprs) => {
-                IR::And(exprs.into_iter().map(|ir| self.plan_expr(ir)).collect())
-            }
-            QueryExprIR::Not(expr) => IR::Not(Box::new(self.plan_expr(*expr))),
-            QueryExprIR::Negate(expr) => IR::Negate(Box::new(self.plan_expr(*expr))),
-
-            QueryExprIR::Eq(exprs) => {
-                IR::Eq(exprs.into_iter().map(|ir| self.plan_expr(ir)).collect())
-            }
+            QueryExprIR::Null => tree!(IR::Null),
+            QueryExprIR::Bool(x) => tree!(IR::Bool(x)),
+            QueryExprIR::Integer(x) => tree!(IR::Integer(x)),
+            QueryExprIR::Float(x) => tree!(IR::Float(x)),
+            QueryExprIR::String(x) => tree!(IR::String(x)),
+            QueryExprIR::Ident(x) => tree!(IR::Var(x)),
+            QueryExprIR::Parameter(x) => tree!(IR::Parameter(x)),
+            QueryExprIR::Named(name, expr) => tree!(IR::Set(name), self.plan_expr(*expr)),
+            QueryExprIR::List(exprs) => tree!(IR::List => self => exprs),
+            QueryExprIR::Or(exprs) => tree!(IR::Or => self => exprs),
+            QueryExprIR::Xor(exprs) => tree!(IR::Xor => self => exprs),
+            QueryExprIR::And(exprs) => tree!(IR::And => self => exprs),
+            QueryExprIR::Not(expr) => tree!(IR::Not, self.plan_expr(*expr)),
+            QueryExprIR::Negate(expr) => tree!(IR::Negate, self.plan_expr(*expr)),
+            QueryExprIR::Eq(exprs) => tree!(IR::Eq => self => exprs),
             QueryExprIR::Neq(_) => todo!(),
             QueryExprIR::Lt(_) => todo!(),
             QueryExprIR::Gt(_) => todo!(),
             QueryExprIR::Le(_) => todo!(),
             QueryExprIR::Ge(_) => todo!(),
-            QueryExprIR::In(op) => IR::In(Box::new((self.plan_expr(op.0), self.plan_expr(op.1)))),
-            QueryExprIR::Add(exprs) => {
-                IR::Add(exprs.into_iter().map(|ir| self.plan_expr(ir)).collect())
-            }
-            QueryExprIR::Sub(exprs) => {
-                IR::Sub(exprs.into_iter().map(|ir| self.plan_expr(ir)).collect())
-            }
-            QueryExprIR::Mul(exprs) => {
-                IR::Mul(exprs.into_iter().map(|ir| self.plan_expr(ir)).collect())
-            }
-            QueryExprIR::Div(exprs) => {
-                IR::Div(exprs.into_iter().map(|ir| self.plan_expr(ir)).collect())
-            }
-            QueryExprIR::Pow(exprs) => {
-                IR::Pow(exprs.into_iter().map(|ir| self.plan_expr(ir)).collect())
-            }
-            QueryExprIR::Modulo(exprs) => {
-                IR::Modulo(exprs.into_iter().map(|ir| self.plan_expr(ir)).collect())
-            }
-            QueryExprIR::IsNull(expr) => IR::IsNull(Box::new(self.plan_expr(*expr))),
+            QueryExprIR::In(op) => tree!(IR::In, self.plan_expr(op.0), self.plan_expr(op.1)),
+            QueryExprIR::Add(exprs) => tree!(IR::Add => self => exprs),
+            QueryExprIR::Sub(exprs) => tree!(IR::Sub => self => exprs),
+            QueryExprIR::Mul(exprs) => tree!(IR::Mul => self => exprs),
+            QueryExprIR::Div(exprs) => tree!(IR::Div => self => exprs),
+            QueryExprIR::Pow(exprs) => tree!(IR::Pow => self => exprs),
+            QueryExprIR::Modulo(exprs) => tree!(IR::Modulo => self => exprs),
+            QueryExprIR::IsNull(expr) => tree!(IR::IsNull, self.plan_expr(*expr)),
             QueryExprIR::GetElement(op) => {
-                IR::GetElement(Box::new((self.plan_expr(op.0), self.plan_expr(op.1))))
+                tree!(IR::GetElement, self.plan_expr(op.0), self.plan_expr(op.1))
             }
-            QueryExprIR::GetElements(op) => IR::GetElements(Box::new((
-                self.plan_expr(op.0),
-                op.1.map(|ir| self.plan_expr(ir)),
-                op.2.map(|ir| self.plan_expr(ir)),
-            ))),
-            QueryExprIR::StartsWith(op) => IR::FuncInvocation(
-                "@starts_with".to_string(),
-                vec![self.plan_expr(op.0), self.plan_expr(op.1)],
-            ),
-            QueryExprIR::EndsWith(op) => IR::FuncInvocation(
-                "@ends_with".to_string(),
-                vec![self.plan_expr(op.0), self.plan_expr(op.1)],
-            ),
-            QueryExprIR::Contains(op) => IR::FuncInvocation(
-                "@contains".to_string(),
-                vec![self.plan_expr(op.0), self.plan_expr(op.1)],
-            ),
-            QueryExprIR::RegexMatches(op) => IR::FuncInvocation(
-                "@regex_matches".to_string(),
-                vec![self.plan_expr(op.0), self.plan_expr(op.1)],
-            ),
-            QueryExprIR::Property(expr, name) => IR::FuncInvocation(
-                "property".to_string(),
-                vec![self.plan_expr(*expr), IR::String(name)],
-            ),
-            QueryExprIR::FuncInvocation(name, params) => match name.as_str() {
+            QueryExprIR::GetElements(op) => {
+                let list_var = self.next_var();
+                let list = tree!(IR::Set(list_var.to_string()), self.plan_expr(op.0));
+
+                tree!(
+                    IR::Block,
+                    list,
+                    tree!(
+                        IR::GetElements,
+                        tree!(IR::Var(list_var.to_string())),
+                        op.1.map_or_else(|| tree!(IR::Integer(0)), |ir| self.plan_expr(ir)),
+                        op.2.map_or_else(
+                            || tree!(IR::Length, tree!(IR::Var(list_var))),
+                            |ir| self.plan_expr(ir)
+                        )
+                    )
+                )
+            }
+            QueryExprIR::FuncInvocation(name, exprs) => match name.as_str() {
                 "range" => {
-                    let mut iter = params.into_iter();
+                    let mut iter = exprs.into_iter();
                     match (iter.next(), iter.next(), iter.next(), iter.next()) {
-                        (Some(length), None, None, None) => IR::Range(Box::new((
-                            IR::Integer(0),
-                            IR::Sub(vec![self.plan_expr(length), IR::Integer(1)]),
-                            IR::Integer(1),
-                        ))),
-                        (Some(from), Some(to), None, None) => IR::Range(Box::new((
+                        (Some(length), None, None, None) => tree!(
+                            IR::Range,
+                            self.plan_expr(QueryExprIR::Integer(0)),
+                            self.plan_expr(QueryExprIR::Sub(
+                                vec![length, QueryExprIR::Integer(1),]
+                            )),
+                            self.plan_expr(QueryExprIR::Integer(1))
+                        ),
+                        (Some(from), Some(to), None, None) => tree!(
+                            IR::Range,
                             self.plan_expr(from),
                             self.plan_expr(to),
-                            IR::Integer(1),
-                        ))),
-                        (Some(from), Some(to), Some(step), None) => IR::Range(Box::new((
+                            self.plan_expr(QueryExprIR::Integer(1))
+                        ),
+                        (Some(from), Some(to), Some(step), None) => tree!(
+                            IR::Range,
                             self.plan_expr(from),
                             self.plan_expr(to),
-                            self.plan_expr(step),
-                        ))),
+                            self.plan_expr(step)
+                        ),
                         _ => todo!(),
                     }
                 }
-                name => IR::FuncInvocation(
-                    name.to_string(),
-                    params.into_iter().map(|ir| self.plan_expr(ir)).collect(),
-                ),
+                name => tree!(IR::FuncInvocation(name.to_string()) => self => exprs),
             },
-            QueryExprIR::Map(attrs) => IR::Map(
+            QueryExprIR::Map(attrs) => tree!(IR::Map ;
                 attrs
                     .into_iter()
-                    .map(|(k, ir)| (k, self.plan_expr(ir)))
-                    .collect(),
+                    .map(|(k, ir)| tree!(IR::Var(k), self.plan_expr(ir)))
             ),
         }
     }
@@ -192,64 +249,79 @@ impl Planner {
         &mut self,
         pattern: Pattern,
         iter: &mut IntoIter<QueryIR>,
-    ) -> IR {
-        let create_nodes = pattern
-            .nodes
-            .into_iter()
-            .map(|n| {
-                let labels = IR::List(
-                    n.labels
-                        .iter()
-                        .map(|l| IR::String((*l).to_string()))
-                        .collect(),
-                );
-                let attrs = IR::Map(
-                    n.attrs
-                        .into_iter()
-                        .map(|(k, ir)| (k, self.plan_expr(ir)))
-                        .collect(),
-                );
-                IR::Set(
-                    n.alias.to_string(),
-                    Box::new(IR::FuncInvocation(
-                        String::from("create_node"),
-                        vec![labels, attrs],
-                    )),
-                )
-            })
-            .collect();
-        let create_relationships = pattern
-            .relationships
-            .into_iter()
-            .map(|l| {
-                let relationship_type = IR::String(l.relationship_type.to_string());
-                let attrs = IR::Map(
-                    l.attrs
-                        .into_iter()
-                        .map(|(k, ir)| (k, self.plan_expr(ir)))
-                        .collect(),
-                );
-                let from = IR::Var(l.from.to_string());
-                let to = IR::Var(l.to.to_string());
-                IR::Set(
-                    l.alias.to_string(),
-                    Box::new(IR::FuncInvocation(
-                        String::from("create_relationship"),
-                        vec![relationship_type, from, to, attrs],
-                    )),
-                )
-            })
-            .collect();
-        match iter.next() {
-            Some(body_ir) => IR::Block(vec![
-                IR::Block(create_nodes),
-                IR::Block(create_relationships),
-                self.plan_query(body_ir, iter),
-            ]),
-            None => IR::Block(vec![
-                IR::Block(create_nodes),
-                IR::Block(create_relationships),
-            ]),
+    ) -> DynTree<IR> {
+        if let Some(body_ir) = iter.next() {
+            tree!(
+                IR::Block,
+                tree!(IR::Block ; pattern.nodes.into_iter().map(|n| {
+                    tree!(
+                        IR::Set(n.alias.to_string()),
+                        tree!(
+                            IR::FuncInvocation(String::from("create_node")),
+                            tree!(IR::List ; n
+                                .labels
+                                .iter()
+                                .map(|l| tree!(IR::String((*l).to_string())))),
+                            tree!(IR::Map ;
+                                n.attrs
+                                    .into_iter()
+                                    .map(|(k, ir)| tree!(IR::Var(k), self.plan_expr(ir)))
+                            )
+                        )
+                    )
+                })),
+                tree!(IR::Block ; pattern.relationships.into_iter().map(|l| {
+                    tree!(
+                        IR::Set(l.alias.to_string()),
+                        tree!(
+                            IR::FuncInvocation(String::from("create_relationship")),
+                            tree!(IR::String(l.relationship_type.to_string())),
+                            tree!(IR::Var(l.from.to_string())),
+                            tree!(IR::Var(l.to.to_string())),
+                            tree!(IR::Map ;
+                                l.attrs
+                                    .into_iter()
+                                    .map(|(k, ir)| tree!(IR::Var(k), self.plan_expr(ir))))
+                        )
+                    )
+                })),
+                self.plan_query(body_ir, iter)
+            )
+        } else {
+            tree!(
+                IR::Block,
+                tree!(IR::Block ; pattern.nodes.into_iter().map(|n| {
+                    tree!(
+                        IR::Set(n.alias.to_string()),
+                        tree!(
+                            IR::FuncInvocation(String::from("create_node")),
+                            tree!(IR::List ; n
+                                .labels
+                                .iter()
+                                .map(|l| tree!(IR::String((*l).to_string())))),
+                            tree!(IR::Map ;
+                                n.attrs
+                                    .into_iter()
+                                    .map(|(k, ir)| tree!(IR::Var(k), self.plan_expr(ir))))
+                        )
+                    )
+                })),
+                tree!(IR::Block ; pattern.relationships.into_iter().map(|l| {
+                    tree!(
+                        IR::Set(l.alias.to_string()),
+                        tree!(
+                            IR::FuncInvocation(String::from("create_relationship")),
+                            tree!(IR::String(l.relationship_type.to_string())),
+                            tree!(IR::Var(l.from.to_string())),
+                            tree!(IR::Var(l.to.to_string())),
+                            tree!(IR::Map ;
+                                l.attrs
+                                    .into_iter()
+                                    .map(|(k, ir)| tree!(IR::Var(k), self.plan_expr(ir))))
+                        )
+                    )
+                }))
+            )
         }
     }
 
@@ -257,14 +329,15 @@ impl Planner {
         &mut self,
         exprs: Vec<QueryExprIR>,
         iter: &mut IntoIter<QueryIR>,
-    ) -> IR {
-        let deleted_entities = exprs.into_iter().map(|ir| self.plan_expr(ir)).collect();
-        match iter.next() {
-            Some(body_ir) => IR::Block(vec![
-                IR::FuncInvocation(String::from("delete_entity"), deleted_entities),
-                self.plan_query(body_ir, iter),
-            ]),
-            None => IR::FuncInvocation(String::from("delete_entity"), deleted_entities),
+    ) -> DynTree<IR> {
+        if let Some(body_ir) = iter.next() {
+            tree!(
+                IR::Block,
+                tree!(IR::FuncInvocation(String::from("delete_entity")) => self => exprs),
+                self.plan_query(body_ir, iter)
+            )
+        } else {
+            tree!(IR::FuncInvocation(String::from("delete_entity")) => self => exprs)
         }
     }
 
@@ -273,97 +346,115 @@ impl Planner {
         expr: QueryExprIR,
         iter: &mut IntoIter<QueryIR>,
         alias: String,
-    ) -> IR {
+    ) -> DynTree<IR> {
         let list = self.plan_expr(expr);
-        match list {
-            IR::Range(op) => {
-                let init = IR::Set(alias.to_string(), Box::new(op.0));
-                let condition = IR::Le(Box::new((IR::Var(alias.to_string()), op.1)));
-                let next = IR::Set(
-                    alias.to_string(),
-                    Box::new(IR::Add(vec![IR::Var(alias), op.2])),
-                );
-                let body_ir = iter.next().unwrap();
-                let body = self.plan_query(body_ir, iter);
-                IR::For(Box::new((init, condition, next, body)))
-            }
-            x => {
-                let list_var = self.next_var();
-                let index_var = self.next_var();
-                let list = IR::Set(list_var.to_string(), Box::new(x));
-                let init = IR::Set(index_var.to_string(), Box::new(IR::Integer(0)));
-                let condition = IR::Lt(Box::new((
-                    IR::Var(index_var.to_string()),
-                    IR::Length(Box::new(IR::Var(list_var.to_string()))),
-                )));
-                let next = IR::Set(
-                    index_var.to_string(),
-                    Box::new(IR::Add(vec![
-                        IR::Var(index_var.to_string()),
-                        IR::Integer(1),
-                    ])),
-                );
-                let body_ir = iter.next().unwrap();
-                let body = self.plan_query(body_ir, iter);
-                IR::Block(vec![
-                    list,
-                    IR::For(Box::new((
-                        init,
-                        condition,
-                        next,
-                        IR::Block(vec![
-                            IR::Set(
-                                alias,
-                                Box::new(IR::GetElement(Box::new((
-                                    IR::Var(list_var),
-                                    IR::Var(index_var),
-                                )))),
-                            ),
-                            body,
-                        ]),
-                    ))),
-                ])
-            }
+        if matches!(list.root().data(), IR::Range) {
+            let init = tree!(
+                IR::Set(alias.to_string()),
+                list.root().child(0).as_cloned_subtree()
+            );
+            let condition = tree!(
+                IR::Le,
+                tree!(IR::Var(alias.to_string())),
+                list.root().child(1).as_cloned_subtree()
+            );
+            let next = tree!(
+                IR::Set(alias.to_string()),
+                tree!(
+                    IR::Add,
+                    tree!(IR::Var(alias)),
+                    list.root().child(2).as_cloned_subtree()
+                )
+            );
+            let body_ir = iter.next().unwrap();
+            let body = self.plan_query(body_ir, iter);
+            tree!(IR::For, init, condition, next, body)
+        } else {
+            let list_var = self.next_var();
+            let index_var = self.next_var();
+            let list = tree!(IR::Set(list_var.to_string()), list);
+            let init = tree!(IR::Set(index_var.to_string()), tree!(IR::Integer(0)));
+            let condition = tree!(
+                IR::Lt,
+                tree!(IR::Var(index_var.to_string())),
+                tree!(IR::Length, tree!(IR::Var(list_var.to_string())))
+            );
+            let next = tree!(
+                IR::Set(index_var.to_string()),
+                tree!(
+                    IR::Add,
+                    tree!(IR::Var(index_var.to_string())),
+                    tree!(IR::Integer(1))
+                )
+            );
+            let body_ir = iter.next().unwrap();
+            let body = self.plan_query(body_ir, iter);
+            tree!(
+                IR::Block,
+                list,
+                tree!(
+                    IR::For,
+                    init,
+                    condition,
+                    next,
+                    tree!(
+                        IR::Block,
+                        tree!(
+                            IR::Set(alias),
+                            tree!(
+                                IR::GetElement,
+                                tree!(IR::Var(list_var)),
+                                tree!(IR::Var(index_var))
+                            )
+                        ),
+                        body
+                    )
+                )
+            )
         }
     }
 
     fn plan_node_scan(
         &mut self,
         node: NodePattern,
-        body: IR,
-    ) -> IR {
-        let init = IR::Block(vec![
-            IR::Set(
-                format!("iter_{}", node.alias),
-                Box::new(IR::FuncInvocation(
-                    String::from("create_node_iter"),
-                    vec![IR::List(node.labels.into_iter().map(IR::String).collect())],
-                )),
-            ),
-            IR::Set(
-                node.alias.to_string(),
-                Box::new(IR::FuncInvocation(
-                    String::from("next_node"),
-                    vec![IR::Var(format!("iter_{}", node.alias))],
-                )),
+        body: DynTree<IR>,
+    ) -> DynTree<IR> {
+        let init = tree!(
+            IR::Block,
+            tree!(
+                IR::Set(format!("iter_{}", node.alias)),
+                tree!(
+                    IR::FuncInvocation(String::from("create_node_iter")),
+                    tree!(IR::List ; node
+                            .labels
+                            .into_iter()
+                            .map(|label| tree!(IR::String(label))))
+                )
             ),
-        ]);
-        let condition = IR::IsNode(Box::new(IR::Var(node.alias.to_string())));
-        let next = IR::Set(
-            node.alias.to_string(),
-            Box::new(IR::FuncInvocation(
-                String::from("next_node"),
-                vec![IR::Var(format!("iter_{}", node.alias))],
-            )),
+            tree!(
+                IR::Set(node.alias.to_string()),
+                tree!(
+                    IR::FuncInvocation(String::from("next_node")),
+                    tree!(IR::Var(format!("iter_{}", node.alias)))
+                )
+            )
+        );
+        let condition = tree!(IR::IsNode, tree!(IR::Var(node.alias.to_string())));
+        let next = tree!(
+            IR::Set(node.alias.to_string()),
+            tree!(
+                IR::FuncInvocation(String::from("next_node")),
+                tree!(IR::Var(format!("iter_{}", node.alias)))
+            )
         );
-        IR::For(Box::new((init, condition, next, body)))
+        tree!(IR::For, init, condition, next, body)
     }
 
     fn plan_match(
         &mut self,
         pattern: Pattern,
         iter: &mut IntoIter<QueryIR>,
-    ) -> IR {
+    ) -> DynTree<IR> {
         if pattern.relationships.is_empty() {
             let mut body = self.plan_query(iter.next().unwrap(), iter);
             for node in pattern.nodes.into_iter().rev() {
@@ -371,54 +462,65 @@ impl Planner {
             }
             return body;
         }
-        IR::Null
+        tree!(IR::Null)
     }
 
     fn plan_query(
         &mut self,
         ir: QueryIR,
         iter: &mut IntoIter<QueryIR>,
-    ) -> IR {
+    ) -> DynTree<IR> {
         match ir {
-            QueryIR::Call(name, exprs) => IR::For(Box::new((
-                IR::Block(vec![
-                    IR::Set(
-                        "res".to_string(),
-                        Box::new(IR::FuncInvocation(
-                            name.to_lowercase(),
-                            exprs.into_iter().map(|ir| self.plan_expr(ir)).collect(),
-                        )),
+            QueryIR::Call(name, exprs) => tree!(
+                IR::For,
+                tree!(
+                    IR::Block,
+                    tree!(
+                        IR::Set("res".to_string()),
+                        tree!(IR::FuncInvocation(name.to_lowercase()) => self => exprs)
                     ),
-                    IR::Set("i".to_string(), Box::new(IR::Integer(0))),
-                ]),
-                IR::Lt(Box::new((
-                    IR::Var("i".to_string()),
-                    IR::Length(Box::new(IR::Var("res".to_string()))),
-                ))),
-                IR::Set(
-                    "i".to_string(),
-                    Box::new(IR::Add(vec![IR::Var("i".to_string()), IR::Integer(1)])),
+                    tree!(IR::Set("i".to_string()), tree!(IR::Integer(0)))
+                ),
+                tree!(
+                    IR::Lt,
+                    tree!(IR::Var("i".to_string())),
+                    tree!(IR::Length, tree!(IR::Var("res".to_string())))
                 ),
-                IR::Return(Box::new(IR::List(vec![IR::GetElement(Box::new((
-                    IR::Var("res".to_string()),
-                    IR::Var("i".to_string()),
-                )))]))),
-            ))),
+                tree!(
+                    IR::Set("i".to_string()),
+                    tree!(
+                        IR::Add,
+                        tree!(IR::Var("i".to_string())),
+                        tree!(IR::Integer(1))
+                    )
+                ),
+                tree!(
+                    IR::Return,
+                    tree!(
+                        IR::List,
+                        tree!(
+                            IR::GetElement,
+                            tree!(IR::Var("res".to_string())),
+                            tree!(IR::Var("i".to_string()))
+                        )
+                    )
+                )
+            ),
             QueryIR::Match(pattern) => self.plan_match(pattern, iter),
             QueryIR::Unwind(expr, alias) => self.plan_unwind(expr, iter, alias),
-            QueryIR::Where(expr) => IR::If(Box::new((
+            QueryIR::Where(expr) => tree!(
+                IR::If,
                 self.plan_expr(expr),
-                self.plan_query(iter.next().unwrap(), iter),
-            ))),
+                self.plan_query(iter.next().unwrap(), iter)
+            ),
             QueryIR::Create(pattern) => self.plan_create(pattern, iter),
             QueryIR::Delete(exprs) => self.plan_delete(exprs, iter),
-            QueryIR::With(exprs) => IR::Block(vec![
-                IR::Block(exprs.into_iter().map(|ir| self.plan_expr(ir)).collect()),
-                self.plan_query(iter.next().unwrap(), iter),
-            ]),
-            QueryIR::Return(exprs) => IR::Return(Box::new(IR::List(
-                exprs.into_iter().map(|ir| self.plan_expr(ir)).collect(),
-            ))),
+            QueryIR::With(exprs) => tree!(
+                IR::Block,
+                tree!(IR::Block => self => exprs),
+                self.plan_query(iter.next().unwrap(), iter)
+            ),
+            QueryIR::Return(exprs) => tree!(IR::Return, tree!(IR::List => self => exprs)),
             QueryIR::Query(q) => {
                 let iter = &mut q.into_iter();
                 self.plan_query(iter.next().unwrap(), iter)
@@ -431,7 +533,7 @@ impl Planner {
         &mut self,
         ir: QueryIR,
         _debug: bool,
-    ) -> IR {
+    ) -> DynTree<IR> {
         self.plan_query(ir, &mut Vec::new().into_iter())
     }
 }
diff --git a/graph/src/runtime.rs b/graph/src/runtime.rs
index f32949f6..f1195124 100644
--- a/graph/src/runtime.rs
+++ b/graph/src/runtime.rs
@@ -1,3 +1,5 @@
+use orx_tree::{DynNode, NodeRef};
+
 use crate::{
     ast::QueryExprIR, graph::Graph, matrix::Iter, planner::IR, value::Contains, value::Value,
 };
@@ -1062,9 +1064,9 @@ pub fn ro_run(
     g: &Graph,
     runtime: &mut Runtime,
     result_fn: &mut dyn FnMut(&Graph, Value),
-    ir: &IR,
+    ir: &DynNode<IR>,
 ) -> Result<Value, String> {
-    match ir {
+    match ir.data() {
         IR::Null => Ok(Value::Null),
         IR::Bool(x) => Ok(Value::Bool(*x)),
         IR::Integer(x) => Ok(Value::Int(*x)),
@@ -1078,18 +1080,18 @@ pub fn ro_run(
             || Err(format!("Parameter {x} not found")),
             |v| Ok(v.to_owned()),
         ),
-        IR::List(irs) => Ok(Value::List(
-            irs.iter()
-                .map(|ir| ro_run(vars, g, runtime, result_fn, ir))
+        IR::List => Ok(Value::List(
+            ir.children()
+                .map(|ir| ro_run(vars, g, runtime, result_fn, &ir))
                 .collect::<Result<Vec<_>, _>>()?,
         )),
-        IR::Length(ir) => match ro_run(vars, g, runtime, result_fn, ir)? {
+        IR::Length => match ro_run(vars, g, runtime, result_fn, &&ir.child(0))? {
             Value::List(arr) => Ok(Value::Int(arr.len() as _)),
             _ => Err("Length operator requires a list".to_string()),
         },
-        IR::GetElement(op) => {
-            let arr = ro_run(vars, g, runtime, result_fn, &op.0)?;
-            let i = ro_run(vars, g, runtime, result_fn, &op.1)?;
+        IR::GetElement => {
+            let arr = ro_run(vars, g, runtime, result_fn, &&ir.child(0))?;
+            let i = ro_run(vars, g, runtime, result_fn, &&ir.child(1))?;
             match (arr, i) {
                 (Value::List(values), Value::Int(i)) => {
                     if i >= 0 && i < values.len() as _ {
@@ -1102,27 +1104,16 @@ pub fn ro_run(
                 v => Err(format!("Type mismatch: expected List but was {v:?}")),
             }
         }
-        IR::GetElements(op) => {
-            let arr = ro_run(vars, g, runtime, result_fn, &op.0)?;
-            match (&op.1, &op.2) {
-                (None, None) => get_elements(arr, None, None),
-                (None, Some(b)) => {
-                    get_elements(arr, None, Some(ro_run(vars, g, runtime, result_fn, b)?))
-                }
-                (Some(a), None) => {
-                    get_elements(arr, Some(ro_run(vars, g, runtime, result_fn, a)?), None)
-                }
-                (Some(a), Some(b)) => get_elements(
-                    arr,
-                    Some(ro_run(vars, g, runtime, result_fn, a)?),
-                    Some(ro_run(vars, g, runtime, result_fn, b)?),
-                ),
-            }
+        IR::GetElements => {
+            let arr = ro_run(vars, g, runtime, result_fn, &ir.child(0))?;
+            let a = ro_run(vars, g, runtime, result_fn, &ir.child(1))?;
+            let b = ro_run(vars, g, runtime, result_fn, &ir.child(2))?;
+            get_elements(arr, a, b)
         }
-        IR::Range(op) => {
-            let start = ro_run(vars, g, runtime, result_fn, &op.0)?;
-            let end = ro_run(vars, g, runtime, result_fn, &op.1)?;
-            let step = ro_run(vars, g, runtime, result_fn, &op.2)?;
+        IR::Range => {
+            let start = ro_run(vars, g, runtime, result_fn, &ir.child(0))?;
+            let end = ro_run(vars, g, runtime, result_fn, &ir.child(1))?;
+            let step = ro_run(vars, g, runtime, result_fn, &ir.child(2))?;
             match (start, end, step) {
                 (Value::Int(start), Value::Int(end), Value::Int(step)) => {
                     Ok(Value::List(if step < 0 {
@@ -1140,18 +1131,18 @@ pub fn ro_run(
                 _ => Err("Range operator requires two integers".to_string()),
             }
         }
-        IR::IsNull(ir) => match ro_run(vars, g, runtime, result_fn, ir)? {
+        IR::IsNull => match ro_run(vars, g, runtime, result_fn, &ir.child(0))? {
             Value::Null => Ok(Value::Bool(true)),
             _ => Ok(Value::Bool(false)),
         },
-        IR::IsNode(ir) => match ro_run(vars, g, runtime, result_fn, ir)? {
+        IR::IsNode => match ro_run(vars, g, runtime, result_fn, &ir.child(0))? {
             Value::Node(_) => Ok(Value::Bool(true)),
             _ => Ok(Value::Bool(false)),
         },
-        IR::Or(irs) => {
+        IR::Or => {
             let mut is_null = false;
-            for ir in irs {
-                match ro_run(vars, g, runtime, result_fn, ir)? {
+            for ir in ir.children() {
+                match ro_run(vars, g, runtime, result_fn, &ir)? {
                     Value::Bool(true) => return Ok(Value::Bool(true)),
                     Value::Bool(false) => {}
                     Value::Null => is_null = true,
@@ -1164,10 +1155,10 @@ pub fn ro_run(
 
             Ok(Value::Bool(false))
         }
-        IR::Xor(irs) => {
+        IR::Xor => {
             let mut last = None;
-            for ir in irs {
-                match ro_run(vars, g, runtime, result_fn, ir)? {
+            for ir in ir.children() {
+                match ro_run(vars, g, runtime, result_fn, &ir)? {
                     Value::Bool(b) => last = Some(last.map_or(b, |l| logical_xor(l, b))),
                     Value::Null => return Ok(Value::Null),
                     _ => return Err(format!("Type mismatch: expected Bool but was {ir:?}")),
@@ -1176,10 +1167,10 @@ pub fn ro_run(
             Ok(Value::Bool(last.unwrap_or(false)))
         }
 
-        IR::And(irs) => {
+        IR::And => {
             let mut is_null = false;
-            for ir in irs {
-                match ro_run(vars, g, runtime, result_fn, ir)? {
+            for ir in ir.children() {
+                match ro_run(vars, g, runtime, result_fn, &ir)? {
                     Value::Bool(false) => return Ok(Value::Bool(false)),
                     Value::Bool(true) => {}
                     Value::Null => is_null = true,
@@ -1192,12 +1183,12 @@ pub fn ro_run(
 
             Ok(Value::Bool(true))
         }
-        IR::Not(ir) => match ro_run(vars, g, runtime, result_fn, ir)? {
+        IR::Not => match ro_run(vars, g, runtime, result_fn, &ir.child(0))? {
             Value::Bool(b) => Ok(Value::Bool(!b)),
             Value::Null => Ok(Value::Null),
             _ => Err("InvalidArgumentType: Not operator requires a boolean or null".to_string()),
         },
-        IR::Negate(ir) => match ro_run(vars, g, runtime, result_fn, ir)? {
+        IR::Negate => match ro_run(vars, g, runtime, result_fn, &ir.child(0))? {
             Value::Int(i) => Ok(Value::Int(-i)),
             Value::Float(f) => Ok(Value::Float(-f)),
             Value::Null => Ok(Value::Null),
@@ -1205,97 +1196,97 @@ pub fn ro_run(
                 Err("InvalidArgumentType: Negate operator requires an Integer or Float".to_string())
             }
         },
-        IR::Eq(irs) => {
-            let iter = irs.iter().map(|ir| ro_run(vars, g, runtime, result_fn, ir));
-            all_equals(iter)
-        }
-        IR::Neq(irs) => irs
-            .iter()
-            .flat_map(|ir| ro_run(vars, g, runtime, result_fn, ir))
+        IR::Eq => all_equals(
+            ir.children()
+                .map(|ir| ro_run(vars, g, runtime, result_fn, &ir)),
+        ),
+        IR::Neq => ir
+            .children()
+            .flat_map(|ir| ro_run(vars, g, runtime, result_fn, &ir))
             .reduce(|a, b| Value::Bool(a != b))
             .ok_or_else(|| "Neq operator requires at least one argument".to_string()),
-        IR::Lt(op) => match (
-            ro_run(vars, g, runtime, result_fn, &op.0)?,
-            ro_run(vars, g, runtime, result_fn, &op.1)?,
+        IR::Lt => match (
+            ro_run(vars, g, runtime, result_fn, &ir.child(0))?,
+            ro_run(vars, g, runtime, result_fn, &ir.child(1))?,
         ) {
             (Value::Int(a), Value::Int(b)) => Ok(Value::Bool(a < b)),
             _ => Err("Lt operator requires two integers".to_string()),
         },
-        IR::Gt(op) => match (
-            ro_run(vars, g, runtime, result_fn, &op.0)?,
-            ro_run(vars, g, runtime, result_fn, &op.1)?,
+        IR::Gt => match (
+            ro_run(vars, g, runtime, result_fn, &ir.child(0))?,
+            ro_run(vars, g, runtime, result_fn, &ir.child(1))?,
         ) {
             (Value::Int(a), Value::Int(b)) => Ok(Value::Bool(a > b)),
             _ => Err("Gt operator requires two integers".to_string()),
         },
-        IR::Le(op) => match (
-            ro_run(vars, g, runtime, result_fn, &op.0)?,
-            ro_run(vars, g, runtime, result_fn, &op.1)?,
+        IR::Le => match (
+            ro_run(vars, g, runtime, result_fn, &ir.child(0))?,
+            ro_run(vars, g, runtime, result_fn, &ir.child(1))?,
         ) {
             (Value::Int(a), Value::Int(b)) => Ok(Value::Bool(a <= b)),
             _ => Err("Le operator requires two integers".to_string()),
         },
-        IR::Ge(op) => match (
-            ro_run(vars, g, runtime, result_fn, &op.0)?,
-            ro_run(vars, g, runtime, result_fn, &op.1)?,
+        IR::Ge => match (
+            ro_run(vars, g, runtime, result_fn, &ir.child(0))?,
+            ro_run(vars, g, runtime, result_fn, &ir.child(1))?,
         ) {
             (Value::Int(a), Value::Int(b)) => Ok(Value::Bool(a >= b)),
             _ => Err("Ge operator requires two integers".to_string()),
         },
-        IR::In(op) => {
-            let value = ro_run(vars, g, runtime, result_fn, &op.0)?;
-            let list = ro_run(vars, g, runtime, result_fn, &op.1)?;
+        IR::In => {
+            let value = ro_run(vars, g, runtime, result_fn, &ir.child(0))?;
+            let list = ro_run(vars, g, runtime, result_fn, &ir.child(1))?;
             list_contains(&list, &value)
         }
-        IR::Add(irs) => irs
-            .iter()
-            .map(|ir| ro_run(vars, g, runtime, result_fn, ir))
+        IR::Add => ir
+            .children()
+            .map(|ir| ro_run(vars, g, runtime, result_fn, &ir))
             .reduce(|acc, value| acc? + value?)
             .ok_or_else(|| "Add operator requires at least one operand".to_string())?,
-        IR::Sub(irs) => irs
-            .iter()
-            .flat_map(|ir| ro_run(vars, g, runtime, result_fn, ir))
+        IR::Sub => ir
+            .children()
+            .flat_map(|ir| ro_run(vars, g, runtime, result_fn, &ir))
             .reduce(|a, b| match (a, b) {
                 (Value::Int(a), Value::Int(b)) => Value::Int(a - b),
                 _ => Value::Null,
             })
             .ok_or_else(|| "Sub operator requires at least one argument".to_string()),
-        IR::Mul(irs) => irs
-            .iter()
-            .flat_map(|ir| ro_run(vars, g, runtime, result_fn, ir))
+        IR::Mul => ir
+            .children()
+            .flat_map(|ir| ro_run(vars, g, runtime, result_fn, &ir))
             .reduce(|a, b| match (a, b) {
                 (Value::Int(a), Value::Int(b)) => Value::Int(a * b),
                 _ => Value::Null,
             })
             .ok_or_else(|| "Mul operator requires at least one argument".to_string()),
-        IR::Div(irs) => irs
-            .iter()
-            .flat_map(|ir| ro_run(vars, g, runtime, result_fn, ir))
+        IR::Div => ir
+            .children()
+            .flat_map(|ir| ro_run(vars, g, runtime, result_fn, &ir))
             .reduce(|a, b| match (a, b) {
                 (Value::Int(a), Value::Int(b)) => Value::Int(a / b),
                 _ => Value::Null,
             })
             .ok_or_else(|| "Div operator requires at least one argument".to_string()),
-        IR::Pow(irs) => irs
-            .iter()
-            .flat_map(|ir| ro_run(vars, g, runtime, result_fn, ir))
+        IR::Pow => ir
+            .children()
+            .flat_map(|ir| ro_run(vars, g, runtime, result_fn, &ir))
             .reduce(|a, b| match (a, b) {
                 (Value::Int(a), Value::Int(b)) => Value::Float((a as f64).powf(b as _)),
                 _ => Value::Null,
             })
             .ok_or_else(|| "Pow operator requires at least one argument".to_string()),
-        IR::Modulo(irs) => irs
-            .iter()
-            .flat_map(|ir| ro_run(vars, g, runtime, result_fn, ir))
+        IR::Modulo => ir
+            .children()
+            .flat_map(|ir| ro_run(vars, g, runtime, result_fn, &ir))
             .reduce(|a, b| match (a, b) {
                 (Value::Int(a), Value::Int(b)) => Value::Int(a % b),
                 _ => Value::Null,
             })
             .ok_or_else(|| "Modulo operator requires at least one argument".to_string()),
-        IR::FuncInvocation(name, irs) => {
-            let args = irs
-                .iter()
-                .map(|ir| ro_run(vars, g, runtime, result_fn, ir))
+        IR::FuncInvocation(name) => {
+            let args = ir
+                .children()
+                .map(|ir| ro_run(vars, g, runtime, result_fn, &ir))
                 .collect::<Result<Vec<_>, _>>()?;
             #[allow(clippy::option_if_let_else)]
             if let Some(func) = runtime.read_functions.get(name) {
@@ -1304,44 +1295,44 @@ pub fn ro_run(
                 Err(format!("Function {name} not found"))
             }
         }
-        IR::Map(items) => Ok(Value::Map(
-            items
-                .iter()
-                .map(|(key, ir)| {
+        IR::Map => Ok(Value::Map(
+            ir.children()
+                .map(|child| {
                     (
-                        key.to_string(),
-                        ro_run(vars, g, runtime, result_fn, ir).unwrap_or(Value::Null),
+                        child.data().to_string(),
+                        ro_run(vars, g, runtime, result_fn, &child.child(0)).unwrap_or(Value::Null),
                     )
                 })
                 .collect(),
         )),
-        IR::Set(x, ir) => {
-            let v = ro_run(vars, g, runtime, result_fn, ir)?;
+        IR::Set(x) => {
+            let v = ro_run(vars, g, runtime, result_fn, &ir.child(0))?;
             vars.insert(x.to_string(), v.clone());
             Ok(v)
         }
-        IR::If(op) => match ro_run(vars, g, runtime, result_fn, &op.0)? {
-            Value::Bool(true) => ro_run(vars, g, runtime, result_fn, &op.1),
+        IR::If => match ro_run(vars, g, runtime, result_fn, &ir.child(0))? {
+            Value::Bool(true) => ro_run(vars, g, runtime, result_fn, &ir.child(1)),
             _ => Ok(Value::Null),
         },
-        IR::For(op) => {
-            ro_run(vars, g, runtime, result_fn, &op.0)?;
-            while ro_run(vars, g, runtime, result_fn, &op.1)? == Value::Bool(true) {
-                ro_run(vars, g, runtime, result_fn, &op.3)?;
-                ro_run(vars, g, runtime, result_fn, &op.2)?;
+        IR::For => {
+            ro_run(vars, g, runtime, result_fn, &ir.child(0))?;
+            while ro_run(vars, g, runtime, result_fn, &ir.child(1))? == Value::Bool(true) {
+                ro_run(vars, g, runtime, result_fn, &ir.child(3))?;
+                ro_run(vars, g, runtime, result_fn, &ir.child(2))?;
             }
             Ok(Value::Null)
         }
-        IR::Return(ir) => {
-            let v = ro_run(vars, g, runtime, result_fn, ir)?;
+        IR::Return => {
+            let v = ro_run(vars, g, runtime, result_fn, &ir.child(0))?;
             result_fn(g, v);
             Ok(Value::Null)
         }
-        IR::Block(irs) => {
-            for ir in irs {
-                ro_run(vars, g, runtime, result_fn, ir)?;
+        IR::Block => {
+            let mut v = Value::Null;
+            for ir in ir.children() {
+                v = ro_run(vars, g, runtime, result_fn, &ir)?;
             }
-            Ok(Value::Null)
+            Ok(v)
         }
     }
 }
@@ -1352,9 +1343,9 @@ pub fn run(
     g: &mut Graph,
     runtime: &mut Runtime,
     result_fn: &mut dyn FnMut(&Graph, Value),
-    ir: &IR,
+    ir: &DynNode<IR>,
 ) -> Result<Value, String> {
-    match ir {
+    match ir.data() {
         IR::Null => Ok(Value::Null),
         IR::Bool(x) => Ok(Value::Bool(*x)),
         IR::Integer(x) => Ok(Value::Int(*x)),
@@ -1368,18 +1359,18 @@ pub fn run(
             || Err(format!("Parameter {x} not found")),
             |v| Ok(v.to_owned()),
         ),
-        IR::List(irs) => Ok(Value::List(
-            irs.iter()
-                .map(|ir| run(vars, g, runtime, result_fn, ir))
+        IR::List => Ok(Value::List(
+            ir.children()
+                .map(|ir| run(vars, g, runtime, result_fn, &ir))
                 .collect::<Result<Vec<_>, _>>()?,
         )),
-        IR::Length(ir) => match run(vars, g, runtime, result_fn, ir)? {
+        IR::Length => match run(vars, g, runtime, result_fn, &ir.child(0))? {
             Value::List(arr) => Ok(Value::Int(arr.len() as _)),
             _ => Err("Length operator requires a list".to_string()),
         },
-        IR::GetElement(op) => {
-            let arr = run(vars, g, runtime, result_fn, &op.0)?;
-            let i = run(vars, g, runtime, result_fn, &op.1)?;
+        IR::GetElement => {
+            let arr = run(vars, g, runtime, result_fn, &ir.child(0))?;
+            let i = run(vars, g, runtime, result_fn, &ir.child(1))?;
             match (arr, i) {
                 (Value::List(values), Value::Int(i)) => {
                     if i >= 0 && i < values.len() as _ {
@@ -1392,27 +1383,16 @@ pub fn run(
                 v => Err(format!("Type mismatch: expected List but was {v:?}")),
             }
         }
-        IR::GetElements(op) => {
-            let arr = run(vars, g, runtime, result_fn, &op.0)?;
-            match (&op.1, &op.2) {
-                (None, None) => get_elements(arr, None, None),
-                (None, Some(b)) => {
-                    get_elements(arr, None, Some(run(vars, g, runtime, result_fn, b)?))
-                }
-                (Some(a), None) => {
-                    get_elements(arr, Some(run(vars, g, runtime, result_fn, a)?), None)
-                }
-                (Some(a), Some(b)) => get_elements(
-                    arr,
-                    Some(run(vars, g, runtime, result_fn, a)?),
-                    Some(run(vars, g, runtime, result_fn, b)?),
-                ),
-            }
+        IR::GetElements => {
+            let arr = run(vars, g, runtime, result_fn, &ir.child(0))?;
+            let a = run(vars, g, runtime, result_fn, &ir.child(1))?;
+            let b = run(vars, g, runtime, result_fn, &ir.child(2))?;
+            get_elements(arr, a, b)
         }
-        IR::Range(op) => {
-            let start = run(vars, g, runtime, result_fn, &op.0)?;
-            let end = run(vars, g, runtime, result_fn, &op.1)?;
-            let step = run(vars, g, runtime, result_fn, &op.2)?;
+        IR::Range => {
+            let start = run(vars, g, runtime, result_fn, &ir.child(0))?;
+            let end = run(vars, g, runtime, result_fn, &ir.child(1))?;
+            let step = run(vars, g, runtime, result_fn, &ir.child(2))?;
             match (start, end, step) {
                 (Value::Int(start), Value::Int(end), Value::Int(step)) => {
                     if start >= end && step < 0 {
@@ -1437,22 +1417,22 @@ pub fn run(
                 _ => Err("Range operator requires two integers".to_string()),
             }
         }
-        IR::IsNull(ir) => match run(vars, g, runtime, result_fn, ir)? {
+        IR::IsNull => match run(vars, g, runtime, result_fn, &ir.child(0))? {
             Value::Null => Ok(Value::Bool(true)),
             _ => Ok(Value::Bool(false)),
         },
-        IR::IsNode(ir) => match run(vars, g, runtime, result_fn, ir)? {
+        IR::IsNode => match run(vars, g, runtime, result_fn, &ir.child(0))? {
             Value::Node(_) => Ok(Value::Bool(true)),
             _ => Ok(Value::Bool(false)),
         },
-        IR::Or(irs) => {
+        IR::Or => {
             let mut is_null = false;
-            for ir in irs {
-                match ro_run(vars, g, runtime, result_fn, ir)? {
+            for ir in ir.children() {
+                match run(vars, g, runtime, result_fn, &ir)? {
                     Value::Bool(true) => return Ok(Value::Bool(true)),
                     Value::Bool(false) => {}
                     Value::Null => is_null = true,
-                    _ => return Err(format!("Type mismatch: expected Bool but was {ir:?}")),
+                    _ => return Err(format!("Type mismatch: expected Bool but was {:?}", &ir)),
                 }
             }
             if is_null {
@@ -1461,10 +1441,10 @@ pub fn run(
 
             Ok(Value::Bool(false))
         }
-        IR::Xor(irs) => {
+        IR::Xor => {
             let mut last = None;
-            for ir in irs {
-                match run(vars, g, runtime, result_fn, ir)? {
+            for ir in ir.children() {
+                match run(vars, g, runtime, result_fn, &ir)? {
                     Value::Bool(b) => last = Some(last.map_or(b, |l| logical_xor(l, b))),
                     Value::Null => return Ok(Value::Null),
                     _ => return Err(format!("Type mismatch: expected Bool but was {ir:?}")),
@@ -1472,10 +1452,10 @@ pub fn run(
             }
             Ok(Value::Bool(last.unwrap_or(false)))
         }
-        IR::And(irs) => {
+        IR::And => {
             let mut is_null = false;
-            for ir in irs {
-                match run(vars, g, runtime, result_fn, ir)? {
+            for ir in ir.children() {
+                match run(vars, g, runtime, result_fn, &ir)? {
                     Value::Bool(false) => return Ok(Value::Bool(false)),
                     Value::Bool(true) => {}
                     Value::Null => is_null = true,
@@ -1488,12 +1468,12 @@ pub fn run(
 
             Ok(Value::Bool(true))
         }
-        IR::Not(ir) => match run(vars, g, runtime, result_fn, ir)? {
+        IR::Not => match run(vars, g, runtime, result_fn, &ir.child(0))? {
             Value::Bool(b) => Ok(Value::Bool(!b)),
             Value::Null => Ok(Value::Null),
             _ => Err("InvalidArgumentType: Not operator requires a boolean or null".to_string()),
         },
-        IR::Negate(ir) => match run(vars, g, runtime, result_fn, ir)? {
+        IR::Negate => match run(vars, g, runtime, result_fn, &ir.child(0))? {
             Value::Int(i) => Ok(Value::Int(-i)),
             Value::Float(f) => Ok(Value::Float(-f)),
             Value::Null => Ok(Value::Null),
@@ -1501,97 +1481,97 @@ pub fn run(
                 Err("InvalidArgumentType: Negate operator requires an Integer or Float".to_string())
             }
         },
-        IR::Eq(irs) => {
-            let iter = irs.iter().map(|ir| run(vars, g, runtime, result_fn, ir));
-            all_equals(iter)
-        }
-        IR::Neq(irs) => irs
-            .iter()
-            .flat_map(|ir| run(vars, g, runtime, result_fn, ir))
+        IR::Eq => all_equals(
+            ir.children()
+                .map(|ir| run(vars, g, runtime, result_fn, &ir)),
+        ),
+        IR::Neq => ir
+            .children()
+            .flat_map(|ir| run(vars, g, runtime, result_fn, &ir))
             .reduce(|a, b| Value::Bool(a != b))
             .ok_or_else(|| "Neq operator requires at least one argument".to_string()),
-        IR::Lt(op) => match (
-            run(vars, g, runtime, result_fn, &op.0)?,
-            run(vars, g, runtime, result_fn, &op.1)?,
+        IR::Lt => match (
+            run(vars, g, runtime, result_fn, &ir.child(0))?,
+            run(vars, g, runtime, result_fn, &ir.child(1))?,
         ) {
             (Value::Int(a), Value::Int(b)) => Ok(Value::Bool(a < b)),
             _ => Err("Lt operator requires two integers".to_string()),
         },
-        IR::Gt(op) => match (
-            run(vars, g, runtime, result_fn, &op.0)?,
-            run(vars, g, runtime, result_fn, &op.1)?,
+        IR::Gt => match (
+            run(vars, g, runtime, result_fn, &ir.child(0))?,
+            run(vars, g, runtime, result_fn, &ir.child(1))?,
         ) {
             (Value::Int(a), Value::Int(b)) => Ok(Value::Bool(a > b)),
             _ => Err("Gt operator requires two integers".to_string()),
         },
-        IR::Le(op) => match (
-            run(vars, g, runtime, result_fn, &op.0)?,
-            run(vars, g, runtime, result_fn, &op.1)?,
+        IR::Le => match (
+            run(vars, g, runtime, result_fn, &ir.child(0))?,
+            run(vars, g, runtime, result_fn, &ir.child(1))?,
         ) {
             (Value::Int(a), Value::Int(b)) => Ok(Value::Bool(a <= b)),
             _ => Err("Le operator requires two integers".to_string()),
         },
-        IR::Ge(op) => match (
-            run(vars, g, runtime, result_fn, &op.0)?,
-            run(vars, g, runtime, result_fn, &op.1)?,
+        IR::Ge => match (
+            run(vars, g, runtime, result_fn, &ir.child(0))?,
+            run(vars, g, runtime, result_fn, &ir.child(1))?,
         ) {
             (Value::Int(a), Value::Int(b)) => Ok(Value::Bool(a >= b)),
             _ => Err("Ge operator requires two integers".to_string()),
         },
-        IR::In(op) => {
-            let value = run(vars, g, runtime, result_fn, &op.0)?;
-            let list = run(vars, g, runtime, result_fn, &op.1)?;
+        IR::In => {
+            let value = run(vars, g, runtime, result_fn, &ir.child(0))?;
+            let list = run(vars, g, runtime, result_fn, &ir.child(1))?;
             list_contains(&list, &value)
         }
-        IR::Add(irs) => irs
-            .iter()
-            .map(|ir| run(vars, g, runtime, result_fn, ir))
+        IR::Add => ir
+            .children()
+            .map(|ir| run(vars, g, runtime, result_fn, &ir))
             .reduce(|acc, value| acc? + value?)
             .ok_or_else(|| "Add operator requires at least one operand".to_string())?,
-        IR::Sub(irs) => irs
-            .iter()
-            .flat_map(|ir| run(vars, g, runtime, result_fn, ir))
+        IR::Sub => ir
+            .children()
+            .flat_map(|ir| run(vars, g, runtime, result_fn, &ir))
             .reduce(|a, b| match (a, b) {
                 (Value::Int(a), Value::Int(b)) => Value::Int(a - b),
                 _ => Value::Null,
             })
             .ok_or_else(|| "Sub operator requires at least one argument".to_string()),
-        IR::Mul(irs) => irs
-            .iter()
-            .flat_map(|ir| run(vars, g, runtime, result_fn, ir))
+        IR::Mul => ir
+            .children()
+            .flat_map(|ir| run(vars, g, runtime, result_fn, &ir))
             .reduce(|a, b| match (a, b) {
                 (Value::Int(a), Value::Int(b)) => Value::Int(a * b),
                 _ => Value::Null,
             })
             .ok_or_else(|| "Mul operator requires at least one argument".to_string()),
-        IR::Div(irs) => irs
-            .iter()
-            .flat_map(|ir| run(vars, g, runtime, result_fn, ir))
+        IR::Div => ir
+            .children()
+            .flat_map(|ir| run(vars, g, runtime, result_fn, &ir))
             .reduce(|a, b| match (a, b) {
                 (Value::Int(a), Value::Int(b)) => Value::Int(a / b),
                 _ => Value::Null,
             })
             .ok_or_else(|| "Div operator requires at least one argument".to_string()),
-        IR::Pow(irs) => irs
-            .iter()
-            .flat_map(|ir| run(vars, g, runtime, result_fn, ir))
+        IR::Pow => ir
+            .children()
+            .flat_map(|ir| run(vars, g, runtime, result_fn, &ir))
             .reduce(|a, b| match (a, b) {
                 (Value::Int(a), Value::Int(b)) => Value::Float((a as f64).powf(b as _)),
                 _ => Value::Null,
             })
             .ok_or_else(|| "Pow operator requires at least one argument".to_string()),
-        IR::Modulo(irs) => irs
-            .iter()
-            .flat_map(|ir| ro_run(vars, g, runtime, result_fn, ir))
+        IR::Modulo => ir
+            .children()
+            .flat_map(|ir| run(vars, g, runtime, result_fn, &ir))
             .reduce(|a, b| match (a, b) {
                 (Value::Int(a), Value::Int(b)) => Value::Int(a % b),
                 _ => Value::Null,
             })
             .ok_or_else(|| "Modulo operator requires at least one argument".to_string()),
-        IR::FuncInvocation(name, irs) => {
-            let args = irs
-                .iter()
-                .map(|ir| run(vars, g, runtime, result_fn, ir))
+        IR::FuncInvocation(name) => {
+            let args = ir
+                .children()
+                .map(|ir| run(vars, g, runtime, result_fn, &ir))
                 .collect::<Result<Vec<_>, _>>()?;
             if let Some(func) = runtime.write_functions.get(name) {
                 func(g, runtime, args)
@@ -1601,44 +1581,44 @@ pub fn run(
                 Err(format!("Function {name} not found"))
             }
         }
-        IR::Map(items) => Ok(Value::Map(
-            items
-                .iter()
-                .map(|(key, ir)| {
+        IR::Map => Ok(Value::Map(
+            ir.children()
+                .map(|child| {
                     (
-                        key.to_string(),
-                        run(vars, g, runtime, result_fn, ir).unwrap_or(Value::Null),
+                        child.data().to_string(),
+                        run(vars, g, runtime, result_fn, &child.child(0)).unwrap_or(Value::Null),
                     )
                 })
                 .collect(),
         )),
-        IR::Set(x, ir) => {
-            let v = run(vars, g, runtime, result_fn, ir)?;
+        IR::Set(x) => {
+            let v = run(vars, g, runtime, result_fn, &ir.child(0))?;
             vars.insert(x.to_string(), v.clone());
             Ok(v)
         }
-        IR::If(op) => match run(vars, g, runtime, result_fn, &op.0)? {
-            Value::Bool(true) => run(vars, g, runtime, result_fn, &op.1),
+        IR::If => match run(vars, g, runtime, result_fn, &ir.child(0))? {
+            Value::Bool(true) => run(vars, g, runtime, result_fn, &ir.child(1)),
             _ => Ok(Value::Null),
         },
-        IR::For(op) => {
-            run(vars, g, runtime, result_fn, &op.0)?;
-            while run(vars, g, runtime, result_fn, &op.1)? == Value::Bool(true) {
-                run(vars, g, runtime, result_fn, &op.3)?;
-                run(vars, g, runtime, result_fn, &op.2)?;
+        IR::For => {
+            run(vars, g, runtime, result_fn, &ir.child(0))?;
+            while run(vars, g, runtime, result_fn, &ir.child(1))? == Value::Bool(true) {
+                run(vars, g, runtime, result_fn, &ir.child(3))?;
+                run(vars, g, runtime, result_fn, &ir.child(2))?;
             }
             Ok(Value::Null)
         }
-        IR::Return(ir) => {
-            let v = run(vars, g, runtime, result_fn, ir)?;
+        IR::Return => {
+            let v = run(vars, g, runtime, result_fn, &ir.child(0))?;
             result_fn(g, v);
             Ok(Value::Null)
         }
-        IR::Block(irs) => {
-            for ir in irs {
-                run(vars, g, runtime, result_fn, ir)?;
+        IR::Block => {
+            let mut v = Value::Null;
+            for ir in ir.children() {
+                v = run(vars, g, runtime, result_fn, &ir)?;
             }
-            Ok(Value::Null)
+            Ok(v)
         }
     }
 }
@@ -1671,11 +1651,11 @@ pub fn evaluate_param(expr: QueryExprIR) -> Value {
 
 fn get_elements(
     arr: Value,
-    start: Option<Value>,
-    end: Option<Value>,
+    start: Value,
+    end: Value,
 ) -> Result<Value, String> {
     match (arr, start, end) {
-        (Value::List(values), Some(Value::Int(mut start)), Some(Value::Int(mut end))) => {
+        (Value::List(values), Value::Int(mut start), Value::Int(mut end)) => {
             if start < 0 {
                 start = (values.len() as i64 + start).max(0);
             }
@@ -1689,24 +1669,7 @@ fn get_elements(
             }
             Ok(Value::List(values[start as usize..end as usize].to_vec()))
         }
-        (Value::List(values), None, Some(Value::Int(mut end))) => {
-            if end < 0 {
-                end = (values.len() as i64 + end).max(0);
-            } else {
-                end = end.min(values.len() as i64);
-            }
-            Ok(Value::List(values[..end as usize].to_vec()))
-        }
-        (Value::List(values), Some(Value::Int(mut start)), None) => {
-            if start < 0 {
-                start = (values.len() as i64 + start).max(0);
-            }
-            start = start.min(values.len() as i64);
-            Ok(Value::List(values[start as usize..].to_vec()))
-        }
-        (_, Some(Value::Null), _) | (_, _, Some(Value::Null)) => Ok(Value::Null),
-        (Value::List(values), None, None) => Ok(Value::List(values)),
-
+        (_, Value::Null, _) | (_, _, Value::Null) => Ok(Value::Null),
         _ => Err("Invalid array range parameters.".to_string()),
     }
 }
diff --git a/src/lib.rs b/src/lib.rs
index 4fa859ca..1dff3886 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -353,7 +353,7 @@ fn graph_parse(
 
     let mut parser = Parser::new(query);
     match parser.parse() {
-        Ok(ir) => Ok(RedisValue::SimpleString(format!("{ir:?}"))),
+        Ok(ir) => Ok(RedisValue::BulkString(format!("{ir:?}"))),
         Err(err) => Err(RedisError::String(err)),
     }
 }
@@ -370,7 +370,7 @@ fn graph_plan(
         Ok(ir) => {
             let mut planner = Planner::new();
             let ir = planner.plan(ir, false);
-            Ok(RedisValue::SimpleString(format!("{ir:?}")))
+            Ok(RedisValue::BulkString(format!("{ir}")))
         }
         Err(err) => Err(RedisError::String(err)),
     }


Reviews:
user: login: coderabbitai[bot]
body: **Actionable comments posted: 3**

<details>
<summary>🔭 Outside diff range comments (3)</summary><blockquote>

<details>
<summary>graph/src/ast.rs (1)</summary><blockquote>

`102-108`: _🛠️ Refactor suggestion_

**Add semantic checks for unary operators (`NOT`, `IS NULL`, `NEGATE`).**

`inner_validate` merely recurses into the operand but does not verify the operand’s type, so invalid expressions such as `NOT 42` or `- "abc"` will pass validation and only fail at runtime.  Performing inexpensive static checks here would catch obvious user mistakes earlier and keep runtime simpler.

```diff
- Self::Not(expr) | Self::IsNull(expr) | Self::Negate(expr) => expr.inner_validate(env),
+ Self::Not(expr) => {
+     if matches!(**expr, Self::Bool(_) | Self::Parameter(_) | Self::Ident(_)) {
+         expr.inner_validate(env)
+     } else {
+         Err("Type mismatch: expected Boolean expression".into())
+     }
+ }
+ Self::IsNull(expr) => expr.inner_validate(env),
+ Self::Negate(expr) => {
+     if matches!(**expr, Self::Integer(_) | Self::Float(_)) {
+         expr.inner_validate(env)
+     } else {
+         Err("Type mismatch: expected numeric expression".into())
+     }
+ }
```

This keeps the validation rules consistent with the expectations enforced in the runtime.

</blockquote></details>
<details>
<summary>graph/src/runtime.rs (2)</summary><blockquote>

`985-999`: _⚠️ Potential issue_

**`range()` semantics diverge between read-only and mutating execution paths.**

`ro_run` produces an ascending list even for a negative step, whereas `run` (mutating path) produces a descending list or empty vector (lines 1268-1284).  Clients calling the read-only API will therefore observe different results from the mutating API for identical Cypher.

Align the two implementations — simplest fix is to copy the `run` logic into `ro_run`.

---

`1563-1576`: _⚠️ Potential issue_

**`all_equals` does not implement the documented semantics.**

The comment says `1 EQ 2 AND 2 EQ 3`, but the code always compares each element to the *first* one.  
Additionally, `prev` is never updated inside the loop.

```diff
- let prev = first?;
- for next in iter {
-     let next = next?;
-     match prev.partial_cmp(&next) {
+ let mut prev = first?;
+ for next in iter {
+     let next = next?;
+     match prev.partial_cmp(&next) {
         …
     }
+     prev = next;           // advance window
 }
```

Without this change `[1, 2, 2]` incorrectly evaluates to `true`.

</blockquote></details>

</blockquote></details>

<details>
<summary>📜 Review details</summary>

**Configuration used: CodeRabbit UI**
**Review profile: CHILL**
**Plan: Pro**

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between 40a9e56e5fb7f8cfd76328f538511cd0ebad71f7 and 8c4f2ca866f9fe667b8f7721d6da479753b8586e.

</details>

<details>
<summary>⛔ Files ignored due to path filters (2)</summary>

* `Cargo.lock` is excluded by `!**/*.lock`
* `graph/Cargo.lock` is excluded by `!**/*.lock`

</details>

<details>
<summary>📒 Files selected for processing (7)</summary>

* `graph/Cargo.toml` (1 hunks)
* `graph/src/ast.rs` (2 hunks)
* `graph/src/cypher.rs` (2 hunks)
* `graph/src/graph.rs` (4 hunks)
* `graph/src/planner.rs` (6 hunks)
* `graph/src/runtime.rs` (18 hunks)
* `src/lib.rs` (2 hunks)

</details>

<details>
<summary>🔇 Additional comments (12)</summary><blockquote>

<details>
<summary>graph/Cargo.toml (1)</summary>

`17-17`: **Added dependency to support the new tree-based IR structure.**

The addition of the `orx-tree` crate dependency is essential for implementing the tree-based structure refactoring of the IR. This change aligns well with the overall PR objective.

</details>
<details>
<summary>graph/src/graph.rs (4)</summary>

`7-7`: **New import for tree-based data structure.**

Adding the `DynTree` import from the newly added `orx-tree` crate to support the refactoring from enum-based to tree-based IR representation.

---

`37-37`: **Changed Graph cache type to use tree-based structure.**

The cache field type has been updated from `Mutex<BTreeMap<String, IR>>` to `Mutex<BTreeMap<String, DynTree<IR>>>` to store the new tree-based IR representation instead of the previous recursive enum structure.

---

`168-169`: **Updated to access the root node of the IR tree.**

Modified to pass a reference to the root node of the tree instead of the IR value directly, using `evaluate.root()` to get the root node from the `DynTree<IR>`.

---

`236-237`: **Updated to access the root node of the IR tree in ro_query.**

Similar to the change in the `query` method, this modifies the read-only query method to pass a reference to the root node of the tree.

</details>
<details>
<summary>src/lib.rs (2)</summary>

`356-357`: **Changed IR output format in graph_parse to BulkString.**

Updated the return type from `RedisValue::SimpleString` to `RedisValue::BulkString` for the IR debug output, which is appropriate for potentially large tree-based IR representations.

---

`373-374`: **Updated IR formatting in graph_plan.**

Changed both the return type (from `SimpleString` to `BulkString`) and the formatting method (from debug format `{ir:?}` to display format `{ir}`). This indicates a new `Display` implementation must be available for the tree-based IR structure.

</details>
<details>
<summary>graph/src/cypher.rs (5)</summary>

`751-755`: **Refactored property access expression to use function invocation form.**

Property access is now represented as a function invocation with the name "property" and arguments being the base expression and the property name. This approach unifies the IR structure by using a generic pattern instead of a dedicated enum variant.

---

`838-842`: **Refactored StartsWith predicate to use function invocation form.**

The string predicate `StartsWith` is now represented as a function invocation with the name "@starts_with" and the string and pattern as arguments. This is part of the broader simplification of the IR structure.

---

`847-851`: **Refactored EndsWith predicate to use function invocation form.**

Similar to the StartsWith predicate, EndsWith is now represented as a function invocation with the name "@ends_with" and the appropriate arguments.

---

`855-859`: **Refactored Contains predicate to use function invocation form.**

The Contains predicate has been refactored to use a function invocation with the name "@contains" instead of a dedicated enum variant.

---

`863-867`: **Refactored RegexMatches predicate to use function invocation form.**

The RegexMatches predicate is now represented as a function invocation with the name "@regex_matches" and the string and pattern as arguments, consistent with the other string predicates.

</details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-30T07:50:11Z
user: login: barakb
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-04-30T08:30:14Z

Review comments:
diff_hunk: @@ -1175,44 +1166,44 @@ pub fn ro_run(
                 Err(format!("Function {name} not found"))
             }
         }
-        IR::Map(items) => Ok(Value::Map(
-            items
-                .iter()
-                .map(|(key, ir)| {
+        IR::Map => Ok(Value::Map(
+            ir.children()
+                .map(|child| {
                     (
-                        key.to_string(),
-                        ro_run(vars, g, runtime, result_fn, ir).unwrap_or(Value::Null),
+                        child.data().to_string(),
+                        ro_run(vars, g, runtime, result_fn, &child.child(0)).unwrap_or(Value::Null),
                     )
                 })
                 .collect(),
path: graph/src/runtime.rs
user: login: coderabbitai[bot]
body: _⚠️ Potential issue_

**Error handling in `IR::Map` silently swallows failures.**

`unwrap_or(Value::Null)` hides real errors (e.g. undefined variables) and returns `null` instead, making debugging extremely hard.

```diff
- ir.children()
-     .map(|child| {
-         (child.data().to_string(),
-          ro_run(vars, g, runtime, result_fn, &child.child(0)).unwrap_or(Value::Null))
-     })
-     .collect()
+ ir.children()
+     .map(|child| {
+         let key = child.data().to_string();
+         let val = ro_run(vars, g, runtime, result_fn, &child.child(0))?;
+         Ok((key, val))
+     })
+     .collect::<Result<_, _>>()?
```

The same pattern exists in the mutating `run` implementation (≈ lines 1455-1463) and should be fixed there as well.  



<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

`````suggestion
IR::Map => Ok(Value::Map(
    ir.children()
        .map(|child| {
            let key = child.data().to_string();
            let val = ro_run(vars, g, runtime, result_fn, &child.child(0))?;
            Ok((key, val))
        })
        .collect::<Result<_, _>>()?,
)),
`````

</details>

<!-- suggestion_end -->

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-04-30T07:50:10Z
author_association: CONTRIBUTOR
diff_hunk: @@ -266,152 +339,181 @@ impl Planner {
         expr: QueryExprIR,
         iter: &mut IntoIter<QueryIR>,
         alias: String,
-    ) -> IR {
+    ) -> DynTree<IR> {
         let list = self.plan_expr(expr);
-        match list {
-            IR::Range(op) => {
-                let init = IR::Set(alias.to_string(), Box::new(op.0));
-                let condition = IR::Le(Box::new((IR::Var(alias.to_string()), op.1)));
-                let next = IR::Set(
-                    alias.to_string(),
-                    Box::new(IR::Add(vec![IR::Var(alias), op.2])),
-                );
-                let body_ir = iter.next().unwrap();
-                let body = self.plan_query(body_ir, iter);
-                IR::For(Box::new((init, condition, next, body)))
-            }
-            x => {
-                let list_var = self.next_var();
-                let index_var = self.next_var();
-                let list = IR::Set(list_var.to_string(), Box::new(x));
-                let init = IR::Set(index_var.to_string(), Box::new(IR::Integer(0)));
-                let condition = IR::Lt(Box::new((
-                    IR::Var(index_var.to_string()),
-                    IR::Length(Box::new(IR::Var(list_var.to_string()))),
-                )));
-                let next = IR::Set(
-                    index_var.to_string(),
-                    Box::new(IR::Add(vec![
-                        IR::Var(index_var.to_string()),
-                        IR::Integer(1),
-                    ])),
-                );
-                let body_ir = iter.next().unwrap();
-                let body = self.plan_query(body_ir, iter);
-                IR::Block(vec![
-                    list,
-                    IR::For(Box::new((
-                        init,
-                        condition,
-                        next,
-                        IR::Block(vec![
-                            IR::Set(
-                                alias,
-                                Box::new(IR::GetElement(Box::new((
-                                    IR::Var(list_var),
-                                    IR::Var(index_var),
-                                )))),
-                            ),
-                            body,
-                        ]),
-                    ))),
-                ])
-            }
+        if matches!(list.root().data(), IR::Range) {
+            let init = tree!(
+                IR::Set(alias.to_string()),
+                list.root().child(0).as_cloned_subtree()
+            );
+            let condition = tree!(
+                IR::Le,
+                tree!(IR::Var(alias.to_string())),
+                list.root().child(1).as_cloned_subtree()
+            );
+            let next = tree!(
+                IR::Set(alias.to_string()),
+                tree!(
+                    IR::Add,
+                    tree!(IR::Var(alias)),
+                    list.root().child(2).as_cloned_subtree()
+                )
+            );
+            let body_ir = iter.next().unwrap();
+            let body = self.plan_query(body_ir, iter);
+            tree!(IR::For, init, condition, next, body)
+        } else {
+            let list_var = self.next_var();
+            let index_var = self.next_var();
+            let list = tree!(IR::Set(list_var.to_string()), list);
+            let init = tree!(IR::Set(index_var.to_string()), tree!(IR::Integer(0)));
+            let condition = tree!(
+                IR::Lt,
+                tree!(IR::Var(index_var.to_string())),
+                tree!(IR::Length, tree!(IR::Var(list_var.to_string())))
+            );
+            let next = tree!(
+                IR::Set(index_var.to_string()),
+                tree!(
+                    IR::Add,
+                    tree!(IR::Var(index_var.to_string())),
+                    tree!(IR::Integer(1))
+                )
+            );
+            let body_ir = iter.next().unwrap();
+            let body = self.plan_query(body_ir, iter);
+            tree!(
+                IR::Block,
+                list,
+                tree!(
+                    IR::For,
+                    init,
+                    condition,
+                    next,
+                    tree!(
+                        IR::Block,
+                        tree!(
+                            IR::Set(alias),
+                            tree!(
+                                IR::GetElement,
+                                tree!(IR::Var(list_var)),
+                                tree!(IR::Var(index_var))
+                            )
+                        ),
+                        body
+                    )
+                )
+            )
         }
     }
 
     fn plan_node_scan(
         &mut self,
         node: NodePattern,
-        body: IR,
-    ) -> IR {
-        let init = IR::Block(vec![
-            IR::Set(
-                format!("iter_{}", node.alias),
-                Box::new(IR::FuncInvocation(
-                    String::from("create_node_iter"),
-                    vec![IR::List(node.labels.into_iter().map(IR::String).collect())],
-                )),
-            ),
-            IR::Set(
-                node.alias.to_string(),
-                Box::new(IR::FuncInvocation(
-                    String::from("next_node"),
-                    vec![IR::Var(format!("iter_{}", node.alias))],
-                )),
+        body: DynTree<IR>,
+    ) -> DynTree<IR> {
+        let init = tree!(
+            IR::Block,
+            tree!(
+                IR::Set(format!("iter_{}", node.alias)),
+                tree!(
+                    IR::FuncInvocation(String::from("create_node_iter")),
+                    tree!(IR::List ; node
+                            .labels
+                            .into_iter()
+                            .map(|label| tree!(IR::String(label))))
+                )
             ),
-        ]);
-        let condition = IR::IsNode(Box::new(IR::Var(node.alias.to_string())));
-        let next = IR::Set(
-            node.alias.to_string(),
-            Box::new(IR::FuncInvocation(
-                String::from("next_node"),
-                vec![IR::Var(format!("iter_{}", node.alias))],
-            )),
+            tree!(
+                IR::Set(node.alias.to_string()),
+                tree!(
+                    IR::FuncInvocation(String::from("next_node")),
+                    tree!(IR::Var(format!("iter_{}", node.alias)))
+                )
+            )
+        );
+        let condition = tree!(IR::IsNode, tree!(IR::Var(node.alias.to_string())));
+        let next = tree!(
+            IR::Set(node.alias.to_string()),
+            tree!(
+                IR::FuncInvocation(String::from("next_node")),
+                tree!(IR::Var(format!("iter_{}", node.alias)))
+            )
         );
-        IR::For(Box::new((init, condition, next, body)))
+        tree!(IR::For, init, condition, next, body)
     }
 
     fn plan_match(
         &mut self,
         pattern: Pattern,
         iter: &mut IntoIter<QueryIR>,
-    ) -> IR {
+    ) -> DynTree<IR> {
         if pattern.relationships.is_empty() {
             let mut body = self.plan_query(iter.next().unwrap(), iter);
             for node in pattern.nodes.into_iter().rev() {
                 body = self.plan_node_scan(node, body);
             }
             return body;
         }
-        IR::Null
+        tree!(IR::Null)
     }
path: graph/src/planner.rs
user: login: coderabbitai[bot]
body: _🛠️ Refactor suggestion_

**`MATCH` with relationships not yet planned.**

When `pattern.relationships` is non-empty the planner returns `tree!(IR::Null)`, effectively producing a no-op execution plan. Queries with relationship patterns will therefore return empty results without any warning.

Either implement relationship planning or surface a descriptive error so users know the feature is unsupported.

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-04-30T07:50:10Z
author_association: CONTRIBUTOR
diff_hunk: @@ -64,119 +150,90 @@ impl Planner {
     fn plan_expr(
         &mut self,
         expr: QueryExprIR,
-    ) -> IR {
+    ) -> DynTree<IR> {
         match expr {
-            QueryExprIR::Null => IR::Null,
-            QueryExprIR::Bool(x) => IR::Bool(x),
-            QueryExprIR::Integer(x) => IR::Integer(x),
-            QueryExprIR::Float(x) => IR::Float(x),
-            QueryExprIR::String(x) => IR::String(x),
-            QueryExprIR::Ident(x) => IR::Var(x),
-            QueryExprIR::Parameter(x) => IR::Parameter(x),
-            QueryExprIR::Named(name, expr) => IR::Set(name, Box::new(self.plan_expr(*expr))),
-            QueryExprIR::List(exprs) => {
-                IR::List(exprs.into_iter().map(|ir| self.plan_expr(ir)).collect())
-            }
-            QueryExprIR::Or(exprs) => {
-                IR::Or(exprs.into_iter().map(|ir| self.plan_expr(ir)).collect())
-            }
-            QueryExprIR::Xor(exprs) => {
-                IR::Xor(exprs.into_iter().map(|ir| self.plan_expr(ir)).collect())
-            }
-            QueryExprIR::And(exprs) => {
-                IR::And(exprs.into_iter().map(|ir| self.plan_expr(ir)).collect())
-            }
-            QueryExprIR::Not(expr) => IR::Not(Box::new(self.plan_expr(*expr))),
-            QueryExprIR::Negate(expr) => IR::Negate(Box::new(self.plan_expr(*expr))),
-
-            QueryExprIR::Eq(exprs) => {
-                IR::Eq(exprs.into_iter().map(|ir| self.plan_expr(ir)).collect())
-            }
+            QueryExprIR::Null => tree!(IR::Null),
+            QueryExprIR::Bool(x) => tree!(IR::Bool(x)),
+            QueryExprIR::Integer(x) => tree!(IR::Integer(x)),
+            QueryExprIR::Float(x) => tree!(IR::Float(x)),
+            QueryExprIR::String(x) => tree!(IR::String(x)),
+            QueryExprIR::Ident(x) => tree!(IR::Var(x)),
+            QueryExprIR::Parameter(x) => tree!(IR::Parameter(x)),
+            QueryExprIR::Named(name, expr) => tree!(IR::Set(name), self.plan_expr(*expr)),
+            QueryExprIR::List(exprs) => tree!(IR::List => self => exprs),
+            QueryExprIR::Or(exprs) => tree!(IR::Or => self => exprs),
+            QueryExprIR::Xor(exprs) => tree!(IR::Xor => self => exprs),
+            QueryExprIR::And(exprs) => tree!(IR::And => self => exprs),
+            QueryExprIR::Not(expr) => tree!(IR::Not, self.plan_expr(*expr)),
+            QueryExprIR::Negate(expr) => tree!(IR::Negate, self.plan_expr(*expr)),
+            QueryExprIR::Eq(exprs) => tree!(IR::Eq => self => exprs),
             QueryExprIR::Neq(_) => todo!(),
             QueryExprIR::Lt(_) => todo!(),
             QueryExprIR::Gt(_) => todo!(),
             QueryExprIR::Le(_) => todo!(),
             QueryExprIR::Ge(_) => todo!(),
-            QueryExprIR::In(op) => IR::In(Box::new((self.plan_expr(op.0), self.plan_expr(op.1)))),
-            QueryExprIR::Add(exprs) => {
-                IR::Add(exprs.into_iter().map(|ir| self.plan_expr(ir)).collect())
-            }
-            QueryExprIR::Sub(exprs) => {
-                IR::Sub(exprs.into_iter().map(|ir| self.plan_expr(ir)).collect())
-            }
-            QueryExprIR::Mul(exprs) => {
-                IR::Mul(exprs.into_iter().map(|ir| self.plan_expr(ir)).collect())
-            }
-            QueryExprIR::Div(exprs) => {
-                IR::Div(exprs.into_iter().map(|ir| self.plan_expr(ir)).collect())
-            }
-            QueryExprIR::Pow(exprs) => {
-                IR::Pow(exprs.into_iter().map(|ir| self.plan_expr(ir)).collect())
-            }
-            QueryExprIR::Modulo(exprs) => {
-                IR::Modulo(exprs.into_iter().map(|ir| self.plan_expr(ir)).collect())
-            }
-            QueryExprIR::IsNull(expr) => IR::IsNull(Box::new(self.plan_expr(*expr))),
+            QueryExprIR::In(op) => tree!(IR::In, self.plan_expr(op.0), self.plan_expr(op.1)),
path: graph/src/planner.rs
user: login: coderabbitai[bot]
body: _⚠️ Potential issue_

**Unimplemented expression variants raise `todo!()` at runtime.**

`Neq`, `Lt`, `Gt`, `Le`, and `Ge` currently fall through to `todo!()`, causing an unconditional panic when users write perfectly valid Cypher such as `a <> b` or `n.age > 30`.

Add the missing lowering logic or at least return a clear planner error to avoid crashing production.

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-04-30T07:50:10Z
author_association: CONTRIBUTOR

PR comments:
user: login: coderabbitai[bot]
created_at: 2025-04-30T07:46:11Z
body: <!-- This is an auto-generated comment: summarize by coderabbit.ai -->
<!-- This is an auto-generated comment: rate limited by coderabbit.ai -->

> [!WARNING]
> ## Rate limit exceeded
> 
> @AviAvni has exceeded the limit for the number of commits or files that can be reviewed per hour. Please wait **4 minutes and 1 seconds** before requesting another review.
> 
> <details>
> <summary>⌛ How to resolve this issue?</summary>
> 
> After the wait time has elapsed, a review can be triggered using the `@coderabbitai review` command as a PR comment. Alternatively, push new commits to this PR.
> 
> We recommend that you space out your commits to avoid hitting the rate limit.
> 
> </details>
> 
> 
> <details>
> <summary>🚦 How do rate limits work?</summary>
> 
> CodeRabbit enforces hourly rate limits for each developer per organization.
> 
> Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.
> 
> Please see our [FAQ](https://docs.coderabbit.ai/faq) for further information.
> 
> </details>
> 
> <details>
> <summary>📥 Commits</summary>
> 
> Reviewing files that changed from the base of the PR and between 8c4f2ca866f9fe667b8f7721d6da479753b8586e and 77eae4ac7c3c5f58a63e905b815109528ee6193b.
> 
> </details>
> 
> <details>
> <summary>⛔ Files ignored due to path filters (1)</summary>
> 
> * `Cargo.lock` is excluded by `!**/*.lock`
> 
> </details>
> 
> <details>
> <summary>📒 Files selected for processing (4)</summary>
> 
> * `graph/Cargo.toml` (1 hunks)
> * `graph/src/cypher.rs` (2 hunks)
> * `graph/src/planner.rs` (6 hunks)
> * `graph/src/runtime.rs` (18 hunks)
> 
> </details>

<!-- end of auto-generated comment: rate limited by coderabbit.ai -->
<!-- walkthrough_start -->

## Walkthrough

This update refactors the intermediate representation (IR) of query expressions and execution plans in the graph module. The IR transitions from a recursive enum-based structure to a tree-based structure using `DynTree<IR>`, with corresponding changes in the planner, runtime, and graph management code. Several enum variants related to property access and string predicates are removed from the AST and replaced by a generic function invocation form. Supporting code is updated to operate on the new tree-based IR, and the dependency on the `orx-tree` crate is introduced. Output formatting in the main library is also adjusted.

## Changes

| Files/Paths                               | Change Summary |
|-------------------------------------------|---------------|
| `graph/Cargo.toml`                        | Added `orx-tree` crate dependency. |
| `graph/src/ast.rs`                        | Removed `Property`, `StartsWith`, `EndsWith`, `Contains`, `RegexMatches` variants from `QueryExprIR`; added `Map` variant; updated validation logic and enum variant order. |
| `graph/src/cypher.rs`                     | Replaced construction of removed AST variants with generic `FuncInvocation` for property access and string predicates. |
| `graph/src/graph.rs`                      | Changed `cache` field in `Graph` from `Mutex<BTreeMap<String, IR>>` to `Mutex<BTreeMap<String, DynTree<IR>>>`; updated methods to use tree root nodes. |
| `graph/src/planner.rs`                    | Refactored IR to use `DynTree<IR>`; replaced recursive enum variants with tree nodes; added `tree!` macro; rewrote planner methods to build/manipulate IR trees; added `Display` for IR. |
| `graph/src/runtime.rs`                    | Updated evaluation logic to operate on `DynNode<IR>` tree nodes; refactored all pattern matching and recursive evaluation to use tree structure. |
| `src/lib.rs`                              | Changed parse and plan result formatting from `SimpleString` to `BulkString`; updated IR formatting to use `Display`. |

## Sequence Diagram(s)

```mermaid
sequenceDiagram
    participant User
    participant Parser
    participant Planner
    participant Graph
    participant Runtime

    User->>Parser: Submit query
    Parser->>Planner: Generate QueryExprIR/QueryIR
    Planner->>Planner: Build DynTree<IR> (tree-based IR)
    Planner->>Graph: Store DynTree<IR> in cache
    Graph->>Runtime: Execute DynTree<IR> via root node
    Runtime->>Runtime: Evaluate IR tree nodes recursively
    Runtime-->>User: Return result
```

## Suggested labels

`enhancement`

## Poem

> In the meadow of code, a tree has grown tall,  
> IRs branch and blossom, no longer boxed at all.  
> Old variants pruned, new functions take root,  
> The planner now sings with a tree-shaped flute.  
> With orx-tree’s help, our queries are free—  
> Hopping through nodes, as happy as can be!  
> 🐇🌳

<!-- walkthrough_end -->
<!-- internal state start -->


<!-- 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 -->

<!-- internal state end -->
<!-- finishing_touch_checkbox_start -->

<details open="true">
<summary>✨ Finishing Touches</summary>

- [ ] <!-- {"checkboxId": "7962f53c-55bc-4827-bfbf-6a18da830691"} --> 📝 Generate Docstrings

</details>

<!-- finishing_touch_checkbox_end -->
<!-- tips_start -->

---

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

<details>
<summary>❤️ Share</summary>

- [X](https://twitter.com/intent/tweet?text=I%20just%20used%20%40coderabbitai%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20the%20proprietary%20code.%20Check%20it%20out%3A&url=https%3A//coderabbit.ai)
- [Mastodon](https://mastodon.social/share?text=I%20just%20used%20%40coderabbitai%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20the%20proprietary%20code.%20Check%20it%20out%3A%20https%3A%2F%2Fcoderabbit.ai)
- [Reddit](https://www.reddit.com/submit?title=Great%20tool%20for%20code%20review%20-%20CodeRabbit&text=I%20just%20used%20CodeRabbit%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20proprietary%20code.%20Check%20it%20out%3A%20https%3A//coderabbit.ai)
- [LinkedIn](https://www.linkedin.com/sharing/share-offsite/?url=https%3A%2F%2Fcoderabbit.ai&mini=true&title=Great%20tool%20for%20code%20review%20-%20CodeRabbit&summary=I%20just%20used%20CodeRabbit%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20proprietary%20code)

</details>

<details>
<summary>🪧 Tips</summary>

### Chat

There are 3 ways to chat with [CodeRabbit](https://coderabbit.ai?utm_source=oss&utm_medium=github&utm_campaign=FalkorDB/falkordb-rs-next-gen&utm_content=54):

- Review comments: Directly reply to a review comment made by CodeRabbit. Example:
  - `I pushed a fix in commit <commit_id>, please review it.`
  - `Generate unit testing code for this file.`
  - `Open a follow-up GitHub issue for this discussion.`
- Files and specific lines of code (under the "Files changed" tab): Tag `@coderabbitai` in a new review comment at the desired location with your query. Examples:
  - `@coderabbitai generate unit testing code for this file.`
  -	`@coderabbitai modularize this function.`
- PR comments: Tag `@coderabbitai` in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
  - `@coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.`
  - `@coderabbitai read src/utils.ts and generate unit testing code.`
  - `@coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.`
  - `@coderabbitai help me debug CodeRabbit configuration file.`

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

### CodeRabbit Commands (Invoked using PR comments)

- `@coderabbitai pause` to pause the reviews on a PR.
- `@coderabbitai resume` to resume the paused reviews.
- `@coderabbitai review` to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
- `@coderabbitai full review` to do a full review from scratch and review all the files again.
- `@coderabbitai summary` to regenerate the summary of the PR.
- `@coderabbitai generate docstrings` to [generate docstrings](https://docs.coderabbit.ai/finishing-touches/docstrings) for this PR.
- `@coderabbitai generate sequence diagram` to generate a sequence diagram of the changes in this PR.
- `@coderabbitai resolve` resolve all the CodeRabbit review comments.
- `@coderabbitai configuration` to show the current CodeRabbit configuration for the repository.
- `@coderabbitai help` to get help.

### Other keywords and placeholders

- Add `@coderabbitai ignore` anywhere in the PR description to prevent this PR from being reviewed.
- Add `@coderabbitai summary` to generate the high-level summary at a specific location in the PR description.
- Add `@coderabbitai` anywhere in the PR title to generate the title automatically.

### CodeRabbit Configuration File (`.coderabbit.yaml`)

- You can programmatically configure CodeRabbit by adding a `.coderabbit.yaml` file to the root of your repository.
- Please see the [configuration documentation](https://docs.coderabbit.ai/guides/configure-coderabbit) for more information.
- If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: `# yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json`

### Documentation and Community

- Visit our [Documentation](https://docs.coderabbit.ai) for detailed information on how to use CodeRabbit.
- Join our [Discord Community](http://discord.gg/coderabbit) to get help, request features, and share feedback.
- Follow us on [X/Twitter](https://twitter.com/coderabbitai) for updates and announcements.

</details>

<!-- tips_end -->
author_association: CONTRIBUTOR
user: login: codecov[bot]
created_at: 2025-04-30T07:52:43Z
body: ## [Codecov](https://app.codecov.io/gh/FalkorDB/falkordb-rs-next-gen/pull/54?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=FalkorDB) Report
Attention: Patch coverage is `84.78261%` with `77 lines` in your changes missing coverage. Please review.
> Project coverage is 61.42%. Comparing base [(`2916308`)](https://app.codecov.io/gh/FalkorDB/falkordb-rs-next-gen/commit/2916308e19bc18b7d9f0829dda2ed48b1d1f5f74?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=FalkorDB) to head [(`77eae4a`)](https://app.codecov.io/gh/FalkorDB/falkordb-rs-next-gen/commit/77eae4ac7c3c5f58a63e905b815109528ee6193b?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=FalkorDB).
> Report is 3 commits behind head on main.

| [Files with missing lines](https://app.codecov.io/gh/FalkorDB/falkordb-rs-next-gen/pull/54?dropdown=coverage&src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=FalkorDB) | Patch % | Lines |
|---|---|---|
| [graph/src/planner.rs](https://app.codecov.io/gh/FalkorDB/falkordb-rs-next-gen/pull/54?src=pr&el=tree&filepath=graph%2Fsrc%2Fplanner.rs&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=FalkorDB#diff-Z3JhcGgvc3JjL3BsYW5uZXIucnM=) | 83.43% | [52 Missing :warning: ](https://app.codecov.io/gh/FalkorDB/falkordb-rs-next-gen/pull/54?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=FalkorDB) |
| [graph/src/runtime.rs](https://app.codecov.io/gh/FalkorDB/falkordb-rs-next-gen/pull/54?src=pr&el=tree&filepath=graph%2Fsrc%2Fruntime.rs&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=FalkorDB#diff-Z3JhcGgvc3JjL3J1bnRpbWUucnM=) | 86.22% | [23 Missing :warning: ](https://app.codecov.io/gh/FalkorDB/falkordb-rs-next-gen/pull/54?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=FalkorDB) |
| [src/lib.rs](https://app.codecov.io/gh/FalkorDB/falkordb-rs-next-gen/pull/54?src=pr&el=tree&filepath=src%2Flib.rs&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=FalkorDB#diff-c3JjL2xpYi5ycw==) | 0.00% | [2 Missing :warning: ](https://app.codecov.io/gh/FalkorDB/falkordb-rs-next-gen/pull/54?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=FalkorDB) |

<details><summary>Additional details and impacted files</summary>


```diff
@@            Coverage Diff             @@
##             main      #54      +/-   ##
==========================================
- Coverage   61.97%   61.42%   -0.56%     
==========================================
  Files           8        8              
  Lines        5378     5371       -7     
==========================================
- Hits         3333     3299      -34     
- Misses       2045     2072      +27     
```

</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/FalkorDB/falkordb-rs-next-gen/pull/54?dropdown=coverage&src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=FalkorDB).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=FalkorDB).

<details><summary> :rocket: New features to boost your workflow: </summary>

- :snowflake: [Test Analytics](https://docs.codecov.com/docs/test-analytics): Detect flaky tests, report on failures, and find test suite problems.
</details>
author_association: NONE