use ruff_python_ast as ast;
use crate::error::{write_caret_line, ExpressionError, ExpressionErrorKind};
use crate::path_mapping::PathFormat;
use crate::symbol_table::SymbolTable;
use crate::value::{ExprValue, Float64};
fn append_sub_error(msg: &mut String, err: &ExpressionError, is_last: bool) {
let Some(src) = err.expr() else {
return;
};
msg.push_str(" ");
msg.push_str(src);
msg.push('\n');
if let (Some(col), Some(end)) = (err.col_offset(), err.end_col_offset()) {
msg.push_str(" ");
let caret_off = err.caret_offset().unwrap_or(0);
let _ = write_caret_line(msg, col, end, caret_off);
if !is_last {
msg.push('\n');
}
}
}
pub const DEFAULT_MEMORY_LIMIT: usize = 100_000_000;
pub const DEFAULT_OPERATION_LIMIT: usize = 10_000_000;
#[derive(Debug)]
pub struct EvalResult {
pub value: ExprValue,
pub peak_memory: usize,
pub operation_count: usize,
}
pub struct Evaluator<'a> {
symtabs: &'a [&'a SymbolTable],
path_format: PathFormat,
expr_source: Option<&'a str>,
memory_limit: usize,
operation_limit: usize,
current_memory: usize,
peak_memory: usize,
operation_count: usize,
recursion_depth: usize,
keyword_renames: &'a std::collections::HashMap<String, String>,
library: &'a crate::function_library::FunctionLibrary,
target_type: Option<crate::types::ExprType>,
regex_cache: std::collections::HashMap<String, regex::Regex>,
}
static EMPTY_KEYWORD_RENAMES: std::sync::LazyLock<std::collections::HashMap<String, String>> =
std::sync::LazyLock::new(std::collections::HashMap::new);
impl<'a> Evaluator<'a> {
pub fn new(symtabs: &'a [&'a SymbolTable]) -> Self {
Self {
symtabs,
path_format: PathFormat::host(),
expr_source: None,
memory_limit: DEFAULT_MEMORY_LIMIT,
operation_limit: DEFAULT_OPERATION_LIMIT,
current_memory: 0,
peak_memory: 0,
operation_count: 0,
recursion_depth: 0,
keyword_renames: &EMPTY_KEYWORD_RENAMES,
library: &crate::default_library::DEFAULT_LIBRARY,
target_type: None,
regex_cache: std::collections::HashMap::new(),
}
}
#[must_use]
pub fn with_library(mut self, library: &'a crate::function_library::FunctionLibrary) -> Self {
self.library = library;
self
}
#[must_use]
pub fn with_memory_limit(mut self, limit: usize) -> Self {
self.memory_limit = limit;
self
}
#[must_use]
pub fn with_operation_limit(mut self, limit: usize) -> Self {
self.operation_limit = limit;
self
}
#[must_use]
pub fn with_path_format(mut self, format: PathFormat) -> Self {
self.path_format = format;
self
}
#[must_use]
pub fn with_target_type(mut self, t: &crate::types::ExprType) -> Self {
self.target_type = Some(t.clone());
self
}
#[must_use]
pub fn with_expr_source(mut self, source: &'a str) -> Self {
self.expr_source = Some(source);
self
}
#[must_use]
pub fn with_keyword_renames(
mut self,
renames: &'a std::collections::HashMap<String, String>,
) -> Self {
self.keyword_renames = renames;
self
}
pub fn peak_memory(&self) -> usize {
self.peak_memory
}
pub fn operation_count(&self) -> usize {
self.operation_count
}
fn evaluate_with_target(
&mut self,
node: &ast::Expr,
target: Option<crate::types::ExprType>,
) -> Result<ExprValue, ExpressionError> {
let saved = std::mem::replace(&mut self.target_type, target);
let result = self.evaluate(node);
self.target_type = saved;
result
}
pub fn evaluate(&mut self, node: &ast::Expr) -> Result<ExprValue, ExpressionError> {
self.recursion_depth += 1;
if self.recursion_depth > super::parse::MAX_EXPRESSION_DEPTH {
self.recursion_depth -= 1;
let err = ExpressionError::expression_too_deep(
self.recursion_depth + 1,
super::parse::MAX_EXPRESSION_DEPTH,
);
return Err(match self.expr_source {
Some(src) => err.with_node(src, node),
None => err,
});
}
let result = self.evaluate_inner(node);
self.recursion_depth -= 1;
match result {
Err(e) if e.expr().is_none() => {
if let Some(src) = &self.expr_source {
Err(e.with_node(src, node))
} else {
Err(e)
}
}
Ok(val) => {
if let Some(ref tt) = self.target_type {
val.coerce(tt, self.path_format).map_err(|msg| {
let e = ExpressionError::new(msg);
if let Some(src) = &self.expr_source {
e.with_node(src, node)
} else {
e
}
})
} else {
Ok(val)
}
}
other => other,
}
}
fn evaluate_inner(&mut self, node: &ast::Expr) -> Result<ExprValue, ExpressionError> {
match node {
ast::Expr::NumberLiteral(n) => self.eval_number(n),
ast::Expr::StringLiteral(s) => self.eval_string(s),
ast::Expr::BooleanLiteral(b) => self.track(ExprValue::Bool(b.value)),
ast::Expr::NoneLiteral(_) => self.track(ExprValue::Null),
ast::Expr::Name(n) => self.eval_name(n),
ast::Expr::Attribute(a) => self.eval_attribute(a),
ast::Expr::BinOp(b) => self.eval_binop(b),
ast::Expr::UnaryOp(u) => self.eval_unaryop(u),
ast::Expr::BoolOp(b) => self.eval_boolop(b),
ast::Expr::Compare(c) => self.eval_compare(c),
ast::Expr::If(i) => self.eval_ifexp(i),
ast::Expr::Call(c) => self.eval_call(c),
ast::Expr::List(l) => self.eval_list(l),
ast::Expr::Subscript(s) => self.eval_subscript(s),
ast::Expr::ListComp(lc) => self.eval_listcomp(lc),
ast::Expr::Slice(s) => self.eval_slice(s),
ast::Expr::Starred(_) => Err(ExpressionError::unsupported(
"Star unpacking is not supported",
)),
ast::Expr::Lambda(_) => Err(ExpressionError::unsupported(
"Lambda expressions are not supported",
)),
ast::Expr::Dict(_) => Err(ExpressionError::unsupported(
"Dict literals are not supported",
)),
ast::Expr::Set(_) => Err(ExpressionError::unsupported(
"Set literals are not supported",
)),
ast::Expr::DictComp(_) => Err(ExpressionError::unsupported(
"Dict comprehensions are not supported",
)),
ast::Expr::SetComp(_) => Err(ExpressionError::unsupported(
"Set comprehensions are not supported",
)),
ast::Expr::Generator(_) => Err(ExpressionError::unsupported(
"Generator expressions are not supported",
)),
ast::Expr::Await(_) => Err(ExpressionError::unsupported(
"Await expressions are not supported",
)),
ast::Expr::FString(_) => Err(ExpressionError::unsupported(
"f-strings are not supported; use string concatenation",
)),
ast::Expr::BytesLiteral(_) => Err(ExpressionError::unsupported(
"Byte strings are not supported",
)),
ast::Expr::EllipsisLiteral(_) => {
Err(ExpressionError::unsupported("Ellipsis is not supported"))
}
ast::Expr::Tuple(_) => Err(ExpressionError::unsupported(
"Tuple literals are not supported",
)),
ast::Expr::Named(_) => Err(ExpressionError::unsupported(
"Walrus operator (:=) is not supported",
)),
_ => Err(ExpressionError::unsupported("Unsupported expression type")),
}
}
fn count_op(&mut self) -> Result<(), ExpressionError> {
self.operation_count = self.operation_count.saturating_add(1);
if self.operation_count > self.operation_limit {
Err(ExpressionError::from_kind(
ExpressionErrorKind::OperationLimitExceeded {
count: self.operation_count,
limit: self.operation_limit,
},
))
} else {
Ok(())
}
}
fn collect_symbol_names(&self) -> Vec<String> {
let mut names = Vec::new();
for symtab in self.symtabs {
names.extend(symtab.all_paths(""));
}
names.sort();
names.dedup();
names
}
fn track(&mut self, value: ExprValue) -> Result<ExprValue, ExpressionError> {
if let ExprValue::Float(f) = &value {
if f.value().is_infinite() {
return Err(ExpressionError::float_error(
"Float operation produced infinity",
));
}
if f.value().is_nan() {
return Err(ExpressionError::float_error("Float operation produced NaN"));
}
}
self.current_memory += value.memory_size();
if self.current_memory > self.peak_memory {
self.peak_memory = self.current_memory;
}
if self.current_memory > self.memory_limit {
Err(ExpressionError::from_kind(
ExpressionErrorKind::MemoryLimitExceeded {
used: self.current_memory,
limit: self.memory_limit,
},
))
} else {
Ok(value)
}
}
fn release(&mut self, value: &ExprValue) {
let size = value.memory_size();
self.current_memory = self.current_memory.saturating_sub(size);
}
fn dispatch_with_node(
&mut self,
name: &str,
args: Vec<ExprValue>,
node: Option<&ast::Expr>,
) -> Result<ExprValue, ExpressionError> {
self.count_op()?;
let input_size: usize = args.iter().map(|a| a.memory_size()).sum();
let lib = self.library;
let result = lib.call(name, &args, self).map_err(|e| {
if let (Some(src), Some(n)) = (self.expr_source, node) {
e.with_node(src, n)
} else {
e
}
})?;
self.current_memory = self.current_memory.saturating_sub(input_size);
self.track(result)
}
fn dispatch_with_span(
&mut self,
name: &str,
args: Vec<ExprValue>,
range: ruff_text_size::TextRange,
) -> Result<ExprValue, ExpressionError> {
self.count_op()?;
let input_size: usize = args.iter().map(|a| a.memory_size()).sum();
let lib = self.library;
let result = lib.call(name, &args, self).map_err(|e| {
if let Some(src) = self.expr_source {
e.with_span(src, range.start().to_usize(), range.end().to_usize())
} else {
e
}
})?;
self.current_memory = self.current_memory.saturating_sub(input_size);
self.track(result)
}
fn eval_number(&mut self, n: &ast::ExprNumberLiteral) -> Result<ExprValue, ExpressionError> {
match &n.value {
ast::Number::Int(i) => {
let val: i64 = i.as_i64().ok_or_else(ExpressionError::integer_overflow)?;
self.track(ExprValue::Int(val))
}
ast::Number::Float(f) => {
if f.is_infinite() {
return Err(ExpressionError::float_error(
"Float operation produced infinity",
));
}
if f.is_nan() {
return Err(ExpressionError::float_error("Float operation produced NaN"));
}
let original = self.expr_source.as_ref().and_then(|src| {
let start = n.range.start().to_usize();
let end = n.range.end().to_usize();
if end <= src.len() {
let s = &src[start..end];
if s.ends_with('.') || s.starts_with('.') || s.contains('_') {
None
} else {
Some(s.to_string())
}
} else {
None
}
});
self.track(ExprValue::Float(if let Some(s) = original {
Float64::with_str(*f, s)?
} else {
Float64::new(*f)?
}))
}
ast::Number::Complex { .. } => Err(ExpressionError::unsupported(
"Complex numbers are not supported",
)),
}
}
fn eval_string(&mut self, s: &ast::ExprStringLiteral) -> Result<ExprValue, ExpressionError> {
for part in &s.value {
if matches!(
part.flags.prefix(),
ast::str_prefix::StringLiteralPrefix::Unicode
) {
return Err(ExpressionError::new(
"Unicode string prefix u'...' is not supported. Use '...' or \"...\" instead.",
));
}
}
self.track(ExprValue::String(s.value.to_string()))
}
fn check_path_format(&self, value: &ExprValue, name: &str) -> Result<(), ExpressionError> {
match value {
ExprValue::Path { format, .. } if *format != self.path_format => {
Err(ExpressionError::new(format!(
"Path format mismatch for '{name}': value has {format:?} but evaluator uses {:?}",
self.path_format
)))
}
ExprValue::ListPath(items, fmt, _) if !items.is_empty() && *fmt != self.path_format => {
Err(ExpressionError::new(format!(
"Path format mismatch for '{name}': value has {fmt:?} but evaluator uses {:?}",
self.path_format
)))
}
_ => Ok(()),
}
}
fn eval_name(&mut self, n: &ast::ExprName) -> Result<ExprValue, ExpressionError> {
let name = n.id.as_str();
match name {
"True" | "true" => return self.track(ExprValue::Bool(true)),
"False" | "false" => return self.track(ExprValue::Bool(false)),
"None" | "null" => return self.track(ExprValue::Null),
_ => {}
}
for symtab in self.symtabs.iter().rev() {
if let Some(val) = symtab.get_value(name) {
self.check_path_format(val, name)?;
return self.track(val.clone());
}
}
let available = self.collect_symbol_names();
let suggestion = crate::edit_distance::suggest_closest(
name,
&available.iter().map(|s| s.as_str()).collect::<Vec<_>>(),
);
Err(ExpressionError::from_kind(
ExpressionErrorKind::UndefinedVariable {
name: name.to_string(),
suggestion,
},
))
}
fn eval_attribute(&mut self, a: &ast::ExprAttribute) -> Result<ExprValue, ExpressionError> {
let dotted_path = build_dotted_name_from_attr(a);
if let Some(ref path) = dotted_path {
let resolved = resolve_keyword_renames(path, self.keyword_renames);
for symtab in self.symtabs.iter().rev() {
if let Some(val) = symtab.get_value(&resolved) {
self.count_op()?;
self.check_path_format(val, &resolved)?;
return self.track(val.clone());
}
}
}
let value = match self.evaluate(&a.value) {
Ok(v) => v,
Err(_) if dotted_path.is_some() => {
let path = dotted_path.as_ref().unwrap();
let available = self.collect_symbol_names();
let suggestion = crate::edit_distance::suggest_closest(
path,
&available.iter().map(|s| s.as_str()).collect::<Vec<_>>(),
);
let src = self.expr_source.unwrap_or("");
let attr_node = ast::Expr::Attribute(a.clone());
return Err(
ExpressionError::from_kind(ExpressionErrorKind::UndefinedVariable {
name: path.clone(),
suggestion,
})
.with_node(src, &attr_node),
);
}
Err(e) => return Err(e),
};
let attr = a.attr.as_str();
let prop_name = format!("__property_{attr}__");
let attr_node = ast::Expr::Attribute(a.clone());
match self.dispatch_with_node(&prop_name, vec![value.clone()], Some(&attr_node)) {
Ok(v) => Ok(v),
Err(_) => {
let src = self.expr_source.unwrap_or("");
let val_type = value.expr_type();
let val_type_str = if val_type.code() == crate::types::TypeCode::Unresolved
&& !val_type.params().is_empty()
{
val_type.params()[0].to_string()
} else {
val_type.to_string()
};
{
let lib = self.library;
if !lib.get_signatures(attr).is_empty() {
return Err(ExpressionError::new(format!(
"'{attr}' is a method, not a property. Did you mean {attr}()?"
))
.with_node(src, &attr_node));
}
}
{
let lib = self.library;
let valid_types: Vec<String> = lib
.get_signatures(&prop_name)
.iter()
.filter_map(|e| e.signature.sig_params().first())
.filter(|t: &&crate::types::ExprType| !t.is_symbolic())
.map(|t: &crate::types::ExprType| t.to_string())
.collect::<std::collections::BTreeSet<_>>()
.into_iter()
.collect();
if !valid_types.is_empty() {
return Err(ExpressionError::new(format!(
"'{attr}' property is not available for {val_type_str}. Available for: {}",
valid_types.join(", ")
)).with_node(src, &attr_node));
}
}
Err(ExpressionError::new(format!(
"Cannot access attribute '{attr}' on {val_type_str}"
))
.with_node(src, &attr_node))
}
}
}
fn eval_binop(&mut self, b: &ast::ExprBinOp) -> Result<ExprValue, ExpressionError> {
let op_name = match b.op {
ast::Operator::Add => "__add__",
ast::Operator::Sub => "__sub__",
ast::Operator::Mult => "__mul__",
ast::Operator::Div => "__truediv__",
ast::Operator::FloorDiv => "__floordiv__",
ast::Operator::Mod => "__mod__",
ast::Operator::Pow => "__pow__",
ast::Operator::BitAnd => {
return Err(ExpressionError::unsupported(
"Bitwise AND (&) is not supported",
))
}
ast::Operator::BitOr => {
return Err(ExpressionError::unsupported(
"Bitwise OR (|) is not supported",
))
}
ast::Operator::BitXor => {
return Err(ExpressionError::unsupported(
"Bitwise XOR (^) is not supported",
))
}
ast::Operator::LShift => {
return Err(ExpressionError::unsupported(
"Left shift (<<) is not supported",
))
}
ast::Operator::RShift => {
return Err(ExpressionError::unsupported(
"Right shift (>>) is not supported",
))
}
ast::Operator::MatMult => {
return Err(ExpressionError::unsupported(
"Matrix multiply (@) is not supported",
))
}
};
let left = self.evaluate_with_target(&b.left, None)?;
let right = self.evaluate_with_target(&b.right, None)?;
self.dispatch_with_node(
op_name,
vec![left, right],
Some(&ast::Expr::BinOp(b.clone())),
)
}
fn eval_unaryop(&mut self, u: &ast::ExprUnaryOp) -> Result<ExprValue, ExpressionError> {
self.count_op()?;
if u.op == ast::UnaryOp::Invert {
return Err(ExpressionError::unsupported(
"Bitwise NOT (~) is not supported",
));
}
if matches!(u.op, ast::UnaryOp::USub) {
if let ast::Expr::NumberLiteral(n) = &*u.operand {
if let ast::Number::Int(i) = &n.value {
if let Some(pos) = i.as_i64() {
return self.track(ExprValue::Int(
pos.checked_neg()
.ok_or_else(ExpressionError::integer_overflow)?,
));
}
if let Some(pos) = i.as_u64() {
if pos == 9223372036854775808u64 {
return self.track(ExprValue::Int(i64::MIN));
}
}
return Err(ExpressionError::integer_overflow());
}
}
}
let operand = self.evaluate_with_target(&u.operand, None)?;
let op_name = match u.op {
ast::UnaryOp::USub => "__neg__",
ast::UnaryOp::UAdd => "__pos__",
ast::UnaryOp::Not => "__not__",
ast::UnaryOp::Invert => unreachable!(),
};
self.dispatch_with_node(op_name, vec![operand], Some(&ast::Expr::UnaryOp(u.clone())))
}
fn eval_boolop(&mut self, b: &ast::ExprBoolOp) -> Result<ExprValue, ExpressionError> {
self.count_op()?;
let mut last = ExprValue::Bool(match b.op {
ast::BoolOp::And => true,
ast::BoolOp::Or => false,
});
let mut seen_unresolved = false;
for node in &b.values {
if seen_unresolved {
match self.evaluate(node) {
Ok(val) => match b.op {
ast::BoolOp::And => {
if matches!(&val, ExprValue::Null | ExprValue::Bool(false)) {
return Ok(val);
}
}
ast::BoolOp::Or => {
if !matches!(&val, ExprValue::Null | ExprValue::Bool(false)) {
return Ok(val);
}
}
},
Err(_) => { }
}
continue;
}
last = self.evaluate(node)?;
if last.is_unresolved() {
seen_unresolved = true;
continue;
}
match b.op {
ast::BoolOp::And => {
if matches!(&last, ExprValue::Null | ExprValue::Bool(false)) {
return Ok(last);
}
}
ast::BoolOp::Or => {
if !matches!(&last, ExprValue::Null | ExprValue::Bool(false)) {
return Ok(last);
}
}
}
}
if seen_unresolved {
return self.track(ExprValue::unresolved(ExprType::BOOL));
}
Ok(last)
}
fn eval_compare(&mut self, c: &ast::ExprCompare) -> Result<ExprValue, ExpressionError> {
self.count_op()?;
for op in &c.ops {
match op {
ast::CmpOp::Is => {
return Err(ExpressionError::unsupported(
"'is' operator is not supported; use '=='",
))
}
ast::CmpOp::IsNot => {
return Err(ExpressionError::unsupported(
"'is not' operator is not supported; use '!='",
))
}
_ => {}
}
}
let mut left = self.evaluate_with_target(&c.left, None)?;
for (op, right_node) in c.ops.iter().zip(c.comparators.iter()) {
let right = self.evaluate_with_target(right_node, None)?;
if left.is_unresolved() || right.is_unresolved() {
self.release(&left);
self.release(&right);
return self.track(ExprValue::unresolved(ExprType::BOOL));
}
let (op_name, args) = match op {
ast::CmpOp::Eq => ("__eq__", vec![left.clone(), right.clone()]),
ast::CmpOp::NotEq => ("__ne__", vec![left.clone(), right.clone()]),
ast::CmpOp::Lt => ("__lt__", vec![left.clone(), right.clone()]),
ast::CmpOp::LtE => ("__le__", vec![left.clone(), right.clone()]),
ast::CmpOp::Gt => ("__gt__", vec![left.clone(), right.clone()]),
ast::CmpOp::GtE => ("__ge__", vec![left.clone(), right.clone()]),
ast::CmpOp::In => ("__contains__", vec![right.clone(), left.clone()]),
ast::CmpOp::NotIn => ("__not_contains__", vec![right.clone(), left.clone()]),
ast::CmpOp::Is => {
return Err(ExpressionError::unsupported(
"'is' operator is not supported; use '=='",
))
}
ast::CmpOp::IsNot => {
return Err(ExpressionError::unsupported(
"'is not' operator is not supported; use '!='",
))
}
};
use ruff_text_size::Ranged;
let result_val = self.dispatch_with_span(op_name, args, c.range())?;
let result = match &result_val {
ExprValue::Bool(b) => *b,
_ => true,
};
self.release(&result_val);
if !result {
self.release(&left);
self.release(&right);
return self.track(ExprValue::Bool(false));
}
self.release(&left);
left = right;
}
self.release(&left);
self.track(ExprValue::Bool(true))
}
fn eval_ifexp(&mut self, i: &ast::ExprIf) -> Result<ExprValue, ExpressionError> {
self.count_op()?;
let test = self.evaluate_with_target(&i.test, None)?;
if test.is_unresolved() {
let inner = unwrap_unresolved(&test.expr_type());
let is_bool_compatible = inner == ExprType::BOOL
|| inner.code() == crate::types::TypeCode::Unresolved
|| inner.code() == crate::types::TypeCode::Any
|| (inner.code() == crate::types::TypeCode::Union
&& inner.params().contains(&ExprType::BOOL));
if !is_bool_compatible {
let err =
ExpressionError::new(format!("Condition must be a boolean, got {}", inner));
self.release(&test);
return Err(if let Some(src) = self.expr_source {
err.with_node(src, &i.test)
} else {
err
});
}
self.release(&test);
let body = self.evaluate(&i.body);
let orelse = self.evaluate(&i.orelse);
match (body, orelse) {
(Err(be), Err(oe)) => {
let mut msg = format!(
"Both branches fail in the if/else:\n if-branch: {}\n",
be.message()
);
append_sub_error(&mut msg, &be, false);
msg.push_str(&format!(" else-branch: {}\n", oe.message()));
append_sub_error(&mut msg, &oe, true);
let mut err = ExpressionError::new(msg).with_sub_errors(vec![be, oe]);
if let Some(src) = self.expr_source {
use ruff_text_size::Ranged;
let start = i.range().start().to_usize();
let end = i.range().end().to_usize();
err.set_source_span(src, start, end, 0);
}
Err(err)
}
(Ok(b), Err(_)) => {
let t = unwrap_unresolved(&b.expr_type());
self.track(ExprValue::unresolved(t))
}
(Err(_), Ok(o)) => {
let t = unwrap_unresolved(&o.expr_type());
self.track(ExprValue::unresolved(t))
}
(Ok(b), Ok(o)) => {
let bt = unwrap_unresolved(&b.expr_type());
let ot = unwrap_unresolved(&o.expr_type());
if bt == ot {
self.track(ExprValue::unresolved(bt))
} else {
self.track(ExprValue::unresolved(ExprType::union(vec![bt, ot])))
}
}
}
} else {
if !matches!(&test, ExprValue::Bool(_)) {
let err = ExpressionError::new(format!(
"Condition must be a boolean, got {}",
test.expr_type()
));
return Err(if let Some(src) = self.expr_source {
err.with_node(src, &i.test)
} else {
err
});
}
if matches!(test, ExprValue::Bool(true)) {
self.release(&test);
self.evaluate(&i.body)
} else {
self.release(&test);
self.evaluate(&i.orelse)
}
}
}
fn eval_call(&mut self, c: &ast::ExprCall) -> Result<ExprValue, ExpressionError> {
self.count_op()?;
if !c.arguments.keywords.is_empty() {
return Err(ExpressionError::unsupported(
"Keyword arguments are not supported",
));
}
let mut receiver_value: Option<ExprValue> = None;
let is_method_call;
let func_name = match &*c.func {
ast::Expr::Name(n) => {
is_method_call = false;
Some(n.id.to_string())
}
ast::Expr::Attribute(a) => {
let receiver = self.evaluate(&a.value)?;
receiver_value = Some(receiver);
is_method_call = true;
Some(a.attr.to_string())
}
_ => {
is_method_call = false;
None
}
};
let mut args = Vec::new();
for arg in &c.arguments.args {
args.push(self.evaluate(arg)?);
}
if let Some(recv) = receiver_value {
args.insert(0, recv);
}
let _any_unresolved = args.iter().any(|a| a.is_unresolved());
let args_size: usize = args.iter().map(|a| a.memory_size()).sum();
self.current_memory = self.current_memory.saturating_sub(args_size);
if let Some(name) = &func_name {
if name.starts_with("__") && name.ends_with("__") {
return Err(ExpressionError::new(format!(
"Cannot call '{}' directly",
name
)));
}
{
let lib = self.library;
let result = if is_method_call {
lib.call_method(name, &args, self)
} else {
lib.call(name, &args, self)
};
let result = result.map_err(|e| {
let src = self.expr_source.unwrap_or("");
let call_node = ast::Expr::Call(c.clone());
if is_method_call
&& !lib
.get_signatures(&format!("__property_{name}__"))
.is_empty()
{
return ExpressionError::new(format!(
"'{name}' is a property, not a method. Use .{name} instead of .{name}()"
))
.with_node(src, &call_node);
}
e.with_node(src, &call_node)
})?;
self.track(result)
}
} else {
Err(ExpressionError::new("Cannot call non-function expression"))
}
}
fn eval_list(&mut self, l: &ast::ExprList) -> Result<ExprValue, ExpressionError> {
let list_elem_target = self.target_type.as_ref().and_then(|tt| {
if tt.code() == crate::types::TypeCode::List && tt.params().len() == 1 {
Some(tt.params()[0].clone())
} else {
None
}
});
let saved_target = self.target_type.take();
if let Some(ref elem_t) = list_elem_target {
self.target_type = Some(elem_t.clone());
}
let mut elements = Vec::new();
for elt in &l.elts {
let val = self.evaluate(elt)?;
if matches!(&val, ExprValue::Null) {
self.target_type = saved_target;
return Err(ExpressionError::new("null is not allowed in list literals"));
}
elements.push(val);
}
self.target_type = saved_target;
for e in &elements {
let t = e.expr_type();
if let Some(inner) = t.list_element_type() {
if inner.list_element_type().is_some() {
return Err(ExpressionError::new(
"Lists may be nested at most 2 levels deep",
));
}
}
}
if elements.iter().any(|e| e.is_unresolved()) {
if !elements.is_empty() {
let first_type = unwrap_unresolved(&elements[0].expr_type());
for (_i, e) in elements.iter().enumerate().skip(1) {
let t = unwrap_unresolved(&e.expr_type());
if (first_type == ExprType::INT && t == ExprType::FLOAT)
|| (first_type == ExprType::FLOAT && t == ExprType::INT)
|| (first_type == ExprType::PATH && t == ExprType::STRING)
|| (first_type == ExprType::STRING && t == ExprType::PATH)
{
continue;
}
if t.code() == crate::types::TypeCode::Unresolved
|| first_type.code() == crate::types::TypeCode::Unresolved
{
continue;
}
if t != first_type {
return Err(ExpressionError::new(format!(
"List literal contains incompatible types: {first_type}, {t}"
)));
}
}
}
let elem_type = if elements.is_empty() {
ExprType::NULLTYPE
} else {
let mut result = unwrap_unresolved(&elements[0].expr_type());
for e in elements.iter().skip(1) {
let t = unwrap_unresolved(&e.expr_type());
if t.code() == crate::types::TypeCode::Unresolved {
continue;
}
if result.code() == crate::types::TypeCode::Unresolved {
result = t;
continue;
}
if (result == ExprType::INT && t == ExprType::FLOAT)
|| (result == ExprType::FLOAT && t == ExprType::INT)
{
result = ExprType::FLOAT;
} else if (result == ExprType::PATH && t == ExprType::STRING)
|| (result == ExprType::STRING && t == ExprType::PATH)
{
result = ExprType::STRING;
}
}
result
};
return self.track(ExprValue::unresolved(ExprType::list(elem_type)));
}
if let Some(ref elem_t) = list_elem_target {
let coerced: Result<Vec<ExprValue>, _> = elements
.into_iter()
.map(|e| {
e.coerce(elem_t, self.path_format)
.map_err(ExpressionError::new)
})
.collect();
let list = ExprValue::make_list_checked(self, coerced?, elem_t.clone())?;
return self.track(list);
}
if !elements.is_empty() {
let mut seen_types: Vec<ExprType> = Vec::new();
for e in elements.iter() {
let t = e.expr_type();
if !seen_types.contains(&t) {
seen_types.push(t);
}
}
let dominated: Vec<&ExprType> = seen_types
.iter()
.filter(|t| {
t.code() == crate::types::TypeCode::NullType ||
(**t == ExprType::INT && seen_types.contains(&ExprType::FLOAT)) ||
(**t == ExprType::FLOAT && seen_types.contains(&ExprType::INT)) ||
(**t == ExprType::PATH && seen_types.contains(&ExprType::STRING)) ||
(**t == ExprType::STRING && seen_types.contains(&ExprType::PATH))
})
.collect();
let compatible = dominated.len() == seen_types.len() ||
seen_types.len() == 1 ||
seen_types.iter().all(|t| t.code() == crate::types::TypeCode::List || t.code() == crate::types::TypeCode::NullType);
if !compatible {
let type_strs: Vec<String> = seen_types.iter().map(|t| t.to_string()).collect();
let msg = if type_strs.len() == 2 {
format!(
"List literal contains incompatible types: {} and {}",
type_strs[0], type_strs[1]
)
} else {
let last = type_strs.last().unwrap();
let rest = type_strs[..type_strs.len() - 1].join(", ");
format!("List literal contains incompatible types: {rest}, and {last}")
};
return Err(ExpressionError::new(msg));
}
}
let elem_type = if elements.is_empty() {
ExprType::NULLTYPE
} else {
elements[0].expr_type()
};
let list = ExprValue::make_list_checked(self, elements, elem_type)?;
self.track(list)
}
fn eval_subscript(&mut self, s: &ast::ExprSubscript) -> Result<ExprValue, ExpressionError> {
self.count_op()?;
let value = self.evaluate(&s.value)?;
if matches!(&value, ExprValue::Path { .. }) {
return Err(ExpressionError::new(
"Cannot subscript type path".to_string(),
));
}
if let ast::Expr::Slice(sl) = &*s.slice {
let start = match sl.lower.as_ref().map(|e| self.evaluate(e)).transpose()? {
Some(v) => v,
None => ExprValue::Null,
};
let stop = match sl.upper.as_ref().map(|e| self.evaluate(e)).transpose()? {
Some(v) => v,
None => ExprValue::Null,
};
let step = match sl.step.as_ref().map(|e| self.evaluate(e)).transpose()? {
Some(v) => v,
None => ExprValue::Null,
};
if let ExprValue::Int(0) = &step {
return Err(ExpressionError::new("Slice step cannot be zero"));
}
if value.is_unresolved() {
let inner = unwrap_unresolved(&value.expr_type());
if let Some(elem) = inner.list_element_type() {
return self.track(ExprValue::unresolved(ExprType::list(elem.clone())));
}
return self.track(ExprValue::unresolved(inner));
}
let any_bound_unresolved =
start.is_unresolved() || stop.is_unresolved() || step.is_unresolved();
if any_bound_unresolved {
if value.is_list() {
let elem_type = value.list_elem_type().unwrap();
return self.track(ExprValue::unresolved(ExprType::list(elem_type.clone())));
} else if matches!(&value, ExprValue::String(_)) {
return self.track(ExprValue::unresolved(ExprType::STRING));
}
}
let node = ast::Expr::Subscript(s.clone());
return self.dispatch_with_node(
"__getitem__",
vec![value, start, stop, step],
Some(&node),
);
}
let slice = self.evaluate(&s.slice)?;
if slice.is_unresolved() {
let inner = unwrap_unresolved(&slice.expr_type());
if inner != ExprType::INT
&& inner.code() != crate::types::TypeCode::Unresolved
&& inner.code() != crate::types::TypeCode::Any
{
let mut err = ExpressionError::new("Index must be an integer");
if let Some(src) = self.expr_source {
use ruff_text_size::Ranged;
let start = s.value.range().start().to_usize();
let end = s.range().end().to_usize();
let left_end = s.value.range().end().to_usize();
err.set_source_span(src, start, end, left_end - start);
}
return Err(err);
}
}
self.dispatch_with_node(
"__getitem__",
vec![value, slice],
Some(&ast::Expr::Subscript(s.clone())),
)
}
fn child_evaluator<'b>(&self, symtabs: &'b [&'b SymbolTable]) -> Evaluator<'b>
where
'a: 'b,
{
Evaluator {
symtabs,
path_format: self.path_format,
expr_source: self.expr_source,
memory_limit: self.memory_limit,
operation_limit: self.operation_limit,
current_memory: self.current_memory,
peak_memory: self.peak_memory,
operation_count: self.operation_count,
recursion_depth: self.recursion_depth,
keyword_renames: self.keyword_renames,
library: self.library,
target_type: None,
regex_cache: std::collections::HashMap::new(),
}
}
fn absorb_counters(&mut self, child: &Evaluator) {
self.current_memory = child.current_memory;
self.peak_memory = child.peak_memory;
self.operation_count = child.operation_count;
}
fn eval_listcomp(&mut self, lc: &ast::ExprListComp) -> Result<ExprValue, ExpressionError> {
if lc.generators.len() != 1 {
return Err(ExpressionError::unsupported(
"Multiple 'for' clauses in list comprehensions are not supported",
));
}
let gen = &lc.generators[0];
if gen.ifs.len() > 1 {
return Err(ExpressionError::unsupported(
"Multiple 'if' clauses in a list comprehension are not supported; combine with 'and'",
));
}
if !matches!(&gen.target, ast::Expr::Name(_)) {
return Err(ExpressionError::unsupported(
"Tuple unpacking in list comprehension is not supported",
));
}
if let ast::Expr::Name(n) = &gen.target {
let var_name = n.id.as_str();
if !var_name.is_empty() && !var_name.starts_with(|c: char| c.is_lowercase() || c == '_')
{
return Err(ExpressionError::new(format!(
"Loop variable '{var_name}' must start with a lowercase letter or underscore"
)));
}
}
let iterable = self.evaluate(&gen.iter)?;
let var_name = match &gen.target {
ast::Expr::Name(n) => n.id.to_string(),
_ => unreachable!(),
};
if iterable.is_unresolved() {
let inner = unwrap_unresolved(&iterable.expr_type());
let elem_type = inner.list_element_type().cloned().unwrap_or(ExprType::INT);
let mut tmp = crate::symbol_table::SymbolTable::new();
tmp.set(&var_name, ExprValue::unresolved(elem_type))
.map_err(|e| ExpressionError::new(e.to_string()))?;
let mut combined: Vec<&SymbolTable> = self.symtabs.to_vec();
combined.push(&tmp);
let mut child = self.child_evaluator(&combined);
if let Some(if_clause) = gen.ifs.first() {
let cond = child.evaluate(if_clause)?;
let cond_inner = unwrap_unresolved(&cond.expr_type());
let is_bool_compatible = cond_inner == ExprType::BOOL
|| cond_inner.code() == crate::types::TypeCode::Unresolved
|| cond_inner.code() == crate::types::TypeCode::Any
|| (cond_inner.code() == crate::types::TypeCode::Union
&& cond_inner.params().contains(&ExprType::BOOL));
if !is_bool_compatible {
let err = ExpressionError::new(format!(
"List comprehension filter must be a boolean, got {}",
cond_inner
));
return Err(if let Some(src) = self.expr_source {
err.with_node(src, if_clause)
} else {
err
});
}
}
let body_val = child.evaluate(&lc.elt)?;
self.absorb_counters(&child);
let body_type = unwrap_unresolved(&body_val.expr_type());
return self.track(ExprValue::unresolved(ExprType::list(body_type)));
}
let items: Vec<ExprValue> = if let Some(iter) = iterable.list_iter() {
iter.collect()
} else if let ExprValue::RangeExpr(r) = &iterable {
r.iter().map(ExprValue::Int).collect()
} else {
return Err(ExpressionError::type_error(format!(
"Cannot iterate over {}",
iterable.expr_type()
)));
};
self.release(&iterable);
let mut result = Vec::new();
let base_symtabs: Vec<&SymbolTable> = self.symtabs.to_vec();
for item in &items {
self.count_op()?;
let mut tmp = crate::symbol_table::SymbolTable::new();
tmp.set(&var_name, item.clone())
.map_err(|e| ExpressionError::new(e.to_string()))?;
let mut combined = base_symtabs.clone();
combined.push(&tmp);
let mut child = self.child_evaluator(&combined);
child.regex_cache = std::mem::take(&mut self.regex_cache);
let mut include = true;
if let Some(if_clause) = gen.ifs.first() {
let cond = child.evaluate(if_clause)?;
if let ExprValue::Bool(b) = cond {
include = b;
} else {
let err = ExpressionError::new(format!(
"List comprehension filter must be a boolean, got {}",
cond.expr_type()
));
return Err(if let Some(src) = self.expr_source {
err.with_node(src, if_clause)
} else {
err
});
}
}
if include {
result.push(child.evaluate(&lc.elt)?);
}
self.absorb_counters(&child);
self.regex_cache = child.regex_cache;
self.current_memory = self.current_memory.saturating_sub(item.memory_size());
}
for e in &result {
let t = e.expr_type();
if let Some(inner) = t.list_element_type() {
if inner.list_element_type().is_some() {
return Err(ExpressionError::new(
"Lists may be nested at most 2 levels deep",
));
}
}
}
let elem_type = if result.is_empty() {
ExprType::NULLTYPE
} else {
result[0].expr_type()
};
let list = ExprValue::make_list_checked(self, result, elem_type)?;
self.track(list)
}
fn eval_slice(&mut self, s: &ast::ExprSlice) -> Result<ExprValue, ExpressionError> {
if let Some(step) = &s.step {
let step_val = self.evaluate(step)?;
if let ExprValue::Int(0) = step_val {
return Err(ExpressionError::new("Slice step cannot be zero"));
}
}
self.track(ExprValue::unresolved(ExprType::INT))
}
}
impl<'a> crate::function_library::EvalContext for Evaluator<'a> {
fn path_format(&self) -> PathFormat {
self.path_format
}
fn count_op(&mut self) -> Result<(), ExpressionError> {
self.operation_count = self.operation_count.saturating_add(1);
if self.operation_count > self.operation_limit {
Err(ExpressionError::from_kind(
ExpressionErrorKind::OperationLimitExceeded {
count: self.operation_count,
limit: self.operation_limit,
},
))
} else {
Ok(())
}
}
fn count_ops(&mut self, n: usize) -> Result<(), ExpressionError> {
self.operation_count = self.operation_count.saturating_add(n);
if self.operation_count > self.operation_limit {
Err(ExpressionError::from_kind(
ExpressionErrorKind::OperationLimitExceeded {
count: self.operation_count,
limit: self.operation_limit,
},
))
} else {
Ok(())
}
}
fn count_string_ops(&mut self, len: usize) -> Result<(), ExpressionError> {
let ops = len.div_ceil(256);
self.operation_count = self.operation_count.saturating_add(ops);
if self.operation_count > self.operation_limit {
Err(ExpressionError::from_kind(
ExpressionErrorKind::OperationLimitExceeded {
count: self.operation_count,
limit: self.operation_limit,
},
))
} else {
Ok(())
}
}
fn check_memory(&self, bytes: usize) -> Result<(), ExpressionError> {
let projected = self.current_memory.saturating_add(bytes);
if projected > self.memory_limit {
Err(ExpressionError::from_kind(
ExpressionErrorKind::MemoryLimitExceeded {
used: projected,
limit: self.memory_limit,
},
))
} else {
Ok(())
}
}
fn get_or_compile_regex(&mut self, pattern: &str) -> Result<regex::Regex, ExpressionError> {
if let Some(re) = self.regex_cache.get(pattern) {
return Ok(re.clone());
}
let re = regex::RegexBuilder::new(pattern)
.size_limit(1 << 20)
.build()
.map_err(|e| ExpressionError::new(format!("Invalid regex: {e}")))?;
self.regex_cache.insert(pattern.to_string(), re.clone());
Ok(re)
}
}
fn unwrap_unresolved(t: &ExprType) -> ExprType {
if t.code() == crate::types::TypeCode::Unresolved && !t.params().is_empty() {
t.params()[0].clone()
} else {
t.clone()
}
}
fn build_dotted_name_from_attr(a: &ast::ExprAttribute) -> Option<String> {
let mut parts: Vec<&str> = vec![a.attr.as_str()];
let mut current: &ast::Expr = &a.value;
loop {
match current {
ast::Expr::Name(n) => {
parts.push(n.id.as_str());
break;
}
ast::Expr::Attribute(attr) => {
parts.push(attr.attr.as_str());
current = &attr.value;
}
_ => return None,
}
}
parts.reverse();
Some(parts.join("."))
}
fn resolve_keyword_renames(
name: &str,
renames: &std::collections::HashMap<String, String>,
) -> String {
if renames.is_empty() {
return name.to_string();
}
let mut result = name.to_string();
for (replacement, original) in renames {
let from = format!(".{replacement}");
let to = format!(".{original}");
result = result.replace(&from, &to);
}
result
}
use crate::types::ExprType;