module MasterPlan.Parser (runParser) where
import Control.Monad.State
import Data.Generics
import Data.List (nub)
import qualified Data.List.NonEmpty as NE
import qualified Data.Map as M
import qualified Data.Text as T
import Data.Void
import MasterPlan.Data
import Text.Megaparsec hiding (State, runParser)
import Text.Megaparsec.Char
import qualified Text.Megaparsec.Char.Lexer as L
import Text.Megaparsec.Expr
type Parser = ParsecT Void T.Text (State ProjectSystem)
sc ∷ Parser ()
sc = L.space space1 lineCmnt blockCmnt
where
lineCmnt = L.skipLineComment "//"
blockCmnt = L.skipBlockComment "/*" "*/"
lexeme ∷ Parser a → Parser a
lexeme = L.lexeme sc
symbol ∷ T.Text → Parser T.Text
symbol = L.symbol sc
parens ∷ Parser a → Parser a
parens = between (symbol "(") (symbol ")")
rws ∷ [String]
rws = map show [minBound :: ProjProperty ..]
identifier ∷ Parser String
identifier = (lexeme . try) (p >>= check)
where
p = (:) <$> letterChar <*> many alphaNumChar
check x
| x `elem` rws = fail $ "keyword " ++ show x ++ " cannot be an identifier"
| otherwise = pure x
stringLiteral :: Parser String
stringLiteral = char '"' >> manyTill L.charLiteral (char '"')
percentage :: Parser Float
percentage = do n <- L.float <?> "percentage value"
when (n > 100) $ fail $ "number " ++ show n ++ " is not within (0,100) range"
void $ symbol "%"
pure $ n / 100
nonNegativeNumber :: Parser Float
nonNegativeNumber = L.float
definition ∷ Parser ()
definition =
choice ([ propsProp PTitle stringLiteral (\v p -> p { title = v })
, propsProp PDescription stringLiteral (\v p -> p { description = Just v})
, propsProp PUrl stringLiteral (\v p -> p { url = Just v})
, propsProp POwner stringLiteral (\v p -> p { owner = Just v})
, taskProp PCost nonNegativeNumber (\v b -> case b of BindingAtomic r _ t p -> BindingAtomic r v t p; _ -> b)
, taskProp PTrust percentage (\v b -> case b of BindingAtomic r c _ p -> BindingAtomic r c v p; _ -> b)
, taskProp PProgress percentage (\v b -> case b of BindingAtomic r c t _ -> BindingAtomic r c t v; _ -> b)
, structure ] :: [Parser ()])
where
structure :: Parser ()
structure = do projName <- identifier
projectExpr <- symbol "=" *> expressionParser
sys <- lift get
let deps = dependencies sys projectExpr
when (projName `elem` deps) $ fail $ "definition of \"" ++ projName ++ "\" is recursive"
let binding = M.lookup projName $ bindings sys
newBinding <- case binding of
Nothing -> pure $ BindingExpr (defaultProjectProps { title=projName }) projectExpr
Just BindingExpr {} -> fail $ "Redefinition of \"" ++ projName ++ "\"."
Just (BindingPlaceholder p) -> pure $ BindingExpr p projectExpr
Just BindingAtomic {} -> fail $ "ProjectExpr \"" ++ projName ++ "\" is atomic"
lift $ put $ sys { bindings = M.insert projName newBinding $ bindings sys }
propsProp :: ProjProperty -> Parser a -> (a -> ProjectProperties -> ProjectProperties) -> Parser ()
propsProp prop valueParser modifier =
property prop valueParser setter
where
setter projName val Nothing = pure $ BindingPlaceholder $ modifier val $ defaultProjectProps { title=projName }
setter _ val (Just p) = pure $ everywhere (mkT $ modifier val) p
taskProp :: ProjProperty -> Parser a -> (a -> Binding -> Binding) -> Parser ()
taskProp prop valueParser modifier =
property prop valueParser setter
where
setter projName val Nothing = pure $ modifier val $ defaultTaskProj defaultProjectProps { title=projName }
setter projName _ (Just BindingExpr {}) = fail $ "ProjectExpr \"" ++ projName ++ "\" is not atomic."
setter _ val (Just (BindingPlaceholder p)) = pure $ modifier val $ defaultTaskProj p
setter _ val (Just p@BindingAtomic {}) = pure $ modifier val p
property ∷ ProjProperty → Parser a → (String -> a -> Maybe Binding -> Parser Binding) -> Parser ()
property prop valueParser setter =
do void $ symbol $ T.pack $ show prop
projName <- parens identifier
mBinding <- lift $ M.lookup projName <$> gets bindings
value <- symbol "=" *> valueParser
newBinding <- setter projName value mBinding
let modifySys :: ProjectSystem -> ProjectSystem
modifySys sys = sys { bindings = M.insert projName newBinding $ bindings sys }
lift $ modify modifySys
expressionParser ∷ Parser ProjectExpr
expressionParser =
simplifyProj <$> makeExprParser term table <?> "expression"
where
term = parens expressionParser <|> (Reference <$> identifier)
table = [[binary "*" (combineWith Product)]
,[binary "->" (combineWith Sequence)]
,[binary "+" (combineWith Sum)]]
binary op f = InfixL (f <$ symbol op)
combineWith :: (NE.NonEmpty ProjectExpr -> ProjectExpr) -> ProjectExpr -> ProjectExpr -> ProjectExpr
combineWith c p1 p2 = c $ p1 NE.<| [p2]
dependencies ∷ ProjectSystem -> ProjectExpr → [ProjectKey]
dependencies sys = everything (++) ([] `mkQ` collectDep)
where
collectDep (Reference n) = nub $ n : everything (++) ([] `mkQ` collectDep) (M.lookup n $ bindings sys)
collectDep _ = []
projectSystem :: Parser ProjectSystem
projectSystem =
do between sc eof definitionSeq
lift get
where
definitionSeq = void $ endBy1 definition (symbol ";")
runParser :: FilePath -> T.Text -> Either String ProjectSystem
runParser filename contents = let mr = runParserT projectSystem filename contents
initialPS = ProjectSystem M.empty
in case evalState mr initialPS of
Left e -> Left $ parseErrorPretty' contents e
Right v -> Right v