packages feed

futhark-0.28.1: src/Futhark/Eval.hs

-- | Facilities for evaluating Futhark code.
--
-- This most directly provides the building blocks for @futhark eval@, but this
-- module also contains server interaction machinery used by @futhark repl@ and
-- other commands that make use of the Futhark interpreter.
module Futhark.Eval
  ( EvalConfig (..),
    InterpreterState,
    runExpr,
    evalConfig,
    evalServerOptions,
    runFFI,
    forceValue,
    externaliseLast,
    runInterpreterWith,
    interpretImports,
    initialiseInterpreter,
    newInterpreterState,
    Evaluation (..),
    EvalRecordRef,
    runEvalRecordRef,
  )
where

import Control.Exception (IOException, catch)
import Control.Monad (foldM, unless, void, when, (<=<))
import Control.Monad.Except (ExceptT (..), runExceptT, throwError)
import Control.Monad.Free.Church (F, runF)
import Control.Monad.IO.Class (MonadIO, liftIO)
import Control.Monad.Trans.Class (lift)
import Control.Monad.Trans.Reader (ReaderT (runReaderT), ask)
import Data.Bifunctor (first)
import Data.Either (isLeft)
import Data.IORef (IORef, modifyIORef')
import Data.Map qualified as M
import Data.Maybe (isJust, maybeToList)
import Data.Sequence (Seq, (|>))
import Data.Text qualified as T
import Futhark.Compiler (prettyWarnings, readProgramFilesExceptKnown)
import Futhark.Compiler.Program (FileModule (..), Imports, VFS, fileScope)
import Futhark.Error (badOnLeft, externalErrorS, prettyCompilerError)
import Futhark.FreshNames (VNameSource)
import Futhark.Server qualified as S
import Futhark.Test.Compile (FutharkExe (..), compileProgram, futharkServerCfg)
import Futhark.Util (showText)
import Futhark.Util.Options (ArgDescr (..), FunOptDescr, OptDescr (..))
import Futhark.Util.Pretty (commasep, hPutDoc, hPutDocLn, hardline, putDocLn)
import Language.Futhark.Core (locText)
import Language.Futhark.Interpreter qualified as I
import Language.Futhark.Interpreter.FFI.Push qualified as FFI
import Language.Futhark.Interpreter.FFI.ServerM qualified as FFI
import Language.Futhark.Parser (parseExp)
import Language.Futhark.Parser.Monad (SyntaxError (SyntaxError))
import Language.Futhark.Pretty (toName)
import Language.Futhark.Prop (UncheckedExp, typeOf)
import Language.Futhark.Semantic qualified as T
import Language.Futhark.Syntax (DecBase (LocalDec, ValDec), ProgBase (progDecs), ValBindBase (..), nameToText, typeParamName)
import Language.Futhark.TypeChecker qualified as T
import Prettyprinter (Doc, align, pretty, unAnnotate, vcat, (<+>))
import Prettyprinter.Render.Terminal (AnsiStyle)
import System.Environment (getExecutablePath)
import System.Exit (ExitCode (ExitFailure), exitWith)
import System.FilePath (dropExtension, (</>))
import System.IO (stderr)

-- | The class of monads that can perform expression evaluation.
class (Monad m) => Evaluation m where
  abort :: Doc AnsiStyle -> m b
  trace :: Doc AnsiStyle -> m ()

instance (Evaluation m) => Evaluation (ExceptT (Doc AnsiStyle) m) where
  trace :: Doc AnsiStyle -> ExceptT e m ()
  trace = lift . trace

  abort = throwError

instance Evaluation IO where
  abort reason = do
    hPutDocLn stderr reason
    exitWith $ ExitFailure 1

  trace = putDocLn

newtype EvalRecordRef a
  = EvalRecordRef
      (ExceptT (Doc AnsiStyle) (ReaderT (IORef (Seq (Doc AnsiStyle))) IO) a)
  deriving (Functor, Applicative, Monad, MonadIO)

instance Evaluation EvalRecordRef where
  abort :: Doc AnsiStyle -> EvalRecordRef b
  abort = EvalRecordRef . throwError

  trace :: Doc AnsiStyle -> EvalRecordRef ()
  trace message = EvalRecordRef $ do
    messagesRef <- lift ask
    liftIO $ modifyIORef' messagesRef (|> message)

runEvalRecordRef ::
  IORef (Seq (Doc AnsiStyle)) ->
  EvalRecordRef a ->
  IO (Either (Doc AnsiStyle) a)
runEvalRecordRef msgRef (EvalRecordRef action) =
  flip runReaderT msgRef $ runExceptT action

newtype InterpreterState = InterpreterState (VNameSource, T.Env, I.Ctx, Maybe FFI.Server)

-- | Run an expression in the given interpreter state. The expression is parsed,
-- type checked, and then run. Returns a prettyprinted result. Must be run in a
-- monad that supports aborting and traces.
runExpr ::
  (Evaluation m, MonadIO m) =>
  InterpreterState ->
  T.Text ->
  m (Doc AnsiStyle)
runExpr state str =
  case parseExp "" str of
    Left (SyntaxError _ serr) -> abort $ pretty serr
    Right uexp -> runParsedExpr state uexp

-- | As 'runExpr', but for an expression that has already been parsed -
-- perhaps from somewhere other than a Futhark file.
runParsedExpr ::
  (Evaluation m, MonadIO m) =>
  InterpreterState ->
  UncheckedExp ->
  m (Doc AnsiStyle)
runParsedExpr (InterpreterState (src, env, ctx, s)) uexp = do
  fexp <- case T.checkExp [] src env uexp of
    (_, Left terr) -> do
      abort $ T.prettyTypeError terr
    (_, Right ([], e)) -> pure e
    (_, Right (tparams, e)) ->
      abort $
        vcat
          [ "Inferred type of expression: " <> align (pretty (typeOf e)),
            "The following types are ambiguous: "
              <> commasep (map (pretty . nameToText . toName . typeParamName) tparams)
          ]
  pval <- runInterpreterWith trace s Nothing $ I.interpretExp ctx fexp
  case pval of
    Left err -> abort $ I.prettyInterpreterError err
    Right val -> do
      forced <- liftIO $ forceValue s val
      case forced of
        Left err -> abort $ I.prettyInterpreterError err
        Right val' -> pure $ I.prettyValue val' <> hardline

data EvalConfig = EvalConfig
  { evalPrintWarnings :: Bool,
    evalFile :: Maybe String,
    -- | If @Just@, compile the file using this backend.
    evalBackend :: Maybe String,
    evalSkipCompilation :: Bool,
    evalExtraOptions :: [String],
    evalCompilerOptions :: [String],
    evalFuthark :: Maybe FilePath
  }

evalConfig :: EvalConfig
evalConfig =
  EvalConfig
    { evalPrintWarnings = True,
      evalFile = Nothing,
      evalBackend = Nothing,
      evalSkipCompilation = False,
      evalExtraOptions = [],
      evalCompilerOptions = [],
      evalFuthark = Nothing
    }

-- | Command line options for configuring the use of an external server.
evalServerOptions :: [FunOptDescr EvalConfig]
evalServerOptions =
  [ Option
      "p"
      ["pass-option"]
      ( ReqArg
          ( \opt ->
              Right $ \config ->
                config {evalExtraOptions = opt : evalExtraOptions config}
          )
          "OPT"
      )
      "Pass this option to programs being run.",
    Option
      []
      ["pass-compiler-option"]
      ( ReqArg
          ( \opt ->
              Right $ \config ->
                config {evalCompilerOptions = opt : evalCompilerOptions config}
          )
          "OPT"
      )
      "Pass this option to the compiler.",
    Option
      []
      ["skip-compilation"]
      (NoArg $ Right $ \config -> config {evalSkipCompilation = True})
      "Use already compiled server-mode program.",
    Option
      []
      ["backend"]
      ( ReqArg
          (\backend -> Right $ \config -> config {evalBackend = Just backend})
          "BACKEND"
      )
      "The compiler backend used (defaults to interpreted)."
  ]

-- | Compile the given file and start a server for it. Returns 'Left' on error.
prepareServer :: EvalConfig -> FilePath -> String -> IO (Either T.Text FFI.Server)
prepareServer cfg file backend = runExceptT $ do
  futhark <- liftIO $ maybe getExecutablePath pure $ evalFuthark cfg

  unless (evalSkipCompilation cfg) $
    void $
      compileProgram ("--server" : evalCompilerOptions cfg) (FutharkExe futhark) backend file

  let server_cfg = futharkServerCfg ("." </> dropExtension file) $ evalExtraOptions cfg
  started <-
    liftIO $
      (Right <$> FFI.startServer server_cfg)
        `catch` (\(S.ServerException err) -> pure $ Left err)
        `catch` (\(err :: IOException) -> pure $ Left $ showText err)
  either throwError pure started

-- | Perform an action on the server. A failure is reported as a 'Left'. Calls
-- 'error' if no server is provided.
runFFI ::
  Maybe FFI.Server ->
  FFI.ServerM I.Value ->
  IO (Either I.InterpreterError I.Value)
runFFI Nothing _ = error "External call, but no server."
runFFI (Just server) m =
  first (I.InterpreterError . T.pack) <$> FFI.runServerM server m

-- | Fetch in full a value that may reside on a server, so that it can
-- be printed or otherwise inspected.
forceValue ::
  Maybe FFI.Server ->
  I.Value ->
  IO (Either I.InterpreterError I.Value)
forceValue Nothing v = pure $ Right v
forceValue server v = runFFI server $ FFI.getLazy v

externalise :: FileModule -> FileModule
externalise fm = fm {fileProg = (fileProg fm) {progDecs = map onDec $ progDecs $ fileProg fm}}
  where
    onDec (ValDec vb)
      | isJust $ valBindEntryPoint vb =
          ValDec $ vb {valBindAttrs = "$external" : valBindAttrs vb}
    onDec (LocalDec dec loc) = LocalDec (onDec dec) loc
    onDec dec = dec

-- | Mark the entry points of the last import - which is the file the
-- user actually asked us to load - as external, meaning that calls to
-- them are dispatched to a server instead of being interpreted.
externaliseLast :: Imports -> Imports
externaliseLast [] = []
externaliseLast [(k, fm)] = [(k, externalise fm)]
externaliseLast (x : xs) = x : externaliseLast xs

-- | Type check and interpret the given program (if any), producing a state
-- in which expressions can be evaluated with 'runExpr'.
newInterpreterState ::
  (MonadIO m, Evaluation m) =>
  EvalConfig ->
  VFS ->
  m (Either (Doc AnsiStyle) InterpreterState)
newInterpreterState cfg vfs = runExceptT $ do
  let maybe_file = evalFile cfg
  (ws, imports, src) <-
    badOnLeft prettyCompilerError
      =<< liftIO
        ( runExceptT (readProgramFilesExceptKnown [] vfs $ maybeToList maybe_file)
            `catch` \(err :: IOException) ->
              pure (externalErrorS (show err))
        )
  when (evalPrintWarnings cfg) $
    liftIO . hPutDoc stderr $
      prettyWarnings ws

  (s, tenv, ienv) <- ExceptT $ initialiseInterpreter cfg maybe_file Nothing imports

  pure $ InterpreterState (src, tenv, ienv, s)

-- | Interpret the given imports in order with the provided interpreter runner,
-- and produce the type and interpreter environment of the last one.
interpretImports ::
  (Monad m) =>
  (F I.ExtOp I.Ctx -> m (Either I.InterpreterError I.Ctx)) ->
  Imports ->
  ExceptT (Doc AnsiStyle) m (T.Env, I.Ctx)
interpretImports runner imports = do
  let foldFile ctx =
        badOnLeft I.prettyInterpreterError
          <=< lift . runner . I.interpretImport ctx
  ictx <- foldM foldFile I.initialCtx $ map (fmap fileProg) imports
  let (iname, fm) = last imports
  pure (fileScope fm, ictx {I.ctxEnv = I.ctxImports ictx M.! iname})

-- | Set up an interpreter context for the given program. If a server is
-- provided, or a backend has been requested (in which case a server is
-- started), then the entry points of the loaded file are marked external, such
-- that calls to them are dispatched to the server instead of being interpreted.
-- On failure, a server started here is shut down again, so a returned server is
-- always one that the caller now owns (and must eventually stop). A server
-- passed in by the caller is never shut down here.
initialiseInterpreter ::
  (Evaluation m, MonadIO m) =>
  EvalConfig ->
  Maybe FilePath ->
  Maybe FFI.Server ->
  Imports ->
  m (Either (Doc AnsiStyle) (Maybe FFI.Server, T.Env, I.Ctx))
initialiseInterpreter cfg maybe_file server imports =
  case (server, maybe_file, evalBackend cfg) of
    (Just s, _, _) ->
      evalWith (Just s) $ externaliseLast imports
    (Nothing, Just file, Just backend) -> do
      started <- liftIO $ prepareServer cfg file backend
      case started of
        Left err -> pure $ Left $ pretty err
        Right s -> do
          r <- evalWith (Just s) $ externaliseLast imports
          -- Do not leave a server running if we never got off the ground.
          when (isLeft r) $ void $ liftIO $ FFI.stopServer s
          pure r
    _ -> evalWith Nothing imports
  where
    evalWith s =
      runExceptT
        . fmap (\(tenv, ienv) -> (s, tenv, ienv))
        . interpretImports (runInterpreterWith trace s Nothing)

-- | Run an interpreter action non-interactively. Breakpoints are ignored, as
-- there is no way to enter a debugging prompt. Traces, and the breakpoints
-- that are ignored, are reported with the given function. External calls are
-- dispatched to the given server (if any). Relative paths in IO operations are
-- resolved relative to the given directory, if any, and otherwise relative to
-- the working directory.
runInterpreterWith ::
  (MonadIO m) =>
  (Doc AnsiStyle -> m ()) ->
  Maybe FFI.Server ->
  Maybe FilePath ->
  F I.ExtOp a ->
  m (Either I.InterpreterError a)
runInterpreterWith report s dir m = runF m (pure . Right) intOp
  where
    intOp (I.ExtOpError err) = pure $ Left err
    intOp (I.ExtOpTrace w v c) = do
      report $ pretty w <> ":" <+> align (unAnnotate v)
      c
    intOp (I.ExtOpBreak _ I.BreakNaN _ c) = c
    intOp (I.ExtOpBreak w _ _ c) = do
      report $ pretty (locText w) <> ": ignoring breakpoint."
      c
    intOp (I.ExtOpFFI sm c) = either (pure . Left) c =<< liftIO (runFFI s sm)
    intOp (I.ExtOpIO op c) =
      either (pure . Left . I.InterpreterError) c
        =<< liftIO (I.doIOOp s (maybe id I.ioRelativeTo dir op))