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