free-foil-0.3.2: src/Control/Monad/Free/Foil/Annotated.hs
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE DeriveTraversable #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE PatternSynonyms #-}
{-# LANGUAGE PolyKinds #-}
{-# LANGUAGE StandaloneDeriving #-}
{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE TypeOperators #-}
{-# LANGUAGE UndecidableInstances #-}
-- | Annotating every node of a free foil term.
--
-- A node can be annotated with anything: a source position, a type, a memoised
-- normal form. 'AnnSig' turns a signature into an annotated one, and
-- 'AnnAST' is the annotated syntax it generates.
--
-- The annotation is a /functor of the term/, and that is what lets it depend on
-- the node's scope. In @'AST' binder sig n@ the signature's term parameter /is/
-- the AST at the node's own scope, so an annotation built from it holds terms in
-- that scope — which is what a type annotation in a dependent language needs.
-- An annotation that ignores the term (a source position, say) is @'Const' a@.
--
-- Whether two annotations must agree for their nodes to match is a property of
-- @ann@, not of 'AnnSig'. Two examples, and they are the two you will want:
--
-- An annotation that ignores the term (a source position) and /is/ compared:
--
-- > instance Eq a => ZipMatchK (Const (a :: Type)) where
-- > zipMatchWithK _ (Const l) (Const r)
-- > | l == r = Just (Const l)
-- > | otherwise = Nothing
--
-- An annotation that holds terms (a type, say) and is /ignored/, so that two
-- terms differing only in their types are α-equivalent. Make the held term
-- optional, so that pairing can fail /without/ failing the match:
--
-- > newtype TypeOf term = TypeOf (Maybe term)
-- >
-- > instance ZipMatchK TypeOf where
-- > zipMatchWithK (f :^: M0) (TypeOf l) (TypeOf r) =
-- > Just (TypeOf (do { a <- l; b <- r; f a b }))
--
-- __The instance must return 'Just' unconditionally, and lazily.__ Matching is
-- annotation-blind, so it must succeed whatever the annotations are; the paired
-- annotation is a thunk the (annotation-skipping) 'Bifoldable' never forces. A
-- /strict/ shape — @TypeOf '<$>' f l r@, or anything that yields 'Nothing' when
-- @f@ fails — is a footgun twice over: it breaks blindness (two nodes with
-- different types would fail to match), and it /diverges/ for a finite or
-- lazily-bottomed annotation (a universe tower ending in 'error', say), because
-- forcing the annotation runs off the end. This is why the held term is a 'Maybe':
-- a plain @term@ field would have no value to pair when @f@ fails, forcing a
-- bottom into the result.
--
-- __Do not reach for the generic instance here.__ It compares the annotation's
-- /shape/, so a node carrying a memoised normal form would fail to match the same
-- node without one. (Nor is 'zipMatchViaChooseLeft' available: an annotation
-- holding terms must /construct/ a value at the result index, and cannot simply
-- pick a side.)
--
-- __Note the asymmetry__ in the instances below: 'Bifunctor' and 'Bitraversable'
-- traverse the annotation, but 'Bifoldable' does /not/. This is deliberate — see
-- 'AnnSig'.
module Control.Monad.Free.Foil.Annotated (
AnnSig(..),
AnnAST,
AnnScopedAST,
pattern AnnNode,
annotationOf,
freeVarsOfAnnotated,
) where
import Data.Bifoldable
import Data.Bifunctor
import Data.Bitraversable
import Data.Kind (Type)
import Data.Maybe (mapMaybe)
import Data.ZipMatchK.TH (deriveZipMatchK2)
import Generics.Kind (GenericK (..), Field, Var0, Var1, (:$:),
Atom ((:@:)), (:*:))
import qualified GHC.Generics as GHC
import qualified Control.Monad.Foil as Foil
import Control.Monad.Free.Foil
-- | A signature, with every node annotated by an @ann@ built from the node's term.
--
-- The annotation is applied to the /term/ parameter, so in an
-- @'AST' binder ('AnnSig' ann sig) n@ it holds terms in the node's own scope.
--
-- 'Bifoldable' deliberately __skips the annotation__. That is what makes
-- α-equivalence annotation-blind: 'Control.Monad.Free.Foil.alphaEquiv' consumes a
-- zipped node with 'bifoldMap', so a type annotation is never compared, and two
-- terms that differ only in their annotations are α-equivalent. The price is that
-- 'Control.Monad.Free.Foil.freeVarsOf', which is also 'Bifoldable', does not see
-- variables occurring inside annotations. Use 'freeVarsOfAnnotated' when those
-- matter.
data AnnSig (ann :: Type -> Type) (sig :: Type -> Type -> Type) scope term
= AnnSig (ann term) (sig scope term)
deriving (GHC.Generic)
deriving instance (Functor ann, Functor (sig scope))
=> Functor (AnnSig ann sig scope)
deriving instance (Foldable ann, Foldable (sig scope))
=> Foldable (AnnSig ann sig scope)
deriving instance (Traversable ann, Traversable (sig scope))
=> Traversable (AnnSig ann sig scope)
instance (Functor ann, Bifunctor sig) => Bifunctor (AnnSig ann sig) where
bimap f g (AnnSig ann sig) = AnnSig (fmap g ann) (bimap f g sig)
-- | __The annotation is not folded.__ See 'AnnSig'.
instance Bifoldable sig => Bifoldable (AnnSig ann sig) where
bifoldMap f g (AnnSig _ann sig) = bifoldMap f g sig
instance (Traversable ann, Bitraversable sig) => Bitraversable (AnnSig ann sig) where
bitraverse f g (AnnSig ann sig) = AnnSig <$> traverse g ann <*> bitraverse f g sig
instance GenericK (AnnSig ann sig) where
type RepK (AnnSig ann sig) =
Field (ann :$: Var1) :*: Field ((sig :$: Var0) :@: Var1)
-- | Matching for 'AnnSig' is /derived/, not left to the generic default.
--
-- The generic default would rebuild the "Generics.Kind" view of every node on
-- every comparison, and comparing terms is most of what a typechecker does, so
-- for an annotated signature — where it lands on the hottest path — that is a
-- measurable cost. 'Data.ZipMatchK.TH.deriveZipMatchK2' generates the
-- written-out instance instead: the annotation matched with the term zipper (an
-- annotation is a functor of the term), the inner signature with both. Write
-- @deriveZipMatchK2 ''YourAnnSig@ for a bespoke annotated signature.
deriveZipMatchK2 ''AnnSig
-- | An annotated scope-safe term.
type AnnAST binder ann sig = AST binder (AnnSig ann sig)
-- | An annotated scope-safe term under a binder.
type AnnScopedAST binder ann sig = ScopedAST binder (AnnSig ann sig)
-- | An annotated node.
pattern AnnNode
:: ann (AnnAST binder ann sig n)
-> sig (AnnScopedAST binder ann sig n) (AnnAST binder ann sig n)
-> AnnAST binder ann sig n
pattern AnnNode ann sig = Node (AnnSig ann sig)
{-# COMPLETE Var, AnnNode #-}
-- | The annotation of a term, unless it is a variable (which is not a node, so it
-- carries none).
annotationOf :: AnnAST binder ann sig n -> Maybe (ann (AnnAST binder ann sig n))
annotationOf = \case
Var _ -> Nothing
AnnNode ann _ -> Just ann
-- | The free variables of an annotated term, __including__ those occurring inside
-- annotations.
--
-- 'Control.Monad.Free.Foil.freeVarsOf' misses the latter, since 'Bifoldable' skips
-- the annotation (see 'AnnSig').
freeVarsOfAnnotated
:: (Foil.Distinct n, Foil.CoSinkable binder, Bifoldable sig, Foldable ann)
=> AnnAST binder ann sig n -> [Foil.Name n]
freeVarsOfAnnotated = \case
Var name -> [name]
AnnNode ann sig ->
foldMap freeVarsOfAnnotated ann
<> bifoldMap freeVarsOfAnnotatedScoped freeVarsOfAnnotated sig
-- | The free variables of an annotated scoped term, including those inside
-- annotations.
freeVarsOfAnnotatedScoped
:: (Foil.Distinct n, Foil.CoSinkable binder, Bifoldable sig, Foldable ann)
=> AnnScopedAST binder ann sig n -> [Foil.Name n]
freeVarsOfAnnotatedScoped (ScopedAST binder body) =
case Foil.assertDistinct binder of
Foil.Distinct ->
mapMaybe (Foil.unsinkNamePattern binder) (freeVarsOfAnnotated body)