packages feed

language-puppet-1.5.1: src/Puppet/Parser/Internal.hs

{-# LANGUAGE TupleSections #-}
{-# OPTIONS_GHC -Wno-unrecognised-pragmas #-}

{-# HLINT ignore "Use id" #-}
{-# HLINT ignore "Use <$>" #-}
module Puppet.Parser.Internal where

import Control.Monad.Combinators.Expr
import qualified Data.Char as Char
import qualified Data.List as List
import qualified Data.List.NonEmpty as NE
import qualified Data.Maybe.Strict as S
import qualified Data.Scientific as Scientific
import qualified Data.Text as Text
import qualified Data.Vector as V
import Puppet.Language
import Puppet.Parser.Types
import Text.Megaparsec
import Text.Megaparsec.Char
import qualified Text.Megaparsec.Char.Lexer as Lexer
import qualified Text.Regex.PCRE.ByteString.Utils as Regex
import XPrelude.Extra hiding (key, many, some, try)

-- | Space consumer
sc :: Parser ()
sc = Lexer.space space1 (Lexer.skipLineComment "#") (Lexer.skipBlockComment "/*" "*/")

-- | Lexeme consumes spaces after the input parser
lexeme :: Parser a -> Parser a
lexeme = Lexer.lexeme sc

-- | Consumes a text then consumes spaces
symbol :: Text -> Parser ()
symbol = void . Lexer.symbol sc

-- | Consumes a character then consumes spaces
symbolic :: Char -> Parser ()
symbolic = lexeme . void . single

braces :: Parser a -> Parser a
braces = between (symbolic '{') (symbolic '}')

parens :: Parser a -> Parser a
parens = between (symbolic '(') (symbolic ')')

brackets :: Parser a -> Parser a
brackets = between (symbolic '[') (symbolic ']')

comma :: Parser ()
comma = symbolic ','

sepComma :: Parser a -> Parser [a]
sepComma p = p `sepEndBy` comma

sepComma1 :: Parser a -> Parser [a]
sepComma1 p = p `sepEndBy1` comma

-- | Parses an 'Expression'.
expression :: Parser Expression
expression = do
  expr <- makeExprParser (lexeme terminal) expressionTable
  ms <- optional $ do
    symbolic '?'
    let case_expr = do
          c <-
            SelectorDefault <$ symbol "default" -- default case
              <|> SelectorType <$> try datatype
              <|> fmap
                SelectorValue
                ( UVariableReference <$> variableReference
                    <|> UBoolean <$> puppetBool
                    <|> UUndef <$ symbol "undef"
                    <|> literalValue
                    <|> UInterpolable <$> interpolableString
                    <|> URegexp <$> termRegexp
                )
          symbol "=>"
          e <- expression
          pure (c :!: e)
    cases <- braces (sepComma1 case_expr)
    pure (ConditionalValue expr (V.fromList cases))
  case ms of
    Nothing -> pure expr
    Just cv -> pure cv

stringLiteral' :: Parser Text
stringLiteral' = between (char '\'') (symbolic '\'') interior
  where
    interior = Text.pack . concat <$> many (some (noneOf ['\'', '\\']) <|> (char '\\' *> fmap escape anySingle))
    escape '\'' = "'"
    escape x = ['\\', x]

identifier :: Parser (Tokens Text)
identifier = takeWhile1P Nothing isIdentifierChar

-- | Only Ascii, hyphens (-) are not allowed.
isIdentifierChar :: Char -> Bool
isIdentifierChar x = Char.isAsciiLower x || Char.isAsciiUpper x || Char.isDigit x || (x == '_')

-- | Like 'indentifier' but hyphens (-) are allowed.
bareword :: Parser Text
bareword = Text.cons <$> satisfy Char.isAsciiLower <*> takeWhileP Nothing isBarewordChar
  where
    isBarewordChar :: Char -> Bool
    isBarewordChar x = isIdentifierChar x || (x == '-')

reserved :: Text -> Parser ()
reserved s =
  try $ do
    void (chunk s)
    notFollowedBy (satisfy isIdentifierChar)
    sc

qualif :: Parser Text -> Parser Text
qualif p = do
  header <- option "" (chunk "::")
  (header <>) . Text.intercalate "::" <$> p `sepBy1` chunk "::"

qualif1 :: Parser Text -> Parser Text
qualif1 p = do
  r <- qualif p
  unless ("::" `Text.isInfixOf` r) (fail "This parser is not qualified")
  pure r

-- | Consumes a var $foo and then spaces
variableReference :: Parser Text
variableReference = do
  v <- char '$' *> lexeme variableName
  when (Text.all Char.isDigit v) (fail "Can't assign fully numeric variables")
  pure v

variableName :: Parser Text
variableName = qualif identifier

-- yay with reserved words
typeName :: Parser Text
typeName = className

className :: Parser Text
className = lexeme $ qualif $ genericModuleName False

funcName :: Parser Text
funcName = lexeme $ qualif $ genericModuleName False

moduleName :: Parser Text
moduleName = lexeme $ genericModuleName False

parameterName :: Parser Text
parameterName = moduleName

resourceNameRef :: Parser Text
resourceNameRef = lexeme $ qualif (genericModuleName True)

genericModuleName :: Bool -> Parser Text
genericModuleName isReference = do
  let acceptable x = Char.isAsciiLower x || Char.isDigit x || (x == '_')
      firstletter =
        if isReference
          then fmap Char.toLower (satisfy Char.isAsciiUpper)
          else satisfy Char.isAsciiLower
  Text.cons <$> firstletter <*> takeWhileP Nothing acceptable

-- | Variable expression
varExpression :: Parser Expression
varExpression = Terminal . UVariableReference <$> variableReference

-- | String interpolation
interpolableString :: Parser (Vector Expression)
interpolableString =
  V.fromList
    <$> between
      (char '"')
      (symbolic '"')
      (many (interpolableVariableReference <|> doubleQuotedStringContent <|> fmap (Terminal . UString . Text.singleton) (char '$')))
  where
    doubleQuotedStringContent =
      Terminal . UString . Text.pack . concat
        <$> some ((char '\\' *> fmap escaper anySingle) <|> some (noneOf ['"', '\\', '$']))
    escaper :: Char -> String
    escaper 'n' = "\n"
    escaper 't' = "\t"
    escaper 'r' = "\r"
    escaper '"' = "\""
    escaper '\\' = "\\"
    escaper '$' = "$"
    escaper x = ['\\', x]
    -- this is specialized because we can't be "tokenized" here
    varname = Text.concat <$> some (chunk "::" <|> identifier)
    varexpr = Terminal . UVariableReference <$> varname
    indexchain = makeExprParser varexpr [[Postfix indexLookupChain]] -- e.g: os['release']['major']
    interpolableVariableReference = do
      void (char '$')
      let fenced =
            try (indexchain <* char '}')
              <|> try (varexpr <* char '}')
              <|> (expression <* char '}')
      (symbolic '{' *> fenced) <|> try varexpr <|> pure (Terminal (UString (Text.singleton '$')))

integerOrDouble :: Parser (Either Integer Double)
integerOrDouble = Left <$> hex <|> (either Right Left . Scientific.floatingOrInteger <$> Lexer.scientific)
  where
    hex = chunk "0x" *> Lexer.hexadecimal

puppetArray :: Parser UnresolvedValue
puppetArray = fmap (UArray . V.fromList) (brackets (sepComma expression)) <?> "Array"

puppetHash :: Parser UnresolvedValue
puppetHash = fmap (UHash . V.fromList) (braces (sepComma hashPart)) <?> "Hash"
  where
    hashPart = (:!:) <$> (expression <* symbol "=>") <*> expression

puppetBool :: Parser Bool
puppetBool =
  (reserved "true" >> pure True)
    <|> (reserved "false" >> pure False)
    <?> "Boolean"

resourceReferenceRaw :: Parser (Text, [Expression])
resourceReferenceRaw = do
  let restype_parser = qualif (genericModuleName True)
      resnames_parser = brackets (expression `sepBy1` comma)
  (,) <$> restype_parser <*> resnames_parser <?> "Resource reference"

resourceReference :: Parser UnresolvedValue
resourceReference = do
  (restype, resnames) <- resourceReferenceRaw
  pure $ UResourceReference restype $ case resnames of
    [x] -> x
    _ -> Terminal $ UArray (V.fromList resnames)

-- | Functions that have named that are not valid ...
specialFunctions :: Parser Text
specialFunctions =
  chunk "Integer"
    <|> chunk "Numeric"
    <|> chunk "Sensitive"

-- The first argument defines if non-parenthesized arguments are acceptable
genFunctionCall :: Bool -> Parser (Text, Vector Expression)
genFunctionCall nonparens = do
  fname <- (specialFunctions <|> funcName) <?> "Function name"
  let -- first check if the function arg is not a qualified name (ex.: include foo::bar)
      -- if it is not, then we expect an expression
      qualif_param = Terminal . UString <$> qualif1 moduleName <* notFollowedBy (single '(') -- <* lookAhead (anySingleBut '(')
      func_arg expr = try qualif_param <|> expr <?> "Function argument"
      terminalF = terminalG FunctionWithoutParens
      expressionF = makeExprParser (lexeme terminalF) expressionTable <?> "Function expression"
      withparens = parens (func_arg expression `sepEndBy` comma)
      withoutparens =
        if nonparens
          then func_arg expressionF `sepEndBy1` comma
          else fail "Not an argument list allowed with function without parentheses"
  args <- withparens <|> withoutparens
  pure (fname, V.fromList args)

literalValue :: Parser UnresolvedValue
literalValue = lexeme (fmap UString stringLiteral' <|> fmap UString bareword <|> fmap UNumber numericalvalue <?> "Literal Value")
  where
    signed :: (Num n) => Parser (n -> n)
    signed = (negate <$ char '-') <|> pure (\x -> x)
    numericalvalue =
      ((,) <$> signed <*> integerOrDouble) >>= \case
        (s, Left x) -> pure (s (fromIntegral x))
        (s, Right y) -> pure (s (Scientific.fromFloatDigits y))

data TerminalMode
  = FunctionWithoutParens
  | StandardMode

-- this is a hack for functions :(
terminalG :: TerminalMode -> Parser Expression
terminalG mode =
  parens expression
    <|> fmap (Terminal . UInterpolable) interpolableString
    <|> (Terminal UUndef <$ reserved "undef")
    <|> fmap (Terminal . URegexp) termRegexp
    <|> varExpression
    <|> fmap Terminal puppetArray
    <|> fmap Terminal puppetHash
    <|> fmap (Terminal . UBoolean) puppetBool
    <|> case mode of
      FunctionWithoutParens -> remaining
      StandardMode -> lambda <|> remaining
  where
    lambda = fmap Terminal (fmap UHOLambdaCall (try lambdaCall) <|> try funcCall)
    remaining =
      fmap (Terminal . UDataType) datatype
        <|> fmap Terminal resourceReference
        <|> fmap Terminal literalValue
    funcCall :: Parser UnresolvedValue
    funcCall = uncurry UFunctionCall <$> genFunctionCall False

regexp :: Parser Text
regexp = do
  void (single '/')
  Text.pack . concat
    <$> many (do { void (char '\\'); x <- anySingle; return ['\\', x] } <|> some (noneOf ['/', '\\']))
    <* symbolic '/'

compileRegexp :: Text -> Parser CompRegex
compileRegexp p = case Regex.compile' Regex.compBlank Regex.execBlank (encodeUtf8 p) of
  Right r -> pure $ CompRegex p r
  Left ms -> fail ("Can't parse regexp /" <> Text.unpack p <> "/ : " ++ show ms)

termRegexp :: Parser CompRegex
termRegexp = regexp >>= compileRegexp

terminal :: Parser Expression
terminal = terminalG StandardMode

expressionTable :: [[Operator Parser Expression]]
expressionTable =
  [ [Postfix indexLookupChain], -- http://stackoverflow.com/questions/10475337/parsec-expr-repeated-prefix-postfix-operator-not-supported
    [Prefix (symbolic '-' $> Negate)],
    [Prefix (symbolic '!' $> Not)],
    [InfixL (symbolic '.' $> FunctionApplication)],
    [InfixL (reserved "in" $> Contains)],
    [ InfixL (symbolic '/' $> Division),
      InfixL (symbolic '*' $> Multiplication)
    ],
    [ InfixL (symbolic '+' $> Addition),
      InfixL (symbolic '-' $> Substraction)
    ],
    [ InfixL (symbol "<<" $> LeftShift),
      InfixL (symbol ">>" $> RightShift)
    ],
    [ InfixL (symbol "==" $> Equal),
      InfixL (symbol "!=" $> Different)
    ],
    [ InfixL (symbol "=~" $> RegexMatch),
      InfixL (symbol "!~" $> NotRegexMatch)
    ],
    [ InfixL (symbol ">=" $> MoreEqualThan),
      InfixL (symbol "<=" $> LessEqualThan),
      InfixL (symbol ">" $> MoreThan),
      InfixL (symbol "<" $> LessThan)
    ],
    [ InfixL (reserved "and" $> And),
      InfixL (reserved "or" $> Or)
    ]
  ]

-- | Postfix of a chain of lookup indexes such as "['release']['major']"
indexLookupChain :: Parser (Expression -> Expression)
indexLookupChain = List.foldr1 (flip (.)) <$> some checkLookup
  where
    checkLookup = flip Lookup <$> brackets expression

stringExpression :: Parser Expression
stringExpression =
  Terminal . UInterpolable <$> interpolableString
    <|> (reserved "undef" $> Terminal UUndef)
    <|> Terminal . UBoolean <$> puppetBool
    <|> varExpression
    <|> Terminal <$> literalValue

-- | a = b = 0
chainedVariableReferences :: Parser [Text]
chainedVariableReferences = do
  h <- variableReference
  t <- many (try next)
  pure (h : t)
  where
    next = symbolic '=' *> variableReference <* lookAhead (single '=' *> space1)

varAssign :: Parser VarAssignDecl
varAssign = do
  p <- getSourcePos
  mt <- optional datatype
  vs <- chainedVariableReferences
  void $ symbolic '='
  expr <- expression
  pe <- getSourcePos
  pure (VarAssignDecl mt vs expr (p :!: pe))

nodeDecl :: Parser [NodeDecl]
nodeDecl = do
  p <- getSourcePos
  reserved "node"
  let toString (UString s) = s
      toString (UNumber n) = scientific2text n
      toString _ = panic "Can't happen at nodeDecl"
      nodename = (reserved "default" >> pure NodeDefault) <|> fmap (NodeName . toString) literalValue
  ns <- (fmap NodeMatch termRegexp <|> nodename) `sepBy1` comma
  inheritance <- option S.Nothing (fmap S.Just (reserved "inherits" *> nodename))
  st <- braces statementList
  pe <- getSourcePos
  pure [NodeDecl n st inheritance (p :!: pe) | n <- ns]

defineDecl :: Parser DefineDecl
defineDecl = do
  p <- getSourcePos
  reserved "define"
  name <- typeName
  -- TODO check native type
  params <- option V.empty puppetClassParameters
  st <- braces statementList
  pe <- getSourcePos
  pure (DefineDecl name params st (p :!: pe))

puppetClassParameters :: Parser Parameters
puppetClassParameters = V.fromList <$> parens (sepComma var)
  where
    toStrictMaybe (Just x) = S.Just x
    toStrictMaybe Nothing = S.Nothing
    var :: Parser (Pair (Pair Text (S.Maybe UDataType)) (S.Maybe Expression))
    var = do
      tp <- toStrictMaybe <$> optional datatype
      n <- variableReference
      df <- toStrictMaybe <$> optional (symbolic '=' *> expression)
      pure ((n :!: tp) :!: df)

puppetIfStyleCondition :: Parser (Pair Expression (Vector Statement))
puppetIfStyleCondition = (:!:) <$> expression <*> braces statementList

unlessCondition :: Parser ConditionalDecl
unlessCondition = do
  p <- getSourcePos
  reserved "unless"
  (cond :!: stmts) <- puppetIfStyleCondition
  elsecond <- option V.empty (reserved "else" *> braces statementList)
  let ec = [Terminal (UBoolean True) :!: elsecond | not (V.null elsecond)]
  pe <- getSourcePos
  pure (ConditionalDecl (V.fromList ((Not cond :!: stmts) : ec)) (p :!: pe))

ifCondition :: Parser ConditionalDecl
ifCondition = do
  p <- getSourcePos
  reserved "if"
  maincond <- puppetIfStyleCondition
  others <- many (reserved "elsif" *> puppetIfStyleCondition)
  elsecond <- option V.empty (reserved "else" *> braces statementList)
  let ec = [Terminal (UBoolean True) :!: elsecond | not (V.null elsecond)]
  pe <- getSourcePos
  pure (ConditionalDecl (V.fromList (maincond : others ++ ec)) (p :!: pe))

caseCondition :: Parser ConditionalDecl
caseCondition = do
  let puppetRegexpCase = Terminal . URegexp <$> termRegexp
      defaultCase = Terminal (UBoolean True) <$ reserved "default"
      matchesToExpression e (x, stmts) = f x :!: stmts
        where
          f = case x of
            (Terminal (UBoolean _)) -> identity
            (Terminal (URegexp _)) -> RegexMatch e
            _ -> Equal e
      cases = do
        matches <- (puppetRegexpCase <|> defaultCase <|> expression) `sepBy1` comma
        void $ symbolic ':'
        stmts <- braces statementList
        pure $ map (,stmts) matches
  p <- getSourcePos
  reserved "case"
  expr1 <- expression
  condlist <- concat <$> braces (some cases)
  pe <- getSourcePos
  pure (ConditionalDecl (V.fromList (map (matchesToExpression expr1) condlist)) (p :!: pe))

data OperatorChain a
  = OperatorChain a LinkType (OperatorChain a)
  | EndOfChain a

instance Foldable OperatorChain where
  foldMap f (EndOfChain x) = f x
  foldMap f (OperatorChain a _ nx) = f a <> foldMap f nx

operatorChainStatement :: OperatorChain a -> a
operatorChainStatement (OperatorChain a _ _) = a
operatorChainStatement (EndOfChain x) = x

zipChain :: OperatorChain a -> [(a, a, LinkType)]
zipChain (OperatorChain a d nx) = (a, operatorChainStatement nx, d) : zipChain nx
zipChain (EndOfChain _) = []

depOperator :: Parser LinkType
depOperator =
  (RBefore <$ symbol "->")
    <|> (RNotify <$ symbol "~>")

assignment :: Parser AttributeDecl
assignment =
  (AttributeDecl <$> lexeme key <*> arrowOp <*> expression)
    <|> (AttributeWildcard <$> (symbolic '*' *> symbol "=>" *> expression))
  where
    key = bareword <?> "Assignment key"
    arrowOp =
      (AssignArrow <$ symbol "=>")
        <|> (AppendArrow <$ symbol "+>")

-- | Resource Collector
resCollDecl :: Position -> Text -> Parser ResCollDecl
resCollDecl p restype = do
  openchev <- some (char '<')
  when (length openchev > 2) (fail "Too many brackets")
  void $ symbolic '|'
  e <- option AlwaysTrue searchExpression
  void (char '|')
  void (count (length openchev) (char '>'))
  sc
  overrides <- option [] $ braces (sepComma assignment)
  let collectortype =
        if length openchev == 1
          then Collector
          else ExportedCollector
  pe <- getSourcePos
  pure (ResCollDecl collectortype restype e (V.fromList overrides) (p :!: pe))
  where
    searchExpression :: Parser SearchExpression
    searchExpression =
      let searchTable :: [[Operator Parser SearchExpression]]
          searchTable =
            [ [ InfixL (reserved "and" $> AndSearch),
                InfixL (reserved "or" $> OrSearch)
              ]
            ]
          searchterm = parens searchExpression <|> check
          check = do
            attrib <- parameterName
            opr <-
              (EqualitySearch <$ symbol "==")
                <|> (NonEqualitySearch <$ symbol "!=")
            term <- stringExpression
            pure (opr attrib term)
       in makeExprParser (lexeme searchterm) searchTable

classDecl :: Parser ClassDecl
classDecl = do
  p <- getSourcePos
  reserved "class"
  ClassDecl
    <$> className
    <*> option V.empty puppetClassParameters
    <*> option S.Nothing (fmap S.Just (reserved "inherits" *> className))
    <*> braces statementList
    <*> ((p :!:) <$> getSourcePos)

mainFuncDecl :: Parser MainFuncDecl
mainFuncDecl = do
  p <- getSourcePos
  (fname, args) <- genFunctionCall True
  pe <- getSourcePos
  pure (MainFuncDecl fname args (p :!: pe))

hoLambdaDecl :: Parser HigherOrderLambdaDecl
hoLambdaDecl = do
  p <- getSourcePos
  fc <- lambdaCall
  pe <- getSourcePos
  pure (HigherOrderLambdaDecl fc (p :!: pe))

dotLambdaDecl :: Parser HigherOrderLambdaDecl
dotLambdaDecl = do
  p <- getSourcePos
  ex <- expression
  pe <- getSourcePos
  hf <- case ex of
    FunctionApplication e (Terminal (UHOLambdaCall hf)) -> do
      unless (null (hf ^. hoLambdaExpr)) (fail "Can't call a function with . and ()")
      pure (hf & hoLambdaExpr .~ V.singleton e)
    Terminal (UHOLambdaCall hf) -> do
      when (null (hf ^. hoLambdaExpr)) (fail "This function needs data to operate on")
      pure hf
    _ -> fail "A method chained by dots."
  pure (HigherOrderLambdaDecl hf (p :!: pe))

resDefaultDecl :: Parser ResDefaultDecl
resDefaultDecl = do
  p <- getSourcePos
  rnd <- resourceNameRef
  let assignmentList = V.fromList <$> sepComma1 assignment
  asl <- braces assignmentList
  pe <- getSourcePos
  pure (ResDefaultDecl rnd asl (p :!: pe))

resOverrideDecl :: Parser [ResOverrideDecl]
resOverrideDecl = do
  p <- getSourcePos
  restype <- resourceNameRef
  names <- brackets (expression `sepBy1` comma) <?> "Resource reference values"
  assignments <- V.fromList <$> braces (sepComma assignment)
  pe <- getSourcePos
  pure [ResOverrideDecl restype n assignments (p :!: pe) | n <- names]

arrayof :: Parser p -> Parser [p]
arrayof p = symbolic '[' *> sepBy p comma <* symbolic ']'

-- | Heterogeneous chain (interleaving resource declarations with
-- resource references) needs to be supported:
--
--    class { 'docker::service': } ->
--    Class['docker']
chainableResources :: Parser [Statement]
chainableResources = do
  let withresname = do
        p <- getSourcePos
        restype <- resourceNameRef
        lookAhead anySingle >>= \case
          '[' -> do
            resnames <- brackets (expression `sepBy1` comma)
            pe <- getSourcePos
            pure (ChainResRefr restype resnames (p :!: pe))
          _ -> ChainResColl <$> resCollDecl p restype
  let oneresource = pure <$> try withresname <|> map ChainResDecl <$> resDeclGroup
  chain <- parseRelationships (oneresource <|> concat <$> arrayof oneresource)
  let relations = do
        (g1, g2, lt) <- zipChain chain
        (rt1, rn1, _ :!: pe1) <- concatMap extractResRef g1
        (rt2, rn2, ps2 :!: _) <- concatMap extractResRef g2
        pure (DepDecl (rt1 :!: rn1) (rt2 :!: rn2) lt (pe1 :!: ps2))
  pure $ map DependencyDeclaration relations <> (chain ^.. folded . folded . to extractChainStatement . folded)
  where
    extractResRef :: ChainableRes -> [(Text, Expression, PPosition)]
    extractResRef (ChainResColl _) = []
    extractResRef (ChainResDecl (ResDecl rt rn _ _ pp)) = [(rt, rn, pp)]
    extractResRef (ChainResRefr rt rns pp) = [(rt, rn, pp) | rn <- rns]

    extractChainStatement :: ChainableRes -> [Statement]
    extractChainStatement (ChainResColl r) = [ResourceCollectionDeclaration r]
    extractChainStatement (ChainResDecl d) = [ResourceDeclaration d]
    extractChainStatement ChainResRefr {} = []

    parseRelationships :: Parser a -> Parser (OperatorChain a)
    parseRelationships p = do
      g <- p
      o <- optional depOperator
      case o of
        Just o' -> OperatorChain g o' <$> parseRelationships p
        Nothing -> pure (EndOfChain g)

    resDeclGroup :: Parser [ResDecl]
    resDeclGroup = do
      let resourceName = expression
          resourceDeclaration = do
            p <- getSourcePos
            names <- brackets (sepComma1 resourceName) <|> fmap pure resourceName
            void $ symbolic ':'
            vals <- fmap V.fromList (sepComma assignment)
            pe <- getSourcePos
            pure [(n, vals, p :!: pe) | n <- names]
          groupDeclaration = (,) <$> many (char '@') <*> typeName <* symbolic '{'
      (virts, rtype) <- try groupDeclaration -- for matching reasons, this gets a try until the opening brace
      let sep = symbolic ';' <|> comma
      x <- resourceDeclaration `sepEndBy1` sep
      void $ symbolic '}'
      virtuality <- case virts of
        "" -> pure Normal
        "@" -> pure Virtual
        "@@" -> pure Exported
        _ -> fail "Invalid virtuality"
      return [ResDecl rtype rname conts virtuality pos | (rname, conts, pos) <- concat x]

statement :: Parser [Statement]
statement =
  (pure . HigherOrderLambdaDeclaration <$> try dotLambdaDecl)
    <|> (pure . VarAssignmentDeclaration <$> varAssign)
    <|> (map NodeDeclaration <$> nodeDecl)
    <|> (pure . DefineDeclaration <$> defineDecl)
    <|> (pure . ConditionalDeclaration <$> unlessCondition)
    <|> (pure . ConditionalDeclaration <$> ifCondition)
    <|> (pure . ConditionalDeclaration <$> caseCondition)
    <|> (pure . ResourceDefaultDeclaration <$> try resDefaultDecl)
    <|> (map ResourceOverrideDeclaration <$> try resOverrideDecl)
    <|> chainableResources
    <|> (pure . ClassDeclaration <$> classDecl)
    <|> (pure . HigherOrderLambdaDeclaration <$> try hoLambdaDecl)
    <|> (pure . MainFunctionDeclaration <$> mainFuncDecl)
    <?> "Statement"

datatype :: Parser UDataType
datatype =
  dtString
    <|> dtInteger
    <|> dtFloat
    <|> dtNumeric
    <|> (UDTBoolean <$ reserved "Boolean")
    <|> (UDTScalar <$ reserved "Scalar")
    <|> (UDTData <$ reserved "Data")
    <|> (UDTAny <$ reserved "Any")
    <|> (UDTCollection <$ reserved "Collection")
    <|> dtArray
    <|> dtHash
    <|> (UDTUndef <$ reserved "Undef")
    <|> (reserved "Optional" *> (UDTOptional <$> brackets datatype))
    <|> (UNotUndef <$ reserved "NotUndef")
    <|> (reserved "Variant" *> (UDTVariant . NE.fromList <$> brackets (datatype `sepBy1` symbolic ',')))
    <|> (reserved "Regexp" *> (UDTRegexp <$> optional (brackets termRegexp)))
    <|> (UDTDeferred <$ reserved "Deferred")
    <|> (reserved "Sensitive" *> (UDTSensitive <$> brackets datatype))
    -- while all the other cases are straightforward, it seems that the
    -- following syntax is a valid regexp for puppet:
    --   '^dqsqsdqs$'
    -- instead of:
    --   /^dqsqsdqs$/
    --
    -- That is the reason there is a "quotedRegexp" case
    <|> (reserved "Pattern" *> (UDTPattern . NE.fromList <$> brackets ((termRegexp <|> quotedRegexp) `sepBy1` symbolic ',')))
    <|> (reserved "Enum" *> (UDTEnum . NE.fromList <$> brackets (expression `sepBy1` symbolic ',')))
    <|> dtExternal
    <?> "UDataType"
  where
    quotedRegexp = stringLiteral' >>= compileRegexp
    integer = integerOrDouble >>= either (return . fromIntegral) (\d -> fail ("Integer value expected, instead of " ++ show d))
    float = either fromIntegral identity <$> integerOrDouble
    dtArgs str def parseArgs = do
      void $ reserved str
      fromMaybe def <$> optional (brackets parseArgs)
    dtbounded s constructor parser = dtArgs s (constructor Nothing Nothing) $ do
      lst <- parser `sepBy1` symbolic ','
      case lst of
        [minlen] -> return $ constructor (Just minlen) Nothing
        [minlen, maxlen] -> return $ constructor (Just minlen) (Just maxlen)
        _ -> fail ("Too many arguments to datatype " ++ Text.unpack s)
    dtString = dtbounded "String" UDTString integer
    dtInteger = dtbounded "Integer" UDTInteger integer
    dtFloat = dtbounded "Float" UDTFloat float
    dtNumeric = dtbounded "Numeric" (\ma mb -> UDTVariant (UDTFloat ma mb :| [UDTInteger (truncate <$> ma) (truncate <$> mb)])) float
    dtArray = do
      reserved "Array"
      ml <- optional $ brackets $ do
        tp <- datatype
        rst <- optional (symbolic ',' *> integer `sepBy1` symbolic ',')
        return (tp, rst)
      case ml of
        Nothing -> return (UDTArray UDTData 0 Nothing)
        Just (t, Nothing) -> return (UDTArray t 0 Nothing)
        Just (t, Just [mi]) -> return (UDTArray t mi Nothing)
        Just (t, Just [mi, mx]) -> return (UDTArray t mi (Just mx))
        Just (_, Just _) -> fail "Too many arguments to datatype Array"
    dtHash = do
      reserved "Hash"
      ml <- optional $ brackets $ do
        tk <- datatype
        symbolic ','
        tv <- datatype
        rst <- optional (symbolic ',' *> integer `sepBy1` symbolic ',')
        return (tk, tv, rst)
      case ml of
        Nothing -> return (UDTHash UDTScalar UDTData 0 Nothing)
        Just (tk, tv, Nothing) -> return (UDTHash tk tv 0 Nothing)
        Just (tk, tv, Just [mi]) -> return (UDTHash tk tv mi Nothing)
        Just (tk, tv, Just [mi, mx]) -> return (UDTHash tk tv mi (Just mx))
        Just (_, _, Just _) -> fail "Too many arguments to datatype Hash"
    dtExternal =
      choice
        [ reserved "Stdlib::Absolutepath" $> UDTData,
          reserved "Stdlib::Base32" $> UDTData,
          reserved "Stdlib::Base64" $> UDTData,
          reserved "Stdlib::Compat::Absolute_path" $> UDTData,
          reserved "Stdlib::Compat::Array" $> UDTData,
          reserved "Stdlib::Compat::Bool" $> UDTData,
          reserved "Stdlib::Compat::Float" $> UDTData,
          reserved "Stdlib::Compat::Hash" $> UDTData,
          reserved "Stdlib::Compat::Integer" $> UDTData,
          reserved "Stdlib::Compat::Ip_address" $> UDTData,
          reserved "Stdlib::Compat::Ipv4" $> UDTData,
          reserved "Stdlib::Compat::Ipv6" $> UDTData,
          reserved "Stdlib::Compat::Numeric" $> UDTData,
          reserved "Stdlib::Compat::String" $> UDTData,
          reserved "Stdlib::Ensure::Service" $> UDTData,
          reserved "Stdlib::Filemode" $> UDTData,
          reserved "Stdlib::Filesource" $> UDTData,
          reserved "Stdlib::Fqdn" $> UDTData,
          reserved "Stdlib::Host" $> UDTData,
          reserved "Stdlib::HTTPSUrl" $> UDTData,
          reserved "Stdlib::HTTPUrl" $> UDTData,
          reserved "Stdlib::IP::Address::Nosubnet" $> UDTData,
          reserved "Stdlib::Ip_address" $> UDTData,
          reserved "Stdlib::IP::Address" $> UDTData,
          reserved "Stdlib::IP::Address::V4::CIDR" $> UDTData,
          reserved "Stdlib::IP::Address::V4::Nosubnet" $> UDTData,
          reserved "Stdlib::IP::Address::V4" $> UDTData,
          reserved "Stdlib::IP::Address::V6::Alternative" $> UDTData,
          reserved "Stdlib::IP::Address::V6::Compressed" $> UDTData,
          reserved "Stdlib::IP::Address::V6::Full" $> UDTData,
          reserved "Stdlib::IP::Address::V6::Nosubnet::Alternative" $> UDTData,
          reserved "Stdlib::IP::Address::V6::Nosubnet::Compressed" $> UDTData,
          reserved "Stdlib::IP::Address::V6::Nosubnet::Full" $> UDTData,
          reserved "Stdlib::IP::Address::V6::Nosubnet" $> UDTData,
          reserved "Stdlib::IP::Address::V6" $> UDTData,
          reserved "Stdlib::Ipv4" $> UDTData,
          reserved "Stdlib::Ipv6" $> UDTData,
          reserved "Stdlib::MAC" $> UDTData,
          reserved "Stdlib::Port::Privileged" $> UDTData,
          reserved "Stdlib::Port" $> UDTData,
          reserved "Stdlib::Port::Unprivileged" $> UDTData,
          reserved "Stdlib::Unixpath" $> UDTData,
          reserved "Stdlib::Windowspath" $> UDTData,
          reserved "Nginx::ErrorLogSeverity" $> UDTData,
          reserved "Jenkins::Tunnel" $> UDTData,
          reserved "Systemd::Unit" $> UDTData,
          reserved "Systemd::ServiceLimits" $> UDTData,
          reserved "Systemd::Dropin" $> UDTData,
          reserved "Systemd::JournaldSettings" $> UDTData,
          reserved "Systemd::JournaldSettings::Ensure" $> UDTData,
          reserved "Systemd::LogindSettings" $> UDTData,
          reserved "Systemd::LogindSettings::Ensure" $> UDTData,
          reserved "Systemd::CoredumpSettings" $> UDTData
        ]

statementList :: Parser (Vector Statement)
statementList = V.fromList . concat <$> many statement

lambdaCall :: Parser HOLambdaCall
lambdaCall = do
  let tostrict (Just x) = S.Just x
      tostrict Nothing = S.Nothing
  HOLambdaCall
    <$> lambFunc
    <*> parameters
    <*> lambParams
    <*> (symbolic '{' *> fmap (V.fromList . concat) (many (try statement)))
    <*> fmap tostrict (optional expression)
    <* symbolic '}'
  where
    parameters :: Parser (V.Vector Expression)
    parameters = maybe V.empty V.fromList <$> optional (parens (expression `sepBy` comma))
    lambFunc :: Parser LambdaFunc
    lambFunc = LambdaFunc <$> moduleName
    lambParams :: Parser LambdaParameters
    lambParams = between (symbolic '|') (symbolic '|') hp
      where
        lambdaParameter :: Parser LambdaParameter
        lambdaParameter = LambdaParam <$> optional datatype <*> lexeme (char '$' *> identifier)
        hp = V.fromList <$> lambdaParameter `sepBy1` comma