packages feed

husk-scheme-3.12: 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 Paths_husk_scheme
import qualified Language.Scheme.Core as LSC -- Scheme Interpreter
import Language.Scheme.Types                 -- Scheme data types
import qualified Language.Scheme.Util as LSU (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 System.Cmd (system)
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 (ExitCode (..), exitWith, exitFailure)
import System.IO

main :: IO ()
main = do 
  args <- getArgs

  let (actions, nonOpts, msgs) = getOpt Permute options args
  opts <- foldl (>>=) (return defaultOptions) actions
  let Options {optSchemeRev = schemeRev} = opts

  if null nonOpts 
     then do 
       LSC.showBanner
       runRepl schemeRev
     else runOne schemeRev nonOpts

--
-- Command line options section
--

data Options = Options {
    optSchemeRev :: String -- RxRS version
    }

-- |Default values for the command line options
defaultOptions :: Options
defaultOptions = Options {
    optSchemeRev = "5"
    }
options :: [OptDescr (Options -> IO Options)]
options = [
  Option ['r'] ["revision"] (ReqArg writeRxRSVersion "Scheme") "scheme RxRS version",
  Option ['h', '?'] ["help"] (NoArg showHelp) "show usage information"
  ]

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"
-- TODO: specify scheme rev via command line
--  putStrLn "  -r --revision     Specify the scheme revision to use:"
--  putStrLn ""
--  putStrLn "                      5 - r5rs (default)"
--  putStrLn "                      7 - r7rs small"
  putStrLn ""
  exitWith ExitSuccess

--
-- REPL Section
--

flushStr :: String -> IO ()
flushStr str = putStr str >> hFlush stdout

-- |Execute a single scheme file from the command line
runOne :: String -> [String] -> IO ()
runOne _ args = do
  env <- LSC.r5rsEnv >>= flip LSV.extendEnv
                          [((LSV.varNamespace, "args"),
                           List $ map String $ drop 1 args)]

  result <- (LSC.runIOThrows $ liftM show $ 
             LSC.evalLisp env (List [Atom "load", String (args !! 0)]))
  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 ())

-- |Start the REPL (interactive interpreter)
runRepl :: String -> IO ()
runRepl _ = do
    env <- LSC.r5rsEnv

    let settings = HL.Settings (completeScheme env) Nothing True
    HL.runInputT settings (loop env)
    where
        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
                        result <- liftIO (LSC.evalString env input)
                        if (length result) > 0
                           then do HL.outputStrLn result
                                   loop env
                           else loop env

-- |Auto-complete using scheme symbols
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 = case DL.stripPrefix (reverse pre) lnL of
                     Just s -> s
                     Nothing -> lnL
   return (unusedLnL, comps)

  -- Not loaded into an env, so we need to list them here
  specialForms = map (\ s -> Atom s) [ 
       "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 [] = []