packages feed

mikrokosmos-0.4.0: source/Main.hs

module Main where

import           Control.Monad.Trans
import           Control.Monad.State
import           Control.Exception
import           Data.List
import           System.Directory
import           System.Console.Haskeline
import           Text.ParserCombinators.Parsec hiding (try)
import           Format
import           Interpreter
import           Environment
import           Options hiding (defaultOptions)


-- Lambda interpreter
-- The actions of the interpreter are written here. It allows to execute normal
-- actions (bindings and evaluation), and interpreter specific actions, as "quit"
-- or "load".


-- | Runs the interpreter with default settings and an empty context.
main :: IO ()
main =
  -- Uses the Options library, which requires the program to start with
  -- runCommand. The flags are stored in opts and other command line arguments
  -- are stored in args.
  runCommand $ \opts args -> do
  
  -- Reads the flags
  case flagVersion opts of
    True -> putStrLn versionText
    False ->
      case args of
        [] -> runInputT defaultSettings ( outputStrLn initialText
                                          >> interpreterLoop defaultEnv
                                        )
        [filename] -> executeFile filename
        _ -> putStrLn "Wrong number of arguments"


-- | Interpreter awaiting for an instruction.
interpreterLoop :: Environment -> InputT IO ()
interpreterLoop environment = do
  -- Gets the user input on the interpreter
  -- and parses it to a concrete action.
  minput <- getInputLine promptText
  let interpreteraction =
        case minput of
          Nothing -> Quit
          Just "" -> EmptyLine
          Just input -> case parse interpreteractionParser "" input of
            Left _  -> Error
            Right a -> a

  -- Executes the parsed action, every action may affect the
  -- context in a way, and returns the control to the interpreter. 
  case interpreteraction of
    -- Interprets an action
    Interpret action -> case runState (act action) environment of
                          (output, newenv) -> do
                            outputActions newenv output
                            interpreterLoop newenv

    -- Loads a module and its dependencies given its name.
    -- Avoids repeated modules keeping only their first ocurrence.
    Load modulename -> do
      modules <- lift $ (nub <$> readAllModuleDepsRecursively [modulename])
      files <- lift $ mapM findFilename modules

      -- Concats all the module contents
      maybeactions <- (fmap concat) . sequence <$> (lift $ mapM loadFile files)
      case maybeactions of
        Nothing -> do
          outputStrLn "Error loading file"
          interpreterLoop environment
        Just actions -> case runState (multipleAct actions) environment of
                          (output, newenv) -> do
                            outputActions newenv output
                            interpreterLoop newenv
    
    -- Ignores the empty line
    EmptyLine -> interpreterLoop environment
    
    -- Exists the interpreter
    Quit -> return ()

    -- Restarts the interpreter context
    Restart -> interpreterLoop defaultEnv

    -- Unknown command
    Error -> do
      outputStr (if getColor environment then formatFormula else "")
      outputStrLn "Unknown command"
      outputStr end
      interpreterLoop environment

    -- Sets the verbose option
    SetVerbose setting -> do
      outputStrLn $
        (if getColor environment then formatFormula else "") ++
        "verbose mode: " ++ if setting then "on" else "off" ++
        end
      interpreterLoop (changeVerbose environment setting)
      
    -- Sets the color option
    SetColor setting -> do
      outputStrLn $
        (if getColor environment then formatFormula else "") ++
        "color mode: " ++ if setting then "on" else "off" ++
        end
      interpreterLoop (changeColor environment setting)

    -- Sets the ski option
    SetSki setting -> do
      outputStrLn $
        (if getColor environment then formatFormula else "") ++
        "ski mode: " ++ if setting then "on" else "off" ++
        end
      interpreterLoop (changeSkioutput environment setting)

    -- Sets the types option
    SetTypes setting -> do
      outputStrLn $
        (if getColor environment then formatFormula else "") ++
        "types: " ++ if setting then "on" else "off" ++
        end
      interpreterLoop (changeTypes environment setting)

    -- Prints the help
    Help -> outputStr helpText >> interpreterLoop environment




-- | Outputs results from actions. Given a list of options and outputs,
--   formats and prints them in console.
outputActions :: Environment -> [String] -> InputT IO ()
outputActions environment output = do
    outputStr (if getColor environment then formatFormula else "")
    mapM_ (outputStr . format) output
    outputStr end
  where
    format = formatColor . formatVerbose

    formatColor s
      | getColor environment = s
      | otherwise            = unlines $ map decolor $ lines s
    
    formatVerbose "" = ""
    formatVerbose s
      | not (getVerbose environment) = (++"\n") . last . lines $ s
      | otherwise                    = s



-- Loading and reading files
-- | Loads the given filename and returns the complete list of actions.
--   Returns Nothing if there is an error reading or parsing the file.
loadFile :: String -> IO (Maybe [Action])
loadFile filename = do
  putStrLn $ formatLoading ++ "Loading " ++ filename ++ "..." ++ end
  input <- try $ (readFile filename) :: IO (Either IOException String)
  case input of
    Left _ -> return Nothing
    Right inputs -> do
      let parsing = map (parse actionParser "") $ filter (/="") $ lines inputs
      let actions = map (\x -> case x of
                                 Left _  -> Nothing
                                 Right a -> Just a) parsing
      return $ sequence actions

-- | Executes the commands inside a file. A .mkr file can contain a sequence of
--   expressions and variable bindings, and it is interpreted sequentially.
executeFile :: Filename -> IO ()
executeFile filename = do
  maybeloadfile <- loadFile filename
  case maybeloadfile of
    Nothing    -> putStrLn "Error loading file"
    Just actions -> case runState (multipleAct actions) defaultEnv of
                      (outputs, _) -> mapM_ (putStr . format) outputs
                      where
                        format :: String -> String
                        format "" = ""
                        format s = (++"\n") . last . lines $ s


-- | Reads module dependencies
readFileDependencies :: Filename -> IO [Modulename]
readFileDependencies filename = do
  input <- try $ (readFile filename) :: IO (Either IOException String)
  case input of
    Left _ -> return []
    Right inputs -> return $
      map (drop 9) (filter (isPrefixOf "#INCLUDE ") $ filter (/="") $ lines inputs)

-- | Reads all the dependencies from a module list
readAllModuleDeps :: [Modulename] -> IO [Modulename]
readAllModuleDeps modulenames = do
  files <- mapM findFilename modulenames
  deps <- mapM readFileDependencies files
  return $ concat deps

-- | Read module dependencies recursively
readAllModuleDepsRecursively :: [Modulename] -> IO [Modulename]
readAllModuleDepsRecursively modulenames = do
  newmodulenames <- readAllModuleDeps modulenames
  let allmodulenames = nub (newmodulenames ++ modulenames)
  if modulenames == allmodulenames
    then return modulenames
    else readAllModuleDepsRecursively allmodulenames

-- | Given a module name, returns the filename associated with it
findFilename :: Modulename -> IO Filename
findFilename s = do
  appdir <- getAppUserDataDirectory "mikrokosmos"
  homedir <- getHomeDirectory

  -- Looks for the module in the common locations
  head <$> filterM doesFileExist
    [ "lib/" ++ s ++ ".mkr"
    , "./" ++ s ++ ".mkr"
    , appdir ++ "/" ++ s ++ ".mkr"
    , homedir ++ "/" ++ s ++ ".mkr"
    ]


-- Flags
-- | Flags datatype
data MainFlags = MainFlags
  { flagExec :: String
  , flagVersion :: Bool
  }

instance Options MainFlags where
  -- | Flags definition
  defineOptions = pure MainFlags
    <*> simpleOption "exec" ""
    "A file to execute and show its results"
    <*> simpleOption "version" False
    "Show program version"