jsonnet-0.3.0.1: src/Language/Jsonnet/Parser.hs
{- |
Module : Language.Jsonnet.Parser
Copyright : (c) 2020-2021 Alexandre Moreno
SPDX-License-Identifier : BSD-3-Clause OR Apache-2.0
Maintainer : Alexandre Moreno <alexmorenocano@gmail.com>
Stability : experimental
Portability : non-portable
Parser for Jsonnet source code.
-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE RecordWildCards #-}
{-# LANGUAGE TupleSections #-}
module Language.Jsonnet.Parser
( parse,
resolveImports,
)
where
import Control.Applicative hiding (many, some)
import Control.Arrow (left)
import Control.Monad
import Control.Monad.Combinators.Expr
import qualified Control.Monad.Combinators.NonEmpty as NE
import Control.Monad.Except
import Data.Char
import Data.Either
import Data.Fix
import Data.Functor
import Data.Functor.Sum
import Data.List (intercalate)
import Data.List.NonEmpty (NonEmpty)
import Data.Text (Text)
import qualified Data.Text as T
import qualified Data.Text.IO as T
import Data.Void
import GHC.IO.Exception hiding (IOError)
import Language.Jsonnet.Annotate
import Language.Jsonnet.Common
import Language.Jsonnet.Error
import Language.Jsonnet.Parser.SrcSpan
import Language.Jsonnet.Syntax
import Language.Jsonnet.Syntax.Annotated
import System.Directory
import System.FilePath.Posix (takeDirectory)
import System.IO.Error (tryIOError)
import Text.Megaparsec hiding (ParseError, parse)
import Text.Megaparsec.Char
import qualified Text.Megaparsec.Char.Lexer as L
type Parser = Parsec Void Text
parse ::
MonadError Error m =>
-- | File name (only for source location annotations)
FilePath ->
-- | Input for parser
Text ->
-- | AST with unresolved imports
m Expr'
parse fp inp =
liftEither $
left (ParserError . ParseError) $
runParser (sc *> exprP <* eof) fp inp
resolveImports ::
(MonadError Error m, MonadIO m) =>
-- | File path (modules are resolved relative to this path)
FilePath ->
-- | AST with unresolved imports
Expr' ->
-- | AST with imports resolved
m Expr
resolveImports fp = foldFixM go
where
go (AnnF (InL e) a) = pure $ Fix $ AnnF e a
go (AnnF (InR (Const (Import fp'))) a) =
resolveImports fp'
=<< parse fp'
=<< readImportFile fp' a
readImportFile fp' a = do
inp <- readFile' fp'
liftEither $ left (ParserError . flip ImportError (Just a)) inp
where
readFile' =
liftIO
. tryIOError
. withCurrentDirectory (takeDirectory fp)
. T.readFile
sc :: Parser ()
sc = L.space space1 lineComment blockComment
where
lineComment = L.skipLineComment "//" <|> L.skipLineComment "#"
blockComment = L.skipBlockComment "/*" "*/"
symbol :: Text -> Parser Text
symbol = L.symbol sc
lexeme :: Parser a -> Parser a
lexeme = L.lexeme sc
braces :: Parser a -> Parser a
braces = between (symbol "{") (symbol "}")
brackets :: Parser a -> Parser a
brackets = between (symbol "[") (symbol "]")
parens :: Parser a -> Parser a
parens = between (symbol "(") (symbol ")")
comma :: Parser Text
comma = symbol ","
colon :: Parser Text
colon = symbol ":"
annotateLoc :: Parser (f a) -> Parser (AnnF f SrcSpan a)
annotateLoc p = do
begin <- getSourcePos
res <- p
AnnF res . SrcSpan begin <$> getSourcePos
identifier :: Parser String
identifier = do
ident <- p
when (ident `elem` reservedKeywords) $
fail $
"Keyword " <> ident <> " cannot be an identifier."
pure ident
where
p =
lexeme
( (:)
<$> (letterChar <|> char '_')
<*> many (alphaNumChar <|> char '_')
)
keywordP :: Text -> Parser Text
keywordP keyword = lexeme (string keyword <* notFollowedBy alphaNumChar)
-- unfinished string parser
stringLiteral :: Parser String
stringLiteral = quoted (char '\"') <|> quoted (char '\'')
where
quoted c =
c
*> manyTill
( try escapeUnicode <|> escapeAscii
<|> anySingle
)
c
escapeAscii :: Parser Char
escapeAscii = do
void (char '\\')
choice
[ char '\"' $> '\"',
char '\'' $> '\'', -- this one is jsonnet specific
char '\\' $> '\\',
char '/' $> '/',
char 'n' $> '\n',
char 'r' $> '\r',
char 'f' $> '\f',
char 't' $> '\t',
char 'b' $> '\b'
]
escapeUnicode :: Parser Char
escapeUnicode = do
_ <- string "\\u"
hex <- ("0x" ++) <$> count 4 hexDigitChar
pure (chr $ read hex)
verbatimString :: Parser String
verbatimString = char '@' *> (quoted (char '\'') <|> quoted (char '\"'))
where
quoted c =
c
*> manyTill
(c *> c <|> anySingle)
(try $ c <* notFollowedBy c)
textBlock :: Parser String
textBlock = do
_ <- symbol "|||" <* sc'
ref <- L.indentLevel
x <- line
xs <-
manyTill
(L.indentGuard (sc'' ref) EQ ref >> line)
(try $ sc' *> symbol "|||")
pure $ concat (x : xs)
where
line :: Parser String
line = (++) <$> many (anySingleBut '\n') <*> some (char '\n')
sc' :: Parser ()
sc' = L.space (void $ some (char ' ' <|> char '\t')) empty empty
sc'' :: Pos -> Parser ()
sc'' x = void $ count' 0 (unPos x - 1) (oneOf [' ', '\t'])
unquoted :: Parser Expr'
unquoted = Fix <$> annotateLoc (mkStrF <$> identifier)
stringP :: Parser Expr'
stringP =
Fix
<$> annotateLoc
( mkStrF
<$> ( verbatimString
<|> stringLiteral
<|> textBlock
)
)
numberP :: Parser Expr'
numberP = Fix <$> annotateLoc number
where
number = mkFloatF <$> lexeme L.scientific
identP :: Parser Expr'
identP = Fix <$> annotateLoc (mkIdentF <$> (try (T.unpack <$> symbol "$") <|> identifier))
booleanP :: Parser Expr'
booleanP = Fix <$> annotateLoc boolean
where
boolean =
keywordP "true" $> mkBoolF True
<|> keywordP "false" $> mkBoolF False
nullP :: Parser Expr'
nullP = Fix <$> annotateLoc null
where
null = keywordP "null" $> mkNullF
errorP :: Parser Expr'
errorP = Fix <$> annotateLoc error
where
error = keywordP "error" *> (mkErrorF <$> exprP)
assertP :: Parser Expr'
assertP = Fix <$> annotateLoc assert
where
assert = do
cond <- keywordP "assert" *> exprP
msg <- optional (colon *> exprP)
_ <- symbol ";"
mkAssertF cond msg <$> exprP
ifElseP :: Parser Expr'
ifElseP = Fix <$> annotateLoc ifElseExpr
where
ifElseExpr = do
cond <- keywordP "if" *> exprP
expr <- keywordP "then" *> exprP
option
(mkIfF cond expr)
(keywordP "else" *> (mkIfElseF cond expr <$> exprP))
paramsP :: Parser [Param Expr']
paramsP = parens (param `sepEndBy` comma)
where
param = (,) <$> identifier <*> optional (symbol "=" *> exprP)
function ::
Parser [Param Expr'] ->
Parser Expr' ->
Parser Expr'
function ps expr = Fix <$> annotateLoc (mkFunF <$> ps <*> expr)
functionP :: Parser Expr'
functionP = keywordP "function" *> function paramsP exprP
forspecP :: Parser (CompSpec Expr')
forspecP = do
_ <- keywordP "for"
var <- identifier
_ <- keywordP "in"
forspec <- exprP
ifspec <- optional (keywordP "if" *> exprP)
pure CompSpec {..}
binding :: Parser (String, Expr')
binding = do
name <- identifier
_ <- symbol "="
expr <- exprP
pure (name, expr)
localFunc :: Parser (String, Expr')
localFunc = do
name <- identifier
ps <- paramsP
_ <- symbol "="
expr <- function (pure ps) exprP
pure (name, expr)
localBndsP :: Parser (NonEmpty (String, Expr'))
localBndsP = do
_ <- keywordP "local"
(try binding <|> localFunc) `NE.sepBy1` comma
localP :: Parser Expr'
localP = Fix <$> annotateLoc localExpr
where
localExpr = do
bnds <- localBndsP
_ <- symbol ";"
mkLocalF bnds <$> exprP
arrayP :: Parser Expr'
arrayP = Fix <$> annotateLoc (brackets (try arrayComp <|> array))
where
array = mkArrayF <$> (exprP `sepEndBy` comma)
arrayComp = do
expr <- exprP <* optional comma
comps <- NE.some forspecP
return $ mkArrCompF expr comps
objectP :: Parser Expr'
objectP = Fix <$> annotateLoc (braces (try objectComp <|> object))
where
object = do
xs <- eitherP localP fieldP `sepEndBy` comma
let (ls, fs) = (lefts xs, rights xs)
pure $ mkObjectF fs ls
fieldP = try methodP <|> pairP
pairP = do
key <- keyP
(override, visibility) <-
(,)
<$> option False (symbol "+" $> True) <*> sepP
value <- exprP
pure $ EField {..}
keyP = brackets exprP <|> unquoted <|> stringP
methodP = do
let override = False
key <- unquoted
ps <- paramsP
visibility <- sepP
value <- function (pure ps) exprP
pure $ EField {..}
sepP =
try (symbol ":::" $> Forced)
<|> try (symbol "::" $> Hidden)
<|> symbol ":" $> Visible
localP = do
_ <- keywordP "local"
try binding <|> localFunc
objectComp = do
locals1 <- localP `sepEndBy` comma
expr <- pairP <* optional comma
locals2 <- localP `sepEndBy` comma
comps <- NE.some forspecP
return $ mkObjCompF expr (locals1 <> locals2) comps
importP :: Parser Expr'
importP = Fix <$> annotateLoc importDecl
where
importDecl = mkImportF <$> (keywordP "import" *> stringLiteral)
binary ::
Text ->
(Expr' -> Expr' -> Expr') ->
Operator Parser Expr'
binary name f = InfixL (f <$ operator name)
where
operator sym = try $ symbol sym <* notFollowedBy opChar
opChar = oneOf (":~+&|^=<>*/%" :: String) <?> "operator"
prefix ::
Text ->
(Expr' -> Expr') ->
Operator Parser Expr'
prefix name f = Prefix (f <$ symbol name)
-- | associativity and operator precedence
-- 1. @e(...)@ @e[...]@ @e.f@ (application and indexing)
-- 2. @+@ @-@ @!@ @~@ (the unary operators)
-- 3. @*@ @/@ @%@ (these, and the remainder below, are binary operators)
-- 4. @+@ @-@
-- 5. @<<@ @>>@
-- 6. @<@ @>@ @<=@ @>=@ @in@
-- 7. @==@ @!=@
-- 8. @&@
-- 9. @^@
-- 10. @|@
-- 11. @&&@
-- 12. @||@
-- default is associate to the left
opTable :: [[Operator Parser Expr']]
opTable =
[ [Postfix postfixOperators],
[ prefix "+" (mkUnyOp Plus),
prefix "-" (mkUnyOp Minus),
prefix "!" (mkUnyOp LNot),
prefix "~" (mkUnyOp Compl)
],
[ binary "*" (mkBinOp Mul),
binary "/" (mkBinOp Div),
binary "%" (mkBinOp Mod)
],
[ binary "+" (mkBinOp Add),
binary "-" (mkBinOp Sub),
Postfix postfixObjectMerge
],
[ binary ">>" (mkBinOp ShiftR),
binary "<<" (mkBinOp ShiftL)
],
[ binary "in" (mkBinOp In),
binary ">" (mkBinOp Gt),
binary "<=" (mkBinOp Le),
binary ">=" (mkBinOp Ge),
binary "<" (mkBinOp Lt)
],
[ binary "==" (mkBinOp Eq),
binary "!=" (mkBinOp Ne)
],
[binary "&" (mkBinOp And)],
[binary "^" (mkBinOp Xor)],
[binary "|" (mkBinOp Or)],
[binary "&&" (mkBinOp LAnd)],
[binary "||" (mkBinOp LOr)]
]
-- | shorthand syntax for object composition:
-- when the right-hand side is an object literal the '+'
-- operator can be elided.
postfixObjectMerge :: Parser (Expr' -> Expr')
postfixObjectMerge = flip (mkBinOp Add) <$> objectP
-- | application, indexing and lookup: e(...) e[...] e.f
-- all have the same precedence (the highest)
postfixOperators :: Parser (Expr' -> Expr')
postfixOperators =
foldr1 (flip (.))
<$> some
( applyP
<|> try sliceP
<|> indexP
<|> lookupP
)
indexP :: Parser (Expr' -> Expr')
indexP = flip mkIndex <$> brackets exprP
lookupP :: Parser (Expr' -> Expr')
lookupP = flip mkLookup <$> (symbol "." *> unquoted)
-- arguments are many postional followed by many named
-- just like Python
applyP :: Parser (Expr' -> Expr')
applyP = flip mkApply <$> argsP
where
argsP :: Parser (Args Expr')
argsP = Args <$> parens (args `sepEndBy` comma) <*> tailstrict
where
args = try named <|> posal
posal = Pos <$> exprP
named = Named <$> identifier <*> (symbol "=" *> exprP)
tailstrict = option Lazy (keywordP "tailstrict" $> Strict)
sliceP :: Parser (Expr' -> Expr')
sliceP = brackets $ do
start <- optional exprP <* colon
end <- optional exprP
step <- optional (colon *> optional exprP)
pure $ mkSlice start end (join step)
primP :: Parser Expr'
primP =
lexeme $
choice
[ try identP,
numberP,
stringP,
booleanP,
nullP,
ifElseP,
functionP,
objectP,
arrayP,
localP,
importP,
errorP,
assertP,
parens exprP
]
exprP :: Parser Expr'
exprP = makeExprParser primP opTable
reservedKeywords :: [String]
reservedKeywords =
[ "assert",
"else",
"error",
"false",
"for",
"function",
"if",
"import",
"importstr",
"in",
"local",
"null",
"tailstrict",
"then",
"true"
]