husk-scheme-3.18: hs-src/Interpreter/shell.hs
{- |
Module : Main
Copyright : Justin Ethier
Licence : MIT (see LICENSE in the distribution)
Maintainer : github.com/justinethier
Stability : experimental
Portability : portable
This file implements a REPL /shell/ to host the interpreter, and also
allows execution of stand-alone files containing Scheme code.
-}
module Main where
import qualified Language.Scheme.Core as LSC -- Scheme Interpreter
import Language.Scheme.Types -- Scheme data types
import qualified Language.Scheme.Util as LSU (countAllLetters, countLetters, strip)
import qualified Language.Scheme.Variables as LSV -- Scheme variable operations
import Control.Monad.Error
import qualified Data.Char as DC
import qualified Data.List as DL
import Data.Maybe (fromMaybe)
import System.Console.GetOpt
import qualified System.Console.Haskeline as HL
import qualified System.Console.Haskeline.Completion as HLC
import System.Environment
import System.Exit (exitSuccess)
import System.IO
main :: IO ()
main = do
args <- getArgs
let (actions, nonOpts, _) = getOpt Permute options args
opts <- foldl (>>=) (return defaultOptions) actions
let Options {optInter = interactive, optSchemeRev = schemeRev} = opts
if null nonOpts
then do
LSC.showBanner
env <- liftIO $ getRuntimeEnv schemeRev
runRepl env
else do
runOne (getRuntimeEnv schemeRev) nonOpts interactive
--
-- Command line options section
--
data Options = Options {
optInter :: Bool,
optSchemeRev :: String -- RxRS version
}
-- |Default values for the command line options
defaultOptions :: Options
defaultOptions = Options {
optInter = False,
optSchemeRev = "5"
}
options :: [OptDescr (Options -> IO Options)]
options = [
Option ['i'] ["interactive"] (NoArg getInter) "load file and run REPL",
Option ['r'] ["revision"] (ReqArg writeRxRSVersion "Scheme") "scheme RxRS version",
Option ['h', '?'] ["help"] (NoArg showHelp) "show usage information"
]
where
getInter opt = return opt { optInter = True }
writeRxRSVersion arg opt = return opt { optSchemeRev = arg }
showHelp :: Options -> IO Options
showHelp _ = do
putStrLn "Usage: huski [options] [file]"
putStrLn ""
putStrLn " huski is the husk scheme interpreter."
putStrLn ""
putStrLn " File is a scheme source file to execute. If no file is specified"
putStrLn " the husk REPL will be started."
putStrLn ""
putStrLn " Options may be any of the following:"
putStrLn ""
putStrLn " -h, --help Display this information"
putStrLn " -i Start interactive REPL after file is executed. This"
putStrLn " option has no effect if a file is not specified. "
-- putStrLn " --revision rev Specify the scheme revision to use:"
-- putStrLn ""
-- putStrLn " 5 - r5rs (default)"
-- putStrLn " 7 - r7rs small"
putStrLn ""
exitSuccess
--
-- REPL Section
--
flushStr :: String -> IO ()
flushStr str = putStr str >> hFlush stdout
getRuntimeEnv :: String -> IO Env
getRuntimeEnv "7" = LSC.r7rsEnv
getRuntimeEnv _ = LSC.r5rsEnv
-- |Execute a single scheme file from the command line
runOne :: IO Env -> [String] -> Bool -> IO ()
runOne initEnv args interactive = do
env <- initEnv >>= flip LSV.extendEnv
[((LSV.varNamespace, "args"),
List $ map String $ drop 1 args)]
result <- (LSC.runIOThrows $ liftM show $
LSC.evalLisp env (List [Atom "load", String (head args)]))
_ <- case result of
Just errMsg -> putStrLn errMsg
_ -> do
-- Call into (main) if it exists...
alreadyDefined <- liftIO $ LSV.isBound env "main"
let argv = List $ map String args
when alreadyDefined (do
mainResult <- (LSC.runIOThrows $ liftM show $
LSC.evalLisp env (List [Atom "main", List [Atom "quote", argv]]))
case mainResult of
Just errMsg -> putStrLn errMsg
_ -> return ())
when interactive (do
runRepl env)
-- |Start the REPL (interactive interpreter)
runRepl :: Env -> IO ()
runRepl env' = do
let settings = HL.Settings (completeScheme env') Nothing True
HL.runInputT settings (loop env')
where
-- Main REPL loop
loop :: Env -> HL.InputT IO ()
loop env = do
minput <- HL.getInputLine "huski> "
case minput of
Nothing -> return ()
Just i -> do
case LSU.strip i of
"quit" -> return ()
"" -> loop env -- ignore inputs of just whitespace
input -> do
inputLines <- getMultiLine [input]
let input' = unlines inputLines
result <- liftIO (LSC.evalString env input')
if not (null result)
then do HL.outputStrLn result
loop env
else loop env
-- Read another input line, if necessary
getMultiLine previous = do
if test previous
then do
mb_input <- HL.getInputLine ""
case mb_input of
Nothing -> return previous
Just input -> getMultiLine $ previous ++ [input]
else return previous
-- Check if we need another input line
-- This just does a bare minimum, and could be more robust
test ls = do
let cOpen = LSU.countAllLetters '(' ls
cClose = LSU.countAllLetters ')' ls
cOpen > cClose
-- |Auto-complete using scheme symbols
completeScheme :: Env -> (String, String)
-> IO (String, [HLC.Completion])
-- Right after a ')' it seems more useful to autocomplete next closed parenthesis
completeScheme _ (lnL@(')':_), _) = do
let cOpen = LSU.countLetters '(' lnL
cClose = LSU.countLetters ')' lnL
if cOpen > cClose
then return (lnL, [HL.Completion ")" ")" False])
else return (lnL, [])
completeScheme env (lnL, lnR) = do
complete $ reverse $ readAtom lnL
where
complete ('"' : _) = do
-- Special case, inside a string it seems more
-- useful to autocomplete filenames
liftIO $ HLC.completeFilename (lnL, lnR)
complete pre = do
-- Get list of possible completions from ENV
xps <- LSV.recExportsFromEnv env
let allDefs = xps ++ specialForms
let allDefs' = filter (\ (Atom a) -> DL.isPrefixOf pre a) allDefs
let comps = map (\ (Atom a) -> HL.Completion a a False) allDefs'
-- Get unused portion of the left-hand string
let unusedLnL = fromMaybe lnL (DL.stripPrefix (reverse pre) lnL)
return (unusedLnL, comps)
-- Not loaded into an env, so we need to list them here
specialForms = map Atom [
"define"
, "define-syntax"
, "expand"
, "hash-table-delete!"
, "hash-table-set!"
, "if"
, "lambda"
, "let-syntax"
, "letrec-syntax"
, "quote"
, "set!"
, "set-car!"
, "set-cdr!"
, "string-set!"
, "vector-set!"]
-- Read until the end of the current symbol (atom), if there is one.
-- There is also a special case for files if a double-quote is found.
readAtom (c:cs)
| c == '"' = ['"'] -- Save to indicate file completion to caller
| c == '(' = []
| c == '[' = []
| DC.isSpace c = []
| otherwise = (c : readAtom cs)
readAtom [] = []