packages feed

free-foil-0.4.0: test/Control/Monad/Foil/PatternTransportSpec.hs

{-# LANGUAGE DataKinds           #-}
{-# LANGUAGE GADTs               #-}
{-# LANGUAGE InstanceSigs        #-}
{-# LANGUAGE KindSignatures      #-}
{-# LANGUAGE RankNTypes          #-}
{-# LANGUAGE ScopedTypeVariables #-}

-- | A pattern may carry fields indexed by its own scope, the standard example
-- being a telescope, where each step has a type in the scope the steps before
-- it extend to. 'Foil.withPattern' rebuilds a pattern at an /unrelated/ ambient
-- scope, and hands the instance no renaming for such a field: the only thing
-- relating the two scopes is the pair of binders each step produces.
--
-- 'Foil.PatternTransport' is that missing renaming. This module pins down what
-- it has to do, on the smallest pattern that can tell the difference: a chain
-- of binders where each step carries a name from the scope before it, so that a
-- payload can point at a binder the traversal is about to refresh.
--
-- The two halves are: a payload naming something the pattern does not bind is
-- left alone, and a payload naming one of the pattern's own binders follows
-- that binder when it is refreshed.
--
-- The second half is what the generic implementation of 'Foil.withPattern'
-- cannot do, since it replaces the binders and leaves the other fields as they
-- stand. That is now refused rather than answered wrongly: deriving the
-- instances for 'Chain' below — @deriveGenericK ''Chain@ and then an empty
-- @HasNameBinders@ instance — is a type error naming the offending field,
--
-- > A field of the binder/pattern is indexed by a Foil scope
-- >   Foil.Name outerScope : S
--
-- so the refusal cannot be tested here, only recorded. Before it, the derived
-- route compiled and gave the binders @[3,4]@ with the payloads left at
-- @[0,1]@.
module Control.Monad.Foil.PatternTransportSpec (spec) where

import           Test.Hspec

import qualified Control.Monad.Foil as Foil

-- | A chain of binders, each carrying a name in the scope before it.
data Chain (n :: Foil.S) (l :: Foil.S) where
  ChainEmpty :: Chain n n
  ChainCons  :: Foil.Name n -> Foil.NameBinder n i -> Chain i l -> Chain n l

instance Foil.CoSinkable Chain where
  coSinkabilityProof rename ChainEmpty cont = cont rename ChainEmpty
  coSinkabilityProof rename (ChainCons payload binder rest) cont =
    Foil.coSinkabilityProof rename binder $ \rename' binder' ->
      Foil.coSinkabilityProof rename' rest $ \rename'' rest' ->
        cont rename'' (ChainCons (rename payload) binder' rest')

  withPattern
    :: forall f o n l r. Foil.Distinct o
    => (forall x y z r'. Foil.Distinct z
          => Foil.Scope z
          -> Foil.NameBinder x y
          -> (forall z'. Foil.DExt z z' => f x y z z' -> Foil.NameBinder z z' -> r')
          -> r')
    -> (forall x z z'. Foil.DExt z z' => f x x z z')
    -> (forall x y y' z z' z''. (Foil.DExt z z', Foil.DExt z' z'')
          => f x y z z' -> f y y' z' z'' -> f x y' z z'')
    -> Foil.Scope o
    -> Chain n l
    -> (forall o'. Foil.DExt o o' => f n l o o' -> Chain o o' -> Foil.Scope o' -> r)
    -> r
  withPattern withBinder unit comp = go Foil.verbatimTransport
    where
      go :: forall n' l' o' r'. Foil.Distinct o'
         => Foil.PatternTransport n' o'
         -> Foil.Scope o'
         -> Chain n' l'
         -> (forall o''. Foil.DExt o' o'' => f n' l' o' o'' -> Chain o' o'' -> Foil.Scope o'' -> r')
         -> r'
      go _transport scope ChainEmpty cont = cont unit ChainEmpty scope
      go transport scope (ChainCons payload binder rest) cont =
        withBinder scope binder $ \fbinder binder' ->
          go (Foil.transportUnderBinder transport binder binder')
             (Foil.extendScope binder' scope)
             rest $ \frest rest' scope'' ->
            cont (comp fbinder frest)
              (ChainCons (Foil.transportPayload transport payload) binder' rest')
              scope''

-- | The result of processing one binder, when there is nothing to carry.
data NoInfo (x :: Foil.S) (y :: Foil.S) (z :: Foil.S) (z' :: Foil.S) = NoInfo

-- | Refresh a chain against an ambient scope, renaming only the binders that
-- clash with it. This is 'Foil.withRefreshedPattern' without the substitution.
refreshChain
  :: Foil.Distinct o
  => Foil.Scope o
  -> Chain n l
  -> (forall o'. Foil.DExt o o' => Chain o o' -> r)
  -> r
refreshChain scope chain cont =
  Foil.withPattern
    (\scope' binder k ->
      Foil.withRefreshed scope' (Foil.nameOf binder) (k NoInfo))
    NoInfo
    (\NoInfo NoInfo -> NoInfo)
    scope
    chain
    (\NoInfo chain' _scope' -> cont chain')

-- | A chain of two binders, allocated under one binder that it does not bind.
--
-- The binders are the raw names 1 and 2. The first payload is the raw name 0,
-- which the chain does not bind; the second is the raw name 1, which it does.
withChain
  :: (forall i l. Foil.Distinct i => Chain i l -> r) -> r
withChain cont =
  Foil.withFresh Foil.emptyScope $ \b0 ->
    let scope0 = Foil.extendScope b0 Foil.emptyScope
     in Foil.withFresh scope0 $ \b1 ->
          let scope1 = Foil.extendScope b1 scope0
           in Foil.withFresh scope1 $ \b2 ->
                cont (ChainCons (Foil.nameOf b0) b1
                       (ChainCons (Foil.nameOf b1) b2 ChainEmpty))

-- | A scope holding the raw names 0, 1 and 2, so that both of the chain's
-- binders clash with it.
withClashingScope :: (forall o. Foil.Distinct o => Foil.Scope o -> r) -> r
withClashingScope cont =
  Foil.withFresh Foil.emptyScope $ \b0 ->
    let scope0 = Foil.extendScope b0 Foil.emptyScope
     in Foil.withFresh scope0 $ \b1 ->
          let scope1 = Foil.extendScope b1 scope0
           in Foil.withFresh scope1 $ \b2 ->
                cont (Foil.extendScope b2 scope1)

binders :: Chain n l -> [Foil.RawName]
binders ChainEmpty                  = []
binders (ChainCons _ binder rest)   = Foil.nameId (Foil.nameOf binder) : binders rest

payloads :: Chain n l -> [Foil.RawName]
payloads ChainEmpty                 = []
payloads (ChainCons payload _ rest) = Foil.nameId payload : payloads rest

spec :: Spec
spec = do
  describe "a pattern with scoped payloads" $ do
    it "is built with the binders and payloads the tests expect" $
      withChain $ \chain ->
        (binders chain, payloads chain) `shouldBe` ([1, 2], [0, 1])

    it "keeps its payloads when no binder is refreshed" $
      withChain $ \chain ->
        refreshChain Foil.emptyScope chain $ \chain' ->
          (binders chain', payloads chain') `shouldBe` ([1, 2], [0, 1])

    it "carries a payload along the binder it names when that binder moves" $
      -- Both binders clash and are refreshed to 3 and 4. The payload naming the
      -- chain's own first binder has to become 3; the payload naming something
      -- outside the chain stays 0. Coercing the payloads instead, as the
      -- default implementation does, would leave the first one at 1.
      withChain $ \chain ->
        withClashingScope $ \scope ->
          refreshChain scope chain $ \chain' ->
            (binders chain', payloads chain') `shouldBe` ([3, 4], [0, 3])

  describe "the traversals that never refresh" $
    it "see the binders without walking the payloads" $
      withChain $ \chain ->
        map Foil.nameId (Foil.namesOfPattern chain) `shouldBe` binders chain