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        "strip" => {
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            Ok(Value::String(val.as_string()?.trim().to_string()))
1029        }
1030        "unique" => {
1031            if args.len() != 1 {
1032                return Err(EvalError::WrongArgCount {
1033                    func: name.to_string(),
1034                    expected: 1,
1035                    got: args.len(),
1036                });
1037            }
1038            let val = evaluate(&args[0], vars)?;
1039            let arr = val.as_array()?;
1040            let mut result = Vec::new();
1041            for item in arr {
1042                if !result.iter().any(|v| values_equal(v, item)) {
1043                    result.push(item.clone());
1044                }
1045            }
1046            Ok(Value::Array(result))
1047        }
1048        "env" => {
1049            if args.len() != 1 {
1050                return Err(EvalError::WrongArgCount {
1051                    func: name.to_string(),
1052                    expected: 1,
1053                    got: args.len(),
1054                });
1055            }
1056            let val = evaluate(&args[0], vars)?;
1057            let var_name = val.as_string()?;
1058            match std::env::var(var_name) {
1059                Ok(value) => Ok(Value::String(value)),
1060                Err(_) => Ok(Value::Null),
1061            }
1062        }
1063        _ => Err(EvalError::UndefinedFunction(name.to_string())),
1064    }
1065}
1066
1067fn eval_binary_op(
1068    op: BinaryOp,
1069    left: &Expr,
1070    right: &Expr,
1071    vars: &HashMap<String, Value>,
1072) -> Result<Value, EvalError> {
1073    if op == BinaryOp::And {
1074        let l = evaluate(left, vars)?.as_bool()?;
1075        if !l {
1076            return Ok(Value::Bool(false));
1077        }
1078        return Ok(Value::Bool(evaluate(right, vars)?.as_bool()?));
1079    }
1080    if op == BinaryOp::Or {
1081        let l = evaluate(left, vars)?.as_bool()?;
1082        if l {
1083            return Ok(Value::Bool(true));
1084        }
1085        return Ok(Value::Bool(evaluate(right, vars)?.as_bool()?));
1086    }
1087
1088    let l = evaluate(left, vars)?;
1089    let r = evaluate(right, vars)?;
1090
1091    match op {
1092        BinaryOp::Add => match (&l, &r) {
1093            (Value::String(ls), Value::String(rs)) => Ok(Value::String(format!("{}{}", ls, rs))),
1094            (Value::Array(la), Value::Array(ra)) => {
1095                let mut result = la.clone();
1096                result.extend(ra.clone());
1097                Ok(Value::Array(result))
1098            }
1099            _ => Ok(Value::Number(l.as_number()? + r.as_number()?)),
1100        },
1101        BinaryOp::Sub => Ok(Value::Number(l.as_number()? - r.as_number()?)),
1102        BinaryOp::Mul => Ok(Value::Number(l.as_number()? * r.as_number()?)),
1103        BinaryOp::Mod => {
1104            let divisor = r.as_number()?;
1105            if divisor == 0.0 {
1106                Err(EvalError::DivisionByZero)
1107            } else {
1108                Ok(Value::Number(l.as_number()? % divisor))
1109            }
1110        }
1111        BinaryOp::Div => {
1112            let divisor = r.as_number()?;
1113            if divisor == 0.0 {
1114                Err(EvalError::DivisionByZero)
1115            } else {
1116                Ok(Value::Number(l.as_number()? / divisor))
1117            }
1118        }
1119        BinaryOp::Pow => Ok(Value::Number(l.as_number()?.powf(r.as_number()?))),
1120        BinaryOp::Eq => Ok(Value::Bool(values_equal(&l, &r))),
1121        BinaryOp::Ne => Ok(Value::Bool(!values_equal(&l, &r))),
1122        BinaryOp::Lt => match (&l, &r) {
1123            (Value::String(ls), Value::String(rs)) => Ok(Value::Bool(ls < rs)),
1124            _ => Ok(Value::Bool(l.as_number()? < r.as_number()?)),
1125        },
1126        BinaryOp::Le => match (&l, &r) {
1127            (Value::String(ls), Value::String(rs)) => Ok(Value::Bool(ls <= rs)),
1128            _ => Ok(Value::Bool(l.as_number()? <= r.as_number()?)),
1129        },
1130        BinaryOp::Gt => match (&l, &r) {
1131            (Value::String(ls), Value::String(rs)) => Ok(Value::Bool(ls > rs)),
1132            _ => Ok(Value::Bool(l.as_number()? > r.as_number()?)),
1133        },
1134        BinaryOp::Ge => match (&l, &r) {
1135            (Value::String(ls), Value::String(rs)) => Ok(Value::Bool(ls >= rs)),
1136            _ => Ok(Value::Bool(l.as_number()? >= r.as_number()?)),
1137        },
1138        BinaryOp::Contains | BinaryOp::NotContains => {
1139            let result = match &l {
1140                Value::String(haystack) => {
1141                    let needle = r.as_string()?;
1142                    haystack.contains(needle)
1143                }
1144                Value::Array(arr) => arr.iter().any(|v| values_equal(v, &r)),
1145                Value::Object(obj) => {
1146                    let key = r.as_string()?;
1147                    obj.contains_key(key)
1148                }
1149                _ => {
1150                    return Err(EvalError::TypeError {
1151                        expected: "string, array, or object",
1152                        got: l.type_name(),
1153                    })
1154                }
1155            };
1156            Ok(Value::Bool(if op == BinaryOp::NotContains {
1157                !result
1158            } else {
1159                result
1160            }))
1161        }
1162        BinaryOp::StartsWith | BinaryOp::NotStartsWith => {
1163            let s = l.as_string()?;
1164            let prefix = r.as_string()?;
1165            let result = s.starts_with(prefix);
1166            Ok(Value::Bool(if op == BinaryOp::NotStartsWith {
1167                !result
1168            } else {
1169                result
1170            }))
1171        }
1172        BinaryOp::EndsWith | BinaryOp::NotEndsWith => {
1173            let s = l.as_string()?;
1174            let suffix = r.as_string()?;
1175            let result = s.ends_with(suffix);
1176            Ok(Value::Bool(if op == BinaryOp::NotEndsWith {
1177                !result
1178            } else {
1179                result
1180            }))
1181        }
1182        BinaryOp::Matches | BinaryOp::NotMatches => {
1183            let s = l.as_string()?;
1184            let pattern = r.as_string()?;
1185            let re =
1186                regex::Regex::new(pattern).map_err(|e| EvalError::InvalidRegex(e.to_string()))?;
1187            let result = re.is_match(s);
1188            Ok(Value::Bool(if op == BinaryOp::NotMatches {
1189                !result
1190            } else {
1191                result
1192            }))
1193        }
1194        BinaryOp::And | BinaryOp::Or => unreachable!(),
1195    }
1196}
1197
1198fn values_equal(a: &Value, b: &Value) -> bool {
1199    match (a, b) {
1200        (Value::Number(a), Value::Number(b)) => (a - b).abs() < f64::EPSILON,
1201        (Value::String(a), Value::String(b)) => a == b,
1202        (Value::Bool(a), Value::Bool(b)) => a == b,
1203        (Value::Null, Value::Null) => true,
1204        // Allow null literal to match Type("null") for type comparisons like `type(x) == null`
1205        (Value::Null, Value::Type(t)) | (Value::Type(t), Value::Null) => t == "null",
1206        (Value::Array(a), Value::Array(b)) => {
1207            a.len() == b.len() && a.iter().zip(b.iter()).all(|(x, y)| values_equal(x, y))
1208        }
1209        (Value::Object(a), Value::Object(b)) => {
1210            a.len() == b.len()
1211                && a.iter()
1212                    .all(|(k, v)| b.get(k).map(|bv| values_equal(v, bv)).unwrap_or(false))
1213        }
1214        (Value::Type(a), Value::Type(b)) => a == b,
1215        _ => false,
1216    }
1217}
1218
1219// ============ Public API ============
1220
1221pub fn eval_bool(expr_str: &str, vars: &HashMap<String, Value>) -> Result<bool, EvalError> {
1222    let ast = parse(expr_str)?;
1223    let result = evaluate(&ast, vars)?;
1224    result.as_bool()
1225}
1226
1227#[cfg(test)]
1228mod tests {
1229    use super::*;
1230
1231    fn vars(pairs: &[(&str, Value)]) -> HashMap<String, Value> {
1232        pairs
1233            .iter()
1234            .map(|(k, v)| (k.to_string(), v.clone()))
1235            .collect()
1236    }
1237
1238    #[test]
1239    fn test_number_parsing() {
1240        assert_eq!(parse("42").unwrap(), Expr::Number(42.0));
1241        assert_eq!(parse("0.5").unwrap(), Expr::Number(0.5));
1242    }
1243
1244    #[test]
1245    fn test_string_parsing() {
1246        assert_eq!(
1247            parse(r#""hello""#).unwrap(),
1248            Expr::String("hello".to_string())
1249        );
1250    }
1251
1252    #[test]
1253    fn test_arithmetic() {
1254        let v = vars(&[]);
1255        assert!(eval_bool("1 + 2 == 3", &v).unwrap());
1256        assert!(eval_bool("10 - 3 == 7", &v).unwrap());
1257        assert!(eval_bool("4 * 5 == 20", &v).unwrap());
1258        assert!(eval_bool("10 / 2 == 5", &v).unwrap());
1259        assert!(eval_bool("2 ^ 3 == 8", &v).unwrap());
1260        assert!(eval_bool("1 + 2 * 3 == 7", &v).unwrap());
1261        assert!(eval_bool("(1 + 2) * 3 == 9", &v).unwrap());
1262    }
1263
1264    #[test]
1265    fn test_comparisons() {
1266        let v = vars(&[("n", Value::Number(42.0))]);
1267        assert!(eval_bool("n > 0", &v).unwrap());
1268        assert!(eval_bool("n < 100", &v).unwrap());
1269        assert!(eval_bool("n >= 42", &v).unwrap());
1270        assert!(eval_bool("n <= 42", &v).unwrap());
1271        assert!(eval_bool("n == 42", &v).unwrap());
1272        assert!(eval_bool("n != 0", &v).unwrap());
1273    }
1274
1275    #[test]
1276    fn test_boolean_logic() {
1277        let v = vars(&[("n", Value::Number(42.0))]);
1278        assert!(eval_bool("n > 0 and n < 100", &v).unwrap());
1279        assert!(eval_bool("n < 0 or n > 0", &v).unwrap());
1280        assert!(eval_bool("not (n < 0)", &v).unwrap());
1281    }
1282
1283    #[test]
1284    fn test_array_contains() {
1285        let v = vars(&[("n", Value::Number(2.0))]);
1286        assert!(eval_bool("[1, 2, 3] contains n", &v).unwrap());
1287        assert!(!eval_bool("[4, 5, 6] contains n", &v).unwrap());
1288    }
1289
1290    #[test]
1291    fn test_array_not_contains() {
1292        let v = vars(&[("n", Value::Number(5.0))]);
1293        assert!(eval_bool("not ([1, 2, 3] contains n)", &v).unwrap());
1294        assert!(!eval_bool("not ([4, 5, 6] contains n)", &v).unwrap());
1295    }
1296
1297    #[test]
1298    fn test_object_contains_key() {
1299        let mut obj = HashMap::new();
1300        obj.insert("name".to_string(), Value::String("alice".to_string()));
1301        obj.insert("age".to_string(), Value::Number(30.0));
1302        let v = vars(&[("o", Value::Object(obj))]);
1303        assert!(eval_bool("o contains \"name\"", &v).unwrap());
1304        assert!(eval_bool("o contains \"age\"", &v).unwrap());
1305        assert!(!eval_bool("o contains \"email\"", &v).unwrap());
1306    }
1307
1308    #[test]
1309    fn test_string_operators() {
1310        let v = vars(&[("s", Value::String("hello world".to_string()))]);
1311        assert!(eval_bool(r#"s contains "world""#, &v).unwrap());
1312        assert!(eval_bool(r#"s startswith "hello""#, &v).unwrap());
1313        assert!(eval_bool(r#"s endswith "world""#, &v).unwrap());
1314    }
1315
1316    #[test]
1317    fn test_negated_string_operators() {
1318        let v = vars(&[("s", Value::String("hello world".to_string()))]);
1319        assert!(eval_bool(r#"s not contains "foo""#, &v).unwrap());
1320        assert!(!eval_bool(r#"s not contains "world""#, &v).unwrap());
1321        assert!(eval_bool(r#"s not startswith "foo""#, &v).unwrap());
1322        assert!(!eval_bool(r#"s not startswith "hello""#, &v).unwrap());
1323        assert!(eval_bool(r#"s not endswith "foo""#, &v).unwrap());
1324        assert!(!eval_bool(r#"s not endswith "world""#, &v).unwrap());
1325    }
1326
1327    #[test]
1328    fn test_regex_matches() {
1329        let v = vars(&[("s", Value::String("hello123".to_string()))]);
1330        assert!(eval_bool(r#"s matches /^hello\d+$/"#, &v).unwrap());
1331    }
1332
1333    #[test]
1334    fn test_negated_regex_matches() {
1335        let v = vars(&[("s", Value::String("hello123".to_string()))]);
1336        assert!(eval_bool(r#"s not matches /^foo/"#, &v).unwrap());
1337        assert!(!eval_bool(r#"s not matches /^hello\d+$/"#, &v).unwrap());
1338    }
1339
1340    #[test]
1341    fn test_negated_array_contains() {
1342        let v = vars(&[("n", Value::Number(5.0))]);
1343        assert!(eval_bool("[1, 2, 3] not contains n", &v).unwrap());
1344        assert!(!eval_bool("[4, 5, 6] not contains n", &v).unwrap());
1345    }
1346
1347    #[test]
1348    fn test_len_function() {
1349        let v = vars(&[("s", Value::String("hello".to_string()))]);
1350        assert!(eval_bool("len(s) == 5", &v).unwrap());
1351    }
1352
1353    #[test]
1354    fn test_backslash_in_string() {
1355        // Test that backslash is parsed correctly
1356        let v = vars(&[("p", Value::String("C:\\Users\\test".to_string()))]);
1357
1358        // Should contain "test"
1359        assert!(eval_bool(r#"p contains "test""#, &v).unwrap());
1360
1361        // Should contain backslash (escaped in the expression)
1362        assert!(eval_bool(r#"p contains "\\""#, &v).unwrap());
1363
1364        // Should contain "Users"
1365        assert!(eval_bool(r#"p contains "Users""#, &v).unwrap());
1366    }
1367
1368    #[test]
1369    fn test_array_indexing() {
1370        let v = vars(&[(
1371            "a",
1372            Value::Array(vec![
1373                Value::Number(10.0),
1374                Value::Number(20.0),
1375                Value::Number(30.0),
1376            ]),
1377        )]);
1378        assert!(eval_bool("a[0] == 10", &v).unwrap());
1379        assert!(eval_bool("a[1] == 20", &v).unwrap());
1380        assert!(eval_bool("a[2] == 30", &v).unwrap());
1381    }
1382
1383    #[test]
1384    fn test_object_property_access() {
1385        let mut obj = HashMap::new();
1386        obj.insert("name".to_string(), Value::String("alice".to_string()));
1387        obj.insert("age".to_string(), Value::Number(30.0));
1388        let v = vars(&[("o", Value::Object(obj))]);
1389
1390        assert!(eval_bool(r#"o.name == "alice""#, &v).unwrap());
1391        assert!(eval_bool("o.age == 30", &v).unwrap());
1392        assert!(eval_bool(r#"o["name"] == "alice""#, &v).unwrap());
1393    }
1394
1395    #[test]
1396    fn test_nested_access() {
1397        let inner = Value::Array(vec![Value::Number(1.0), Value::Number(2.0)]);
1398        let mut obj = HashMap::new();
1399        obj.insert("items".to_string(), inner);
1400        let v = vars(&[("o", Value::Object(obj))]);
1401
1402        assert!(eval_bool("o.items[0] == 1", &v).unwrap());
1403        assert!(eval_bool("o.items[1] == 2", &v).unwrap());
1404        assert!(eval_bool("len(o.items) == 2", &v).unwrap());
1405    }
1406
1407    #[test]
1408    fn test_type_function() {
1409        let v = vars(&[
1410            ("n", Value::Number(42.0)),
1411            ("s", Value::String("hello".to_string())),
1412            ("b", Value::Bool(true)),
1413            ("a", Value::Array(vec![])),
1414        ]);
1415
1416        assert!(eval_bool("type(n) == number", &v).unwrap());
1417        assert!(eval_bool("type(s) == string", &v).unwrap());
1418        assert!(eval_bool("type(b) == bool", &v).unwrap());
1419        assert!(eval_bool("type(a) == array", &v).unwrap());
1420    }
1421
1422    #[test]
1423    fn test_keys_function() {
1424        let mut obj = HashMap::new();
1425        obj.insert("a".to_string(), Value::Number(1.0));
1426        obj.insert("b".to_string(), Value::Number(2.0));
1427        let v = vars(&[("o", Value::Object(obj))]);
1428
1429        assert!(eval_bool("len(keys(o)) == 2", &v).unwrap());
1430    }
1431
1432    #[test]
1433    fn test_forall_array() {
1434        let v = vars(&[(
1435            "a",
1436            Value::Array(vec![
1437                Value::Number(1.0),
1438                Value::Number(2.0),
1439                Value::Number(3.0),
1440            ]),
1441        )]);
1442
1443        assert!(eval_bool("x <= 3 forall x in a", &v).unwrap());
1444        assert!(eval_bool("x > 0 forall x in a", &v).unwrap());
1445        assert!(!eval_bool("x > 2 forall x in a", &v).unwrap());
1446    }
1447
1448    #[test]
1449    fn test_forall_object() {
1450        let mut obj = HashMap::new();
1451        obj.insert("a".to_string(), Value::Number(1.0));
1452        obj.insert("b".to_string(), Value::Number(2.0));
1453        obj.insert("c".to_string(), Value::Number(3.0));
1454        let v = vars(&[("o", Value::Object(obj))]);
1455
1456        assert!(eval_bool("x <= 3 forall x in o", &v).unwrap());
1457        assert!(eval_bool("type(x) == number forall x in o", &v).unwrap());
1458    }
1459
1460    #[test]
1461    fn test_object_literal() {
1462        let v = vars(&[]);
1463        assert!(eval_bool(r#"{"a": 1, "b": 2}.a == 1"#, &v).unwrap());
1464        assert!(eval_bool(r#"len({"x": 1, "y": 2}) == 2"#, &v).unwrap());
1465    }
1466
1467    #[test]
1468    fn test_type_literal() {
1469        let v = vars(&[("n", Value::Number(42.0))]);
1470        assert!(eval_bool("type(n) == number", &v).unwrap());
1471        assert!(!eval_bool("type(n) == string", &v).unwrap());
1472    }
1473
1474    #[test]
1475    fn test_len_object() {
1476        let mut obj = HashMap::new();
1477        obj.insert("a".to_string(), Value::Number(1.0));
1478        obj.insert("b".to_string(), Value::Number(2.0));
1479        let v = vars(&[("o", Value::Object(obj))]);
1480
1481        assert!(eval_bool("len(o) == 2", &v).unwrap());
1482    }
1483
1484    #[test]
1485    fn test_bool_comparison() {
1486        let v = vars(&[("b", Value::Bool(true))]);
1487        assert!(eval_bool("b == true", &v).unwrap());
1488        assert!(eval_bool("b != false", &v).unwrap());
1489        assert!(eval_bool("(1 == 1) == true", &v).unwrap());
1490    }
1491
1492    #[test]
1493    fn test_env_function() {
1494        std::env::set_var("CCTR_TEST_VAR", "test_value");
1495        let v = vars(&[]);
1496        assert!(eval_bool(r#"env("CCTR_TEST_VAR") == "test_value""#, &v).unwrap());
1497        assert!(eval_bool(r#"type(env("CCTR_TEST_VAR")) == string"#, &v).unwrap());
1498        // Non-existent env var returns null
1499        assert!(eval_bool(r#"env("CCTR_NONEXISTENT_VAR_12345") == null"#, &v).unwrap());
1500        assert!(eval_bool(r#"type(env("CCTR_NONEXISTENT_VAR_12345")) == null"#, &v).unwrap());
1501        std::env::remove_var("CCTR_TEST_VAR");
1502    }
1503
1504    #[test]
1505    fn test_strip_function() {
1506        let v = vars(&[
1507            ("s", Value::String("  hello  ".to_string())),
1508            ("t", Value::String("\t\nworld\r\n".to_string())),
1509            ("clean", Value::String("no whitespace".to_string())),
1510        ]);
1511        assert!(eval_bool(r#"strip(s) == "hello""#, &v).unwrap());
1512        assert!(eval_bool(r#"strip(t) == "world""#, &v).unwrap());
1513        assert!(eval_bool(r#"strip(clean) == "no whitespace""#, &v).unwrap());
1514        assert!(eval_bool(r#"strip("  test  ") == "test""#, &v).unwrap());
1515    }
1516}