66from __future__ import annotations
77
88import itertools
9- from collections import OrderedDict
109from dataclasses import dataclass , field
1110from typing import TYPE_CHECKING , Any , Iterator
1211
13- from sympy import Not , Or
14- from sympy .logic . boolalg import BooleanFunction
12+ from sympy import Equivalent , Not , Or , Symbol
13+ from sympy .assumptions . cnf import EncodedCNF
1514from sympy .logic .inference import satisfiable
1615
1716from cfnlint .conditions ._utils import get_hash
2625 from cfnlint .context .context import Context , Parameter
2726
2827
29- # Use OrderedDict for LRU-like behavior
30- _satisfiable_cache : OrderedDict [str , bool ] = OrderedDict ()
31- _MAX_CACHE_SIZE = 10000 # Limit cache size to prevent memory issues
32-
33-
34- def _get_from_satisfiable_cache (cnf_hash : str ) -> bool | None :
35- """Get result from cache with LRU behavior"""
36- if cnf_hash in _satisfiable_cache :
37- # Move to end (most recently used)
38- value = _satisfiable_cache .pop (cnf_hash )
39- _satisfiable_cache [cnf_hash ] = value
40- return value
41- return None
42-
43-
44- def _add_to_satisfiable_cache (cnf_hash : str , result : bool ) -> None :
45- """Add result to cache with size management"""
46- if len (_satisfiable_cache ) >= _MAX_CACHE_SIZE :
47- # Remove oldest item (first in OrderedDict)
48- _satisfiable_cache .popitem (last = False )
49- _satisfiable_cache [cnf_hash ] = result
50-
51-
5228@dataclass (frozen = True )
5329class Conditions :
54- # Template level condition management
5530 conditions : dict [str , Condition ] = field (init = True , default_factory = dict )
56- cnf : BooleanFunction | None = field (init = True , default = None )
31+ cnf : EncodedCNF = field (init = True , default_factory = EncodedCNF , compare = False )
32+ _condition_symbols : dict [str , Symbol ] = field (
33+ init = True , default_factory = dict , compare = False
34+ )
5735 _max_scenarios : int = field (init = False , default = 128 )
5836
5937 @classmethod
6038 def create_from_instance (
61- cls , conditions : Any , rules : dict [str , dict ], parameters : dict [str , "Parameter" ]
39+ cls ,
40+ conditions : Any ,
41+ rules : dict [str , dict ],
42+ parameters : dict [str , "Parameter" ],
6243 ) -> "Conditions" :
6344 obj : dict [str , Condition ] = {}
6445 if not isinstance (conditions , dict ):
@@ -69,13 +50,20 @@ def create_from_instance(
6950 del other_conditions [k ]
7051 obj [k ] = Condition .create_from_instance (v , other_conditions )
7152 except ValueError :
72- # this is a default condition so we can keep the name but it will
73- # not associate with another condition and will always be true/false
7453 obj [k ] = Condition .create_from_instance (
7554 {"Fn::Equals" : [None , None ]}, conditions
7655 )
7756
78- cnf = None
57+ # Build EncodedCNF with a Symbol per condition name
58+ cnf = EncodedCNF ()
59+ condition_symbols : dict [str , Symbol ] = {}
60+ for name , cond in obj .items ():
61+ sym = Symbol (name )
62+ condition_symbols [name ] = sym
63+ # Add equivalence: Symbol(name) <-> condition's boolean expression
64+ cnf .add_prop (Equivalent (sym , cond .cnf ))
65+
66+ # Add parameter AllowedValues constraints
7967 for p_k , p_v in parameters .items ():
8068 if not p_v .allowed_values :
8169 continue
@@ -90,21 +78,17 @@ def create_from_instance(
9078 if i .left .instance in allowed_values :
9179 allowed_values .remove (i .left .instance )
9280
93- if not allowed_values :
94- if cnf is None :
95- cnf = Or (* equals_cnfs )
96- else :
97- cnf = cnf & Or (* equals_cnfs )
81+ if not allowed_values and equals_cnfs :
82+ cnf .add_prop (Or (* equals_cnfs ))
9883
99- return cls (conditions = obj , cnf = cnf )
84+ return cls (conditions = obj , cnf = cnf , _condition_symbols = condition_symbols )
10085
10186 def evolve (self , status : dict [str , bool ]) -> "Conditions" :
10287 cls = self .__class__
10388
10489 if not status :
10590 return self
10691
107- # Check if we're trying to set the same status
10892 all_same = True
10993 for condition , condition_status in status .items ():
11094 if (
@@ -117,50 +101,30 @@ def evolve(self, status: dict[str, bool]) -> "Conditions":
117101 return self
118102
119103 conditions : dict [str , Condition ] = {}
120- cnf = self .cnf
104+ cnf = self .cnf . copy ()
121105 for condition , value in self .conditions .items ():
122106 s = status .get (condition , value .status )
123107 try :
124108 conditions [condition ] = value .evolve (status = s )
125- if s is not None :
126- if cnf :
127- cnf = (
128- cnf & conditions [condition ].cnf
129- if s
130- else cnf & Not (conditions [condition ].cnf )
131- )
132- else :
133- cnf = (
134- conditions [condition ].cnf
135- if s
136- else Not (conditions [condition ].cnf )
137- )
109+ if s is not None and condition in self ._condition_symbols :
110+ sym = self ._condition_symbols [condition ]
111+ cnf .add_prop (sym if s else Not (sym ))
138112 except ValueError as e :
139113 raise Unsatisfiable (
140114 new_status = status ,
141115 current_status = self .status ,
142116 ) from e
143117
144- cnf_hash = get_hash (str (cnf ))
145- cached_result = _get_from_satisfiable_cache (cnf_hash )
146- if cached_result is not None :
147- if not cached_result :
148- raise Unsatisfiable (
149- new_status = status ,
150- current_status = self .status ,
151- )
152- else :
153- is_sat = satisfiable (cnf )
154- _add_to_satisfiable_cache (cnf_hash , bool (is_sat ))
155- if not is_sat :
156- raise Unsatisfiable (
157- new_status = status ,
158- current_status = self .status ,
159- )
118+ if not satisfiable (cnf ):
119+ raise Unsatisfiable (
120+ new_status = status ,
121+ current_status = self .status ,
122+ )
160123
161124 return cls (
162125 conditions = conditions ,
163126 cnf = cnf ,
127+ _condition_symbols = self ._condition_symbols ,
164128 )
165129
166130 def _build_conditions (self , conditions : set [str ]) -> Iterator ["Conditions" ]:
0 commit comments