Skip to main content

oxirs_arq/
expression_compiler.rs

1//! SPARQL Expression Compiler and Evaluator with LRU Caching
2//!
3//! This module implements a two-phase pipeline:
4//!   1. `ExprCompiler::compile` — parses an expression string into `CompiledExpr`
5//!   2. `ExprCompiler::evaluate` — evaluates a `CompiledExpr` against variable bindings
6//!
7//! An `ExprCache` wraps the compiler with an LRU cache (VecDeque + HashMap) to avoid
8//! re-parsing frequently used expressions.
9//!
10//! # Supported syntax
11//!
12//! - Integer and floating-point literals (`42`, `3.14`)
13//! - Double-quoted string literals (`"hello"`)
14//! - IRI references (`<http://example.org/>`)
15//! - Variable references (`?varName`)
16//! - Unary minus (`-expr`) and logical NOT (`!expr`)
17//! - Binary arithmetic: `+`, `-`, `*`, `/`
18//! - Binary comparison: `=`, `!=`, `<`, `<=`, `>`, `>=`
19//! - Binary logical: `&&`, `||`
20//! - `IF(cond, then, else)`
21//! - SPARQL built-in calls: `BOUND`, `ISIRI`, `ISLITERAL`, `ISBLANK`,
22//!   `STR`, `LANG`, `DATATYPE`, `COALESCE`
23//! - Arbitrary function calls: `FUNC(arg1, arg2, ...)`
24
25use std::collections::{HashMap, VecDeque};
26use std::fmt;
27
28// ─── Value ─────────────────────────────────────────────────────────────────
29
30/// A runtime value produced by evaluating a SPARQL expression.
31#[derive(Debug, Clone, PartialEq)]
32pub enum ExprValue {
33    /// Boolean result (e.g. from comparison or BOUND)
34    Bool(bool),
35    /// Integer numeric value
36    Integer(i64),
37    /// Double-precision floating-point value
38    Double(f64),
39    /// Plain or language-tagged string literal
40    Str(String),
41    /// IRI value
42    Iri(String),
43    /// Blank-node identifier
44    Blank(String),
45    /// Unbound variable (no value in binding map)
46    Unbound,
47}
48
49impl fmt::Display for ExprValue {
50    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
51        match self {
52            Self::Bool(b) => write!(f, "{b}"),
53            Self::Integer(i) => write!(f, "{i}"),
54            Self::Double(d) => write!(f, "{d}"),
55            Self::Str(s) => write!(f, "\"{s}\""),
56            Self::Iri(i) => write!(f, "<{i}>"),
57            Self::Blank(b) => write!(f, "_:{b}"),
58            Self::Unbound => write!(f, "UNBOUND"),
59        }
60    }
61}
62
63// ─── Binary operators ───────────────────────────────────────────────────────
64
65/// Binary operators supported in compiled expressions.
66#[derive(Debug, Clone, PartialEq, Eq)]
67pub enum BinOp {
68    /// Addition
69    Add,
70    /// Subtraction
71    Sub,
72    /// Multiplication
73    Mul,
74    /// Division
75    Div,
76    /// Equality
77    Eq,
78    /// Inequality
79    Ne,
80    /// Less-than
81    Lt,
82    /// Less-than-or-equal
83    Le,
84    /// Greater-than
85    Gt,
86    /// Greater-than-or-equal
87    Ge,
88    /// Logical AND
89    And,
90    /// Logical OR
91    Or,
92}
93
94// ─── Compiled expression ────────────────────────────────────────────────────
95
96/// A compiled SPARQL expression tree.
97#[derive(Debug, Clone)]
98pub enum CompiledExpr {
99    /// A literal value (string representation stored, parsed at eval time)
100    Literal(String),
101    /// A variable reference (without the leading `?`)
102    Variable(String),
103    /// An IRI reference (without angle brackets)
104    IriRef(String),
105    /// Unary arithmetic negation
106    Neg(Box<CompiledExpr>),
107    /// Logical NOT
108    Not(Box<CompiledExpr>),
109    /// Binary operation
110    BinOp(BinOp, Box<CompiledExpr>, Box<CompiledExpr>),
111    /// Function call: function name + argument list
112    FuncCall(String, Vec<CompiledExpr>),
113    /// Conditional IF(condition, then-branch, else-branch)
114    If(Box<CompiledExpr>, Box<CompiledExpr>, Box<CompiledExpr>),
115}
116
117// ─── Errors ─────────────────────────────────────────────────────────────────
118
119/// Error returned when an expression string cannot be parsed.
120#[derive(Debug, Clone)]
121pub struct CompileError(pub String);
122
123impl fmt::Display for CompileError {
124    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
125        write!(f, "CompileError: {}", self.0)
126    }
127}
128
129impl std::error::Error for CompileError {}
130
131/// Error returned when a compiled expression cannot be evaluated.
132#[derive(Debug, Clone)]
133pub struct EvalError(pub String);
134
135impl fmt::Display for EvalError {
136    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
137        write!(f, "EvalError: {}", self.0)
138    }
139}
140
141impl std::error::Error for EvalError {}
142
143// ─── Parser internals ────────────────────────────────────────────────────────
144
145/// A simple hand-written recursive-descent parser for SPARQL filter expressions.
146struct Parser<'a> {
147    input: &'a [u8],
148    pos: usize,
149}
150
151impl<'a> Parser<'a> {
152    fn new(s: &'a str) -> Self {
153        Self {
154            input: s.as_bytes(),
155            pos: 0,
156        }
157    }
158
159    fn peek(&self) -> Option<u8> {
160        self.input.get(self.pos).copied()
161    }
162
163    fn advance(&mut self) {
164        if self.pos < self.input.len() {
165            self.pos += 1;
166        }
167    }
168
169    fn skip_ws(&mut self) {
170        while let Some(c) = self.peek() {
171            if c == b' ' || c == b'\t' || c == b'\r' || c == b'\n' {
172                self.advance();
173            } else {
174                break;
175            }
176        }
177    }
178
179    fn expect(&mut self, ch: u8) -> Result<(), CompileError> {
180        self.skip_ws();
181        match self.peek() {
182            Some(c) if c == ch => {
183                self.advance();
184                Ok(())
185            }
186            other => Err(CompileError(format!(
187                "expected '{}' at pos {}, got {:?}",
188                ch as char,
189                self.pos,
190                other.map(|b| b as char)
191            ))),
192        }
193    }
194
195    /// Parse a comma-separated list of expressions up to a closing paren.
196    fn parse_arg_list(&mut self) -> Result<Vec<CompiledExpr>, CompileError> {
197        self.expect(b'(')?;
198        self.skip_ws();
199        let mut args = Vec::new();
200        if self.peek() == Some(b')') {
201            self.advance();
202            return Ok(args);
203        }
204        loop {
205            args.push(self.parse_or()?);
206            self.skip_ws();
207            match self.peek() {
208                Some(b',') => {
209                    self.advance();
210                }
211                Some(b')') => {
212                    self.advance();
213                    break;
214                }
215                other => {
216                    return Err(CompileError(format!(
217                        "expected ',' or ')' at pos {}, got {:?}",
218                        self.pos,
219                        other.map(|b| b as char)
220                    )))
221                }
222            }
223        }
224        Ok(args)
225    }
226
227    // ── Grammar (ascending precedence) ──────────────────────────────────────
228    // or  → and ( "||" and )*
229    // and → cmp ( "&&" cmp )*
230    // cmp → add ( ("="|"!="|"<"|"<="|">"|">=") add )?
231    // add → mul ( ("+"|"-") mul )*
232    // mul → unary ( ("*"|"/") unary )*
233    // unary → "-" unary | "!" unary | primary
234    // primary → literal | variable | iri | func | "(" or ")"
235
236    fn parse_or(&mut self) -> Result<CompiledExpr, CompileError> {
237        let mut left = self.parse_and()?;
238        loop {
239            self.skip_ws();
240            if self.input.get(self.pos..self.pos + 2) == Some(b"||") {
241                self.pos += 2;
242                let right = self.parse_and()?;
243                left = CompiledExpr::BinOp(BinOp::Or, Box::new(left), Box::new(right));
244            } else {
245                break;
246            }
247        }
248        Ok(left)
249    }
250
251    fn parse_and(&mut self) -> Result<CompiledExpr, CompileError> {
252        let mut left = self.parse_cmp()?;
253        loop {
254            self.skip_ws();
255            if self.input.get(self.pos..self.pos + 2) == Some(b"&&") {
256                self.pos += 2;
257                let right = self.parse_cmp()?;
258                left = CompiledExpr::BinOp(BinOp::And, Box::new(left), Box::new(right));
259            } else {
260                break;
261            }
262        }
263        Ok(left)
264    }
265
266    fn parse_cmp(&mut self) -> Result<CompiledExpr, CompileError> {
267        let left = self.parse_add()?;
268        self.skip_ws();
269        let op = if self.input.get(self.pos..self.pos + 2) == Some(b"!=") {
270            self.pos += 2;
271            Some(BinOp::Ne)
272        } else if self.input.get(self.pos..self.pos + 2) == Some(b"<=") {
273            self.pos += 2;
274            Some(BinOp::Le)
275        } else if self.input.get(self.pos..self.pos + 2) == Some(b">=") {
276            self.pos += 2;
277            Some(BinOp::Ge)
278        } else if self.peek() == Some(b'=') {
279            self.pos += 1;
280            Some(BinOp::Eq)
281        } else if self.peek() == Some(b'<') {
282            self.pos += 1;
283            Some(BinOp::Lt)
284        } else if self.peek() == Some(b'>') {
285            self.pos += 1;
286            Some(BinOp::Gt)
287        } else {
288            None
289        };
290        if let Some(op) = op {
291            let right = self.parse_add()?;
292            Ok(CompiledExpr::BinOp(op, Box::new(left), Box::new(right)))
293        } else {
294            Ok(left)
295        }
296    }
297
298    fn parse_add(&mut self) -> Result<CompiledExpr, CompileError> {
299        let mut left = self.parse_mul()?;
300        loop {
301            self.skip_ws();
302            match self.peek() {
303                Some(b'+') => {
304                    self.advance();
305                    let right = self.parse_mul()?;
306                    left = CompiledExpr::BinOp(BinOp::Add, Box::new(left), Box::new(right));
307                }
308                Some(b'-') => {
309                    // Don't consume yet — could be a unary minus in next primary
310                    // Consume only if it really is a binary minus (preceded by operand)
311                    self.advance();
312                    let right = self.parse_mul()?;
313                    left = CompiledExpr::BinOp(BinOp::Sub, Box::new(left), Box::new(right));
314                }
315                _ => break,
316            }
317        }
318        Ok(left)
319    }
320
321    fn parse_mul(&mut self) -> Result<CompiledExpr, CompileError> {
322        let mut left = self.parse_unary()?;
323        loop {
324            self.skip_ws();
325            match self.peek() {
326                Some(b'*') => {
327                    self.advance();
328                    let right = self.parse_unary()?;
329                    left = CompiledExpr::BinOp(BinOp::Mul, Box::new(left), Box::new(right));
330                }
331                Some(b'/') => {
332                    self.advance();
333                    let right = self.parse_unary()?;
334                    left = CompiledExpr::BinOp(BinOp::Div, Box::new(left), Box::new(right));
335                }
336                _ => break,
337            }
338        }
339        Ok(left)
340    }
341
342    fn parse_unary(&mut self) -> Result<CompiledExpr, CompileError> {
343        self.skip_ws();
344        match self.peek() {
345            Some(b'-') => {
346                self.advance();
347                let inner = self.parse_unary()?;
348                Ok(CompiledExpr::Neg(Box::new(inner)))
349            }
350            Some(b'!') => {
351                self.advance();
352                let inner = self.parse_unary()?;
353                Ok(CompiledExpr::Not(Box::new(inner)))
354            }
355            _ => self.parse_primary(),
356        }
357    }
358
359    fn parse_primary(&mut self) -> Result<CompiledExpr, CompileError> {
360        self.skip_ws();
361        match self.peek() {
362            Some(b'"') => self.parse_string_literal(),
363            Some(b'<') => self.parse_iri(),
364            Some(b'?') => self.parse_variable(),
365            Some(b'(') => {
366                self.advance();
367                let expr = self.parse_or()?;
368                self.expect(b')')?;
369                Ok(expr)
370            }
371            Some(c) if c.is_ascii_digit() => self.parse_number(),
372            Some(c) if c.is_ascii_alphabetic() || c == b'_' => self.parse_name_or_call(),
373            other => Err(CompileError(format!(
374                "unexpected character at pos {}: {:?}",
375                self.pos,
376                other.map(|b| b as char)
377            ))),
378        }
379    }
380
381    fn parse_string_literal(&mut self) -> Result<CompiledExpr, CompileError> {
382        self.advance(); // consume opening '"'
383        let start = self.pos;
384        while let Some(c) = self.peek() {
385            if c == b'"' {
386                break;
387            }
388            if c == b'\\' {
389                self.advance();
390            }
391            self.advance();
392        }
393        let s = std::str::from_utf8(&self.input[start..self.pos])
394            .map_err(|e| CompileError(format!("UTF-8 error in string: {e}")))?
395            .to_string();
396        self.expect(b'"')?;
397        Ok(CompiledExpr::Literal(format!("\"{s}\"")))
398    }
399
400    fn parse_iri(&mut self) -> Result<CompiledExpr, CompileError> {
401        self.advance(); // consume '<'
402        let start = self.pos;
403        while let Some(c) = self.peek() {
404            if c == b'>' {
405                break;
406            }
407            self.advance();
408        }
409        let iri = std::str::from_utf8(&self.input[start..self.pos])
410            .map_err(|e| CompileError(format!("UTF-8 error in IRI: {e}")))?
411            .to_string();
412        self.expect(b'>')?;
413        Ok(CompiledExpr::IriRef(iri))
414    }
415
416    fn parse_variable(&mut self) -> Result<CompiledExpr, CompileError> {
417        self.advance(); // consume '?'
418        let start = self.pos;
419        while let Some(c) = self.peek() {
420            if c.is_ascii_alphanumeric() || c == b'_' {
421                self.advance();
422            } else {
423                break;
424            }
425        }
426        let name = std::str::from_utf8(&self.input[start..self.pos])
427            .map_err(|e| CompileError(format!("UTF-8 error in variable: {e}")))?
428            .to_string();
429        Ok(CompiledExpr::Variable(name))
430    }
431
432    fn parse_number(&mut self) -> Result<CompiledExpr, CompileError> {
433        let start = self.pos;
434        let mut has_dot = false;
435        while let Some(c) = self.peek() {
436            if c.is_ascii_digit() {
437                self.advance();
438            } else if c == b'.' && !has_dot {
439                has_dot = true;
440                self.advance();
441            } else {
442                break;
443            }
444        }
445        let num_str = std::str::from_utf8(&self.input[start..self.pos])
446            .map_err(|e| CompileError(format!("UTF-8 error in number: {e}")))?;
447        Ok(CompiledExpr::Literal(num_str.to_string()))
448    }
449
450    fn parse_name_or_call(&mut self) -> Result<CompiledExpr, CompileError> {
451        let start = self.pos;
452        while let Some(c) = self.peek() {
453            if c.is_ascii_alphanumeric() || c == b'_' {
454                self.advance();
455            } else {
456                break;
457            }
458        }
459        let name = std::str::from_utf8(&self.input[start..self.pos])
460            .map_err(|e| CompileError(format!("UTF-8 error in name: {e}")))?
461            .to_string();
462
463        self.skip_ws();
464        // If followed by '(' it's a function call
465        if self.peek() == Some(b'(') {
466            let upper = name.to_uppercase();
467            // Special-case IF
468            if upper == "IF" {
469                let args = self.parse_arg_list()?;
470                if args.len() != 3 {
471                    return Err(CompileError(format!(
472                        "IF requires exactly 3 arguments, got {}",
473                        args.len()
474                    )));
475                }
476                let mut it = args.into_iter();
477                let cond = it.next().expect("checked len");
478                let then = it.next().expect("checked len");
479                let else_ = it.next().expect("checked len");
480                Ok(CompiledExpr::If(
481                    Box::new(cond),
482                    Box::new(then),
483                    Box::new(else_),
484                ))
485            } else {
486                let args = self.parse_arg_list()?;
487                Ok(CompiledExpr::FuncCall(upper, args))
488            }
489        } else {
490            // bare name — treat as a literal string token (e.g. "true"/"false")
491            let lower = name.to_lowercase();
492            if lower == "true" {
493                Ok(CompiledExpr::Literal("true".to_string()))
494            } else if lower == "false" {
495                Ok(CompiledExpr::Literal("false".to_string()))
496            } else {
497                Err(CompileError(format!(
498                    "unexpected bare identifier '{name}' at pos {start}"
499                )))
500            }
501        }
502    }
503}
504
505// ─── Compiler ────────────────────────────────────────────────────────────────
506
507/// Compiles SPARQL filter expression strings into `CompiledExpr` trees.
508#[derive(Debug, Clone, Default)]
509pub struct ExprCompiler;
510
511impl ExprCompiler {
512    /// Create a new compiler instance.
513    pub fn new() -> Self {
514        Self
515    }
516
517    /// Compile an expression string into a `CompiledExpr`.
518    pub fn compile(&self, expr_str: &str) -> Result<CompiledExpr, CompileError> {
519        let mut p = Parser::new(expr_str.trim());
520        let expr = p.parse_or()?;
521        p.skip_ws();
522        if p.pos != p.input.len() {
523            return Err(CompileError(format!(
524                "unexpected trailing input at pos {}: '{}'",
525                p.pos,
526                std::str::from_utf8(&p.input[p.pos..]).unwrap_or("<invalid utf8>")
527            )));
528        }
529        Ok(expr)
530    }
531
532    /// Evaluate a compiled expression with the given variable bindings.
533    pub fn evaluate(
534        &self,
535        expr: &CompiledExpr,
536        bindings: &HashMap<String, ExprValue>,
537    ) -> Result<ExprValue, EvalError> {
538        match expr {
539            CompiledExpr::Literal(s) => Self::eval_literal(s),
540            CompiledExpr::Variable(name) => {
541                Ok(bindings.get(name).cloned().unwrap_or(ExprValue::Unbound))
542            }
543            CompiledExpr::IriRef(iri) => Ok(ExprValue::Iri(iri.clone())),
544            CompiledExpr::Neg(inner) => {
545                let v = self.evaluate(inner, bindings)?;
546                match v {
547                    ExprValue::Integer(i) => Ok(ExprValue::Integer(-i)),
548                    ExprValue::Double(d) => Ok(ExprValue::Double(-d)),
549                    _ => Err(EvalError(format!("cannot negate {v}"))),
550                }
551            }
552            CompiledExpr::Not(inner) => {
553                let v = self.evaluate(inner, bindings)?;
554                match v {
555                    ExprValue::Bool(b) => Ok(ExprValue::Bool(!b)),
556                    _ => Err(EvalError(format!("! requires boolean, got {v}"))),
557                }
558            }
559            CompiledExpr::BinOp(op, left, right) => self.eval_binop(op, left, right, bindings),
560            CompiledExpr::FuncCall(name, args) => self.eval_func(name, args, bindings),
561            CompiledExpr::If(cond, then_expr, else_expr) => {
562                let cv = self.evaluate(cond, bindings)?;
563                match cv {
564                    ExprValue::Bool(true) => self.evaluate(then_expr, bindings),
565                    ExprValue::Bool(false) => self.evaluate(else_expr, bindings),
566                    _ => Err(EvalError(format!("IF condition must be boolean, got {cv}"))),
567                }
568            }
569        }
570    }
571
572    fn eval_literal(s: &str) -> Result<ExprValue, EvalError> {
573        // Double-quoted string
574        if s.starts_with('"') && s.ends_with('"') && s.len() >= 2 {
575            return Ok(ExprValue::Str(s[1..s.len() - 1].to_string()));
576        }
577        // Boolean
578        if s == "true" {
579            return Ok(ExprValue::Bool(true));
580        }
581        if s == "false" {
582            return Ok(ExprValue::Bool(false));
583        }
584        // Integer
585        if let Ok(i) = s.parse::<i64>() {
586            return Ok(ExprValue::Integer(i));
587        }
588        // Double
589        if let Ok(d) = s.parse::<f64>() {
590            return Ok(ExprValue::Double(d));
591        }
592        Err(EvalError(format!("cannot parse literal: '{s}'")))
593    }
594
595    fn eval_binop(
596        &self,
597        op: &BinOp,
598        left: &CompiledExpr,
599        right: &CompiledExpr,
600        bindings: &HashMap<String, ExprValue>,
601    ) -> Result<ExprValue, EvalError> {
602        // Short-circuit evaluation for And/Or
603        if *op == BinOp::And {
604            let lv = self.evaluate(left, bindings)?;
605            match lv {
606                ExprValue::Bool(false) => return Ok(ExprValue::Bool(false)),
607                ExprValue::Bool(true) => {
608                    let rv = self.evaluate(right, bindings)?;
609                    return match rv {
610                        ExprValue::Bool(b) => Ok(ExprValue::Bool(b)),
611                        _ => Err(EvalError(format!("&& requires boolean RHS, got {rv}"))),
612                    };
613                }
614                _ => return Err(EvalError(format!("&& requires boolean LHS, got {lv}"))),
615            }
616        }
617        if *op == BinOp::Or {
618            let lv = self.evaluate(left, bindings)?;
619            match lv {
620                ExprValue::Bool(true) => return Ok(ExprValue::Bool(true)),
621                ExprValue::Bool(false) => {
622                    let rv = self.evaluate(right, bindings)?;
623                    return match rv {
624                        ExprValue::Bool(b) => Ok(ExprValue::Bool(b)),
625                        _ => Err(EvalError(format!("|| requires boolean RHS, got {rv}"))),
626                    };
627                }
628                _ => return Err(EvalError(format!("|| requires boolean LHS, got {lv}"))),
629            }
630        }
631
632        let lv = self.evaluate(left, bindings)?;
633        let rv = self.evaluate(right, bindings)?;
634
635        match op {
636            BinOp::Add => Self::numeric_op(&lv, &rv, |a, b| a + b, |a, b| a + b),
637            BinOp::Sub => Self::numeric_op(&lv, &rv, |a, b| a - b, |a, b| a - b),
638            BinOp::Mul => Self::numeric_op(&lv, &rv, |a, b| a * b, |a, b| a * b),
639            BinOp::Div => match (&lv, &rv) {
640                (ExprValue::Integer(_), ExprValue::Integer(0)) => {
641                    Err(EvalError("division by zero".to_string()))
642                }
643                (ExprValue::Double(_), ExprValue::Double(d)) if *d == 0.0 => {
644                    Err(EvalError("division by zero (double)".to_string()))
645                }
646                _ => Self::numeric_op(&lv, &rv, |a, b| a / b, |a, b| a / b),
647            },
648            BinOp::Eq => Ok(ExprValue::Bool(Self::values_equal(&lv, &rv))),
649            BinOp::Ne => Ok(ExprValue::Bool(!Self::values_equal(&lv, &rv))),
650            BinOp::Lt => Self::compare_op(&lv, &rv, std::cmp::Ordering::Less),
651            BinOp::Le => Self::compare_op_le(&lv, &rv),
652            BinOp::Gt => Self::compare_op(&lv, &rv, std::cmp::Ordering::Greater),
653            BinOp::Ge => Self::compare_op_ge(&lv, &rv),
654            BinOp::And | BinOp::Or => unreachable!("handled above"),
655        }
656    }
657
658    fn numeric_op<FI, FD>(
659        lv: &ExprValue,
660        rv: &ExprValue,
661        fi: FI,
662        fd: FD,
663    ) -> Result<ExprValue, EvalError>
664    where
665        FI: Fn(i64, i64) -> i64,
666        FD: Fn(f64, f64) -> f64,
667    {
668        match (lv, rv) {
669            (ExprValue::Integer(a), ExprValue::Integer(b)) => Ok(ExprValue::Integer(fi(*a, *b))),
670            (ExprValue::Double(a), ExprValue::Double(b)) => Ok(ExprValue::Double(fd(*a, *b))),
671            (ExprValue::Integer(a), ExprValue::Double(b)) => {
672                Ok(ExprValue::Double(fd(*a as f64, *b)))
673            }
674            (ExprValue::Double(a), ExprValue::Integer(b)) => {
675                Ok(ExprValue::Double(fd(*a, *b as f64)))
676            }
677            _ => Err(EvalError(format!(
678                "numeric operation requires numeric operands, got {lv} and {rv}"
679            ))),
680        }
681    }
682
683    fn values_equal(a: &ExprValue, b: &ExprValue) -> bool {
684        match (a, b) {
685            (ExprValue::Bool(x), ExprValue::Bool(y)) => x == y,
686            (ExprValue::Integer(x), ExprValue::Integer(y)) => x == y,
687            (ExprValue::Double(x), ExprValue::Double(y)) => x == y,
688            (ExprValue::Integer(x), ExprValue::Double(y)) => (*x as f64) == *y,
689            (ExprValue::Double(x), ExprValue::Integer(y)) => *x == (*y as f64),
690            (ExprValue::Str(x), ExprValue::Str(y)) => x == y,
691            (ExprValue::Iri(x), ExprValue::Iri(y)) => x == y,
692            (ExprValue::Blank(x), ExprValue::Blank(y)) => x == y,
693            (ExprValue::Unbound, ExprValue::Unbound) => true,
694            _ => false,
695        }
696    }
697
698    fn compare_op(
699        lv: &ExprValue,
700        rv: &ExprValue,
701        target: std::cmp::Ordering,
702    ) -> Result<ExprValue, EvalError> {
703        let ord = Self::numeric_cmp(lv, rv)?;
704        Ok(ExprValue::Bool(ord == target))
705    }
706
707    fn compare_op_le(lv: &ExprValue, rv: &ExprValue) -> Result<ExprValue, EvalError> {
708        let ord = Self::numeric_cmp(lv, rv)?;
709        Ok(ExprValue::Bool(
710            ord == std::cmp::Ordering::Less || ord == std::cmp::Ordering::Equal,
711        ))
712    }
713
714    fn compare_op_ge(lv: &ExprValue, rv: &ExprValue) -> Result<ExprValue, EvalError> {
715        let ord = Self::numeric_cmp(lv, rv)?;
716        Ok(ExprValue::Bool(
717            ord == std::cmp::Ordering::Greater || ord == std::cmp::Ordering::Equal,
718        ))
719    }
720
721    fn numeric_cmp(lv: &ExprValue, rv: &ExprValue) -> Result<std::cmp::Ordering, EvalError> {
722        match (lv, rv) {
723            (ExprValue::Integer(a), ExprValue::Integer(b)) => Ok(a.cmp(b)),
724            (ExprValue::Double(a), ExprValue::Double(b)) => a
725                .partial_cmp(b)
726                .ok_or_else(|| EvalError("NaN comparison".to_string())),
727            (ExprValue::Integer(a), ExprValue::Double(b)) => (*a as f64)
728                .partial_cmp(b)
729                .ok_or_else(|| EvalError("NaN".to_string())),
730            (ExprValue::Double(a), ExprValue::Integer(b)) => a
731                .partial_cmp(&(*b as f64))
732                .ok_or_else(|| EvalError("NaN".to_string())),
733            (ExprValue::Str(a), ExprValue::Str(b)) => Ok(a.cmp(b)),
734            _ => Err(EvalError(format!("cannot compare {lv} and {rv}"))),
735        }
736    }
737
738    fn eval_func(
739        &self,
740        name: &str,
741        args: &[CompiledExpr],
742        bindings: &HashMap<String, ExprValue>,
743    ) -> Result<ExprValue, EvalError> {
744        match name {
745            "BOUND" => {
746                if args.len() != 1 {
747                    return Err(EvalError(format!(
748                        "BOUND expects 1 arg, got {}",
749                        args.len()
750                    )));
751                }
752                let v = self.evaluate(&args[0], bindings)?;
753                Ok(ExprValue::Bool(!matches!(v, ExprValue::Unbound)))
754            }
755            "ISIRI" | "ISURI" => {
756                if args.len() != 1 {
757                    return Err(EvalError(format!(
758                        "{name} expects 1 arg, got {}",
759                        args.len()
760                    )));
761                }
762                let v = self.evaluate(&args[0], bindings)?;
763                Ok(ExprValue::Bool(matches!(v, ExprValue::Iri(_))))
764            }
765            "ISLITERAL" => {
766                if args.len() != 1 {
767                    return Err(EvalError(format!(
768                        "ISLITERAL expects 1 arg, got {}",
769                        args.len()
770                    )));
771                }
772                let v = self.evaluate(&args[0], bindings)?;
773                Ok(ExprValue::Bool(matches!(
774                    v,
775                    ExprValue::Str(_)
776                        | ExprValue::Integer(_)
777                        | ExprValue::Double(_)
778                        | ExprValue::Bool(_)
779                )))
780            }
781            "ISBLANK" => {
782                if args.len() != 1 {
783                    return Err(EvalError(format!(
784                        "ISBLANK expects 1 arg, got {}",
785                        args.len()
786                    )));
787                }
788                let v = self.evaluate(&args[0], bindings)?;
789                Ok(ExprValue::Bool(matches!(v, ExprValue::Blank(_))))
790            }
791            "STR" => {
792                if args.len() != 1 {
793                    return Err(EvalError(format!("STR expects 1 arg, got {}", args.len())));
794                }
795                let v = self.evaluate(&args[0], bindings)?;
796                let s = match &v {
797                    ExprValue::Str(s) => s.clone(),
798                    ExprValue::Iri(s) => s.clone(),
799                    ExprValue::Integer(i) => i.to_string(),
800                    ExprValue::Double(d) => d.to_string(),
801                    ExprValue::Bool(b) => b.to_string(),
802                    ExprValue::Blank(b) => b.clone(),
803                    ExprValue::Unbound => return Err(EvalError("STR of UNBOUND".to_string())),
804                };
805                Ok(ExprValue::Str(s))
806            }
807            "LANG" => {
808                if args.len() != 1 {
809                    return Err(EvalError(format!("LANG expects 1 arg, got {}", args.len())));
810                }
811                let v = self.evaluate(&args[0], bindings)?;
812                match v {
813                    ExprValue::Str(_) => Ok(ExprValue::Str(String::new())),
814                    _ => Err(EvalError(format!("LANG requires string literal, got {v}"))),
815                }
816            }
817            "DATATYPE" => {
818                if args.len() != 1 {
819                    return Err(EvalError(format!(
820                        "DATATYPE expects 1 arg, got {}",
821                        args.len()
822                    )));
823                }
824                let v = self.evaluate(&args[0], bindings)?;
825                let dt = match v {
826                    ExprValue::Str(_) => "http://www.w3.org/2001/XMLSchema#string",
827                    ExprValue::Integer(_) => "http://www.w3.org/2001/XMLSchema#integer",
828                    ExprValue::Double(_) => "http://www.w3.org/2001/XMLSchema#double",
829                    ExprValue::Bool(_) => "http://www.w3.org/2001/XMLSchema#boolean",
830                    _ => return Err(EvalError("DATATYPE not applicable".to_string())),
831                };
832                Ok(ExprValue::Iri(dt.to_string()))
833            }
834            "COALESCE" => {
835                for arg in args {
836                    let v = self.evaluate(arg, bindings)?;
837                    if !matches!(v, ExprValue::Unbound) {
838                        return Ok(v);
839                    }
840                }
841                Ok(ExprValue::Unbound)
842            }
843            "IF" => {
844                // IF as a function call (alternative to CompiledExpr::If)
845                if args.len() != 3 {
846                    return Err(EvalError(format!("IF expects 3 args, got {}", args.len())));
847                }
848                let cv = self.evaluate(&args[0], bindings)?;
849                match cv {
850                    ExprValue::Bool(true) => self.evaluate(&args[1], bindings),
851                    ExprValue::Bool(false) => self.evaluate(&args[2], bindings),
852                    _ => Err(EvalError(format!("IF condition must be boolean, got {cv}"))),
853                }
854            }
855            other => Err(EvalError(format!("unknown function: {other}"))),
856        }
857    }
858}
859
860// ─── LRU cache ──────────────────────────────────────────────────────────────
861
862/// LRU expression cache backed by a `VecDeque` (order) + `HashMap` (storage).
863///
864/// Compiled expressions are stored in the map; the deque tracks access order
865/// from oldest (front) to newest (back). On capacity overflow the front entry
866/// is evicted.
867pub struct ExprCache {
868    cache: HashMap<String, CompiledExpr>,
869    order: VecDeque<String>,
870    max_size: usize,
871    compiler: ExprCompiler,
872    hits: u64,
873    misses: u64,
874}
875
876impl ExprCache {
877    /// Create a new cache with the given maximum number of entries.
878    pub fn new(max_size: usize) -> Self {
879        Self {
880            cache: HashMap::new(),
881            order: VecDeque::new(),
882            max_size,
883            compiler: ExprCompiler::new(),
884            hits: 0,
885            misses: 0,
886        }
887    }
888
889    /// Retrieve a compiled expression from cache, compiling it if not present.
890    pub fn get_or_compile(&mut self, expr_str: &str) -> Result<&CompiledExpr, CompileError> {
891        if self.cache.contains_key(expr_str) {
892            // Move to back (most-recently-used)
893            if let Some(pos) = self.order.iter().position(|k| k == expr_str) {
894                self.order.remove(pos);
895            }
896            self.order.push_back(expr_str.to_string());
897            self.hits += 1;
898            return Ok(self.cache.get(expr_str).expect("just confirmed present"));
899        }
900
901        // Cache miss — compile
902        self.misses += 1;
903        let compiled = self.compiler.compile(expr_str)?;
904
905        // Evict LRU entry if at capacity
906        if self.cache.len() >= self.max_size && !self.cache.is_empty() {
907            if let Some(oldest) = self.order.pop_front() {
908                self.cache.remove(&oldest);
909            }
910        }
911
912        self.cache.insert(expr_str.to_string(), compiled);
913        self.order.push_back(expr_str.to_string());
914        Ok(self.cache.get(expr_str).expect("just inserted"))
915    }
916
917    /// Current number of entries in the cache.
918    pub fn cache_size(&self) -> usize {
919        self.cache.len()
920    }
921
922    /// Clear all cached entries and reset statistics.
923    pub fn clear(&mut self) {
924        self.cache.clear();
925        self.order.clear();
926        self.hits = 0;
927        self.misses = 0;
928    }
929
930    /// Number of cache hits since last clear.
931    pub fn hits(&self) -> u64 {
932        self.hits
933    }
934
935    /// Number of cache misses since last clear.
936    pub fn misses(&self) -> u64 {
937        self.misses
938    }
939}
940
941// ─── Tests ──────────────────────────────────────────────────────────────────
942
943#[cfg(test)]
944mod tests {
945    use super::*;
946
947    fn bindings(pairs: &[(&str, ExprValue)]) -> HashMap<String, ExprValue> {
948        pairs
949            .iter()
950            .map(|(k, v)| (k.to_string(), v.clone()))
951            .collect()
952    }
953
954    fn compiler() -> ExprCompiler {
955        ExprCompiler::new()
956    }
957
958    fn compile(s: &str) -> CompiledExpr {
959        compiler().compile(s).expect("compile failed")
960    }
961
962    fn eval(expr: &CompiledExpr, b: &HashMap<String, ExprValue>) -> ExprValue {
963        compiler().evaluate(expr, b).expect("eval failed")
964    }
965
966    // ── Literal parsing ─────────────────────────────────────────────────────
967
968    #[test]
969    fn test_literal_integer() {
970        let e = compile("42");
971        let v = eval(&e, &bindings(&[]));
972        assert_eq!(v, ExprValue::Integer(42));
973    }
974
975    #[test]
976    fn test_literal_zero() {
977        let e = compile("0");
978        let v = eval(&e, &bindings(&[]));
979        assert_eq!(v, ExprValue::Integer(0));
980    }
981
982    #[test]
983    fn test_literal_double() {
984        let e = compile("3.15");
985        let v = eval(&e, &bindings(&[]));
986        assert_eq!(v, ExprValue::Double(3.15));
987    }
988
989    #[test]
990    fn test_literal_string() {
991        let e = compile(r#""hello world""#);
992        let v = eval(&e, &bindings(&[]));
993        assert_eq!(v, ExprValue::Str("hello world".to_string()));
994    }
995
996    #[test]
997    fn test_literal_empty_string() {
998        let e = compile(r#""""#);
999        let v = eval(&e, &bindings(&[]));
1000        assert_eq!(v, ExprValue::Str(String::new()));
1001    }
1002
1003    #[test]
1004    fn test_literal_true() {
1005        let e = compile("true");
1006        let v = eval(&e, &bindings(&[]));
1007        assert_eq!(v, ExprValue::Bool(true));
1008    }
1009
1010    #[test]
1011    fn test_literal_false() {
1012        let e = compile("false");
1013        let v = eval(&e, &bindings(&[]));
1014        assert_eq!(v, ExprValue::Bool(false));
1015    }
1016
1017    // ── Variable lookup ─────────────────────────────────────────────────────
1018
1019    #[test]
1020    fn test_variable_bound() {
1021        let e = compile("?x");
1022        let v = eval(&e, &bindings(&[("x", ExprValue::Integer(7))]));
1023        assert_eq!(v, ExprValue::Integer(7));
1024    }
1025
1026    #[test]
1027    fn test_variable_unbound() {
1028        let e = compile("?missing");
1029        let v = eval(&e, &bindings(&[]));
1030        assert_eq!(v, ExprValue::Unbound);
1031    }
1032
1033    #[test]
1034    fn test_variable_string_value() {
1035        let e = compile("?name");
1036        let v = eval(
1037            &e,
1038            &bindings(&[("name", ExprValue::Str("Alice".to_string()))]),
1039        );
1040        assert_eq!(v, ExprValue::Str("Alice".to_string()));
1041    }
1042
1043    // ── IRI ─────────────────────────────────────────────────────────────────
1044
1045    #[test]
1046    fn test_iri_ref() {
1047        let e = compile("<http://example.org/foo>");
1048        let v = eval(&e, &bindings(&[]));
1049        assert_eq!(v, ExprValue::Iri("http://example.org/foo".to_string()));
1050    }
1051
1052    // ── Arithmetic ──────────────────────────────────────────────────────────
1053
1054    #[test]
1055    fn test_add_integers() {
1056        let e = compile("1 + 2");
1057        let v = eval(&e, &bindings(&[]));
1058        assert_eq!(v, ExprValue::Integer(3));
1059    }
1060
1061    #[test]
1062    fn test_sub_integers() {
1063        let e = compile("10 - 3");
1064        let v = eval(&e, &bindings(&[]));
1065        assert_eq!(v, ExprValue::Integer(7));
1066    }
1067
1068    #[test]
1069    fn test_mul_integers() {
1070        let e = compile("4 * 5");
1071        let v = eval(&e, &bindings(&[]));
1072        assert_eq!(v, ExprValue::Integer(20));
1073    }
1074
1075    #[test]
1076    fn test_div_integers() {
1077        let e = compile("10 / 2");
1078        let v = eval(&e, &bindings(&[]));
1079        assert_eq!(v, ExprValue::Integer(5));
1080    }
1081
1082    #[test]
1083    fn test_add_doubles() {
1084        let e = compile("1.5 + 2.5");
1085        let v = eval(&e, &bindings(&[]));
1086        assert_eq!(v, ExprValue::Double(4.0));
1087    }
1088
1089    #[test]
1090    fn test_arithmetic_with_variable() {
1091        let e = compile("?x + 10");
1092        let v = eval(&e, &bindings(&[("x", ExprValue::Integer(5))]));
1093        assert_eq!(v, ExprValue::Integer(15));
1094    }
1095
1096    #[test]
1097    fn test_unary_neg_integer() {
1098        let e = compile("-5");
1099        let v = eval(&e, &bindings(&[]));
1100        assert_eq!(v, ExprValue::Integer(-5));
1101    }
1102
1103    #[test]
1104    fn test_unary_neg_double() {
1105        let e = compile("-3.15");
1106        let v = eval(&e, &bindings(&[]));
1107        assert_eq!(v, ExprValue::Double(-3.15));
1108    }
1109
1110    // ── Comparison ──────────────────────────────────────────────────────────
1111
1112    #[test]
1113    fn test_eq_true() {
1114        let e = compile("5 = 5");
1115        let v = eval(&e, &bindings(&[]));
1116        assert_eq!(v, ExprValue::Bool(true));
1117    }
1118
1119    #[test]
1120    fn test_eq_false() {
1121        let e = compile("5 = 6");
1122        let v = eval(&e, &bindings(&[]));
1123        assert_eq!(v, ExprValue::Bool(false));
1124    }
1125
1126    #[test]
1127    fn test_ne() {
1128        let e = compile("5 != 6");
1129        let v = eval(&e, &bindings(&[]));
1130        assert_eq!(v, ExprValue::Bool(true));
1131    }
1132
1133    #[test]
1134    fn test_lt() {
1135        let e = compile("3 < 5");
1136        let v = eval(&e, &bindings(&[]));
1137        assert_eq!(v, ExprValue::Bool(true));
1138    }
1139
1140    #[test]
1141    fn test_le() {
1142        let e = compile("5 <= 5");
1143        let v = eval(&e, &bindings(&[]));
1144        assert_eq!(v, ExprValue::Bool(true));
1145    }
1146
1147    #[test]
1148    fn test_gt() {
1149        let e = compile("7 > 5");
1150        let v = eval(&e, &bindings(&[]));
1151        assert_eq!(v, ExprValue::Bool(true));
1152    }
1153
1154    #[test]
1155    fn test_ge() {
1156        let e = compile("5 >= 5");
1157        let v = eval(&e, &bindings(&[]));
1158        assert_eq!(v, ExprValue::Bool(true));
1159    }
1160
1161    // ── Boolean ─────────────────────────────────────────────────────────────
1162
1163    #[test]
1164    fn test_and_true() {
1165        let e = compile("true && true");
1166        let v = eval(&e, &bindings(&[]));
1167        assert_eq!(v, ExprValue::Bool(true));
1168    }
1169
1170    #[test]
1171    fn test_and_false() {
1172        let e = compile("true && false");
1173        let v = eval(&e, &bindings(&[]));
1174        assert_eq!(v, ExprValue::Bool(false));
1175    }
1176
1177    #[test]
1178    fn test_or_false_true() {
1179        let e = compile("false || true");
1180        let v = eval(&e, &bindings(&[]));
1181        assert_eq!(v, ExprValue::Bool(true));
1182    }
1183
1184    #[test]
1185    fn test_not_false() {
1186        let e = compile("!false");
1187        let v = eval(&e, &bindings(&[]));
1188        assert_eq!(v, ExprValue::Bool(true));
1189    }
1190
1191    #[test]
1192    fn test_not_true() {
1193        let e = compile("!true");
1194        let v = eval(&e, &bindings(&[]));
1195        assert_eq!(v, ExprValue::Bool(false));
1196    }
1197
1198    // ── IF ──────────────────────────────────────────────────────────────────
1199
1200    #[test]
1201    fn test_if_true_branch() {
1202        let e = compile("IF(true, 1, 2)");
1203        let v = eval(&e, &bindings(&[]));
1204        assert_eq!(v, ExprValue::Integer(1));
1205    }
1206
1207    #[test]
1208    fn test_if_false_branch() {
1209        let e = compile("IF(false, 1, 2)");
1210        let v = eval(&e, &bindings(&[]));
1211        assert_eq!(v, ExprValue::Integer(2));
1212    }
1213
1214    #[test]
1215    fn test_if_with_condition_expr() {
1216        let e = compile("IF(3 > 2, 100, 200)");
1217        let v = eval(&e, &bindings(&[]));
1218        assert_eq!(v, ExprValue::Integer(100));
1219    }
1220
1221    // ── Built-in functions ───────────────────────────────────────────────────
1222
1223    #[test]
1224    fn test_bound_true() {
1225        let e = compile("BOUND(?x)");
1226        let v = eval(&e, &bindings(&[("x", ExprValue::Integer(1))]));
1227        assert_eq!(v, ExprValue::Bool(true));
1228    }
1229
1230    #[test]
1231    fn test_bound_false() {
1232        let e = compile("BOUND(?missing)");
1233        let v = eval(&e, &bindings(&[]));
1234        assert_eq!(v, ExprValue::Bool(false));
1235    }
1236
1237    #[test]
1238    fn test_isiri_true() {
1239        let e = compile("ISIRI(?x)");
1240        let v = eval(
1241            &e,
1242            &bindings(&[("x", ExprValue::Iri("http://ex.org/".to_string()))]),
1243        );
1244        assert_eq!(v, ExprValue::Bool(true));
1245    }
1246
1247    #[test]
1248    fn test_isiri_false() {
1249        let e = compile("ISIRI(?x)");
1250        let v = eval(
1251            &e,
1252            &bindings(&[("x", ExprValue::Str("not-iri".to_string()))]),
1253        );
1254        assert_eq!(v, ExprValue::Bool(false));
1255    }
1256
1257    #[test]
1258    fn test_isliteral_integer() {
1259        let e = compile("ISLITERAL(?x)");
1260        let v = eval(&e, &bindings(&[("x", ExprValue::Integer(42))]));
1261        assert_eq!(v, ExprValue::Bool(true));
1262    }
1263
1264    #[test]
1265    fn test_isblank_true() {
1266        let e = compile("ISBLANK(?b)");
1267        let v = eval(&e, &bindings(&[("b", ExprValue::Blank("b1".to_string()))]));
1268        assert_eq!(v, ExprValue::Bool(true));
1269    }
1270
1271    #[test]
1272    fn test_str_integer() {
1273        let e = compile("STR(?x)");
1274        let v = eval(&e, &bindings(&[("x", ExprValue::Integer(99))]));
1275        assert_eq!(v, ExprValue::Str("99".to_string()));
1276    }
1277
1278    #[test]
1279    fn test_str_iri() {
1280        let e = compile("STR(?x)");
1281        let v = eval(
1282            &e,
1283            &bindings(&[("x", ExprValue::Iri("http://example.org/".to_string()))]),
1284        );
1285        assert_eq!(v, ExprValue::Str("http://example.org/".to_string()));
1286    }
1287
1288    #[test]
1289    fn test_lang_plain_string() {
1290        let e = compile(r#"LANG(?x)"#);
1291        let v = eval(&e, &bindings(&[("x", ExprValue::Str("hello".to_string()))]));
1292        assert_eq!(v, ExprValue::Str(String::new()));
1293    }
1294
1295    #[test]
1296    fn test_datatype_string() {
1297        let e = compile(r#"DATATYPE(?x)"#);
1298        let v = eval(&e, &bindings(&[("x", ExprValue::Str("hi".to_string()))]));
1299        assert_eq!(
1300            v,
1301            ExprValue::Iri("http://www.w3.org/2001/XMLSchema#string".to_string())
1302        );
1303    }
1304
1305    #[test]
1306    fn test_datatype_integer() {
1307        let e = compile("DATATYPE(?x)");
1308        let v = eval(&e, &bindings(&[("x", ExprValue::Integer(1))]));
1309        assert_eq!(
1310            v,
1311            ExprValue::Iri("http://www.w3.org/2001/XMLSchema#integer".to_string())
1312        );
1313    }
1314
1315    #[test]
1316    fn test_coalesce_first_bound() {
1317        let e = compile("COALESCE(?a, ?b, 42)");
1318        let v = eval(
1319            &e,
1320            &bindings(&[("a", ExprValue::Integer(1)), ("b", ExprValue::Integer(2))]),
1321        );
1322        assert_eq!(v, ExprValue::Integer(1));
1323    }
1324
1325    #[test]
1326    fn test_coalesce_skip_unbound() {
1327        let e = compile("COALESCE(?missing, 99)");
1328        let v = eval(&e, &bindings(&[]));
1329        assert_eq!(v, ExprValue::Integer(99));
1330    }
1331
1332    #[test]
1333    fn test_coalesce_all_unbound() {
1334        let e = compile("COALESCE(?a, ?b)");
1335        let v = eval(&e, &bindings(&[]));
1336        assert_eq!(v, ExprValue::Unbound);
1337    }
1338
1339    // ── Nested expressions ────────────────────────────────────────────────────
1340
1341    #[test]
1342    fn test_nested_arithmetic() {
1343        let e = compile("(2 + 3) * 4");
1344        let v = eval(&e, &bindings(&[]));
1345        assert_eq!(v, ExprValue::Integer(20));
1346    }
1347
1348    #[test]
1349    fn test_nested_comparison_and() {
1350        let e = compile("3 > 2 && 5 < 10");
1351        let v = eval(&e, &bindings(&[]));
1352        assert_eq!(v, ExprValue::Bool(true));
1353    }
1354
1355    #[test]
1356    fn test_complex_variable_expr() {
1357        let e = compile("?age >= 18 && ?age < 65");
1358        let v = eval(&e, &bindings(&[("age", ExprValue::Integer(30))]));
1359        assert_eq!(v, ExprValue::Bool(true));
1360    }
1361
1362    // ── Error cases ───────────────────────────────────────────────────────────
1363
1364    #[test]
1365    fn test_div_by_zero_error() {
1366        let e = compile("10 / 0");
1367        let result = compiler().evaluate(&e, &bindings(&[]));
1368        assert!(result.is_err());
1369    }
1370
1371    #[test]
1372    fn test_type_mismatch_neg() {
1373        let e = compile(r#"-"hello""#);
1374        let result = compiler().evaluate(&e, &bindings(&[]));
1375        assert!(result.is_err());
1376    }
1377
1378    #[test]
1379    fn test_compile_error_bare_name() {
1380        let result = ExprCompiler::new().compile("undefined_function");
1381        assert!(result.is_err());
1382    }
1383
1384    #[test]
1385    fn test_if_wrong_arg_count() {
1386        let result = ExprCompiler::new().compile("IF(true, 1)");
1387        assert!(result.is_err());
1388    }
1389
1390    // ── ExprCache ─────────────────────────────────────────────────────────────
1391
1392    #[test]
1393    fn test_cache_basic() {
1394        let mut cache = ExprCache::new(10);
1395        cache.get_or_compile("1 + 1").expect("compile ok");
1396        assert_eq!(cache.cache_size(), 1);
1397        assert_eq!(cache.misses(), 1);
1398        assert_eq!(cache.hits(), 0);
1399    }
1400
1401    #[test]
1402    fn test_cache_hit() {
1403        let mut cache = ExprCache::new(10);
1404        cache.get_or_compile("2 + 2").expect("ok");
1405        cache.get_or_compile("2 + 2").expect("ok");
1406        assert_eq!(cache.hits(), 1);
1407        assert_eq!(cache.misses(), 1);
1408        assert_eq!(cache.cache_size(), 1);
1409    }
1410
1411    #[test]
1412    fn test_cache_lru_eviction() {
1413        let mut cache = ExprCache::new(3);
1414        cache.get_or_compile("1").expect("ok"); // miss, [1]
1415        cache.get_or_compile("2").expect("ok"); // miss, [1,2]
1416        cache.get_or_compile("3").expect("ok"); // miss, [1,2,3]
1417                                                // Access "1" again to make it recent
1418        cache.get_or_compile("1").expect("ok"); // hit, [2,3,1]
1419                                                // Insert "4" — should evict "2" (oldest)
1420        cache.get_or_compile("4").expect("ok"); // miss, [3,1,4]
1421        assert_eq!(cache.cache_size(), 3);
1422        // "2" should be evicted; "1" should still be there
1423        assert!(cache.cache.contains_key("1"));
1424        assert!(!cache.cache.contains_key("2"));
1425        assert!(cache.cache.contains_key("3"));
1426        assert!(cache.cache.contains_key("4"));
1427    }
1428
1429    #[test]
1430    fn test_cache_clear() {
1431        let mut cache = ExprCache::new(5);
1432        cache.get_or_compile("42").expect("ok");
1433        cache.clear();
1434        assert_eq!(cache.cache_size(), 0);
1435        assert_eq!(cache.hits(), 0);
1436        assert_eq!(cache.misses(), 0);
1437    }
1438
1439    #[test]
1440    fn test_cache_multiple_expressions() {
1441        let mut cache = ExprCache::new(10);
1442        let exprs = ["1 + 1", "2 * 3", "true && false", "?x = 5", "BOUND(?y)"];
1443        for s in &exprs {
1444            cache.get_or_compile(s).expect("ok");
1445        }
1446        assert_eq!(cache.cache_size(), exprs.len());
1447        assert_eq!(cache.misses(), exprs.len() as u64);
1448    }
1449
1450    #[test]
1451    fn test_string_comparison() {
1452        let e = compile(r#""apple" = "apple""#);
1453        let v = eval(&e, &bindings(&[]));
1454        assert_eq!(v, ExprValue::Bool(true));
1455    }
1456
1457    #[test]
1458    fn test_mixed_numeric_types() {
1459        let e = compile("3 + 1.5");
1460        let v = eval(&e, &bindings(&[]));
1461        assert_eq!(v, ExprValue::Double(4.5));
1462    }
1463}