use std::{cmp::Ordering, collections::HashMap};
use thiserror::Error;
use crate::{
dialogue::{Value, ValueType, builder::BuildError},
host::{HostError, RepliqueHost},
parser::{
self,
expr::{BinaryOp, UnaryOp},
},
vm::EvalCtx,
};
#[derive(Debug, Clone)]
pub(crate) enum Expr {
Var(String),
Litteral(Value),
LitteralDict(HashMap<String, Expr>),
Attr {
base: Box<Expr>,
attrs: Vec<String>,
},
#[allow(unused)]
Function {
name: String,
args: Vec<Expr>,
},
Unary {
op: UnaryOp,
rhs: Box<Expr>,
},
Binary {
op: BinaryOp,
lhs: Box<Expr>,
rhs: Box<Expr>,
},
}
impl TryFrom<parser::expr::Expr> for Expr {
type Error = BuildError;
fn try_from(value: parser::expr::Expr) -> Result<Self, BuildError> {
match value {
parser::expr::Expr::Var { name } => Ok(Expr::Var(name)),
parser::expr::Expr::Function { name, args } => Ok(Expr::Function {
name: name.value,
args: args
.into_iter()
.map(|a| a.value.try_into())
.collect::<Result<_, _>>()?,
}),
parser::expr::Expr::Attr { base, attrs } => Ok(Expr::Attr {
base: Box::new(base.value.try_into()?),
attrs: attrs.into_iter().map(|s| s.value).collect(),
}),
parser::expr::Expr::Litteral { value } => Ok(Expr::Litteral(value.into())),
parser::expr::Expr::LitteralDict(value) => {
let map: HashMap<String, Expr> = value
.into_iter()
.map(|(key, val)| val.value.try_into().map(|expr| (key.value, expr)))
.collect::<Result<_, _>>()?;
Ok(Expr::LitteralDict(map))
}
parser::expr::Expr::Unary { op, rhs } => Ok(Expr::Unary {
op: op.value,
rhs: Box::new(rhs.value.try_into()?),
}),
parser::expr::Expr::Binary { op, lhs, rhs } => Ok(Expr::Binary {
op: op.value,
lhs: Box::new(lhs.value.try_into()?),
rhs: Box::new(rhs.value.try_into()?),
}),
parser::expr::Expr::Error => Err(BuildError::ErrorInExpr),
}
}
}
#[derive(Error, Debug)]
pub enum EvalError {
#[error("Uknown variable {0}")]
UnknownVariable(String),
#[error("Uknown function {0}")]
UnknownFunction(String),
#[error("A condition must be a bool, received {0}")]
NotACondition(String),
#[error("Type mismatch for operator {op}: expected {expected} and received {received}")]
TypeMismatch {
op: String,
expected: String,
received: String,
},
#[error("Division by zero")]
DivisionByZero,
#[error("Integer overflow for operator {op}")]
Overflow { op: String },
#[error("Dict has no attribute {0}")]
DictHasNoAttr(String),
#[error("Attribute {name} expected a dict or a subject and received {received}")]
AttrExpectedDict { name: String, received: String },
#[error("Host error: {0}")]
HostError(#[from] HostError),
}
impl Expr {
pub(crate) fn eval<H: RepliqueHost>(
&self,
ctx: &mut EvalCtx<'_, H>,
) -> Result<Value, EvalError> {
match self {
Expr::Var(name) => ctx
.vars
.get(name)
.cloned()
.ok_or_else(|| EvalError::UnknownVariable(name.clone())),
Expr::Attr { base, attrs } => {
let mut val = base.eval(ctx)?;
for attr in attrs {
val = match val {
Value::Dict(mut map) => map
.remove(attr)
.ok_or_else(|| EvalError::DictHasNoAttr(attr.clone()))?,
Value::String(subject) => ctx.host.feature(&subject, attr)?,
other => {
return Err(EvalError::AttrExpectedDict {
name: attr.clone(),
received: other.vtype().to_string(),
});
}
};
}
Ok(val)
}
Expr::Litteral(value) => Ok(value.clone()),
Expr::LitteralDict(map) => {
let map = map
.iter()
.map(|(key, expr)| expr.eval(ctx).map(|val| (key.clone(), val)))
.collect::<Result<_, _>>()?;
Ok(Value::Dict(map))
}
Expr::Function { name, args } => {
let args = args
.iter()
.map(|a| a.eval(ctx))
.collect::<Result<Vec<_>, _>>()?;
ctx.call(name, args).map_err(EvalError::HostError)
}
Expr::Unary { op, rhs } => unary(*op, rhs.eval(ctx)?),
Expr::Binary {
op: op @ (BinaryOp::And | BinaryOp::Or),
lhs,
rhs,
} => {
let lhs = as_bool(*op, lhs.eval(ctx)?)?;
let settled = matches!(op, BinaryOp::Or);
if lhs == settled {
return Ok(Value::Bool(settled));
}
Ok(Value::Bool(as_bool(*op, rhs.eval(ctx)?)?))
}
Expr::Binary { op, lhs, rhs } => {
let (lhs, rhs) = (lhs.eval(ctx)?, rhs.eval(ctx)?);
binary(ctx.host, *op, lhs, rhs)
}
}
}
}
fn unary(op: UnaryOp, rhs: Value) -> Result<Value, EvalError> {
match op {
UnaryOp::Not => match rhs {
Value::Bool(val) => Ok(Value::Bool(!val)),
val => Err(EvalError::TypeMismatch {
op: op.to_string(),
expected: ValueType::Bool.to_string(),
received: val.vtype().to_string(),
}),
},
UnaryOp::Neg => match rhs {
Value::Int(val) => val
.checked_neg()
.map(Value::Int)
.ok_or(EvalError::Overflow { op: op.to_string() }),
Value::Float(val) => Ok(Value::Float(-val)),
val => Err(EvalError::TypeMismatch {
op: op.to_string(),
expected: format!("{} or {}", ValueType::Int, ValueType::Float),
received: val.vtype().to_string(),
}),
},
}
}
fn binary<H: RepliqueHost>(
host: &mut H,
op: BinaryOp,
lhs: Value,
rhs: Value,
) -> Result<Value, EvalError> {
match op {
BinaryOp::Or => Ok(Value::Bool(as_bool(op, lhs)? || as_bool(op, rhs)?)),
BinaryOp::And => Ok(Value::Bool(as_bool(op, lhs)? && as_bool(op, rhs)?)),
BinaryOp::Eq => Ok(Value::Bool(equals(op, lhs, rhs)?)),
BinaryOp::Ne => Ok(Value::Bool(!equals(op, lhs, rhs)?)),
BinaryOp::Lt => Ok(Value::Bool(matches!(
compare(op, lhs, rhs)?,
Ordering::Less
))),
BinaryOp::Le => Ok(Value::Bool(matches!(
compare(op, lhs, rhs)?,
Ordering::Less | Ordering::Equal
))),
BinaryOp::Gt => Ok(Value::Bool(matches!(
compare(op, lhs, rhs)?,
Ordering::Greater
))),
BinaryOp::Ge => Ok(Value::Bool(matches!(
compare(op, lhs, rhs)?,
Ordering::Greater | Ordering::Equal
))),
BinaryOp::Has => Ok(Value::Bool(has(host, op, lhs, rhs)?)),
BinaryOp::Add | BinaryOp::Sub | BinaryOp::Mul | BinaryOp::Div => arith(op, lhs, rhs),
}
}
fn has<H: RepliqueHost>(
host: &mut H,
op: BinaryOp,
lhs: Value,
rhs: Value,
) -> Result<bool, EvalError> {
match (lhs, rhs) {
(Value::Dict(map), Value::String(key)) => Ok(map.contains_key(&key)),
(Value::String(subject), Value::String(feature)) => {
Ok(host.has_feature(&subject, &feature)?)
}
(Value::Dict(_) | Value::String(_), rhs) => Err(EvalError::TypeMismatch {
op: op.to_string(),
expected: ValueType::String.to_string(),
received: rhs.vtype().to_string(),
}),
(lhs, _) => Err(EvalError::TypeMismatch {
op: op.to_string(),
expected: format!("{} or {}", ValueType::Dict, ValueType::String),
received: lhs.vtype().to_string(),
}),
}
}
enum Nums {
Ints(i64, i64),
Floats(f64, f64),
}
impl Nums {
fn floats(self) -> (f64, f64) {
match self {
Self::Ints(lhs, rhs) => (lhs as f64, rhs as f64),
Self::Floats(lhs, rhs) => (lhs, rhs),
}
}
}
fn nums(op: BinaryOp, lhs: Value, rhs: Value) -> Result<Nums, EvalError> {
match (lhs, rhs) {
(Value::Int(lhs), Value::Int(rhs)) => Ok(Nums::Ints(lhs, rhs)),
(Value::Float(lhs), Value::Float(rhs)) => Ok(Nums::Floats(lhs, rhs)),
(Value::Int(lhs), Value::Float(rhs)) => Ok(Nums::Floats(lhs as f64, rhs)),
(Value::Float(lhs), Value::Int(rhs)) => Ok(Nums::Floats(lhs, rhs as f64)),
(lhs, rhs) => {
let culprit = match lhs {
Value::Int(_) | Value::Float(_) => rhs,
lhs => lhs,
};
Err(EvalError::TypeMismatch {
op: op.to_string(),
expected: format!("{} or {}", ValueType::Int, ValueType::Float),
received: culprit.vtype().to_string(),
})
}
}
}
fn arith(op: BinaryOp, lhs: Value, rhs: Value) -> Result<Value, EvalError> {
let nums = nums(op, lhs, rhs)?;
if matches!(op, BinaryOp::Div) {
let (lhs, rhs) = nums.floats();
if rhs == 0.0 {
return Err(EvalError::DivisionByZero);
}
return Ok(Value::Float(lhs / rhs));
}
match nums {
Nums::Ints(lhs, rhs) => match op {
BinaryOp::Add => lhs.checked_add(rhs),
BinaryOp::Sub => lhs.checked_sub(rhs),
BinaryOp::Mul => lhs.checked_mul(rhs),
_ => unreachable!("only `+`, `-`, `*` and `/` reach here"),
}
.map(Value::Int)
.ok_or(EvalError::Overflow { op: op.to_string() }),
Nums::Floats(lhs, rhs) => Ok(Value::Float(match op {
BinaryOp::Add => lhs + rhs,
BinaryOp::Sub => lhs - rhs,
BinaryOp::Mul => lhs * rhs,
_ => unreachable!("only `+`, `-`, `*` and `/` reach here"),
})),
}
}
fn equals(op: BinaryOp, lhs: Value, rhs: Value) -> Result<bool, EvalError> {
match (lhs, rhs) {
(Value::Bool(lhs), Value::Bool(rhs)) => Ok(lhs == rhs),
(Value::String(lhs), Value::String(rhs)) => Ok(lhs == rhs),
(Value::Dict(lhs), Value::Dict(rhs)) => Ok(lhs == rhs),
(lhs, rhs) if lhs.vtype().is_number() && rhs.vtype().is_number() => {
Ok(match nums(op, lhs, rhs)? {
Nums::Ints(lhs, rhs) => lhs == rhs,
Nums::Floats(lhs, rhs) => lhs == rhs,
})
}
(lhs, rhs) => Err(EvalError::TypeMismatch {
op: op.to_string(),
expected: lhs.vtype().to_string(),
received: rhs.vtype().to_string(),
}),
}
}
fn compare(op: BinaryOp, lhs: Value, rhs: Value) -> Result<Ordering, EvalError> {
Ok(match nums(op, lhs, rhs)? {
Nums::Ints(lhs, rhs) => lhs.cmp(&rhs),
Nums::Floats(lhs, rhs) => lhs.total_cmp(&rhs),
})
}
fn as_bool(op: BinaryOp, value: Value) -> Result<bool, EvalError> {
match value {
Value::Bool(val) => Ok(val),
val => Err(EvalError::TypeMismatch {
op: op.to_string(),
expected: ValueType::Bool.to_string(),
received: val.vtype().to_string(),
}),
}
}
#[cfg(test)]
mod tests {
use crate::{
host::{NoHost, test::TestHost},
vm::VarStore,
};
use super::*;
fn bin(op: BinaryOp, lhs: Value, rhs: Value) -> Result<Value, EvalError> {
binary(&mut NoHost, op, lhs, rhs)
}
fn feature(subject: &str, feature: &str) -> Result<Value, EvalError> {
binary(
&mut TestHost::default(),
BinaryOp::Has,
str(subject),
str(feature),
)
}
fn eval_with<H: RepliqueHost>(expr: &Expr, host: &mut H) -> Result<Value, EvalError> {
expr.eval(&mut EvalCtx {
vars: &VarStore::default(),
host,
})
}
fn call(name: &str, args: Vec<Expr>) -> Expr {
Expr::Function {
name: name.to_owned(),
args,
}
}
fn int(value: i64) -> Expr {
Expr::Litteral(Value::Int(value))
}
fn str(text: &str) -> Value {
Value::String(text.to_owned())
}
fn dict(entries: &[(&str, Value)]) -> Value {
Value::Dict(
entries
.iter()
.map(|(key, val)| ((*key).to_owned(), val.clone()))
.collect(),
)
}
fn logic(op: BinaryOp, lhs: Expr, rhs: Expr) -> Expr {
Expr::Binary {
op,
lhs: Box::new(lhs),
rhs: Box::new(rhs),
}
}
fn boolean(value: bool) -> Expr {
Expr::Litteral(Value::Bool(value))
}
#[test]
fn and_and_or_do_not_evaluate_a_right_hand_side_that_cannot_change_the_answer() {
for (op, lhs) in [(BinaryOp::And, false), (BinaryOp::Or, true)] {
let mut host = TestHost::default();
let value = eval_with(&logic(op, boolean(lhs), call("boom", vec![])), &mut host);
assert_eq!(value.unwrap(), Value::Bool(lhs), "{op}");
assert_eq!(host.called(), [] as [&str; 0], "{op}");
}
}
#[test]
fn and_and_or_evaluate_the_right_hand_side_when_the_left_one_does_not_settle() {
for (op, lhs, n, expected) in [
(BinaryOp::And, true, 2, true),
(BinaryOp::And, true, 3, false),
(BinaryOp::Or, false, 2, true),
(BinaryOp::Or, false, 3, false),
] {
let mut host = TestHost::default();
let value = eval_with(
&logic(op, boolean(lhs), call("is_even", vec![int(n)])),
&mut host,
);
assert_eq!(
value.unwrap(),
Value::Bool(expected),
"{lhs} {op} is_even({n})"
);
assert_eq!(host.called(), ["is_even"]);
}
}
#[test]
fn error_on_and_or_whose_side_is_not_a_bool() {
for op in [BinaryOp::And, BinaryOp::Or] {
let left = eval_with(&logic(op, int(1), boolean(true)), &mut NoHost);
assert!(matches!(left, Err(EvalError::TypeMismatch { .. })), "{op}");
let reached = matches!(op, BinaryOp::And);
let right = eval_with(&logic(op, boolean(reached), int(1)), &mut NoHost);
assert!(matches!(right, Err(EvalError::TypeMismatch { .. })), "{op}");
}
}
#[test]
fn a_function_is_answered_by_the_host() {
let mut host = TestHost::default();
let value = eval_with(
&call("upper_new", vec![Expr::Litteral(str("alice"))]),
&mut host,
);
assert_eq!(value.unwrap(), str("ALICE"));
assert_eq!(host.calls, [("upper_new".to_owned(), vec![str("alice")])]);
}
#[test]
fn the_arguments_of_a_function_reach_the_host_evaluated() {
let mut host = TestHost::default();
let arg = Expr::Binary {
op: BinaryOp::Add,
lhs: Box::new(int(1)),
rhs: Box::new(int(2)),
};
let value = eval_with(&call("double", vec![arg]), &mut host);
assert_eq!(value.unwrap(), Value::Int(6));
assert_eq!(host.calls, [("double".to_owned(), vec![Value::Int(3)])]);
}
#[test]
fn a_function_is_a_value_like_any_other() {
let mut host = TestHost::default();
let expr = Expr::Binary {
op: BinaryOp::Add,
lhs: Box::new(call("double", vec![call("double", vec![int(2)])])),
rhs: Box::new(int(1)),
};
assert_eq!(eval_with(&expr, &mut host).unwrap(), Value::Int(9));
assert_eq!(
host.calls,
[
("double".to_owned(), vec![Value::Int(2)]),
("double".to_owned(), vec![Value::Int(4)]),
]
);
}
#[test]
fn a_variable_reaches_the_host_as_its_value() {
let vars = VarStore::new(HashMap::from([("name".to_owned(), str("bob"))]));
let mut host = TestHost::default();
let value = call("upper", vec![Expr::Var("name".to_owned())]).eval(&mut EvalCtx {
vars: &vars,
host: &mut host,
});
assert_eq!(value.unwrap(), str("BOB"));
}
#[test]
fn error_on_a_function_the_host_does_not_know() {
let mut host = TestHost::default();
let err = eval_with(&call("unknown", vec![]), &mut host).unwrap_err();
assert!(matches!(
err,
EvalError::HostError(HostError::UnknownFunction(name)) if name == "unknown"
));
}
#[test]
fn error_on_a_function_the_host_refuses_to_answer() {
let mut host = TestHost::default();
let err = eval_with(&call("boom", vec![]), &mut host).unwrap_err();
assert!(matches!(
err,
EvalError::HostError(HostError::Failed { ref name, .. }) if name == "boom"
));
}
#[test]
fn no_host_answers_no_function_at_all() {
let err = eval_with(
&call("upper_new", vec![Expr::Litteral(str("A"))]),
&mut NoHost,
)
.unwrap_err();
assert!(matches!(
err,
EvalError::HostError(HostError::UnknownFunction(_))
));
}
#[test]
fn an_argument_that_does_not_evaluate_never_reaches_the_host() {
let mut host = TestHost::default();
let err = eval_with(
&call("double", vec![Expr::Var("missing".to_owned())]),
&mut host,
)
.unwrap_err();
assert!(matches!(err, EvalError::UnknownVariable(_)));
assert_eq!(host.called(), [] as [&str; 0]);
}
#[test]
fn two_dicts_are_equal_when_they_hold_the_same_entries() {
let left = dict(&[("hp", Value::Int(1)), ("name", str("Leon"))]);
let right = dict(&[("name", str("Leon")), ("hp", Value::Int(1))]);
assert_eq!(
bin(BinaryOp::Eq, left.clone(), right).unwrap(),
Value::Bool(true)
);
assert_eq!(
bin(BinaryOp::Eq, left.clone(), dict(&[("hp", Value::Int(2))])).unwrap(),
Value::Bool(false)
);
assert_eq!(
bin(BinaryOp::Ne, left.clone(), dict(&[])).unwrap(),
Value::Bool(true)
);
}
#[test]
fn a_dict_compared_to_anything_else_is_a_type_mismatch() {
let left = dict(&[("hp", Value::Int(1))]);
assert!(matches!(
bin(BinaryOp::Eq, left.clone(), Value::Int(1)),
Err(EvalError::TypeMismatch { .. })
));
assert!(matches!(
bin(BinaryOp::Add, left, Value::Int(1)),
Err(EvalError::TypeMismatch { .. })
));
}
#[test]
fn an_attr_reads_its_way_down_the_dicts() {
let vars = HashMap::from([(
"player".to_owned(),
dict(&[("stats", dict(&[("hp", Value::Int(12))]))]),
)]);
let mut ctx = EvalCtx {
vars: &VarStore::new(vars),
host: &mut NoHost,
};
let attr = |attrs: &[&str]| Expr::Attr {
base: Box::new(Expr::Var("player".to_owned())),
attrs: attrs.iter().map(|a| (*a).to_owned()).collect(),
};
assert_eq!(
attr(&["stats", "hp"]).eval(&mut ctx).unwrap(),
Value::Int(12)
);
assert!(matches!(
attr(&["stats", "mp"]).eval(&mut ctx),
Err(EvalError::DictHasNoAttr(_))
));
assert!(matches!(
attr(&["stats", "hp", "deeper"]).eval(&mut ctx),
Err(EvalError::AttrExpectedDict { .. })
));
}
#[test]
fn and_and_or_take_booleans_only() {
use BinaryOp::{And, Or};
assert_eq!(
bin(And, Value::Bool(true), Value::Bool(false)).unwrap(),
Value::Bool(false)
);
assert_eq!(
bin(Or, Value::Bool(true), Value::Bool(false)).unwrap(),
Value::Bool(true)
);
assert!(matches!(
bin(And, Value::Int(1), Value::Bool(true)),
Err(EvalError::TypeMismatch { .. })
));
}
#[test]
fn an_int_meeting_a_float_is_promoted() {
assert_eq!(
bin(BinaryOp::Add, Value::Int(1), Value::Float(0.5)).unwrap(),
Value::Float(1.5)
);
assert_eq!(
bin(BinaryOp::Eq, Value::Int(1), Value::Float(1.0)).unwrap(),
Value::Bool(true)
);
assert_eq!(
bin(BinaryOp::Lt, Value::Int(1), Value::Float(1.5)).unwrap(),
Value::Bool(true)
);
}
#[test]
fn arithmetic_on_two_ints_stays_an_int() {
assert_eq!(
bin(BinaryOp::Add, Value::Int(2), Value::Int(3)).unwrap(),
Value::Int(5)
);
assert_eq!(
bin(BinaryOp::Sub, Value::Int(2), Value::Int(3)).unwrap(),
Value::Int(-1)
);
assert_eq!(
bin(BinaryOp::Mul, Value::Int(2), Value::Int(3)).unwrap(),
Value::Int(6)
);
}
#[test]
fn a_division_always_gives_a_float() {
assert_eq!(
bin(BinaryOp::Div, Value::Int(7), Value::Int(2)).unwrap(),
Value::Float(3.5)
);
}
#[test]
fn a_string_is_never_turned_into_a_number() {
let err = bin(BinaryOp::Add, str("2"), Value::Int(2)).unwrap_err();
assert!(matches!(
err,
EvalError::TypeMismatch { ref received, .. } if received == "string"
));
}
#[test]
fn two_types_that_have_nothing_in_common_do_not_compare() {
assert!(matches!(
bin(BinaryOp::Eq, Value::Bool(true), str("true")),
Err(EvalError::TypeMismatch { .. })
));
assert!(matches!(
bin(BinaryOp::Lt, str("a"), str("b")),
Err(EvalError::TypeMismatch { .. })
));
}
#[test]
fn values_of_the_same_type_compare_as_expected() {
assert_eq!(
bin(BinaryOp::Eq, str("a"), str("a")).unwrap(),
Value::Bool(true)
);
assert_eq!(
bin(BinaryOp::Ne, Value::Bool(true), Value::Bool(false)).unwrap(),
Value::Bool(true)
);
assert_eq!(
bin(BinaryOp::Ge, Value::Int(2), Value::Int(2)).unwrap(),
Value::Bool(true)
);
}
#[test]
fn error_on_a_division_by_zero() {
assert!(matches!(
bin(BinaryOp::Div, Value::Int(1), Value::Int(0)),
Err(EvalError::DivisionByZero)
));
assert!(matches!(
bin(BinaryOp::Div, Value::Float(1.0), Value::Float(0.0)),
Err(EvalError::DivisionByZero)
));
}
#[test]
fn error_on_an_integer_overflow() {
assert!(matches!(
bin(BinaryOp::Add, Value::Int(i64::MAX), Value::Int(1)),
Err(EvalError::Overflow { .. })
));
assert!(matches!(
unary(UnaryOp::Neg, Value::Int(i64::MIN)),
Err(EvalError::Overflow { .. })
));
}
#[test]
fn a_unary_operator_takes_what_it_can_use() {
assert_eq!(
unary(UnaryOp::Not, Value::Bool(true)).unwrap(),
Value::Bool(false)
);
assert_eq!(unary(UnaryOp::Neg, Value::Int(2)).unwrap(), Value::Int(-2));
assert_eq!(
unary(UnaryOp::Neg, Value::Float(0.5)).unwrap(),
Value::Float(-0.5)
);
assert!(matches!(
unary(UnaryOp::Not, Value::Int(1)),
Err(EvalError::TypeMismatch { .. })
));
}
#[test]
fn has_finds_the_keys_of_a_dict() {
let stats = dict(&[("strength", Value::Int(12))]);
assert_eq!(
bin(BinaryOp::Has, stats.clone(), str("strength")).unwrap(),
Value::Bool(true)
);
assert_eq!(
bin(BinaryOp::Has, stats, str("magic")).unwrap(),
Value::Bool(false)
);
assert_eq!(
bin(BinaryOp::Has, dict(&[]), str("strength")).unwrap(),
Value::Bool(false)
);
}
#[test]
fn has_does_not_look_at_the_value_under_the_key() {
let stats = dict(&[
("zero", Value::Int(0)),
("off", Value::Bool(false)),
("blank", str("")),
("empty", dict(&[])),
]);
for key in ["zero", "off", "blank", "empty"] {
assert_eq!(
bin(BinaryOp::Has, stats.clone(), str(key)).unwrap(),
Value::Bool(true),
"{key}"
);
}
}
#[test]
fn has_is_case_sensitive() {
let stats = dict(&[("strength", Value::Int(12))]);
assert_eq!(
bin(BinaryOp::Has, stats, str("Strength")).unwrap(),
Value::Bool(false)
);
}
#[test]
fn has_does_not_search_the_nested_dicts() {
let player = dict(&[("stats", dict(&[("strength", Value::Int(12))]))]);
assert_eq!(
bin(BinaryOp::Has, player.clone(), str("stats")).unwrap(),
Value::Bool(true)
);
assert_eq!(
bin(BinaryOp::Has, player, str("strength")).unwrap(),
Value::Bool(false)
);
}
#[test]
fn error_on_has_asked_of_what_is_neither_a_dict_nor_a_string() {
for lhs in [Value::Int(1), Value::Bool(true), Value::Float(0.5)] {
let received = lhs.vtype().to_string();
let err = bin(BinaryOp::Has, lhs, str("strength")).unwrap_err();
assert!(
matches!(
&err,
EvalError::TypeMismatch { op, expected, received: got }
if op == "has" && expected == "dict or string" && *got == received
),
"{err}"
);
}
}
#[test]
fn error_on_a_key_that_is_not_a_string() {
let err = bin(BinaryOp::Has, dict(&[]), Value::Int(1)).unwrap_err();
assert!(
matches!(
&err,
EvalError::TypeMismatch { op, expected, received }
if op == "has" && expected == "string" && received == "int"
),
"{err}"
);
}
#[test]
fn error_on_a_feature_that_is_not_a_string() {
let err = bin(BinaryOp::Has, str("Alice"), Value::Int(1)).unwrap_err();
assert!(
matches!(
&err,
EvalError::TypeMismatch { op, expected, received }
if op == "has" && expected == "string" && received == "int"
),
"{err}"
);
}
#[test]
fn has_asks_the_host_for_the_features_of_a_subject() {
assert_eq!(feature("Alice", "happy").unwrap(), Value::Bool(true));
assert_eq!(feature("Alice", "sad").unwrap(), Value::Bool(false));
assert_eq!(feature("Bob", "sad").unwrap(), Value::Bool(true));
assert_eq!(feature("Bob", "happy").unwrap(), Value::Bool(false));
assert_eq!(feature("Alice", "hungry").unwrap(), Value::Bool(true));
assert_eq!(feature("Bob", "hungry").unwrap(), Value::Bool(true));
}
#[test]
fn a_feature_no_subject_has_is_still_a_feature() {
assert_eq!(feature("Alice", "angry").unwrap(), Value::Bool(false));
assert_eq!(feature("Bob", "angry").unwrap(), Value::Bool(false));
}
#[test]
fn error_on_a_feature_the_host_does_not_know() {
let err = feature("Alice", "sleepy").unwrap_err();
assert!(
matches!(
&err,
EvalError::HostError(HostError::UnknownFeature(name)) if name == "sleepy"
),
"{err}"
);
}
#[test]
fn error_on_a_subject_the_host_does_not_know() {
let err = feature("Caroline", "happy").unwrap_err();
assert!(
matches!(
&err,
EvalError::HostError(HostError::UnknownSubject(name)) if name == "Caroline"
),
"{err}"
);
}
#[test]
fn has_on_a_dict_never_reaches_the_host() {
let value = binary(
&mut TestHost::default(),
BinaryOp::Has,
dict(&[("sleepy", Value::Bool(true))]),
str("sleepy"),
);
assert_eq!(value.unwrap(), Value::Bool(true));
}
#[test]
fn no_host_knows_no_feature() {
assert!(matches!(
bin(BinaryOp::Has, str("Alice"), str("happy")),
Err(EvalError::HostError(HostError::UnknownFeature(name))) if name == "happy"
));
}
fn feature_of(base: Expr, attrs: &[&str]) -> Expr {
Expr::Attr {
base: Box::new(base),
attrs: attrs.iter().map(|attr| (*attr).to_owned()).collect(),
}
}
#[test]
fn an_attr_on_a_subject_reads_its_feature_from_the_host() {
let alice = || Expr::Litteral(str("Alice"));
let mut host = TestHost::default();
assert_eq!(
eval_with(&feature_of(alice(), &["mana"]), &mut host).unwrap(),
Value::Int(12)
);
assert_eq!(
eval_with(&feature_of(alice(), &["stats", "hp"]), &mut host).unwrap(),
Value::Int(7)
);
}
#[test]
fn the_subject_of_an_attr_can_come_from_a_variable() {
let vars = VarStore::new(HashMap::from([("who".to_owned(), str("Alice"))]));
let mut host = TestHost::default();
let value = feature_of(Expr::Var("who".to_owned()), &["mana"]).eval(&mut EvalCtx {
vars: &vars,
host: &mut host,
});
assert_eq!(value.unwrap(), Value::Int(12));
}
#[test]
fn error_on_a_feature_the_host_cannot_give() {
let read = |subject: &str, feature: &str| {
eval_with(
&feature_of(Expr::Litteral(str(subject)), &[feature]),
&mut TestHost::default(),
)
};
assert!(matches!(
read("Bob", "mana"),
Err(EvalError::HostError(HostError::FeatureNotOnSubject { .. }))
));
assert!(matches!(
read("Alice", "luck"),
Err(EvalError::HostError(HostError::UnknownFeature(_)))
));
assert!(matches!(
read("Zoe", "mana"),
Err(EvalError::HostError(HostError::UnknownSubject(_)))
));
assert!(matches!(
eval_with(
&feature_of(Expr::Litteral(str("Alice")), &["mana"]),
&mut NoHost
),
Err(EvalError::HostError(HostError::UnknownFeature(_)))
));
}
#[test]
fn error_on_an_attr_read_on_what_is_neither_a_dict_nor_a_subject() {
assert!(matches!(
eval_with(&feature_of(int(3), &["mana"]), &mut TestHost::default()),
Err(EvalError::AttrExpectedDict { .. })
));
}
#[test]
fn has_reads_its_subject_and_its_feature_from_the_variables() {
let vars = VarStore::new(HashMap::from([
("who".to_owned(), str("Bob")),
("mood".to_owned(), str("sad")),
]));
let expr = Expr::Binary {
op: BinaryOp::Has,
lhs: Box::new(Expr::Var("who".to_owned())),
rhs: Box::new(Expr::Var("mood".to_owned())),
};
let value = expr.eval(&mut EvalCtx {
vars: &vars,
host: &mut TestHost::default(),
});
assert_eq!(value.unwrap(), Value::Bool(true));
}
#[test]
fn has_reads_its_dict_and_its_key_from_the_variables() {
let vars = VarStore::new(HashMap::from([
("stats".to_owned(), dict(&[("strength", Value::Int(12))])),
("key".to_owned(), str("strength")),
]));
let expr = Expr::Binary {
op: BinaryOp::Has,
lhs: Box::new(Expr::Var("stats".to_owned())),
rhs: Box::new(Expr::Var("key".to_owned())),
};
let value = expr.eval(&mut EvalCtx {
vars: &vars,
host: &mut NoHost,
});
assert_eq!(value.unwrap(), Value::Bool(true));
}
#[test]
fn has_reads_a_dict_the_host_answers_with() {
struct StatsHost;
impl RepliqueHost for StatsHost {
fn call(&mut self, name: &str, _: Vec<Value>) -> Result<Value, HostError> {
match name {
"get_stats" => Ok(Value::Dict(HashMap::from([(
"strength".to_owned(),
Value::Int(12),
)]))),
_ => Err(HostError::UnknownFunction(name.to_owned())),
}
}
fn has_feature(&mut self, _: &str, _: &str) -> Result<bool, HostError> {
Ok(false)
}
fn feature(&mut self, _: &str, name: &str) -> Result<Value, HostError> {
Err(HostError::UnknownFeature(name.to_owned()))
}
}
let has = |key: &str| Expr::Binary {
op: BinaryOp::Has,
lhs: Box::new(call("get_stats", vec![])),
rhs: Box::new(Expr::Litteral(str(key))),
};
assert_eq!(
eval_with(&has("strength"), &mut StatsHost).unwrap(),
Value::Bool(true)
);
assert_eq!(
eval_with(&has("magic"), &mut StatsHost).unwrap(),
Value::Bool(false)
);
}
}