use crate::ast::{Arg, Expr, Modal, Term};
use crate::{Constants, Diagnostics, Sig, Span};
use delhi_syntax::{AgentId, FormulaId, Store};
#[derive(Default, Clone, Debug)]
pub struct Bindings(Vec<(String, String)>);
impl Bindings {
pub fn get(&self, var: &str) -> Option<&str> {
self.0.iter().rev().find(|(v, _)| v == var).map(|(_, o)| o.as_str())
}
pub fn with(&self, var: &str, obj: &str) -> Bindings {
let mut v = self.0.clone();
v.push((var.to_string(), obj.to_string()));
Bindings(v)
}
}
impl From<Vec<(String, String)>> for Bindings {
fn from(v: Vec<(String, String)>) -> Self {
Bindings(v)
}
}
fn mk_not(store: &mut Store, a: FormulaId) -> FormulaId {
let (t, f) = (store.tru(), store.fls());
if a == t {
f
} else if a == f {
t
} else {
store.not(a)
}
}
fn mk_and(store: &mut Store, a: FormulaId, b: FormulaId) -> FormulaId {
let (t, f) = (store.tru(), store.fls());
if a == f || b == f {
f
} else if a == t {
b
} else if b == t {
a
} else {
store.and(a, b)
}
}
fn mk_or(store: &mut Store, a: FormulaId, b: FormulaId) -> FormulaId {
let (t, f) = (store.tru(), store.fls());
if a == t || b == t {
t
} else if a == f {
b
} else if b == f {
a
} else {
store.or(a, b)
}
}
fn mk_implies(store: &mut Store, a: FormulaId, b: FormulaId) -> FormulaId {
let na = mk_not(store, a);
mk_or(store, na, b)
}
pub(crate) fn resolve_args(
term: &Term,
sig: &Sig,
binds: &Bindings,
diags: &mut Diagnostics,
) -> Option<Vec<String>> {
let mut out = Vec::with_capacity(term.args.len());
for a in &term.args {
match a {
Arg::Obj(o) => {
if !sig.objects.contains_key(o) {
diags.push(term.span, format!("`{o}` is not a declared object"));
return None;
}
out.push(o.clone());
}
Arg::Var(v) => match binds.get(v) {
Some(o) => out.push(o.to_string()),
None => {
diags.push(term.span, format!("`?{v}` is not bound here"));
return None;
}
},
Arg::Ty(t) => {
diags
.push(term.span, format!("type name `{t}` is only allowed inside `constants`"));
return None;
}
}
}
Some(out)
}
pub(crate) fn resolve_agents(
names: &[Arg],
sig: &Sig,
binds: &Bindings,
span: Span,
diags: &mut Diagnostics,
) -> Vec<AgentId> {
let mut out = Vec::with_capacity(names.len());
for a in names {
let name = match a {
Arg::Obj(o) => o.clone(),
Arg::Var(v) => match binds.get(v) {
Some(o) => o.to_string(),
None => {
diags.push(span, format!("`?{v}` is not bound here"));
continue;
}
},
Arg::Ty(t) => {
diags.push(span, format!("`{t}` is a type, not an agent"));
continue;
}
};
match sig.agent_id(&name) {
Some(i) => out.push(i),
None => diags.push(span, format!("`{name}` is not a declared agent")),
}
}
out
}
pub fn lower_formula(
e: &Expr,
sig: &Sig,
consts: &Constants,
binds: &Bindings,
store: &mut Store,
diags: &mut Diagnostics,
) -> FormulaId {
match e {
Expr::Hole(s) => {
diags.push(*s, "`_` is a query hole and has no meaning here");
store.fls()
}
Expr::True(_) => store.tru(),
Expr::False(_) => store.fls(),
Expr::Not(inner, _) => {
let f = lower_formula(inner, sig, consts, binds, store, diags);
mk_not(store, f)
}
Expr::And(a, b, _) => {
let x = lower_formula(a, sig, consts, binds, store, diags);
let y = lower_formula(b, sig, consts, binds, store, diags);
mk_and(store, x, y)
}
Expr::Or(a, b, _) => {
let x = lower_formula(a, sig, consts, binds, store, diags);
let y = lower_formula(b, sig, consts, binds, store, diags);
mk_or(store, x, y)
}
Expr::Implies(a, b, _) => {
let x = lower_formula(a, sig, consts, binds, store, diags);
let y = lower_formula(b, sig, consts, binds, store, diags);
mk_implies(store, x, y)
}
Expr::Atom(term) => {
let Some(args) = resolve_args(term, sig, binds, diags) else {
return store.fls();
};
if consts.is_constant_pred(&term.pred) {
return if consts.lookup(&term.pred, &args).unwrap_or(false) {
store.tru()
} else {
store.fls()
};
}
match sig.atom_id(&term.pred, &args) {
Some(id) => store.atom(id),
None => {
diags.push(
term.span,
format!(
"no proposition `{}`; check the name, arity, and argument types",
crate::ground::atom_key(&term.pred, &args)
),
);
store.fls()
}
}
}
Expr::Modality { op, agents, cond, body, span } => {
let inner = lower_formula(body, sig, consts, binds, store, diags);
if matches!(op, Modal::Common) {
let mask: u32 = match agents {
None => {
let n = sig.n_agents();
if n >= 32 {
u32::MAX
} else {
(1u32 << n) - 1
}
}
Some(names) => resolve_agents(names, sig, binds, *span, diags)
.into_iter()
.fold(0u32, |m, i| m | (1u32 << i)),
};
return store.common(mask, inner);
}
let Some(names) = agents else {
diags.push(*span, "only `C` accepts `[*]`; name the agents explicitly");
return store.fls();
};
let ids = resolve_agents(names, sig, binds, *span, diags);
if ids.is_empty() {
return store.fls();
}
let mut parts = Vec::with_capacity(ids.len());
for i in ids {
let f = match op {
Modal::Knows => store.knows(i, inner),
Modal::Believes => match cond {
Some(psi) => {
let c = lower_formula(psi, sig, consts, binds, store, diags);
store.cond_bel(i, c, inner)
}
None => store.believes(i, inner),
},
Modal::Safe => store.safe(i, inner),
Modal::KnowsDual => store.considers_possible(i, inner),
Modal::BelievesDual => store.not_ruled_out(i, inner),
Modal::SafeDual => store.safe_dual(i, inner),
Modal::KnowsWhether => store.knows_whether(i, inner),
Modal::BelievesWhether => store.believes_whether(i, inner),
Modal::Ignorant => store.ignorant(i, inner),
Modal::Undecided => store.undecided(i, inner),
Modal::Common => unreachable!("handled above"),
};
parts.push(f);
}
store.all(&parts)
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{parse_file, Constants, Diagnostics, Parser, Sig};
use delhi_syntax::Store;
const HEADER: &str = r#"
types { Location - Object }
objects { alice, bob - Location }
agents { alice, bob }
props { p, q }
constants { !adjacent(Location, Location), adjacent(alice, bob) }
initially { }
actions {}
"#;
fn setup() -> (Sig, Constants) {
let mut d = Diagnostics::default();
let ast = parse_file(HEADER, &mut d);
let sig = Sig::build(&ast, &mut d);
let c = Constants::build(&ast, &sig, &mut d);
assert!(d.is_empty(), "setup errors:\n{}", d.render(HEADER));
(sig, c)
}
fn lower(src: &str, s: &mut Store) -> FormulaId {
let (sig, c) = setup();
let mut d = Diagnostics::default();
let toks = crate::lex(src, &mut d);
let mut p = Parser::new(&toks);
let e = p.parse_expr(&mut d);
let f = lower_formula(&e, &sig, &c, &Bindings::default(), s, &mut d);
assert!(d.is_empty(), "lowering errors for `{src}`:\n{}", d.render(src));
f
}
#[test]
fn sugar_lowers_to_the_same_id_as_its_expansion() {
let mut s = Store::default();
let (sig, _) = setup();
let a = sig.agent_id("alice").unwrap();
let p_atom = sig.atom_id("p", &[]).unwrap();
let got = lower("Kw[alice] p", &mut s);
let want = {
let x = s.atom(p_atom);
s.knows_whether(a, x)
};
assert_eq!(got, want, "Kw must lower through knows_whether");
let got = lower("?[alice] p", &mut s);
let want = {
let x = s.atom(p_atom);
s.ignorant(a, x)
};
assert_eq!(got, want);
let got = lower("S'[alice] p", &mut s);
let want = {
let x = s.atom(p_atom);
s.safe_dual(a, x)
};
assert_eq!(got, want);
}
#[test]
fn agent_lists_distribute_over_knowledge() {
let mut s = Store::default();
let (sig, _) = setup();
let (a, b) = (sig.agent_id("alice").unwrap(), sig.agent_id("bob").unwrap());
let p_atom = sig.atom_id("p", &[]).unwrap();
let got = lower("K[alice, bob] p", &mut s);
let want = {
let x = s.atom(p_atom);
s.knows_all(&[a, b], x)
};
assert_eq!(got, want);
}
#[test]
fn agent_lists_do_not_distribute_over_common_knowledge() {
let mut s = Store::default();
let (sig, _) = setup();
let (a, b) = (sig.agent_id("alice").unwrap(), sig.agent_id("bob").unwrap());
let p_atom = sig.atom_id("p", &[]).unwrap();
let got = lower("C[alice, bob] p", &mut s);
let mask = (1u32 << a) | (1u32 << b);
let want = {
let x = s.atom(p_atom);
s.common(mask, x)
};
assert_eq!(got, want);
let wrong = {
let x = s.atom(p_atom);
let ca = s.common(1 << a, x);
let cb = s.common(1 << b, x);
s.and(ca, cb)
};
assert_ne!(got, wrong, "C must NOT distribute over its agent list");
}
#[test]
fn c_star_covers_every_declared_agent() {
let mut s = Store::default();
let (sig, _) = setup();
let p_atom = sig.atom_id("p", &[]).unwrap();
let got = lower("C[*] p", &mut s);
let want = {
let x = s.atom(p_atom);
s.common(0b11, x)
};
assert_eq!(got, want);
}
#[test]
fn constants_fold_to_top_and_bottom() {
let mut s = Store::default();
let got_true = lower("adjacent(alice, bob)", &mut s);
assert_eq!(got_true, s.tru());
let got_false = lower("adjacent(bob, alice)", &mut s);
assert_eq!(got_false, s.fls());
}
#[test]
fn a_declared_but_unlisted_constant_instance_folds_silently_to_false() {
let src = r#"
types { Location - Object }
objects { hall, study, kitchen - Location }
agents { }
props { }
constants { adjacent(hall, study) }
initially { }
actions {}
"#;
let mut setup_d = Diagnostics::default();
let ast = parse_file(src, &mut setup_d);
let sig = Sig::build(&ast, &mut setup_d);
let c = Constants::build(&ast, &sig, &mut setup_d);
assert!(setup_d.is_empty(), "setup errors:\n{}", setup_d.render(src));
assert!(c.is_constant_pred("adjacent"));
assert_eq!(c.lookup("adjacent", &["hall".to_string(), "kitchen".to_string()]), None);
let mut s = Store::default();
let mut d = Diagnostics::default();
let expr_src = "adjacent(hall, kitchen)";
let toks = crate::lex(expr_src, &mut d);
let mut p = Parser::new(&toks);
let e = p.parse_expr(&mut d);
let f = lower_formula(&e, &sig, &c, &Bindings::default(), &mut s, &mut d);
assert!(
d.is_empty(),
"a declared-but-unlisted instance must fold silently, not report:\n{}",
d.render(expr_src)
);
assert_eq!(f, s.fls(), "unlisted instance of a declared constant folds to false");
}
#[test]
fn folding_propagates_so_an_impossible_precondition_collapses_to_bottom() {
let mut s = Store::default();
assert_eq!(lower("p & adjacent(bob, alice)", &mut s), s.fls());
assert_eq!(lower("adjacent(bob, alice) & p", &mut s), s.fls());
let p_only = {
let (sig, _) = setup();
s.atom(sig.atom_id("p", &[]).unwrap())
};
assert_eq!(lower("p & adjacent(alice, bob)", &mut s), p_only);
assert_eq!(lower("p | adjacent(bob, alice)", &mut s), p_only);
assert_eq!(lower("p | adjacent(alice, bob)", &mut s), s.tru());
assert_eq!(lower("!adjacent(alice, bob)", &mut s), s.fls());
assert_eq!(lower("!adjacent(bob, alice)", &mut s), s.tru());
}
#[test]
fn variables_resolve_through_the_bindings() {
let (sig, c) = setup();
let mut s = Store::default();
let mut d = Diagnostics::default();
let src = "adjacent(?x, bob)";
let toks = crate::lex(src, &mut d);
let mut p = Parser::new(&toks);
let e = p.parse_expr(&mut d);
let b = Bindings::from(vec![("x".to_string(), "alice".to_string())]);
let f = lower_formula(&e, &sig, &c, &b, &mut s, &mut d);
assert!(d.is_empty(), "{}", d.render(src));
assert_eq!(f, s.tru(), "?x bound to alice makes adjacent(alice,bob) true");
}
#[test]
fn an_unbound_variable_is_reported() {
let (sig, c) = setup();
let mut s = Store::default();
let mut d = Diagnostics::default();
let toks = crate::lex("p(?nope)", &mut d);
let mut p = Parser::new(&toks);
let e = p.parse_expr(&mut d);
let _ = lower_formula(&e, &sig, &c, &Bindings::default(), &mut s, &mut d);
assert!(d.items().iter().any(|x| x.message.contains("?nope")));
}
#[test]
fn a_type_name_outside_constants_is_reported() {
let (sig, c) = setup();
let mut s = Store::default();
let mut d = Diagnostics::default();
let toks = crate::lex("p(Location)", &mut d);
let mut p = Parser::new(&toks);
let e = p.parse_expr(&mut d);
let _ = lower_formula(&e, &sig, &c, &Bindings::default(), &mut s, &mut d);
assert!(d
.items()
.iter()
.any(|x| x.message.contains("Location") && x.message.contains("constants")));
}
#[test]
fn an_undeclared_agent_is_reported() {
let (sig, c) = setup();
let mut s = Store::default();
let mut d = Diagnostics::default();
let toks = crate::lex("K[nobody] p", &mut d);
let mut p = Parser::new(&toks);
let e = p.parse_expr(&mut d);
let _ = lower_formula(&e, &sig, &c, &Bindings::default(), &mut s, &mut d);
assert!(d.items().iter().any(|x| x.message.contains("nobody")));
}
#[test]
fn a_typo_d_object_in_a_constant_predicate_is_reported_not_folded_silently() {
let (sig, c) = setup();
let mut s = Store::default();
let mut d = Diagnostics::default();
let src = "adjacent(alice, bobo)";
let toks = crate::lex(src, &mut d);
let mut p = Parser::new(&toks);
let e = p.parse_expr(&mut d);
let f = lower_formula(&e, &sig, &c, &Bindings::default(), &mut s, &mut d);
assert!(
d.items().iter().any(|x| x.message.contains("bobo")),
"a typo'd object must be reported, not silently folded:\n{}",
d.render(src)
);
assert_eq!(f, s.fls(), "lowering still yields a value (⊥) even though it errored");
}
#[test]
fn a_typo_d_object_in_an_ordinary_proposition_is_still_reported() {
let src = r#"
types { Location - Object }
objects { hall, study - Location }
agents { }
props { at(Location) }
initially { }
actions {}
"#;
let mut setup_d = Diagnostics::default();
let ast = parse_file(src, &mut setup_d);
let sig = Sig::build(&ast, &mut setup_d);
let c = Constants::build(&ast, &sig, &mut setup_d);
assert!(setup_d.is_empty(), "setup errors:\n{}", setup_d.render(src));
let mut s = Store::default();
let mut d = Diagnostics::default();
let expr_src = "at(bogus)";
let toks = crate::lex(expr_src, &mut d);
let mut p = Parser::new(&toks);
let e = p.parse_expr(&mut d);
let _ = lower_formula(&e, &sig, &c, &Bindings::default(), &mut s, &mut d);
assert!(
d.items().iter().any(|x| x.message.contains("bogus")),
"a typo'd object in an ordinary proposition must still be reported:\n{}",
d.render(expr_src)
);
}
}