packages feed

ginger2-2.0.0.0: src/Language/Ginger/Parse.hs

{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE TupleSections #-}
{-# OPTIONS_GHC -Wno-unused-imports  #-}

module Language.Ginger.Parse
( P
, expr
, statement
, template
, parseGinger
, parseGingerFile
, parseGingerWith
, parseGingerFileWith
, POptions (..)
, defPOptions
, BlockTrimming (..)
, BlockStripping (..)
, simplifyS
)
where

import Control.Monad (void, when, replicateM)
import Control.Monad.Reader (Reader, runReader, ask, asks)
import Data.Char (isAlphaNum, isAlpha, isDigit, isSpace, chr)
import Data.List.NonEmpty (NonEmpty (..))
import Data.Maybe (catMaybes)
import Data.Text (Text)
import qualified Data.Text as Text
import qualified Data.Text.IO as Text
import Data.Vector (Vector)
import qualified Data.Vector as V
import Data.Void (Void)
import Text.Megaparsec as Megaparsec
import Text.Megaparsec.Char

import Language.Ginger.AST as AST
import Language.Ginger.SourcePosition as AST

--------------------------------------------------------------------------------
-- Parser Type
--------------------------------------------------------------------------------

type P = ParsecT Void Text (Reader POptions)

data BlockTrimming
  = NoTrimBlocks
  | TrimBlocks
  deriving (Show, Read, Eq, Ord, Enum, Bounded)

data BlockStripping
  = NoStripBlocks
  | StripBlocks
  deriving (Show, Read, Eq, Ord, Enum, Bounded)

data PolicyOverride
  = Default
  | Never
  | Always
  deriving (Show, Read, Eq, Ord, Enum, Bounded)

class OverridablePolicy a where
  override :: PolicyOverride -> a -> a

instance OverridablePolicy BlockTrimming where
  override Default a = a
  override Never _ = NoTrimBlocks
  override Always _ = TrimBlocks

instance OverridablePolicy BlockStripping where
  override Default a = a
  override Never _ = NoStripBlocks
  override Always _ = StripBlocks

data POptions =
  POptions
    { pstateTrimBlocks :: !BlockTrimming
    , pstateStripBlocks :: !BlockStripping
    }
  deriving (Show, Read, Eq)

instance OverridablePolicy POptions where
  override o (POptions tr st) = POptions (override o tr) (override o st)


defPOptions :: POptions
defPOptions = POptions
  { pstateTrimBlocks = TrimBlocks
  , pstateStripBlocks = NoStripBlocks
  }

--------------------------------------------------------------------------------
-- Running Parsers
--------------------------------------------------------------------------------

parseGinger :: P a -> FilePath -> Text -> Either String a
parseGinger = parseGingerWith defPOptions

parseGingerWith :: POptions -> P a -> FilePath -> Text -> Either String a
parseGingerWith options p filename input =
  mapLeft errorBundlePretty $ runReader (runParserT p filename input) options

parseGingerFile :: P a -> FilePath -> IO (Either String a)
parseGingerFile = parseGingerFileWith defPOptions

parseGingerFileWith :: POptions -> P a -> FilePath -> IO (Either String a)
parseGingerFileWith options p filename = do
  input <- Text.readFile filename
  return $ mapLeft errorBundlePretty $ runReader (runParserT p filename input) options

mapLeft :: (a -> b) -> Either a c -> Either b c
mapLeft f (Left x) = Left (f x)
mapLeft _ (Right x) = Right x

--------------------------------------------------------------------------------
-- Primitives etc.
--------------------------------------------------------------------------------

positioned :: (SourcePosition -> a -> b) -> P a -> P b
positioned setPos inner = do
  pos <- convertSourcePos <$> getSourcePos
  setPos pos <$> inner

convertSourcePos :: SourcePos -> SourcePosition
convertSourcePos sp =
  SourcePosition
    { AST.sourceFile = Text.pack $ Megaparsec.sourceName sp
    , AST.sourceLine = unPos $ Megaparsec.sourceLine sp
    , AST.sourceColumn = unPos $ Megaparsec.sourceColumn sp
    }

identifierChar :: P Char
identifierChar = satisfy isIdentifierChar

isIdentifierChar :: Char -> Bool
isIdentifierChar c =
  isAlphaNum c || c == '_'

identifierInitialChar :: P Char
identifierInitialChar = satisfy isIdentifierInitialChar

isIdentifierInitialChar :: Char -> Bool
isIdentifierInitialChar c =
  isAlpha c || c == '_'

isOperatorChar :: Char -> Bool
isOperatorChar c =
  c `elem` ("&|%^*+-/~.=!,(){}[]" :: [Char])

operatorChar :: P Char
operatorChar = satisfy isOperatorChar

operator :: Text -> P ()
operator op = try $ do
  void $ chunk op
  notFollowedBy operatorChar
  space

keyword :: Text -> P ()
keyword kw = try $ do
  void $ chunk kw
  notFollowedBy identifierChar
  space

identifierRaw :: P Text
identifierRaw = do
  Text.cons <$> identifierInitialChar
            <*> takeWhileP (Just "identifier char") isIdentifierChar
            <* space

identifier :: P Identifier
identifier = Identifier <$> identifierRaw

stringLit :: P Text
stringLit =
  choice
    [ char q *> (Text.pack . catMaybes <$> many (stringLitChar q)) <* char q <* space
    | q <- ['"', '\'']
    ]

stringLitChar :: Char -> P (Maybe Char)
stringLitChar q =
  escapedStringLitChar <|> (Just <$> plainStringLitChar q)

escapedStringLitChar :: P (Maybe Char)
escapedStringLitChar = do
  void $ char '\\'
  c <- satisfy (const True)
  case c of
    '0' -> pure . Just $ '\0'
    'a' -> pure . Just $ '\a'
    'b' -> pure . Just $ '\b'
    'f' -> pure . Just $ '\f'
    'n' -> pure . Just $ '\n'
    'r' -> pure . Just $ '\r'
    't' -> pure . Just $ '\t'
    'v' -> pure . Just $ '\v'
    '"' -> pure . Just $ '"'
    '\'' -> pure . Just $ '\''
    '\\' -> pure . Just $ '\\'
    '&' -> pure Nothing
    'x' -> hexEscape 2
    'u' -> hexEscape 4
    'U' -> hexEscape 8
    t -> unexpected (Tokens $ t :| [])
  where
    hexEscape n = do
      ns <- replicateM n hexChar
      pure . Just . chr $ read ("0x" ++ ns)

hexChar :: P Char
hexChar = satisfy isHexChar

isHexChar :: Char -> Bool
isHexChar x =
  isDigit x ||
  (x >= 'a' && x <= 'z') || 
  (x >= 'A' && x <= 'Z')

plainStringLitChar :: Char -> P Char
plainStringLitChar q = satisfy (`notElem` ['\\', q])

intLit :: P Integer
intLit = do
  read <$> (intDigits <* space)

intDigits :: P String
intDigits = try $ do
  sign <- try $ (option "" $ "-" <$ char '-')
  str <- some digit
  pure (sign ++ str)

floatLit :: P Double
floatLit = do
  m <- do
    sign <- try $ (option "" $ "-" <$ char '-')
    intPart <- many digit
    void $ char '.'
    fracPart <- many digit
    when (null intPart && null fracPart)
      (unexpected $ Label $ 'd' :| "ot-only float")
    pure $ sign ++
           (if null intPart then "0" else intPart) ++
           "." ++
           (if null fracPart then "0" else fracPart)
  e <- option "" $ do
    void $ char 'E' <|> char 'e'
    ('e' :) <$> intDigits
  space
  pure . read $ m ++ e


digit :: P Char
digit = satisfy isDigit

equals :: P ()
equals = char '=' *> space

comma :: P ()
comma = char ',' *> space

colon :: P ()
colon = char ':' *> space

parenthesized :: P a -> P a
parenthesized = betweenT "(" ")"

bracketed :: P a -> P a
bracketed = betweenT "[" "]"

braced :: P a -> P a
braced = betweenT "{" "}"

betweenT :: Text -> Text -> P c -> P c
betweenT o c =
  between (chunk o *> space) (chunk c *> space)

--------------------------------------------------------------------------------
-- Argument lists
--------------------------------------------------------------------------------

callArgs :: P ([Expr], [(Identifier, Expr)])
callArgs = do
  args <- parenthesized $ argPair `sepBy` comma
  let posArgs = [ e | (Nothing, e) <- args ]
      kwArgs = [ (k, e) | (Just k, e) <- args ]
  pure (posArgs, kwArgs)

argPair :: P (Maybe Identifier, Expr)
argPair = try kwArgPair <|> ((Nothing ,) <$> expr)

kwArgPair :: P (Maybe Identifier, Expr)
kwArgPair =
  (,) <$> (Just <$> identifier) <*> (equals *> expr)

argsSig :: P [(Identifier, Maybe Expr)]
argsSig = parenthesized $ argSig `sepBy` comma

argSig :: P (Identifier, Maybe Expr)
argSig =
  (,) <$> identifier
      <*> optional (chunk "=" *> space *> expr)
  

--------------------------------------------------------------------------------
-- Expression parsers
--------------------------------------------------------------------------------

expr :: P Expr
expr = positioned PositionedE ternaryExpr <?> "expression"

ternaryExpr :: P Expr
ternaryExpr = do
  lhs <- booleanExpr
  option lhs $ exprTail lhs
  where
    exprTail lhs = try $ do
      keyword "if"
      cond <- booleanExpr
      keyword "else"
      rhs <- ternaryExpr
      pure $ TernaryE cond lhs rhs

binaryExpr :: P BinaryOperator -> P Expr -> P Expr
binaryExpr op sub = do
  lhs <- sub
  exprTail lhs
  where
    exprTail lhs = choice
      [ do
          o <- op
          rhs <- sub
          exprTail (BinaryE o lhs rhs)
      , pure lhs
      ]

booleanExpr :: P Expr
booleanExpr = binaryExpr booleanOp unaryNotExpr

booleanOp :: P BinaryOperator
booleanOp = choice
  [ BinopAnd <$ keyword "and"
  , BinopOr <$ keyword "or"
  ]

unaryNotExpr :: P Expr
unaryNotExpr =
  (keyword "not" *> (NotE <$> unaryNotExpr))
  <|>
  comparativeExpr

comparativeExpr :: P Expr
comparativeExpr = binaryExpr comparativeOp testExpr

comparativeOp :: P BinaryOperator
comparativeOp = choice
  [ BinopEqual <$ operator "=="
  , BinopNotEqual <$ operator "!="
  , BinopGT <$ operator ">"
  , BinopGTE <$ operator ">="
  , BinopLT <$ operator "<"
  , BinopLTE <$ operator "<="
  , BinopIn <$ keyword "in"
  ]

testExpr :: P Expr
testExpr = do
  lhs <- sub
  option lhs $ exprTail lhs
  where
    sub = concatExpr

    exprTail lhs = do
      keyword "is"
      wrapper <- option id $ NotE <$ keyword "not"
      test <- VarE <$> identifier
      (posArgs, kwArgs) <- option ([], []) (try callArgs <|> try soloArg)
      pure . wrapper $ IsE lhs test posArgs kwArgs

soloArg :: P ([Expr], [(Identifier, Expr)])
soloArg = do
  notFollowedBy $ choice
    [ keyword "and"
    , keyword "or"
    , keyword "in"
    , keyword "is"
    , keyword "not"
    ]
  arg <- expr
  pure ([arg], [])

concatExpr :: P Expr
concatExpr = binaryExpr concatOp additiveExpr

concatOp :: P BinaryOperator
concatOp = BinopConcat <$ operator "~"

additiveExpr :: P Expr
additiveExpr = binaryExpr additiveOp multiplicativeExpr

additiveOp :: P BinaryOperator
additiveOp = choice
  [ BinopPlus <$ operator "+"
  , BinopMinus <$ operator "-"
  ]

multiplicativeExpr :: P Expr
multiplicativeExpr = binaryExpr multiplicativeOp powerExpr

multiplicativeOp :: P BinaryOperator
multiplicativeOp = choice
  [ BinopIntDiv <$ operator "//"
  , BinopDiv <$ operator "/"
  , BinopMod <$ operator "%"
  , BinopMul <$ operator "*"
  ]

powerExpr :: P Expr
powerExpr = binaryExpr powerOp memberAccessExpr

powerOp :: P BinaryOperator
powerOp = BinopPower <$ operator "**"

memberAccessExpr :: P Expr
memberAccessExpr = do
  lhs <- simpleExpr
  exprTail lhs
  where
    exprTail lhs = dotTail lhs
              <|> bracketsTail lhs
              <|> callTail lhs
              <|> filterTail lhs
              <|> pure lhs

    dotTail lhs = do
      chunk "." *> space
      selector <- identifier
      exprTail (DotE lhs selector)

    sliceCont lhs op1May = do
      try $ chunk ":" *> space
      op2May <- optional expr
      pure $ SliceE lhs op1May op2May

    bracketsTail lhs = do
      t <- bracketed $ do
        op1May <- optional expr
        case op1May of
          Nothing ->
            sliceCont lhs op1May
          Just op1 -> do
            choice
              [ sliceCont lhs op1May
              , pure $ IndexE lhs op1
              ]
      exprTail t

    callTail lhs = do
      (posArgs, kwArgs) <- callArgs
      exprTail (CallE lhs posArgs kwArgs)

    filterTail lhs = do
      chunk "|" *> space
      callable <- simpleExpr
      (posArgs, kwArgs) <- option ([], []) $ callArgs
      exprTail (FilterE lhs callable posArgs kwArgs)

list :: P (Vector Expr)
list = V.fromList <$> bracketed (expr `sepBy` comma)

dict :: P [(Expr, Expr)]
dict = braced (dictPair `sepBy` comma)

dictPair :: P (Expr, Expr)
dictPair =
  (,) <$> expr <*> (colon *> expr)

simpleExpr :: P Expr
simpleExpr = choice
  [ parenthesized expr
  , StringLitE <$> stringLit
  , ListE <$> list
  , DictE <$> dict
  , FloatLitE <$> try floatLit
  , IntLitE <$> try intLit
  , try (operator "-") *> space *> (NegateE <$> (parenthesized expr <|> (VarE <$> identifier)))
  , BoolE True <$ (keyword "true" <|> keyword "True")
  , BoolE False <$ (keyword "false" <|> keyword "False")
  , NoneE <$ (keyword "none" <|> keyword "None")
  , VarE <$> identifier
  ]

-------------------------------------------------------------------------------- 
-- Statement-level tokens
--------------------------------------------------------------------------------

overrideToken :: P PolicyOverride
overrideToken =
  choice
    [ Always <$ chunk "-"
    , Never <$ chunk "+"
    , pure Default
    ]

openWithOverride :: Text -> P ()
openWithOverride base = do
  policy <- ask
  let defLeader = case pstateStripBlocks policy of
        StripBlocks -> inlineSpace
        NoStripBlocks -> pure ()
  void $ choice
        [ try $ inlineSpace *> chunk base *> chunk "-" <* notFollowedBy operatorChar
        , try $ chunk base *> chunk "+" <* notFollowedBy operatorChar
        , try $ chunk base <* notFollowedBy operatorChar
        , try $ defLeader *> chunk base <* notFollowedBy operatorChar
        ]
  space

inlineSpace :: P ()
inlineSpace =
  void $
    takeWhileP
      (Just "non-newline whitespace")
      (\c -> isSpace c && c `notElem` ['\r', '\n'])

anyNewline :: P Text
anyNewline =
  choice
    [ chunk "\r\n"
    , chunk "\n"
    , chunk "\r"
    ]

closeWithOverride :: Text -> P ()
closeWithOverride base = do
  ovr <- try $ overrideToken <* chunk base
  policy <- asks (override ovr)
  when (pstateTrimBlocks policy == TrimBlocks) $
    void . optional . try $ inlineSpace *> (void anyNewline <|> eof)

openComment :: P ()
openComment = openWithOverride "{#"

closeComment :: P ()
closeComment = closeWithOverride "#}"

openInterpolation :: P ()
openInterpolation = openWithOverride "{{"

closeInterpolation :: P ()
closeInterpolation = closeWithOverride "}}"

openFlow :: P ()
openFlow = openWithOverride "{%"

closeFlow :: P ()
closeFlow = closeWithOverride "%}"


-------------------------------------------------------------------------------- 
-- Statement parsers
--------------------------------------------------------------------------------

template :: P Template
template = do
  Template <$> extends <*> statement

extends :: P (Maybe Text)
extends =
  optional . flow "extends" $ do
    stringLit <* space

simplifyS :: Statement -> Statement
simplifyS = traverseS simplifyOne id
  where
    simplifyOne (GroupS xs) = wrap xs
    simplifyOne s = s

statement :: P Statement
statement =
  wrap <$> many singleStatement

joinImmediates :: [Statement] -> [Statement]
joinImmediates (ImmediateS a : ImmediateS b : xs) =
  joinImmediates (ImmediateS (a <> b) : xs)
joinImmediates (PositionedS pos (ImmediateS a) : PositionedS _ (ImmediateS b) : xs) =
  joinImmediates (PositionedS pos (ImmediateS (a <> b)) : xs)
joinImmediates (PositionedS pos (ImmediateS a) : ImmediateS b : xs) =
  joinImmediates (PositionedS pos (ImmediateS (a <> b)) : xs)
joinImmediates (x:xs) = x : joinImmediates xs
joinImmediates [] = []

wrap :: [Statement] -> Statement
wrap [] = ImmediateS mempty
wrap [x] = x
wrap xs = GroupS (joinImmediates xs)

singleStatement :: P Statement
singleStatement =
  positioned PositionedS $
    choice
      [ commentStatement
      , interpolationStatement
      , controlStatement
      , immediateStatement
      ]

immediateStatement :: P Statement
immediateStatement =
  ImmediateS . Encoded <$> 
    choice
      [ anyNewline
      , fmap mconcat . some $
          notFollowedBy (openComment <|> openInterpolation <|> openFlow) >> chunk "{"
          <|>
          takeWhile1P Nothing (`notElem` ['{', '\n', '\r'])
      ]

commentStatement :: P Statement
commentStatement =
  between openComment closeComment $
    CommentS . Text.strip . Text.pack <$> many (notFollowedBy closeComment *> anySingle)

interpolationStatement :: P Statement
interpolationStatement =
  between openInterpolation closeInterpolation $
    InterpolationS <$> expr

controlStatement :: P Statement
controlStatement =
  choice
    [ ifStatement
    , forStatement
    , macroStatement
    , callStatement
    , filterStatement
    , setStatement
    , setBlockStatement
    , includeStatement
    , importStatement
    , blockStatement
    , withStatement
    ]

flow :: Text -> P a -> P a
flow kw inner = do
  try (openFlow >> keyword kw)
  inner <* closeFlow

flow_ :: Text -> P ()
flow_ kw = flow kw nop

withFlow :: Text -> P a -> P b -> (a -> b -> c) -> P c
withFlow kw header body combine =
  combine <$> flow kw header <*> body <* flow_ ("end" <> kw)

nop :: P ()
nop = pure ()

ifStatement :: P Statement
ifStatement = do
  withFlow "if" expr body makeIf
  where
    body = do
      yes <- statement
      noMay <- optional $ flow_ "else" *> statement
      pure (yes, noMay)
    makeIf cond (yes, noMay) = IfS cond yes noMay

forStatement :: P Statement
forStatement = do
  withFlow "for" forHeader forBody makeFor
  where
    forHeader = do
      (keyMay, val) <- do
        a' <- identifier
        bMay' <- optional $ comma *> identifier
        pure $ case (a', bMay') of
          (a, Just b) -> (Just a, b)
          (a, Nothing) -> (Nothing, a)
      keyword "in"
      iteree <- expr
      filterMay <- optional $ keyword "if" *> expr
      recursivity <- option NotRecursive $ Recursive <$ keyword "recursive"
      pure (keyMay, val, iteree, filterMay, recursivity)
    forBody = do
      body <- statement
      elseMay <- optional $ flow_ "else" *> statement
      pure (body, elseMay)
    makeFor (keyMay, val, iteree, filterMay, recursivity) (body, elseMay) =
      ForS keyMay val iteree filterMay recursivity body elseMay

macroStatement :: P Statement
macroStatement = do
  withFlow "macro" macroHeader macroBody makeMacro
  where
    macroHeader :: P (Identifier, [MacroArg])
    macroHeader =
      (,) <$> identifier <*> option [] argsSig
    macroBody =
      statement
    makeMacro :: (Identifier, [MacroArg]) -> Statement -> Statement
    makeMacro (name, args) body = MacroS name args body

callStatement :: P Statement
callStatement = do
  withFlow "call" callHeader callBody makeCall
  where
    callHeader = do
      (,) <$> identifier <*> callArgs
    callBody = statement
    makeCall (callee, (args, kwargs)) body = CallS callee args kwargs body

filterStatement :: P Statement
filterStatement = do
  withFlow "filter" filterHeader filterBody makeFilter
  where
    filterHeader = do
      (,) <$> identifier <*> callArgs
    filterBody = statement
    makeFilter (filteree, (args, kwargs)) body = FilterS filteree args kwargs body

setPair :: P (Identifier, Expr)
setPair = (,) <$> identifier <* chunk "=" <* space <*> expr

setTargetPair :: P (SetTarget, Expr)
setTargetPair = (,) <$> setTarget <* chunk "=" <* space <*> expr

setTarget :: P SetTarget
setTarget = do
  leader <- identifier
  selectorMay <- optional $ chunk "." *> identifier
  pure $ case selectorMay of
    Nothing -> SetVar leader
    Just selector -> SetMutable leader selector

setStatement :: P Statement
setStatement = try $ do
  flow "set" $ uncurry SetS <$> setTargetPair

setBlockStatement :: P Statement
setBlockStatement = do
  withFlow "set" setBlockHeader setBlockBody makeSetBlock
  where
    setBlockHeader = (,) <$> setTarget <*> optional (chunk "|" *> space *> expr)
    setBlockBody = statement
    makeSetBlock (name, filterMay) body =
      SetBlockS name body filterMay

includeStatement :: P Statement
includeStatement =
  flow "include" $
    IncludeS
      <$> expr
      <*> option RequireMissing (IgnoreMissing <$ keyword "ignore" <* keyword "missing")
      <*> option WithContext (choice
            [ WithContext <$ keyword "with" <* keyword "context"
            , WithoutContext <$ keyword "without" <* keyword "context"
            ]
          )

importStatement :: P Statement
importStatement =
  wildcardImportStatement <|> explicitImportStatement

wildcardImportStatement :: P Statement
wildcardImportStatement =
  flow "import" $
    ImportS
      <$> expr
      <*> optional (keyword "as" *> identifier)
      <*> pure Nothing
      <*> option RequireMissing (IgnoreMissing <$ keyword "ignore" <* keyword "missing")
      <*> option WithoutContext (choice
            [ WithContext <$ keyword "with" <* keyword "context"
            , WithoutContext <$ keyword "without" <* keyword "context"
            ]
          )

explicitImportStatement :: P Statement
explicitImportStatement =
  flow "from" $
    ImportS
      <$> expr
      <*> optional (keyword "as" *> identifier)
      <*  keyword "import"
      <*> (Just <$> do
            notFollowedBy (choice $ map keyword ["ignore", "with", "without"])
            importPair `sepBy` comma
          )
      <*> option RequireMissing (IgnoreMissing <$ keyword "ignore" <* keyword "missing")
      <*> option WithoutContext (choice
            [ WithContext <$ keyword "with" <* keyword "context"
            , WithoutContext <$ keyword "without" <* keyword "context"
            ]
          )

importPair :: P (Identifier, Maybe Identifier)
importPair = (,) <$> identifier <*> optional (keyword "as" *> identifier)

withStatement :: P Statement
withStatement = do
  withFlow "with" withHeader withBody makeWith
  where
    withHeader = setPair `sepBy` comma
    withBody = statement
    makeWith = WithS

blockStatement :: P Statement
blockStatement = do
  (name, scopedness, requiredness) <- flow "block" $
    (,,) <$> identifier
         <*> (option NotScoped $ Scoped <$ keyword "scoped")
         <*> (option Optional $ Required <$ keyword "required")
  body <- statement
  void $ flow "endblock" (optional $ keyword (identifierName name))
  pure $ BlockS name (Block body scopedness requiredness)