use crate::ast::Expr;
use crate::lower_formula::{lower_formula, Bindings};
use crate::{Diagnostics, Parser, Problem, Sig};
use delhi_mb::State;
pub const MAX_CANDIDATES: usize = 20_000;
pub const HOLE: &str = "_";
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Answer {
pub matches: Vec<String>,
pub considered: usize,
pub truncated: bool,
}
pub fn modal_literals(sig: &Sig, depth: usize) -> Vec<String> {
let mut level: Vec<String> = Vec::new();
for a in 0..sig.n_atoms() {
let name = sig.atom_name(a as u32);
level.push(name.to_string());
level.push(format!("!{name}"));
}
let mut all = level.clone();
for _ in 0..depth {
let mut next = Vec::new();
for inner in &level {
for i in 0..sig.n_agents() {
let who = sig.agent_name(i as u32);
next.push(format!("K[{who}] {inner}"));
next.push(format!("B[{who}] {inner}"));
}
if all.len() + next.len() > MAX_CANDIDATES {
break;
}
}
all.extend(next.iter().cloned());
level = next;
if all.len() > MAX_CANDIDATES {
break;
}
}
all.truncate(MAX_CANDIDATES);
all
}
fn fill(pattern: &Expr, filler: &Expr) -> Expr {
match pattern {
Expr::Hole(_) => filler.clone(),
Expr::True(_) | Expr::False(_) | Expr::Atom(_) => pattern.clone(),
Expr::Not(a, s) => Expr::Not(Box::new(fill(a, filler)), *s),
Expr::And(a, b, s) => Expr::And(Box::new(fill(a, filler)), Box::new(fill(b, filler)), *s),
Expr::Or(a, b, s) => Expr::Or(Box::new(fill(a, filler)), Box::new(fill(b, filler)), *s),
Expr::Implies(a, b, s) => {
Expr::Implies(Box::new(fill(a, filler)), Box::new(fill(b, filler)), *s)
}
Expr::Modality { op, agents, cond, body, span } => Expr::Modality {
op: op.clone(),
agents: agents.clone(),
cond: cond.as_ref().map(|c| Box::new(fill(c, filler))),
body: Box::new(fill(body, filler)),
span: *span,
},
}
}
fn has_hole(e: &Expr) -> bool {
match e {
Expr::Hole(_) => true,
Expr::True(_) | Expr::False(_) | Expr::Atom(_) => false,
Expr::Not(a, _) => has_hole(a),
Expr::And(a, b, _) | Expr::Or(a, b, _) | Expr::Implies(a, b, _) => {
has_hole(a) || has_hole(b)
}
Expr::Modality { cond, body, .. } => {
cond.as_ref().is_some_and(|c| has_hole(c)) || has_hole(body)
}
}
}
fn parse(text: &str) -> (Expr, Diagnostics) {
let mut diags = Diagnostics::default();
let toks = crate::lex(text, &mut diags);
let expr = Parser::new(&toks).parse_expr(&mut diags);
(expr, diags)
}
fn hole_spans(pattern: &str) -> Vec<(usize, usize)> {
let mut diags = Diagnostics::default();
crate::lex(pattern, &mut diags)
.iter()
.filter(|t| t.tok == crate::Tok::Hole)
.map(|t| (t.span.start, t.span.end))
.collect()
}
fn render(pattern: &str, holes: &[(usize, usize)], candidate: &str) -> String {
let mut out = String::with_capacity(pattern.len() + candidate.len());
let mut last = 0;
for &(start, end) in holes {
out.push_str(&pattern[last..start]);
out.push('(');
out.push_str(candidate);
out.push(')');
last = end;
}
out.push_str(&pattern[last..]);
out
}
fn complement(candidate: &str) -> String {
let (prefix, lit) = match candidate.rfind("] ") {
Some(i) => candidate.split_at(i + 2),
None => ("", candidate),
};
match lit.strip_prefix('!') {
Some(rest) => format!("{prefix}{rest}"),
None => format!("{prefix}!{lit}"),
}
}
pub fn ask(p: &mut Problem, state: &State, pattern: &str, depth: usize) -> Result<Answer, String> {
let (pat, mut diags) = parse(pattern);
if !diags.is_empty() {
return Err(diags.render(pattern));
}
let pat = crate::expand(&pat, &p.defs, &mut diags);
if !diags.is_empty() {
return Err(diags.render(pattern));
}
if !has_hole(&pat) {
return Err(format!(
"the pattern needs a `{HOLE}` to fill — try `B[agent] {HOLE}`, or `{HOLE}` on its own"
));
}
let holes = hole_spans(pattern);
let candidates = modal_literals(&p.sig, depth);
if candidates.is_empty() {
return Ok(Answer { matches: Vec::new(), considered: 0, truncated: false });
}
let parsed: Vec<Expr> = candidates.iter().map(|c| parse(c).0).collect();
let mut probe = Diagnostics::default();
let first = fill(&pat, &parsed[0]);
let _ =
lower_formula(&first, &p.sig, &p.consts, &Bindings::default(), &mut p.store, &mut probe);
if !probe.is_empty() {
return Err(probe.render(pattern));
}
let mut hit: std::collections::HashSet<&str> = std::collections::HashSet::new();
let mut order: Vec<&String> = Vec::new();
for (c, tree) in candidates.iter().zip(&parsed) {
let mut quiet = Diagnostics::default();
let f = lower_formula(
&fill(&pat, tree),
&p.sig,
&p.consts,
&Bindings::default(),
&mut p.store,
&mut quiet,
);
if quiet.is_empty() && state.entails(&p.store, f) {
hit.insert(c.as_str());
order.push(c);
}
}
let matches = order
.into_iter()
.filter(|c| {
let is_negative = complement(c).len() < c.len();
!(is_negative && hit.contains(complement(c).as_str()))
})
.map(|c| render(pattern, &holes, c))
.collect();
Ok(Answer {
matches,
considered: candidates.len(),
truncated: candidates.len() >= MAX_CANDIDATES,
})
}
#[cfg(test)]
mod tests {
use super::*;
const COIN: &str = r#"
types{ Actor - Object } objects{ a, b - Actor } agents{ a, b } props{ h }
initially { h, ?[a] h, B[a] h }
actions {}
"#;
fn problem(src: &str) -> (Problem, State) {
let p = Problem::parse(src).unwrap_or_else(|e| panic!("{e}"));
let s = p.state.clone();
(p, s)
}
#[test]
fn candidate_count_follows_the_signature_and_the_depth() {
let (p, _) = problem(COIN);
assert_eq!(modal_literals(&p.sig, 0).len(), 2);
assert_eq!(modal_literals(&p.sig, 1).len(), 2 + 8);
assert_eq!(modal_literals(&p.sig, 2).len(), 2 + 8 + 32);
}
#[test]
fn candidates_are_ordered_shallowest_first() {
let (p, _) = problem(COIN);
let c = modal_literals(&p.sig, 2);
let depth_of = |s: &str| s.matches('[').count();
let depths: Vec<usize> = c.iter().map(|s| depth_of(s)).collect();
assert!(depths.windows(2).all(|w| w[0] <= w[1]), "not shallowest-first: {depths:?}");
}
#[test]
fn asking_what_an_agent_believes_separates_belief_from_knowledge() {
let (mut p, s) = problem(COIN);
let believes_a = ask(&mut p, &s, "B[a] _", 0).expect("valid pattern");
assert_eq!(believes_a.matches, vec!["B[a] (h)"]);
let knows_a = ask(&mut p, &s, "K[a] _", 0).expect("valid pattern");
assert!(knows_a.matches.is_empty(), "a knows nothing here: {:?}", knows_a.matches);
let knows_b = ask(&mut p, &s, "K[b] _", 0).expect("valid pattern");
assert_eq!(knows_b.matches, vec!["K[b] (h)"]);
}
#[test]
fn asking_what_an_agent_is_ignorant_of_reports_the_atom_once() {
let (mut p, s) = problem(COIN);
let ignorant = ask(&mut p, &s, "?[a] _", 0).expect("valid pattern");
assert!(ignorant.matches.iter().any(|m| m.contains("(h)")), "got {:?}", ignorant.matches);
assert!(
!ignorant.matches.iter().any(|m| m.contains("(!h)")),
"the negated twin is redundant: {:?}",
ignorant.matches
);
}
#[test]
fn depth_reaches_nested_attitudes_that_depth_zero_cannot() {
let (mut p, s) = problem(COIN);
let shallow = ask(&mut p, &s, "K[b] _", 0).expect("ok");
assert!(!shallow.matches.iter().any(|m| m.contains("B[a]")));
let deep = ask(&mut p, &s, "K[b] _", 1).expect("ok");
assert!(deep.matches.iter().any(|m| m == "K[b] (B[a] h)"), "got {:?}", deep.matches);
assert!(deep.considered > shallow.considered);
}
#[test]
fn a_bare_hole_enumerates_what_simply_holds() {
let (mut p, s) = problem(COIN);
let a = ask(&mut p, &s, "_", 0).expect("ok");
assert_eq!(a.matches, vec!["(h)"], "h is true, !h is not");
}
#[test]
fn an_underscore_inside_an_identifier_is_not_a_hole() {
let src = r#"
types{ Actor - Object } objects{ a - Actor } agents{ a }
props{ at_park, mary_home }
initially { at_park }
actions {}
"#;
let (mut p, s) = problem(src);
let a = ask(&mut p, &s, "_ & at_park", 0).expect("the pattern is valid");
assert!(
a.matches.iter().any(|m| m == "(at_park) & at_park"),
"the atom must survive intact: {:?}",
a.matches
);
assert!(
!a.matches.iter().any(|m| m.contains("at(")),
"no match may contain a torn identifier: {:?}",
a.matches
);
let c = modal_literals(&p.sig, 0);
assert!(c.contains(&"at_park".to_string()) && c.contains(&"!mary_home".to_string()));
}
#[test]
fn every_hole_in_a_pattern_takes_the_same_filler() {
let (mut p, s) = problem(COIN);
let a = ask(&mut p, &s, "_ & _", 0).expect("valid");
assert_eq!(a.matches, vec!["(h) & (h)"], "got {:?}", a.matches);
let both = ask(&mut p, &s, "B[a] _ & K[b] _", 0).expect("valid");
assert_eq!(both.matches, vec!["B[a] (h) & K[b] (h)"], "got {:?}", both.matches);
}
#[test]
fn substitution_is_structural_so_precedence_cannot_bite() {
let (mut p, s) = problem(COIN);
let a = ask(&mut p, &s, "!_", 1).expect("valid");
assert!(a.matches.iter().any(|m| m == "!(K[a] h)"), "got {:?}", a.matches);
assert!(!a.matches.iter().any(|m| m == "!(B[a] h)"), "a does believe h: {:?}", a.matches);
}
#[test]
fn a_hole_written_in_a_file_is_rejected_with_a_diagnostic() {
let e = Problem::parse(
r#"types{} objects{} agents{} props{ h } initially{ h } goal { _ } actions{}"#,
)
.unwrap_err();
assert!(e.contains("query hole"), "got {e}");
}
#[test]
fn a_pattern_without_a_hole_is_rejected_as_such() {
let (mut p, s) = problem(COIN);
let e = ask(&mut p, &s, "B[a] h", 0).unwrap_err();
assert!(e.contains('_'), "the error should say what is missing: {e}");
}
#[test]
fn a_malformed_pattern_reports_its_own_diagnostic() {
let (mut p, s) = problem(COIN);
let e = ask(&mut p, &s, "B[nobody] _", 0).unwrap_err();
assert!(e.contains("nobody"), "got {e}");
}
#[test]
fn the_candidate_bound_is_honoured_and_declared() {
let src = r#"
types{ Actor - Object } objects{ a, b, c - Actor } agents{ a, b, c }
props{ p, q, r, s }
initially { p } actions {}
"#;
let (mut p, st) = problem(src);
let a = ask(&mut p, &st, "_", 9).expect("ok");
assert!(a.truncated, "depth 9 over 3 agents must hit the bound");
assert!(a.considered <= MAX_CANDIDATES);
}
}