use crate::expr::{Expr, Tri};
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ExplainNode {
pub label: ExplainLabel,
pub result: Tri,
pub children: Vec<ExplainNode>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ExplainLabel {
Claim(String),
Not,
All,
Any,
}
pub fn explain<F>(expr: &Expr, lookup: &F) -> ExplainNode
where
F: Fn(&str) -> Tri,
{
match expr {
Expr::Claim(name) => ExplainNode {
label: ExplainLabel::Claim(name.clone()),
result: lookup(name),
children: vec![],
},
Expr::Not(inner) => {
let inner_node = explain(inner, lookup);
let result = !inner_node.result;
ExplainNode {
label: ExplainLabel::Not,
result,
children: vec![inner_node],
}
}
Expr::And(children) => {
let kids: Vec<ExplainNode> = children.iter().map(|c| explain(c, lookup)).collect();
let result = combine_and(kids.iter().map(|c| c.result));
ExplainNode {
label: ExplainLabel::All,
result,
children: kids,
}
}
Expr::Or(children) => {
let kids: Vec<ExplainNode> = children.iter().map(|c| explain(c, lookup)).collect();
let result = combine_or(kids.iter().map(|c| c.result));
ExplainNode {
label: ExplainLabel::Any,
result,
children: kids,
}
}
}
}
fn combine_and(iter: impl Iterator<Item = Tri>) -> Tri {
let mut all_true = true;
for t in iter {
match t {
Tri::False => return Tri::False,
Tri::True => {}
Tri::Unknown => all_true = false,
}
}
if all_true { Tri::True } else { Tri::Unknown }
}
fn combine_or(iter: impl Iterator<Item = Tri>) -> Tri {
let mut all_false = true;
for t in iter {
match t {
Tri::True => return Tri::True,
Tri::False => {}
Tri::Unknown => all_false = false,
}
}
if all_false { Tri::False } else { Tri::Unknown }
}
#[cfg(test)]
mod tests {
use super::*;
use crate::expr::parse;
fn lookup_from<'a>(claims: &'a [(&'a str, bool)]) -> impl Fn(&str) -> Tri + use<'a> {
move |name: &str| match claims.iter().find(|(n, _)| *n == name) {
Some((_, true)) => Tri::True,
Some((_, false)) => Tri::False,
None => Tri::Unknown,
}
}
#[test]
fn claim_leaf() {
let e = parse("foo").unwrap();
let node = explain(&e, &lookup_from(&[("foo", true)]));
assert_eq!(node.label, ExplainLabel::Claim("foo".into()));
assert_eq!(node.result, Tri::True);
assert!(node.children.is_empty());
}
#[test]
fn flat_all_tree_for_and_chain() {
let e = parse("a and b and c").unwrap();
let node = explain(&e, &lookup_from(&[("a", true), ("b", true), ("c", false)]));
assert_eq!(node.label, ExplainLabel::All);
assert_eq!(node.result, Tri::False);
assert_eq!(node.children.len(), 3);
assert_eq!(node.children[2].result, Tri::False);
}
#[test]
fn flat_any_tree_for_implies_or_chain() {
let e = parse("a implies b or c").unwrap();
let node = explain(&e, &lookup_from(&[("a", false), ("b", false), ("c", true)]));
assert_eq!(node.label, ExplainLabel::Any);
assert_eq!(node.result, Tri::True);
assert_eq!(node.children.len(), 3);
assert_eq!(node.children[0].label, ExplainLabel::Not);
assert_eq!(node.children[0].result, Tri::True);
assert_eq!(node.children[0].children.len(), 1);
assert_eq!(
node.children[0].children[0].label,
ExplainLabel::Claim("a".into())
);
assert_eq!(node.children[0].children[0].result, Tri::False);
}
#[test]
fn top_level_result_agrees_with_evaluate() {
let e = parse("(a and b) or (c and not d)").unwrap();
for &assignment in &[
&[("a", true), ("b", true)][..],
&[("a", false), ("c", true), ("d", false)][..],
&[("a", false), ("c", true), ("d", true)][..],
&[("a", false), ("c", false)][..],
&[][..],
] {
let look = lookup_from(assignment);
let node = explain(&e, &look);
let direct = crate::expr::evaluate(&e, &look);
assert_eq!(
node.result, direct,
"tree root vs evaluate disagreed for {assignment:?}"
);
}
}
}