1use crate::ast::Init;
4use crate::lower_formula::{lower_formula, Bindings};
5use crate::{
6 build_declarative, build_explicit, ground_actions, parse_file, Constants, Ctx, Diagnostics,
7 GroundAction, Sig,
8};
9use delhi_mb::State;
10use delhi_syntax::{FormulaId, Store};
11
12#[derive(Debug)]
17pub struct Problem {
18 pub store: Store,
20 pub sig: Sig,
22 pub consts: Constants,
24 pub defs: crate::Defs,
27 pub state: State,
29 pub goal: Option<FormulaId>,
31 pub invariants: Vec<(FormulaId, String)>,
36 pub actions: Vec<GroundAction>,
38}
39
40impl Problem {
41 pub fn parse(src: &str) -> Result<Problem, String> {
48 match Problem::check(src) {
49 (Some(p), diags) if diags.is_empty() => Ok(p),
50 (_, diags) => Err(diags.render(src)),
51 }
52 }
53
54 pub fn check(src: &str) -> (Option<Problem>, Diagnostics) {
61 let mut diags = Diagnostics::default();
62 let mut ast = parse_file(src, &mut diags);
63
64 let defs = crate::Defs::build(&ast, &mut diags);
68 crate::expand_ast(&mut ast, &defs, &mut diags);
69
70 let sig = Sig::build(&ast, &mut diags);
71 let mut consts = Constants::build(&ast, &sig, &mut diags);
72 crate::rules::saturate(&ast, &sig, &mut consts, &mut diags);
75 let mut store = Store::default();
76
77 let state = {
78 let ctx = Ctx { sig: &sig, consts: &consts };
79 match &ast.init {
80 Some(Init::Declarative(items, block)) => {
84 build_declarative(items, *block, &ctx, &mut store, &mut diags)
85 }
86 Some(Init::Explicit { worlds, edges, span }) => {
87 build_explicit(worlds, edges, *span, &ctx, &mut store, &mut diags)
88 }
89 None => None,
90 }
91 };
92
93 let goal = ast
94 .goal
95 .as_ref()
96 .map(|g| lower_formula(g, &sig, &consts, &Bindings::default(), &mut store, &mut diags));
97
98 let invariants: Vec<(FormulaId, String)> = ast
99 .invariants
100 .iter()
101 .map(|(e, sp)| {
102 let f =
103 lower_formula(e, &sig, &consts, &Bindings::default(), &mut store, &mut diags);
104 (f, src[sp.start.min(src.len())..sp.end.min(src.len())].trim().to_string())
105 })
106 .collect();
107
108 let actions = ground_actions(&ast.actions, &sig, &consts, &mut store, &mut diags);
109
110 let problem = state.map(|state| Problem {
111 store,
112 sig,
113 consts,
114 defs,
115 state,
116 goal,
117 invariants,
118 actions,
119 });
120 (problem, diags)
121 }
122
123 pub fn action(&self, name: &str) -> Option<&GroundAction> {
126 self.actions.iter().find(|a| a.name == name)
127 }
128
129 pub fn entails(&self, f: FormulaId) -> bool {
133 debug_assert!(
134 (f as usize) < self.store.len(),
135 "formula must come from this problem's store"
136 );
137 self.state.entails(&self.store, f)
138 }
139
140 pub fn violated(&self, state: &State) -> Vec<&str> {
146 self.invariants
147 .iter()
148 .filter(|(f, _)| !state.entails(&self.store, *f))
149 .map(|(_, text)| text.as_str())
150 .collect()
151 }
152}
153
154pub fn load(path: &str) -> Result<Problem, String> {
157 let src = std::fs::read_to_string(path).map_err(|e| format!("{path}: {e}"))?;
158 Problem::parse(&src)
159}
160
161#[cfg(test)]
162mod tests {
163 use super::*;
164
165 const SRC: &str = r#"
166 types{ Actor - Object } objects{ a, b - Actor } agents{ a, b } props{ p }
167 initially { p, ?[a] p, B[a] p }
168 invariants {
169 !((B[a] p & B[b] !p) | (B[a] !p & B[b] p))
170 K[b] p
171 }
172 actions { lie() { actor b, announces !p, a observes, b observes } }
173 "#;
174
175 #[test]
176 fn an_invariant_holding_initially_can_still_be_broken_by_an_action() {
177 let mut p = Problem::parse(SRC).unwrap_or_else(|e| panic!("{e}"));
180 assert!(p.violated(&p.state).is_empty(), "clean at the start");
181
182 let n = p.sig.n_agents();
183 let def = p.action("lie()").expect("action").def.clone();
184 let am = delhi_mb::build(&def, &mut p.store, n);
185 let after = p.state.clone().apply(&p.store, &am).expect("applicable");
186
187 let bad = p.violated(&after);
188 assert_eq!(bad.len(), 1, "exactly the disagreement one: {bad:?}");
189 assert!(bad[0].starts_with("!(("), "got {:?}", bad[0]);
190 }
191
192 #[test]
193 fn a_violation_quotes_the_constraint_exactly_as_written() {
194 let p = Problem::parse(SRC).unwrap_or_else(|e| panic!("{e}"));
198 let texts: Vec<&str> = p.invariants.iter().map(|(_, t)| t.as_str()).collect();
199 assert_eq!(texts[0], "!((B[a] p & B[b] !p) | (B[a] !p & B[b] p))");
200 assert_eq!(texts[1], "K[b] p");
201 for t in &texts {
202 assert_eq!(
203 t.matches('(').count(),
204 t.matches(')').count(),
205 "parens must balance in the quoted text: {t}"
206 );
207 }
208 }
209
210 #[test]
211 fn a_file_with_no_invariants_section_has_none_and_violates_nothing() {
212 let p = Problem::parse(r#"types{} objects{} agents{} props{ p } initially{ p } actions{}"#)
213 .unwrap_or_else(|e| panic!("{e}"));
214 assert!(p.invariants.is_empty());
215 assert!(p.violated(&p.state).is_empty());
216 }
217}