packages feed

free-foil-0.3.2: test/Control/Monad/Free/Foil/AnnotatedSpec.hs

{-# LANGUAGE DataKinds             #-}
{-# LANGUAGE DeriveGeneric         #-}
{-# LANGUAGE DeriveTraversable     #-}
{-# LANGUAGE FlexibleContexts      #-}
{-# LANGUAGE FlexibleInstances     #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE PatternSynonyms       #-}
{-# LANGUAGE TemplateHaskell       #-}
{-# LANGUAGE TypeFamilies          #-}
{-# LANGUAGE UndecidableInstances  #-}
{-# OPTIONS_GHC -Wno-orphans #-}

-- | Annotated syntax: one 'AnnSig' serving two annotations with opposite matching
-- behaviour, and the 'Bifoldable' asymmetry that α-equivalence relies on.
module Control.Monad.Free.Foil.AnnotatedSpec (spec) where

import           Data.Bifunctor.TH
import           Data.Functor.Const                (Const (..))
import           Data.Kind                         (Type)
import           Data.ZipMatchK
import           Generics.Kind.TH                  (deriveGenericK)
import qualified GHC.Generics                      as GHC
import           Test.Hspec

import qualified Control.Monad.Foil                as Foil
import           Control.Monad.Free.Foil
import           Control.Monad.Free.Foil.Annotated

-- | A minimal λ-calculus signature.
data LamSig scope term = AppSig term term | LamSig scope
  deriving (GHC.Generic, Functor, Foldable, Traversable)

deriveBifunctor ''LamSig
deriveBifoldable ''LamSig
deriveBitraversable ''LamSig
deriveGenericK ''LamSig
instance ZipMatchK LamSig

instance ZipMatchK Int where zipMatchWithK = zipMatchViaEq

-- | An annotation that ignores the term — a source position, say — and __is
-- compared__ when matching.
instance Eq a => ZipMatchK (Const (a :: Type)) where
  zipMatchWithK _ (Const l) (Const r)
    | l == r    = Just (Const l)
    | otherwise = Nothing

type Positioned = AnnAST Foil.NameBinder (Const Int) LamSig

-- | An annotation that holds a term at the node's own scope — a type, say.
--
-- It is optional, and it has to be: an annotation holding a term needs a term to
-- hold, so a closed annotated term could not be built at all without either a
-- 'Maybe' or a knot.
newtype TypeOf term = TypeOf (Maybe term)
  deriving (GHC.Generic, Functor, Foldable, Traversable)

deriveGenericK ''TypeOf

-- | Matching __ignores__ the annotation: it always succeeds, pairing the terms it
-- can and dropping what it cannot.
--
-- The /generic/ instance would not do: it compares the annotation's shape, so a
-- node with a memoised type would fail to match the same node without one.
instance ZipMatchK TypeOf where
  zipMatchWithK (f :^: M0) (TypeOf l) (TypeOf r) =
    Just (TypeOf (do { a <- l; b <- r; f a b }))

type Typed = AnnAST Foil.NameBinder TypeOf LamSig

-- | @\\x. x x@, annotated at every node with a source position.
positioned :: Int -> Positioned Foil.VoidS
positioned k = Foil.withFresh Foil.emptyScope $ \binder ->
  case Foil.assertDistinct binder of
    Foil.Distinct ->
      let x = Var (Foil.nameOf binder)
       in AnnNode (Const k)
            (LamSig (ScopedAST binder (AnnNode (Const k) (AppSig x x))))

-- | @\\x. x@, carrying the given type annotation at its root.
typed :: Maybe (Typed Foil.VoidS) -> Typed Foil.VoidS
typed ty = Foil.withFresh Foil.emptyScope $ \binder ->
  case Foil.assertDistinct binder of
    Foil.Distinct ->
      AnnNode (TypeOf ty) (LamSig (ScopedAST binder (Var (Foil.nameOf binder))))

-- | @\\x. x x@, with no type annotations, to have a structurally different term.
selfApp :: Typed Foil.VoidS
selfApp = Foil.withFresh Foil.emptyScope $ \binder ->
  case Foil.assertDistinct binder of
    Foil.Distinct ->
      let x = Var (Foil.nameOf binder)
       in AnnNode (TypeOf Nothing)
            (LamSig (ScopedAST binder (AnnNode (TypeOf Nothing) (AppSig x x))))

spec :: Spec
spec = do
  describe "an annotation that ignores the term (Const)" $ do
    it "matches when the annotations agree" $
      alphaEquiv Foil.emptyScope (positioned 1) (positioned 1) `shouldBe` True

    it "does not match when they differ" $
      alphaEquiv Foil.emptyScope (positioned 1) (positioned 2) `shouldBe` False

  describe "an annotation that holds a term (a type)" $ do
    it "is ignored when matching: terms differing only in their types are alpha-equivalent" $
      -- The property a dependent typechecker needs. It works because Bifoldable
      -- skips the annotation, so alphaEquiv never walks into it.
      alphaEquiv Foil.emptyScope (typed Nothing) (typed (Just (typed Nothing)))
        `shouldBe` True

    it "still distinguishes terms that differ in the term itself" $
      alphaEquiv Foil.emptyScope (typed Nothing) selfApp `shouldBe` False

    it "never forces the annotation: matching is lazy in it" $
      -- The blind instance returns 'Just' unconditionally with a thunked
      -- annotation, so an annotation that is bottom must not break the match.
      -- A strict instance would throw here instead of returning True.
      alphaEquiv Foil.emptyScope
        (typed (Just (error "annotation forced")))
        (typed (Just (error "annotation forced")))
        `shouldBe` True

  describe "freeVarsOfAnnotated" $
    it "sees a variable that freeVarsOf misses, because it occurs in an annotation" $
      Foil.withFresh Foil.emptyScope $ \outer ->
        case Foil.assertDistinct outer of
          Foil.Distinct ->
            let x = Var (Foil.nameOf outer)                      -- free at this scope
                scope = Foil.extendScope outer Foil.emptyScope
             in Foil.withFresh scope $ \inner ->
                  case Foil.assertDistinct inner of
                    Foil.Distinct ->
                      -- \y. y, annotated with a type that mentions x
                      let node = AnnNode (TypeOf (Just x))
                                   (LamSig (ScopedAST inner (Var (Foil.nameOf inner))))
                       in do
                            freeVarsOf node `shouldBe` []
                            length (freeVarsOfAnnotated node) `shouldBe` 1