packages feed

thrift-compiler-0.1.0.1: plugins/Thrift/Compiler/Plugins/Haskell.hs

{-
  Copyright (c) Meta Platforms, Inc. and affiliates.
  All rights reserved.

  This source code is licensed under the BSD-style license found in the
  LICENSE file in the root directory of this source tree.
-}

module Thrift.Compiler.Plugins.Haskell
  ( Haskell, HSType, HS
  , SpecialType(..)
  , HsVectorKind(..), hsVectorImport, hsVectorQual
  , HsInterface(..), RenameMap
  , LangOpts(..), defaultHsOpts
  , toCamel
  ) where

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
  HsInterface e1 r1 <> HsInterface e2 r2 =
    HsInterface (Map.unionWith (++) e1 e2) (Map.union r1 r2)

instance Monoid HsInterface where
  mempty = HsInterface Map.empty Map.empty


-- | 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 $ Some $ THashMap k v
      resolve (TSet u) [("HashSet",_)] = pure $ Some $ 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 $ Some u
      resolve u ((_,a):_) =
        typeError (annLoc a) $ AnnotationMismatch (AnnType u) a

      special = pure . Some . TSpecial

      ifFlag
        :: Bool
        -> (forall t. HSType t -> Some HSType)
        -> Some HSType
        -> Some HSType
      ifFlag flag fun (Some u)
        | flag      = fun u
        | otherwise = Some u
      i64ToInt :: HSType t -> Some HSType
      i64ToInt I64 = Some $ TSpecial HsInt
      i64ToInt u  = Some u
      map2HashMap (TMap k v) = Some $ THashMap k v
      map2HashMap u = Some u
      set2HashSet (TSet u) = Some $ THashSet u
      set2HashSet u = Some 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 <$> traverse (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 e@Enum{..} name =
    fixCase $ if
      | Just prefix <- getPrefix (getAnns enumAnns) -> prefix <> name
      | otherwise -> enumName <> "_" <> name
    where
      fixCase = case enumFlavourTag opts e of
        PseudoEnum{} -> lowercase
        _ -> 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

  enumFlavourTag _ Enum{..}
    | hasSimpleAnn "hs.pseudoenum" = PseudoEnum False
    | hasValueAnn "hs.pseudoenum" "thriftenum" = PseudoEnum True
    | hasSimpleAnn "hs.nounknown" = SumTypeEnum True
    | otherwise = SumTypeEnum False
    where
      hasSimpleAnn t = or
        [ saTag == t
        | SimpleAnn{..} <- getAnns enumAnns
        ]
      hasValueAnn t v = or
        [ vaTag == t && av == v
        | ValueAnn{ vaVal=TextAnn av _, ..} <- getAnns enumAnns
        ]

  -- Back-Translators

  backTranslateType HsInt = (Some I64, "Int")
  backTranslateType HsString = (Some TText, "String")
  backTranslateType HsByteString = (Some TText, "ByteString")
  backTranslateType (HsVector kind u) = (Some (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{..} ->
    case enumFlavourTag opts e of
      PseudoEnum{} ->
        mkNewtype (packT enumName) (packHs $ renameEnum opts e)
          (packHs $ ("un" <>) $ renameEnum opts e) ++
        concatMap
        (\EnumValue{..} ->
          mkValue (packT enumName) (packHs $ renameEnumAlt opts e evName))
        enumConstants
      SumTypeEnum{} ->
        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 supported yet
  D_Service{} -> []
  -- Interaction are not supported yet
  D_Interaction{} -> []

  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)
  | E.Module _ _ _ _ decls <- annotate env m' =
    [ thriftSym
    | decl <- decls
    , (Scoped (GlobalSymbol hsSymbol _) _) <- Foldable.toList decl
    , Just thriftSym <- [Map.lookup hsSymbol rmap]
    ]
  | otherwise = error "getHsIncludeDeps"
  where
    -- 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 _ _ _ _ = []