|
26 | 26 | collect,
|
27 | 27 | extract_lhs,
|
28 | 28 | flatten_label,
|
| 29 | + free_vars, |
29 | 30 | minimize_term,
|
30 | 31 | set_cell,
|
31 | 32 | top_down,
|
|
34 | 35 | from pyk.kcfg import KCFG
|
35 | 36 | from pyk.kcfg.kcfg import Step
|
36 | 37 | from pyk.kcfg.minimize import KCFGMinimizer
|
| 38 | +from pyk.kdist import kdist |
37 | 39 | from pyk.prelude.bytes import bytesToken
|
38 | 40 | from pyk.prelude.collections import map_empty
|
39 |
| -from pyk.prelude.k import DOTS |
| 41 | +from pyk.prelude.k import DOTS, GENERATED_TOP_CELL |
40 | 42 | from pyk.prelude.kbool import notBool
|
41 | 43 | from pyk.prelude.kint import INT, intToken
|
42 | 44 | from pyk.prelude.ml import mlEqualsFalse, mlEqualsTrue
|
@@ -103,7 +105,7 @@ def cut_point_rules(
|
103 | 105 | break_on_basic_blocks: bool,
|
104 | 106 | break_on_load_program: bool,
|
105 | 107 | ) -> list[str]:
|
106 |
| - return ['FOUNDRY-CHEAT-CODES.rename'] + KEVMSemantics.cut_point_rules( |
| 108 | + return ['FOUNDRY-CHEAT-CODES.rename', 'FOUNDRY-ACCOUNTS.forget'] + KEVMSemantics.cut_point_rules( |
107 | 109 | break_on_jumpi,
|
108 | 110 | break_on_jump,
|
109 | 111 | break_on_calls,
|
@@ -153,14 +155,145 @@ def _exec_rename_custom_step(self, cterm: CTerm) -> KCFGExtendResult | None:
|
153 | 155 | _LOGGER.info(f'Renaming {target_var.name} to {name}')
|
154 | 156 | return Step(CTerm(new_cterm.config, constraints), 1, (), ['foundry_rename'], cut=True)
|
155 | 157 |
|
156 |
| - def custom_step(self, cterm: CTerm, _cterm_symbolic: CTermSymbolic) -> KCFGExtendResult | None: |
| 158 | + def _check_forget_pattern(self, cterm: CTerm) -> bool: |
| 159 | + """Given a CTerm, check if the rule 'FOUNDRY-ACCOUNTS.forget' is at the top of the K_CELL. |
| 160 | + This method checks if the 'FOUNDRY-ACCOUNTS.forget' rule is at the top of the `K_CELL` in the given `cterm`. |
| 161 | + If the rule matches, the resulting substitution is cached in `_cached_subst` for later use in `custom_step` |
| 162 | + :param cterm: The CTerm representing the current state of the proof node. |
| 163 | + :return: `True` if the pattern matches and a custom step can be made; `False` otherwise. |
| 164 | + """ |
| 165 | + abstract_pattern = KSequence( |
| 166 | + [ |
| 167 | + KApply('cheatcode_forget', [KVariable('###TERM1'), KVariable('###OPERATOR'), KVariable('###TERM2')]), |
| 168 | + KVariable('###CONTINUATION'), |
| 169 | + ] |
| 170 | + ) |
| 171 | + self._cached_subst = abstract_pattern.match(cterm.cell('K_CELL')) |
| 172 | + return self._cached_subst is not None |
| 173 | + |
| 174 | + def _exec_forget_custom_step(self, cterm: CTerm, cterm_symbolic: CTermSymbolic) -> KCFGExtendResult | None: |
| 175 | + """Remove the constraint at the top of K_CELL of a given CTerm from its path constraints, |
| 176 | + as part of the 'FOUNDRY-ACCOUNTS.forget' cut-rule. |
| 177 | + :param cterm: CTerm representing a proof node |
| 178 | + :param cterm_symbolic: CTermSymbolic instance |
| 179 | + :return: A Step of depth 1 carrying a new configuration in which the constraint is consumed from the top |
| 180 | + of the K cell and is removed from the initial path constraints if it existed, together with |
| 181 | + information that the `cheatcode_forget` rule has been applied. |
| 182 | + """ |
| 183 | + |
| 184 | + def _find_constraints_to_keep(cterm: CTerm, constraint_vars: frozenset[str]) -> set[KInner]: |
| 185 | + range_patterns: list[KInner] = [ |
| 186 | + mlEqualsTrue(KApply('_<Int_', KVariable('###VARL', INT), KVariable('###VARR', INT))), |
| 187 | + mlEqualsTrue(KApply('_<=Int_', KVariable('###VARL', INT), KVariable('###VARR', INT))), |
| 188 | + mlEqualsTrue(notBool(KApply('_==Int_', KVariable('###VARL', INT), KVariable('###VARR', INT)))), |
| 189 | + ] |
| 190 | + constraints_to_keep: set[KInner] = set() |
| 191 | + for constraint in cterm.constraints: |
| 192 | + for pattern in range_patterns: |
| 193 | + subst_rcp = pattern.match(constraint) |
| 194 | + if subst_rcp is not None and ( |
| 195 | + ( |
| 196 | + type(subst_rcp['###VARL']) is KVariable |
| 197 | + and subst_rcp['###VARL'].name in constraint_vars |
| 198 | + and type(subst_rcp['###VARR']) is KToken |
| 199 | + ) |
| 200 | + or ( |
| 201 | + type(subst_rcp['###VARR']) is KVariable |
| 202 | + and subst_rcp['###VARR'].name in constraint_vars |
| 203 | + and type(subst_rcp['###VARL']) is KToken |
| 204 | + ) |
| 205 | + ): |
| 206 | + constraints_to_keep.add(constraint) |
| 207 | + break |
| 208 | + return constraints_to_keep |
| 209 | + |
| 210 | + def _filter_constraints_by_simplification( |
| 211 | + cterm_symbolic: CTermSymbolic, |
| 212 | + initial_cterm: CTerm, |
| 213 | + constraints_to_remove: list[KInner], |
| 214 | + constraints_to_keep: set[KInner], |
| 215 | + constraints: set[KInner], |
| 216 | + empty_config: CTerm, |
| 217 | + ) -> set[KInner]: |
| 218 | + for constraint_variant in constraints_to_remove: |
| 219 | + simplification_cterm = initial_cterm.add_constraint(constraint_variant) |
| 220 | + result_cterm, _ = cterm_symbolic.simplify(simplification_cterm) |
| 221 | + # Extract constraints that appear after simplification but are not in the 'to keep' set |
| 222 | + result_constraints = set(result_cterm.constraints).difference(constraints_to_keep) |
| 223 | + |
| 224 | + if len(result_constraints) == 1: |
| 225 | + target_constraint = single(result_constraints) |
| 226 | + if target_constraint in constraints: |
| 227 | + _LOGGER.info(f'forgetBranch: removing constraint: {target_constraint}') |
| 228 | + constraints.remove(target_constraint) |
| 229 | + break |
| 230 | + else: |
| 231 | + _LOGGER.info(f'forgetBranch: constraint: {target_constraint} not found in current constraints') |
| 232 | + else: |
| 233 | + # If no constraints or multiple constraints appear, log this scenario. |
| 234 | + if len(result_constraints) == 0: |
| 235 | + _LOGGER.info(f'forgetBranch: constraint {constraint_variant} entailed by remaining constraints') |
| 236 | + result_cterm, _ = cterm_symbolic.simplify(CTerm(empty_config.config, [constraint_variant])) |
| 237 | + if len(result_cterm.constraints) == 1: |
| 238 | + to_remove = single(result_cterm.constraints) |
| 239 | + if to_remove in constraints: |
| 240 | + _LOGGER.info(f'forgetBranch: removing constraint: {to_remove}') |
| 241 | + constraints.remove(to_remove) |
| 242 | + else: |
| 243 | + _LOGGER.info( |
| 244 | + f'forgetBranch: more than one constraint found after simplification and removal:\n{result_constraints}' |
| 245 | + ) |
| 246 | + return constraints |
| 247 | + |
| 248 | + _operators = ['_==Int_', '_=/=Int_', '_<=Int_', '_<Int_', '_>=Int_', '_>Int_'] |
| 249 | + subst = self._cached_subst |
| 250 | + assert subst is not None |
| 251 | + # Extract the terms and operator from the substitution |
| 252 | + fst_term = subst['###TERM1'] |
| 253 | + snd_term = subst['###TERM2'] |
| 254 | + operator = subst['###OPERATOR'] |
| 255 | + assert isinstance(operator, KToken) |
| 256 | + # Construct the positive and negative constraints |
| 257 | + pos_constraint = mlEqualsTrue(KApply(_operators[int(operator.token)], fst_term, snd_term)) |
| 258 | + neg_constraint = mlEqualsTrue(notBool(KApply(_operators[int(operator.token)], fst_term, snd_term))) |
| 259 | + # To be able to better simplify, we maintain range constraints on the variables present in the constraint |
| 260 | + constraint_vars: frozenset[str] = free_vars(fst_term).union(free_vars(snd_term)) |
| 261 | + constraints_to_keep: set[KInner] = _find_constraints_to_keep(cterm, constraint_vars) |
| 262 | + |
| 263 | + # Set up initial configuration for constraint simplification, and simplify it to get all |
| 264 | + # of the kept constraints in the form in which they will appear after constraint simplification |
| 265 | + kevm = KEVM(kdist.get('kontrol.foundry')) |
| 266 | + empty_config: CTerm = CTerm.from_kast(kevm.definition.empty_config(GENERATED_TOP_CELL)) |
| 267 | + initial_cterm, _ = cterm_symbolic.simplify(CTerm(empty_config.config, constraints_to_keep)) |
| 268 | + constraints_to_keep = set(initial_cterm.constraints) |
| 269 | + |
| 270 | + # Simplify in the presence of constraints to keep, then remove the constraints to keep to |
| 271 | + # reveal simplified constraint, then remove if present in original constraints |
| 272 | + new_constraints: set[KInner] = _filter_constraints_by_simplification( |
| 273 | + cterm_symbolic=cterm_symbolic, |
| 274 | + initial_cterm=initial_cterm, |
| 275 | + constraints_to_remove=[pos_constraint, neg_constraint], |
| 276 | + constraints_to_keep=constraints_to_keep, |
| 277 | + constraints=set(cterm.constraints), |
| 278 | + empty_config=empty_config, |
| 279 | + ) |
| 280 | + |
| 281 | + # Update the K_CELL with the continuation |
| 282 | + new_cterm = CTerm.from_kast(set_cell(cterm.kast, 'K_CELL', KSequence(subst['###CONTINUATION']))) |
| 283 | + return Step(CTerm(new_cterm.config, new_constraints), 1, (), ['cheatcode_forget'], cut=True) |
| 284 | + |
| 285 | + def custom_step(self, cterm: CTerm, cterm_symbolic: CTermSymbolic) -> KCFGExtendResult | None: |
157 | 286 | if self._check_rename_pattern(cterm):
|
158 | 287 | return self._exec_rename_custom_step(cterm)
|
| 288 | + elif self._check_forget_pattern(cterm): |
| 289 | + return self._exec_forget_custom_step(cterm, cterm_symbolic) |
159 | 290 | else:
|
160 |
| - return super().custom_step(cterm, _cterm_symbolic) |
| 291 | + return super().custom_step(cterm, cterm_symbolic) |
161 | 292 |
|
162 | 293 | def can_make_custom_step(self, cterm: CTerm) -> bool:
|
163 |
| - return self._check_rename_pattern(cterm) or super().can_make_custom_step(cterm) |
| 294 | + return any( |
| 295 | + [self._check_rename_pattern(cterm), self._check_forget_pattern(cterm), super().can_make_custom_step(cterm)] |
| 296 | + ) |
164 | 297 |
|
165 | 298 |
|
166 | 299 | class FoundryKEVM(KEVM):
|
|
0 commit comments