use std::collections::VecDeque;
use tatara_lisp_eval::{MapKey, Value};
pub const WILDCARD: &str = "_";
#[must_use]
pub fn matches(pattern: &Value, value: &Value) -> bool {
match (pattern, value) {
(Value::Keyword(k), _) if k.trim_start_matches(':') == WILDCARD => true,
(Value::List(p), Value::List(v)) => {
p.len() == v.len() && p.iter().zip(v.iter()).all(|(p, v)| matches(p, v))
}
(Value::Map(p), Value::Map(v)) => p
.iter()
.all(|(k, pv)| v.get(k).is_some_and(|vv| matches(pv, vv))),
_ => data_eq(pattern, value),
}
}
#[must_use]
pub fn first_match(mailbox: &VecDeque<Value>, pattern: &Value) -> Option<usize> {
mailbox.iter().position(|m| matches(pattern, m))
}
#[must_use]
pub fn data_eq(a: &Value, b: &Value) -> bool {
match (a, b) {
(Value::Nil, Value::Nil) => true,
(Value::Bool(x), Value::Bool(y)) => x == y,
(Value::Int(x), Value::Int(y)) => x == y,
(Value::Float(x), Value::Float(y)) => x == y,
#[allow(clippy::cast_precision_loss)]
(Value::Int(x), Value::Float(y)) | (Value::Float(y), Value::Int(x)) => (*x as f64) == *y,
(Value::Str(x), Value::Str(y))
| (Value::Symbol(x), Value::Symbol(y))
| (Value::Keyword(x), Value::Keyword(y)) => x == y,
(Value::List(x), Value::List(y)) => {
x.len() == y.len() && x.iter().zip(y.iter()).all(|(a, b)| data_eq(a, b))
}
(Value::Map(x), Value::Map(y)) => {
x.len() == y.len()
&& x.iter()
.all(|(k, v): (&MapKey, &Value)| y.get(k).is_some_and(|w| data_eq(v, w)))
}
(Value::Error(x), Value::Error(y)) => x.tag == y.tag && x.message == y.message,
_ => false,
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::sync::Arc;
fn kw(s: &str) -> Value {
Value::Keyword(Arc::from(s))
}
fn list(items: Vec<Value>) -> Value {
Value::list(items)
}
#[test]
fn the_wildcard_matches_anything() {
assert!(matches(&kw("_"), &Value::Int(1)));
assert!(matches(&kw("_"), &list(vec![])));
assert!(matches(&kw("_"), &Value::Nil));
}
#[test]
fn a_list_pattern_matches_by_position_and_length() {
let p = list(vec![kw("reply"), Value::Int(7), kw("_")]);
assert!(matches(
&p,
&list(vec![kw("reply"), Value::Int(7), Value::Int(1)])
));
assert!(!matches(
&p,
&list(vec![kw("reply"), Value::Int(8), Value::Int(1)])
));
assert!(!matches(&p, &list(vec![kw("reply"), Value::Int(7)])));
}
#[test]
fn numbers_compare_the_way_blue_does() {
assert!(matches(&Value::Int(4), &Value::Float(4.0)));
assert!(!matches(&Value::Int(4), &Value::Str(Arc::from("4"))));
}
#[test]
fn the_oldest_match_wins_and_non_matches_are_skipped_not_taken() {
let mut mb = VecDeque::new();
mb.push_back(kw("noise"));
mb.push_back(list(vec![kw("ok"), Value::Int(1)]));
mb.push_back(list(vec![kw("ok"), Value::Int(2)]));
let p = list(vec![kw("ok"), kw("_")]);
assert_eq!(first_match(&mb, &p), Some(1));
assert_eq!(mb.len(), 3, "matching must not consume");
assert_eq!(first_match(&mb, &kw("absent")), None);
}
}