use super::{CmpOp, IterEnv, Namespace, Value, WhenEnv, WhenError, WhenExpr};
use crate::scope::Scope;
pub(super) fn eval(e: &WhenExpr, env: &WhenEnv<'_>) -> Result<Value, WhenError> {
match e {
WhenExpr::Literal(v) => Ok(v.clone()),
WhenExpr::Ident { ns, name } => match ns {
Namespace::Facts => match env.facts.get(name) {
Some(f) => Ok(Value::from(f)),
None => Ok(Value::Null),
},
Namespace::Vars => match env.vars.get(name) {
Some(v) => Ok(Value::String(v.clone())),
None => Ok(Value::Null),
},
Namespace::Iter => Ok(eval_iter_value(name, env.iter.as_ref())),
},
WhenExpr::Call { ns, method, args } => match ns {
Namespace::Iter => eval_iter_call(method, args, env),
_ => Err(WhenError::Eval(format!(
"function-call evaluation not supported on namespace {ns:?}"
))),
},
WhenExpr::Not(inner) => Ok(Value::Bool(!eval(inner, env)?.truthy())),
WhenExpr::And(l, r) => {
let lv = eval(l, env)?;
if !lv.truthy() {
return Ok(Value::Bool(false));
}
Ok(Value::Bool(eval(r, env)?.truthy()))
}
WhenExpr::Or(l, r) => {
let lv = eval(l, env)?;
if lv.truthy() {
return Ok(Value::Bool(true));
}
Ok(Value::Bool(eval(r, env)?.truthy()))
}
WhenExpr::Cmp { left, op, right } => {
let lv = eval(left, env)?;
let rv = eval(right, env)?;
Ok(Value::Bool(apply_cmp(&lv, *op, &rv)?))
}
WhenExpr::Matches { left, pattern } => {
let lv = eval(left, env)?;
match lv {
Value::String(s) => Ok(Value::Bool(pattern.is_match(&s))),
other => Err(WhenError::Eval(format!(
"`matches` left-hand side must be a string; got {}",
other.type_name()
))),
}
}
WhenExpr::List(items) => {
let mut out = Vec::with_capacity(items.len());
for item in items {
out.push(eval(item, env)?);
}
Ok(Value::List(out))
}
}
}
fn eval_iter_value(name: &str, iter: Option<&IterEnv<'_>>) -> Value {
let Some(iter) = iter else {
return Value::Null;
};
match name {
"path" => Value::String(iter.path.to_string_lossy().into_owned()),
"basename" => match iter.path.file_name().and_then(|s| s.to_str()) {
Some(s) => Value::String(s.to_string()),
None => Value::Null,
},
"parent_name" => iter
.path
.parent()
.and_then(|p| p.file_name())
.and_then(|s| s.to_str())
.map_or(Value::Null, |s| Value::String(s.to_string())),
"stem" => iter
.path
.file_stem()
.and_then(|s| s.to_str())
.map_or(Value::Null, |s| Value::String(s.to_string())),
"ext" => iter
.path
.extension()
.and_then(|s| s.to_str())
.map_or(Value::Null, |s| Value::String(s.to_string())),
"is_dir" => Value::Bool(iter.is_dir),
_ => Value::Null,
}
}
fn eval_iter_call(method: &str, args: &[WhenExpr], env: &WhenEnv<'_>) -> Result<Value, WhenError> {
match method {
"has_file" => {
if args.len() != 1 {
return Err(WhenError::Eval(format!(
"iter.has_file expects exactly 1 argument; got {}",
args.len()
)));
}
let pattern = match eval(&args[0], env)? {
Value::String(s) => s,
other => {
return Err(WhenError::Eval(format!(
"iter.has_file argument must be a string; got {}",
other.type_name()
)));
}
};
Ok(Value::Bool(iter_has_file(env.iter.as_ref(), &pattern)?))
}
_ => Err(WhenError::Eval(format!(
"unknown iter method {method:?} (parser should have caught this)"
))),
}
}
fn iter_has_file(iter: Option<&IterEnv<'_>>, pattern: &str) -> Result<bool, WhenError> {
let Some(iter) = iter else {
return Ok(false);
};
if !iter.is_dir {
return Ok(false);
}
if !pattern
.chars()
.any(|c| matches!(c, '*' | '?' | '[' | ']' | '{' | '}'))
&& !pattern.starts_with('!')
{
let candidate = iter.path.join(pattern);
return Ok(iter.index.contains_file(&candidate));
}
let combined = format!("{}/{}", iter.path.to_string_lossy(), pattern);
let scope = Scope::from_patterns(std::slice::from_ref(&combined))
.map_err(|e| WhenError::Eval(format!("iter.has_file: invalid glob: {e}")))?;
Ok(iter
.index
.files()
.any(|e| scope.matches(&e.path, iter.index)))
}
fn apply_cmp(l: &Value, op: CmpOp, r: &Value) -> Result<bool, WhenError> {
use Value::{Bool, Int, List, Null, String as S};
match op {
CmpOp::Eq => Ok(values_equal(l, r)),
CmpOp::Ne => Ok(!values_equal(l, r)),
CmpOp::Lt | CmpOp::Le | CmpOp::Gt | CmpOp::Ge => match (l, r) {
(Int(a), Int(b)) => Ok(cmp_ord(a, b, op)),
(S(a), S(b)) => Ok(cmp_ord(&a.as_str(), &b.as_str(), op)),
_ => Err(WhenError::Eval(format!(
"cannot compare {} with {}",
l.type_name(),
r.type_name(),
))),
},
CmpOp::In => match r {
List(items) => Ok(items.iter().any(|x| values_equal(l, x))),
S(haystack) => match l {
S(needle) => Ok(haystack.contains(needle.as_str())),
_ => Err(WhenError::Eval(format!(
"`in` with a string right-hand side requires a string left; got {}",
l.type_name()
))),
},
_ => {
let _ = (Bool(false), Null);
Err(WhenError::Eval(format!(
"`in` right-hand side must be a list or string; got {}",
r.type_name()
)))
}
},
}
}
fn values_equal(a: &Value, b: &Value) -> bool {
match (a, b) {
(Value::Bool(x), Value::Bool(y)) => x == y,
(Value::Int(x), Value::Int(y)) => x == y,
(Value::String(x), Value::String(y)) => x == y,
(Value::Null, Value::Null) => true,
(Value::List(x), Value::List(y)) => {
x.len() == y.len() && x.iter().zip(y.iter()).all(|(a, b)| values_equal(a, b))
}
_ => false,
}
}
fn cmp_ord<T: PartialOrd>(a: &T, b: &T, op: CmpOp) -> bool {
match op {
CmpOp::Lt => a < b,
CmpOp::Le => a <= b,
CmpOp::Gt => a > b,
CmpOp::Ge => a >= b,
_ => unreachable!(),
}
}