1use std::collections::{BTreeMap, BTreeSet};
19
20use crate::{ProofExpr, ProofTerm};
21
22#[derive(Debug, Clone, PartialEq)]
24pub enum Counterexample {
25 Witness(Vec<(String, i64)>),
27 Valuation(Vec<(String, bool)>),
29}
30
31impl Counterexample {
32 pub fn render(&self) -> String {
34 match self {
35 Counterexample::Witness(bindings) => {
36 let parts: Vec<String> =
37 bindings.iter().map(|(v, n)| format!("{v} = {n}")).collect();
38 format!("false when {}", parts.join(", "))
39 }
40 Counterexample::Valuation(bindings) => {
41 let parts: Vec<String> = bindings
42 .iter()
43 .map(|(a, b)| format!("{a} = {}", if *b { "true" } else { "false" }))
44 .collect();
45 format!("false when {}", parts.join(", "))
46 }
47 }
48 }
49}
50
51pub fn find_counterexample(premises: &[ProofExpr], goal: &ProofExpr) -> Option<Counterexample> {
56 if is_arithmetic(goal) {
61 return arithmetic_witness(premises, goal);
62 }
63 propositional_model(premises, goal)
64}
65
66const GRID: &[i64] = &[0, 1, -1, 2, -2, 3, -3, 10, -10, 100, -100];
71
72fn arithmetic_witness(premises: &[ProofExpr], goal: &ProofExpr) -> Option<Counterexample> {
73 let mut vars: BTreeSet<String> = BTreeSet::new();
74 for p in premises {
75 arith_vars_expr(p, &mut vars);
76 }
77 arith_vars_expr(goal, &mut vars);
78 let vars: Vec<String> = vars.into_iter().collect();
79 if vars.is_empty() || vars.len() > 3 || !is_arithmetic(goal) {
82 return None;
83 }
84
85 let mut env = BTreeMap::new();
86 search_grid(&vars, 0, &mut env, premises, goal)
87}
88
89fn search_grid(
90 vars: &[String],
91 i: usize,
92 env: &mut BTreeMap<String, i64>,
93 premises: &[ProofExpr],
94 goal: &ProofExpr,
95) -> Option<Counterexample> {
96 if i == vars.len() {
97 let premises_hold = premises.iter().all(|p| eval_expr(p, env) == Some(true));
99 let goal_false = eval_expr(goal, env) == Some(false);
100 if premises_hold && goal_false {
101 return Some(Counterexample::Witness(
102 vars.iter().map(|v| (v.clone(), env[v])).collect(),
103 ));
104 }
105 return None;
106 }
107 for &val in GRID {
108 env.insert(vars[i].clone(), val);
109 if let Some(w) = search_grid(vars, i + 1, env, premises, goal) {
110 return Some(w);
111 }
112 }
113 env.remove(&vars[i]);
114 None
115}
116
117fn is_arithmetic(e: &ProofExpr) -> bool {
119 match e {
120 ProofExpr::Identity(l, r) => is_arith_term(l) && is_arith_term(r),
121 ProofExpr::And(l, r) | ProofExpr::Or(l, r) | ProofExpr::Implies(l, r) => {
122 is_arithmetic(l) && is_arithmetic(r)
123 }
124 ProofExpr::Not(p) => is_arithmetic(p),
125 ProofExpr::ForAll { body, .. } | ProofExpr::Exists { body, .. } => is_arithmetic(body),
126 _ => false,
127 }
128}
129
130fn is_arith_term(t: &ProofTerm) -> bool {
131 match t {
132 ProofTerm::Constant(_) | ProofTerm::Variable(_) => true,
133 ProofTerm::Function(name, args) => {
134 matches!(
135 name.as_str(),
136 "add" | "sub" | "mul" | "le" | "lt" | "ge" | "gt"
137 ) && args.iter().all(is_arith_term)
138 }
139 _ => false,
140 }
141}
142
143fn arith_vars_expr(e: &ProofExpr, out: &mut BTreeSet<String>) {
147 match e {
148 ProofExpr::Identity(l, r) => {
149 arith_vars_term(l, out);
150 arith_vars_term(r, out);
151 }
152 ProofExpr::And(l, r)
153 | ProofExpr::Or(l, r)
154 | ProofExpr::Implies(l, r)
155 | ProofExpr::Iff(l, r) => {
156 arith_vars_expr(l, out);
157 arith_vars_expr(r, out);
158 }
159 ProofExpr::Not(p) => arith_vars_expr(p, out),
160 ProofExpr::ForAll { variable, body } | ProofExpr::Exists { variable, body } => {
161 arith_vars_expr(body, out);
162 out.insert(variable.clone());
163 }
164 _ => {}
165 }
166}
167
168fn arith_vars_term(t: &ProofTerm, out: &mut BTreeSet<String>) {
169 match t {
170 ProofTerm::Variable(s) => {
171 out.insert(s.clone());
172 }
173 ProofTerm::Constant(s) if s.parse::<i64>().is_err() && s != "true" && s != "false" => {
177 out.insert(s.clone());
178 }
179 ProofTerm::Function(_, args) => {
180 for a in args {
181 arith_vars_term(a, out);
182 }
183 }
184 _ => {}
185 }
186}
187
188fn eval_expr(e: &ProofExpr, env: &BTreeMap<String, i64>) -> Option<bool> {
190 match e {
191 ProofExpr::Identity(l, r) => {
192 if let Some(truth) = eval_cmp(l, env) {
194 let claimed = match r {
195 ProofTerm::Constant(s) if s == "true" => true,
196 ProofTerm::Constant(s) if s == "false" => false,
197 _ => return None,
198 };
199 return Some(truth == claimed);
200 }
201 Some(eval_term(l, env)? == eval_term(r, env)?)
202 }
203 ProofExpr::And(l, r) => Some(eval_expr(l, env)? && eval_expr(r, env)?),
204 ProofExpr::Or(l, r) => Some(eval_expr(l, env)? || eval_expr(r, env)?),
205 ProofExpr::Implies(l, r) => Some(!eval_expr(l, env)? || eval_expr(r, env)?),
206 ProofExpr::Not(p) => Some(!eval_expr(p, env)?),
207 ProofExpr::ForAll { body, .. } | ProofExpr::Exists { body, .. } => eval_expr(body, env),
210 _ => None,
211 }
212}
213
214fn eval_cmp(t: &ProofTerm, env: &BTreeMap<String, i64>) -> Option<bool> {
215 let ProofTerm::Function(name, args) = t else { return None };
216 if args.len() != 2 {
217 return None;
218 }
219 let a = eval_term(&args[0], env)?;
220 let b = eval_term(&args[1], env)?;
221 match name.as_str() {
222 "le" => Some(a <= b),
223 "lt" => Some(a < b),
224 "ge" => Some(a >= b),
225 "gt" => Some(a > b),
226 _ => None,
227 }
228}
229
230fn eval_term(t: &ProofTerm, env: &BTreeMap<String, i64>) -> Option<i64> {
231 match t {
232 ProofTerm::Constant(s) => s
233 .parse::<i64>()
234 .ok()
235 .or_else(|| env.get(s).copied()),
236 ProofTerm::Variable(s) => env.get(s).copied(),
237 ProofTerm::Function(name, args) if args.len() == 2 => {
238 let a = eval_term(&args[0], env)?;
239 let b = eval_term(&args[1], env)?;
240 match name.as_str() {
241 "add" => a.checked_add(b),
242 "sub" => a.checked_sub(b),
243 "mul" => a.checked_mul(b),
244 _ => None,
245 }
246 }
247 _ => None,
248 }
249}
250
251fn propositional_model(premises: &[ProofExpr], goal: &ProofExpr) -> Option<Counterexample> {
257 let mut atoms: BTreeSet<String> = BTreeSet::new();
258 for p in premises {
259 prop_atoms(p, &mut atoms);
260 }
261 prop_atoms(goal, &mut atoms);
262 let atoms: Vec<String> = atoms.into_iter().collect();
263 if atoms.is_empty() || atoms.len() > 16 {
264 return None;
265 }
266 for mask in 0u32..(1u32 << atoms.len()) {
267 let mut val = BTreeMap::new();
268 for (i, a) in atoms.iter().enumerate() {
269 val.insert(a.clone(), (mask >> i) & 1 == 1);
270 }
271 let premises_hold = premises.iter().all(|p| eval_prop(p, &val) == Some(true));
272 let goal_false = eval_prop(goal, &val) == Some(false);
273 if premises_hold && goal_false {
274 return Some(Counterexample::Valuation(
275 atoms.iter().map(|a| (a.clone(), val[a])).collect(),
276 ));
277 }
278 }
279 None
280}
281
282fn atom_key(e: &ProofExpr) -> Option<String> {
284 match e {
285 ProofExpr::Predicate { .. } | ProofExpr::Atom(_) | ProofExpr::Identity(..) => {
286 Some(format!("{e}"))
287 }
288 _ => None,
289 }
290}
291
292fn prop_atoms(e: &ProofExpr, out: &mut BTreeSet<String>) {
293 match e {
294 ProofExpr::And(l, r)
295 | ProofExpr::Or(l, r)
296 | ProofExpr::Implies(l, r)
297 | ProofExpr::Iff(l, r) => {
298 prop_atoms(l, out);
299 prop_atoms(r, out);
300 }
301 ProofExpr::Not(p) => prop_atoms(p, out),
302 _ => {
303 if let Some(k) = atom_key(e) {
304 out.insert(k);
305 }
306 }
307 }
308}
309
310fn eval_prop(e: &ProofExpr, val: &BTreeMap<String, bool>) -> Option<bool> {
311 match e {
312 ProofExpr::And(l, r) => Some(eval_prop(l, val)? && eval_prop(r, val)?),
313 ProofExpr::Or(l, r) => Some(eval_prop(l, val)? || eval_prop(r, val)?),
314 ProofExpr::Implies(l, r) => Some(!eval_prop(l, val)? || eval_prop(r, val)?),
315 ProofExpr::Iff(l, r) => Some(eval_prop(l, val)? == eval_prop(r, val)?),
316 ProofExpr::Not(p) => Some(!eval_prop(p, val)?),
317 _ => atom_key(e).and_then(|k| val.get(&k).copied()),
318 }
319}