haskelldynamicinterpreterhotswapghc-api

Can I compile a haskell function from a string at runtime (using plugins)?


I have an application where, for various reasons, I need to run arbitrary, user supplied code. (SafeHaskell makes this nice and secure). I've looked at the plugins package, which is really nice for loading from a .hi file on disc.

However, for my program design, it would be ideal if I could store these user programs in a database, then directly compile them to functions which I can use in my program.

So, if the function I'm compiling has the following type:

someFunction :: MyIn -> MyOut

I'm looking to write some function that will generate that function from a string:

hotCompile :: String -> IO (MyIn -> MyOut)

where string contains the haskell code code for "someFunction".

Does anybody know if there's a way to do this, preferably using the plugins package? I have come across the GHC API a little bit, but I don't know much about it and how it would relate to this.

Note that I've tried hint, but it is unsuitable for my application because it is not threadsafe.


Solution

  • Use the package hint we can define eval very easily, following is an example as self-contained script (you still need nix to run it)

    #!/usr/bin/env nix-shell
    #! nix-shell -p "haskellPackages.ghcWithPackages (p: with p; [hint])"
    #! nix-shell -i "ghci -ignore-dot-ghci -fdefer-type-errors -XTypeApplications"
    
    {-# LANGUAGE ScopedTypeVariables, TypeApplications, PartialTypeSignatures #-}
    
    import Data.Typeable (Typeable)
    import qualified Language.Haskell.Interpreter as Hint
    
    -- DOC: https://www.stackage.org/lts-18.18/package/hint-0.9.0.4
    
    eval :: forall t. Typeable t => String -> IO t
    eval s = do
        mr <- Hint.runInterpreter $ do
            Hint.setImports ["Prelude"]
            Hint.interpret s (Hint.as :: t)
        case mr of
            Left err -> error (show err)
            Right r -> pure r
    
    -- * Interpret expressions into values:
    
    e1 = eval @Int "1 + 1 :: Int"
    e2 = eval @String "\"hello eval\""
    
    -- * Send values from your compiled program to your interpreted program by interpreting a function:
    
    e3 = do
        f <- eval @(Int -> [Int]) "\\x -> [1..x]"
        pure (f 5)