packages feed

sr-extra-1.64: Extra/TH.hs

-- | Pretty print Loc info for stack traces

{-# LANGUAGE CPP, FlexibleInstances, ScopedTypeVariables, TemplateHaskell #-}
{-# OPTIONS -Wall -Wno-orphans #-}

module Extra.TH
    ( here
    , Loc
    , noLoc
    , prettyLocs
    , addConstraints
    , addConstraint
    , removeConstraints
    ) where

import Control.Lens (_1, over)
import Data.Generics (everywhere, mkT)
import Data.List (intersperse, nub)
import Data.Map as Map (findWithDefault, fromList, fromListWith, lookup, Map, toList)
import Data.Maybe (mapMaybe)
#if !MIN_VERSION_base(4,11,0)
import Data.Monoid ((<>))
#endif
import Data.Tuple (swap)
import Extra.Generics (gFind)
import Language.Haskell.TH (appT, conT, Dec(InstanceD), ExpQ, Loc(..), location, Name, nameBase, newName, Q, Type(..), TypeQ, varT)
import Language.Haskell.TH.Instances ()
import Language.Haskell.TH.Lift (lift)
import Text.PrettyPrint.HughesPJClass (Doc, hcat, Pretty(..), text)

here :: ExpQ
here = lift =<< location

instance Pretty Loc where
    pPrint = prettyLoc

noLoc :: Loc
noLoc = Loc "" "" "" (0, 0) (0, 0)

prettyLoc :: Loc -> Doc
prettyLoc (Loc _filename _package modul (line, col) _) = text (modul <> ":" ++ show line ++ ":" ++ show col)

prettyLocs :: [Loc] -> Doc
prettyLocs locs = text "[" <> hcat (intersperse (text " ← ") (fmap prettyLoc locs)) <> text "]"

-- | Add some simple constraints to a 'Dec'. The Dec is typically one
-- emitted by a function such as makeAcidic or deriveSafeCopy.
addConstraints :: [(Name, String)] -> Dec -> Q Dec
addConstraints cs (InstanceD mo cxt typ decs) = do
  let mp :: Map String [Name]
      mp = Map.fromListWith (<>) (fmap (swap . over _1 (: [])) cs)
  (extra :: [Type]) <- concat <$> mapM (uncurry constrain) (Map.toList mp)
  return $ InstanceD mo (cxt ++ extra) typ decs
    where
      -- Apply some type constraints to a type variable
      constrain vstring tnames = do
        vname <- newName vstring
        mapM (\tname -> appT (conT tname) (varT vname)) tnames
addConstraints _ d = return d

-- | Unify the type variables in the existing constraints with those
-- in the new constraint based on nameBase.  If the type variable does
-- not appear in the declaration it is an error.
addConstraint :: Name -> TypeQ -> Dec -> Q Dec
addConstraint tname typeq (InstanceD mo cxt inst decs) = do
  let vnames = nub $ mapMaybe isVar (gFind (inst : cxt) :: [Type])
      mp :: Map String Name
      mp = Map.fromList (fmap (\n -> (nameBase n, n)) vnames)
  (extra :: Type) <- everywhere (mkT (fixTypeVar mp)) <$> appT (conT tname) typeq
  return $ InstanceD mo (cxt ++ [extra]) inst decs
    where
      isVar :: Type -> Maybe Name
      isVar (VarT name) = Just name
      isVar _ = Nothing
      fixTypeVar :: Map String Name -> Type -> Type
      fixTypeVar mp (VarT n) = VarT (maybe n id (Map.lookup (nameBase n) mp))
      fixTypeVar _mp t = t
addConstraint _ _ d = return d

-- | Remove simple constraints from a 'Dec' based on type class name
-- and type variable name.
removeConstraints :: [(Name, String)] -> Dec -> Q Dec
removeConstraints cs (InstanceD mo cxt typ decs) = do
  let mp :: Map String [Name]
      mp = Map.fromListWith (<>) (fmap (swap . over _1 (: [])) cs)
  return $ InstanceD mo (filter (testPred mp) cxt) typ decs
    where
      testPred :: Map String [Name] -> Type -> Bool
      -- Discard any predicate that matches a map element
      testPred mp (AppT (ConT tname) (VarT vname)) =
          not (elem tname (Map.findWithDefault [] (nameBase vname) mp))
      testPred _ _ = True -- keep anything else
removeConstraints _ d = return d