packages feed

disco-0.1.0.0: src/Disco/Interactive/CmdLine.hs

-----------------------------------------------------------------------------
-- |
-- Module      :  Disco.Interactive.CmdLine
-- Copyright   :  disco team and contributors
-- Maintainer  :  byorgey@gmail.com
--
-- SPDX-License-Identifier: BSD-3-Clause
--
-- Definition of the command-line REPL interface for Disco.
--
-----------------------------------------------------------------------------

module Disco.Interactive.CmdLine
  ( -- * Command-line options record

    DiscoOpts(..)

    -- * optparse-applicative command line parsers
  , discoOpts, discoInfo

    -- * main

  , discoMain

  ) where

import           Control.Lens                           hiding (use)
import           Control.Monad                          (unless)
import qualified Control.Monad.Catch                    as CMC
import           Control.Monad.IO.Class                 (MonadIO (..))
import           Data.Foldable                          (forM_)
import           Data.List                              (isPrefixOf)
import           Data.Maybe                             (isJust)
import           System.Exit                            (exitFailure,
                                                         exitSuccess)

import qualified Options.Applicative                    as O
import           System.Console.Haskeline               as H

import           Disco.Error
import           Disco.Eval
import           Disco.Interactive.Commands
import           Disco.Messages
import           Disco.Module                           (miExts)
import           Disco.Pretty

import           Disco.Effects.State
import           Polysemy
import           Polysemy.ConstraintAbsorber.MonadCatch
import           Polysemy.Error

------------------------------------------------------------
-- Command-line options parser
------------------------------------------------------------

-- | Command-line options for disco.
data DiscoOpts = DiscoOpts
  { evaluate  :: Maybe String  -- ^ A single expression to evaluate
  , cmdFile   :: Maybe String  -- ^ Execute the commands in a given file
  , checkFile :: Maybe String  -- ^ Check a file and then exit
  , debugFlag :: Bool
  }

discoOpts :: O.Parser DiscoOpts
discoOpts = DiscoOpts
  <$> O.optional (
        O.strOption (mconcat
          [ O.long "evaluate"
          , O.short 'e'
          , O.help "evaluate an expression"
          , O.metavar "TERM"
          ])
      )
  <*> O.optional (
        O.strOption (mconcat
          [ O.long "file"
          , O.short 'f'
          , O.help "execute the commands in a file"
          , O.metavar "FILE"
          ])
      )
  <*> O.optional (
        O.strOption (mconcat
          [ O.long "check"
          , O.help "check a file without starting the interactive REPL"
          , O.metavar "FILE"
          ])
      )
  <*> O.switch (
        mconcat
        [ O.long "debug"
        , O.help "print debugging information"
        , O.short 'd'
        ]
        )

discoInfo :: O.ParserInfo DiscoOpts
discoInfo = O.info (O.helper <*> discoOpts) $ mconcat
  [ O.fullDesc
  , O.progDesc "Command-line interface for Disco, a programming language for discrete mathematics."
  , O.header "disco v0.1"
  ]

optsToCfg :: DiscoOpts -> DiscoConfig
optsToCfg opts = initDiscoConfig & debugMode .~ debugFlag opts

------------------------------------------------------------
-- Command-line interface
------------------------------------------------------------

banner :: String
banner = "Welcome to Disco!\n\nA language for programming discrete mathematics.\n\n"

discoMain :: IO ()
discoMain = do
  opts <- O.execParser discoInfo

  let batch = any isJust [evaluate opts, cmdFile opts, checkFile opts]
  unless batch $ putStr banner
  runDisco (optsToCfg opts) $ do
    case checkFile opts of
      Just file -> do
        res <- handleLoad file
        liftIO $ if res then exitSuccess else exitFailure
      Nothing   -> return ()
    case cmdFile opts of
      Just file -> do
        mcmds <- loadFile file
        case mcmds of
          Nothing   -> return ()
          Just cmds -> mapM_ handleCMD (lines cmds)
      Nothing   -> return ()
    forM_ (evaluate opts) handleCMD
    unless batch loop

  where

    -- These types used to involve InputT Disco, but we now use Final
    -- (InputT IO) in the list of effects.  see
    -- https://github.com/polysemy-research/polysemy/issues/395 for
    -- inspiration.

    ctrlC :: MonadIO m => m a -> SomeException -> m a
    ctrlC act e = do
      liftIO $ print e
      act

    withCtrlC :: (MonadIO m, CMC.MonadCatch m) => m a -> m a -> m a
    withCtrlC resume act = CMC.catch act (ctrlC resume)

    loop :: Members DiscoEffects r => Sem r ()
    loop = do
      minput <- embedFinal $ withCtrlC (return $ Just "") (getInputLine "Disco> ")
      case minput of
        Nothing -> return ()
        Just input
          | ":q" `isPrefixOf` input && input `isPrefixOf` ":quit" -> do
              liftIO $ putStrLn "Goodbye!"
              return ()
          | ":{" `isPrefixOf` input -> do
              multiLineLoop []
              loop
          | otherwise -> do
              mapError @_ @DiscoError (Panic . show) $
                absorbMonadCatch $
                withCtrlC (return ()) $
                handleCMD input
              loop

    multiLineLoop :: Members DiscoEffects r => [String] -> Sem r ()
    multiLineLoop ls = do
      minput <- embedFinal $ withCtrlC (return Nothing) (getInputLine "Disco| ")
      case minput of
        Nothing -> return ()
        Just input
          | ":}" `isPrefixOf` input -> do
              mapError @_ @DiscoError (Panic . show) $
                absorbMonadCatch $
                withCtrlC (return ()) $
                handleCMD (unlines (reverse ls))
          | otherwise -> do
              multiLineLoop (input:ls)

-- | Parse and run the command corresponding to some REPL input.
handleCMD :: Members DiscoEffects r => String -> Sem r ()
handleCMD "" = return ()
handleCMD s = do
  exts <- use @TopInfo (replModInfo . miExts)
  case parseLine discoCommands exts s of
    Left m  -> info (text m)
    Right l -> catch @DiscoError (dispatch discoCommands l) (info . pretty')
                -- The above has to be catch, not outputErrors, because
                -- the latter won't resume afterwards.