packages feed

hsjuiz-0.2.6: src/Juiz/TH.hs

{-# LANGUAGE TemplateHaskell #-}
-- | Process / Container / ContainerProcess / Component のエントリポイントを生成する Template Haskell。
--
-- > {-# LANGUAGE TemplateHaskell #-}
-- > increment :: Int64 -> IO Int64
-- > increment arg1 = pure (arg1 + 1)
-- >
-- > juizProcess 'increment 'manifest
--
-- 生成するのは juiz が探す次の C の関数 (bindings/cjuiz/README.md)。
--
-- * @manifest_entry_point@: マニフェストを返す
-- * @process_factory_entry_point@ / @container_factory_entry_point@ / @container_process_factory_entry_point@:
--   Process の関数 / Container を作る関数 / ContainerProcess の関数を返す
-- * @container_factory_destroy_entry_point@: Container を解放する
-- * 関数そのもの (@hsjuiz_\<モジュール\>_\<関数\>_process_entry_point@ など)
--
-- Component ('juizComponent') では、上の factory の関数の名前が @\<関数名\>_factory_entry_point@ などになり、
-- マニフェストは @component_manifest_entry_point@ が返す。
module Juiz.TH
  ( juizProcess
  , juizContainer
  , juizContainerWithDestroy
  , juizContainerProcess
  , juizComponent
  , ComponentEntry
  , componentProcess
  , componentContainer
  , componentContainerWithDestroy
  , componentContainerProcess
  ) where

import Control.Monad (replicateM)
import qualified Data.Text as T
import Data.Char (isAscii, isAlphaNum)
import Foreign.Ptr (FunPtr)
import Language.Haskell.TH

import Juiz.Component (componentManifestEntryPoint, containerPart, containerProcessPart, processPart)
import Juiz.Container
  ( containerCreateEntryPoint
  , containerCreateEntryPointWithDestroy
  , containerDestroyEntryPoint
  , containerManifestEntryPoint
  , containerProcessEntryPoint
  , containerProcessManifestEntryPoint
  )
import Juiz.Internal.FFI
  ( ContainerCreateFunction
  , ContainerDestroyFunction
  , ContainerFactoryFunction
  , ContainerProcessFactoryFunction
  , ContainerProcessFunction
  , ManifestFunction
  , ProcessFactoryFunction
  , ProcessFunction
  )
import Juiz.Manifest (ContainerManifest, ProcessManifest, factory)
import Juiz.Process (manifestEntryPoint, processEntryPoint)

-- | @juizProcess 'func 'manifest@: 関数 func を、マニフェスト manifest の Process として公開する。
--
-- func の引数は 'Juiz.Capsule.FromCapsule'、結果は 'Juiz.Capsule.ToCapsule' のインスタンスの型にする。
-- 結果は @IO r@ でも、IO の付かない @r@ でもよい。引数はマニフェストに足した順に渡される。
-- func の型は具体的に書く (多相な関数は使えない)。
juizProcess :: Name -> Name -> Q [Dec]
juizProcess fn man = do
  fnE <- ioFunction "juizProcess" fn
  processDecs fn fnE (varE man) "manifest_entry_point" "process_factory"

-- | Process の関数・マニフェスト・関数を返す関数を生成する。factorySym はマニフェストの factory
processDecs :: Name -> ExpQ -> ExpQ -> String -> String -> Q [Dec]
processDecs fn fnE manE manifestSym factorySym = do
  let procSym = entrySymbol fn "process"
      manifestName = mkName ("juizManifestEntryPoint_" ++ nameBase fn)
      processName = mkName ("juizProcessEntryPoint_" ++ nameBase fn)
      processPtrName = mkName ("juizProcessEntryPointPtr_" ++ nameBase fn)
      factoryName = mkName ("juizProcessFactoryEntryPoint_" ++ nameBase fn)
  sequence
    [ sigD manifestName [t| ManifestFunction |]
    , valD (varP manifestName) (normalB [| manifestEntryPoint $manE $fnE |]) []
    , sigD processName [t| ProcessFunction |]
    , valD (varP processName) (normalB [| processEntryPoint $manE $fnE |]) []
    , forImpD cCall safe ("&" ++ procSym) processPtrName [t| FunPtr ProcessFunction |]
    , sigD factoryName [t| ProcessFactoryFunction |]
    , valD (varP factoryName) (normalB [| pure $(varE processPtrName) |]) []
    , exportD manifestSym manifestName [t| ManifestFunction |]
    , exportD procSym processName [t| ProcessFunction |]
    , exportD (factorySym ++ "_entry_point") factoryName [t| ProcessFactoryFunction |]
    ]

-- | @juizContainer 'create 'manifest@: 関数 create を、マニフェスト manifest ('Juiz.Manifest.containerManifest')
-- の Container を作る関数として公開する。
--
-- create の引数はマニフェストに足した順に、設定ファイルの containers に書いた値 (無ければ既定値) を受け取る。
-- 結果 (@IO s@ または @s@) が Container の状態になり、ContainerProcess の第 1 引数に渡される。
-- 状態を書き換えるときは @IORef@ などを返す。Container を削除すると状態は GC に返される。
juizContainer :: Name -> Name -> Q [Dec]
juizContainer create man = do
  fnE <- ioFunction "juizContainer" create
  containerDecs create fnE (varE man) Nothing "manifest_entry_point" "container_factory"

-- | @juizContainerWithDestroy 'create 'destroy 'manifest@: 'juizContainer' に加えて、
-- Container を削除するときに @destroy :: s -> IO ()@ を呼ぶ (デバイスを閉じるなど)
juizContainerWithDestroy :: Name -> Name -> Name -> Q [Dec]
juizContainerWithDestroy create destroy man = do
  fnE <- ioFunction "juizContainerWithDestroy" create
  containerDecs create fnE (varE man) (Just destroy) "manifest_entry_point" "container_factory"

-- | Container を作る関数・解放する関数・マニフェスト・作る関数を返す関数を生成する。factorySym はマニフェストの factory
containerDecs :: Name -> ExpQ -> ExpQ -> Maybe Name -> String -> String -> Q [Dec]
containerDecs create fnE manE destroyOpt manifestSym factorySym = do
  let createSym = entrySymbol create "container_create"
      base = nameBase create
      manifestName = mkName ("juizContainerManifestEntryPoint_" ++ base)
      createName = mkName ("juizContainerCreateEntryPoint_" ++ base)
      createPtrName = mkName ("juizContainerCreateEntryPointPtr_" ++ base)
      factoryName = mkName ("juizContainerFactoryEntryPoint_" ++ base)
      destroyName = mkName ("juizContainerDestroyEntryPoint_" ++ base)
      createBody = case destroyOpt of
        Nothing -> [| containerCreateEntryPoint $manE $fnE |]
        Just d -> [| containerCreateEntryPointWithDestroy $manE $fnE $(varE d) |]
  sequence
    [ sigD manifestName [t| ManifestFunction |]
    , valD (varP manifestName) (normalB [| containerManifestEntryPoint $manE $fnE |]) []
    , sigD createName [t| ContainerCreateFunction |]
    , valD (varP createName) (normalB createBody) []
    , forImpD cCall safe ("&" ++ createSym) createPtrName [t| FunPtr ContainerCreateFunction |]
    , sigD factoryName [t| ContainerFactoryFunction |]
    , valD (varP factoryName) (normalB [| pure $(varE createPtrName) |]) []
    , sigD destroyName [t| ContainerDestroyFunction |]
    , valD (varP destroyName) (normalB [| containerDestroyEntryPoint $manE |]) []
    , exportD manifestSym manifestName [t| ManifestFunction |]
    , exportD createSym createName [t| ContainerCreateFunction |]
    , exportD (factorySym ++ "_entry_point") factoryName [t| ContainerFactoryFunction |]
    , exportD (factorySym ++ "_destroy_entry_point") destroyName [t| ContainerDestroyFunction |]
    ]

-- | @juizContainerProcess 'func 'manifest@: 関数 func を、マニフェスト manifest
-- ('Juiz.Manifest.containerProcessManifest') の ContainerProcess として公開する。
--
-- func の第 1 引数は Container の状態 (Container を作る関数の結果と同じ型)。続く引数と結果は 'juizProcess' と同じ。
-- 結びつけた Container の状態の型が違うと、呼び出しは 'Juiz.Error.juizHaskellContainerTypeError' のエラーになる。
juizContainerProcess :: Name -> Name -> Q [Dec]
juizContainerProcess fn man = do
  fnE <- ioFunction "juizContainerProcess" fn
  containerProcessDecs fn fnE (varE man) "manifest_entry_point" "container_process_factory"

-- | ContainerProcess の関数・マニフェスト・関数を返す関数を生成する。factorySym はマニフェストの factory
containerProcessDecs :: Name -> ExpQ -> ExpQ -> String -> String -> Q [Dec]
containerProcessDecs fn fnE manE manifestSym factorySym = do
  let procSym = entrySymbol fn "container_process"
      base = nameBase fn
      manifestName = mkName ("juizContainerProcessManifestEntryPoint_" ++ base)
      processName = mkName ("juizContainerProcessEntryPoint_" ++ base)
      processPtrName = mkName ("juizContainerProcessEntryPointPtr_" ++ base)
      factoryName = mkName ("juizContainerProcessFactoryEntryPoint_" ++ base)
  sequence
    [ sigD manifestName [t| ManifestFunction |]
    , valD (varP manifestName) (normalB [| containerProcessManifestEntryPoint $manE $fnE |]) []
    , sigD processName [t| ContainerProcessFunction |]
    , valD (varP processName) (normalB [| containerProcessEntryPoint $manE $fnE |]) []
    , forImpD cCall safe ("&" ++ procSym) processPtrName [t| FunPtr ContainerProcessFunction |]
    , sigD factoryName [t| ContainerProcessFactoryFunction |]
    , valD (varP factoryName) (normalB [| pure $(varE processPtrName) |]) []
    , exportD manifestSym manifestName [t| ManifestFunction |]
    , exportD procSym processName [t| ContainerProcessFunction |]
    , exportD (factorySym ++ "_entry_point") factoryName [t| ContainerProcessFactoryFunction |]
    ]

-- | 'juizComponent' に渡す、Component の要素
data ComponentEntry
  = EntryProcess Name Name
  | EntryContainer Name Name (Maybe Name)
  | EntryContainerProcess Name Name

-- | @componentProcess 'func 'manifest@: 'juizProcess' と同じ関数とマニフェストの Process
componentProcess :: Name -> Name -> ComponentEntry
componentProcess = EntryProcess

-- | @componentContainer 'create 'manifest@: 'juizContainer' と同じ関数とマニフェストの Container
componentContainer :: Name -> Name -> ComponentEntry
componentContainer create man = EntryContainer create man Nothing

-- | @componentContainerWithDestroy 'create 'destroy 'manifest@: 'juizContainerWithDestroy' と同じ Container
componentContainerWithDestroy :: Name -> Name -> Name -> ComponentEntry
componentContainerWithDestroy create destroy man = EntryContainer create man (Just destroy)

-- | @componentContainerProcess 'func 'manifest@: 'juizContainerProcess' と同じ関数とマニフェストの ContainerProcess
componentContainerProcess :: Name -> Name -> ComponentEntry
componentContainerProcess = EntryContainerProcess

-- | @juizComponent 'manifest [componentProcess 'f 'fManifest, componentContainer 'create 'cManifest, ...]@:
-- 1 つのライブラリに複数の Process / Container / ContainerProcess をまとめた Component として公開する。
--
-- manifest は 'Juiz.Manifest.componentManifest' で作り、名前を設定ファイルの components に書く。
-- 要素のマニフェストは単体のときと同じものでよく、factory (シンボル名) は関数名から付け直す。
-- 生成する C の関数は C++ の COMPONENT_PROCESS_FACTORY などと同じ名前
-- (@component_manifest_entry_point@、@\<関数名\>_factory_entry_point@、@\<関数名\>_manifest_entry_point@ など)。
-- 関数名は Component の中で重ならないようにする (別のモジュールの関数も使える)。
-- 単体の 'juizProcess' などと同じライブラリに入れてもよいが、その関数を Component にも入れることはできない。
juizComponent :: Name -> [ComponentEntry] -> Q [Dec]
juizComponent comp entries = do
  let bases = map (symbolPart . nameBase . entryFunction) entries
  case [b | (i, b) <- zip [0 :: Int ..] bases, b `elem` take i bases] of
    (b : _) -> fail $ "juizComponent: 関数名 " ++ b ++ " が Component の中で重なっています"
    [] -> pure ()
  results <- mapM componentEntryDecs entries
  let manifestName = mkName "juizComponentManifestEntryPoint"
  top <- sequence
    [ sigD manifestName [t| ManifestFunction |]
    , valD (varP manifestName)
        (normalB [| componentManifestEntryPoint $(varE comp) $(listE (map snd results)) |]) []
    , exportD "component_manifest_entry_point" manifestName [t| ManifestFunction |]
    ]
  pure (concatMap fst results ++ top)

entryFunction :: ComponentEntry -> Name
entryFunction (EntryProcess fn _) = fn
entryFunction (EntryContainer fn _ _) = fn
entryFunction (EntryContainerProcess fn _) = fn

-- | 要素のエントリポイントと、component_manifest_entry_point に渡す 'Juiz.Component.ComponentPart' の式
componentEntryDecs :: ComponentEntry -> Q ([Dec], ExpQ)
componentEntryDecs entry = do
  let fn = entryFunction entry
      base = symbolPart (nameBase fn)
      factorySym = base ++ "_factory"
      manifestSym = base ++ "_manifest_entry_point"
      manName = mkName ("juizComponentManifest_" ++ nameBase fn)
      manE = varE manName
      manDecs ty man = sequence
        [ sigD manName ty
        , valD (varP manName) (normalB [| factory (T.pack $(litE (stringL factorySym))) $(varE man) |]) []
        ]
  case entry of
    EntryProcess _ man -> do
      fnE <- ioFunction "componentProcess" fn
      ds <- (++) <$> manDecs [t| ProcessManifest |] man <*> processDecs fn fnE manE manifestSym factorySym
      pure (ds, [| processPart $manE $fnE |])
    EntryContainer _ man destroyOpt -> do
      fnE <- ioFunction "componentContainer" fn
      ds <- (++) <$> manDecs [t| ContainerManifest |] man
                 <*> containerDecs fn fnE manE destroyOpt manifestSym factorySym
      pure (ds, [| containerPart $manE $fnE |])
    EntryContainerProcess _ man -> do
      fnE <- ioFunction "componentContainerProcess" fn
      ds <- (++) <$> manDecs [t| ProcessManifest |] man
                 <*> containerProcessDecs fn fnE manE manifestSym factorySym
      pure (ds, [| containerProcessPart $manE $fnE |])

exportD :: String -> Name -> Q Type -> Q Dec
exportD sym name qty = ForeignD . ExportF CCall sym name <$> qty

-- | 関数そのものを公開する C のシンボル名 (hsjuiz_\<モジュール\>_\<関数\>_\<種類\>_entry_point)
entrySymbol :: Name -> String -> String
entrySymbol fn kind =
  "hsjuiz_" ++ symbolPart (maybe "" id (nameModule fn)) ++ "_" ++ symbolPart (nameBase fn) ++ "_" ++ kind ++ "_entry_point"

-- | 関数 fn を、結果が IO の関数の式 (型注釈付き) にする。結果が IO でなければ pure で包む
ioFunction :: String -> Name -> Q ExpQ
ioFunction what fn = do
  ty <- reifyType fn >>= expandSynonym
  case ty of
    ForallT {} -> polymorphic
    ForallVisT {} -> polymorphic
    _ -> pure ()
  (argTys, resTy) <- splitFunction ty
  resTy' <- expandSynonym resTy
  let isIO = case resTy' of
        AppT (ConT n) _ -> n == ''IO
        _ -> False
      ioResTy = if isIO then resTy' else AppT (ConT ''IO) resTy'
      fnTy = foldr (\a r -> AppT (AppT ArrowT a) r) ioResTy argTys
  xs <- replicateM (length argTys) (newName "arg")
  let call = foldl appE (varE fn) (map varE xs)
      body = if isIO then call else [| pure $call |]
      lam = if null xs then body else lamE (map varP xs) body
  pure (sigE lam (pure fnTy))
  where
    polymorphic = fail $
      what ++ ": " ++ show fn ++ " の型が多相です。Int64 や Text など具体的な型を書いてください"

-- | 関数の型を引数の型の並びと結果の型に分ける
splitFunction :: Type -> Q ([Type], Type)
splitFunction t = do
  t' <- expandSynonym t
  case t' of
    AppT (AppT ArrowT a) r -> do
      (as, res) <- splitFunction r
      pure (a : as, res)
    _ -> pure ([], t')

-- | 引数の無い型シノニム (type Handler = Int64 -> IO Int64 など) を展開する
expandSynonym :: Type -> Q Type
expandSynonym t@(ConT n) = do
  info <- reify n
  case info of
    TyConI (TySynD _ [] t') -> expandSynonym t'
    _ -> pure t
expandSynonym (AppT (ConT n) x) = do
  -- type Result = IO のような、結果の型の頭だけのシノニム
  info <- reify n
  case info of
    TyConI (TySynD _ [] t') -> expandSynonym (AppT t' x)
    _ -> pure (AppT (ConT n) x)
expandSynonym t = pure t

-- | C のシンボル名に使えない文字を _ にする
symbolPart :: String -> String
symbolPart = map (\c -> if isAscii c && isAlphaNum c then c else '_')