compiler/plugins/Thrift/Compiler/Plugins/Haskell.hs (315 lines of code) (raw):
-- Copyright (c) Facebook, Inc. and its affiliates.
{-# LANGUAGE CPP #-}
module Thrift.Compiler.Plugins.Haskell
( Haskell, HSType, HS
, SpecialType(..)
, HsVectorKind(..), hsVectorImport, hsVectorQual
, HsInterface(..), RenameMap
, LangOpts(..), defaultHsOpts
, toCamel
) where
#if __GLASGOW_HASKELL__ > 804
#define This Some
#endif
import Control.Monad.Trans.Reader
import Data.ByteString (ByteString)
import qualified Data.Foldable as Foldable
import qualified Data.Map as Map
import Data.Maybe
import Data.Some
import Data.Text (Text)
import qualified Data.Text as Text
import qualified Data.Text.Encoding as Text
import Data.Type.Equality
import Language.Haskell.Exts.SrcLoc
import Language.Haskell.Names hiding (None, resolve)
import qualified Language.Haskell.Exts.Syntax as E -- TODO: t16933748 refactor
import Thrift.Compiler.Options
import Thrift.Compiler.Parser
import Thrift.Compiler.Plugin
import Thrift.Compiler.Typechecker
import Thrift.Compiler.Typechecker.Monad
import Thrift.Compiler.Types as Thrift hiding (noLoc)
-- Haskell Types ---------------------------------------------------------------
data Haskell
type HSType = Type Haskell
type HS t = t 'Resolved Haskell Thrift.Loc
data HsVectorKind = HsVectorBoxed | HsVectorStorable
deriving (Eq, Ord, Prelude.Enum, Bounded)
hsVectorImport :: HsVectorKind -> Text
hsVectorImport HsVectorBoxed = "Data.Vector"
hsVectorImport HsVectorStorable = "Data.Vector.Storable"
hsVectorQual :: HsVectorKind -> Text
hsVectorQual HsVectorBoxed = "Vector"
hsVectorQual HsVectorStorable = "VectorStorable"
data instance SpecialType Haskell t where
HsInt :: SpecialType Haskell Int
HsString :: SpecialType Haskell String
HsByteString :: SpecialType Haskell ByteString
HsVector :: HsVectorKind -> HSType t -> SpecialType Haskell (List Haskell t)
data HsInterface = HsInterface Environment RenameMap
instance Semigroup HsInterface where
(<>) = mappend
instance Monoid HsInterface where
mempty = HsInterface Map.empty Map.empty
mappend (HsInterface e1 r1) (HsInterface e2 r2) =
HsInterface (Map.unionWith (++) e1 e2) (Map.union r1 r2)
-- | Map from Haskell name qualified Thrift name
type RenameMap = Map.Map Symbol Text
-- Haskell Options -------------------------------------------------------------
data instance LangOpts Haskell = HsOpts
{ hsoptsEnableHaddock :: Bool
, hsoptsUseInt :: Bool
, hsoptsUseHashMap :: Bool
, hsoptsUseHashSet :: Bool
, hsoptsDupNames :: Bool
, hsoptsExtensions :: [Text]
, hsoptsGenPrefix :: FilePath
, hsoptsExtraHasFields :: Bool
}
defaultHsOpts :: LangOpts Haskell
defaultHsOpts = HsOpts
{ hsoptsEnableHaddock = False
, hsoptsUseInt = False
, hsoptsUseHashMap = False
, hsoptsUseHashSet = False
, hsoptsDupNames = False
, hsoptsExtensions = []
, hsoptsGenPrefix = "gen-hs2"
, hsoptsExtraHasFields = False
}
-- Type Class Instance ---------------------------------------------------------
instance Typecheckable Haskell where
type Interface Haskell = HsInterface
-- Annotation Processing
resolveTypeAnnotations ty anns = do
Env{ options = Options{..} } <- ask
case optsLangSpecific of
HsOpts{..} ->
ifFlag hsoptsUseInt i64ToInt .
ifFlag hsoptsUseHashMap map2HashMap .
ifFlag hsoptsUseHashSet set2HashSet <$>
resolve ty (getTypeAnns "hs" anns)
where
resolve
:: HSType t
-> [(Text, Annotation Thrift.Loc)]
-> TC Haskell (Some HSType)
resolve I64 [("Int",_)] = special HsInt
resolve TText [("String",_)] = special HsString
resolve (TMap k v) [("HashMap",_)] = pure $ This $ THashMap k v
resolve (TSet u) [("HashSet",_)] = pure $ This $ THashSet u
resolve TText [("ByteString",_)] = special HsByteString
resolve (TList u) [(vec,_)]
| Just kind <-
lookup vec [(hsVectorQual x,x) | x <- [minBound .. maxBound]] =
special $ HsVector kind u
resolve u [] = pure $ This u
resolve u ((_,a):_) =
typeError (annLoc a) $ AnnotationMismatch (AnnType u) a
special = pure . This . TSpecial
ifFlag
:: Bool
-> (forall t. HSType t -> Some HSType)
-> Some HSType
-> Some HSType
ifFlag flag fun (This u)
| flag = fun u
| otherwise = This u
i64ToInt :: HSType t -> Some HSType
i64ToInt I64 = This $ TSpecial HsInt
i64ToInt u = This u
map2HashMap (TMap k v) = This $ THashMap k v
map2HashMap u = This u
set2HashSet (TSet u) = This $ THashSet u
set2HashSet u = This u
-- Typechecking
qualifySpecialType _ HsInt = HsInt
qualifySpecialType _ HsString = HsString
qualifySpecialType _ HsByteString = HsByteString
qualifySpecialType m (HsVector kind ty) = HsVector kind $ qualifyType m ty
typecheckSpecialConst HsInt (UntypedConst _ (IntConst i _)) =
pure $ Literal $ fromIntegral i
typecheckSpecialConst HsString (UntypedConst _ (StringConst s _)) =
pure $ Literal $ Text.unpack s
typecheckSpecialConst HsByteString (UntypedConst _ (StringConst s _)) =
pure $ Literal $ Text.encodeUtf8 s
typecheckSpecialConst (HsVector _ u) (UntypedConst _ ListConst{..}) =
Literal . List <$> mapT (typecheckConst u . leElem) lvElems
typecheckSpecialConst ty val@(UntypedConst Located{..} _) =
typeError lLocation $ LiteralMismatch (TSpecial ty) val
eqSpecial HsInt HsInt = Just Refl
eqSpecial HsString HsString = Just Refl
eqSpecial HsByteString HsByteString = Just Refl
eqSpecial (HsVector a u) (HsVector b v)
| a == b = apply Refl <$> eqOrAlias u v
eqSpecial _ _ = Nothing
-- Interfaces
getInterface opts tf@ThriftFile{..} = mconcat $
map (getDeclIface opts thriftName mname) thriftDecls
where
mname = E.ModuleName () $ Text.unpack $ renameModule opts tf <> ".Types"
getExtraSymbols opts iface tf@ThriftFile{..} =
maybe [] (getHsIncludeDeps opts iface tf) thriftSplice
-- Renamers
renameModule _ ThriftFile{..} = case getNamespace "hs" thriftHeaders of
Just ns -> ns <> "." <> toCamel thriftName
Nothing -> toCamel thriftName
renameStruct _ Struct{..} = toConstructorName structName
renameField Options{..} ann sname Field{..} =
case optsLangSpecific of
HsOpts{..} ->
let basePrefix
| hsoptsDupNames = ""
| otherwise = sname <> "_"
in lowercase $ fromMaybe basePrefix (getPrefix ann) <> fieldName
renameConst _ = lowercase
renameService _ Service{..} = toConstructorName serviceName
renameFunction _ Function{..} = lowercase $ prefix <> funName
where
prefix = fromMaybe "" $ getPrefix $ getAnns funAnns
renameTypedef _ Typedef{..} = toConstructorName tdName
renameEnum _ Enum{..} = toConstructorName enumName
renameEnumAlt opts@Options{..} e@Enum{..} name =
fixCase $ if
| Just prefix <- getPrefix (getAnns enumAnns) -> prefix <> name
| otherwise -> enumName <> "_" <> name
where
fixCase
| isPseudo opts e = lowercase
| otherwise = uppercase
renameUnion _ Union{..} = toConstructorName unionName
renameUnionAlt _ Union{..} UnionAlt{..} =
toConstructorName $ fromMaybe (unionName <> "_") (getPrefix $ getAnns unionAnns) <>
altName
getUnionEmptyName _ Union{..} =
toConstructorName $ fromMaybe (unionName <> "_") (getPrefix $ getAnns unionAnns) <>
"EMPTY"
fieldsAreUnique Options{ optsLangSpecific = HsOpts{..} } = not hsoptsDupNames
unionAltsAreUnique _ = True
enumAltsAreUnique Options{..} = True
isPseudo _ Enum{..} = or
[ saTag == "hs.psuedoenum" || saTag == "hs.pseudoenum"
| SimpleAnn{..} <- getAnns enumAnns
]
-- Back-Translators
backTranslateType HsInt = (This I64, "Int")
backTranslateType HsString = (This TText, "String")
backTranslateType HsByteString = (This TText, "ByteString")
backTranslateType (HsVector kind u) = (This (TList u), hsVectorQual kind)
backTranslateLiteral HsInt i = ThisLit I64 (fromIntegral i)
backTranslateLiteral HsString s = ThisLit TText (Text.pack s)
backTranslateLiteral HsByteString s = ThisLit TText (Text.decodeUtf8 s)
backTranslateLiteral (HsVector _ u) l = ThisLit (TList u) l
-- Compute Decl Interfaces -----------------------------------------------------
getDeclIface
:: Options Haskell -> Text -> E.ModuleName () -> Parsed Decl -> HsInterface
getDeclIface opts name mname decl = ifaceFromSymbols mname $ case decl of
-- Structs
D_Struct s@Struct{..} ->
mkStruct (packT structName) (packHs $ renameStruct opts s) ++
concatMap
(\field ->
mkSelector (packT structName)
(packHs $ renameField opts (getAnns structAnns) structName field)
(packHs $ renameStruct opts s))
structMembers
-- Unions
D_Union u@Union{..} ->
mkData (packT unionName) (packHs $ renameUnion opts u) ++
concatMap
(\alt ->
mkConstructor (packT unionName)
(packHs $ renameUnionAlt opts u alt)
(packHs $ renameUnion opts u))
unionAlts
-- Enums
D_Enum e@Enum{..}
| isPseudo opts e ->
mkNewtype (packT enumName) (packHs $ renameEnum opts e)
(packHs $ ("un" <>) $ renameEnum opts e) ++
concatMap
(\EnumValue{..} ->
mkValue (packT enumName) (packHs $ renameEnumAlt opts e evName))
enumConstants
| otherwise ->
mkData (packT enumName) (packHs $ renameEnum opts e) ++
concatMap
(\EnumValue{..} ->
mkConstructor (packT enumName)
(packHs $ renameEnumAlt opts e evName)
(packHs $ renameEnum opts e))
enumConstants
-- Typedefs
D_Typedef t@Typedef{..}
| isNewtype (getAnns tdAnns) ->
mkNewtype (packT tdName) (packHs $ renameTypedef opts t)
(packHs $ ("un" <>) $ renameTypedef opts t)
| otherwise ->
mkType (packT tdName) (packHs $ renameTypedef opts t)
-- Constants
D_Const Const{..} ->
mkValue (packT constName) (packHs $ renameConst opts constName)
-- Services are not suppoerted yet
D_Service{} -> []
where
mkValue tname hsname =
[ (Value mname hsname, tname) ]
mkStruct tname hsname =
[ (Data mname hsname, tname)
, (Constructor mname hsname hsname, tname)
]
mkSelector tname hsname tyname =
[ (Selector mname hsname tyname [tyname], tname)
]
mkData tname hsname =
[ (Data mname hsname, tname) ]
mkConstructor tname hsname tyname =
[ (Constructor mname hsname tyname, tname) ]
mkType tname hsname =
[ (Type mname hsname, tname) ]
mkNewtype tname hsname selname =
[ (NewType mname hsname, tname)
, (Constructor mname hsname hsname, tname)
, (Selector mname selname hsname [hsname], tname)
]
packHs = E.Ident () . Text.unpack
packT t = name <> "." <> t
ifaceFromSymbols :: E.ModuleName () -> [(Symbol, Text)] -> HsInterface
ifaceFromSymbols mname ss = HsInterface
(Map.singleton mname symbols)
rmap
where
rmap = Map.fromList ss
symbols = map fst ss
-- HS Include Dependencies -----------------------------------------------------
getHsIncludeDeps
:: Options Haskell
-> HsInterface
-> ThriftFile a l
-> E.Module SrcSpanInfo
-> [Text]
getHsIncludeDeps opts (HsInterface env rmap) tf (E.Module loc mhead ps is ds) =
[ thriftSym
| decl <- decls
, (Scoped (GlobalSymbol hsSymbol _) _) <- Foldable.toList decl
, Just thriftSym <- [Map.lookup hsSymbol rmap]
]
where
E.Module _ _ _ _ decls = annotate env m'
-- Add types module to imports so that haskell-names knows where the symbols
-- come from
m' = E.Module loc mhead ps (thriftImport : is) ds
thriftImport = E.ImportDecl
{ importAnn = emptyLoc
, importModule =
E.ModuleName emptyLoc $ Text.unpack (renameModule opts tf) ++ ".Types"
, importQualified = False
, importSrc = False
, importSafe = False
, importPkg = Nothing
, importAs = Nothing
, importSpecs = Nothing
}
emptyLoc = toSrcInfo noLoc [] noLoc
getHsIncludeDeps _ _ _ _ = []