Search code examples
chaskellruntimetemplate-haskellghc-api

Is it possible to generate and run TemplateHaskell generated code at runtime?


Is it possible to generate and run TemplateHaskell generated code at runtime?

Using C, at runtime, I can:

  • create the source code of a function,
  • call out to gcc to compile it to a .so (linux) (or use llvm, etc.),
  • load the .so and
  • call the function.

Is a similar thing possible with Template Haskell?


Solution

  • Yes, it's possible. The GHC API will compile Template Haskell. A proof-of-concept is available at https://github.com/JohnLato/meta-th, which, although not very sophisticated, shows one general technique that even provides a modicum of type safety. Template Haskell expressions are build using the Meta type, which can then be compiled and loaded into a usable function.

    {-# LANGUAGE ScopedTypeVariables #-}
    {-# LANGUAGE TemplateHaskell #-}
    
    {-# OPTIONS_GHC -Wall #-}
    module Data.Meta.Meta (
    -- * Meta type
      Meta (..)
    
    -- * Functions
    , metaCompile
    ) where
    
    import Language.Haskell.TH
    
    import Data.Typeable as Typ
    import Control.Exception (bracket)
    
    import System.Plugins -- from plugins
    import System.IO
    import System.Directory
    
    newtype Meta a = Meta { unMeta :: ExpQ }
    
    -- | Super-dodgy for the moment, the Meta type should register the
    -- imports it needs.
    metaCompile :: forall a. Typeable a => Meta a -> IO (Either String a)
    metaCompile (Meta expr) = do
      expr' <- runQ expr
    
      -- pretty-print the TH expression as source code to be compiled at
      -- run-time
      let interpStr = pprint expr'
          typeTypeRep = Typ.typeOf (undefined :: a)
    
      let opener = do
            (tfile, h) <- openTempFile "." "fooTmpFile.hs"
            hPutStr h (unlines
                  [ "module TempMod where"
                  , "import Prelude"
                  , "import Language.Haskell.TH"
                  , "import GHC.Num"
                  , "import GHC.Base"
                  , ""
                  , "myFunc :: " ++ show typeTypeRep
                  , "myFunc = " ++ interpStr] )
            hFlush h
            hClose h
            return tfile
      bracket opener removeFile $ \tfile -> do
    
          res <- make tfile ["-O2", "-ddump-simpl"]
          let ofile = case res of
                        MakeSuccess _ fp -> fp
                        MakeFailure errs -> error $ show errs
          print $ "loading from: " ++ show ofile
          r2 <- load (ofile) [] [] "myFunc"
          print "loaded"
    
          case r2 of
            LoadFailure er -> return (Left (show er))
            LoadSuccess _ (fn :: a) -> return $ Right fn
    

    This function takes an ExpQ, and first runs it in IO to create a plain Exp. The Exp is then pretty-printed into source code, which is compiled and loaded at run-time. In practice, I've found that one of the more difficult obstacles is specifying the correct imports in the generated TH code.