packages feed

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

-- |
-- Module                  : Language.Jsonnet.Pretty
-- 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
module Language.Jsonnet.Pretty where

import Control.Applicative (Const (..))
import qualified Data.Aeson as JSON
import qualified Data.Aeson.Key as Key
import qualified Data.Aeson.KeyMap as KeyMap
import qualified Data.Aeson.Text as JSON (encodeToLazyText)
import Data.Bifunctor (bimap, first)
import Data.Bool (bool)
import Data.Fix
import Data.Functor.Sum
import Data.Maybe (fromMaybe)
import Data.List (sortOn)
import Data.List.NonEmpty (NonEmpty)
import qualified Data.List.NonEmpty as NE
import Data.Scientific (Scientific (..))
import qualified Data.Text as T
import qualified Data.Text.Lazy as LT
import Data.Text.Lazy.Builder
import Data.Text.Lazy.Builder.Scientific (scientificBuilder)
import qualified Data.Vector as V
import GHC.IO.Exception (IOException (..))
import Language.Jsonnet.Annotate
import Language.Jsonnet.Common
import Language.Jsonnet.Error
import Language.Jsonnet.Parser.SrcSpan
import Language.Jsonnet.Syntax
import qualified Language.Jsonnet.Syntax.Annotated as Ann
import Prettyprinter
import Text.Megaparsec.Error (errorBundlePretty)
import Text.Megaparsec.Pos
import Unbound.Generics.LocallyNameless (Name, name2String)

(<$$>) :: Doc ann -> Doc ann -> Doc ann
(<$$>) = \x y -> vcat [x, y]

-- reserved keywords
pnull, ptrue, pfalse, pif, pthen, pelse, pimport, pimportstr, pimportbin,
  perror, plocal, pfunction, passert, pfor, pin :: Doc ann
pnull = pretty "null"
ptrue = pretty "true"
pfalse = pretty "false"
pif = pretty "if"
pthen = pretty "then"
pelse = pretty "else"
pimport = pretty "import"
pimportstr = pretty "importstr"
pimportbin = pretty "importbin"
perror = pretty "error"
plocal = pretty "local"
pfunction = pretty "function"
passert = pretty "assert"
pfor = pretty "for"
pin = pretty "in"

prettyName :: Name a -> Doc ann
prettyName = pretty . name2String

ppNumber :: Scientific -> Doc ann
ppNumber s
  | e < 0 || e > 1024 =
    pretty $
      LT.unpack $
        toLazyText $
          scientificBuilder s
  | otherwise = pretty (coefficient s * 10 ^ e)
  where
    e = base10Exponent s

ppJson :: Int -> JSON.Value -> Doc ann
ppJson i = \case
  JSON.Null -> pnull
  JSON.Number n -> ppNumber n
  JSON.Bool True -> ptrue
  JSON.Bool False -> pfalse
  JSON.String s -> ppString (Key.fromText s)
  JSON.Array a -> ppArray a
  JSON.Object o -> ppObject' o
  where
    encloseSep' l r s ds = case ds of
      [] -> l <> r
      _ -> l <$$> indent i (vcat $ punctuate s ds) <$$> r
    ppObject' o = encloseSep' lbrace rbrace comma xs
      where
        prop (k, v) = ppString k <> colon <+> ppJson i v
        xs = map prop (sortOn fst $ KeyMap.toList o)
    ppArray a = encloseSep' lbracket rbracket comma xs
      where
        xs = map (ppJson i) (V.toList a)
    ppString = pretty . LT.unpack . JSON.encodeToLazyText

prettySrcSpan :: SrcSpan -> Doc ann
prettySrcSpan SrcSpan {spanBegin, spanEnd} =
  pretty (sourceName spanBegin)
    <> colon
    <> lc spanBegin spanEnd
  where
    lc (SourcePos _ lb cb) (SourcePos _ le ce)
      | lb == le =
        pretty (unPos lb) <> colon
          <> pretty (unPos cb)
          <> dash
          <> pretty (unPos ce)
      | otherwise =
        pretty (unPos lb) <> colon <> pretty (unPos cb) <> dash
          <> pretty (unPos le)
          <> colon
          <> pretty (unPos ce)
    dash = pretty '-'

prettyParserError :: ParserError -> Doc ann
prettyParserError (ParseError e) = pretty (errorBundlePretty e)
prettyParserError (ImportError (IOError _ _ _ desc _ f) sp) =
  pretty "Parse error:"
    <+> pretty f
    <+> parens (pretty desc)
    <$$> indent 4 (maybe mempty prettySrcSpan sp)

prettyCheckError :: CheckError -> Doc ann
prettyCheckError = \case
  DuplicateParam e ->
    pretty "duplicate parameter"
      <+> squotes (pretty e)
  DuplicateBinding e ->
    pretty "duplicate local var"
      <+> squotes (pretty e)
  PosAfterNamedParam ->
    pretty "positional after named argument"

prettyEvalError :: EvalError -> Doc ann
prettyEvalError = \case
  TypeMismatch expected actual ->
    pretty "type mismatch:"
      <+> pretty "expected"
      <+> pretty (T.unpack expected)
      <+> pretty "but got"
      <+> pretty (T.unpack actual)
  InvalidKey k ->
    pretty "invalid key:"
      <+> pretty k
  DuplicateKey k ->
    pretty "duplicate key:"
      <+> pretty k
  InvalidIndex k ->
    pretty "invalid index:"
      <+> pretty k
  NoSuchKey k ->
    pretty "no such key:"
      <+> pretty k
  IndexOutOfBounds i ->
    pretty "index out of bounds:"
      <+> ppNumber i
  DivByZero ->
    pretty "divide by zero exception"
  VarNotFound v ->
    pretty "variable"
      <+> squotes (pretty $ show v)
      <+> pretty "is not defined"
  ExtVarNotFound v ->
    pretty "external variable"
      <+> squotes (pretty v)
      <+> pretty "is not defined"
  AssertionFailed e ->
    pretty "assertion failed:" <+> pretty e
  StdError e -> pretty e
  RuntimeError e -> pretty e
  ParamNotBound s ->
    pretty "parameter not bound:"
      <+> pretty (show s)
  BadParam s ->
    pretty "function has no parameter"
      <+> squotes (pretty s)
  ManifestError e ->
    pretty "manifest error:"
      <+> pretty e
  TooManyArgs n ->
    pretty "too many args, function has"
      <+> pretty n
      <+> pretty "parameter(s)"

prettyStackFrame :: StackFrame a -> Doc ann
prettyStackFrame StackFrame {name, span = span'} =
  prettySrcSpan span' <+> f (name2String name)
  where
    f "top-level" = mempty
    f x = pretty "function" <+> angles (pretty x)

prettyBacktrace :: Backtrace a -> Doc ann
prettyBacktrace (Backtrace xs) = vcat $ prettyStackFrame <$> xs

prettyError :: Error -> Doc ann
prettyError = \case
  EvalError e bt ->
    pretty "Runtime error:"
      <+> prettyEvalError e
      <$$> indent 2 (prettyBacktrace bt)
  ParserError e -> prettyParserError e
  CheckError e sp ->
    pretty "Static error:"
      <+> prettyCheckError e
      <$$> indent 2 (maybe mempty prettySrcSpan sp)

prettyVisibility :: Visibility -> Doc ann
prettyVisibility = \case
  Visible -> pretty ":"
  Hidden -> pretty "::"
  Forced -> pretty ":::"

commaSep :: [Doc ann] -> Doc ann
commaSep = concatWith (surround comma)

bracketSep :: Doc ann -> [Doc ann] -> Doc ann
bracketSep = encloseSep lbracket rbracket

parensSep :: Doc ann -> [Doc ann] -> Doc ann
parensSep = encloseSep lparen rparen

braceSep :: Doc ann -> [Doc ann] -> Doc ann
braceSep = encloseSep lbrace rbrace

ppField :: EField (Doc ann) -> Doc ann
ppField EField {..} =
  surround (ppOverride <> prettyVisibility visibility) key' value
  where
    ppOverride = pretty (bool "" "+" override)
    key' = bool key (brackets key) computed

ppObject :: [(Ident, Doc ann)] -> [Doc ann] -> Doc ann
ppObject l o =
  encloseSep
    lbrace
    rbrace
    comma
    (mcons (ppLocal' l) o)
  where
    mcons ma as = maybe as (: as) ma
    ppLocal' :: [(Ident, Doc ann)] -> Maybe (Doc ann)
    ppLocal' [] = Nothing
    ppLocal' xs =
      Just $ commaSep ((plocal <+>) . uncurry (surround equals) <$> xs')
      where
        xs' = first pretty <$> xs

ppLocal :: [(Ident, Doc ann)] -> Doc ann -> Doc ann
ppLocal xs e =
  plocal
    <+> commaSep (uncurry (surround equals) <$> xs')
      <> semi
    <+> e
  where
    xs' = first pretty <$> xs

prettyUnyOp :: UnyOp -> Doc ann
prettyUnyOp = \case
  Compl -> pretty "~"
  LNot -> pretty "!"
  Plus -> pretty "+"
  Minus -> pretty "-"
  Err -> pretty "<prim:err>"

prettyBinOp :: BinOp -> Doc ann
prettyBinOp = \case
  Add -> pretty "+"
  Sub -> pretty "-"
  Mul -> pretty "*"
  Div -> pretty "/"
  Mod -> pretty "%"
  Lt -> pretty "<"
  Le -> pretty "<="
  Gt -> pretty ">"
  Ge -> pretty ">="
  Eq -> pretty "=="
  Ne -> pretty "!="
  And -> pretty "&"
  Or -> pretty "|"
  Xor -> pretty "^"
  ShiftL -> pretty "<<"
  ShiftR -> pretty ">>"
  LAnd -> pretty "&&"
  LOr -> pretty "||"
  In -> pretty "in"
  Lookup -> pretty "<prim:lookup>"

ppFun :: [Param (Doc ann)] -> Doc ann -> Doc ann
ppFun ps e = pfunction <> parens (commaSep ps') <+> e
  where
    f = maybe mempty (equals <>)
    ps' = uncurry (<>) . bimap pretty f <$> ps

ppMaybeDoc :: Maybe (Doc ann) -> Doc ann
ppMaybeDoc = fromMaybe mempty

prettyExpr' :: Ann.Expr' -> Doc ann
prettyExpr' = prettyFixExprF' . forget'

prettyImport :: Import -> Doc ann
prettyImport = \case
  Import fp -> pimport <+> squotes (pretty fp)
  Importstr fp -> pimportstr <+> squotes (pretty fp)
  Importbin fp -> pimportbin <+> squotes (pretty fp)

prettyFixExprF' :: Fix ExprF' -> Doc ann
prettyFixExprF' = foldFix $ \case
  InR (Const import') -> prettyImport import'
  InL e -> ppExpr e

prettyFixExprF :: Fix ExprF -> Doc ann
prettyFixExprF = foldFix ppExpr

prettyExpr :: Ann.Expr -> Doc ann
prettyExpr = prettyFixExprF . forget'

ppArgs :: Args (Doc ann) -> Doc ann
ppArgs (Args args _) = parensSep comma (ppArg <$> args)

ppArg :: Arg (Doc ann) -> Doc ann
ppArg (Pos a) = a
ppArg (Named n a) = surround equals (pretty n) a

ppCompSpec :: NonEmpty (CompSpec (Doc ann)) -> Doc ann
ppCompSpec cs = hsep $ f <$> NE.toList cs
  where
    f (CompSpec v f' c) =
      pfor
        <+> pretty v
        <+> pin
        <+> parens f'
        <+> ppMaybeDoc ((pif <+>) <$> c)

ppExpr :: ExprF (Doc ann) -> Doc ann
ppExpr = \case
  ENull -> pnull
  EBool True -> ptrue
  EBool False -> pfalse
  EStr s -> squotes (pretty s)
  ENum n -> ppNumber n
  EFun ps e -> ppFun ps e
  EApply a args -> parens a <> ppArgs args
  EIdent i -> pretty i
  EIf c t -> pif <+> c <+> pthen <+> parens t
  EIfElse c t e -> pif <+> c <+> pthen <+> parens t <+> pelse <+> parens e
  EArr a -> bracketSep comma a
  EObj ls o -> ppObject ls (ppField <$> o)
  ELocal xs e -> ppLocal (NE.toList xs) (parens e)
  EErr a -> perror <+> parens a
  EAssert (Assert c m e) -> passert <+> c <> ppMaybeDoc ((colon <>) <$> m) <> semi <+> e
  EIndex a b -> parens a <> brackets b
  ELookup a i -> enclose a (pretty i) dot
  EUnyOp o a -> parens (prettyUnyOp o <> a)
  EBinOp o a b -> parens (parens a <+> prettyBinOp o <+> parens b)
  ESlice a b e s -> parens a <> bracketSep colon (ppMaybeDoc <$> [b, e, s])
  EArrComp e cs -> lbracket <> e <+> ppCompSpec cs <> rbracket
  EObjComp f ls cs -> ppObject ls [ppField f <+> ppCompSpec cs]