@@ -2824,9 +2824,6 @@ impl Explorer {
28242824 for ( next_state, action_idx, pvals) in successors {
28252825 let canonical = self . maybe_canonicalize ( next_state) ;
28262826 let next_fp = canonical. fingerprint ( ) ;
2827- if self . store . contains ( & next_fp) {
2828- continue ;
2829- }
28302827 let is_new = if fp_only_par {
28312828 self . store . insert_fp_only ( next_fp)
28322829 } else {
@@ -2882,9 +2879,6 @@ impl Explorer {
28822879 for ( next_state, action_idx, pvals) in full_succ {
28832880 let canonical = self . maybe_canonicalize ( next_state) ;
28842881 let next_fp = canonical. fingerprint ( ) ;
2885- if self . store . contains ( & next_fp) {
2886- continue ;
2887- }
28882882 let is_new = if fp_only_par {
28892883 self . store . insert_fp_only ( next_fp)
28902884 } else {
@@ -3013,7 +3007,7 @@ impl Explorer {
30133007 if let Some ( ref mask) = self . view_mask {
30143008 let states = states
30153009 . into_iter ( )
3016- . map ( |s| State :: new_with_view ( ( * s. vars ) . clone ( ) , mask) )
3010+ . map ( |s| State :: new_with_view ( s. vars . to_vec ( ) , mask) )
30173011 . collect ( ) ;
30183012 return Ok ( states) ;
30193013 }
@@ -3549,17 +3543,106 @@ impl Explorer {
35493543 /// The cycle proviso (C3') is checked by the caller: at least one successor must be
35503544 /// new. If not, the caller falls back to full expansion.
35513545 fn compute_stubborn_set ( & self , enabled : & [ usize ] ) -> Vec < usize > {
3552- use std:: collections:: HashSet ;
3553-
35543546 if enabled. len ( ) <= 1 {
35553547 return enabled. to_vec ( ) ;
35563548 }
35573549
3558- let enabled_set: HashSet < usize > = enabled. iter ( ) . copied ( ) . collect ( ) ;
35593550 let n_actions = self . spec . actions . len ( ) ;
35603551
3561- // Key selection: prefer actions relevant to invariants (COI) to avoid missing
3562- // violation traces. If no COI-relevant action is enabled, fall back to first enabled.
3552+ // Fast path: use u64 bitmasks when action count fits (covers most specs)
3553+ if n_actions <= 64 {
3554+ return self . compute_stubborn_set_bitmask ( enabled, n_actions) ;
3555+ }
3556+
3557+ // Fallback for >64 actions
3558+ self . compute_stubborn_set_hashset ( enabled, n_actions)
3559+ }
3560+
3561+ fn compute_stubborn_set_bitmask ( & self , enabled : & [ usize ] , n_actions : usize ) -> Vec < usize > {
3562+ let enabled_mask: u64 = enabled. iter ( ) . fold ( 0u64 , |m, & a| m | ( 1u64 << a) ) ;
3563+ let relevant_mask: u64 = self
3564+ . relevant_actions
3565+ . as_ref ( )
3566+ . map ( |r| r. iter ( ) . fold ( 0u64 , |m, & a| m | ( 1u64 << a) ) )
3567+ . unwrap_or ( 0 ) ;
3568+ let all_mask: u64 = if n_actions == 64 {
3569+ u64:: MAX
3570+ } else {
3571+ ( 1u64 << n_actions) - 1
3572+ } ;
3573+
3574+ let t_key = if relevant_mask != 0 {
3575+ enabled
3576+ . iter ( )
3577+ . find ( |& & a| relevant_mask & ( 1u64 << a) != 0 )
3578+ . copied ( )
3579+ . unwrap_or ( enabled[ 0 ] )
3580+ } else {
3581+ enabled[ 0 ]
3582+ } ;
3583+
3584+ let mut stubborn: u64 = 1u64 << t_key;
3585+ let mut worklist: Vec < usize > = vec ! [ t_key] ;
3586+
3587+ while let Some ( t) = worklist. pop ( ) {
3588+ if enabled_mask & ( 1u64 << t) != 0 {
3589+ for & t_nds in & self . nds [ t] {
3590+ let bit = 1u64 << t_nds;
3591+ if enabled_mask & bit != 0 && stubborn & bit == 0 {
3592+ stubborn |= bit;
3593+ worklist. push ( t_nds) ;
3594+ }
3595+ }
3596+ } else {
3597+ for & t_nes in & self . nes [ t] {
3598+ let bit = 1u64 << t_nes;
3599+ if stubborn & bit == 0 {
3600+ stubborn |= bit;
3601+ worklist. push ( t_nes) ;
3602+ break ;
3603+ }
3604+ }
3605+ }
3606+
3607+ if stubborn & all_mask == all_mask {
3608+ return enabled. to_vec ( ) ;
3609+ }
3610+ }
3611+
3612+ let mut result: Vec < usize > = enabled
3613+ . iter ( )
3614+ . filter ( |& & a| stubborn & ( 1u64 << a) != 0 )
3615+ . copied ( )
3616+ . collect ( ) ;
3617+
3618+ // POR visibility condition
3619+ if result. len ( ) < enabled. len ( ) {
3620+ let result_mask: u64 = result. iter ( ) . fold ( 0u64 , |m, & a| m | ( 1u64 << a) ) ;
3621+ if result_mask & self . visible_actions != 0 {
3622+ return enabled. to_vec ( ) ;
3623+ }
3624+ }
3625+
3626+ // Ensure at least one COI-relevant action
3627+ if relevant_mask != 0 {
3628+ let has_relevant = result. iter ( ) . any ( |& a| relevant_mask & ( 1u64 << a) != 0 ) ;
3629+ if !has_relevant {
3630+ for & a in enabled {
3631+ if relevant_mask & ( 1u64 << a) != 0 {
3632+ result. push ( a) ;
3633+ }
3634+ }
3635+ }
3636+ }
3637+
3638+ result
3639+ }
3640+
3641+ fn compute_stubborn_set_hashset ( & self , enabled : & [ usize ] , n_actions : usize ) -> Vec < usize > {
3642+ use std:: collections:: HashSet ;
3643+
3644+ let enabled_set: HashSet < usize > = enabled. iter ( ) . copied ( ) . collect ( ) ;
3645+
35633646 let t_key = if let Some ( ref relevant) = self . relevant_actions {
35643647 let relevant_set: HashSet < usize > = relevant. iter ( ) . copied ( ) . collect ( ) ;
35653648 enabled
@@ -3576,17 +3659,12 @@ impl Explorer {
35763659
35773660 while let Some ( t) = worklist. pop ( ) {
35783661 if enabled_set. contains ( & t) {
3579- // Enabled: add all NDS(t) that are also enabled
3580- // (actions that could disable t's guard — must be in stubborn set
3581- // to ensure t remains enabled throughout)
35823662 for & t_nds in & self . nds [ t] {
35833663 if enabled_set. contains ( & t_nds) && stubborn. insert ( t_nds) {
35843664 worklist. push ( t_nds) ;
35853665 }
35863666 }
35873667 } else {
3588- // Disabled: pick one NES(t) to potentially enable this action.
3589- // Deterministic: pick the smallest index not already in the set.
35903668 for & t_nes in & self . nes [ t] {
35913669 if stubborn. insert ( t_nes) {
35923670 worklist. push ( t_nes) ;
@@ -3595,23 +3673,17 @@ impl Explorer {
35953673 }
35963674 }
35973675
3598- // Early exit: if stubborn set already contains all actions, no reduction
35993676 if stubborn. len ( ) >= n_actions {
36003677 return enabled. to_vec ( ) ;
36013678 }
36023679 }
36033680
3604- // Return only enabled actions in the stubborn set, preserving input order
36053681 let mut result: Vec < usize > = enabled
36063682 . iter ( )
36073683 . filter ( |a| stubborn. contains ( a) )
36083684 . copied ( )
36093685 . collect ( ) ;
36103686
3611- // POR visibility condition: if the stubborn set contains any action that
3612- // writes to a variable used in an invariant, the reduction is unsound because
3613- // it may hide interleavings that lead to invariant violations. In that case,
3614- // expand to all enabled actions (no reduction).
36153687 if result. len ( ) < enabled. len ( ) {
36163688 let stubborn_mask: u64 =
36173689 result
@@ -3622,20 +3694,14 @@ impl Explorer {
36223694 }
36233695 }
36243696
3625- // Safety fix: ensure stubborn set includes at least one COI-relevant action
3626- // to avoid missing invariant violation traces. If the stubborn set contains
3627- // only irrelevant actions, add all enabled COI-relevant actions.
36283697 if let Some ( ref relevant) = self . relevant_actions {
36293698 let relevant_set: HashSet < usize > = relevant. iter ( ) . copied ( ) . collect ( ) ;
36303699 let has_relevant = result. iter ( ) . any ( |a| relevant_set. contains ( a) ) ;
36313700 if !has_relevant {
3632- let coi_enabled: Vec < usize > = enabled
3633- . iter ( )
3634- . filter ( |a| relevant_set. contains ( a) )
3635- . copied ( )
3636- . collect ( ) ;
3637- if !coi_enabled. is_empty ( ) {
3638- result. extend ( coi_enabled) ;
3701+ for & a in enabled {
3702+ if relevant_set. contains ( & a) {
3703+ result. push ( a) ;
3704+ }
36393705 }
36403706 }
36413707 }
@@ -4415,7 +4481,7 @@ impl Explorer {
44154481 self . enumerate_changed (
44164482 & changed_domains,
44174483 0 ,
4418- & mut ( * state. vars ) . clone ( ) ,
4484+ & mut state. vars . to_vec ( ) ,
44194485 & mut |next_vars : Vec < Value > | {
44204486 let mut ctx = EvalContext :: new ( & state. vars , & next_vars, & self . consts , params) ;
44214487 if eval ( & action. effect , & mut ctx)
0 commit comments