1use crate::ast::Expr;
29use crate::lower_formula::{lower_formula, Bindings};
30use crate::{Diagnostics, Parser, Problem, Sig};
31use delhi_mb::State;
32
33pub const MAX_CANDIDATES: usize = 20_000;
39
40pub const HOLE: &str = "_";
42
43#[derive(Clone, Debug, PartialEq, Eq)]
45pub struct Answer {
46 pub matches: Vec<String>,
48 pub considered: usize,
50 pub truncated: bool,
53}
54
55pub fn modal_literals(sig: &Sig, depth: usize) -> Vec<String> {
62 let mut level: Vec<String> = Vec::new();
63 for a in 0..sig.n_atoms() {
64 let name = sig.atom_name(a as u32);
65 level.push(name.to_string());
66 level.push(format!("!{name}"));
67 }
68 let mut all = level.clone();
69 for _ in 0..depth {
70 let mut next = Vec::new();
71 for inner in &level {
72 for i in 0..sig.n_agents() {
73 let who = sig.agent_name(i as u32);
74 next.push(format!("K[{who}] {inner}"));
75 next.push(format!("B[{who}] {inner}"));
76 }
77 if all.len() + next.len() > MAX_CANDIDATES {
78 break;
79 }
80 }
81 all.extend(next.iter().cloned());
82 level = next;
83 if all.len() > MAX_CANDIDATES {
84 break;
85 }
86 }
87 all.truncate(MAX_CANDIDATES);
88 all
89}
90
91fn fill(pattern: &Expr, filler: &Expr) -> Expr {
101 match pattern {
102 Expr::Hole(_) => filler.clone(),
103 Expr::True(_) | Expr::False(_) | Expr::Atom(_) => pattern.clone(),
104 Expr::Not(a, s) => Expr::Not(Box::new(fill(a, filler)), *s),
105 Expr::And(a, b, s) => Expr::And(Box::new(fill(a, filler)), Box::new(fill(b, filler)), *s),
106 Expr::Or(a, b, s) => Expr::Or(Box::new(fill(a, filler)), Box::new(fill(b, filler)), *s),
107 Expr::Implies(a, b, s) => {
108 Expr::Implies(Box::new(fill(a, filler)), Box::new(fill(b, filler)), *s)
109 }
110 Expr::Modality { op, agents, cond, body, span } => Expr::Modality {
111 op: op.clone(),
112 agents: agents.clone(),
113 cond: cond.as_ref().map(|c| Box::new(fill(c, filler))),
114 body: Box::new(fill(body, filler)),
115 span: *span,
116 },
117 }
118}
119
120fn has_hole(e: &Expr) -> bool {
122 match e {
123 Expr::Hole(_) => true,
124 Expr::True(_) | Expr::False(_) | Expr::Atom(_) => false,
125 Expr::Not(a, _) => has_hole(a),
126 Expr::And(a, b, _) | Expr::Or(a, b, _) | Expr::Implies(a, b, _) => {
127 has_hole(a) || has_hole(b)
128 }
129 Expr::Modality { cond, body, .. } => {
130 cond.as_ref().is_some_and(|c| has_hole(c)) || has_hole(body)
131 }
132 }
133}
134
135fn parse(text: &str) -> (Expr, Diagnostics) {
137 let mut diags = Diagnostics::default();
138 let toks = crate::lex(text, &mut diags);
139 let expr = Parser::new(&toks).parse_expr(&mut diags);
140 (expr, diags)
141}
142
143fn hole_spans(pattern: &str) -> Vec<(usize, usize)> {
145 let mut diags = Diagnostics::default();
146 crate::lex(pattern, &mut diags)
147 .iter()
148 .filter(|t| t.tok == crate::Tok::Hole)
149 .map(|t| (t.span.start, t.span.end))
150 .collect()
151}
152
153fn render(pattern: &str, holes: &[(usize, usize)], candidate: &str) -> String {
159 let mut out = String::with_capacity(pattern.len() + candidate.len());
160 let mut last = 0;
161 for &(start, end) in holes {
162 out.push_str(&pattern[last..start]);
163 out.push('(');
164 out.push_str(candidate);
165 out.push(')');
166 last = end;
167 }
168 out.push_str(&pattern[last..]);
169 out
170}
171
172fn complement(candidate: &str) -> String {
176 let (prefix, lit) = match candidate.rfind("] ") {
177 Some(i) => candidate.split_at(i + 2),
178 None => ("", candidate),
179 };
180 match lit.strip_prefix('!') {
181 Some(rest) => format!("{prefix}{rest}"),
182 None => format!("{prefix}!{lit}"),
183 }
184}
185
186pub fn ask(p: &mut Problem, state: &State, pattern: &str, depth: usize) -> Result<Answer, String> {
192 let (pat, mut diags) = parse(pattern);
193 if !diags.is_empty() {
194 return Err(diags.render(pattern));
195 }
196 let pat = crate::expand(&pat, &p.defs, &mut diags);
198 if !diags.is_empty() {
199 return Err(diags.render(pattern));
200 }
201 if !has_hole(&pat) {
202 return Err(format!(
203 "the pattern needs a `{HOLE}` to fill — try `B[agent] {HOLE}`, or `{HOLE}` on its own"
204 ));
205 }
206 let holes = hole_spans(pattern);
207 let candidates = modal_literals(&p.sig, depth);
208 if candidates.is_empty() {
209 return Ok(Answer { matches: Vec::new(), considered: 0, truncated: false });
210 }
211
212 let parsed: Vec<Expr> = candidates.iter().map(|c| parse(c).0).collect();
216
217 let mut probe = Diagnostics::default();
220 let first = fill(&pat, &parsed[0]);
221 let _ =
222 lower_formula(&first, &p.sig, &p.consts, &Bindings::default(), &mut p.store, &mut probe);
223 if !probe.is_empty() {
224 return Err(probe.render(pattern));
225 }
226
227 let mut hit: std::collections::HashSet<&str> = std::collections::HashSet::new();
228 let mut order: Vec<&String> = Vec::new();
229 for (c, tree) in candidates.iter().zip(&parsed) {
230 let mut quiet = Diagnostics::default();
231 let f = lower_formula(
232 &fill(&pat, tree),
233 &p.sig,
234 &p.consts,
235 &Bindings::default(),
236 &mut p.store,
237 &mut quiet,
238 );
239 if quiet.is_empty() && state.entails(&p.store, f) {
240 hit.insert(c.as_str());
241 order.push(c);
242 }
243 }
244
245 let matches = order
253 .into_iter()
254 .filter(|c| {
255 let is_negative = complement(c).len() < c.len();
256 !(is_negative && hit.contains(complement(c).as_str()))
257 })
258 .map(|c| render(pattern, &holes, c))
261 .collect();
262
263 Ok(Answer {
264 matches,
265 considered: candidates.len(),
266 truncated: candidates.len() >= MAX_CANDIDATES,
267 })
268}
269
270#[cfg(test)]
271mod tests {
272 use super::*;
273
274 const COIN: &str = r#"
275 types{ Actor - Object } objects{ a, b - Actor } agents{ a, b } props{ h }
276 initially { h, ?[a] h, B[a] h }
277 actions {}
278 "#;
279
280 fn problem(src: &str) -> (Problem, State) {
281 let p = Problem::parse(src).unwrap_or_else(|e| panic!("{e}"));
282 let s = p.state.clone();
283 (p, s)
284 }
285
286 #[test]
287 fn candidate_count_follows_the_signature_and_the_depth() {
288 let (p, _) = problem(COIN);
291 assert_eq!(modal_literals(&p.sig, 0).len(), 2);
292 assert_eq!(modal_literals(&p.sig, 1).len(), 2 + 8);
293 assert_eq!(modal_literals(&p.sig, 2).len(), 2 + 8 + 32);
294 }
295
296 #[test]
297 fn candidates_are_ordered_shallowest_first() {
298 let (p, _) = problem(COIN);
301 let c = modal_literals(&p.sig, 2);
302 let depth_of = |s: &str| s.matches('[').count();
303 let depths: Vec<usize> = c.iter().map(|s| depth_of(s)).collect();
304 assert!(depths.windows(2).all(|w| w[0] <= w[1]), "not shallowest-first: {depths:?}");
305 }
306
307 #[test]
308 fn asking_what_an_agent_believes_separates_belief_from_knowledge() {
309 let (mut p, s) = problem(COIN);
313 let believes_a = ask(&mut p, &s, "B[a] _", 0).expect("valid pattern");
314 assert_eq!(believes_a.matches, vec!["B[a] (h)"]);
315
316 let knows_a = ask(&mut p, &s, "K[a] _", 0).expect("valid pattern");
317 assert!(knows_a.matches.is_empty(), "a knows nothing here: {:?}", knows_a.matches);
318
319 let knows_b = ask(&mut p, &s, "K[b] _", 0).expect("valid pattern");
320 assert_eq!(knows_b.matches, vec!["K[b] (h)"]);
321 }
322
323 #[test]
324 fn asking_what_an_agent_is_ignorant_of_reports_the_atom_once() {
325 let (mut p, s) = problem(COIN);
329 let ignorant = ask(&mut p, &s, "?[a] _", 0).expect("valid pattern");
330 assert!(ignorant.matches.iter().any(|m| m.contains("(h)")), "got {:?}", ignorant.matches);
331 assert!(
332 !ignorant.matches.iter().any(|m| m.contains("(!h)")),
333 "the negated twin is redundant: {:?}",
334 ignorant.matches
335 );
336 }
337
338 #[test]
339 fn depth_reaches_nested_attitudes_that_depth_zero_cannot() {
340 let (mut p, s) = problem(COIN);
343 let shallow = ask(&mut p, &s, "K[b] _", 0).expect("ok");
344 assert!(!shallow.matches.iter().any(|m| m.contains("B[a]")));
345
346 let deep = ask(&mut p, &s, "K[b] _", 1).expect("ok");
347 assert!(deep.matches.iter().any(|m| m == "K[b] (B[a] h)"), "got {:?}", deep.matches);
348 assert!(deep.considered > shallow.considered);
349 }
350
351 #[test]
352 fn a_bare_hole_enumerates_what_simply_holds() {
353 let (mut p, s) = problem(COIN);
354 let a = ask(&mut p, &s, "_", 0).expect("ok");
355 assert_eq!(a.matches, vec!["(h)"], "h is true, !h is not");
356 }
357
358 #[test]
359 fn an_underscore_inside_an_identifier_is_not_a_hole() {
360 let src = r#"
364 types{ Actor - Object } objects{ a - Actor } agents{ a }
365 props{ at_park, mary_home }
366 initially { at_park }
367 actions {}
368 "#;
369 let (mut p, s) = problem(src);
370 let a = ask(&mut p, &s, "_ & at_park", 0).expect("the pattern is valid");
371 assert!(
372 a.matches.iter().any(|m| m == "(at_park) & at_park"),
373 "the atom must survive intact: {:?}",
374 a.matches
375 );
376 assert!(
377 !a.matches.iter().any(|m| m.contains("at(")),
378 "no match may contain a torn identifier: {:?}",
379 a.matches
380 );
381 let c = modal_literals(&p.sig, 0);
383 assert!(c.contains(&"at_park".to_string()) && c.contains(&"!mary_home".to_string()));
384 }
385
386 #[test]
387 fn every_hole_in_a_pattern_takes_the_same_filler() {
388 let (mut p, s) = problem(COIN);
391 let a = ask(&mut p, &s, "_ & _", 0).expect("valid");
392 assert_eq!(a.matches, vec!["(h) & (h)"], "got {:?}", a.matches);
393
394 let both = ask(&mut p, &s, "B[a] _ & K[b] _", 0).expect("valid");
396 assert_eq!(both.matches, vec!["B[a] (h) & K[b] (h)"], "got {:?}", both.matches);
397 }
398
399 #[test]
400 fn substitution_is_structural_so_precedence_cannot_bite() {
401 let (mut p, s) = problem(COIN);
404 let a = ask(&mut p, &s, "!_", 1).expect("valid");
405 assert!(a.matches.iter().any(|m| m == "!(K[a] h)"), "got {:?}", a.matches);
406 assert!(!a.matches.iter().any(|m| m == "!(B[a] h)"), "a does believe h: {:?}", a.matches);
407 }
408
409 #[test]
410 fn a_hole_written_in_a_file_is_rejected_with_a_diagnostic() {
411 let e = Problem::parse(
414 r#"types{} objects{} agents{} props{ h } initially{ h } goal { _ } actions{}"#,
415 )
416 .unwrap_err();
417 assert!(e.contains("query hole"), "got {e}");
418 }
419
420 #[test]
421 fn a_pattern_without_a_hole_is_rejected_as_such() {
422 let (mut p, s) = problem(COIN);
426 let e = ask(&mut p, &s, "B[a] h", 0).unwrap_err();
427 assert!(e.contains('_'), "the error should say what is missing: {e}");
428 }
429
430 #[test]
431 fn a_malformed_pattern_reports_its_own_diagnostic() {
432 let (mut p, s) = problem(COIN);
433 let e = ask(&mut p, &s, "B[nobody] _", 0).unwrap_err();
434 assert!(e.contains("nobody"), "got {e}");
435 }
436
437 #[test]
438 fn the_candidate_bound_is_honoured_and_declared() {
439 let src = r#"
442 types{ Actor - Object } objects{ a, b, c - Actor } agents{ a, b, c }
443 props{ p, q, r, s }
444 initially { p } actions {}
445 "#;
446 let (mut p, st) = problem(src);
447 let a = ask(&mut p, &st, "_", 9).expect("ok");
448 assert!(a.truncated, "depth 9 over 3 agents must hit the bound");
449 assert!(a.considered <= MAX_CANDIDATES);
450 }
451}