gargamelle-logic-0.1.0.0: src/Gargamelle/Logic/Contract.hs
{-# LANGUAGE FlexibleContexts #-}
module Gargamelle.Logic.Contract
( AGContract(..)
, contract
, getAssumption
, getGuarantee
, deriveAGGalois
, verifyCircularAG
) where
import Algebra.Lattice (Lattice(..), BoundedJoinSemiLattice(..), BoundedMeetSemiLattice(..))
import Algebra.Lattice.Op (Op(..))
import Algebra.Heyting (Heyting(..))
import Gargamelle.Logic.Defunctionalize (ObsMor(..))
import Gargamelle.Logic.Galois (Galois(..))
{- Note [Lattice Inversion via Op Dualization]
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
[tag:note-lattice-op-dualization]
In formal verification and contract-based design, contract refinement
(C1 ⊑ C2, or meet C1 ∧ C2) means that C1 is a stricter, more capable specification
that can safely substitute for C2 in any environment.
For guarantees G, substitution requires strengthening: if C2 promised G2, C1 must
promise at least G2 (i.e. G1 ⊑ G2, which in standard logic or lattice refinement
corresponds to meet G1 ∧ G2).
For assumptions A, the direction of refinement is fundamentally reversed: if C2
required the environment to satisfy demanding assumption A2, a more capable component
C1 should require LESS of its environment (i.e. accept a wider range of behaviors,
A2 ⊑ A1, or A1 ∨ A2 in the base lattice).
Rather than writing manual ad-hoc case analyses or bifurcated order checks, Gargamelle
exploits the functorial order-dual operator 'Algebra.Lattice.Op' from the @lattices@
package:
Op a ∧ Op b = Op (a ∨ b)
Op a ∨ Op b = Op (a ∧ b)
By wrapping assumptions inside 'Op a', contract refinement becomes the standard product
meet:
(Op A1, G1) ∧ (Op A2, G2) = (Op (A1 ∨ A2), G1 ∧ G2)
This mechanically weakens assumptions (expands admitted environments) while simultaneously
strengthening guarantees (restricts emitted behaviors) with zero runtime branching or
heuristic case splits.
-}
{- Note [Circular Assume-Guarantee Soundness]
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
[tag:note-circular-ag-soundness]
When two components C1 = (A1, G1) and C2 = (A2, G2) interact concurrently across a shared
interface, mutual dependencies can lead to apparent cyclic reasoning:
- C1 guarantees G1 provided C2 satisfies assumption A1.
- C2 guarantees G2 provided C1 satisfies assumption A2.
In a Heyting algebra or intuitionistic logic lattice, circular reasoning is sound if and
only if each component's guarantee discharges the other component's assumption under the
shared interface context:
dir1 = (A1 ∧ G2) ⟹ G1
dir2 = (A2 ∧ G1) ⟹ G2
Sound composition holds when both entailments evaluate to the top element of the lattice:
(dir1 ∧ dir2) == ⊤
If either component fails to establish its guarantee under the environment provided by
its partner, the circular check evaluates to a non-top element, pinpointing an interface
mismatch before deployment.
-}
-- | Assume-Guarantee contract using 'Algebra.Lattice.Op' from @lattices@.
--
-- See Note [Lattice Inversion via Op Dualization] [ref:note-lattice-op-dualization].
--
-- Because refinement is meet (/\), wrapping assumptions in 't:Op' ensures that
-- contract refinement automatically weakens assumptions (as joins \/ in the base lattice)
-- while strengthening guarantees (as meets /\ in the base lattice).
data AGContract a g = AGContract
{ assumptions :: Op a
, guarantees :: g
} deriving (Eq, Show)
-- | Smart constructor for AGContract.
contract :: a -> g -> AGContract a g
contract a = AGContract (Op a)
-- | Extract the raw assumption value.
getAssumption :: AGContract a g -> a
getAssumption (AGContract (Op a) _) = a
-- | Extract the guarantee value.
getGuarantee :: AGContract a g -> g
getGuarantee (AGContract _ g) = g
instance (Lattice a, Lattice g) => Lattice (AGContract a g) where
AGContract a1 g1 /\ AGContract a2 g2 = AGContract (a1 /\ a2) (g1 /\ g2)
AGContract a1 g1 \/ AGContract a2 g2 = AGContract (a1 \/ a2) (g1 \/ g2)
instance (BoundedMeetSemiLattice a, BoundedJoinSemiLattice a, BoundedMeetSemiLattice g)
=> BoundedMeetSemiLattice (AGContract a g) where
top = AGContract (Op bottom) top
instance (BoundedMeetSemiLattice a, BoundedJoinSemiLattice g)
=> BoundedJoinSemiLattice (AGContract a g) where
bottom = AGContract (Op top) bottom
-- | Lift component projections/liftings to an observable Galois connection on AGContracts.
deriveAGGalois
:: (a1 -> a2) -> (a2 -> a1)
-> (g1 -> g2) -> (g2 -> g1)
-> Galois ObsMor (AGContract a1 g1) (AGContract a2 g2)
deriveAGGalois aRestrict aCoRestrict gRestrict gCoRestrict = Galois
{ restrict = SymbolicStep "ag-restrict" $ \(AGContract (Op a) g) -> AGContract (Op (aRestrict a)) (gRestrict g)
, coRestrict = SymbolicStep "ag-coRestrict" $ \(AGContract (Op a) g) -> AGContract (Op (aCoRestrict a)) (gCoRestrict g)
}
-- | Circular Assume-Guarantee Soundness Check on a shared interface:
-- Checks that (A1 /\ G2 ==> G1) /\ (A2 /\ G1 ==> G2) == top.
--
-- See Note [Circular Assume-Guarantee Soundness] [ref:note-circular-ag-soundness].
verifyCircularAG
:: (Heyting l, Eq l)
=> AGContract l l
-> AGContract l l
-> Bool
verifyCircularAG (AGContract (Op a1) g1) (AGContract (Op a2) g2) =
let dir1 = (a1 /\ g2) ==> g1
dir2 = (a2 /\ g1) ==> g2
in (dir1 /\ dir2) == top