Skip to main content

openjd_expr/eval/
evaluator.rs

1// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2// Copyright by contributors to this project.
3// SPDX-License-Identifier: (Apache-2.0 OR MIT)
4
5//! Expression evaluator with memory-bounded and operation-bounded execution.
6//!
7//! Walks the Python AST produced by `ruff_python_parser` and evaluates it
8//! against a symbol table, mirroring the Python `Evaluator` class in
9//! `openjd.expr._eval._evaluator`.
10
11use ruff_python_ast as ast;
12
13use crate::error::{write_caret_line, ExpressionError, ExpressionErrorKind};
14use crate::path_mapping::PathFormat;
15use crate::symbol_table::SymbolTable;
16use crate::value::{ExprValue, Float64};
17
18/// Append the `"  <expr>\n  <caret-line>\n"` block for a sub-error to
19/// `msg`. No-op if the sub-error has no attached source.
20///
21/// Used by `eval_ifexp` to render both branches of a failing ternary.
22/// `trailing_newline = true` keeps the final newline, `false` strips it
23/// (so the very last sub-error doesn't add a dangling blank line).
24fn append_sub_error(msg: &mut String, err: &ExpressionError, is_last: bool) {
25    let Some(src) = err.expr() else {
26        return;
27    };
28    msg.push_str("  ");
29    msg.push_str(src);
30    msg.push('\n');
31    if let (Some(col), Some(end)) = (err.col_offset(), err.end_col_offset()) {
32        msg.push_str("  ");
33        let caret_off = err.caret_offset().unwrap_or(0);
34        let _ = write_caret_line(msg, col, end, caret_off);
35        if !is_last {
36            msg.push('\n');
37        }
38    }
39}
40
41/// Default memory limit: 100 million bytes.
42pub const DEFAULT_MEMORY_LIMIT: usize = 100_000_000; // 100 million bytes per spec
43
44/// Default operation limit: 10 million.
45pub const DEFAULT_OPERATION_LIMIT: usize = 10_000_000;
46
47/// Result of expression evaluation.
48#[derive(Debug)]
49pub struct EvalResult {
50    pub value: ExprValue,
51    pub peak_memory: usize,
52    pub operation_count: usize,
53}
54
55/// Expression evaluator with resource bounds.
56///
57/// Walks a parsed Python AST and evaluates it against symbol tables, using a
58/// function library for operator and function dispatch. Tracks memory and
59/// operation counts to enforce configurable resource limits.
60///
61/// # Builder Pattern
62///
63/// `Evaluator` is crate-private. The public API exposes equivalent builder
64/// methods on [`ParsedExpression`](super::ParsedExpression) that return a
65/// [`EvalBuilder`](super::EvalBuilder), e.g.
66///
67/// ```
68/// use openjd_expr::{ParsedExpression, SymbolTable, PathFormat, ExprValue};
69///
70/// let parsed = ParsedExpression::new("Param.Frame * 2").unwrap();
71/// let mut symtab = SymbolTable::new();
72/// symtab.set("Param.Frame", 5).unwrap();
73///
74/// let result = parsed
75///     .with_path_format(PathFormat::Posix)
76///     .with_memory_limit(50_000_000)
77///     .with_operation_limit(1_000_000)
78///     .evaluate(&[&symtab])
79///     .unwrap();
80/// assert_eq!(result, ExprValue::Int(10));
81/// ```
82pub struct Evaluator<'a> {
83    symtabs: &'a [&'a SymbolTable],
84    path_format: PathFormat,
85    expr_source: Option<&'a str>,
86    memory_limit: usize,
87    operation_limit: usize,
88    current_memory: usize,
89    peak_memory: usize,
90    operation_count: usize,
91    /// Current recursion depth of the evaluate/evaluate_inner call chain.
92    /// Bounded by [`MAX_EXPRESSION_DEPTH`](super::parse::MAX_EXPRESSION_DEPTH)
93    /// to prevent stack exhaustion on deeply-nested ASTs that slipped past
94    /// the parse-phase depth check (e.g., left-associative binop chains).
95    recursion_depth: usize,
96    keyword_renames: &'a std::collections::HashMap<String, String>,
97    library: &'a crate::function_library::FunctionLibrary,
98    target_type: Option<crate::types::ExprType>,
99    regex_cache: std::collections::HashMap<String, regex::Regex>,
100}
101
102static EMPTY_KEYWORD_RENAMES: std::sync::LazyLock<std::collections::HashMap<String, String>> =
103    std::sync::LazyLock::new(std::collections::HashMap::new);
104
105impl<'a> Evaluator<'a> {
106    /// Create a new evaluator with default settings.
107    ///
108    /// Symbol tables are searched in order during name resolution.
109    /// Use [`super::ParsedExpression::evaluator`] instead when evaluating a parsed
110    /// expression, as it pre-configures keyword renames and source context.
111    pub fn new(symtabs: &'a [&'a SymbolTable]) -> Self {
112        Self {
113            symtabs,
114            path_format: PathFormat::host(),
115            expr_source: None,
116            memory_limit: DEFAULT_MEMORY_LIMIT,
117            operation_limit: DEFAULT_OPERATION_LIMIT,
118            current_memory: 0,
119            peak_memory: 0,
120            operation_count: 0,
121            recursion_depth: 0,
122            keyword_renames: &EMPTY_KEYWORD_RENAMES,
123            // The evaluator stores a borrow, so we need a &'static
124            // FunctionLibrary. The public `for_profile` API returns an
125            // `Arc<FunctionLibrary>`; switching the evaluator to hold
126            // the Arc would ripple through its public API, so here we
127            // reach into the crate-private static that backs the
128            // `ExprProfile::current()` profile anyway.
129            library: &crate::default_library::DEFAULT_LIBRARY,
130            target_type: None,
131            regex_cache: std::collections::HashMap::new(),
132        }
133    }
134
135    /// Set a custom function library for operator and function dispatch.
136    ///
137    /// Default: [`FunctionLibrary::for_profile`](crate::FunctionLibrary::for_profile)
138    /// with the current revision's [`ExprProfile`](crate::ExprProfile),
139    /// which includes all built-in functions. Use a custom library to add
140    /// host-context functions or restrict available operations.
141    #[must_use]
142    pub fn with_library(mut self, library: &'a crate::function_library::FunctionLibrary) -> Self {
143        self.library = library;
144        self
145    }
146
147    /// Set the maximum memory (in bytes) that evaluation may consume.
148    ///
149    /// Default: [`DEFAULT_MEMORY_LIMIT`] (100 MB). Every intermediate value
150    /// is tracked; exceeding this limit returns
151    /// [`MemoryLimitExceeded`](crate::ExpressionErrorKind::MemoryLimitExceeded).
152    #[must_use]
153    pub fn with_memory_limit(mut self, limit: usize) -> Self {
154        self.memory_limit = limit;
155        self
156    }
157
158    /// Set the maximum number of operations that evaluation may perform.
159    ///
160    /// Default: [`DEFAULT_OPERATION_LIMIT`] (10M). Each function call costs 1,
161    /// list iterations cost N, and string processing costs ceil(len/256).
162    /// Exceeding this limit returns
163    /// [`OperationLimitExceeded`](crate::ExpressionErrorKind::OperationLimitExceeded).
164    #[must_use]
165    pub fn with_operation_limit(mut self, limit: usize) -> Self {
166        self.operation_limit = limit;
167        self
168    }
169
170    /// Set the path format for path operations and validation.
171    ///
172    /// Default: host-native format ([`PathFormat::host()`]). Controls how
173    /// path properties (`.name`, `.parent`, etc.), path construction, and
174    /// `apply_path_mapping` behave. Also validates that `Path` values in the
175    /// symbol table match this format.
176    #[must_use]
177    pub fn with_path_format(mut self, format: PathFormat) -> Self {
178        self.path_format = format;
179        self
180    }
181
182    /// Set the target type for context-dependent coercion.
183    ///
184    /// When set, influences how empty list literals infer their element type
185    /// and how mixed-type expressions are coerced. Used internally by the
186    /// model layer when the expected type of a template field is known.
187    #[must_use]
188    pub fn with_target_type(mut self, t: &crate::types::ExprType) -> Self {
189        self.target_type = Some(t.clone());
190        self
191    }
192
193    /// Set the original expression source text for error messages.
194    ///
195    /// When set, errors include the source text with caret positioning.
196    /// Automatically configured by [`super::ParsedExpression::evaluator`].
197    #[must_use]
198    pub fn with_expr_source(mut self, source: &'a str) -> Self {
199        self.expr_source = Some(source);
200        self
201    }
202
203    /// Set keyword rename mappings for Python keyword attribute access.
204    ///
205    /// Maps renamed identifiers back to their original names (e.g.,
206    /// `if_` → `if`) so that `Param.if` works despite `if` being a Python
207    /// keyword. Automatically configured by [`super::ParsedExpression::evaluator`].
208    #[must_use]
209    pub fn with_keyword_renames(
210        mut self,
211        renames: &'a std::collections::HashMap<String, String>,
212    ) -> Self {
213        self.keyword_renames = renames;
214        self
215    }
216
217    pub fn peak_memory(&self) -> usize {
218        self.peak_memory
219    }
220    pub fn operation_count(&self) -> usize {
221        self.operation_count
222    }
223
224    /// Evaluate an AST expression node.
225    pub fn evaluate(&mut self, node: &ast::Expr) -> Result<ExprValue, ExpressionError> {
226        // Bound recursion depth so deep ASTs (e.g., left-associative
227        // binop chains produced from short sources like "1+1+1+...+1")
228        // cannot exhaust the stack. This is the single chokepoint: every
229        // sub-node evaluation goes through `evaluate` (or `evaluate_inner`
230        // via the top-level `evaluate`), so incrementing here covers all
231        // recursive descent paths.
232        //
233        // See `specs/expr/evaluator.md` (Depth limit) for the rationale.
234        self.recursion_depth += 1;
235        if self.recursion_depth > super::parse::MAX_EXPRESSION_DEPTH {
236            self.recursion_depth -= 1;
237            let err = ExpressionError::expression_too_deep(
238                self.recursion_depth + 1,
239                super::parse::MAX_EXPRESSION_DEPTH,
240            );
241            return Err(match self.expr_source {
242                Some(src) => err.with_node(src, node),
243                None => err,
244            });
245        }
246        let result = self.evaluate_inner(node);
247        self.recursion_depth -= 1;
248        // Attach caret context to any error that doesn't already have it
249        match result {
250            Err(e) if e.expr().is_none() => {
251                if let Some(src) = &self.expr_source {
252                    Err(e.with_node(src, node))
253                } else {
254                    Err(e)
255                }
256            }
257            Ok(val) => {
258                if let Some(ref tt) = self.target_type {
259                    val.coerce(tt, self.path_format).map_err(|msg| {
260                        let e = ExpressionError::new(msg);
261                        if let Some(src) = &self.expr_source {
262                            e.with_node(src, node)
263                        } else {
264                            e
265                        }
266                    })
267                } else {
268                    Ok(val)
269                }
270            }
271            other => other,
272        }
273    }
274
275    fn evaluate_inner(&mut self, node: &ast::Expr) -> Result<ExprValue, ExpressionError> {
276        match node {
277            ast::Expr::NumberLiteral(n) => self.eval_number(n),
278            ast::Expr::StringLiteral(s) => self.eval_string(s),
279            ast::Expr::BooleanLiteral(b) => self.track(ExprValue::Bool(b.value)),
280            ast::Expr::NoneLiteral(_) => self.track(ExprValue::Null),
281            ast::Expr::Name(n) => self.eval_name(n),
282            ast::Expr::Attribute(a) => self.eval_attribute(a),
283            ast::Expr::BinOp(b) => self.eval_binop(b),
284            ast::Expr::UnaryOp(u) => self.eval_unaryop(u),
285            ast::Expr::BoolOp(b) => self.eval_boolop(b),
286            ast::Expr::Compare(c) => self.eval_compare(c),
287            ast::Expr::If(i) => self.eval_ifexp(i),
288            ast::Expr::Call(c) => self.eval_call(c),
289            ast::Expr::List(l) => self.eval_list(l),
290            ast::Expr::Subscript(s) => self.eval_subscript(s),
291            ast::Expr::ListComp(lc) => self.eval_listcomp(lc),
292            ast::Expr::Slice(s) => self.eval_slice(s),
293            ast::Expr::Starred(_) => Err(ExpressionError::unsupported(
294                "Star unpacking is not supported",
295            )),
296            ast::Expr::Lambda(_) => Err(ExpressionError::unsupported(
297                "Lambda expressions are not supported",
298            )),
299            ast::Expr::Dict(_) => Err(ExpressionError::unsupported(
300                "Dict literals are not supported",
301            )),
302            ast::Expr::Set(_) => Err(ExpressionError::unsupported(
303                "Set literals are not supported",
304            )),
305            ast::Expr::DictComp(_) => Err(ExpressionError::unsupported(
306                "Dict comprehensions are not supported",
307            )),
308            ast::Expr::SetComp(_) => Err(ExpressionError::unsupported(
309                "Set comprehensions are not supported",
310            )),
311            ast::Expr::Generator(_) => Err(ExpressionError::unsupported(
312                "Generator expressions are not supported",
313            )),
314            ast::Expr::Await(_) => Err(ExpressionError::unsupported(
315                "Await expressions are not supported",
316            )),
317            ast::Expr::FString(_) => Err(ExpressionError::unsupported(
318                "f-strings are not supported; use string concatenation",
319            )),
320            ast::Expr::BytesLiteral(_) => Err(ExpressionError::unsupported(
321                "Byte strings are not supported",
322            )),
323            ast::Expr::EllipsisLiteral(_) => {
324                Err(ExpressionError::unsupported("Ellipsis is not supported"))
325            }
326            ast::Expr::Tuple(_) => Err(ExpressionError::unsupported(
327                "Tuple literals are not supported",
328            )),
329            ast::Expr::Named(_) => Err(ExpressionError::unsupported(
330                "Walrus operator (:=) is not supported",
331            )),
332            _ => Err(ExpressionError::unsupported("Unsupported expression type")),
333        }
334    }
335
336    fn count_op(&mut self) -> Result<(), ExpressionError> {
337        self.operation_count = self.operation_count.saturating_add(1);
338        if self.operation_count > self.operation_limit {
339            Err(ExpressionError::from_kind(
340                ExpressionErrorKind::OperationLimitExceeded {
341                    count: self.operation_count,
342                    limit: self.operation_limit,
343                },
344            ))
345        } else {
346            Ok(())
347        }
348    }
349
350    /// Collect all available symbol names from all symbol tables.
351    fn collect_symbol_names(&self) -> Vec<String> {
352        let mut names = Vec::new();
353        for symtab in self.symtabs {
354            names.extend(symtab.all_paths(""));
355        }
356        names.sort();
357        names.dedup();
358        names
359    }
360
361    fn track(&mut self, value: ExprValue) -> Result<ExprValue, ExpressionError> {
362        // Check for infinity/NaN in float results
363        if let ExprValue::Float(f) = &value {
364            if f.value().is_infinite() {
365                return Err(ExpressionError::float_error(
366                    "Float operation produced infinity",
367                ));
368            }
369            if f.value().is_nan() {
370                return Err(ExpressionError::float_error("Float operation produced NaN"));
371            }
372        }
373        self.current_memory += value.memory_size();
374        if self.current_memory > self.peak_memory {
375            self.peak_memory = self.current_memory;
376        }
377        if self.current_memory > self.memory_limit {
378            Err(ExpressionError::from_kind(
379                ExpressionErrorKind::MemoryLimitExceeded {
380                    used: self.current_memory,
381                    limit: self.memory_limit,
382                },
383            ))
384        } else {
385            Ok(value)
386        }
387    }
388
389    fn release(&mut self, value: &ExprValue) {
390        let size = value.memory_size();
391        self.current_memory = self.current_memory.saturating_sub(size);
392    }
393
394    fn dispatch_with_node(
395        &mut self,
396        name: &str,
397        args: Vec<ExprValue>,
398        node: Option<&ast::Expr>,
399    ) -> Result<ExprValue, ExpressionError> {
400        self.count_op()?;
401        let input_size: usize = args.iter().map(|a| a.memory_size()).sum();
402        let lib = self.library;
403        let result = lib.call(name, &args, self).map_err(|e| {
404            if let (Some(src), Some(n)) = (self.expr_source, node) {
405                e.with_node(src, n)
406            } else {
407                e
408            }
409        })?;
410        self.current_memory = self.current_memory.saturating_sub(input_size);
411        self.track(result)
412    }
413
414    /// Like `dispatch_with_node` but uses a `TextRange` for error positioning,
415    /// avoiding the need to clone an AST node just for error context.
416    fn dispatch_with_span(
417        &mut self,
418        name: &str,
419        args: Vec<ExprValue>,
420        range: ruff_text_size::TextRange,
421    ) -> Result<ExprValue, ExpressionError> {
422        self.count_op()?;
423        let input_size: usize = args.iter().map(|a| a.memory_size()).sum();
424        let lib = self.library;
425        let result = lib.call(name, &args, self).map_err(|e| {
426            if let Some(src) = self.expr_source {
427                e.with_span(src, range.start().to_usize(), range.end().to_usize())
428            } else {
429                e
430            }
431        })?;
432        self.current_memory = self.current_memory.saturating_sub(input_size);
433        self.track(result)
434    }
435
436    fn eval_number(&mut self, n: &ast::ExprNumberLiteral) -> Result<ExprValue, ExpressionError> {
437        match &n.value {
438            ast::Number::Int(i) => {
439                let val: i64 = i.as_i64().ok_or_else(ExpressionError::integer_overflow)?;
440                self.track(ExprValue::Int(val))
441            }
442            ast::Number::Float(f) => {
443                if f.is_infinite() {
444                    return Err(ExpressionError::float_error(
445                        "Float operation produced infinity",
446                    ));
447                }
448                if f.is_nan() {
449                    return Err(ExpressionError::float_error("Float operation produced NaN"));
450                }
451                // Preserve original source text for passthrough (e.g., "1.5e3", "3.500")
452                let original = self.expr_source.as_ref().and_then(|src| {
453                    let start = n.range.start().to_usize();
454                    let end = n.range.end().to_usize();
455                    if end <= src.len() {
456                        let s = &src[start..end];
457                        // Don't preserve malformed forms like "3." or ".5" or underscore-containing
458                        if s.ends_with('.') || s.starts_with('.') || s.contains('_') {
459                            None
460                        } else {
461                            Some(s.to_string())
462                        }
463                    } else {
464                        None
465                    }
466                });
467                self.track(ExprValue::Float(if let Some(s) = original {
468                    Float64::with_str(*f, s)?
469                } else {
470                    Float64::new(*f)?
471                }))
472            }
473            ast::Number::Complex { .. } => Err(ExpressionError::unsupported(
474                "Complex numbers are not supported",
475            )),
476        }
477    }
478
479    fn eval_string(&mut self, s: &ast::ExprStringLiteral) -> Result<ExprValue, ExpressionError> {
480        // Reject u'...' prefix (Python 2 compat, not supported in EXPR)
481        for part in &s.value {
482            if matches!(
483                part.flags.prefix(),
484                ast::str_prefix::StringLiteralPrefix::Unicode
485            ) {
486                return Err(ExpressionError::new(
487                    "Unicode string prefix u'...' is not supported. Use '...' or \"...\" instead.",
488                ));
489            }
490        }
491        self.track(ExprValue::String(s.value.to_string()))
492    }
493
494    fn check_path_format(&self, value: &ExprValue, name: &str) -> Result<(), ExpressionError> {
495        match value {
496            ExprValue::Path { format, .. } if *format != self.path_format => {
497                Err(ExpressionError::new(format!(
498                    "Path format mismatch for '{name}': value has {format:?} but evaluator uses {:?}",
499                    self.path_format
500                )))
501            }
502            ExprValue::ListPath(items, fmt, _) if !items.is_empty() && *fmt != self.path_format => {
503                Err(ExpressionError::new(format!(
504                    "Path format mismatch for '{name}': value has {fmt:?} but evaluator uses {:?}",
505                    self.path_format
506                )))
507            }
508            _ => Ok(()),
509        }
510    }
511
512    fn eval_name(&mut self, n: &ast::ExprName) -> Result<ExprValue, ExpressionError> {
513        let name = n.id.as_str();
514        match name {
515            "True" | "true" => return self.track(ExprValue::Bool(true)),
516            "False" | "false" => return self.track(ExprValue::Bool(false)),
517            "None" | "null" => return self.track(ExprValue::Null),
518            _ => {}
519        }
520        for symtab in self.symtabs.iter().rev() {
521            if let Some(val) = symtab.get_value(name) {
522                self.check_path_format(val, name)?;
523                return self.track(val.clone());
524            }
525        }
526        let available = self.collect_symbol_names();
527        let suggestion = crate::edit_distance::suggest_closest(
528            name,
529            &available.iter().map(|s| s.as_str()).collect::<Vec<_>>(),
530        );
531        Err(ExpressionError::from_kind(
532            ExpressionErrorKind::UndefinedVariable {
533                name: name.to_string(),
534                suggestion,
535            },
536        ))
537    }
538
539    fn eval_attribute(&mut self, a: &ast::ExprAttribute) -> Result<ExprValue, ExpressionError> {
540        // Try full dotted path lookup, resolving keyword renames
541        let dotted_path = build_dotted_name_from_attr(a);
542        if let Some(ref path) = dotted_path {
543            let resolved = resolve_keyword_renames(path, self.keyword_renames);
544            for symtab in self.symtabs.iter().rev() {
545                if let Some(val) = symtab.get_value(&resolved) {
546                    self.count_op()?;
547                    self.check_path_format(val, &resolved)?;
548                    return self.track(val.clone());
549                }
550            }
551        }
552        // Fall back: evaluate the value, then access the attribute via library.
553        // If the base evaluation fails (e.g., "Param" is a subtable not a value),
554        // and we had a dotted path, report the dotted path as undefined with suggestions.
555        let value = match self.evaluate(&a.value) {
556            Ok(v) => v,
557            Err(_) if dotted_path.is_some() => {
558                let path = dotted_path.as_ref().unwrap();
559                let available = self.collect_symbol_names();
560                let suggestion = crate::edit_distance::suggest_closest(
561                    path,
562                    &available.iter().map(|s| s.as_str()).collect::<Vec<_>>(),
563                );
564                let src = self.expr_source.unwrap_or("");
565                let attr_node = ast::Expr::Attribute(a.clone());
566                return Err(
567                    ExpressionError::from_kind(ExpressionErrorKind::UndefinedVariable {
568                        name: path.clone(),
569                        suggestion,
570                    })
571                    .with_node(src, &attr_node),
572                );
573            }
574            Err(e) => return Err(e),
575        };
576        let attr = a.attr.as_str();
577        let prop_name = format!("__property_{attr}__");
578        let attr_node = ast::Expr::Attribute(a.clone());
579        match self.dispatch_with_node(&prop_name, vec![value.clone()], Some(&attr_node)) {
580            Ok(v) => Ok(v),
581            Err(_) => {
582                let src = self.expr_source.unwrap_or("");
583                let val_type = value.expr_type();
584                let val_type_str = if val_type.code() == crate::types::TypeCode::Unresolved
585                    && !val_type.params().is_empty()
586                {
587                    val_type.params()[0].to_string()
588                } else {
589                    val_type.to_string()
590                };
591
592                // Check if attr is a known method (not a property)
593                {
594                    let lib = self.library;
595                    if !lib.get_signatures(attr).is_empty() {
596                        return Err(ExpressionError::new(format!(
597                            "'{attr}' is a method, not a property. Did you mean {attr}()?"
598                        ))
599                        .with_node(src, &attr_node));
600                    }
601                }
602
603                // Show available types for this property
604                {
605                    let lib = self.library;
606                    let valid_types: Vec<String> = lib
607                        .get_signatures(&prop_name)
608                        .iter()
609                        .filter_map(|e| e.signature.sig_params().first())
610                        .filter(|t: &&crate::types::ExprType| !t.is_symbolic())
611                        .map(|t: &crate::types::ExprType| t.to_string())
612                        .collect::<std::collections::BTreeSet<_>>()
613                        .into_iter()
614                        .collect();
615                    if !valid_types.is_empty() {
616                        return Err(ExpressionError::new(format!(
617                            "'{attr}' property is not available for {val_type_str}. Available for: {}",
618                            valid_types.join(", ")
619                        )).with_node(src, &attr_node));
620                    }
621                }
622
623                Err(ExpressionError::new(format!(
624                    "Cannot access attribute '{attr}' on {val_type_str}"
625                ))
626                .with_node(src, &attr_node))
627            }
628        }
629    }
630
631    fn eval_binop(&mut self, b: &ast::ExprBinOp) -> Result<ExprValue, ExpressionError> {
632        // Reject unsupported operators early
633        let op_name = match b.op {
634            ast::Operator::Add => "__add__",
635            ast::Operator::Sub => "__sub__",
636            ast::Operator::Mult => "__mul__",
637            ast::Operator::Div => "__truediv__",
638            ast::Operator::FloorDiv => "__floordiv__",
639            ast::Operator::Mod => "__mod__",
640            ast::Operator::Pow => "__pow__",
641            ast::Operator::BitAnd => {
642                return Err(ExpressionError::unsupported(
643                    "Bitwise AND (&) is not supported",
644                ))
645            }
646            ast::Operator::BitOr => {
647                return Err(ExpressionError::unsupported(
648                    "Bitwise OR (|) is not supported",
649                ))
650            }
651            ast::Operator::BitXor => {
652                return Err(ExpressionError::unsupported(
653                    "Bitwise XOR (^) is not supported",
654                ))
655            }
656            ast::Operator::LShift => {
657                return Err(ExpressionError::unsupported(
658                    "Left shift (<<) is not supported",
659                ))
660            }
661            ast::Operator::RShift => {
662                return Err(ExpressionError::unsupported(
663                    "Right shift (>>) is not supported",
664                ))
665            }
666            ast::Operator::MatMult => {
667                return Err(ExpressionError::unsupported(
668                    "Matrix multiply (@) is not supported",
669                ))
670            }
671        };
672
673        let left = self.evaluate(&b.left)?;
674        let right = self.evaluate(&b.right)?;
675        self.dispatch_with_node(
676            op_name,
677            vec![left, right],
678            Some(&ast::Expr::BinOp(b.clone())),
679        )
680    }
681
682    fn eval_unaryop(&mut self, u: &ast::ExprUnaryOp) -> Result<ExprValue, ExpressionError> {
683        self.count_op()?;
684        if u.op == ast::UnaryOp::Invert {
685            return Err(ExpressionError::unsupported(
686                "Bitwise NOT (~) is not supported",
687            ));
688        }
689        // Fold -<int literal> to handle INT64_MIN which can't be represented
690        // as a positive literal followed by negation (matching Python trick)
691        if matches!(u.op, ast::UnaryOp::USub) {
692            if let ast::Expr::NumberLiteral(n) = &*u.operand {
693                if let ast::Number::Int(i) = &n.value {
694                    // Try positive first, then negate
695                    if let Some(pos) = i.as_i64() {
696                        return self.track(ExprValue::Int(
697                            pos.checked_neg()
698                                .ok_or_else(ExpressionError::integer_overflow)?,
699                        ));
700                    }
701                    // Handle i64::MAX + 1 = 9223372036854775808 → -i64::MIN
702                    if let Some(pos) = i.as_u64() {
703                        if pos == 9223372036854775808u64 {
704                            return self.track(ExprValue::Int(i64::MIN));
705                        }
706                    }
707                    return Err(ExpressionError::integer_overflow());
708                }
709            }
710        }
711        let operand = self.evaluate(&u.operand)?;
712        let op_name = match u.op {
713            ast::UnaryOp::USub => "__neg__",
714            ast::UnaryOp::UAdd => "__pos__",
715            ast::UnaryOp::Not => "__not__",
716            ast::UnaryOp::Invert => unreachable!(),
717        };
718        self.dispatch_with_node(op_name, vec![operand], Some(&ast::Expr::UnaryOp(u.clone())))
719    }
720
721    fn eval_boolop(&mut self, b: &ast::ExprBoolOp) -> Result<ExprValue, ExpressionError> {
722        self.count_op()?;
723        let mut last = ExprValue::Bool(match b.op {
724            ast::BoolOp::And => true,
725            ast::BoolOp::Or => false,
726        });
727        let mut seen_unresolved = false;
728        for node in &b.values {
729            if seen_unresolved {
730                // After an unresolved operand, suppress errors in subsequent operands
731                // (the unresolved value might short-circuit at runtime).
732                // But if a subsequent operand determines the result, return it.
733                match self.evaluate(node) {
734                    Ok(val) => match b.op {
735                        ast::BoolOp::And => {
736                            if matches!(&val, ExprValue::Null | ExprValue::Bool(false)) {
737                                return Ok(val);
738                            }
739                        }
740                        ast::BoolOp::Or => {
741                            if !matches!(&val, ExprValue::Null | ExprValue::Bool(false)) {
742                                return Ok(val);
743                            }
744                        }
745                    },
746                    Err(_) => { /* suppressed — unresolved might short-circuit */ }
747                }
748                continue;
749            }
750            last = self.evaluate(node)?;
751            if last.is_unresolved() {
752                seen_unresolved = true;
753                continue;
754            }
755            match b.op {
756                // EXPR semantics: and/or only short-circuit on null and bool false
757                ast::BoolOp::And => {
758                    if matches!(&last, ExprValue::Null | ExprValue::Bool(false)) {
759                        return Ok(last);
760                    }
761                }
762                ast::BoolOp::Or => {
763                    if !matches!(&last, ExprValue::Null | ExprValue::Bool(false)) {
764                        return Ok(last);
765                    }
766                }
767            }
768        }
769        if seen_unresolved {
770            return self.track(ExprValue::unresolved(ExprType::BOOL));
771        }
772        Ok(last)
773    }
774
775    fn eval_compare(&mut self, c: &ast::ExprCompare) -> Result<ExprValue, ExpressionError> {
776        self.count_op()?;
777        // Reject is/is not
778        for op in &c.ops {
779            match op {
780                ast::CmpOp::Is => {
781                    return Err(ExpressionError::unsupported(
782                        "'is' operator is not supported; use '=='",
783                    ))
784                }
785                ast::CmpOp::IsNot => {
786                    return Err(ExpressionError::unsupported(
787                        "'is not' operator is not supported; use '!='",
788                    ))
789                }
790                _ => {}
791            }
792        }
793        let mut left = self.evaluate(&c.left)?;
794        for (op, right_node) in c.ops.iter().zip(c.comparators.iter()) {
795            let right = self.evaluate(right_node)?;
796            if left.is_unresolved() || right.is_unresolved() {
797                self.release(&left);
798                self.release(&right);
799                return self.track(ExprValue::unresolved(ExprType::BOOL));
800            }
801            let (op_name, args) = match op {
802                ast::CmpOp::Eq => ("__eq__", vec![left.clone(), right.clone()]),
803                ast::CmpOp::NotEq => ("__ne__", vec![left.clone(), right.clone()]),
804                ast::CmpOp::Lt => ("__lt__", vec![left.clone(), right.clone()]),
805                ast::CmpOp::LtE => ("__le__", vec![left.clone(), right.clone()]),
806                ast::CmpOp::Gt => ("__gt__", vec![left.clone(), right.clone()]),
807                ast::CmpOp::GtE => ("__ge__", vec![left.clone(), right.clone()]),
808                // For 'in'/'not in', container is first arg (right), item is second (left)
809                ast::CmpOp::In => ("__contains__", vec![right.clone(), left.clone()]),
810                ast::CmpOp::NotIn => ("__not_contains__", vec![right.clone(), left.clone()]),
811                ast::CmpOp::Is => {
812                    return Err(ExpressionError::unsupported(
813                        "'is' operator is not supported; use '=='",
814                    ))
815                }
816                ast::CmpOp::IsNot => {
817                    return Err(ExpressionError::unsupported(
818                        "'is not' operator is not supported; use '!='",
819                    ))
820                }
821            };
822
823            // Use the compare expression's range for error caret positioning
824            // without cloning the entire ExprCompare node.
825            use ruff_text_size::Ranged;
826            let result_val = self.dispatch_with_span(op_name, args, c.range())?;
827            let result = match &result_val {
828                ExprValue::Bool(b) => *b,
829                _ => true,
830            };
831            self.release(&result_val);
832
833            if !result {
834                self.release(&left);
835                self.release(&right);
836                return self.track(ExprValue::Bool(false));
837            }
838            self.release(&left);
839            left = right;
840        }
841        self.release(&left);
842        self.track(ExprValue::Bool(true))
843    }
844
845    fn eval_ifexp(&mut self, i: &ast::ExprIf) -> Result<ExprValue, ExpressionError> {
846        self.count_op()?;
847        let test = self.evaluate(&i.test)?;
848        if test.is_unresolved() {
849            // Check that the unresolved type is compatible with bool
850            let inner = unwrap_unresolved(&test.expr_type());
851            let is_bool_compatible = inner == ExprType::BOOL
852                || inner.code() == crate::types::TypeCode::Unresolved
853                || inner.code() == crate::types::TypeCode::Any
854                || (inner.code() == crate::types::TypeCode::Union
855                    && inner.params().contains(&ExprType::BOOL));
856            if !is_bool_compatible {
857                let err =
858                    ExpressionError::new(format!("Condition must be a boolean, got {}", inner));
859                self.release(&test);
860                return Err(if let Some(src) = self.expr_source {
861                    err.with_node(src, &i.test)
862                } else {
863                    err
864                });
865            }
866            self.release(&test);
867            // Try both branches, catching errors (e.g. fail() in one branch)
868            let body = self.evaluate(&i.body);
869            let orelse = self.evaluate(&i.orelse);
870            match (body, orelse) {
871                (Err(be), Err(oe)) => {
872                    let mut msg = format!(
873                        "Both branches fail in the if/else:\n  if-branch: {}\n",
874                        be.message()
875                    );
876                    append_sub_error(&mut msg, &be, false);
877                    msg.push_str(&format!("  else-branch: {}\n", oe.message()));
878                    append_sub_error(&mut msg, &oe, true);
879                    let mut err = ExpressionError::new(msg).with_sub_errors(vec![be, oe]);
880                    if let Some(src) = self.expr_source {
881                        use ruff_text_size::Ranged;
882                        let start = i.range().start().to_usize();
883                        let end = i.range().end().to_usize();
884                        err.set_source_span(src, start, end, 0);
885                    }
886                    Err(err)
887                }
888                (Ok(b), Err(_)) => {
889                    let t = unwrap_unresolved(&b.expr_type());
890                    self.track(ExprValue::unresolved(t))
891                }
892                (Err(_), Ok(o)) => {
893                    let t = unwrap_unresolved(&o.expr_type());
894                    self.track(ExprValue::unresolved(t))
895                }
896                (Ok(b), Ok(o)) => {
897                    let bt = unwrap_unresolved(&b.expr_type());
898                    let ot = unwrap_unresolved(&o.expr_type());
899                    if bt == ot {
900                        self.track(ExprValue::unresolved(bt))
901                    } else {
902                        self.track(ExprValue::unresolved(ExprType::union(vec![bt, ot])))
903                    }
904                }
905            }
906        } else {
907            // Condition must be bool
908            if !matches!(&test, ExprValue::Bool(_)) {
909                let err = ExpressionError::new(format!(
910                    "Condition must be a boolean, got {}",
911                    test.expr_type()
912                ));
913                return Err(if let Some(src) = self.expr_source {
914                    err.with_node(src, &i.test)
915                } else {
916                    err
917                });
918            }
919            // Condition is already validated as Bool above; match directly.
920            if matches!(test, ExprValue::Bool(true)) {
921                self.release(&test);
922                self.evaluate(&i.body)
923            } else {
924                self.release(&test);
925                self.evaluate(&i.orelse)
926            }
927        }
928    }
929
930    fn eval_call(&mut self, c: &ast::ExprCall) -> Result<ExprValue, ExpressionError> {
931        self.count_op()?;
932        // Reject keyword args and **kwargs
933        if !c.arguments.keywords.is_empty() {
934            return Err(ExpressionError::unsupported(
935                "Keyword arguments are not supported",
936            ));
937        }
938        // Get function name and check receiver
939        let mut receiver_value: Option<ExprValue> = None;
940        let is_method_call;
941        let func_name = match &*c.func {
942            ast::Expr::Name(n) => {
943                is_method_call = false;
944                Some(n.id.to_string())
945            }
946            ast::Expr::Attribute(a) => {
947                let receiver = self.evaluate(&a.value)?;
948                receiver_value = Some(receiver);
949                is_method_call = true;
950                Some(a.attr.to_string())
951            }
952            _ => {
953                is_method_call = false;
954                None
955            }
956        };
957        // Evaluate arguments
958        let mut args = Vec::new();
959        for arg in &c.arguments.args {
960            args.push(self.evaluate(arg)?);
961        }
962
963        // Normalize call convention: receiver (if any) becomes args[0].
964        // After this, every function sees a uniform args list regardless of
965        // whether it was called as obj.method(x) or method(obj, x).
966        if let Some(recv) = receiver_value {
967            args.insert(0, recv);
968        }
969
970        let _any_unresolved = args.iter().any(|a| a.is_unresolved());
971        // Release args from memory tracking — they're consumed by the function.
972        // Done before dispatch so early returns don't leak.
973        let args_size: usize = args.iter().map(|a| a.memory_size()).sum();
974        self.current_memory = self.current_memory.saturating_sub(args_size);
975
976        // Dispatch through function library
977        if let Some(name) = &func_name {
978            if name.starts_with("__") && name.ends_with("__") {
979                return Err(ExpressionError::new(format!(
980                    "Cannot call '{}' directly",
981                    name
982                )));
983            }
984            {
985                let lib = self.library;
986                let result = if is_method_call {
987                    lib.call_method(name, &args, self)
988                } else {
989                    lib.call(name, &args, self)
990                };
991                let result = result.map_err(|e| {
992                    let src = self.expr_source.unwrap_or("");
993                    let call_node = ast::Expr::Call(c.clone());
994                    if is_method_call
995                        && !lib
996                            .get_signatures(&format!("__property_{name}__"))
997                            .is_empty()
998                    {
999                        return ExpressionError::new(format!(
1000                            "'{name}' is a property, not a method. Use .{name} instead of .{name}()"
1001                        ))
1002                        .with_node(src, &call_node);
1003                    }
1004                    e.with_node(src, &call_node)
1005                })?;
1006                self.track(result)
1007            }
1008        } else {
1009            Err(ExpressionError::new("Cannot call non-function expression"))
1010        }
1011    }
1012
1013    fn eval_list(&mut self, l: &ast::ExprList) -> Result<ExprValue, ExpressionError> {
1014        // Extract element target type from list[T] target
1015        let list_elem_target = self.target_type.as_ref().and_then(|tt| {
1016            if tt.code() == crate::types::TypeCode::List && tt.params().len() == 1 {
1017                Some(tt.params()[0].clone())
1018            } else {
1019                None
1020            }
1021        });
1022
1023        // Thread element target type down for nested list evaluation
1024        let saved_target = self.target_type.take();
1025        if let Some(ref elem_t) = list_elem_target {
1026            self.target_type = Some(elem_t.clone());
1027        }
1028
1029        let mut elements = Vec::new();
1030        for elt in &l.elts {
1031            let val = self.evaluate(elt)?;
1032            if matches!(&val, ExprValue::Null) {
1033                self.target_type = saved_target;
1034                return Err(ExpressionError::new("null is not allowed in list literals"));
1035            }
1036            elements.push(val);
1037        }
1038
1039        // Restore target type
1040        self.target_type = saved_target;
1041
1042        // Check nesting depth — max 2 levels (list[list[T]] ok, list[list[list[T]]] not)
1043        for e in &elements {
1044            let t = e.expr_type();
1045            if let Some(inner) = t.list_element_type() {
1046                if inner.list_element_type().is_some() {
1047                    return Err(ExpressionError::new(
1048                        "Lists may be nested at most 2 levels deep",
1049                    ));
1050                }
1051            }
1052        }
1053        if elements.iter().any(|e| e.is_unresolved()) {
1054            // Check type compatibility even with unresolved elements
1055            if !elements.is_empty() {
1056                let first_type = unwrap_unresolved(&elements[0].expr_type());
1057                for (_i, e) in elements.iter().enumerate().skip(1) {
1058                    let t = unwrap_unresolved(&e.expr_type());
1059                    // Allow int/float and path/string mixing
1060                    if (first_type == ExprType::INT && t == ExprType::FLOAT)
1061                        || (first_type == ExprType::FLOAT && t == ExprType::INT)
1062                        || (first_type == ExprType::PATH && t == ExprType::STRING)
1063                        || (first_type == ExprType::STRING && t == ExprType::PATH)
1064                    {
1065                        continue;
1066                    }
1067                    if t.code() == crate::types::TypeCode::Unresolved
1068                        || first_type.code() == crate::types::TypeCode::Unresolved
1069                    {
1070                        continue;
1071                    }
1072                    if t != first_type {
1073                        return Err(ExpressionError::new(format!(
1074                            "List literal contains incompatible types: {first_type}, {t}"
1075                        )));
1076                    }
1077                }
1078            }
1079            let elem_type = if elements.is_empty() {
1080                ExprType::NULLTYPE
1081            } else {
1082                // Compute coerced element type: int+float→float, path+string→string
1083                let mut result = unwrap_unresolved(&elements[0].expr_type());
1084                for e in elements.iter().skip(1) {
1085                    let t = unwrap_unresolved(&e.expr_type());
1086                    if t.code() == crate::types::TypeCode::Unresolved {
1087                        continue;
1088                    }
1089                    if result.code() == crate::types::TypeCode::Unresolved {
1090                        result = t;
1091                        continue;
1092                    }
1093                    if (result == ExprType::INT && t == ExprType::FLOAT)
1094                        || (result == ExprType::FLOAT && t == ExprType::INT)
1095                    {
1096                        result = ExprType::FLOAT;
1097                    } else if (result == ExprType::PATH && t == ExprType::STRING)
1098                        || (result == ExprType::STRING && t == ExprType::PATH)
1099                    {
1100                        result = ExprType::STRING;
1101                    }
1102                }
1103                result
1104            };
1105            return self.track(ExprValue::unresolved(ExprType::list(elem_type)));
1106        }
1107        // If we have a list element target, coerce each element and skip homogeneity check
1108        if let Some(ref elem_t) = list_elem_target {
1109            let coerced: Result<Vec<ExprValue>, _> = elements
1110                .into_iter()
1111                .map(|e| {
1112                    e.coerce(elem_t, self.path_format)
1113                        .map_err(ExpressionError::new)
1114                })
1115                .collect();
1116            let list = ExprValue::make_list_checked(self, coerced?, elem_t.clone())?;
1117            return self.track(list);
1118        }
1119        // Check type consistency
1120        if !elements.is_empty() {
1121            let mut seen_types: Vec<ExprType> = Vec::new();
1122            for e in elements.iter() {
1123                let t = e.expr_type();
1124                if !seen_types.contains(&t) {
1125                    seen_types.push(t);
1126                }
1127            }
1128            // Check compatibility: allow int/float mixing and path/string mixing
1129            let dominated: Vec<&ExprType> = seen_types
1130                .iter()
1131                .filter(|t| {
1132                    // nulltype is compatible with anything
1133                    t.code() == crate::types::TypeCode::NullType ||
1134                // int is compatible if float is also present (promotion)
1135                (**t == ExprType::INT && seen_types.contains(&ExprType::FLOAT)) ||
1136                // float is compatible if int is also present (promotion)
1137                (**t == ExprType::FLOAT && seen_types.contains(&ExprType::INT)) ||
1138                // path is compatible if string is also present
1139                (**t == ExprType::PATH && seen_types.contains(&ExprType::STRING)) ||
1140                // string is compatible if path is also present
1141                (**t == ExprType::STRING && seen_types.contains(&ExprType::PATH))
1142                })
1143                .collect();
1144            // If all types are in compatible pairs, it's fine; otherwise error
1145            let compatible = dominated.len() == seen_types.len() ||
1146                seen_types.len() == 1 ||
1147                // All list types are compatible (make_list handles inner promotion)
1148                seen_types.iter().all(|t| t.code() == crate::types::TypeCode::List || t.code() == crate::types::TypeCode::NullType);
1149            if !compatible {
1150                let type_strs: Vec<String> = seen_types.iter().map(|t| t.to_string()).collect();
1151                let msg = if type_strs.len() == 2 {
1152                    format!(
1153                        "List literal contains incompatible types: {} and {}",
1154                        type_strs[0], type_strs[1]
1155                    )
1156                } else {
1157                    let last = type_strs.last().unwrap();
1158                    let rest = type_strs[..type_strs.len() - 1].join(", ");
1159                    format!("List literal contains incompatible types: {rest}, and {last}")
1160                };
1161                return Err(ExpressionError::new(msg));
1162            }
1163        }
1164        let elem_type = if elements.is_empty() {
1165            ExprType::NULLTYPE
1166        } else {
1167            elements[0].expr_type()
1168        };
1169        let list = ExprValue::make_list_checked(self, elements, elem_type)?;
1170        self.track(list)
1171    }
1172
1173    fn eval_subscript(&mut self, s: &ast::ExprSubscript) -> Result<ExprValue, ExpressionError> {
1174        self.count_op()?;
1175        let value = self.evaluate(&s.value)?;
1176
1177        // Reject subscript on path type
1178        if matches!(&value, ExprValue::Path { .. }) {
1179            return Err(ExpressionError::new(
1180                "Cannot subscript type path".to_string(),
1181            ));
1182        }
1183
1184        // Handle slice syntax: value[start:stop:step]
1185        if let ast::Expr::Slice(sl) = &*s.slice {
1186            let start = match sl.lower.as_ref().map(|e| self.evaluate(e)).transpose()? {
1187                Some(v) => v,
1188                None => ExprValue::Null,
1189            };
1190            let stop = match sl.upper.as_ref().map(|e| self.evaluate(e)).transpose()? {
1191                Some(v) => v,
1192                None => ExprValue::Null,
1193            };
1194            let step = match sl.step.as_ref().map(|e| self.evaluate(e)).transpose()? {
1195                Some(v) => v,
1196                None => ExprValue::Null,
1197            };
1198
1199            if let ExprValue::Int(0) = &step {
1200                return Err(ExpressionError::new("Slice step cannot be zero"));
1201            }
1202
1203            if value.is_unresolved() {
1204                let inner = unwrap_unresolved(&value.expr_type());
1205                if let Some(elem) = inner.list_element_type() {
1206                    return self.track(ExprValue::unresolved(ExprType::list(elem.clone())));
1207                }
1208                return self.track(ExprValue::unresolved(inner));
1209            }
1210
1211            // If any slice bound is unresolved, propagate unresolved
1212            let any_bound_unresolved =
1213                start.is_unresolved() || stop.is_unresolved() || step.is_unresolved();
1214            if any_bound_unresolved {
1215                if value.is_list() {
1216                    let elem_type = value.list_elem_type().unwrap();
1217                    return self.track(ExprValue::unresolved(ExprType::list(elem_type.clone())));
1218                } else if matches!(&value, ExprValue::String(_)) {
1219                    return self.track(ExprValue::unresolved(ExprType::STRING));
1220                }
1221            }
1222
1223            // Dispatch 4-arg __getitem__ through the library
1224            let node = ast::Expr::Subscript(s.clone());
1225            return self.dispatch_with_node(
1226                "__getitem__",
1227                vec![value, start, stop, step],
1228                Some(&node),
1229            );
1230        }
1231
1232        let slice = self.evaluate(&s.slice)?;
1233
1234        // Check index type for unresolved values
1235        if slice.is_unresolved() {
1236            let inner = unwrap_unresolved(&slice.expr_type());
1237            if inner != ExprType::INT
1238                && inner.code() != crate::types::TypeCode::Unresolved
1239                && inner.code() != crate::types::TypeCode::Any
1240            {
1241                let mut err = ExpressionError::new("Index must be an integer");
1242                if let Some(src) = self.expr_source {
1243                    use ruff_text_size::Ranged;
1244                    let start = s.value.range().start().to_usize();
1245                    let end = s.range().end().to_usize();
1246                    let left_end = s.value.range().end().to_usize();
1247                    err.set_source_span(src, start, end, left_end - start);
1248                }
1249                return Err(err);
1250            }
1251        }
1252
1253        // Single-index access: dispatch through library
1254        self.dispatch_with_node(
1255            "__getitem__",
1256            vec![value, slice],
1257            Some(&ast::Expr::Subscript(s.clone())),
1258        )
1259    }
1260
1261    /// Create a child evaluator with an extra symbol table for local scope.
1262    /// The child shares resource counters with the parent; callers must
1263    /// propagate counters back after the child returns.
1264    fn child_evaluator<'b>(&self, symtabs: &'b [&'b SymbolTable]) -> Evaluator<'b>
1265    where
1266        'a: 'b,
1267    {
1268        Evaluator {
1269            symtabs,
1270            path_format: self.path_format,
1271            expr_source: self.expr_source,
1272            memory_limit: self.memory_limit,
1273            operation_limit: self.operation_limit,
1274            current_memory: self.current_memory,
1275            peak_memory: self.peak_memory,
1276            operation_count: self.operation_count,
1277            recursion_depth: self.recursion_depth,
1278            keyword_renames: self.keyword_renames,
1279            library: self.library,
1280            target_type: None,
1281            regex_cache: std::collections::HashMap::new(),
1282        }
1283    }
1284
1285    /// Propagate resource counters back from a child evaluator.
1286    fn absorb_counters(&mut self, child: &Evaluator) {
1287        self.current_memory = child.current_memory;
1288        self.peak_memory = child.peak_memory;
1289        self.operation_count = child.operation_count;
1290    }
1291
1292    fn eval_listcomp(&mut self, lc: &ast::ExprListComp) -> Result<ExprValue, ExpressionError> {
1293        // Validate restrictions
1294        if lc.generators.len() != 1 {
1295            return Err(ExpressionError::unsupported(
1296                "Multiple 'for' clauses in list comprehensions are not supported",
1297            ));
1298        }
1299        let gen = &lc.generators[0];
1300        if gen.ifs.len() > 1 {
1301            return Err(ExpressionError::unsupported(
1302                "Multiple 'if' clauses in a list comprehension are not supported; combine with 'and'",
1303            ));
1304        }
1305        if !matches!(&gen.target, ast::Expr::Name(_)) {
1306            return Err(ExpressionError::unsupported(
1307                "Tuple unpacking in list comprehension is not supported",
1308            ));
1309        }
1310        if let ast::Expr::Name(n) = &gen.target {
1311            let var_name = n.id.as_str();
1312            if !var_name.is_empty() && !var_name.starts_with(|c: char| c.is_lowercase() || c == '_')
1313            {
1314                return Err(ExpressionError::new(format!(
1315                    "Loop variable '{var_name}' must start with a lowercase letter or underscore"
1316                )));
1317            }
1318        }
1319
1320        let iterable = self.evaluate(&gen.iter)?;
1321        let var_name = match &gen.target {
1322            ast::Expr::Name(n) => n.id.to_string(),
1323            _ => unreachable!(),
1324        };
1325
1326        // Unresolved iterable: evaluate body once to determine output type
1327        if iterable.is_unresolved() {
1328            let inner = unwrap_unresolved(&iterable.expr_type());
1329            let elem_type = inner.list_element_type().cloned().unwrap_or(ExprType::INT);
1330            let mut tmp = crate::symbol_table::SymbolTable::new();
1331            tmp.set(&var_name, ExprValue::unresolved(elem_type))
1332                .map_err(|e| ExpressionError::new(e.to_string()))?;
1333            let mut combined: Vec<&SymbolTable> = self.symtabs.to_vec();
1334            combined.push(&tmp);
1335            let mut child = self.child_evaluator(&combined);
1336            // Check filter clause type if present
1337            if let Some(if_clause) = gen.ifs.first() {
1338                let cond = child.evaluate(if_clause)?;
1339                let cond_inner = unwrap_unresolved(&cond.expr_type());
1340                let is_bool_compatible = cond_inner == ExprType::BOOL
1341                    || cond_inner.code() == crate::types::TypeCode::Unresolved
1342                    || cond_inner.code() == crate::types::TypeCode::Any
1343                    || (cond_inner.code() == crate::types::TypeCode::Union
1344                        && cond_inner.params().contains(&ExprType::BOOL));
1345                if !is_bool_compatible {
1346                    let err = ExpressionError::new(format!(
1347                        "List comprehension filter must be a boolean, got {}",
1348                        cond_inner
1349                    ));
1350                    return Err(if let Some(src) = self.expr_source {
1351                        err.with_node(src, if_clause)
1352                    } else {
1353                        err
1354                    });
1355                }
1356            }
1357            let body_val = child.evaluate(&lc.elt)?;
1358            self.absorb_counters(&child);
1359            let body_type = unwrap_unresolved(&body_val.expr_type());
1360            return self.track(ExprValue::unresolved(ExprType::list(body_type)));
1361        }
1362
1363        // Materialize iterable elements
1364        let items: Vec<ExprValue> = if let Some(iter) = iterable.list_iter() {
1365            iter.collect()
1366        } else if let ExprValue::RangeExpr(r) = &iterable {
1367            r.iter().map(ExprValue::Int).collect()
1368        } else {
1369            return Err(ExpressionError::type_error(format!(
1370                "Cannot iterate over {}",
1371                iterable.expr_type()
1372            )));
1373        };
1374        self.release(&iterable);
1375
1376        // Evaluate each element with a child scope
1377        let mut result = Vec::new();
1378        let base_symtabs: Vec<&SymbolTable> = self.symtabs.to_vec();
1379        for item in &items {
1380            self.count_op()?;
1381            let mut tmp = crate::symbol_table::SymbolTable::new();
1382            tmp.set(&var_name, item.clone())
1383                .map_err(|e| ExpressionError::new(e.to_string()))?;
1384            let mut combined = base_symtabs.clone();
1385            combined.push(&tmp);
1386            let mut child = self.child_evaluator(&combined);
1387            child.regex_cache = std::mem::take(&mut self.regex_cache);
1388            let mut include = true;
1389            if let Some(if_clause) = gen.ifs.first() {
1390                let cond = child.evaluate(if_clause)?;
1391                if let ExprValue::Bool(b) = cond {
1392                    include = b;
1393                } else {
1394                    let err = ExpressionError::new(format!(
1395                        "List comprehension filter must be a boolean, got {}",
1396                        cond.expr_type()
1397                    ));
1398                    return Err(if let Some(src) = self.expr_source {
1399                        err.with_node(src, if_clause)
1400                    } else {
1401                        err
1402                    });
1403                }
1404            }
1405            if include {
1406                result.push(child.evaluate(&lc.elt)?);
1407            }
1408            self.absorb_counters(&child);
1409            self.regex_cache = child.regex_cache;
1410            self.current_memory = self.current_memory.saturating_sub(item.memory_size());
1411        }
1412
1413        // Check nesting depth
1414        for e in &result {
1415            let t = e.expr_type();
1416            if let Some(inner) = t.list_element_type() {
1417                if inner.list_element_type().is_some() {
1418                    return Err(ExpressionError::new(
1419                        "Lists may be nested at most 2 levels deep",
1420                    ));
1421                }
1422            }
1423        }
1424        let elem_type = if result.is_empty() {
1425            ExprType::NULLTYPE
1426        } else {
1427            result[0].expr_type()
1428        };
1429        let list = ExprValue::make_list_checked(self, result, elem_type)?;
1430        self.track(list)
1431    }
1432
1433    fn eval_slice(&mut self, s: &ast::ExprSlice) -> Result<ExprValue, ExpressionError> {
1434        if let Some(step) = &s.step {
1435            let step_val = self.evaluate(step)?;
1436            if let ExprValue::Int(0) = step_val {
1437                return Err(ExpressionError::new("Slice step cannot be zero"));
1438            }
1439        }
1440        self.track(ExprValue::unresolved(ExprType::INT))
1441    }
1442}
1443
1444/// Try to build a dotted name from an attribute chain.
1445/// Applies keyword renames to restore original names.
1446impl<'a> crate::function_library::EvalContext for Evaluator<'a> {
1447    fn path_format(&self) -> PathFormat {
1448        self.path_format
1449    }
1450    fn count_op(&mut self) -> Result<(), ExpressionError> {
1451        self.operation_count = self.operation_count.saturating_add(1);
1452        if self.operation_count > self.operation_limit {
1453            Err(ExpressionError::from_kind(
1454                ExpressionErrorKind::OperationLimitExceeded {
1455                    count: self.operation_count,
1456                    limit: self.operation_limit,
1457                },
1458            ))
1459        } else {
1460            Ok(())
1461        }
1462    }
1463    fn count_ops(&mut self, n: usize) -> Result<(), ExpressionError> {
1464        self.operation_count = self.operation_count.saturating_add(n);
1465        if self.operation_count > self.operation_limit {
1466            Err(ExpressionError::from_kind(
1467                ExpressionErrorKind::OperationLimitExceeded {
1468                    count: self.operation_count,
1469                    limit: self.operation_limit,
1470                },
1471            ))
1472        } else {
1473            Ok(())
1474        }
1475    }
1476    fn count_string_ops(&mut self, len: usize) -> Result<(), ExpressionError> {
1477        let ops = len.div_ceil(256);
1478        self.operation_count = self.operation_count.saturating_add(ops);
1479        if self.operation_count > self.operation_limit {
1480            Err(ExpressionError::from_kind(
1481                ExpressionErrorKind::OperationLimitExceeded {
1482                    count: self.operation_count,
1483                    limit: self.operation_limit,
1484                },
1485            ))
1486        } else {
1487            Ok(())
1488        }
1489    }
1490    fn check_memory(&self, bytes: usize) -> Result<(), ExpressionError> {
1491        let projected = self.current_memory.saturating_add(bytes);
1492        if projected > self.memory_limit {
1493            Err(ExpressionError::from_kind(
1494                ExpressionErrorKind::MemoryLimitExceeded {
1495                    used: projected,
1496                    limit: self.memory_limit,
1497                },
1498            ))
1499        } else {
1500            Ok(())
1501        }
1502    }
1503    fn get_or_compile_regex(&mut self, pattern: &str) -> Result<regex::Regex, ExpressionError> {
1504        if let Some(re) = self.regex_cache.get(pattern) {
1505            return Ok(re.clone());
1506        }
1507        let re = regex::RegexBuilder::new(pattern)
1508            .size_limit(1 << 20)
1509            .build()
1510            .map_err(|e| ExpressionError::new(format!("Invalid regex: {e}")))?;
1511        self.regex_cache.insert(pattern.to_string(), re.clone());
1512        Ok(re)
1513    }
1514}
1515
1516/// Unwrap unresolved[T] to T, or return the type as-is if not unresolved.
1517fn unwrap_unresolved(t: &ExprType) -> ExprType {
1518    if t.code() == crate::types::TypeCode::Unresolved && !t.params().is_empty() {
1519        t.params()[0].clone()
1520    } else {
1521        t.clone()
1522    }
1523}
1524
1525/// Walks a chain of `ExprAttribute` nodes by reference and returns the dotted
1526/// name (e.g. `Param.Foo.Bar`), or `None` if the chain does not terminate at a
1527/// plain `Name`. Avoids cloning the AST.
1528fn build_dotted_name_from_attr(a: &ast::ExprAttribute) -> Option<String> {
1529    let mut parts: Vec<&str> = vec![a.attr.as_str()];
1530    let mut current: &ast::Expr = &a.value;
1531    loop {
1532        match current {
1533            ast::Expr::Name(n) => {
1534                parts.push(n.id.as_str());
1535                break;
1536            }
1537            ast::Expr::Attribute(attr) => {
1538                parts.push(attr.attr.as_str());
1539                current = &attr.value;
1540            }
1541            _ => return None,
1542        }
1543    }
1544    parts.reverse();
1545    Some(parts.join("."))
1546}
1547
1548fn resolve_keyword_renames(
1549    name: &str,
1550    renames: &std::collections::HashMap<String, String>,
1551) -> String {
1552    if renames.is_empty() {
1553        return name.to_string();
1554    }
1555    let mut result = name.to_string();
1556    for (replacement, original) in renames {
1557        // Replace .replacement with .original in dotted paths
1558        let from = format!(".{replacement}");
1559        let to = format!(".{original}");
1560        result = result.replace(&from, &to);
1561    }
1562    result
1563}
1564
1565use crate::types::ExprType;