packages feed

pisigma-0.1.0.3: src/Tools/Interpreter/REPL.hs

{-# OPTIONS_GHC -fno-warn-orphans          #-}
{-# LANGUAGE    GeneralizedNewtypeDeriving #-}
{-# LANGUAGE    OverloadedStrings          #-}
{-# LANGUAGE    ScopedTypeVariables        #-}

module Tools.Interpreter.REPL
  ( REPL (..)
  , REPLCommand (..)
  , handleCommand
  , repl
  , runREPL )
  where

import Prelude hiding (catch)
import Control.Monad.Error
import Control.Monad.State
import Control.Monad.Trans
import Data.Char
import qualified Data.List as List
import System.Console.Haskeline.Class
import System.IO
import Data.Version (showVersion)

import Language.PiSigma.Check
import Language.PiSigma.Equality
import Language.PiSigma.Evaluate
import Language.PiSigma.Lexer
  as Lexer
    ( Parser )
import Language.PiSigma.Normalise
import Language.PiSigma.Parser
import Language.PiSigma.Pretty
import Language.PiSigma.Syntax
import qualified Language.PiSigma.Util.String.Internal
  as Internal
import qualified Language.PiSigma.Util.String.Parser
  as Parser

import Paths_pisigma

-- * Instances needed for newtype deriving

instance (Error e, MonadException m) => MonadException (ErrorT e m) where
  catch m = ErrorT . catch (runErrorT m) . (runErrorT .)
  block   = mapErrorT block
  unblock = mapErrorT unblock

instance (Error e, MonadHaskeline m) => MonadHaskeline (ErrorT e m) where
  getInputLine = lift . getInputLine
  getInputChar = lift . getInputChar
  outputStr    = lift . outputStr
  outputStrLn  = lift . outputStrLn

-- * Types

-- | The REPL monad has exceptions, a line editor with history using
-- Haskeline, and state.
newtype REPL a = REPL { unREPL :: ErrorT REPLError (HaskelineT (StateT REPLState IO)) a }
  deriving ( Monad
           , MonadException
           , MonadError REPLError
           , MonadHaskeline
           , MonadIO
           , MonadState REPLState )

-- | The state currently keeps track of identifiers defined and their
-- types, as well as a list of files currently loaded.
data REPLState =
  REPLState
    { replState :: (Scope, EnvEntries)
    , replFiles :: [FilePath]
    }

-- TODO: Add a search path and verbosity options.

-- ** Commands

-- | Commands that can be entered at the REPL prompt.
data REPLCommand =
    Load String
  | Reload
  | Quit
  | EvalPhrase Phrase
  | Noop
  | Clear
  | Equal (Term,Term)
  | TypeOf Term
  | Help
  | Browse
  | Startup
  | Version

-- ** Errors

-- | Normal Errors abort the current command but do not quit the REPL.
-- If we actually wish to quit, we can throw a Terminate error.
data REPLError =
    Error
  | Terminate

instance Error REPLError where
  noMsg    = Error
  strMsg _ = Error

-- * REPL monad

initialREPLState :: REPLState
initialREPLState = 
  REPLState (emptyScope, emptyE) []

runREPL :: REPL () -> IO ()
runREPL r =
  do
    runStateT (runHaskelineT (setComplete pisigmaCompletion defaultSettings)
                         (runErrorT (unREPL r)))
              initialREPLState
    return ()

-- * Haskeline

-- | Completion in PiSigma. For the moment, we use file name completion
-- while in a load command, and identifier completion everywhere else.
pisigmaCompletion :: CompletionFunc (StateT REPLState IO)
pisigmaCompletion (x1, x2)
  | ":l" `List.isPrefixOf` List.reverse x1 = completeFilename (x1, x2)
  | otherwise                              = completeWord Nothing " " identifier (x1, x2)
  where
    identifier :: String -> StateT REPLState IO [Completion]
    identifier x = do
      (Scope sc, _) <- gets replState
      let names = map fst sc
          cands = filter (Internal.fromString x `Internal.isPrefixOf`) names
      return $ map (simpleCompletion . Internal.toString) (List.sort cands)

-- * Help

-- | Preliminary interpreter help message.
help :: String
help = List.unlines
  [ "PiSigma currently supports the following commands:"
  , ""
  , "  :l         load a source file"
  , "  :r         reload current source file"
  , "  :b         browse current identifiers"
  , "  :c         clear the environment"
  , "  :t         ask for the type of a term"
  , "  :e         test two terms for beta equality"
  , "  :v         version"
  , "  :q         quit"
  , ""
  , "Type a declaration or an expression to evaluate it."
  ]

-- * REPL core

-- | Performs a single REPL step. Print prompt, read input,
-- dispatch.
replStep :: REPL ()
replStep =
  do
    f <- gets replFiles
    x <- getInputLine (List.unwords (List.reverse f) ++ "> ")
    c <- interpretInput (fmap id x)
    handleCommand c

-- | Run the interpreter as long as desired.
repl :: REPL ()
repl =
  do
    catchError replStep
      (\ e -> case e of
                Error      ->  return ()
                Terminate  ->  throwError Terminate)
    repl

-- * Command handling

-- | Preliminary input interpretation, based on the
-- current parser and no particular intelligence in
-- parsing commands correctly.
interpretInput :: Maybe String -> REPL REPLCommand
interpretInput Nothing  = return Quit
interpretInput (Just x)
  | ":l" `List.isPrefixOf` x = return $ Load $ norm $ strip xArg
  | ":r" `List.isPrefixOf` x = return Reload
  | ":b" `List.isPrefixOf` x = return Browse
  | ":q" `List.isPrefixOf` x = return Quit
  | ":c" `List.isPrefixOf` x = return Clear
  | ":e" `List.isPrefixOf` x = parseInputInteractive s2Terms Equal  xArg
  | ":t" `List.isPrefixOf` x = parseInputInteractive sTerm   TypeOf xArg
  | ":h" `List.isPrefixOf` x
 || ":?" `List.isPrefixOf` x = return Help
  | ":v" `List.isPrefixOf` x = return Version
  | ":"  `List.isPrefixOf` x = replMessage "unknown command" >> return Noop
  | otherwise                = parseInputInteractive sPhrase EvalPhrase x
  where
    norm  :: String -> String
    norm []              = []
    norm ('\\' : c : xs) = c : norm xs
    norm (       c : xs) = c : norm xs

    strip :: String -> String
    strip                = List.reverse
                         . List.dropWhile isSpace
                         . List.reverse
                         . List.dropWhile isSpace

    xArg  :: String
    xArg                 = snd $ break (== ' ') x

-- | Turn a string into a REPL command.
parseInput :: FilePath
           -> Parser t
           -> (t -> REPLCommand)
           -> Parser.String
           -> REPL REPLCommand
parseInput f p cmd s =
    case parse p f s of
      Left err   -> do
        replMessage $ "Parse error: " ++ (show) err ++ "\n"
        return Noop
      Right p'   -> return (cmd p')

parseInputInteractive :: Parser t
                      -> (t -> REPLCommand)
                      -> String
                      -> REPL REPLCommand
parseInputInteractive p cmd = parseInput "<interactive>" p cmd . Parser.fromString

-- | Command handler. Returns a flag indicating whether
-- the interpreter should continue.
handleCommand :: REPLCommand -> REPL ()
handleCommand c =
  case c of
    Version-> replMessage $ "PiSigma version " ++ showVersion version
    Startup-> handleCommand Version >> replMessage "Type :? for help."
    Help   -> replMessage help
    Load f ->
      do
        fs <- gets replFiles
        if f `elem` fs then
            replMessage $ "Skipping " ++ f ++ "."
          else do
            mx <- liftIO $ liftM (Just) (Parser.readFile f)
                    `catch` (\ (_ :: IOException) -> return Nothing)
            case mx of
              Nothing -> do
                replMessage $ "Could not find " ++ f ++ "."
                throwError Error
              Just x -> do
                -- Allow source files to have interpreter commands at the top;
                -- we currently use this as a replacement for a module system
                let spanP s      = case Parser.uncons s of
                                     Just (':', _) -> True
                                     _             -> False
                    (cmds, rest) = List.span spanP $ Parser.lines x
                mapM_ (\ c' -> handleCommand =<< (interpretInput . Just . Parser.toString) c') cmds
                p <- parseInput f sProg (EvalPhrase . Prog)
                                (Parser.unlines (List.replicate (List.length cmds) "" ++ rest))
                handleCommand p
                modify (\ s -> s { replFiles =
                                            case replFiles s of
                                             (g : fs') | g == f -> g : fs'
                                             xs                 -> f : xs })
                -- We print the "Loaded" message after executing initial
                -- interpreter commands in order to reflect the dependency
                -- order of different source files.
                replMessage $ "Loaded " ++ f ++ "."
    Reload ->
      do
        f <- gets replFiles
        handleCommand Clear
        mapM_ (handleCommand . Load) (List.reverse f)
    Browse ->
      do
        (Scope sc, _) <- gets replState
        let names :: [Pretty]
            names = map (text . Seq . fst) sc
        liftIO $ putPretty $ vcat names
        liftIO $ putStrLn ""
    Quit ->
      throwError Terminate
    EvalPhrase (Prog p) ->
      execProg p
    EvalPhrase (Term t) ->
      execTerm t
    Equal (t1,t2) ->
      eqTerms t1 t2
    TypeOf t ->
      inferTerm t
    Clear ->
      put initialREPLState
    Noop ->
      return ()

-- ** Command handler helpers

execProg :: Prog -> REPL ()
execProg p =
  do
    s <- get
    let (con,env) = replState s
    case run env (checkProg (p,con)) of
      Right s' -> put (s { replState = s' })
      Left e   -> do
                    liftIO $ Internal.putStrLn e
                    throwError Error

execTerm :: Term -> REPL ()
execTerm t =
  do
    s <- get
    let (con,env) = replState s
        p = do
          a  <- infer (t,con)
          t' <- nf [] (t,con)
          pa <- evalPrint a
          pt <- evalPrint t'
          return $ fromPretty $ pt <$> colon <+> align pa
    case run env p of
        Right (m,_) -> liftIO $ Internal.putStrLn m
        Left e      -> do
                         liftIO $ Internal.putStrLn e
                         throwError Error

eqTerms :: Term -> Term -> REPL ()
eqTerms t1 t2 =
  do
    s <- get
    let (con,env) = replState s
        p = eq (t1,con) (t2,con)
    case run env p of
        Right _ -> liftIO $ putStrLn "yes"
        Left e  -> do
                     liftIO $ Internal.putStrLn e
                     throwError Error
    

inferTerm :: Term -> REPL ()
inferTerm t =
  do
    s <- get
    let (con,env) = replState s
        p = evalPrint =<< infer (t,con)
    case run env p of
        Right (m,_) -> do
          liftIO $ putPretty m
          liftIO $ putStrLn ""
        Left e      -> do
                         liftIO $ Internal.putStrLn e
                         throwError Error

-- | Placeholder for a message function that can depend
-- on verbosity settings.
replMessage :: String -> REPL ()
replMessage = liftIO . putStrLn