hs-bindgen-1.0.0.0: src-internal/HsBindgen/Backend/TH/Translation.hs
{-# LANGUAGE MagicHash #-}
{-# LANGUAGE TemplateHaskellQuotes #-}
module HsBindgen.Backend.TH.Translation (
mkDecl,
) where
import Control.Monad (liftM2)
import Data.ByteString qualified as BS
import Data.List qualified as List
import Data.Set qualified as Set
import Data.Text qualified as Text
import DeBruijn (Add (..), EmptyCtx, Env (..), lookupEnv)
import Foreign.C (CChar (..))
import Foreign.C.Types qualified
import GHC.Base qualified
import GHC.Float (castDoubleToWord64, castFloatToWord32, castWord32ToFloat,
castWord64ToDouble)
import Language.Haskell.TH (Quote)
import Language.Haskell.TH qualified as TH
import Language.Haskell.TH.Syntax qualified as TH
import HsBindgen.Backend.Global
import HsBindgen.Backend.Hs.AST qualified as Hs
import HsBindgen.Backend.Hs.CallConv
import HsBindgen.Backend.Hs.Haddock.Documentation qualified as HsDoc
import HsBindgen.Backend.Hs.Name qualified as Hs
import HsBindgen.Backend.Level
import HsBindgen.Backend.SHs.AST
import HsBindgen.Backend.SHs.AST.Expr (FBind (FBind))
import HsBindgen.Backend.SHs.Translation.Common
import HsBindgen.Config (FieldNamingStrategy)
import HsBindgen.Errors
import HsBindgen.Guasi
import HsBindgen.Imports
import HsBindgen.Instances as Inst
import HsBindgen.IR.C qualified as C
import HsBindgen.Language.Haskell qualified as Hs
import HsBindgen.NameHint
import HsBindgen.Util.Rational (canBeRepresentedAsRational)
{-------------------------------------------------------------------------------
Backend definition
-------------------------------------------------------------------------------}
mkGlobalExpr :: Quote q => Global LvlTerm -> q TH.Exp
mkGlobalExpr g = case g.cat of
GVar -> TH.varE g.name
GCon -> TH.conE g.name
mkExpr :: Guasi q => Env ctx TH.Name -> SExpr ctx -> q TH.Exp
mkExpr env expr = case asNaryEApp expr of
(EBoxedTup n, args) ->
prettyTupleExpr Boxed env n args
(EUnboxedTup n, args) ->
prettyTupleExpr Unboxed env n args
(EApp{} , _) ->
panicWith
"Unexpected function application after unrolling function applications"
expr
_otherwise -> mkRolledExpr env expr
-- See 'mkExpr' but do not unroll/recognize function applications.
mkRolledExpr :: Guasi q => Env ctx TH.Name -> SExpr ctx -> q TH.Exp
mkRolledExpr env expr = case expr of
EGlobal n -> mkGlobalExpr n
EFree n -> TH.varE $ mkHsTermName n
EBound x -> TH.varE (lookupEnv x env)
ECon n -> TH.conE $ mkHsName n
EUnboxedIntegral i -> TH.sigE (TH.litE (TH.IntPrimL i)) (TH.conT ''GHC.Base.Int#)
EIntegral i Nothing -> TH.litE (TH.IntegerL i)
EIntegral i (Just t) -> TH.sigE (TH.litE (TH.IntegerL i)) (mkType EmptyEnv t)
-- TH doesn't have floating-point literals, because it represents them
-- using the Rational type, which is incorrect. (See GHC ticket #13124.)
--
-- To work around this problem, we cast floating-point numbers to
-- Word32/Word64 and then cast back.
EFloat f t ->
TH.sigE
( if canBeRepresentedAsRational f
then [| f |]
else [| Foreign.C.Types.CFloat $ castWord32ToFloat $( TH.lift $ castFloatToWord32 f ) |]
)
(mkType EmptyEnv t)
EDouble d t ->
TH.sigE
( if canBeRepresentedAsRational d
then [| d |]
else [| Foreign.C.Types.CDouble $ castWord64ToDouble $( TH.lift $ castDoubleToWord64 d ) |]
)
(mkType EmptyEnv t)
ECChar (CChar i) -> [| CChar $(TH.lift i) |]
-- Not @[| s |]@: cross-stage lifting of a 'String' yields a list of 'Char'
-- literals unless the @TH:liftString@ rewrite rule fires, which needs @-O@.
-- This is problematic during development.
EString s -> TH.litE (TH.StringL s)
ECString bs ->
TH.appE
(mkGlobalExpr (bindgenGlobalTerm ByteString_pack))
(TH.listE $ map (\w -> TH.litE (TH.IntegerL (fromIntegral w))) (BS.unpack bs))
EApp f x -> TH.appE (mkExpr env f) (mkExpr env x)
EInfix op x y -> TH.infixE
(Just $ mkExpr env x)
(mkGlobalExpr $ infixOpGlobal op)
(Just $ mkExpr env y)
ELam (NameHint x) f -> do
x' <- TH.newName x
TH.lamE [TH.varP x'] (mkExpr (env :> x') f)
EUnusedLam f ->
TH.lamE [TH.wildP] (mkExpr env f)
ECase x alts -> TH.caseE (mkExpr env x)
[ case alt of
SAlt c add hints b -> do
(xs, env') <- newNames env add hints
TH.match
(TH.conP (mkHsName c) (map TH.varP xs))
(TH.normalB $ mkExpr env' b)
[]
SAltNoConstr hints b -> do
-- SAltNoConstr has one name hint only
-- guaranteed by the type.
(xs, env') <- newNames env (AS AZ) hints
case xs of
[] ->
panicPure "Expected one name hint, but SAltNoConstr had none"
[v] ->
TH.match
(TH.varP v)
(TH.normalB $ mkExpr env' b)
[]
vs ->
panicPure $ "Expected one name hint, but SAltNoConstr had more: " <> show vs
SAltUnboxedTuple add hints b -> do
(xs, env') <- newNames env add hints
TH.match
(TH.unboxedTupP $ map TH.varP xs)
(TH.normalB $ mkExpr env' b)
[]
| alt <- alts
]
EUnit -> TH.tupE []
-- Handled in 'mkExpr'.
EBoxedTup{} ->
panicWith
"Unexpected boxed, unsaturated tuple after unrolling function applications"
expr
-- Handled in 'mkExpr'.
EUnboxedTup{} ->
panicWith
"Unexpected unboxed, unsaturated tuple after unrolling function applications"
expr
EList xs -> TH.listE $ mkExpr env <$> xs
ETypeApp f t -> TH.appTypeE (mkExpr env f) (mkType EmptyEnv t)
ERecCon con fs -> TH.recConE (mkHsName con) (fmap (mkFBind env) fs)
mkFBind :: Guasi q => Env ctx TH.Name -> FBind ctx -> q (TH.Name, TH.Exp)
mkFBind env (FBind label expr) = (TH.mkName label,) <$> mkExpr env expr
mkPat :: Quote q => PatExpr -> q TH.Pat
mkPat = \case
PEApps n xs -> TH.conP (mkHsName n) (map mkPat xs)
PELit i -> TH.litP (TH.IntegerL i)
mkType :: Guasi q => Env ctx TH.Name -> SType ctx -> q TH.Type
mkType env ty = case asNaryTApp ty of
(TBoxedTup n, args) ->
prettyTupleType env n args
(TApp{}, _) ->
panicWith
"Unexpected type application after unrolling type applications"
ty
_otherwise ->
mkRolledType env ty
-- See 'mkType' but do not unroll/recognize type applications.
mkRolledType :: Guasi q => Env ctx TH.Name -> SType ctx -> q TH.Type
mkRolledType env ty = case ty of
TGlobal n -> TH.conT n.name
TClass cls -> TH.conT $ (.name) $ typeClassGlobal cls
TCon n -> TH.conT $ mkHsName n
TFun a b -> TH.arrowT `TH.appT` mkType env a `TH.appT` mkType env b
TLit n -> TH.litT (TH.numTyLit (toInteger n))
TStrLit s -> TH.litT (TH.strTyLit s)
TExt extRef -> lookupExtType extRef
TBound x -> TH.varT (lookupEnv x env)
TFree n -> TH.varT $ mkHsName n
TApp f t -> TH.appT (mkType env f) (mkType env t)
TUnit -> pure $ TH.TupleT 0
-- Handled in 'mkType'.
TBoxedTup{} ->
panicWith
"Unexpected unsaturated tuple after unrolling type application"
ty
TEq -> TH.conT (bindgenGlobalType TypeEquality_type).name
TForall hints add ctxt body -> do
let bndr tv = TH.PlainTV tv TH.SpecifiedSpec
(xs, env') <- newNames env add hints
TH.forallT
(map bndr xs)
(traverse (mkType env') ctxt)
(mkType env' body)
TList t -> TH.appT TH.listT $ mkType env t
mkDecl :: forall q. Guasi q => FieldNamingStrategy -> SDecl -> q [TH.Dec]
mkDecl fns = \case
DTypSyn typSyn -> do
targetType <- mkType EmptyEnv typSyn.typ
pure [TH.TySynD (mkHsName typSyn.name) [] targetType]
DInst inst -> do
instanceDec <-
TH.instanceD
(return [])
(TH.forallT
[]
(mapM (mkType EmptyEnv) inst.super)
(appsT (TH.conT $ (.name) $ typeClassGlobal inst.clss)
(map (mkType EmptyEnv) inst.args)))
(concat [
map instTySyn inst.types
, map (\(x, f) -> simpleDecl x.name f) inst.decs
])
-- TODO <https://github.com/well-typed/hs-bindgen/issues/976>
-- We should add haddock comment to the class head, but we cannot due to
-- a GHC bug. We also don't put the comments on any of the class members
-- (type synonyms / functions) because that leads to similar bugs.
--
-- putDocTypeM _type_ inst.comment
pure [instanceDec]
DRecord record -> do
let fields :: [q TH.VarBangType]
docs :: [(Hs.Name Hs.NsVar, Maybe HsDoc.Comment)]
(fields, docs) = unzip
[ ( TH.varBangType (mkHsName field.name) $
TH.bangType
(TH.bang TH.noSourceUnpackedness TH.noSourceStrictness)
(mkType EmptyEnv field.typ)
, (field.name, field.comment)
)
| field <- record.fields
]
traverse_ (uncurry (putLocalFieldDocM fns record.con)) docs
decl <-
TH.dataD
(TH.cxt [])
(mkHsName record.typ)
[]
Nothing
[TH.recC (mkHsName record.con) fields]
(nestedDeriving record.deriv)
putLocalDocM record.typ record.comment
pure [decl]
DEmptyData empty -> do
decl <- TH.dataD (TH.cxt []) (mkHsName empty.name) [] Nothing [] []
putLocalDocM empty.name empty.comment
pure [decl]
DNewtype newtyp -> do
let field :: q TH.VarBangType
field = TH.varBangType (mkHsName newtyp.field.name) $
TH.bangType
(TH.bang TH.noSourceUnpackedness TH.noSourceStrictness)
(mkType EmptyEnv newtyp.field.typ)
putLocalFieldDocM fns newtyp.con newtyp.field.name newtyp.field.comment
decl <-
TH.newtypeD
(TH.cxt [])
(mkHsName newtyp.name)
[]
Nothing
(TH.recC (mkHsName newtyp.con) [field])
(nestedDeriving newtyp.deriv)
putLocalDocM (newtyp.name) (newtyp.comment)
pure [decl]
DDerivingInstance deriv -> do
s' <- strategy deriv.strategy
-- NOTE: We can't attach documentation to standalone deriving clauses.
-- See 'GHC.Internal.TH.Lib.withDecDoc.doc_loc'.
fmap singleton $ TH.standaloneDerivWithStrategyD
(Just s')
(TH.cxt [])
(mkType EmptyEnv deriv.typ)
DForeignImport foreignImport -> do
let safety :: TH.Safety
safety = case foreignImport.safety of
Safe -> TH.Safe
Unsafe -> TH.Unsafe
callconv :: TH.Callconv
impent :: String
(callconv, impent) =
case foreignImport.callConv of
CallConvUserlandCapi _ -> (TH.CCall,
Text.unpack foreignImport.origName.text
)
CallConvGhcCapi header -> (TH.CApi, concat [
header
, Text.unpack foreignImport.origName.text
])
CallConvGhcCCall style -> (TH.CCall, concat [
case style of
ImportAsValue -> ""
ImportAsPtr -> "&"
, Text.unpack foreignImport.origName.text
])
importType = foldr (TFun . (.typ)) foreignImport.result.typ foreignImport.parameters
decl <-
fmap TH.ForeignD $
TH.ImportF
<$> pure callconv
<*> pure safety
<*> pure impent
<*> pure (mkHsTermName foreignImport.name)
<*> mkType EmptyEnv importType
putLocalDocTermNameM foreignImport.name foreignImport.comment
pure [decl]
DBinding binding -> do
let bindingName :: TH.Name
bindingName = mkHsTermName binding.name
bindingType :: SType EmptyCtx
bindingType = foldr (TFun . (.typ)) binding.result.typ binding.parameters
decls <- sequence $
map (pragma binding.name) binding.pragmas
++ [
TH.SigD <$> pure bindingName
<*> mkType EmptyEnv bindingType
, simpleDecl bindingName binding.body
]
putLocalDocTermNameM binding.name binding.comment
pure decls
DPatternSynonym patSyn -> do
let thPatSynName = mkHsName patSyn.name
decls <- sequence
[ TH.patSynSigD
thPatSynName
(mkType EmptyEnv patSyn.typ)
, TH.patSynD
thPatSynName
(TH.prefixPatSyn [])
TH.implBidir
(mkPat patSyn.rhs)
]
putLocalDocM patSyn.name patSyn.comment
pure decls
DCompletePragma completePragma ->
List.singleton
<$> TH.pragCompleteD (map mkHsName completePragma.patterns) Nothing
where
simpleDecl :: TH.Name -> SExpr EmptyCtx -> q TH.Dec
simpleDecl x f = TH.valD (TH.varP x) (TH.normalB $ mkExpr EmptyEnv f) []
instTySyn :: (Global LvlType, [ClosedType], ClosedType) -> q TH.Dec
instTySyn (g, typArgs, typSyn) =
TH.TySynInstD
<$> liftM2
(TH.TySynEqn Nothing)
(mkType EmptyEnv (foldl (\acc x -> acc `TApp` x) (TGlobal g) typArgs))
(mkType EmptyEnv typSyn)
pragma :: Hs.TermName -> Pragma -> q TH.Dec
pragma n = \case
NOINLINE ->
TH.pragInlD (mkHsTermName n) TH.NoInline TH.FunLike TH.AllPhases
-- | Nested deriving clauses (part of a datatype declaration)
nestedDeriving :: forall q.
Guasi q
=> [(Hs.Strategy ClosedType, [Inst.TypeClass])] -> [q TH.DerivClause]
nestedDeriving = map aux
where
aux :: (Hs.Strategy ClosedType, [Inst.TypeClass]) -> q TH.DerivClause
aux (s, clss) = do
s' <- strategy s
TH.derivClause (Just s') (map (TH.conT . (.name) . typeClassGlobal) clss)
strategy :: Guasi q => Hs.Strategy ClosedType -> q TH.DerivStrategy
strategy Hs.DeriveNewtype = return TH.NewtypeStrategy
strategy Hs.DeriveStock = return TH.StockStrategy
strategy (Hs.DeriveVia ty) = TH.ViaStrategy <$> mkType EmptyEnv ty
{-------------------------------------------------------------------------------
Monad functionality
-------------------------------------------------------------------------------}
appsT :: Quote q => q TH.Type -> [q TH.Type] -> q TH.Type
appsT = foldl' TH.appT
-- | Create a 'TH.name' from an 'Hs.Name'
--
-- Be careful! This function uses 'TH.mkName'. Names created with 'TH.mkName'
-- are resolved in the context of the use site of the splice. That is, used
-- symbols /must be in scope/, and users must import the probably only
-- indirectly-used modules.
mkHsName :: Hs.Name ns -> TH.Name
mkHsName = TH.mkName . Hs.nameToStr
-- | Create a 'TH.name' from an 'Hs.TermName'
--
-- Be careful! This function uses 'TH.mkName'. Names created with 'TH.mkName'
-- are resolved in the context of the use site of the splice. That is, used
-- symbols /must be in scope/, and users must import the probably only
-- indirectly-used modules.
mkHsTermName :: Hs.TermName -> TH.Name
mkHsTermName = TH.mkName . Hs.termNameToStr
newNames ::
Quote q
=> Env ctx TH.Name
-> Add n ctx ctx'
-> Vec n NameHint
-> q ([TH.Name], Env ctx' TH.Name)
newNames env AZ _ = return ([], env)
newNames env (AS n) (NameHint hint ::: hints) = do
(xs, env') <- newNames env n hints
x <- TH.newName hint
return (x : xs, env' :> x)
putLocalFieldDocM ::
Guasi g
=> FieldNamingStrategy
-> Hs.Name Hs.NsConstr
-> Hs.Name Hs.NsVar
-> Maybe HsDoc.Comment
-> g ()
putLocalFieldDocM fns parent field = traverse_ (putLocalFieldDoc fns parent field)
{-------------------------------------------------------------------------------
Tuples
-------------------------------------------------------------------------------}
data TupleType = Boxed | Unboxed
prettyTupleExpr ::
Guasi q
=> TupleType
-> Env ctx TH.Name
-> Plus2
-> [SExpr ctx]
-> q TH.Exp
prettyTupleExpr ty env n decls = case compare arity nDecls of
LT ->
panicPure $ mconcat [
"Too many declarations ("
, show nDecls
, ") for "
, show arity ++ "-tuple"
]
_otherwise -> do
declExprs <- mapM (mkExpr env) decls
let nMissing :: Int
nMissing = arity - nDecls
fakeDecls :: [Maybe TH.Exp]
fakeDecls = map Just declExprs ++ replicate nMissing Nothing
pure $ thTupleCon fakeDecls
where
arity, nDecls :: Int
arity = fromIntegral (applyPlus2 n)
nDecls = length decls
thTupleCon :: [Maybe TH.Exp] -> TH.Exp
thTupleCon = case ty of
Boxed -> TH.TupE
Unboxed -> TH.UnboxedTupE
prettyTupleType :: Guasi q => Env ctx TH.Name -> Plus2 -> [SType ctx] -> q TH.Type
prettyTupleType env n decls = case compare arity nDecls of
LT ->
panicPure $ mconcat [
"Too many declarations ("
, show nDecls
, ") for "
, show arity ++ "-tuple"
]
_otherwise -> foldl' TH.appT (TH.tupleT arity) $ mkType env <$> decls
where
arity, nDecls :: Int
arity = fromIntegral (applyPlus2 n)
nDecls = length decls
{-------------------------------------------------------------------------------
Helpers
-------------------------------------------------------------------------------}
panicWith :: Show a => String -> a -> b
panicWith msg x = panicPure $ msg ++ ": " ++ show x
-- | Look up a type name from an external binding spec.
--
-- If the name is not in scope, report a helpful error message telling the user
-- which module they need to import, then falls back to 'TH.mkName' to avoid
-- cascading type errors.
--
-- See https://github.com/well-typed/hs-bindgen/issues/1622.
lookupExtType :: Guasi q => Hs.ExtRef -> q TH.Type
lookupExtType extRef = do
mName <- lookupTypeName qualName
case mName of
Just n -> TH.conT n
Nothing -> do
modifyGuasi (putMissingModule extRef.moduleName)
TH.conT (TH.mkName qualName)
where
qualName :: String
qualName = concat [
Hs.moduleNameToString extRef.moduleName
, "."
, Text.unpack extRef.name.text
]
putMissingModule :: Hs.ModuleName -> GuasiState -> GuasiState
putMissingModule m s = s & #missingModules %~ Set.insert m