@@ -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