master-plan-0.3.1: src/MasterPlan/Parser.hs
{-|
Module : MasterPlan.Parser
Description : export parser for project systems
Copyright : (c) Rodrigo Setti, 2017
License : MIT
Maintainer : rodrigosetti@gmail.com
Stability : experimental
Portability : POSIX
-}
{-# LANGUAGE OverloadedLists #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE UnicodeSyntax #-}
module MasterPlan.Parser (ProjectKey, runParser) where
import Control.Monad.State
import qualified Data.List.NonEmpty as NE
import Data.Maybe (fromMaybe, isJust)
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 = Parsec Void T.Text
-- |Space consumer
sc ∷ Parser ()
sc = L.space space1 (L.skipLineComment "//") $
L.skipBlockComment "/*" "*/"
lexeme ∷ Parser a → Parser a
lexeme = L.lexeme sc
symbol ∷ T.Text → Parser T.Text
symbol = L.symbol sc
-- | 'parens' parses something between parenthesis.
parens ∷ Parser a → Parser a
parens = between (symbol "(") (symbol ")")
-- |list of reserved words
rws ∷ [String]
rws = map show [minBound :: ProjAttribute ..]
identifier ∷ Parser String
identifier = (lexeme . try) $ (:) <$> letterChar <*> many alphaNumChar
projectKey :: Parser String
projectKey = (identifier >>= check) <?> "project key"
where
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
-- |Parses the part of right-hand-side after the optional properties
-- (literal string title or properties between curly brackets)
expression ∷ ProjectProperties -> Parser (Project ProjectKey)
expression props =
simplify <$> makeExprParser term table <?> "expression"
where
term = parens (expression defaultProjectProps) <|> (Annotated <$> projectKey)
table = [[binary "*" (combineWith Product)]
,[binary "->" (combineWith Sequence)]
,[binary "+" (combineWith Sum)]]
binary op f = InfixL (f <$ symbol op)
combineWith c p1 p2 = c props $ p1 NE.<| [p2]
-- |Parses the entire right-hand-side of definitions
binding :: ProjectKey -> Parser (Project ProjectKey)
binding key = do (props, mc, mt, mp) <- try simpleTitle <|> try bracketAttributes <|> noAttributes
case (mc, mt, mp) of
(Nothing, Nothing, Nothing) ->
try (sc *> optional (symbol "=") *> expression props) <|>
pure (Atomic props defaultCost defaultTrust defaultProgress)
(mc', mt', mp') -> pure $ Atomic props
(fromMaybe defaultCost mc')
(fromMaybe defaultTrust mt')
(fromMaybe defaultProgress mp')
where
attrKey :: Parser ProjAttribute
attrKey = do n <- identifier <?> "attribute name"
case lookup n [(show a, a) | a <- [minBound::ProjAttribute ..]] of
Nothing -> fail $ "invalid attribute: \"" ++ n ++ "\""
Just a -> pure a
simpleTitle, bracketAttributes, noAttributes :: Parser (ProjectProperties, Maybe Cost, Maybe Trust, Maybe Progress)
simpleTitle = do s <- stringLiteral <?> "title"
pure (defaultProjectProps {title= Just s}, Nothing, Nothing, Nothing)
bracketAttributes = symbol "{" *> attributes (defaultProjectProps {title=Just key}) Nothing Nothing Nothing
noAttributes = pure (defaultProjectProps {title=Just key}, Nothing, Nothing, Nothing)
attributes :: ProjectProperties -> Maybe Cost -> Maybe Trust -> Maybe Progress
-> Parser (ProjectProperties, Maybe Cost, Maybe Trust, Maybe Progress)
attributes props mc mt mp =
try (sc *> symbol "}" *> pure (props, mc, mt, mp)) <|>
do attr <- sc *> attrKey
case attr of
PTitle -> do s <- stringLiteral <?> "title"
attributes (props {title=Just s}) mc mt mp
PDescription -> do when (isJust $ description props) $ fail "redefinition of description"
s <- stringLiteral <?> "description"
attributes (props {description=Just s}) mc mt mp
PUrl -> do when (isJust $ url props) $ fail "redefinition of url"
s <- stringLiteral <?> "url"
attributes (props {url=Just s}) mc mt mp
POwner -> do when (isJust $ owner props) $ fail "redefinition of owner"
s <- stringLiteral <?> "owner"
attributes (props {owner=Just s}) mc mt mp
PCost -> do when (isJust mc) $ fail "redefinition of cost"
c <- Cost <$> nonNegativeNumber <?> "cost"
attributes props (Just c) mt mp
PTrust -> do when (isJust mt) $ fail "redefinition of cost"
t <- Trust <$> percentage <?> "trust"
attributes props mc (Just t) mp
PProgress -> do when (isJust mp) $ fail "redefinition of progress"
p <- Progress <$> percentage <?> "progress"
attributes props mc mt (Just p)
-- |Parses the entire plan file, given the name of the "root" project.
-- returns this project
plan :: Bool -- ^ strict mode: fails if a reference has no definition
-> ProjectKey -- ^ the name of the root project
-> Parser ProjectExpr
plan strictMode root =
do bindings <- definitions []
case lookup root bindings of
Nothing -> fail $ "root project \"" ++ root ++ "\" is undefined"
Just p -> resolveReferences bindings [root] p
where
-- definitions will parse into a list of tuples: (key, Project ProjectKey)
definitions :: [(ProjectKey, Project ProjectKey)] -> Parser [(ProjectKey, Project ProjectKey)]
definitions ds = do key <- sc *> projectKey
when (key `elem` map fst ds) $ fail $ "redefinition of \"" ++ key ++ "\""
b <- binding key <* symbol ";"
let ds' = (key,b):ds
(try eof *> pure ds') <|> definitions ds'
resolveReferences :: [(ProjectKey, Project ProjectKey)]
-> [ProjectKey]
-> Project ProjectKey
-> Parser ProjectExpr
resolveReferences bs ks (Sum r ps) = Sum r <$> mapM (resolveReferences bs ks) ps
resolveReferences bs ks (Sequence r ps) = Sequence r <$> mapM (resolveReferences bs ks) ps
resolveReferences bs ks (Product r ps) = Product r <$> mapM (resolveReferences bs ks) ps
resolveReferences bs ks (Annotated k)
| k `elem` ks = fail $ "definition of \"" ++ k ++ "\" is recursive"
| otherwise = case lookup k bs of
Nothing
| strictMode -> fail $ "project \"" ++ k ++ "\" is undefined"
| otherwise -> pure $ Atomic defaultProjectProps {title=Just k}
defaultCost defaultTrust defaultProgress
Just p -> resolveReferences bs (k:ks) p
resolveReferences _ _ (Atomic r c t p) = pure $ Atomic r c t p
runParser :: Bool -- ^ strict mode: fails if a reference has no definition
-> FilePath -- ^ the file name to use in error messages
-> T.Text -- ^ contents of the plan to parse
-> ProjectKey -- ^ the name of the root project
-> Either String ProjectExpr
runParser strictMode filename contents root =
case parse (plan strictMode root) filename contents of
Left e -> Left $ parseErrorPretty' contents e
Right v -> Right v