Skip to content

Commit 2f4d4f5

Browse files
authored
Cleanups to handling ml predicates and substitutions (#4625)
~Blocked on: #4631 ~Blocked on: #4630 ~Blocked on: #4633 While reviewing and going over #4621 with @Stevengre , it became somewhat clear that how we handle turning substitions into ML predicates is a bit dirty. This attempts to clean this up a bit. Where potentially breaking changes to API are introduced here, I've checked if it affects the following repos when I mention "downstream" below: `evm-semantics kontrol wasm-semantics riscv-semantics mir-semantics`. In particular: - The function `CTerm.anti_unify` has a simplification where it reuses a function from `kast.manip` instead of reimplementing it. - The functions `CSubst.from_pred` and `CSubst.pred` are added, as replacements for `Subst.ml_pred`. This is because `Subst.ml_pred` doesn't have a good way to produce correctly sorted predicates, because it's in module `kast.inner`. - `Subst.ml_pred` is removed, and tests are updated to use the new `CSubst` variant. None of the downstream repositories use `Subst.ml_pred` directly. - The new `CSubst.pred` correctly sorts the generated `#Equals` clauses, defaulting to `K` sort or if a `KDefinition` is supplied using it to do sort inference. It also provides options for controlling whether we include the substitution or the constraints in the generated predicate. - A test is added for a `CSubst.pred` case which caused a bug in the integration tests dealing with identity substitutions. - The `CTermSymbolic.implies` function is updated to reuse `CSubst.from_pred` instead of reimplementing it. - On the case of duplicate entries, the first is kept and the latter are made as predicates.
1 parent 15cbf88 commit 2f4d4f5

File tree

8 files changed

+70
-69
lines changed

8 files changed

+70
-69
lines changed

pyk/src/pyk/cterm/cterm.py

Lines changed: 25 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,12 @@
66
from typing import TYPE_CHECKING
77

88
from ..kast import KInner
9-
from ..kast.inner import KApply, KRewrite, KToken, Subst, bottom_up
9+
from ..kast.inner import KApply, KRewrite, KToken, KVariable, Subst, bottom_up
1010
from ..kast.manip import (
1111
abstract_term_safely,
1212
build_claim,
1313
build_rule,
14+
extract_subst,
1415
flatten_label,
1516
free_vars,
1617
ml_pred_to_bool,
@@ -20,9 +21,9 @@
2021
split_config_and_constraints,
2122
split_config_from,
2223
)
23-
from ..prelude.k import GENERATED_TOP_CELL
24+
from ..prelude.k import GENERATED_TOP_CELL, K
2425
from ..prelude.kbool import andBool, orBool
25-
from ..prelude.ml import is_bottom, is_top, mlAnd, mlBottom, mlEqualsTrue, mlImplies, mlTop
26+
from ..prelude.ml import is_bottom, is_top, mlAnd, mlBottom, mlEquals, mlEqualsTrue, mlImplies, mlTop
2627
from ..utils import unique
2728

2829
if TYPE_CHECKING:
@@ -217,17 +218,7 @@ def anti_unify(
217218
if KToken('true', 'Bool') not in [disjunct_lhs, disjunct_rhs]:
218219
new_cterm = new_cterm.add_constraint(mlEqualsTrue(orBool([disjunct_lhs, disjunct_rhs])))
219220

220-
new_constraints = []
221-
fvs = new_cterm.free_vars
222-
len_fvs = 0
223-
while len_fvs < len(fvs):
224-
len_fvs = len(fvs)
225-
for constraint in common_constraints:
226-
if constraint not in new_constraints:
227-
constraint_fvs = free_vars(constraint)
228-
if any(fv in fvs for fv in constraint_fvs):
229-
new_constraints.append(constraint)
230-
fvs = fvs | constraint_fvs
221+
new_constraints = remove_useless_constraints(common_constraints, new_cterm.free_vars)
231222

232223
for constraint in new_constraints:
233224
new_cterm = new_cterm.add_constraint(constraint)
@@ -341,6 +332,26 @@ def from_dict(dct: dict[str, Any]) -> CSubst:
341332
constraints = (KInner.from_dict(c) for c in dct['constraints'])
342333
return CSubst(subst=subst, constraints=constraints)
343334

335+
@staticmethod
336+
def from_pred(pred: KInner) -> CSubst:
337+
"""Extract from a boolean predicate a CSubst."""
338+
subst, pred = extract_subst(pred)
339+
return CSubst(subst=subst, constraints=flatten_label('#And', pred))
340+
341+
def pred(self, sort_with: KDefinition | None = None, subst: bool = True, constraints: bool = True) -> KInner:
342+
"""Return an ML predicate representing this substitution."""
343+
_preds: list[KInner] = []
344+
if subst:
345+
for k, v in self.subst.minimize().items():
346+
sort = K
347+
if sort_with is not None:
348+
_sort = sort_with.sort(v)
349+
sort = _sort if _sort is not None else sort
350+
_preds.append(mlEquals(KVariable(k, sort=sort), v, arg_sort=sort))
351+
if constraints:
352+
_preds.extend(self.constraints)
353+
return mlAnd(_preds)
354+
344355
@property
345356
def constraint(self) -> KInner:
346357
"""Return the set of constraints as a single flattened constraint using `mlAnd`."""

pyk/src/pyk/cterm/symbolic.py

Lines changed: 3 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@
2525
kore_server,
2626
)
2727
from ..prelude.k import GENERATED_TOP_CELL, K_ITEM
28-
from ..prelude.ml import is_top, mlEquals
28+
from ..prelude.ml import mlAnd
2929

3030
if TYPE_CHECKING:
3131
from collections.abc import Iterable, Iterator
@@ -267,19 +267,8 @@ def implies(
267267
raise ValueError('Received empty predicate for valid implication.')
268268
ml_subst = self.kore_to_kast(result.substitution)
269269
ml_pred = self.kore_to_kast(result.predicate)
270-
ml_preds = flatten_label('#And', ml_pred)
271-
if is_top(ml_subst):
272-
csubst = CSubst(subst=Subst({}), constraints=ml_preds)
273-
return CTermImplies(csubst, (), None, result.logs)
274-
subst_pattern = mlEquals(KVariable('###VAR'), KVariable('###TERM'))
275-
_subst: dict[str, KInner] = {}
276-
for subst_pred in flatten_label('#And', ml_subst):
277-
m = subst_pattern.match(subst_pred)
278-
if m is not None and type(m['###VAR']) is KVariable:
279-
_subst[m['###VAR'].name] = m['###TERM']
280-
else:
281-
raise AssertionError(f'Received a non-substitution from implies endpoint: {subst_pred}')
282-
csubst = CSubst(subst=Subst(_subst), constraints=ml_preds)
270+
ml_subst_pred = mlAnd(flatten_label('#And', ml_subst) + flatten_label('#And', ml_pred))
271+
csubst = CSubst.from_pred(ml_subst_pred)
283272
return CTermImplies(csubst, (), None, result.logs)
284273

285274
def assume_defined(self, cterm: CTerm, module_name: str | None = None) -> CTerm:

pyk/src/pyk/kast/inner.py

Lines changed: 0 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -749,20 +749,6 @@ def from_pred(pred: KInner) -> Subst:
749749
raise ValueError(f'Invalid substitution predicate: {conjunct}')
750750
return Subst(subst)
751751

752-
@property
753-
def ml_pred(self) -> KInner:
754-
"""Turn this `Subst` into a matching logic predicate using `{_#Equals_}` operator."""
755-
items = []
756-
for k in self:
757-
if KVariable(k) != self[k]:
758-
items.append(KApply('#Equals', [KVariable(k), self[k]]))
759-
if len(items) == 0:
760-
return KApply('#Top')
761-
ml_term = items[0]
762-
for _i in items[1:]:
763-
ml_term = KApply('#And', [ml_term, _i])
764-
return ml_term
765-
766752
@property
767753
def pred(self) -> KInner:
768754
"""Turn this `Subst` into a boolean predicate using `_==K_` operator."""

pyk/src/pyk/kcfg/show.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -469,7 +469,9 @@ def dump(self, cfgid: str, cfg: KCFG, dump_dir: Path, dot: bool = False) -> None
469469
cover_file = covers_dir / f'config_{cover.source.id}_{cover.target.id}.txt'
470470
cover_constraint_file = covers_dir / f'constraint_{cover.source.id}_{cover.target.id}.txt'
471471

472-
subst_equalities = flatten_label('#And', cover.csubst.subst.ml_pred)
472+
subst_equalities = flatten_label(
473+
'#And', cover.csubst.pred(sort_with=self.kprint.definition, constraints=False)
474+
)
473475

474476
if not cover_file.exists():
475477
cover_file.write_text('\n'.join(self.kprint.pretty_print(se) for se in subst_equalities))

pyk/src/pyk/kcfg/tui.py

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -309,7 +309,12 @@ def _cterm_text(cterm: CTerm) -> tuple[str, str]:
309309
term_str, constraint_str = _cterm_text(crewrite)
310310

311311
elif type(self._element) is KCFG.Cover:
312-
subst_equalities = map(_boolify, flatten_label('#And', self._element.csubst.subst.ml_pred))
312+
subst_equalities = map(
313+
_boolify,
314+
flatten_label(
315+
'#And', self._element.csubst.pred(sort_with=self._kprint.definition, constraints=False)
316+
),
317+
)
313318
constraints = map(_boolify, flatten_label('#And', self._element.csubst.constraint))
314319
term_str = '\n'.join(self._kprint.pretty_print(se) for se in subst_equalities)
315320
constraint_str = '\n'.join(self._kprint.pretty_print(c) for c in constraints)
@@ -320,7 +325,10 @@ def _cterm_text(cterm: CTerm) -> tuple[str, str]:
320325
term_strs.append('')
321326
term_strs.append(f' - {shorten_hashes(target_id)}')
322327
if len(csubst.subst) > 0:
323-
subst_equalities = map(_boolify, flatten_label('#And', csubst.subst.ml_pred))
328+
subst_equalities = map(
329+
_boolify,
330+
flatten_label('#And', csubst.pred(sort_with=self._kprint.definition, constraints=False)),
331+
)
324332
term_strs.extend(f' {self._kprint.pretty_print(cline)}' for cline in subst_equalities)
325333
if len(csubst.constraints) > 0:
326334
constraints = map(_boolify, flatten_label('#And', csubst.constraint))

pyk/src/pyk/proof/reachability.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -487,14 +487,16 @@ def from_spec_modules(
487487

488488
return res
489489

490-
def path_constraints(self, final_node_id: NodeIdLike) -> KInner:
490+
def path_constraints(self, final_node_id: NodeIdLike, sort_with: KDefinition | None = None) -> KInner:
491491
path = self.shortest_path_to(final_node_id)
492492
curr_constraint: KInner = mlTop()
493493
for edge in reversed(path):
494494
if type(edge) is KCFG.Split:
495495
assert len(edge.targets) == 1
496496
csubst = edge.splits[edge.targets[0].id]
497-
curr_constraint = mlAnd([csubst.subst.minimize().ml_pred, csubst.constraint, curr_constraint])
497+
curr_constraint = mlAnd(
498+
[csubst.pred(sort_with=sort_with, constraints=False), csubst.constraint, curr_constraint]
499+
)
498500
if type(edge) is KCFG.Cover:
499501
curr_constraint = mlAnd([edge.csubst.constraint, edge.csubst.subst.apply(curr_constraint)])
500502
return mlAnd(flatten_label('#And', curr_constraint))

pyk/src/tests/unit/kast/test_subst.py

Lines changed: 0 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@
77

88
from pyk.kast.inner import KApply, KLabel, KVariable, Subst
99
from pyk.kast.manip import extract_subst
10-
from pyk.prelude.kbool import TRUE
1110
from pyk.prelude.kint import INT, intToken
1211
from pyk.prelude.ml import mlAnd, mlEquals, mlEqualsTrue, mlOr, mlTop
1312

@@ -108,25 +107,6 @@ def test_unapply(term: KInner, subst: dict[str, KInner], expected: KInner) -> No
108107
assert actual == expected
109108

110109

111-
ML_PRED_TEST_DATA: Final = (
112-
('empty', Subst({}), KApply('#Top')),
113-
('singleton', Subst({'X': TRUE}), KApply('#Equals', [KVariable('X'), TRUE])),
114-
(
115-
'double',
116-
Subst({'X': TRUE, 'Y': intToken(4)}),
117-
KApply(
118-
'#And',
119-
[KApply('#Equals', [KVariable('X'), TRUE]), KApply('#Equals', [KVariable('Y'), intToken(4)])],
120-
),
121-
),
122-
)
123-
124-
125-
@pytest.mark.parametrize('test_id,subst,pred', ML_PRED_TEST_DATA, ids=[test_id for test_id, *_ in ML_PRED_TEST_DATA])
126-
def test_ml_pred(test_id: str, subst: Subst, pred: KInner) -> None:
127-
assert subst.ml_pred == pred
128-
129-
130110
_0 = intToken(0)
131111
_EQ = KLabel('_==Int_')
132112
EXTRACT_SUBST_TEST_DATA: Final[tuple[tuple[KInner, dict[str, KInner], KInner], ...]] = (

pyk/src/tests/unit/test_cterm.py

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,10 @@
99
from pyk.kast import Atts, KAtt
1010
from pyk.kast.inner import KApply, KLabel, KRewrite, KSequence, KSort, KVariable, Subst
1111
from pyk.kast.outer import KClaim
12-
from pyk.prelude.k import GENERATED_TOP_CELL
12+
from pyk.prelude.k import GENERATED_TOP_CELL, K
13+
from pyk.prelude.kbool import TRUE
1314
from pyk.prelude.kint import INT, intToken
14-
from pyk.prelude.ml import mlAnd, mlEqualsTrue
15+
from pyk.prelude.ml import mlAnd, mlEquals, mlEqualsTrue, mlTop
1516

1617
from .utils import a, b, c, f, g, ge_ml, h, k, lt_ml, x, y, z
1718

@@ -188,6 +189,28 @@ def test_from_kast(test_id: str, kast: KInner, expected: CTerm) -> None:
188189
assert cterm == expected
189190

190191

192+
ML_PRED_TEST_DATA: Final = (
193+
('empty', CSubst(Subst({})), mlTop()),
194+
('singleton', CSubst(Subst({'X': TRUE})), mlEquals(KVariable('X', sort=K), TRUE, arg_sort=K)),
195+
('identity', CSubst(Subst({'X': KVariable('X')})), mlTop()),
196+
(
197+
'double',
198+
CSubst(Subst({'X': TRUE, 'Y': intToken(4)})),
199+
mlAnd(
200+
[
201+
mlEquals(KVariable('X', sort=K), TRUE, arg_sort=K),
202+
mlEquals(KVariable('Y', sort=K), intToken(4), arg_sort=K),
203+
]
204+
),
205+
),
206+
)
207+
208+
209+
@pytest.mark.parametrize('test_id,csubst,pred', ML_PRED_TEST_DATA, ids=[test_id for test_id, *_ in ML_PRED_TEST_DATA])
210+
def test_ml_pred(test_id: str, csubst: CSubst, pred: KInner) -> None:
211+
assert csubst.pred() == pred
212+
213+
191214
APPLY_TEST_DATA: Final = (
192215
(CTerm.top(), CSubst(), CTerm.top()),
193216
(CTerm.bottom(), CSubst(), CTerm.bottom()),

0 commit comments

Comments
 (0)