packages feed

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

{-# LANGUAGE DataKinds            #-}
{-# LANGUAGE DeriveFunctor        #-}
{-# LANGUAGE FlexibleContexts     #-}
{-# LANGUAGE FlexibleInstances    #-}
{-# LANGUAGE TemplateHaskell      #-}
{-# LANGUAGE TypeFamilies         #-}
{-# LANGUAGE UndecidableInstances #-}

-- | A record of sinkable fields is itself sinkable, with nothing written by
-- hand: 'deriveGenericK' plus empty 'SinkableK' and 'Sinkable' instances,
-- and the whole record then sinks in one coercion. This is the supported
-- alternative to a private @unsafeCoerce@ helper for environment records.
-- (A record holding the 'Scope' itself is refused — no @SinkableK Scope@ —
-- which is exactly the field such a helper would coerce unsoundly.)
module Control.Monad.Foil.SinkableSpec (spec) where

import           Data.Bifunctor.TH       (deriveBifunctor)
import qualified Data.Map                as Map
import           Generics.Kind.TH        (deriveGenericK)
import           Test.Hspec

import           Control.Monad.Foil
import           Control.Monad.Free.Foil (AST (Var))

-- | A miniature term type, enough for a table of terms in the record.
data ExprSig scope term = AppSig term term | LamSig scope
  deriving (Functor)
deriveBifunctor ''ExprSig

type Expr = AST NameBinder ExprSig

-- | The shape of a type checker's environment: scope-free fields next to
-- names, terms, tables, and pairs whose first component is scope-free.
data Env (n :: S) = Env
  { envDepth   :: Int
  , envNames   :: [Name n]
  , envTable   :: Map.Map String (Expr n)
  , envGoal    :: Maybe (Expr n, Expr n)
  , envSpelled :: [(String, Name n)]
  }

deriveGenericK ''Env

instance SinkableK Env
instance Sinkable Env

-- | 'sink', with the target scope pinned by a binder the caller holds.
sunkVia :: (Sinkable e, DExt n l) => NameBinder n l -> e n -> e l
sunkVia _ = sink

spec :: Spec
spec = describe "a record of sinkable fields" $
  it "derives Sinkable and sinks whole, contents untouched" $
    withFresh emptyScope $ \binder ->
      let x = nameOf binder
          env = Env 7 [x] (Map.singleton "f" (Var x)) (Just (Var x, Var x)) [("x", x)]
       in withFresh (extendScope binder emptyScope) $ \binder2 -> do
            let env' = sunkVia binder2 env   -- one coercion for the whole record
            envDepth env' `shouldBe` 7
            map nameId (envNames env') `shouldBe` [nameId x]
            Map.keys (envTable env') `shouldBe` ["f"]
            [nameId y | (_, y) <- envSpelled env'] `shouldBe` [nameId x]
            case envGoal env' of
              Just (Var a, Var b) -> (nameId a, nameId b) `shouldBe` (nameId x, nameId x)
              _                   -> expectationFailure "the goal lost its shape"