Skip to content

Commit a0e0e75

Browse files
committed
Refine counterexample generation
1 parent 1bfec1f commit a0e0e75

File tree

1 file changed

+118
-37
lines changed

1 file changed

+118
-37
lines changed

examples/model-checking/src/counterexample.rs

Lines changed: 118 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -212,74 +212,96 @@ impl CounterexampleGenerator {
212212
/// Generate a lasso counterexample for liveness violations.
213213
///
214214
/// Finds a stem from initial to a fair cycle.
215+
/// The lasso structure:
216+
/// - Stem: path from initial to the loop entry (exclusive of loop entry)
217+
/// - Loop: cycle starting and ending at the same state
218+
///
219+
/// The full trace is: `stem[0] -> stem[1] -> ... -> loop[0] -> loop[1] -> ... -> loop[0] -> ...`
215220
pub fn generate_lasso(&self, eg_states: Ref) -> Option<Counterexample> {
216221
let initial = self.ts.initial();
217222

218-
// Find initial states that can reach EG states
219-
let initial_to_eg = self.bdd().apply_and(initial, self.reach_backward(eg_states));
223+
// Find initial states that are in EG or can reach EG states
224+
let can_reach_eg = self.reach_backward(eg_states);
225+
let initial_to_eg = self.bdd().apply_and(initial, can_reach_eg);
220226
if self.bdd().is_zero(initial_to_eg) {
221227
return None;
222228
}
223229

224-
// Generate stem: path from initial to EG
225-
let stem = self.generate_stem(initial_to_eg, eg_states);
230+
// Check if initial is already in EG (can start loop immediately)
231+
let initial_in_eg = self.bdd().apply_and(initial, eg_states);
232+
233+
if !self.bdd().is_zero(initial_in_eg) {
234+
// Initial state is in EG - empty stem, start loop from initial
235+
let (start_bdd, start_lits) = self.pick_one_state(initial_in_eg);
236+
let start_state = self.extract_state(&start_lits);
237+
let loop_states = self.generate_loop(eg_states, start_bdd, &start_state);
238+
return Some(Counterexample::Lasso { stem: vec![], loop_states });
239+
}
240+
241+
// Generate stem: path from initial to first EG state
242+
let (stem, loop_entry_bdd, loop_entry_state) = self.generate_stem(initial_to_eg, eg_states);
226243

227-
// Generate loop: cycle within EG states
228-
let loop_start = stem.last().cloned().unwrap_or_default();
229-
let loop_states = self.generate_loop(eg_states, &loop_start);
244+
// Generate loop: cycle within EG states starting from loop_entry
245+
let loop_states = self.generate_loop(eg_states, loop_entry_bdd, &loop_entry_state);
230246

231247
Some(Counterexample::Lasso { stem, loop_states })
232248
}
233249

234-
/// Generate stem from initial to target states.
235-
fn generate_stem(&self, from: Ref, to: Ref) -> Vec<State> {
236-
let mut trace = Vec::new();
250+
/// Generate stem from initial to first EG state.
251+
/// Returns tuple: (stem states excluding loop entry, loop entry BDD, loop entry State).
252+
fn generate_stem(&self, from: Ref, eg_states: Ref) -> (Vec<State>, Ref, State) {
253+
let mut stem = Vec::new();
237254

238255
let (mut current_bdd, mut current_lits) = self.pick_one_state(from);
239-
trace.push(self.extract_state(&current_lits));
240256

241-
// BFS forward until we hit target
257+
// Walk forward until we hit EG states
242258
for _ in 0..100 {
243-
// Depth limit
244-
let successors = self.ts.image(current_bdd);
245-
let hit_target = self.bdd().apply_and(successors, to);
259+
let current_state = self.extract_state(&current_lits);
246260

247-
if !self.bdd().is_zero(hit_target) {
248-
// Reached target
249-
(_, current_lits) = self.pick_one_state(hit_target);
250-
trace.push(self.extract_state(&current_lits));
261+
// Check if current state is in EG
262+
let in_eg = self.bdd().apply_and(current_bdd, eg_states);
263+
if !self.bdd().is_zero(in_eg) {
264+
// Current state is in EG - this is the loop entry
265+
return (stem, current_bdd, current_state);
266+
}
267+
268+
// Add to stem and continue
269+
stem.push(current_state);
270+
271+
let successors = self.ts.image(current_bdd);
272+
if self.bdd().is_zero(successors) {
251273
break;
252274
}
253275

254-
// Continue forward
255276
(current_bdd, current_lits) = self.pick_one_state(successors);
256-
trace.push(self.extract_state(&current_lits));
257277
}
258278

259-
trace
279+
// Fallback (shouldn't happen if from can reach eg_states)
280+
let state = self.extract_state(&current_lits);
281+
(stem, current_bdd, state)
260282
}
261283

262-
/// Generate a loop within EG states.
263-
fn generate_loop(&self, eg_states: Ref, start: &State) -> Vec<State> {
264-
let mut trace = Vec::new();
265-
266-
// Convert start state back to BDD
267-
let start_bdd = self.state_to_bdd(start);
284+
/// Generate a loop within EG states starting from a specific state.
285+
/// Returns the cycle including the start state as `loop[0]`.
286+
fn generate_loop(&self, eg_states: Ref, start_bdd: Ref, start_state: &State) -> Vec<State> {
287+
let mut trace = vec![start_state.clone()];
268288
let mut current_bdd = start_bdd;
269289

270-
// Find a cycle by following successors within EG
290+
// Follow successors within EG until we return to start
271291
for _ in 0..100 {
272292
let successors = self.ts.image(current_bdd);
273293
let next_in_eg = self.bdd().apply_and(successors, eg_states);
274294

275295
if self.bdd().is_zero(next_in_eg) {
296+
// No successors in EG - shouldn't happen for valid EG states
276297
break;
277298
}
278299

279-
// Check if we've returned to start
300+
// Check if we can return to start (completing the loop)
280301
let back_to_start = self.bdd().apply_and(next_in_eg, start_bdd);
281-
if !self.bdd().is_zero(back_to_start) && !trace.is_empty() {
282-
// Completed the loop
302+
if !self.bdd().is_zero(back_to_start) {
303+
// Can return to start - loop is complete
304+
// Don't add start again, it's already at trace[0]
283305
break;
284306
}
285307

@@ -343,6 +365,7 @@ impl CounterexampleGenerator {
343365
}
344366

345367
/// Convert a State back to a BDD.
368+
#[allow(dead_code)]
346369
fn state_to_bdd(&self, state: &State) -> Ref {
347370
let literals: Vec<Lit> = self
348371
.ts
@@ -432,6 +455,7 @@ mod tests {
432455
// The 2-bit counter cycles: 00 -> 01 -> 10 -> 11 -> 00
433456
// All reachable states can reach all other reachable states,
434457
// so EG(true) = all reachable states.
458+
// Since initial (00) is already in EG, the stem is empty.
435459
let eg_states = ts.reachable();
436460

437461
let cex = gen.generate_lasso(eg_states);
@@ -441,13 +465,17 @@ mod tests {
441465

442466
match &cex {
443467
Counterexample::Lasso { stem, loop_states } => {
444-
// Stem should start from initial (x=0, y=0)
445-
assert!(!stem.is_empty());
446-
assert_eq!(stem[0].get("x"), Some(false));
447-
assert_eq!(stem[0].get("y"), Some(false));
468+
// Initial state (00) is in EG, so stem is empty
469+
// Loop starts from initial and cycles through all states
470+
assert!(stem.is_empty(), "Stem should be empty when initial is in EG");
448471

449-
// Loop should be non-empty (we have a cycle)
472+
// Loop should contain the full cycle
450473
assert!(!loop_states.is_empty(), "Loop should contain at least one state");
474+
assert_eq!(loop_states.len(), 4, "Should have all 4 states in the cycle");
475+
476+
// Loop should start from initial (x=0, y=0)
477+
assert_eq!(loop_states[0].get("x"), Some(false));
478+
assert_eq!(loop_states[0].get("y"), Some(false));
451479

452480
println!("Stem length: {}", stem.len());
453481
println!("Loop length: {}", loop_states.len());
@@ -487,4 +515,57 @@ mod tests {
487515
let cex = gen.generate_lasso(empty);
488516
assert!(cex.is_none());
489517
}
518+
519+
#[test]
520+
fn test_lasso_with_nonempty_stem() {
521+
// Create a system where initial is NOT in EG, requiring a stem.
522+
// System: x transitions 0 -> 1 -> 1 (self-loop at 1)
523+
// Initial: x=0, EG states: x=1 (the self-loop)
524+
let bdd = Rc::new(Bdd::default());
525+
let mut ts = TransitionSystem::new(bdd);
526+
527+
let x = Var::new("x");
528+
ts.declare_var(x.clone());
529+
530+
let x_pres = ts.var_manager().get_present(&x).unwrap();
531+
532+
let x_bdd = ts.bdd().mk_var(x_pres);
533+
534+
// Initial: x=0
535+
let initial = ts.bdd().mk_cube([x_pres.neg()]);
536+
ts.set_initial(initial);
537+
538+
// Transition: x' = 1 (always goes to x=1, stays at x=1)
539+
let x_trans = ts.assign_var(&x, ts.bdd().one());
540+
let transition = ts.build_transition(&[x_trans]);
541+
ts.set_transition(transition);
542+
543+
let ts = Rc::new(ts);
544+
let gen = CounterexampleGenerator::new(ts.clone());
545+
546+
// EG states = {x=1} (the self-loop)
547+
// Initial x=0 is NOT in EG, so stem should be [x=0]
548+
let eg_states = x_bdd;
549+
550+
let cex = gen.generate_lasso(eg_states);
551+
assert!(cex.is_some());
552+
let cex = cex.unwrap();
553+
println!("{}", cex);
554+
555+
match &cex {
556+
Counterexample::Lasso { stem, loop_states } => {
557+
// Stem should contain initial state (x=0)
558+
assert_eq!(stem.len(), 1, "Stem should have one state (x=0)");
559+
assert_eq!(stem[0].get("x"), Some(false));
560+
561+
// Loop should be x=1 (self-loop, just one state)
562+
assert_eq!(loop_states.len(), 1, "Self-loop should have one state");
563+
assert_eq!(loop_states[0].get("x"), Some(true));
564+
565+
println!("Stem length: {}", stem.len());
566+
println!("Loop length: {}", loop_states.len());
567+
}
568+
_ => panic!("Expected lasso counterexample"),
569+
}
570+
}
490571
}

0 commit comments

Comments
 (0)