packages feed

jsonnet-0.4.0.0: src/Language/Jsonnet/Parser.hs

{-# LANGUAGE OverloadedStrings #-}

-- |
-- 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.
module Language.Jsonnet.Parser
  ( parse,
    resolveImports,
    reservedKeywords,
  )
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 Control.Monad.IO.Class (MonadIO, liftIO)
import Data.Char
import Data.Either
import Data.Fix
import Data.Functor
import Data.Functor.Sum
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 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
import Data.Word (Word8)
import qualified Data.ByteString as BS

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 :: forall m.
  (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')) a) =
      case import' of
        Import fp' -> do
          resolveImports fp'
            =<< parse fp'
            =<< readImportFile fp' a
        Importstr fp' -> do
          content <- readImportFile fp' a
          pure $ Fix $ AnnF (EStr content) a
        Importbin fp' -> do
          content <- readBinaryFile fp' a
          pure $ mkBinaryLiteral content a

    readImportFile :: FilePath -> SrcSpan -> m Text
    readImportFile = readFileWith T.readFile

    readBinaryFile :: FilePath -> SrcSpan -> m [Word8]
    readBinaryFile = readFileWith (fmap BS.unpack . BS.readFile)

    readFileWith :: (FilePath -> IO a) -> FilePath -> SrcSpan -> m a
    readFileWith reader fp' a = do
      inp <- (handleIO . reader) fp'
      liftEither $ left (ParserError . flip ImportError (Just a)) inp
      where
        handleIO :: IO a -> m (Either IOError a)
        handleIO =
          liftIO
          . tryIOError
          . withCurrentDirectory (takeDirectory fp)

    mkBinaryLiteral :: [Word8] -> SrcSpan -> Expr
    mkBinaryLiteral ws a = Fix $ AnnF (EArr $ map wordExpr ws) a
      where
        wordExpr :: Word8 -> Expr
        wordExpr w = Fix $ AnnF (ENum (fromIntegral w)) a

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'])

identStringP :: Parser Expr'
identStringP = Fix <$> annotateLoc (mkStrF <$> identifier)

stringP :: Parser Expr'
stringP =
  Fix
    <$> annotateLoc
      ( mkStrF
          <$> ( verbatimString
                  <|> stringLiteral
                  <|> textBlock
              )
      )
    <?> "string"

numberP :: Parser Expr'
numberP = Fix <$> annotateLoc number <?> "number"
  where
    number = mkFloatF <$> lexeme L.scientific

identP :: Parser Expr'
identP =
  Fix
    <$> annotateLoc
      (mkIdentF <$> (try (T.unpack <$> symbol "$") <|> identifier))
    <?> "identifier"

booleanP :: Parser Expr'
booleanP = Fix <$> annotateLoc boolean <?> "bool"
  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 <?> "if"
  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)
    <?> "function"

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 <?> "local"
  where
    localExpr = do
      bnds <- localBndsP
      _ <- symbol ";"
      mkLocalF bnds <$> exprP

arrayP :: Parser Expr'
arrayP = Fix <$> annotateLoc (brackets (try arrayComp <|> array)) <?> "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)) <?> "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, computed) <- keyP
      (override, visibility) <-
        (,)
          <$> option False (symbol "+" $> True) <*> sepP
      value <- exprP
      pure $ EField {..}
    keyP = ((,True) <$> brackets exprP) <|> ((,False) <$> (identStringP <|> stringP))
    methodP = do
      let override = False
          computed = False
      key <- identStringP
      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 <?> "import"
  where
    importDecl = mkImportF <$> (keywordP "import" *> stringLiteral)

importstrP :: Parser Expr'
importstrP = Fix <$> annotateLoc importstrDecl <?> "importstr"
  where
    importstrDecl = mkImportstrF <$> (keywordP "importstr" *> stringLiteral)

importbinP :: Parser Expr'
importbinP = Fix <$> annotateLoc importbinDecl <?> "importbin"
  where
    importbinDecl = mkImportbinF <$> (keywordP "importbin" *> 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 "." *> annotatedIdent) <?> "."
  where
    annotatedIdent :: Parser (Ident, SrcSpan)
    annotatedIdent = do
      begin <- getSourcePos
      res <- identifier
      end <- getSourcePos
      let sourceSpan = SrcSpan begin end
      pure (res, sourceSpan)

-- 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,
        importstrP,
        importbinP,
        importP,
        errorP,
        assertP,
        parens exprP
      ]

exprP :: Parser Expr'
exprP = makeExprParser primP opTable

reservedKeywords :: [String]
reservedKeywords =
  [ "assert",
    "else",
    "error",
    "false",
    "for",
    "function",
    "if",
    "import",
    "importstr",
    "importbin",
    "in",
    "local",
    "null",
    "tailstrict",
    "then",
    "true"
  ]