fourmolu-0.7.0.0: src/Ormolu/Config/TH.hs
{-# LANGUAGE CPP #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE TemplateHaskell #-}
module Ormolu.Config.TH
( allNothing,
unpackFieldsWithSuffix,
-- * BijectiveMap
BijectiveMap,
mkBijectiveMap,
parseTextWith,
showTextWith,
showAllValues,
)
where
import Control.Monad (forM, (>=>))
import Data.List (intercalate, nub)
import Language.Haskell.TH
import Language.Haskell.TH.Syntax (lift)
import Text.Printf (printf)
allNothing :: Name -> Q Exp
allNothing name = do
ty <- reifyType name
foldl appE (conE name) $
replicate (getArity ty) [|Nothing|]
unpackFieldsWithSuffix :: Name -> String -> Q Pat
unpackFieldsWithSuffix name suffix = do
typeForCon <-
reify name >>= \case
DataConI _ _ typeForCon -> return typeForCon
info -> fail $ "allNothing requires the Name of a data constructor, got: " <> show info
allConsInType <-
getAllConstructors typeForCon
>>= either (fail . printf "Unexpected parent of data constructor: %s" . show) return
fields <-
case filter (elem name . getConstructorNames) allConsInType of
[con] | Just fields <- conFieldNames con -> return fields
_ -> fail $ "Could not find unique record constructor in: " <> show allConsInType
conP name $ map (varP . mkName . (<> suffix) . nameBase) fields
where
conFieldNames = \case
NormalC {} -> Nothing
RecC _ tys -> Just $ map fst3 tys
InfixC {} -> Nothing
ForallC {} -> Nothing
GadtC {} -> Nothing
RecGadtC _ tys _ -> Just $ map fst3 tys
fst3 (x, _, _) = x
data BijectiveMap a = BijectiveMap
{ parseTextWith :: String -> Either String a,
showTextWith :: a -> String,
getAllOptions :: [String]
}
showAllValues :: BijectiveMap a -> String
showAllValues = uncommas . map show . getAllOptions
-- | Generate a `BijectiveMap a` value with the given map.
--
-- Checks the following:
-- * all Names in given list refer to a constructor of type `a`
-- * all Names in given list refer to a 0-arity constructor
-- * all constructors in type `a` are accounted for.
mkBijectiveMap :: [(Name, String)] -> Q Exp
mkBijectiveMap mapping = do
let (conNames, allOptions) = unzip mapping
-- check all names refer to constructors
(conTypes, conParents) <-
fmap unzip . forM conNames $ \name ->
reify name >>= \case
DataConI _ ty parent -> pure (ty, parent)
info ->
fail $
printf
"mkBijectiveMap requires all Names refer to data constructors, got %s: %s"
(show name)
(show info)
-- check that all constructors are in same type
parent <-
case nub conParents of
[parent] -> return parent
parents -> fail $ "mkBijectiveMap requires all Names refer to data constructors in the same type, got: " <> show parents
-- check that all constructors are 0-arity
case filter ((/= 0) . getArity) conTypes of
[] -> return ()
_ -> fail "mkBijectiveMap requires all constructors have 0-arity"
-- check that all constructors are represented
allConsInType <-
getAllConstructors parent
>>= either (fail . printf "Unexpected parent of data constructors: %s" . show) return
case filter (`notElem` conNames) (concatMap getConstructorNames allConsInType) of
[] -> return ()
missing -> fail $ "Missing constructors: " ++ show missing
unknown <- newName "unknown"
let parser =
lamCaseE . concat $
[ flip map mapping $ \(name, option) ->
match
(litP $ stringL option)
(normalB [|Right $(conE name)|])
[],
[ match
(varP unknown)
( normalB
[|
Left . unlines $
[ "unknown value: " <> show $(varE unknown),
"Valid values are: " <> $(lift $ uncommas $ map show allOptions)
]
|]
)
[]
]
]
shower =
lamCaseE $
flip map mapping $ \(name, option) ->
match (conP name []) (normalB $ lift option) []
[|
BijectiveMap
{ parseTextWith = $parser,
showTextWith = $shower,
getAllOptions = $(lift allOptions)
}
|]
----------------------------------------------------------------------------
-- Helpers
{- FOURMOLU_DISABLE -}
{- https://github.com/fourmolu/fourmolu#limitations -}
getArity :: Type -> Int
getArity = \case
ForallT _ _ ty -> getArity ty
AppT (AppT ArrowT _) ty -> 1 + getArity ty
#if MIN_VERSION_template_haskell(2,17,0)
AppT (AppT (AppT MulArrowT _) _) ty -> 1 + getArity ty
#endif
_ -> 0
{- FOURMOLU_ENABLE -}
getAllConstructors :: Name -> Q (Either Info [Con])
getAllConstructors =
reify >=> \case
TyConI (DataD _ _ _ _ cons _) -> return $ Right cons
info -> return $ Left info
-- Could return multiple names for GADTs like 'A, B :: Foo'
getConstructorNames :: Con -> [Name]
getConstructorNames = \case
NormalC n _ -> [n]
RecC n _ -> [n]
InfixC _ n _ -> [n]
ForallC _ _ c -> getConstructorNames c
GadtC ns _ _ -> ns
RecGadtC ns _ _ -> ns
uncommas :: [String] -> String
uncommas [] = ""
uncommas [s] = s
uncommas [s0, s1] = s0 <> " or " <> s1
uncommas ss =
let pre = init ss
end = last ss
in intercalate ", " pre <> "or " <> end