1from __future__ import annotations
  2
  3import json
  4import logging
  5from dataclasses import dataclass
  6from typing import TYPE_CHECKING, Any, Final
  7
  8from ..cterm import CSubst, CTerm, build_claim
  9from ..kast.inner import KApply, KInner, Subst
 10from ..kast.manip import extract_lhs, extract_rhs, flatten_label
 11from ..prelude.k import GENERATED_TOP_CELL
 12from ..prelude.kbool import BOOL, FALSE, TRUE
 13from ..prelude.ml import is_bottom, is_top, mlAnd, mlEquals, mlEqualsFalse, mlEqualsTrue
 14from ..utils import ensure_dir_path
 15from .proof import FailureInfo, Proof, ProofStatus, ProofSummary, Prover, StepResult
 16
 17if TYPE_CHECKING:
 18    from collections.abc import Iterable, Mapping
 19    from pathlib import Path
 20
 21    from ..kast.inner import KSort
 22    from ..kast.outer import KClaim, KDefinition
 23    from ..kcfg import KCFGExplore
 24    from ..ktool.kprint import KPrint
 25
 26_LOGGER: Final = logging.getLogger(__name__)
 27
 28
[docs]
 29class ImpliesProof(Proof):
 30    antecedent: KInner
 31    consequent: KInner
 32    bind_universally: bool
 33    simplified_antecedent: KInner | None
 34    simplified_consequent: KInner | None
 35    csubst: CSubst | None
 36
 37    def __init__(
 38        self,
 39        id: str,
 40        antecedent: KInner,
 41        consequent: KInner,
 42        bind_universally: bool = False,
 43        simplified_antecedent: KInner | None = None,
 44        simplified_consequent: KInner | None = None,
 45        csubst: CSubst | None = None,
 46        proof_dir: Path | None = None,
 47        subproof_ids: Iterable[str] = (),
 48        admitted: bool = False,
 49    ):
 50        super().__init__(id=id, proof_dir=proof_dir, subproof_ids=subproof_ids, admitted=admitted)
 51        self.antecedent = antecedent
 52        self.consequent = consequent
 53        self.bind_universally = bind_universally
 54        self.simplified_antecedent = simplified_antecedent
 55        self.simplified_consequent = simplified_consequent
 56        self.csubst = csubst
 57
[docs]
 58    def commit(self, result: StepResult) -> None:
 59        proof_type = type(self).__name__
 60        if isinstance(result, ImpliesProofResult):
 61            self.csubst = result.csubst
 62            self.simplified_antecedent = result.simplified_antecedent
 63            self.simplified_consequent = result.simplified_consequent
 64            _LOGGER.info(f'{proof_type} finished {self.id}: {self.status}')
 65        else:
 66            raise ValueError(f'Incorrect result type, expected ImpliesProofResult: {result}') 
 67
 68    @property
 69    def own_status(self) -> ProofStatus:
 70        if self.admitted:
 71            return ProofStatus.PASSED
 72        if self.simplified_antecedent is None or self.simplified_consequent is None:
 73            return ProofStatus.PENDING
 74        if self.csubst is None:
 75            return ProofStatus.FAILED
 76        return ProofStatus.PASSED
 77
 78    @property
 79    def can_progress(self) -> bool:
 80        return self.simplified_antecedent is None or self.simplified_consequent is None
 81
[docs]
 82    def write_proof_data(self, subproofs: bool = False) -> None:
 83        super().write_proof_data()
 84        if not self.proof_dir:
 85            return
 86        ensure_dir_path(self.proof_dir)
 87        ensure_dir_path(self.proof_dir / self.id)
 88        proof_path = self.proof_dir / self.id / 'proof.json'
 89        if not self.up_to_date:
 90            proof_json = json.dumps(self.dict)
 91            proof_path.write_text(proof_json)
 92            _LOGGER.info(f'Updated proof file {self.id}: {proof_path}') 
 93
[docs]
 94    @classmethod
 95    def from_dict(cls: type[ImpliesProof], dct: Mapping[str, Any], proof_dir: Path | None = None) -> ImpliesProof:
 96        id = dct['id']
 97        antecedent = KInner.from_dict(dct['antecedent'])
 98        consequent = KInner.from_dict(dct['consequent'])
 99        simplified_antecedent = (
100            KInner.from_dict(dct['simplified_antecedent']) if 'simplified_antecedent' in dct else None
101        )
102        simplified_consequent = (
103            KInner.from_dict(dct['simplified_consequent']) if 'simplified_consequent' in dct else None
104        )
105        csubst = CSubst.from_dict(dct['csubst']) if 'csubst' in dct else None
106        subproof_ids = dct['subproof_ids']
107        admitted = dct.get('admitted', False)
108        return ImpliesProof(
109            id,
110            antecedent,
111            consequent,
112            simplified_antecedent=simplified_antecedent,
113            simplified_consequent=simplified_consequent,
114            csubst=csubst,
115            admitted=admitted,
116            subproof_ids=subproof_ids,
117            proof_dir=proof_dir,
118        ) 
119
120    @property
121    def dict(self) -> dict[str, Any]:
122        dct = super().dict
123        dct['type'] = 'ImpliesProof'
124        dct['antecedent'] = self.antecedent.to_dict()
125        dct['consequent'] = self.consequent.to_dict()
126        if self.simplified_antecedent is not None:
127            dct['simplified_antecedent'] = self.simplified_antecedent.to_dict()
128        if self.simplified_consequent is not None:
129            dct['simplified_consequent'] = self.simplified_consequent.to_dict()
130        if self.csubst is not None:
131            dct['csubst'] = self.csubst.to_dict()
132        return dct 
133
134
[docs]
135class EqualityProof(ImpliesProof):
136    def __init__(
137        self,
138        id: str,
139        lhs_body: KInner,
140        rhs_body: KInner,
141        sort: KSort,
142        constraints: Iterable[KInner] = (),
143        simplified_constraints: KInner | None = None,
144        simplified_equality: KInner | None = None,
145        csubst: CSubst | None = None,
146        proof_dir: Path | None = None,
147        subproof_ids: Iterable[str] = (),
148        admitted: bool = False,
149    ):
150        antecedent = mlAnd(constraints)
151        consequent = mlEquals(lhs_body, rhs_body, arg_sort=sort, sort=GENERATED_TOP_CELL)
152        super().__init__(
153            id,
154            antecedent,
155            consequent,
156            bind_universally=True,
157            simplified_antecedent=simplified_constraints,
158            simplified_consequent=simplified_equality,
159            csubst=csubst,
160            proof_dir=proof_dir,
161            subproof_ids=subproof_ids,
162            admitted=admitted,
163        )
164        _LOGGER.warning(
165            'Building an EqualityProof that has known soundness issues: See https://github.com/runtimeverification/haskell-backend/issues/3605.'
166        )
167
[docs]
168    @staticmethod
169    def read_proof_data(proof_dir: Path, id: str) -> EqualityProof:
170        proof_path = proof_dir / id / 'proof.json'
171        if Proof.proof_data_exists(id, proof_dir):
172            proof_dict = json.loads(proof_path.read_text())
173            return EqualityProof.from_dict(proof_dict, proof_dir)
174
175        raise ValueError(f'Could not load Proof from file {id}: {proof_path}') 
176
[docs]
177    @staticmethod
178    def from_claim(claim: KClaim, defn: KDefinition, proof_dir: Path | None = None) -> EqualityProof:
179        claim_body = defn.add_sort_params(claim.body)
180        sort = defn.sort_strict(claim_body)
181        lhs_body = extract_lhs(claim_body)
182        rhs_body = extract_rhs(claim_body)
183        if not (claim.ensures is None or claim.ensures == TRUE):
184            raise ValueError(f'Cannot convert claim to EqualityProof due to non-trival ensures clause {claim.ensures}')
185        constraints = [mlEquals(TRUE, c, arg_sort=BOOL) for c in flatten_label('_andBool_', claim.requires)]
186        return EqualityProof(claim.label, lhs_body, rhs_body, sort, constraints=constraints, proof_dir=proof_dir) 
187
188    @property
189    def equality(self) -> KApply:
190        assert type(self.consequent) is KApply
191        return self.consequent
192
193    @property
194    def lhs_body(self) -> KInner:
195        return self.equality.args[0]
196
197    @property
198    def rhs_body(self) -> KInner:
199        return self.equality.args[1]
200
201    @property
202    def sort(self) -> KSort:
203        return self.equality.label.params[0]
204
205    @property
206    def constraint(self) -> KInner:
207        return self.antecedent
208
209    @property
210    def constraints(self) -> list[KInner]:
211        return flatten_label('#And', self.constraint)
212
213    @property
214    def simplified_constraints(self) -> KInner | None:
215        return self.simplified_antecedent
216
217    @property
218    def simplified_equality(self) -> KInner | None:
219        return self.simplified_consequent
220
[docs]
221    @classmethod
222    def from_dict(cls: type[EqualityProof], dct: Mapping[str, Any], proof_dir: Path | None = None) -> EqualityProof:
223        implies_proof = ImpliesProof.from_dict(dct, proof_dir=proof_dir)
224        assert type(implies_proof.consequent) is KApply
225        return EqualityProof(
226            id=implies_proof.id,
227            lhs_body=implies_proof.consequent.args[0],
228            rhs_body=implies_proof.consequent.args[1],
229            sort=implies_proof.consequent.label.params[0],
230            constraints=flatten_label('#And', implies_proof.antecedent),
231            simplified_constraints=implies_proof.simplified_antecedent,
232            simplified_equality=implies_proof.simplified_consequent,
233            csubst=implies_proof.csubst,
234            proof_dir=implies_proof.proof_dir,
235            subproof_ids=implies_proof.subproof_ids,
236            admitted=implies_proof.admitted,
237        ) 
238
239    @property
240    def dict(self) -> dict[str, Any]:
241        dct = super().dict
242        dct['type'] = 'EqualityProof'
243        return dct
244
[docs]
245    def pretty(self, kprint: KPrint) -> Iterable[str]:
246        lines = [
247            f'LHS: {kprint.pretty_print(self.lhs_body)}',
248            f'RHS: {kprint.pretty_print(self.rhs_body)}',
249            f'Constraints: {kprint.pretty_print(mlAnd(self.constraints))}',
250            f'Equality: {kprint.pretty_print(self.equality)}',
251        ]
252        if self.simplified_constraints:
253            lines.append(f'Simplified constraints: {kprint.pretty_print(self.simplified_constraints)}')
254        if self.simplified_equality:
255            lines.append(f'Simplified equality: {kprint.pretty_print(self.simplified_equality)}')
256        if self.csubst is not None:
257            lines.append(f'Implication csubst: {self.csubst}')
258        lines.append(f'Status: {self.status}')
259        return lines 
260
261    @property
262    def summary(self) -> EqualitySummary:
263        return EqualitySummary(self.id, self.status, self.admitted) 
264
265
[docs]
266@dataclass(frozen=True)
267class EqualitySummary(ProofSummary):
268    id: str
269    status: ProofStatus
270    admitted: bool
271
272    @property
273    def lines(self) -> list[str]:
274        return [
275            f'EqualityProof: {self.id}',
276            f'    status: {self.status}',
277            f'    admitted: {self.admitted}',
278        ] 
279
280
[docs]
281class RefutationProof(ImpliesProof):
282    def __init__(
283        self,
284        id: str,
285        pre_constraints: Iterable[KInner],
286        last_constraint: KInner,
287        simplified_antecedent: KInner | None = None,
288        simplified_consequent: KInner | None = None,
289        csubst: CSubst | None = None,
290        proof_dir: Path | None = None,
291        subproof_ids: Iterable[str] = (),
292        admitted: bool = False,
293    ):
294        antecedent = mlAnd(mlEqualsTrue(c) for c in pre_constraints)
295        consequent = mlEqualsFalse(last_constraint)
296        super().__init__(
297            id,
298            antecedent,
299            consequent,
300            bind_universally=True,
301            simplified_antecedent=simplified_antecedent,
302            simplified_consequent=simplified_consequent,
303            csubst=csubst,
304            subproof_ids=subproof_ids,
305            proof_dir=proof_dir,
306            admitted=admitted,
307        )
308        _LOGGER.warning(
309            'Building a RefutationProof that has known soundness issues: See https://github.com/runtimeverification/haskell-backend/issues/3605.'
310        )
311
[docs]
312    @staticmethod
313    def read_proof_data(proof_dir: Path, id: str) -> RefutationProof:
314        proof_path = proof_dir / id / 'proof.json'
315        if Proof.proof_data_exists(id, proof_dir):
316            proof_dict = json.loads(proof_path.read_text())
317            return RefutationProof.from_dict(proof_dict, proof_dir)
318
319        raise ValueError(f'Could not load Proof from file {id}: {proof_path}') 
320
321    @property
322    def pre_constraints(self) -> list[KInner]:
323        return flatten_label('#And', self.antecedent)
324
325    @property
326    def last_constraint(self) -> KInner:
327        assert type(self.consequent) is KApply
328        return self.consequent.args[1]
329
330    @property
331    def simplified_constraints(self) -> KInner | None:
332        return self.simplified_antecedent
333
[docs]
334    @classmethod
335    def from_dict(cls: type[RefutationProof], dct: Mapping[str, Any], proof_dir: Path | None = None) -> RefutationProof:
336        implies_proof = ImpliesProof.from_dict(dct, proof_dir=proof_dir)
337        assert type(implies_proof.consequent) is KApply
338        return RefutationProof(
339            id=implies_proof.id,
340            pre_constraints=flatten_label('#And', implies_proof.antecedent),
341            last_constraint=implies_proof.consequent.args[1],
342            simplified_antecedent=implies_proof.simplified_antecedent,
343            simplified_consequent=implies_proof.simplified_consequent,
344            csubst=implies_proof.csubst,
345            proof_dir=implies_proof.proof_dir,
346            subproof_ids=implies_proof.subproof_ids,
347            admitted=implies_proof.admitted,
348        ) 
349
350    @property
351    def dict(self) -> dict[str, Any]:
352        dct = super().dict
353        dct['type'] = 'RefutationProof'
354        return dct
355
356    @property
357    def summary(self) -> RefutationSummary:
358        return RefutationSummary(self.id, self.status)
359
[docs]
360    def pretty(self, kprint: KPrint) -> Iterable[str]:
361        lines = [
362            f'Constraints: {kprint.pretty_print(mlAnd(self.pre_constraints))}',
363            f'Last constraint: {kprint.pretty_print(self.last_constraint)}',
364        ]
365        if self.csubst is not None:
366            lines.append(f'Implication csubst: {self.csubst}')
367        lines.append(f'Status: {self.status}')
368        return lines 
369
[docs]
370    def to_claim(self, claim_id: str) -> tuple[KClaim, Subst]:
371        return build_claim(
372            claim_id,
373            init_config=self.last_constraint,
374            final_config=FALSE,
375            init_constraints=self.pre_constraints,
376            final_constraints=[],
377        ) 
 
378
379
[docs]
380@dataclass(frozen=True)
381class RefutationSummary(ProofSummary):
382    id: str
383    status: ProofStatus
384
385    @property
386    def lines(self) -> list[str]:
387        return [
388            f'RefutationProof: {self.id}',
389            f'    status: {self.status}',
390        ] 
391
392
[docs]
393@dataclass
394class ImpliesProofResult(StepResult):
395    csubst: CSubst | None
396    simplified_antecedent: KInner | None
397    simplified_consequent: KInner | None 
398
399
[docs]
400class ImpliesProver(Prover):
401    proof: ImpliesProof
402
403    def __init__(self, proof: ImpliesProof, kcfg_explore: KCFGExplore):
404        super().__init__(kcfg_explore)
405        self.proof = proof
406
[docs]
407    def step_proof(self) -> Iterable[StepResult]:
408        proof_type = type(self.proof).__name__
409        _LOGGER.info(f'Attempting {proof_type} {self.proof.id}')
410
411        if self.proof.status is not ProofStatus.PENDING:
412            _LOGGER.info(f'{proof_type} finished {self.proof.id}: {self.proof.status}')
413            return []
414
415        # to prove the equality, we check the implication of the form `constraints #Implies LHS #Equals RHS`, i.e.
416        # "LHS equals RHS under these constraints"
417        simplified_antecedent, _ = self.kcfg_explore.cterm_symbolic.kast_simplify(self.proof.antecedent)
418        simplified_consequent, _ = self.kcfg_explore.cterm_symbolic.kast_simplify(self.proof.consequent)
419        _LOGGER.info(f'Simplified antecedent: {self.kcfg_explore.pretty_print(simplified_antecedent)}')
420        _LOGGER.info(f'Simplified consequent: {self.kcfg_explore.pretty_print(simplified_consequent)}')
421
422        csubst: CSubst | None = None
423
424        if is_bottom(simplified_antecedent):
425            _LOGGER.warning(f'Antecedent of implication (proof constraints) simplifies to #Bottom {self.proof.id}')
426            csubst = CSubst(Subst({}), ())
427
428        elif is_top(simplified_consequent):
429            _LOGGER.warning(f'Consequent of implication (proof equality) simplifies to #Top {self.proof.id}')
430            csubst = CSubst(Subst({}), ())
431
432        else:
433            # TODO: we should not be forced to include the dummy configuration in the antecedent and consequent
434            dummy_config = self.kcfg_explore.cterm_symbolic._definition.empty_config(sort=GENERATED_TOP_CELL)
435            _result = self.kcfg_explore.cterm_symbolic.implies(
436                antecedent=CTerm(config=dummy_config, constraints=[simplified_antecedent]),
437                consequent=CTerm(config=dummy_config, constraints=[simplified_consequent]),
438                bind_universally=self.proof.bind_universally,
439            )
440            result = _result.csubst
441            if result is not None:
442                csubst = result
443
444        _LOGGER.info(f'{proof_type} finished {self.proof.id}: {self.proof.status}')
445        return [
446            ImpliesProofResult(
447                csubst=csubst, simplified_antecedent=simplified_antecedent, simplified_consequent=simplified_consequent
448            )
449        ] 
450
[docs]
451    def failure_info(self) -> FailureInfo:
452        # TODO add implementation
453        return FailureInfo()