Skip to main content

cctr_expr/
lib.rs

1//! Expression language parser and evaluator for cctr constraints.
2//!
3//! Supports:
4//! - Numbers: `42`, `-3.14`, `0.5`
5//! - Strings: `"hello"`, `"with \"escapes\""`
6//! - Booleans: `true`, `false`
7//! - Arrays: `[1, 2, 3]`, `["a", "b"]`
8//! - Objects: `{"key": value, ...}`
9//! - Arithmetic: `+`, `-`, `*`, `/`, `^`
10//! - Comparison: `==`, `!=`, `<`, `<=`, `>`, `>=`
11//! - Logical: `and`, `or`, `not`
12//! - String ops: `contains`, `startswith`, `endswith`, `matches`
13//! - Membership: `in`
14//! - Array/object access: `a[0]`, `obj["key"]`, `obj.key`
15//! - Functions: `len(s)`, `type(v)`, `keys(obj)`
16//! - Quantifiers: `expr forall x in arr`
17//!
18//! # Example
19//!
20//! ```
21//! use cctr_expr::{eval_bool, Value};
22//! use std::collections::HashMap;
23//!
24//! let mut vars = HashMap::new();
25//! vars.insert("n".to_string(), Value::Number(42.0));
26//!
27//! assert!(eval_bool("n > 0 and n < 100", &vars).unwrap());
28//! ```
29
30use std::collections::HashMap;
31use thiserror::Error;
32use winnow::ascii::{digit1, multispace0, multispace1};
33use winnow::combinator::{alt, delimited, opt, preceded, repeat, separated, terminated};
34use winnow::error::ContextError;
35use winnow::prelude::*;
36use winnow::token::{any, none_of, one_of, take_while};
37
38// ============ Value Types ============
39
40#[derive(Debug, Clone, PartialEq)]
41pub enum Value {
42    Number(f64),
43    String(String),
44    Bool(bool),
45    Null,
46    Array(Vec<Value>),
47    Object(HashMap<String, Value>),
48    Type(String),
49}
50
51impl Value {
52    pub fn as_bool(&self) -> Result<bool, EvalError> {
53        match self {
54            Value::Bool(b) => Ok(*b),
55            _ => Err(EvalError::TypeError {
56                expected: "bool",
57                got: self.type_name(),
58            }),
59        }
60    }
61
62    pub fn as_number(&self) -> Result<f64, EvalError> {
63        match self {
64            Value::Number(n) => Ok(*n),
65            _ => Err(EvalError::TypeError {
66                expected: "number",
67                got: self.type_name(),
68            }),
69        }
70    }
71
72    pub fn as_string(&self) -> Result<&str, EvalError> {
73        match self {
74            Value::String(s) => Ok(s),
75            _ => Err(EvalError::TypeError {
76                expected: "string",
77                got: self.type_name(),
78            }),
79        }
80    }
81
82    pub fn as_array(&self) -> Result<&[Value], EvalError> {
83        match self {
84            Value::Array(a) => Ok(a),
85            _ => Err(EvalError::TypeError {
86                expected: "array",
87                got: self.type_name(),
88            }),
89        }
90    }
91
92    pub fn as_object(&self) -> Result<&HashMap<String, Value>, EvalError> {
93        match self {
94            Value::Object(o) => Ok(o),
95            _ => Err(EvalError::TypeError {
96                expected: "object",
97                got: self.type_name(),
98            }),
99        }
100    }
101
102    pub fn type_name(&self) -> &'static str {
103        match self {
104            Value::Number(_) => "number",
105            Value::String(_) => "string",
106            Value::Bool(_) => "bool",
107            Value::Null => "null",
108            Value::Array(_) => "array",
109            Value::Object(_) => "object",
110            Value::Type(_) => "type",
111        }
112    }
113
114    pub fn type_value(&self) -> Value {
115        Value::Type(self.type_name().to_string())
116    }
117}
118
119// ============ AST Types ============
120
121#[derive(Debug, Clone, PartialEq)]
122pub enum Expr {
123    Number(f64),
124    String(String),
125    Bool(bool),
126    Null,
127    Var(String),
128    Array(Vec<Expr>),
129    Object(Vec<(String, Expr)>),
130    TypeLiteral(String),
131    UnaryOp {
132        op: UnaryOp,
133        expr: Box<Expr>,
134    },
135    BinaryOp {
136        op: BinaryOp,
137        left: Box<Expr>,
138        right: Box<Expr>,
139    },
140    FuncCall {
141        name: String,
142        args: Vec<Expr>,
143    },
144    Index {
145        expr: Box<Expr>,
146        index: Box<Expr>,
147    },
148    Property {
149        expr: Box<Expr>,
150        name: String,
151    },
152    ForAll {
153        predicate: Box<Expr>,
154        var: String,
155        iterable: Box<Expr>,
156    },
157}
158
159#[derive(Debug, Clone, Copy, PartialEq)]
160pub enum UnaryOp {
161    Not,
162    Neg,
163}
164
165#[derive(Debug, Clone, Copy, PartialEq)]
166pub enum BinaryOp {
167    Add,
168    Sub,
169    Mul,
170    Div,
171    Mod,
172    Pow,
173    Eq,
174    Ne,
175    Lt,
176    Le,
177    Gt,
178    Ge,
179    And,
180    Or,
181    Contains,
182    NotContains,
183    StartsWith,
184    NotStartsWith,
185    EndsWith,
186    NotEndsWith,
187    Matches,
188    NotMatches,
189}
190
191#[derive(Error, Debug, Clone, PartialEq)]
192pub enum EvalError {
193    #[error("type error: expected {expected}, got {got}")]
194    TypeError {
195        expected: &'static str,
196        got: &'static str,
197    },
198    #[error("undefined variable: {0}")]
199    UndefinedVariable(String),
200    #[error("undefined function: {0}")]
201    UndefinedFunction(String),
202    #[error("invalid regex: {0}")]
203    InvalidRegex(String),
204    #[error("division by zero")]
205    DivisionByZero,
206    #[error("parse error: {0}")]
207    ParseError(String),
208    #[error("wrong number of arguments for {func}: expected {expected}, got {got}")]
209    WrongArgCount {
210        func: String,
211        expected: usize,
212        got: usize,
213    },
214    #[error("index out of bounds: {index} (len: {len})")]
215    IndexOutOfBounds { index: i64, len: usize },
216    #[error("key not found: {0}")]
217    KeyNotFound(String),
218}
219
220// ============ Parser ============
221
222fn ws<'a, P, O>(p: P) -> impl Parser<&'a str, O, ContextError>
223where
224    P: Parser<&'a str, O, ContextError>,
225{
226    delimited(multispace0, p, multispace0)
227}
228
229fn number(input: &mut &str) -> ModalResult<Expr> {
230    let neg: Option<char> = opt('-').parse_next(input)?;
231    let int_part: &str = digit1.parse_next(input)?;
232    let frac_part: Option<&str> = opt(preceded('.', digit1)).parse_next(input)?;
233
234    let mut s = String::new();
235    if neg.is_some() {
236        s.push('-');
237    }
238    s.push_str(int_part);
239    if let Some(frac) = frac_part {
240        s.push('.');
241        s.push_str(frac);
242    }
243
244    Ok(Expr::Number(s.parse().unwrap()))
245}
246
247fn string_char(input: &mut &str) -> ModalResult<char> {
248    let c: char = none_of('"').parse_next(input)?;
249    if c == '\\' {
250        let escaped: char = any.parse_next(input)?;
251        Ok(match escaped {
252            'n' => '\n',
253            't' => '\t',
254            'r' => '\r',
255            '"' => '"',
256            '\\' => '\\',
257            c => c,
258        })
259    } else {
260        Ok(c)
261    }
262}
263
264fn string_literal(input: &mut &str) -> ModalResult<Expr> {
265    let chars: String = delimited(
266        '"',
267        repeat(0.., string_char).fold(String::new, |mut s, c| {
268            s.push(c);
269            s
270        }),
271        '"',
272    )
273    .parse_next(input)?;
274    Ok(Expr::String(chars))
275}
276
277fn regex_literal(input: &mut &str) -> ModalResult<Expr> {
278    '/'.parse_next(input)?;
279    let mut s = String::new();
280    loop {
281        let c: char = any.parse_next(input)?;
282        if c == '/' {
283            break;
284        }
285        if c == '\\' {
286            let escaped: char = any.parse_next(input)?;
287            s.push('\\');
288            s.push(escaped);
289        } else {
290            s.push(c);
291        }
292    }
293    Ok(Expr::String(s))
294}
295
296fn ident(input: &mut &str) -> ModalResult<String> {
297    let first: char = one_of(|c: char| c.is_ascii_alphabetic() || c == '_').parse_next(input)?;
298    let rest: &str =
299        take_while(0.., |c: char| c.is_ascii_alphanumeric() || c == '_').parse_next(input)?;
300    Ok(format!("{}{}", first, rest))
301}
302
303fn var_or_bool_or_func(input: &mut &str) -> ModalResult<Expr> {
304    let name = ident.parse_next(input)?;
305
306    let _ = multispace0.parse_next(input)?;
307    if input.starts_with('(') {
308        '('.parse_next(input)?;
309        let _ = multispace0.parse_next(input)?;
310        let args: Vec<Expr> = separated(0.., ws(expr), ws(',')).parse_next(input)?;
311        let _ = multispace0.parse_next(input)?;
312        ')'.parse_next(input)?;
313        return Ok(Expr::FuncCall { name, args });
314    }
315
316    match name.as_str() {
317        "true" => Ok(Expr::Bool(true)),
318        "false" => Ok(Expr::Bool(false)),
319        // null is both a value and a type literal - as a standalone value we treat it as Null,
320        // but when used in type comparison (type(x) == null) it matches as a TypeLiteral
321        "null" => Ok(Expr::TypeLiteral(name)),
322        // Type keywords
323        "number" | "string" | "bool" | "array" | "object" => Ok(Expr::TypeLiteral(name)),
324        _ => Ok(Expr::Var(name)),
325    }
326}
327
328fn array(input: &mut &str) -> ModalResult<Expr> {
329    let elements: Vec<Expr> = delimited(
330        ('[', multispace0),
331        separated(0.., ws(expr), ws(',')),
332        (multispace0, ']'),
333    )
334    .parse_next(input)?;
335    Ok(Expr::Array(elements))
336}
337
338fn object_key(input: &mut &str) -> ModalResult<String> {
339    alt((
340        // Quoted key: "foo"
341        delimited(
342            '"',
343            repeat(0.., string_char).fold(String::new, |mut s, c| {
344                s.push(c);
345                s
346            }),
347            '"',
348        ),
349        // Unquoted identifier key
350        ident,
351    ))
352    .parse_next(input)
353}
354
355fn object_entry(input: &mut &str) -> ModalResult<(String, Expr)> {
356    let key = ws(object_key).parse_next(input)?;
357    ws(':').parse_next(input)?;
358    let value = ws(expr).parse_next(input)?;
359    Ok((key, value))
360}
361
362fn object(input: &mut &str) -> ModalResult<Expr> {
363    let entries: Vec<(String, Expr)> = delimited(
364        ('{', multispace0),
365        separated(0.., object_entry, ws(',')),
366        (multispace0, '}'),
367    )
368    .parse_next(input)?;
369    Ok(Expr::Object(entries))
370}
371
372const TYPE_KEYWORDS: &[&str] = &["number", "string", "bool", "null", "array", "object"];
373
374fn type_literal(input: &mut &str) -> ModalResult<Expr> {
375    for &kw in TYPE_KEYWORDS {
376        if input.starts_with(kw) {
377            let after = &(*input)[kw.len()..];
378            let next_char = after.chars().next();
379            if next_char
380                .map(|c| c.is_ascii_alphanumeric() || c == '_')
381                .unwrap_or(false)
382            {
383                continue;
384            }
385            *input = after;
386            return Ok(Expr::TypeLiteral(kw.to_string()));
387        }
388    }
389    Err(winnow::error::ErrMode::Backtrack(ContextError::new()))
390}
391
392fn atom(input: &mut &str) -> ModalResult<Expr> {
393    let _ = multispace0.parse_next(input)?;
394    alt((
395        delimited(('(', multispace0), expr, (multispace0, ')')),
396        array,
397        object,
398        string_literal,
399        regex_literal,
400        number,
401        var_or_bool_or_func,
402        type_literal,
403    ))
404    .parse_next(input)
405}
406
407fn postfix(input: &mut &str) -> ModalResult<Expr> {
408    let mut base = atom.parse_next(input)?;
409    loop {
410        let _ = multispace0.parse_next(input)?;
411        if input.starts_with('[') {
412            '['.parse_next(input)?;
413            let _ = multispace0.parse_next(input)?;
414            let index = expr.parse_next(input)?;
415            let _ = multispace0.parse_next(input)?;
416            ']'.parse_next(input)?;
417            base = Expr::Index {
418                expr: Box::new(base),
419                index: Box::new(index),
420            };
421        } else if input.starts_with('.') {
422            '.'.parse_next(input)?;
423            let name = ident.parse_next(input)?;
424            base = Expr::Property {
425                expr: Box::new(base),
426                name,
427            };
428        } else {
429            break;
430        }
431    }
432    Ok(base)
433}
434
435fn unary(input: &mut &str) -> ModalResult<Expr> {
436    let _ = multispace0.parse_next(input)?;
437    let neg: Option<char> = opt('-').parse_next(input)?;
438    if neg.is_some() {
439        let e = unary.parse_next(input)?;
440        return Ok(Expr::UnaryOp {
441            op: UnaryOp::Neg,
442            expr: Box::new(e),
443        });
444    }
445    postfix(input)
446}
447
448fn pow(input: &mut &str) -> ModalResult<Expr> {
449    let base = unary.parse_next(input)?;
450    let _ = multispace0.parse_next(input)?;
451    let caret: Option<char> = opt('^').parse_next(input)?;
452    if caret.is_some() {
453        let _ = multispace0.parse_next(input)?;
454        let exp = pow.parse_next(input)?;
455        Ok(Expr::BinaryOp {
456            op: BinaryOp::Pow,
457            left: Box::new(base),
458            right: Box::new(exp),
459        })
460    } else {
461        Ok(base)
462    }
463}
464
465fn term(input: &mut &str) -> ModalResult<Expr> {
466    let init = pow.parse_next(input)?;
467
468    repeat(0.., (ws(one_of(['*', '/', '%'])), pow))
469        .fold(
470            move || init.clone(),
471            |acc, (op_char, val): (char, Expr)| {
472                let op = match op_char {
473                    '*' => BinaryOp::Mul,
474                    '/' => BinaryOp::Div,
475                    '%' => BinaryOp::Mod,
476                    _ => unreachable!(),
477                };
478                Expr::BinaryOp {
479                    op,
480                    left: Box::new(acc),
481                    right: Box::new(val),
482                }
483            },
484        )
485        .parse_next(input)
486}
487
488fn arith(input: &mut &str) -> ModalResult<Expr> {
489    let init = term.parse_next(input)?;
490
491    repeat(0.., (ws(one_of(['+', '-'])), term))
492        .fold(
493            move || init.clone(),
494            |acc, (op_char, val): (char, Expr)| {
495                let op = if op_char == '+' {
496                    BinaryOp::Add
497                } else {
498                    BinaryOp::Sub
499                };
500                Expr::BinaryOp {
501                    op,
502                    left: Box::new(acc),
503                    right: Box::new(val),
504                }
505            },
506        )
507        .parse_next(input)
508}
509
510fn peek_non_ident(input: &mut &str) -> ModalResult<()> {
511    let next = input.chars().next();
512    if next
513        .map(|c| c.is_ascii_alphanumeric() || c == '_')
514        .unwrap_or(false)
515    {
516        Err(winnow::error::ErrMode::Backtrack(ContextError::new()))
517    } else {
518        Ok(())
519    }
520}
521
522fn cmp_op(input: &mut &str) -> ModalResult<BinaryOp> {
523    alt((
524        "==".value(BinaryOp::Eq),
525        "!=".value(BinaryOp::Ne),
526        "<=".value(BinaryOp::Le),
527        ">=".value(BinaryOp::Ge),
528        "<".value(BinaryOp::Lt),
529        ">".value(BinaryOp::Gt),
530        (
531            terminated("not", peek_non_ident),
532            multispace1,
533            terminated("contains", peek_non_ident),
534        )
535            .value(BinaryOp::NotContains),
536        (
537            terminated("not", peek_non_ident),
538            multispace1,
539            terminated("startswith", peek_non_ident),
540        )
541            .value(BinaryOp::NotStartsWith),
542        (
543            terminated("not", peek_non_ident),
544            multispace1,
545            terminated("endswith", peek_non_ident),
546        )
547            .value(BinaryOp::NotEndsWith),
548        (
549            terminated("not", peek_non_ident),
550            multispace1,
551            terminated("matches", peek_non_ident),
552        )
553            .value(BinaryOp::NotMatches),
554        terminated("contains", peek_non_ident).value(BinaryOp::Contains),
555        terminated("startswith", peek_non_ident).value(BinaryOp::StartsWith),
556        terminated("endswith", peek_non_ident).value(BinaryOp::EndsWith),
557        terminated("matches", peek_non_ident).value(BinaryOp::Matches),
558    ))
559    .parse_next(input)
560}
561
562fn comparison(input: &mut &str) -> ModalResult<Expr> {
563    let left = arith.parse_next(input)?;
564    let _ = multispace0.parse_next(input)?;
565
566    let op_opt: Option<BinaryOp> = opt(cmp_op).parse_next(input)?;
567    match op_opt {
568        Some(op) => {
569            let _ = multispace0.parse_next(input)?;
570            let right = arith.parse_next(input)?;
571            Ok(Expr::BinaryOp {
572                op,
573                left: Box::new(left),
574                right: Box::new(right),
575            })
576        }
577        None => Ok(left),
578    }
579}
580
581fn not_expr(input: &mut &str) -> ModalResult<Expr> {
582    let _ = multispace0.parse_next(input)?;
583    let not_kw: Option<&str> = opt(terminated("not", peek_non_ident)).parse_next(input)?;
584    if not_kw.is_some() {
585        let _ = multispace0.parse_next(input)?;
586        let e = not_expr.parse_next(input)?;
587        Ok(Expr::UnaryOp {
588            op: UnaryOp::Not,
589            expr: Box::new(e),
590        })
591    } else {
592        comparison(input)
593    }
594}
595
596fn and_expr(input: &mut &str) -> ModalResult<Expr> {
597    let init = not_expr.parse_next(input)?;
598
599    repeat(
600        0..,
601        preceded((multispace0, "and", peek_non_ident, multispace0), not_expr),
602    )
603    .fold(
604        move || init.clone(),
605        |acc, val| Expr::BinaryOp {
606            op: BinaryOp::And,
607            left: Box::new(acc),
608            right: Box::new(val),
609        },
610    )
611    .parse_next(input)
612}
613
614fn or_expr(input: &mut &str) -> ModalResult<Expr> {
615    let init = and_expr.parse_next(input)?;
616
617    repeat(
618        0..,
619        preceded((multispace0, "or", peek_non_ident, multispace0), and_expr),
620    )
621    .fold(
622        move || init.clone(),
623        |acc, val| Expr::BinaryOp {
624            op: BinaryOp::Or,
625            left: Box::new(acc),
626            right: Box::new(val),
627        },
628    )
629    .parse_next(input)
630}
631
632fn forall_expr(input: &mut &str) -> ModalResult<Expr> {
633    let predicate = or_expr.parse_next(input)?;
634    let _ = multispace0.parse_next(input)?;
635
636    let forall_kw: Option<&str> = opt(terminated("forall", peek_non_ident)).parse_next(input)?;
637    if forall_kw.is_some() {
638        let _ = multispace0.parse_next(input)?;
639        let var = ident.parse_next(input)?;
640        let _ = multispace0.parse_next(input)?;
641        terminated("in", peek_non_ident).parse_next(input)?;
642        let _ = multispace0.parse_next(input)?;
643        let iterable = or_expr.parse_next(input)?;
644        Ok(Expr::ForAll {
645            predicate: Box::new(predicate),
646            var,
647            iterable: Box::new(iterable),
648        })
649    } else {
650        Ok(predicate)
651    }
652}
653
654fn expr(input: &mut &str) -> ModalResult<Expr> {
655    forall_expr(input)
656}
657
658pub fn parse(input: &str) -> Result<Expr, EvalError> {
659    let original_input = input.trim();
660    let mut input = original_input;
661    match expr.parse_next(&mut input) {
662        Ok(e) => {
663            let remaining = input.trim();
664            if remaining.is_empty() {
665                Ok(e)
666            } else {
667                Err(EvalError::ParseError(format!(
668                    "unexpected trailing input: '{}'",
669                    remaining
670                )))
671            }
672        }
673        Err(_) => {
674            // Provide helpful error messages for common mistakes
675            if original_input.starts_with('#') {
676                Err(EvalError::ParseError(
677                    "comments are not supported (lines starting with '#' are treated as constraints)".to_string()
678                ))
679            } else if original_input.contains("//") {
680                Err(EvalError::ParseError(
681                    "comments are not supported ('// ...' is not valid)".to_string(),
682                ))
683            } else if original_input.is_empty() {
684                Err(EvalError::ParseError("empty constraint".to_string()))
685            } else {
686                // Try to give a hint about what went wrong
687                let first_word = original_input.split_whitespace().next().unwrap_or("");
688                if !first_word
689                    .chars()
690                    .next()
691                    .map(|c| c.is_alphabetic() || c == '_')
692                    .unwrap_or(false)
693                    && !first_word.starts_with('(')
694                    && !first_word.starts_with('-')
695                    && !first_word.starts_with('"')
696                    && !first_word.starts_with('[')
697                    && !first_word.starts_with('{')
698                    && !first_word
699                        .chars()
700                        .next()
701                        .map(|c| c.is_numeric())
702                        .unwrap_or(false)
703                {
704                    Err(EvalError::ParseError(format!(
705                        "invalid syntax near '{}' - constraints must be expressions like 'x > 0' or 'len(arr) == 3'",
706                        first_word
707                    )))
708                } else {
709                    Err(EvalError::ParseError(format!(
710                        "invalid expression syntax in '{}'",
711                        if original_input.len() > 50 {
712                            format!("{}...", &original_input[..50])
713                        } else {
714                            original_input.to_string()
715                        }
716                    )))
717                }
718            }
719        }
720    }
721}
722
723// ============ Evaluator ============
724
725pub fn evaluate(expr: &Expr, vars: &HashMap<String, Value>) -> Result<Value, EvalError> {
726    match expr {
727        Expr::Number(n) => Ok(Value::Number(*n)),
728        Expr::String(s) => Ok(Value::String(s.clone())),
729        Expr::Bool(b) => Ok(Value::Bool(*b)),
730        Expr::Null => Ok(Value::Null),
731        Expr::TypeLiteral(t) => Ok(Value::Type(t.clone())),
732        Expr::Var(name) => vars
733            .get(name)
734            .cloned()
735            .ok_or_else(|| EvalError::UndefinedVariable(name.clone())),
736        Expr::Array(elements) => {
737            let values: Result<Vec<_>, _> = elements.iter().map(|e| evaluate(e, vars)).collect();
738            Ok(Value::Array(values?))
739        }
740        Expr::Object(entries) => {
741            let mut map = HashMap::new();
742            for (key, val_expr) in entries {
743                map.insert(key.clone(), evaluate(val_expr, vars)?);
744            }
745            Ok(Value::Object(map))
746        }
747        Expr::UnaryOp { op, expr } => {
748            let val = evaluate(expr, vars)?;
749            match op {
750                UnaryOp::Not => Ok(Value::Bool(!val.as_bool()?)),
751                UnaryOp::Neg => Ok(Value::Number(-val.as_number()?)),
752            }
753        }
754        Expr::BinaryOp { op, left, right } => eval_binary_op(*op, left, right, vars),
755        Expr::FuncCall { name, args } => eval_func_call(name, args, vars),
756        Expr::Index { expr, index } => {
757            let base = evaluate(expr, vars)?;
758            let idx = evaluate(index, vars)?;
759            match &base {
760                Value::Array(arr) => {
761                    let i = idx.as_number()?;
762                    let actual_index = if i < 0.0 {
763                        // Negative indexing: -1 is last element, -2 is second to last, etc.
764                        let neg_idx = (-i) as usize;
765                        if neg_idx > arr.len() {
766                            return Err(EvalError::IndexOutOfBounds {
767                                index: i as i64,
768                                len: arr.len(),
769                            });
770                        }
771                        arr.len() - neg_idx
772                    } else {
773                        i as usize
774                    };
775                    arr.get(actual_index)
776                        .cloned()
777                        .ok_or(EvalError::IndexOutOfBounds {
778                            index: i as i64,
779                            len: arr.len(),
780                        })
781                }
782                Value::String(s) => {
783                    let i = idx.as_number()?;
784                    let chars: Vec<char> = s.chars().collect();
785                    let actual_index = if i < 0.0 {
786                        let neg_idx = (-i) as usize;
787                        if neg_idx > chars.len() {
788                            return Err(EvalError::IndexOutOfBounds {
789                                index: i as i64,
790                                len: chars.len(),
791                            });
792                        }
793                        chars.len() - neg_idx
794                    } else {
795                        i as usize
796                    };
797                    chars
798                        .get(actual_index)
799                        .map(|c| Value::String(c.to_string()))
800                        .ok_or(EvalError::IndexOutOfBounds {
801                            index: i as i64,
802                            len: chars.len(),
803                        })
804                }
805                Value::Object(obj) => {
806                    let key = idx.as_string()?;
807                    obj.get(key)
808                        .cloned()
809                        .ok_or_else(|| EvalError::KeyNotFound(key.to_string()))
810                }
811                _ => Err(EvalError::TypeError {
812                    expected: "array, string, or object",
813                    got: base.type_name(),
814                }),
815            }
816        }
817        Expr::Property { expr, name } => {
818            let base = evaluate(expr, vars)?;
819            let obj = base.as_object()?;
820            obj.get(name)
821                .cloned()
822                .ok_or_else(|| EvalError::KeyNotFound(name.clone()))
823        }
824        Expr::ForAll {
825            predicate,
826            var,
827            iterable,
828        } => {
829            let iter_val = evaluate(iterable, vars)?;
830            let items = match &iter_val {
831                Value::Array(arr) => arr.clone(),
832                Value::Object(obj) => obj.values().cloned().collect(),
833                _ => {
834                    return Err(EvalError::TypeError {
835                        expected: "array or object",
836                        got: iter_val.type_name(),
837                    });
838                }
839            };
840            for item in items {
841                let mut local_vars = vars.clone();
842                local_vars.insert(var.clone(), item);
843                let result = evaluate(predicate, &local_vars)?;
844                if !result.as_bool()? {
845                    return Ok(Value::Bool(false));
846                }
847            }
848            Ok(Value::Bool(true))
849        }
850    }
851}
852
853fn eval_func_call(
854    name: &str,
855    args: &[Expr],
856    vars: &HashMap<String, Value>,
857) -> Result<Value, EvalError> {
858    match name {
859        "len" => {
860            if args.len() != 1 {
861                return Err(EvalError::WrongArgCount {
862                    func: name.to_string(),
863                    expected: 1,
864                    got: args.len(),
865                });
866            }
867            let val = evaluate(&args[0], vars)?;
868            match val {
869                Value::String(s) => Ok(Value::Number(s.chars().count() as f64)),
870                Value::Array(a) => Ok(Value::Number(a.len() as f64)),
871                Value::Object(o) => Ok(Value::Number(o.len() as f64)),
872                _ => Err(EvalError::TypeError {
873                    expected: "string, array, or object",
874                    got: val.type_name(),
875                }),
876            }
877        }
878        "type" => {
879            if args.len() != 1 {
880                return Err(EvalError::WrongArgCount {
881                    func: name.to_string(),
882                    expected: 1,
883                    got: args.len(),
884                });
885            }
886            let val = evaluate(&args[0], vars)?;
887            Ok(Value::Type(val.type_name().to_string()))
888        }
889        "keys" => {
890            if args.len() != 1 {
891                return Err(EvalError::WrongArgCount {
892                    func: name.to_string(),
893                    expected: 1,
894                    got: args.len(),
895                });
896            }
897            let val = evaluate(&args[0], vars)?;
898            let obj = val.as_object()?;
899            let mut keys: Vec<String> = obj.keys().cloned().collect();
900            keys.sort();
901            let keys: Vec<Value> = keys.into_iter().map(Value::String).collect();
902            Ok(Value::Array(keys))
903        }
904        "values" => {
905            if args.len() != 1 {
906                return Err(EvalError::WrongArgCount {
907                    func: name.to_string(),
908                    expected: 1,
909                    got: args.len(),
910                });
911            }
912            let val = evaluate(&args[0], vars)?;
913            let obj = val.as_object()?;
914            // Sort by keys and return corresponding values
915            let mut pairs: Vec<(&String, &Value)> = obj.iter().collect();
916            pairs.sort_by_key(|(k, _)| *k);
917            let values: Vec<Value> = pairs.into_iter().map(|(_, v)| v.clone()).collect();
918            Ok(Value::Array(values))
919        }
920        "sum" => {
921            if args.len() != 1 {
922                return Err(EvalError::WrongArgCount {
923                    func: name.to_string(),
924                    expected: 1,
925                    got: args.len(),
926                });
927            }
928            let val = evaluate(&args[0], vars)?;
929            let arr = val.as_array()?;
930            let mut total = 0.0;
931            for item in arr {
932                total += item.as_number()?;
933            }
934            Ok(Value::Number(total))
935        }
936        "min" => {
937            if args.len() != 1 {
938                return Err(EvalError::WrongArgCount {
939                    func: name.to_string(),
940                    expected: 1,
941                    got: args.len(),
942                });
943            }
944            let val = evaluate(&args[0], vars)?;
945            let arr = val.as_array()?;
946            if arr.is_empty() {
947                return Err(EvalError::TypeError {
948                    expected: "non-empty array",
949                    got: "empty array",
950                });
951            }
952            let mut min_val = arr[0].as_number()?;
953            for item in arr.iter().skip(1) {
954                let n = item.as_number()?;
955                if n < min_val {
956                    min_val = n;
957                }
958            }
959            Ok(Value::Number(min_val))
960        }
961        "max" => {
962            if args.len() != 1 {
963                return Err(EvalError::WrongArgCount {
964                    func: name.to_string(),
965                    expected: 1,
966                    got: args.len(),
967                });
968            }
969            let val = evaluate(&args[0], vars)?;
970            let arr = val.as_array()?;
971            if arr.is_empty() {
972                return Err(EvalError::TypeError {
973                    expected: "non-empty array",
974                    got: "empty array",
975                });
976            }
977            let mut max_val = arr[0].as_number()?;
978            for item in arr.iter().skip(1) {
979                let n = item.as_number()?;
980                if n > max_val {
981                    max_val = n;
982                }
983            }
984            Ok(Value::Number(max_val))
985        }
986        "abs" => {
987            if args.len() != 1 {
988                return Err(EvalError::WrongArgCount {
989                    func: name.to_string(),
990                    expected: 1,
991                    got: args.len(),
992                });
993            }
994            let val = evaluate(&args[0], vars)?;
995            Ok(Value::Number(val.as_number()?.abs()))
996        }
997        "lower" => {
998            if args.len() != 1 {
999                return Err(EvalError::WrongArgCount {
1000                    func: name.to_string(),
1001                    expected: 1,
1002                    got: args.len(),
1003                });
1004            }
1005            let val = evaluate(&args[0], vars)?;
1006            Ok(Value::String(val.as_string()?.to_lowercase()))
1007        }
1008        "upper" => {
1009            if args.len() != 1 {
1010                return Err(EvalError::WrongArgCount {
1011                    func: name.to_string(),
1012                    expected: 1,
1013                    got: args.len(),
1014                });
1015            }
1016            let val = evaluate(&args[0], vars)?;
1017            Ok(Value::String(val.as_string()?.to_uppercase()))
1018        }
1019        "unique" => {
1020            if args.len() != 1 {
1021                return Err(EvalError::WrongArgCount {
1022                    func: name.to_string(),
1023                    expected: 1,
1024                    got: args.len(),
1025                });
1026            }
1027            let val = evaluate(&args[0], vars)?;
1028            let arr = val.as_array()?;
1029            let mut result = Vec::new();
1030            for item in arr {
1031                if !result.iter().any(|v| values_equal(v, item)) {
1032                    result.push(item.clone());
1033                }
1034            }
1035            Ok(Value::Array(result))
1036        }
1037        "env" => {
1038            if args.len() != 1 {
1039                return Err(EvalError::WrongArgCount {
1040                    func: name.to_string(),
1041                    expected: 1,
1042                    got: args.len(),
1043                });
1044            }
1045            let val = evaluate(&args[0], vars)?;
1046            let var_name = val.as_string()?;
1047            match std::env::var(var_name) {
1048                Ok(value) => Ok(Value::String(value)),
1049                Err(_) => Ok(Value::Null),
1050            }
1051        }
1052        _ => Err(EvalError::UndefinedFunction(name.to_string())),
1053    }
1054}
1055
1056fn eval_binary_op(
1057    op: BinaryOp,
1058    left: &Expr,
1059    right: &Expr,
1060    vars: &HashMap<String, Value>,
1061) -> Result<Value, EvalError> {
1062    if op == BinaryOp::And {
1063        let l = evaluate(left, vars)?.as_bool()?;
1064        if !l {
1065            return Ok(Value::Bool(false));
1066        }
1067        return Ok(Value::Bool(evaluate(right, vars)?.as_bool()?));
1068    }
1069    if op == BinaryOp::Or {
1070        let l = evaluate(left, vars)?.as_bool()?;
1071        if l {
1072            return Ok(Value::Bool(true));
1073        }
1074        return Ok(Value::Bool(evaluate(right, vars)?.as_bool()?));
1075    }
1076
1077    let l = evaluate(left, vars)?;
1078    let r = evaluate(right, vars)?;
1079
1080    match op {
1081        BinaryOp::Add => match (&l, &r) {
1082            (Value::String(ls), Value::String(rs)) => Ok(Value::String(format!("{}{}", ls, rs))),
1083            (Value::Array(la), Value::Array(ra)) => {
1084                let mut result = la.clone();
1085                result.extend(ra.clone());
1086                Ok(Value::Array(result))
1087            }
1088            _ => Ok(Value::Number(l.as_number()? + r.as_number()?)),
1089        },
1090        BinaryOp::Sub => Ok(Value::Number(l.as_number()? - r.as_number()?)),
1091        BinaryOp::Mul => Ok(Value::Number(l.as_number()? * r.as_number()?)),
1092        BinaryOp::Mod => {
1093            let divisor = r.as_number()?;
1094            if divisor == 0.0 {
1095                Err(EvalError::DivisionByZero)
1096            } else {
1097                Ok(Value::Number(l.as_number()? % divisor))
1098            }
1099        }
1100        BinaryOp::Div => {
1101            let divisor = r.as_number()?;
1102            if divisor == 0.0 {
1103                Err(EvalError::DivisionByZero)
1104            } else {
1105                Ok(Value::Number(l.as_number()? / divisor))
1106            }
1107        }
1108        BinaryOp::Pow => Ok(Value::Number(l.as_number()?.powf(r.as_number()?))),
1109        BinaryOp::Eq => Ok(Value::Bool(values_equal(&l, &r))),
1110        BinaryOp::Ne => Ok(Value::Bool(!values_equal(&l, &r))),
1111        BinaryOp::Lt => match (&l, &r) {
1112            (Value::String(ls), Value::String(rs)) => Ok(Value::Bool(ls < rs)),
1113            _ => Ok(Value::Bool(l.as_number()? < r.as_number()?)),
1114        },
1115        BinaryOp::Le => match (&l, &r) {
1116            (Value::String(ls), Value::String(rs)) => Ok(Value::Bool(ls <= rs)),
1117            _ => Ok(Value::Bool(l.as_number()? <= r.as_number()?)),
1118        },
1119        BinaryOp::Gt => match (&l, &r) {
1120            (Value::String(ls), Value::String(rs)) => Ok(Value::Bool(ls > rs)),
1121            _ => Ok(Value::Bool(l.as_number()? > r.as_number()?)),
1122        },
1123        BinaryOp::Ge => match (&l, &r) {
1124            (Value::String(ls), Value::String(rs)) => Ok(Value::Bool(ls >= rs)),
1125            _ => Ok(Value::Bool(l.as_number()? >= r.as_number()?)),
1126        },
1127        BinaryOp::Contains | BinaryOp::NotContains => {
1128            let result = match &l {
1129                Value::String(haystack) => {
1130                    let needle = r.as_string()?;
1131                    haystack.contains(needle)
1132                }
1133                Value::Array(arr) => arr.iter().any(|v| values_equal(v, &r)),
1134                Value::Object(obj) => {
1135                    let key = r.as_string()?;
1136                    obj.contains_key(key)
1137                }
1138                _ => {
1139                    return Err(EvalError::TypeError {
1140                        expected: "string, array, or object",
1141                        got: l.type_name(),
1142                    })
1143                }
1144            };
1145            Ok(Value::Bool(if op == BinaryOp::NotContains {
1146                !result
1147            } else {
1148                result
1149            }))
1150        }
1151        BinaryOp::StartsWith | BinaryOp::NotStartsWith => {
1152            let s = l.as_string()?;
1153            let prefix = r.as_string()?;
1154            let result = s.starts_with(prefix);
1155            Ok(Value::Bool(if op == BinaryOp::NotStartsWith {
1156                !result
1157            } else {
1158                result
1159            }))
1160        }
1161        BinaryOp::EndsWith | BinaryOp::NotEndsWith => {
1162            let s = l.as_string()?;
1163            let suffix = r.as_string()?;
1164            let result = s.ends_with(suffix);
1165            Ok(Value::Bool(if op == BinaryOp::NotEndsWith {
1166                !result
1167            } else {
1168                result
1169            }))
1170        }
1171        BinaryOp::Matches | BinaryOp::NotMatches => {
1172            let s = l.as_string()?;
1173            let pattern = r.as_string()?;
1174            let re =
1175                regex::Regex::new(pattern).map_err(|e| EvalError::InvalidRegex(e.to_string()))?;
1176            let result = re.is_match(s);
1177            Ok(Value::Bool(if op == BinaryOp::NotMatches {
1178                !result
1179            } else {
1180                result
1181            }))
1182        }
1183        BinaryOp::And | BinaryOp::Or => unreachable!(),
1184    }
1185}
1186
1187fn values_equal(a: &Value, b: &Value) -> bool {
1188    match (a, b) {
1189        (Value::Number(a), Value::Number(b)) => (a - b).abs() < f64::EPSILON,
1190        (Value::String(a), Value::String(b)) => a == b,
1191        (Value::Bool(a), Value::Bool(b)) => a == b,
1192        (Value::Null, Value::Null) => true,
1193        // Allow null literal to match Type("null") for type comparisons like `type(x) == null`
1194        (Value::Null, Value::Type(t)) | (Value::Type(t), Value::Null) => t == "null",
1195        (Value::Array(a), Value::Array(b)) => {
1196            a.len() == b.len() && a.iter().zip(b.iter()).all(|(x, y)| values_equal(x, y))
1197        }
1198        (Value::Object(a), Value::Object(b)) => {
1199            a.len() == b.len()
1200                && a.iter()
1201                    .all(|(k, v)| b.get(k).map(|bv| values_equal(v, bv)).unwrap_or(false))
1202        }
1203        (Value::Type(a), Value::Type(b)) => a == b,
1204        _ => false,
1205    }
1206}
1207
1208// ============ Public API ============
1209
1210pub fn eval_bool(expr_str: &str, vars: &HashMap<String, Value>) -> Result<bool, EvalError> {
1211    let ast = parse(expr_str)?;
1212    let result = evaluate(&ast, vars)?;
1213    result.as_bool()
1214}
1215
1216#[cfg(test)]
1217mod tests {
1218    use super::*;
1219
1220    fn vars(pairs: &[(&str, Value)]) -> HashMap<String, Value> {
1221        pairs
1222            .iter()
1223            .map(|(k, v)| (k.to_string(), v.clone()))
1224            .collect()
1225    }
1226
1227    #[test]
1228    fn test_number_parsing() {
1229        assert_eq!(parse("42").unwrap(), Expr::Number(42.0));
1230        assert_eq!(parse("0.5").unwrap(), Expr::Number(0.5));
1231    }
1232
1233    #[test]
1234    fn test_string_parsing() {
1235        assert_eq!(
1236            parse(r#""hello""#).unwrap(),
1237            Expr::String("hello".to_string())
1238        );
1239    }
1240
1241    #[test]
1242    fn test_arithmetic() {
1243        let v = vars(&[]);
1244        assert!(eval_bool("1 + 2 == 3", &v).unwrap());
1245        assert!(eval_bool("10 - 3 == 7", &v).unwrap());
1246        assert!(eval_bool("4 * 5 == 20", &v).unwrap());
1247        assert!(eval_bool("10 / 2 == 5", &v).unwrap());
1248        assert!(eval_bool("2 ^ 3 == 8", &v).unwrap());
1249        assert!(eval_bool("1 + 2 * 3 == 7", &v).unwrap());
1250        assert!(eval_bool("(1 + 2) * 3 == 9", &v).unwrap());
1251    }
1252
1253    #[test]
1254    fn test_comparisons() {
1255        let v = vars(&[("n", Value::Number(42.0))]);
1256        assert!(eval_bool("n > 0", &v).unwrap());
1257        assert!(eval_bool("n < 100", &v).unwrap());
1258        assert!(eval_bool("n >= 42", &v).unwrap());
1259        assert!(eval_bool("n <= 42", &v).unwrap());
1260        assert!(eval_bool("n == 42", &v).unwrap());
1261        assert!(eval_bool("n != 0", &v).unwrap());
1262    }
1263
1264    #[test]
1265    fn test_boolean_logic() {
1266        let v = vars(&[("n", Value::Number(42.0))]);
1267        assert!(eval_bool("n > 0 and n < 100", &v).unwrap());
1268        assert!(eval_bool("n < 0 or n > 0", &v).unwrap());
1269        assert!(eval_bool("not (n < 0)", &v).unwrap());
1270    }
1271
1272    #[test]
1273    fn test_array_contains() {
1274        let v = vars(&[("n", Value::Number(2.0))]);
1275        assert!(eval_bool("[1, 2, 3] contains n", &v).unwrap());
1276        assert!(!eval_bool("[4, 5, 6] contains n", &v).unwrap());
1277    }
1278
1279    #[test]
1280    fn test_array_not_contains() {
1281        let v = vars(&[("n", Value::Number(5.0))]);
1282        assert!(eval_bool("not ([1, 2, 3] contains n)", &v).unwrap());
1283        assert!(!eval_bool("not ([4, 5, 6] contains n)", &v).unwrap());
1284    }
1285
1286    #[test]
1287    fn test_object_contains_key() {
1288        let mut obj = HashMap::new();
1289        obj.insert("name".to_string(), Value::String("alice".to_string()));
1290        obj.insert("age".to_string(), Value::Number(30.0));
1291        let v = vars(&[("o", Value::Object(obj))]);
1292        assert!(eval_bool("o contains \"name\"", &v).unwrap());
1293        assert!(eval_bool("o contains \"age\"", &v).unwrap());
1294        assert!(!eval_bool("o contains \"email\"", &v).unwrap());
1295    }
1296
1297    #[test]
1298    fn test_string_operators() {
1299        let v = vars(&[("s", Value::String("hello world".to_string()))]);
1300        assert!(eval_bool(r#"s contains "world""#, &v).unwrap());
1301        assert!(eval_bool(r#"s startswith "hello""#, &v).unwrap());
1302        assert!(eval_bool(r#"s endswith "world""#, &v).unwrap());
1303    }
1304
1305    #[test]
1306    fn test_negated_string_operators() {
1307        let v = vars(&[("s", Value::String("hello world".to_string()))]);
1308        assert!(eval_bool(r#"s not contains "foo""#, &v).unwrap());
1309        assert!(!eval_bool(r#"s not contains "world""#, &v).unwrap());
1310        assert!(eval_bool(r#"s not startswith "foo""#, &v).unwrap());
1311        assert!(!eval_bool(r#"s not startswith "hello""#, &v).unwrap());
1312        assert!(eval_bool(r#"s not endswith "foo""#, &v).unwrap());
1313        assert!(!eval_bool(r#"s not endswith "world""#, &v).unwrap());
1314    }
1315
1316    #[test]
1317    fn test_regex_matches() {
1318        let v = vars(&[("s", Value::String("hello123".to_string()))]);
1319        assert!(eval_bool(r#"s matches /^hello\d+$/"#, &v).unwrap());
1320    }
1321
1322    #[test]
1323    fn test_negated_regex_matches() {
1324        let v = vars(&[("s", Value::String("hello123".to_string()))]);
1325        assert!(eval_bool(r#"s not matches /^foo/"#, &v).unwrap());
1326        assert!(!eval_bool(r#"s not matches /^hello\d+$/"#, &v).unwrap());
1327    }
1328
1329    #[test]
1330    fn test_negated_array_contains() {
1331        let v = vars(&[("n", Value::Number(5.0))]);
1332        assert!(eval_bool("[1, 2, 3] not contains n", &v).unwrap());
1333        assert!(!eval_bool("[4, 5, 6] not contains n", &v).unwrap());
1334    }
1335
1336    #[test]
1337    fn test_len_function() {
1338        let v = vars(&[("s", Value::String("hello".to_string()))]);
1339        assert!(eval_bool("len(s) == 5", &v).unwrap());
1340    }
1341
1342    #[test]
1343    fn test_backslash_in_string() {
1344        // Test that backslash is parsed correctly
1345        let v = vars(&[("p", Value::String("C:\\Users\\test".to_string()))]);
1346
1347        // Should contain "test"
1348        assert!(eval_bool(r#"p contains "test""#, &v).unwrap());
1349
1350        // Should contain backslash (escaped in the expression)
1351        assert!(eval_bool(r#"p contains "\\""#, &v).unwrap());
1352
1353        // Should contain "Users"
1354        assert!(eval_bool(r#"p contains "Users""#, &v).unwrap());
1355    }
1356
1357    #[test]
1358    fn test_array_indexing() {
1359        let v = vars(&[(
1360            "a",
1361            Value::Array(vec![
1362                Value::Number(10.0),
1363                Value::Number(20.0),
1364                Value::Number(30.0),
1365            ]),
1366        )]);
1367        assert!(eval_bool("a[0] == 10", &v).unwrap());
1368        assert!(eval_bool("a[1] == 20", &v).unwrap());
1369        assert!(eval_bool("a[2] == 30", &v).unwrap());
1370    }
1371
1372    #[test]
1373    fn test_object_property_access() {
1374        let mut obj = HashMap::new();
1375        obj.insert("name".to_string(), Value::String("alice".to_string()));
1376        obj.insert("age".to_string(), Value::Number(30.0));
1377        let v = vars(&[("o", Value::Object(obj))]);
1378
1379        assert!(eval_bool(r#"o.name == "alice""#, &v).unwrap());
1380        assert!(eval_bool("o.age == 30", &v).unwrap());
1381        assert!(eval_bool(r#"o["name"] == "alice""#, &v).unwrap());
1382    }
1383
1384    #[test]
1385    fn test_nested_access() {
1386        let inner = Value::Array(vec![Value::Number(1.0), Value::Number(2.0)]);
1387        let mut obj = HashMap::new();
1388        obj.insert("items".to_string(), inner);
1389        let v = vars(&[("o", Value::Object(obj))]);
1390
1391        assert!(eval_bool("o.items[0] == 1", &v).unwrap());
1392        assert!(eval_bool("o.items[1] == 2", &v).unwrap());
1393        assert!(eval_bool("len(o.items) == 2", &v).unwrap());
1394    }
1395
1396    #[test]
1397    fn test_type_function() {
1398        let v = vars(&[
1399            ("n", Value::Number(42.0)),
1400            ("s", Value::String("hello".to_string())),
1401            ("b", Value::Bool(true)),
1402            ("a", Value::Array(vec![])),
1403        ]);
1404
1405        assert!(eval_bool("type(n) == number", &v).unwrap());
1406        assert!(eval_bool("type(s) == string", &v).unwrap());
1407        assert!(eval_bool("type(b) == bool", &v).unwrap());
1408        assert!(eval_bool("type(a) == array", &v).unwrap());
1409    }
1410
1411    #[test]
1412    fn test_keys_function() {
1413        let mut obj = HashMap::new();
1414        obj.insert("a".to_string(), Value::Number(1.0));
1415        obj.insert("b".to_string(), Value::Number(2.0));
1416        let v = vars(&[("o", Value::Object(obj))]);
1417
1418        assert!(eval_bool("len(keys(o)) == 2", &v).unwrap());
1419    }
1420
1421    #[test]
1422    fn test_forall_array() {
1423        let v = vars(&[(
1424            "a",
1425            Value::Array(vec![
1426                Value::Number(1.0),
1427                Value::Number(2.0),
1428                Value::Number(3.0),
1429            ]),
1430        )]);
1431
1432        assert!(eval_bool("x <= 3 forall x in a", &v).unwrap());
1433        assert!(eval_bool("x > 0 forall x in a", &v).unwrap());
1434        assert!(!eval_bool("x > 2 forall x in a", &v).unwrap());
1435    }
1436
1437    #[test]
1438    fn test_forall_object() {
1439        let mut obj = HashMap::new();
1440        obj.insert("a".to_string(), Value::Number(1.0));
1441        obj.insert("b".to_string(), Value::Number(2.0));
1442        obj.insert("c".to_string(), Value::Number(3.0));
1443        let v = vars(&[("o", Value::Object(obj))]);
1444
1445        assert!(eval_bool("x <= 3 forall x in o", &v).unwrap());
1446        assert!(eval_bool("type(x) == number forall x in o", &v).unwrap());
1447    }
1448
1449    #[test]
1450    fn test_object_literal() {
1451        let v = vars(&[]);
1452        assert!(eval_bool(r#"{"a": 1, "b": 2}.a == 1"#, &v).unwrap());
1453        assert!(eval_bool(r#"len({"x": 1, "y": 2}) == 2"#, &v).unwrap());
1454    }
1455
1456    #[test]
1457    fn test_type_literal() {
1458        let v = vars(&[("n", Value::Number(42.0))]);
1459        assert!(eval_bool("type(n) == number", &v).unwrap());
1460        assert!(!eval_bool("type(n) == string", &v).unwrap());
1461    }
1462
1463    #[test]
1464    fn test_len_object() {
1465        let mut obj = HashMap::new();
1466        obj.insert("a".to_string(), Value::Number(1.0));
1467        obj.insert("b".to_string(), Value::Number(2.0));
1468        let v = vars(&[("o", Value::Object(obj))]);
1469
1470        assert!(eval_bool("len(o) == 2", &v).unwrap());
1471    }
1472
1473    #[test]
1474    fn test_bool_comparison() {
1475        let v = vars(&[("b", Value::Bool(true))]);
1476        assert!(eval_bool("b == true", &v).unwrap());
1477        assert!(eval_bool("b != false", &v).unwrap());
1478        assert!(eval_bool("(1 == 1) == true", &v).unwrap());
1479    }
1480
1481    #[test]
1482    fn test_env_function() {
1483        std::env::set_var("CCTR_TEST_VAR", "test_value");
1484        let v = vars(&[]);
1485        assert!(eval_bool(r#"env("CCTR_TEST_VAR") == "test_value""#, &v).unwrap());
1486        assert!(eval_bool(r#"type(env("CCTR_TEST_VAR")) == string"#, &v).unwrap());
1487        // Non-existent env var returns null
1488        assert!(eval_bool(r#"env("CCTR_NONEXISTENT_VAR_12345") == null"#, &v).unwrap());
1489        assert!(eval_bool(r#"type(env("CCTR_NONEXISTENT_VAR_12345")) == null"#, &v).unwrap());
1490        std::env::remove_var("CCTR_TEST_VAR");
1491    }
1492}