packages feed

funcons-tools-0.1.0.0: src/Funcons/RunOptions.hs

{-# LANGUAGE OverloadedStrings, TupleSections #-}

module Funcons.RunOptions where

import Funcons.Types
import Funcons.Parser

import Text.ParserCombinators.Parsec
import qualified Text.ParserCombinators.Parsec.Token as P
import qualified Text.ParserCombinators.Parsec.Language as L 

import qualified Data.Map as M
import Control.Monad (when)
import Data.Char (isSpace)
import Data.Text (pack)
import Data.List (isSuffixOf, isPrefixOf)
import Data.List.Split (splitOn)

import System.Directory (doesFileExist)

type GeneralOptions = M.Map Name String
type BuiltinFunconsOptions = M.Map Name Funcons
type TestOptions = M.Map Name Funcons
type InputValues = M.Map Name [Values]

data RunOptions = RunOptions {
            mfuncon_term        :: Maybe Funcons
        ,   general_opts        :: GeneralOptions 
        ,   builtin_funcons     :: BuiltinFunconsOptions 
        ,   expected_outcomes   :: TestOptions
        ,   given_inputs        :: InputValues
        }

defaultRunOptions :: RunOptions
defaultRunOptions = RunOptions Nothing M.empty M.empty M.empty M.empty

optionsOverride opts opts' = RunOptions
    (maybe (mfuncon_term opts) Just (mfuncon_term opts'))
    (general_opts opts `M.union` general_opts opts')
    (builtin_funcons opts `M.union` builtin_funcons opts')
    (expected_outcomes opts `M.union` expected_outcomes opts')
    (given_inputs opts `M.union` given_inputs opts')

funcon_term :: RunOptions -> Funcons
funcon_term = maybe err id . mfuncon_term  
    where err = error "Please give a .fct file as an argument or use the --funcon-term flag"

bool_opt_default :: Bool -> Name -> M.Map Name String -> Bool
bool_opt_default def nm m = case M.lookup nm m of
    Nothing         -> def 
    Just "false"    -> False
    _               -> True


bool_opt :: Name -> M.Map Name String -> Bool
bool_opt nm m = bool_opt_default False nm m 

do_refocus :: RunOptions -> Bool
do_refocus opts = bool_opt "refocus" (general_opts opts)

max_restarts :: RunOptions -> Maybe Int
max_restarts = fmap read . M.lookup "max-restarts" . general_opts

do_abrupt_terminate :: RunOptions -> Bool
do_abrupt_terminate = not . bool_opt "no-abrupt-termination" . general_opts

pp_full_environments :: RunOptions -> Bool
pp_full_environments = bool_opt "full-environments" . general_opts

show_result :: RunOptions -> Bool
show_result opts = if bool_opt "hide-result" (general_opts opts) 
    then False
    else not (interactive_mode opts)

show_counts :: RunOptions -> Bool
show_counts opts = if bool_opt "display-steps" (general_opts opts) 
    then not (interactive_mode opts)
    else False

show_mutable :: RunOptions -> [Name]
show_mutable = maybe [] (map pack . splitOn ",") . M.lookup "display-mutable-entity"  . general_opts

hide_output :: RunOptions -> [Name]
hide_output = maybe [] (map pack . splitOn ",") . M.lookup "hide-output-entity"  . general_opts

hide_input :: RunOptions -> [Name]
hide_input = maybe [] (map pack . splitOn ",") . M.lookup "hide-input-entity"  . general_opts

hide_control :: RunOptions -> [Name]
hide_control = maybe [] (map pack . splitOn ",") . M.lookup "hide-control-entity" . general_opts 

interactive_mode :: RunOptions -> Bool
interactive_mode opts = if bool_opt "interactive-mode" (general_opts opts)
    then M.null (inputValues opts) 
    else False

pp_string_outputs :: RunOptions -> Bool
pp_string_outputs = bool_opt "format-string-outputs" . general_opts

string_inputs :: RunOptions -> Bool
string_inputs = bool_opt "string-inputs" . general_opts

show_tests :: RunOptions -> Bool
show_tests opts = if bool_opt "hide-tests" (general_opts opts) 
        then False
        else M.size (expected_outcomes opts) > 0

show_output_only :: RunOptions -> Bool
show_output_only opts = if bool_opt "show-output-only" (general_opts opts)
        then True
        else interactive_mode opts

auto_config :: RunOptions -> Bool
auto_config opts = bool_opt_default True "auto-config" (general_opts opts)

inputValues :: RunOptions -> InputValues 
inputValues = given_inputs

booleanOptions = ["refocus", "full-environments", "hide-result", "display-steps"
    , "no-abrupt-termination", "interactive-mode", "string-inputs", "format-string-outputs"
    , "hide-tests", "show-output-only", "auto-config"]
booleanOptions_ = map ("--" ++) booleanOptions

stringOptions = ["display-mutable-entity", "hide-output-entity"
    , "hide-control-entity", "hide-input-entity", "max-restarts"]
stringOptions_ = map ("--" ++) stringOptions

allOptions = "funcon-term" : booleanOptions ++ stringOptions 
allOptions_ = "--funcon-term" : booleanOptions_ ++ stringOptions_
    
run_options :: [String] -> IO (RunOptions, [String])
run_options = fold (defaultRunOptions, [])
 where  fold (opts,errors) (arg:args)
            | arg `elem` booleanOptions_ = 
                let (val, rest) 
                        | not (null args) 
                        , not (isPrefixOf "--" (head args)) = (head args, tail args)
                        | otherwise = ("true", args)
                    opts' = opts {general_opts = M.insert (pack (tail (tail arg)))
                                    val (general_opts opts)}
                in fold (opts',errors) rest
            | arg `elem` stringOptions_ && length args > 0 =
                let opts' = opts {general_opts = M.insert (pack (tail (tail arg))) 
                                    (head args) (general_opts opts)}
                in fold (opts', errors) (tail args)
            | arg == "--funcon-term" &&  length args > 0 =
                let opts' = opts {mfuncon_term = Just (read (head args))}
                in fold (opts', errors) (tail args)
            | isSuffixOf ".fct" arg = do
                fct <- readFile arg
                let cfg_name = take (length arg - 4) arg ++ ".config"
                exists <- doesFileExist cfg_name
                opts' <- if exists && auto_config opts 
                            then readFile cfg_name >>= 
                                    return . flip (parseAndApplyConfig cfg_name) opts
                            else return opts 
                let opts'' = opts' {mfuncon_term = Just (read fct)}
                fold (opts'', errors) args
            | isSuffixOf ".config" arg = fold (opts, errors) ("--config":arg:args)
            | arg == "--config" && length args > 0 = do
                let cfg_name = head args
                exists <- doesFileExist cfg_name
                when (not exists) (error ("config file not found: " ++ cfg_name))
                str <- readFile cfg_name 
                let opts' = parseAndApplyConfig cfg_name str opts
                fold (opts', errors) (tail args)
            | otherwise = do
                exists <- doesFileExist (arg++".fct")
                if exists then fold (opts, errors) ((arg++".fct"):args)
                          else fold (opts, arg:errors) args
        fold (opts, errors) [] = return (opts, errors)

parseAndApplyConfig :: FilePath -> String -> RunOptions -> RunOptions
parseAndApplyConfig fp str = optionsOverride (parser pRunOptions fp str)

pRunOptions :: Parser RunOptions
pRunOptions = foldr optionsOverride defaultRunOptions <$> 
                    many (choice [  keyword "general" *> braces pGeneral
                                 ,  keyword "funcons" *> braces pBuiltinFuncons
                                 ,  keyword "tests" *> braces pTestOutcomes
                                 ,  keyword "inputs" *> braces pInputValues ] )
 where  runOptions :: Maybe RunOptions -> Maybe BuiltinFunconsOptions -> 
                        Maybe TestOptions -> RunOptions
        runOptions mopts mbin mtests = let opts = maybe defaultRunOptions id mopts
                in opts { builtin_funcons = maybe M.empty id mbin
                        , expected_outcomes = maybe M.empty id mtests }

pGeneral :: Parser RunOptions
pGeneral = toOpts <$> 
                (optionMaybe (keyword "funcon-term" *> colon *> pFuncons <* semiColon))
            <*> (M.fromList <$> many pKeyValues)
 where toOpts mf gen = defaultRunOptions {mfuncon_term = mf, general_opts = gen}
       pKeyValues = pBoolOpts <|> pStringOpts
        where   pBoolOpts = choice (map pKeyValue booleanOptions)
                 where pKeyValue key = (pack key,) . maybe "true" id 
                            <$ keyword key <*> optionMaybe (colon *> pBool) <* semiColon
                    
                pStringOpts = choice (map pKeyValue stringOptions)
                 where pKeyValue key = (pack key,) <$ keyword key <* colon <*> pStringValue <* semiColon

pBool :: Parser String
pBool = string "true" <|> string "false"

pStringValue :: Parser String
pStringValue = filter (not . isSpace) . concat <$> commaSep1 stringValue
    where stringValue = many1 (choice [alphaNum, char '-'])

pFunconName = many1 (choice [alphaNum, char '-'])
 
pFunconTerm :: Parser Funcons
pFunconTerm = pFuncons

pBuiltinFuncons :: Parser RunOptions
pBuiltinFuncons = (\list -> defaultRunOptions {builtin_funcons = M.fromList list}) <$> 
    many ((,) . pack <$> pFunconName <* equals <*> pFunconTerm <* semiColon)

pTestOutcomes :: Parser RunOptions
pTestOutcomes = toOptions <$> (M.union <$> pResult <*> pEntityValues)
    where pResult = mStoreResult <$> optionMaybe (id <$ keyword "result-term" <* colon 
                                        <*> pFunconTerm <* semiColon)
            where mStoreResult Nothing = M.empty
                  mStoreResult (Just f) = M.singleton "result-term" f
          pEntityValues = M.fromList <$>    
            many (((,) . pack <$> pFunconName <* colon <*> pFunconTerm <* semiColon))
          toOptions map = defaultRunOptions { expected_outcomes = map }

pInputValues :: Parser RunOptions 
pInputValues = (\list -> defaultRunOptions { given_inputs = M.fromList list}) <$>
    many (toPair <$> pFunconName <* colon <*> pFunconTerm <* semiColon) 
    where toPair nm f = case recursiveFunconValue f of 
                Just (List vs)  -> (pack nm, vs)
                _               -> error ("inputs for " ++ nm ++ " not a list")

-- simple lexer
languageDef = L.emptyDef {
                    P.identStart = lower
                ,   P.identLetter = lower <|> oneOf "-"
                ,   P.reservedNames = allOptions ++ ["result-term"]
                ,   P.reservedOpNames = [":"]
                ,   P.commentLine = "--"
                }

language = P.makeTokenParser languageDef

equals = whitespace *> char '=' <* whitespace
colon = whitespace *> char ':' <* whitespace
semiColon = whitespace *> char ';' <* whitespace

whitespace = P.whiteSpace language
comma = P.comma language
keyword = P.reserved language
braces = P.braces language
identifier = P.identifier language
stringLiteral = P.stringLiteral language
commaSep1 = P.commaSep1 language
semiSep = P.semiSep language