1use std::collections::BTreeSet;
20
21use smtlite::{Model, Solution};
22
23#[derive(Clone, Copy, Debug, PartialEq, Eq)]
30pub struct BranchRec {
31 pub site: u64,
33 pub taken: bool,
35}
36
37#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord)]
39pub enum FlipOutcome {
40 Verified,
43 ProvenUnflippable,
45 SolverUnknown,
47 #[default]
50 NotPosed,
51 RunFailed,
53 Unconfirmed,
55 AlreadyExplored,
57}
58
59#[derive(Clone, Debug, Default)]
61pub struct FlipRec {
62 pub at: u64,
64 pub input: Vec<(String, u64)>,
66 pub verified: bool,
68 pub outcome: FlipOutcome,
70}
71
72#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord)]
74pub enum PropertyOutcome {
75 Confirmed,
77 Proven,
79 SolverUnknown,
81 #[default]
83 NotPosed,
84 RunFailed,
86 Unreproduced,
88}
89
90#[derive(Clone, Debug, Default)]
92pub struct PropertyRec {
93 pub label: String,
95 pub outcome: PropertyOutcome,
97 pub violation: Option<(u64, u64)>,
99 pub input: Vec<(String, u64)>,
101}
102
103#[derive(Clone, Debug, Default)]
106pub struct SinkRec {
107 pub site: u64,
109 pub kind: String,
111 pub args: Vec<(String, u64)>,
113}
114
115#[derive(Clone, Debug, Default)]
117pub struct RunResult {
118 pub result: Option<i64>,
120 pub trapped: Option<String>,
122 pub log: Vec<BranchRec>,
124 pub sinks: Vec<SinkRec>,
126}
127
128#[derive(Clone, Debug, Default)]
131pub struct Report {
132 pub result: Option<i64>,
134 pub runs: usize,
136 pub flips_verified: usize,
138 pub sinks: Vec<SinkRec>,
140 pub properties: Vec<PropertyRec>,
142 pub flips: Vec<FlipRec>,
144 pub gaps: Vec<String>,
146 pub exhausted: bool,
148}
149
150#[derive(Clone, Copy, Debug, PartialEq, Eq)]
152pub struct Opts {
153 pub max_runs: usize,
155 pub max_flips: usize,
157}
158
159impl Default for Opts {
160 fn default() -> Self {
161 Self {
162 max_runs: 64,
163 max_flips: 32,
164 }
165 }
166}
167
168pub trait Frontend {
174 type Seed: Clone;
176 type Walk;
178
179 fn run(&self, seed: &Self::Seed) -> Result<RunResult, String>;
182
183 fn walk(&self, seed: &Self::Seed, log: &[BranchRec]) -> Option<Self::Walk>;
186
187 fn flip(&self, walk: &mut Self::Walk, i: usize) -> Solution;
189
190 fn site(&self, walk: &Self::Walk, i: usize) -> u64;
192
193 fn model_inputs(&self, model: &Model, walk: &Self::Walk) -> Vec<(String, u64)>;
195
196 fn apply_inputs(&self, seed: &Self::Seed, inputs: &[(String, u64)]) -> Self::Seed;
198
199 fn walk_gaps(&self, _walk: &Self::Walk) -> Vec<String> {
201 Vec::new()
202 }
203
204 fn walk_sinks(&self, _walk: &Self::Walk) -> Vec<SinkRec> {
206 Vec::new()
207 }
208}
209
210struct Pending<S> {
211 seed: S,
212 run: RunResult,
213}
214
215struct Attempt<S> {
216 rec: FlipRec,
217 verified: bool,
218 ran: bool,
219 gap: Option<String>,
220 child: Option<Pending<S>>,
221}
222
223#[must_use]
230pub fn explore<F: Frontend>(frontend: &F, base: &F::Seed, opts: &Opts) -> Report {
231 let mut report = Report::default();
232 let mut seen: BTreeSet<Vec<bool>> = BTreeSet::new();
233
234 let first = match frontend.run(base) {
235 Ok(r) => r,
236 Err(e) => {
237 report.gaps.push(format!("run failed: {e}"));
238 return report;
239 }
240 };
241 report.runs = 1;
242 report.result = first.result;
243 let mut frontier = vec![Pending { seed: base.clone(), run: first }];
244
245 while let Some(item) = frontier.pop() {
246 if report.runs >= opts.max_runs || report.flips_verified >= opts.max_flips {
247 return report;
248 }
249 if let Some(t) = &item.run.trapped {
250 report.gaps.push(format!("run trapped: {t}"));
251 }
252 let path: Vec<bool> = item.run.log.iter().map(|b| b.taken).collect();
253 if !seen.insert(path) {
254 continue;
255 }
256 let Some(mut walk) = frontend.walk(&item.seed, &item.run.log) else {
257 report
258 .gaps
259 .push("walk could not replay the concrete path".to_string());
260 continue;
261 };
262 report.sinks.extend(frontend.walk_sinks(&walk));
263 report.gaps.extend(frontend.walk_gaps(&walk));
264
265 let (mut spent, mut flipped) = (0usize, 0usize);
269 let mut attempts = Vec::new();
270 for i in (0..item.run.log.len()).rev() {
271 if report.runs + spent >= opts.max_runs
272 || report.flips_verified + flipped >= opts.max_flips
273 {
274 break;
275 }
276 let a = attempt(frontend, &item.seed, &item.run.log, &mut walk, i, &seen);
277 spent += usize::from(a.ran);
278 flipped += usize::from(a.verified);
279 attempts.push(a);
280 }
281
282 for a in attempts {
283 report.runs += usize::from(a.ran);
284 if let Some(g) = a.gap {
285 report.gaps.push(g);
286 }
287 report.flips.push(a.rec);
288 if a.verified {
289 report.flips_verified += 1;
290 if let Some(child) = a.child {
291 frontier.push(child);
292 }
293 }
294 }
295 }
296 report.exhausted = true;
297 report
298}
299
300fn attempt<F: Frontend>(
301 frontend: &F,
302 seed: &F::Seed,
303 log: &[BranchRec],
304 walk: &mut F::Walk,
305 i: usize,
306 seen: &BTreeSet<Vec<bool>>,
307) -> Attempt<F::Seed> {
308 let at = frontend.site(walk, i);
309 let solution = frontend.flip(walk, i);
310 let Solution::Sat(m) = &solution else {
311 let outcome = match solution {
312 Solution::Unsat => FlipOutcome::ProvenUnflippable,
313 Solution::Unknown => FlipOutcome::SolverUnknown,
314 _ => FlipOutcome::NotPosed,
315 };
316 let gap = (outcome == FlipOutcome::SolverUnknown)
317 .then(|| format!("solver Unknown at {at:#x}"));
318 return Attempt {
319 rec: FlipRec { at, input: Vec::new(), verified: false, outcome },
320 verified: false,
321 ran: false,
322 gap,
323 child: None,
324 };
325 };
326
327 let inputs = frontend.model_inputs(m, walk);
328 let child_seed = frontend.apply_inputs(seed, &inputs);
329 let child_run = match frontend.run(&child_seed) {
330 Ok(r) => r,
331 Err(_) => {
332 return Attempt {
333 rec: FlipRec {
334 at,
335 input: inputs,
336 verified: false,
337 outcome: FlipOutcome::RunFailed,
338 },
339 verified: false,
340 ran: true,
341 gap: None,
342 child: None,
343 };
344 }
345 };
346
347 let prefix_ok = child_run.log.len() > i
350 && child_run.log[..i]
351 .iter()
352 .zip(&log[..i])
353 .all(|(c, p)| c.site == p.site && c.taken == p.taken);
354 let flipped =
355 prefix_ok && child_run.log[i].site == log[i].site && child_run.log[i].taken != log[i].taken;
356
357 let child_path: Vec<bool> = child_run.log.iter().map(|b| b.taken).collect();
358 let verified = flipped && !seen.contains(&child_path);
359 let outcome = if verified {
360 FlipOutcome::Verified
361 } else if flipped {
362 FlipOutcome::AlreadyExplored
363 } else {
364 FlipOutcome::Unconfirmed
365 };
366
367 Attempt {
368 rec: FlipRec { at, input: inputs, verified, outcome },
369 verified,
370 ran: true,
371 gap: None,
372 child: verified.then_some(Pending { seed: child_seed, run: child_run }),
373 }
374}
375
376#[cfg(test)]
377mod tests {
378 use super::*;
379
380 struct Toy;
384
385 #[derive(Clone)]
386 struct Seed {
387 x: u8,
388 }
389
390 struct Walk {
391 solver: smtlite::Solver,
392 x: smtlite::Bv,
393 conds: Vec<smtlite::Bv>,
394 taken: Vec<bool>,
395 path: Vec<smtlite::Bv>,
396 }
397
398 impl Frontend for Toy {
399 type Seed = Seed;
400 type Walk = Walk;
401
402 fn run(&self, seed: &Self::Seed) -> Result<RunResult, String> {
403 let mut log = Vec::new();
404 let x = seed.x;
405 log.push(BranchRec { site: 0x100, taken: x < 0x10 });
406 if x < 0x10 {
407 log.push(BranchRec { site: 0x200, taken: x == 0x42 });
408 }
409 Ok(RunResult { result: Some(i64::from(x)), trapped: None, log, sinks: Vec::new() })
410 }
411
412 fn walk(&self, _seed: &Self::Seed, log: &[BranchRec]) -> Option<Self::Walk> {
413 let mut solver = smtlite::Solver::new();
414 let x = solver.var("in!x", 8);
415 let mut conds = Vec::new();
416 let mut taken = Vec::new();
417 let mut path = Vec::new();
418 for b in log {
419 let cond = match b.site {
420 0x100 => x.ult(&smtlite::Bv::val(0x10, 8)),
421 0x200 => x.eq(&smtlite::Bv::val(0x42, 8)),
422 _ => return None,
423 };
424 path.push(if b.taken { cond.clone() } else { cond.not() });
425 conds.push(cond);
426 taken.push(b.taken);
427 }
428 Some(Walk { solver, x, conds, taken, path })
429 }
430
431 fn flip(&self, walk: &mut Self::Walk, i: usize) -> Solution {
432 let g_i = if walk.taken[i] {
435 walk.conds[i].not()
436 } else {
437 walk.conds[i].clone()
438 };
439 let mut assumptions = walk.path[..i].to_vec();
440 assumptions.push(g_i);
441 walk.solver.check_all(&assumptions)
442 }
443
444 fn site(&self, walk: &Self::Walk, i: usize) -> u64 {
445 let _ = walk;
446 [0x100, 0x200][i]
447 }
448
449 fn model_inputs(&self, model: &Model, _walk: &Self::Walk) -> Vec<(String, u64)> {
450 model.get("in!x").map(|v| vec![("in!x".to_string(), v)]).unwrap_or_default()
451 }
452
453 fn apply_inputs(&self, _seed: &Self::Seed, inputs: &[(String, u64)]) -> Self::Seed {
454 Seed { x: inputs.first().map_or(0, |(_, v)| *v as u8) }
455 }
456 }
457
458 #[test]
459 fn the_loop_verifies_a_real_flip_and_proves_the_unreachable_one() {
460 let r = explore(&Toy, &Seed { x: 0 }, &Opts { max_runs: 64, max_flips: 32 });
461 assert_eq!(r.runs, 3, "flips={:?}", r.flips);
467 assert_eq!(r.flips_verified, 1, "flips={:?}", r.flips);
468 assert_eq!(
469 r.flips.iter().map(|f| (f.at, f.outcome)).collect::<Vec<_>>(),
470 vec![
471 (0x200, FlipOutcome::ProvenUnflippable),
472 (0x100, FlipOutcome::Verified),
473 (0x100, FlipOutcome::AlreadyExplored),
474 ]
475 );
476 assert!(r.exhausted);
477 }
478}