packages feed

free-foil-0.5.0: test/laws/Control/Monad/Free/Foil/Laws/Mirror.hs

{-# LANGUAGE DataKinds           #-}
{-# LANGUAGE FlexibleContexts    #-}
{-# LANGUAGE GADTs               #-}
{-# LANGUAGE LambdaCase          #-}
{-# LANGUAGE RankNTypes          #-}
{-# LANGUAGE ScopedTypeVariables #-}
-- | The laws of "Control.Monad.Free.Foil.Laws" for a language written with
-- the plain foil: its 'Sinkable' instance, 'rbind', 'substitute' and
-- α-equivalence. Terms are generated in a /mirror/, a free foil syntax with
-- the same constructors, and compared by converting them back and using
-- 'alphaEqNameless'.
module Control.Monad.Free.Foil.Laws.Mirror (
  Mirror (..),
  AlphaEquivOf (..),
  MirrorLaw (..),
  mirrorSpec,
) where

import           Control.Monad                (forM_)
import           Data.Bitraversable           (Bitraversable)
import           Test.Hspec
import           Test.QuickCheck

import           Control.Monad.Foil
import           Control.Monad.Foil.Internal  (Substitution (..))
import           Control.Monad.Foil.Laws
import           Control.Monad.Foil.Relative  (RelMonad, liftRM, rbind, rreturn)
import           Control.Monad.Free.Foil      (AST, refreshAST)
import           Control.Monad.Free.Foil.Laws
import           Data.ZipMatchK               (ZipMatchK)

-- | A plain foil language @e@ and its free foil mirror.
data Mirror binder sig e = Mirror
  { mirrorSyntax     :: SyntaxGen binder sig
    -- | From the mirror.
  , mirrorFrom       :: forall n. AST binder sig n -> e n
    -- | To the mirror.
  , mirrorTo         :: forall n. e n -> AST binder sig n
    -- | The language's 'substitute'.
  , mirrorSubstitute :: forall i o. Distinct o => Scope o -> Substitution e i o -> e i -> e o
    -- | The language's α-equivalence, if it has one.
  , mirrorAlphaEquiv :: Maybe (AlphaEquivOf e)
  }

-- | An α-equivalence check of a language.
newtype AlphaEquivOf e = AlphaEquivOf (forall n. Distinct n => Scope n -> e n -> e n -> Bool)

-- | The laws checked by 'mirrorSpec', by name.
data MirrorLaw
  = MirrorSinkable SinkLaw
  | MirrorLiftRMIdentity
  | MirrorLiftRMComposition
  | MirrorSinkAgreesWithLiftRM
  | MirrorSubstLeftUnit
  | MirrorSubstRightUnit
  | MirrorSubstRightUnitExplicit
  | MirrorSubstAssociativity
  | MirrorRbindRightUnit
  | MirrorRbindAssociativity
  | MirrorSubstituteAgreesWithRbind
  | MirrorAlphaEquivRefresh
  | MirrorAlphaEquivAgrees
  deriving (Eq, Show)

-- | Convert the values of a substitution.
mapSubst :: (e o -> e' o) -> Substitution e i o -> Substitution e' i o
mapSubst f (UnsafeSubstitution env) = UnsafeSubstitution (fmap f env)

-- | All the laws for a plain foil language, given a verdict for each law
-- along each class of renamings (the class is 'Inclusions' for the laws
-- that take no renaming).
mirrorSpec
  :: forall binder sig e.
     ( Bitraversable sig, ZipMatchK sig, UnifiablePattern binder, SinkableK binder
     , Sinkable e, InjectName e, RelMonad Name e )
  => Mirror binder sig e
  -> (RenamingClass -> MirrorLaw -> Verdict)
  -> Spec
mirrorSpec m verdict = do
  describe "Sinkable" $
    sinkableSpec (const eqE) showE
      (\ctx -> from <$> sized (genAST sg ctx . min 30))
      (map from . shrinkAST . to)
      (\cls name -> verdict cls (MirrorSinkable name))

  describe "liftRM (through rbind)" $ do
    onTerms MirrorLiftRMIdentity "liftRM n id t ≡α t" $ \n t -> withCtx n $ \scope ->
      cmp "liftRM n id t" (liftRME scope id t) "t" t
    onRenamings MirrorLiftRMComposition "liftRM k (g . f) t ≡α liftRM k g (liftRM l f t)" $
      \(Chain' _ l k f g _) t -> withCtx l $ \scopeL -> withCtx k $ \scopeK ->
        cmp "liftRM k (g . f) t" (liftRME scopeK (rename g . rename f) t)
            "liftRM k g (liftRM l f t)" (liftRME scopeK (rename g) (liftRME scopeL (rename f) t))

  describe "sinkabilityProof and liftRM" $
    onRenamings MirrorSinkAgreesWithLiftRM "sinkabilityProof f t ≡α liftRM l f t" $
      \(Chain' _ l _ f _ _) t -> withCtx l $ \scopeL ->
        cmp "sinkabilityProof f t" (sinkabilityProof (rename f) t)
            "liftRM l f t" (liftRME scopeL (rename f) t)

  describe "substitute" $ do
    onSubsts MirrorSubstLeftUnit "left unit" $ \i scope1 _ s1 _ _ ->
      conjoin [ cmp "substitute o1 s1 x" (subst scope1 s1 (injectName x))
                    "lookupSubst s1 x" (lookupSubst s1 x)
              | x <- ctxNames i ]
    onTerms MirrorSubstRightUnit "right unit" $ \n t -> withCtx n $ \scope ->
      cmp "substitute n identitySubst t" (subst scope identitySubst t) "t" t
    onTerms MirrorSubstRightUnitExplicit "right unit (explicit identity)" $ \n t -> withCtx n $ \scope ->
      cmp "substitute n (explicit identity) t" (subst scope (explicitIdentity n) t) "t" t
    onSubsts MirrorSubstAssociativity "associativity" $ \i scope1 scope2 s1 s2 t ->
      let composite = nameMapToSubstitution $ addNameBinderList (ctxChain i)
            [ subst scope2 s2 (lookupSubst s1 x) | x <- ctxNames i ] emptyNameMap
       in cmp "substitute o2 s2 (substitute o1 s1 t)" (subst scope2 s2 (subst scope1 s1 t))
              "substitute o2 (s2 ⊙ s1) t" (subst scope2 composite t)

  describe "rbind" $ do
    onTerms MirrorRbindRightUnit "right unit" $ \n t -> withCtx n $ \scope ->
      cmp "rbind n t rreturn" (rbindE scope t rreturnE) "t" t
    onSubsts MirrorRbindAssociativity "associativity" $ \_ scope1 scope2 s1 s2 t ->
      let f = lookupSubst s1
          g = lookupSubst s2
       in cmp "rbind o2 (rbind o1 t f) g" (rbindE scope2 (rbindE scope1 t f) g)
              "rbind o2 t (\\x -> rbind o2 (f x) g)" (rbindE scope2 t (\x -> rbindE scope2 (f x) g))
    onSubsts MirrorSubstituteAgreesWithRbind "agrees with substitute" $ \_ scope1 _ s1 _ t ->
      cmp "substitute o1 s1 t" (subst scope1 s1 t)
          "rbind o1 t (lookupSubst s1)" (rbindE scope1 t (lookupSubst s1))

  case mirrorAlphaEquiv m of
    Nothing -> pure ()
    Just (AlphaEquivOf alphaEquivE) -> describe "α-equivalence" $ do
      onTerms MirrorAlphaEquivRefresh "alphaEquiv n t t' for an α-variant t' (refreshAST of the mirror)" $
        \n t -> withCtx n $ \scope ->
          let t' = from (refreshAST scope (to t))
           in counterexample ("t' = " <> showE t') $
                alphaEquivE scope t t'
      law (verdict Inclusions MirrorAlphaEquivAgrees) "alphaEquiv agrees with a nameless comparison" $
        forAllShow (genPairCase sg) (showPairCase sg) $ \(PairCase n t1 t2) -> withCtx n $ \scope ->
          alphaEquivE scope (from t1) (from t2) === alphaEqNameless (sgPatternNames sg) t1 t2
  where
    sg = mirrorSyntax m
    from :: AST binder sig n -> e n
    from = mirrorFrom m
    to :: e n -> AST binder sig n
    to = mirrorTo m
    subst :: Distinct o => Scope o -> Substitution e i o -> e i -> e o
    subst = mirrorSubstitute m

    showE :: e n -> String
    showE = showAST sg . to

    eqE :: e n -> e n -> Bool
    eqE a b = alphaEqNameless (sgPatternNames sg) (to a) (to b)

    cmp :: String -> e n -> String -> e n -> Property
    cmp lhsName lhs rhsName rhs =
      counterexample (lhsName <> " = " <> showE lhs) $
        counterexample (rhsName <> " = " <> showE rhs) $
          eqE lhs rhs

    rbindE :: Distinct o => Scope o -> e i -> (Name i -> e o) -> e o
    rbindE = rbind

    rreturnE :: Name n -> e n
    rreturnE = rreturn

    liftRME :: Distinct o => Scope o -> (Name i -> Name o) -> e i -> e o
    liftRME = liftRM

    explicitIdentity :: Ctx n -> Substitution e n n
    explicitIdentity n =
      nameMapToSubstitution (addNameBinderList (ctxChain n) (map injectName (ctxNames n)) emptyNameMap)

    onTerms :: MirrorLaw -> String -> (forall n. Ctx n -> e n -> Property) -> Spec
    onTerms name title p = law (verdict Inclusions name) title $
      forAllShrinkShow (genTermCase sg) shrinkTermCase (showTermCase sg) $ \(TermCase n t) ->
        p n (from t)

    onRenamings :: MirrorLaw -> String -> (forall n. Chain' n -> e n -> Property) -> Spec
    onRenamings name title p = forM_ allRenamingClasses $ \cls ->
      describe ("along " <> show cls) $ law (verdict cls name) title $
        forAllShrinkShow (genRenamingCase sg cls) shrinkRenamingCase (showRenamingCase sg) $
          \(RenamingCase chain t) -> p chain (from t)

    onSubsts
      :: MirrorLaw -> String
      -> (forall i o1 o2. (Distinct o1, Distinct o2)
            => Ctx i -> Scope o1 -> Scope o2
            -> Substitution e i o1 -> Substitution e o1 o2
            -> e i -> Property)
      -> Spec
    onSubsts name title p = describe title $
      forM_ [ (k1, k2) | k1 <- [minBound .. maxBound], k2 <- [minBound .. maxBound] ] $ \(k1, k2) ->
        law (verdict Inclusions name) ("s1 " <> show k1 <> ", s2 " <> show k2) $
          forAllShrinkShow (genSubstCase sg k1 k2) shrinkSubstCase (showSubstCase sg) $
            \(SubstCase i o1 o2 s1 s2 t) -> withCtx o1 $ \scope1 -> withCtx o2 $ \scope2 ->
              p i scope1 scope2
                (mapSubst from (substValue s1)) (mapSubst from (substValue s2)) (from t)