use crate::analysis::expr::ast::{BinaryOp, Expr, LiteralValue, UnaryOp};
use crate::runtime::conversion::integer_to_decimal;
use crate::runtime::functions::call_function;
use crate::runtime::model::{
lookup_field, parse_qualified_field_with_interfaces, FieldLookup, Row, RuntimeValue,
};
pub fn evaluate_expr(
expr: &Expr,
workspaces: &std::collections::BTreeMap<String, Vec<Row>>,
row_index: usize,
) -> Result<RuntimeValue, String> {
let interface_ids: Vec<String> = workspaces.keys().cloned().collect();
evaluate_expr_with_interfaces(expr, workspaces, row_index, &interface_ids)
}
fn evaluate_expr_with_interfaces(
expr: &Expr,
workspaces: &std::collections::BTreeMap<String, Vec<Row>>,
row_index: usize,
interface_ids: &[String],
) -> Result<RuntimeValue, String> {
match expr {
Expr::Literal { value, .. } => Ok(literal_to_runtime(value)),
Expr::FieldRef { target, .. } => {
let qualified = parse_qualified_field_with_interfaces(target, interface_ids)
.ok_or_else(|| format!("invalid field reference '{target}'"))?;
let rows = workspaces
.get(&qualified.interface_id)
.ok_or_else(|| format!("unknown interface '{}'", qualified.interface_id))?;
let row = rows
.get(row_index)
.ok_or_else(|| format!("row index {row_index} out of range"))?;
Ok(match lookup_field(row, &qualified.field_name) {
FieldLookup::Missing => RuntimeValue::missing(),
FieldLookup::Present(value) => value,
})
}
Expr::Unary { op, expr, .. } => {
let inner = evaluate_expr_with_interfaces(expr, workspaces, row_index, interface_ids)?;
match op {
UnaryOp::Negate => match inner {
RuntimeValue::Integer(v) => Ok(RuntimeValue::Integer(-v)),
RuntimeValue::Decimal(v) => Ok(RuntimeValue::Decimal(-v)),
other => Err(format!("negate unsupported for {other:?}")),
},
UnaryOp::Not => match inner.as_bool() {
Some(v) => Ok(RuntimeValue::Boolean(!v)),
None => Err("not requires boolean".into()),
},
}
}
Expr::Binary {
op, left, right, ..
} => match op {
BinaryOp::And => {
let left_val =
evaluate_expr_with_interfaces(left, workspaces, row_index, interface_ids)?;
let left_bool = left_val
.as_bool()
.ok_or_else(|| "and requires boolean operands".to_string())?;
if !left_bool {
return Ok(RuntimeValue::Boolean(false));
}
let right_val =
evaluate_expr_with_interfaces(right, workspaces, row_index, interface_ids)?;
let right_bool = right_val
.as_bool()
.ok_or_else(|| "and requires boolean operands".to_string())?;
Ok(RuntimeValue::Boolean(right_bool))
}
BinaryOp::Or => {
let left_val =
evaluate_expr_with_interfaces(left, workspaces, row_index, interface_ids)?;
let left_bool = left_val
.as_bool()
.ok_or_else(|| "or requires boolean operands".to_string())?;
if left_bool {
return Ok(RuntimeValue::Boolean(true));
}
let right_val =
evaluate_expr_with_interfaces(right, workspaces, row_index, interface_ids)?;
let right_bool = right_val
.as_bool()
.ok_or_else(|| "or requires boolean operands".to_string())?;
Ok(RuntimeValue::Boolean(right_bool))
}
_ => {
let left_val =
evaluate_expr_with_interfaces(left, workspaces, row_index, interface_ids)?;
let right_val =
evaluate_expr_with_interfaces(right, workspaces, row_index, interface_ids)?;
evaluate_binary(*op, &left_val, &right_val)
}
},
Expr::Call { callee, args, .. } => {
let evaluated_args: Result<Vec<_>, _> = args
.iter()
.map(|arg| evaluate_expr_with_interfaces(arg, workspaces, row_index, interface_ids))
.collect();
call_function(callee, &evaluated_args?)
}
}
}
fn literal_to_runtime(value: &LiteralValue) -> RuntimeValue {
match value {
LiteralValue::Boolean(v) => RuntimeValue::Boolean(*v),
LiteralValue::String(v) => RuntimeValue::String(v.clone()),
LiteralValue::Integer(v) => RuntimeValue::Integer(*v),
LiteralValue::Decimal(v) => RuntimeValue::Decimal(*v),
}
}
fn values_equal(left: &RuntimeValue, right: &RuntimeValue) -> bool {
if left == right {
return true;
}
match (left, right) {
(RuntimeValue::Integer(a), RuntimeValue::Decimal(b)) => {
integer_to_decimal(*a).is_ok_and(|promoted| promoted == *b)
}
(RuntimeValue::Decimal(a), RuntimeValue::Integer(b)) => {
integer_to_decimal(*b).is_ok_and(|promoted| *a == promoted)
}
_ => false,
}
}
fn evaluate_binary(
op: BinaryOp,
left: &RuntimeValue,
right: &RuntimeValue,
) -> Result<RuntimeValue, String> {
match op {
BinaryOp::Add => match (left, right) {
(RuntimeValue::Integer(a), RuntimeValue::Integer(b)) => a
.checked_add(*b)
.map(RuntimeValue::Integer)
.ok_or_else(|| "integer overflow".into()),
(RuntimeValue::Decimal(a), RuntimeValue::Decimal(b)) => {
Ok(RuntimeValue::Decimal(a + b))
}
(RuntimeValue::String(a), RuntimeValue::String(b)) => {
Ok(RuntimeValue::String(format!("{a}{b}")))
}
(RuntimeValue::Integer(a), RuntimeValue::Decimal(b)) => {
Ok(RuntimeValue::Decimal(integer_to_decimal(*a)? + b))
}
(RuntimeValue::Decimal(a), RuntimeValue::Integer(b)) => {
Ok(RuntimeValue::Decimal(a + integer_to_decimal(*b)?))
}
_ => Err("add type mismatch".into()),
},
BinaryOp::Sub => match (left, right) {
(RuntimeValue::Integer(a), RuntimeValue::Integer(b)) => a
.checked_sub(*b)
.map(RuntimeValue::Integer)
.ok_or_else(|| "integer overflow".into()),
(RuntimeValue::Decimal(a), RuntimeValue::Decimal(b)) => {
Ok(RuntimeValue::Decimal(a - b))
}
(RuntimeValue::Integer(a), RuntimeValue::Decimal(b)) => {
Ok(RuntimeValue::Decimal(integer_to_decimal(*a)? - b))
}
(RuntimeValue::Decimal(a), RuntimeValue::Integer(b)) => {
Ok(RuntimeValue::Decimal(a - integer_to_decimal(*b)?))
}
_ => Err("sub type mismatch".into()),
},
BinaryOp::Mul => match (left, right) {
(RuntimeValue::Integer(a), RuntimeValue::Integer(b)) => a
.checked_mul(*b)
.map(RuntimeValue::Integer)
.ok_or_else(|| "integer overflow".into()),
(RuntimeValue::Decimal(a), RuntimeValue::Decimal(b)) => {
Ok(RuntimeValue::Decimal(a * b))
}
(RuntimeValue::Integer(a), RuntimeValue::Decimal(b)) => {
Ok(RuntimeValue::Decimal(integer_to_decimal(*a)? * b))
}
(RuntimeValue::Decimal(a), RuntimeValue::Integer(b)) => {
Ok(RuntimeValue::Decimal(a * integer_to_decimal(*b)?))
}
_ => Err("mul type mismatch".into()),
},
BinaryOp::Div => match (left, right) {
(RuntimeValue::Integer(a), RuntimeValue::Integer(b)) if *b != 0 => a
.checked_div(*b)
.map(RuntimeValue::Integer)
.ok_or_else(|| "integer division overflow".into()),
(RuntimeValue::Decimal(a), RuntimeValue::Decimal(b)) if *b != 0.0 => {
Ok(RuntimeValue::Decimal(a / b))
}
(RuntimeValue::Integer(a), RuntimeValue::Decimal(b)) if *b != 0.0 => {
Ok(RuntimeValue::Decimal(integer_to_decimal(*a)? / b))
}
(RuntimeValue::Decimal(a), RuntimeValue::Integer(b)) if *b != 0 => {
Ok(RuntimeValue::Decimal(a / integer_to_decimal(*b)?))
}
_ => Err("div type mismatch or division by zero".into()),
},
BinaryOp::Eq => Ok(RuntimeValue::Boolean(values_equal(left, right))),
BinaryOp::Neq => Ok(RuntimeValue::Boolean(!values_equal(left, right))),
BinaryOp::Lt | BinaryOp::Lte | BinaryOp::Gt | BinaryOp::Gte => {
compare_ordered(op, left, right).map(RuntimeValue::Boolean)
}
BinaryOp::In => match right {
RuntimeValue::List(items) => Ok(RuntimeValue::Boolean(
items.iter().any(|item| values_equal(left, item)),
)),
RuntimeValue::String(haystack) => match left.as_str() {
Some(needle) => Ok(RuntimeValue::Boolean(haystack.contains(needle))),
None if left.is_null() || left.is_missing() => Ok(RuntimeValue::Null),
None => Err("in with string right-hand side requires string left".into()),
},
_ => Err("in requires list or string on the right".into()),
},
BinaryOp::Contains => match left {
RuntimeValue::List(items) => Ok(RuntimeValue::Boolean(
items.iter().any(|item| values_equal(item, right)),
)),
RuntimeValue::String(haystack) => match right.as_str() {
Some(needle) => Ok(RuntimeValue::Boolean(haystack.contains(needle))),
None if right.is_null() || right.is_missing() => Ok(RuntimeValue::Null),
None => Err("contains with string left-hand side requires string right".into()),
},
_ => Err("contains requires list or string on the left".into()),
},
BinaryOp::And | BinaryOp::Or => unreachable!("handled by short-circuit evaluation"),
}
}
fn compare_ordered(
op: BinaryOp,
left: &RuntimeValue,
right: &RuntimeValue,
) -> Result<bool, String> {
use BinaryOp::{Gt, Gte, Lt, Lte};
let ordering = match (left, right) {
(RuntimeValue::Integer(a), RuntimeValue::Integer(b)) => a.cmp(b),
(RuntimeValue::Decimal(a), RuntimeValue::Decimal(b)) => a
.partial_cmp(b)
.ok_or_else(|| "decimal comparison failed".to_string())?,
(RuntimeValue::Integer(a), RuntimeValue::Decimal(b)) => integer_to_decimal(*a)?
.partial_cmp(b)
.ok_or_else(|| "decimal comparison failed".to_string())?,
(RuntimeValue::Decimal(a), RuntimeValue::Integer(b)) => a
.partial_cmp(&integer_to_decimal(*b)?)
.ok_or_else(|| "decimal comparison failed".to_string())?,
(RuntimeValue::String(a), RuntimeValue::String(b)) => a.cmp(b),
(RuntimeValue::Boolean(a), RuntimeValue::Boolean(b)) => a.cmp(b),
_ => return Err("comparison type mismatch".into()),
};
Ok(match op {
Lt => ordering == std::cmp::Ordering::Less,
Lte => ordering != std::cmp::Ordering::Greater,
Gt => ordering == std::cmp::Ordering::Greater,
Gte => ordering != std::cmp::Ordering::Less,
_ => return Err("invalid comparison operator".into()),
})
}