packages feed

cuddle-1.7.0.0: src/Codec/CBOR/Cuddle/Parser.hs

{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE OverloadedLabels #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE TypeData #-}
{-# LANGUAGE TypeFamilies #-}

module Codec.CBOR.Cuddle.Parser where

import Codec.CBOR.Cuddle.CDDL
import Codec.CBOR.Cuddle.CDDL.CtlOp (CtlOp)
import Codec.CBOR.Cuddle.CDDL.CtlOp qualified as COp
import Codec.CBOR.Cuddle.Comments (
  Comment,
  HasComment (..),
  WithComment (..),
  appendComment,
  withComment,
  (!*>),
  (<*!),
 )
import Codec.CBOR.Cuddle.Parser.Lexer (
  Parser,
  charInRange,
  pCommentBlock,
  space,
  trailingSpace,
 )
import Control.Applicative.Combinators.NonEmpty qualified as NE
import Data.ByteString.Base16 qualified as Base16
import Data.Foldable (Foldable (..))
import Data.Functor (void, ($>))
import Data.List.NonEmpty (NonEmpty (..))
import Data.List.NonEmpty qualified as NE
import Data.Maybe (isJust)
import Data.Text (Text)
import Data.Text qualified as T
import Data.Text.Encoding (encodeUtf8)
import Data.TreeDiff (ToExpr)
import Data.Void (Void)
import GHC.Generics (Generic)
import GHC.Word (Word64, Word8)
import Optics.Core ((&), (.~))
import Text.Megaparsec
import Text.Megaparsec.Char hiding (space)
import Text.Megaparsec.Char qualified as C
import Text.Megaparsec.Char.Lexer qualified as L

type data ParserStage

newtype instance XXTopLevel ParserStage = ParserXXTopLevel Comment
  deriving (Generic, Show, Eq, ToExpr)

newtype instance XXType2 ParserStage = ParserXXType2 Void
  deriving (Generic, Show, Eq, ToExpr)

newtype instance XTerm ParserStage = ParserXTerm {unParserXTerm :: Comment}
  deriving (Generic, Semigroup, Monoid, Show, Eq, ToExpr)

newtype instance XRule ParserStage = ParserXRule {unParserXRule :: Comment}
  deriving (Generic, Semigroup, Monoid, Show, Eq, ToExpr)

newtype instance XCddl ParserStage = ParserXCddl [Comment]
  deriving (Generic, Semigroup, Monoid, Show, Eq, ToExpr)

instance HasComment (XTerm ParserStage) where
  commentL = #unParserXTerm

instance HasComment (XRule ParserStage) where
  commentL = #unParserXRule

pCDDL :: Parser (CDDL ParserStage)
pCDDL = do
  initialComments <- many (try $ C.space *> pCommentBlock <* notFollowedBy pRule)
  initialRuleComment <- C.space *> optional pCommentBlock
  initialRule <- pRule
  cddlTail <- many $ pTopLevel <* C.space
  eof
    $> CDDL
      (appendComment initialRule (fold initialRuleComment))
      cddlTail
      (ParserXXTopLevel <$> initialComments)

pTopLevel :: Parser (TopLevel ParserStage)
pTopLevel = try tlRule <|> tlComment
  where
    tlRule = do
      mCmt <- optional pCommentBlock
      rule <- pRule
      pure . TopLevelRule $ appendComment rule (fold mCmt)
    tlComment = XXTopLevel . ParserXXTopLevel <$> pCommentBlock

pRule :: Parser (Rule ParserStage)
pRule = do
  name <- pName
  genericParam <- optcomp pGenericParameters
  cmt <- space
  (assign, typeOrGrp) <-
    choice
      [ try $
          (,)
            <$> pAssignT
            <* space
            <*> (TOGType <$> pType0 <* notFollowedBy (space >> (":" <|> "=>")))
      , (,) <$> pAssignG <* space <*> (TOGGroup <$> pGrpEntry)
      ]
  pure $ Rule name genericParam assign typeOrGrp (ParserXRule cmt)

pName :: Parser Name
pName = label "name" $ do
  fc <- firstChar
  rest <- many midChar
  pure $ Name . T.pack $ (fc : rest)
  where
    firstChar = letterChar <|> char '@' <|> char '_' <|> char '$'
    midChar =
      alphaNumChar
        <|> char '@'
        <|> char '_'
        <|> char '$'
        <|> ((char '.' <|> char '-') <* notFollowedBy (space1 <|> eof <|> void eol))

pAssignT :: Parser Assign
pAssignT =
  choice
    [ AssignEq <$ "="
    , AssignExt <$ "/="
    ]

pAssignG :: Parser Assign
pAssignG =
  choice
    [ AssignEq <$ "="
    , AssignExt <$ "//="
    ]

pGenericParameter :: Parser (GenericParameter ParserStage)
pGenericParameter = GenericParameter <$> pName <*> pure mempty

pGenericParameters :: Parser (GenericParameters ParserStage)
pGenericParameters =
  GenericParameters
    <$> between "<" ">" (NE.sepBy1 (space !*> pGenericParameter <*! space) ",")

pGenericArg :: Parser (GenericArg ParserStage)
pGenericArg =
  GenericArg
    <$> between "<" ">" (NE.sepBy1 (space !*> pType1 <*! space) ",")

pType0 :: Parser (Type0 ParserStage)
pType0 = Type0 <$> sepBy1' (space !*> pType1 <*! trailingSpace) (try "/")

pType1 :: Parser (Type1 ParserStage)
pType1 = do
  v <- pType2
  rest <- optional $ do
    (cmtFst, tyOp) <- try $ do
      cmt <- space
      tyOp <- pTyOp
      pure (cmt, tyOp)
    cmtSnd <- space
    w <- pType2
    pure (cmtFst, tyOp, cmtSnd, w)
  case rest of
    Just (cmtFst, tyOp, cmtSnd, w) ->
      pure $ Type1 v (Just (tyOp, w)) . ParserXTerm $ cmtFst <> cmtSnd
    Nothing -> pure $ Type1 v Nothing mempty

pType2 :: Parser (Type2 ParserStage)
pType2 =
  choice
    [ T2Value <$> pValue
    , T2Name <$> pName <*> optional pGenericArg
    , T2Group <$> label "group" ("(" *> pType0Cmt <* ")")
    , T2Map <$> label "map" ("{" *> pGroup <* "}")
    , T2Array <$> label "array" ("[" *> space !*> pGroup <*! space <* "]")
    , T2Unwrapped <$> ("~" *> space *> pName) <*> optional pGenericArg
    , do
        _ <- "&"
        cmt <- space
        choice
          [ T2Enum <$> ("(" *> space !*> pGroup <*! pure cmt <*! space <* ")")
          , T2EnumRef <$> pName <*> optional pGenericArg
          ]
    , "#" *> do
        mmajor :: Maybe Word8 <- optional L.decimal
        case mmajor of
          Just major -> do
            mminor <- optional ("." *> L.decimal)
            let
              pTag
                | major == 6 = T2Tag mminor <$> ("(" *> pType0Cmt <* ")")
                | otherwise = empty
            pTag <|> pure (T2DataItem major mminor)
          Nothing -> pure T2Any
    ]
  where
    pType0Cmt = do
      pre <- space
      Type0 (t :| ts) <- pType0
      post <- space
      pure . Type0 $ (t & commentL .~ (pre <> post)) :| ts

pHeadNumber :: Parser Word64
pHeadNumber = L.decimal

pRangeOp :: Parser RangeBound
pRangeOp = label "range operator" $ try ("..." $> ClOpen) <|> (".." $> Closed)

pCtlOp :: Parser CtlOp
pCtlOp =
  label "control operator" $
    "."
      *> choice
        ( try
            <$> [ "cborseq" $> COp.Cborseq
                , "cbor" $> COp.Cbor
                , "size" $> COp.Size
                , "bits" $> COp.Bits
                , "within" $> COp.Within
                , "and" $> COp.And
                , "lt" $> COp.Lt
                , "le" $> COp.Le
                , "gt" $> COp.Gt
                , "ge" $> COp.Ge
                , "eq" $> COp.Eq
                , "ne" $> COp.Ne
                , "default" $> COp.Default
                , "regexp" $> COp.Regexp
                ]
        )

pGroup :: Parser (Group ParserStage)
pGroup = Group <$> NE.sepBy1 (space !*> pGrpChoice) "//"

pGrpChoice :: Parser (GrpChoice ParserStage)
pGrpChoice = GrpChoice <$> many (space !*> pGrpEntry <*! pOptCom) <*> mempty

pGrpEntry :: Parser (GroupEntry ParserStage)
pGrpEntry = do
  occur <- optcomp pOccur
  cmt <- space
  WithComment cmt' variant <-
    choice
      [ try $ do
          mKey <- optcomp $ pMemberKey <*! space
          t0 <- pType0
          pure $ GEType <$> sequence mKey <*> pure t0
      , try $ withComment <$> (GERef <$> pName <*> optional pGenericArg)
      , withComment . GEGroup <$> ("(" *> space !*> pGroup <*! space <* ")")
      ]
  pure $ GroupEntry occur variant (ParserXTerm $ cmt <> cmt')

pMemberKey :: Parser (WithComment (MemberKey ParserStage))
pMemberKey =
  choice
    [ try $ do
        t1 <- pType1
        cmt0 <- space
        cmt1 <- fold <$> optcomp ("^" *> space) <* "=>"
        pure $ WithComment (cmt0 <> cmt1) (MKType t1)
    , try $ do
        name <- pName
        cmt <- space
        _ <- ":"
        pure . WithComment cmt $ MKBareword name
    , do
        val <- pValue
        cmt <- space
        _ <- ":"
        pure . WithComment cmt $ MKValue val
    ]

pOptCom :: Parser Comment
pOptCom = space <*! (fold <$> optional ("," >> space))

pOccur :: Parser OccurrenceIndicator
pOccur =
  label "occurrence indicator" $
    choice
      [ char '+' $> OIOneOrMore
      , char '?' $> OIOptional
      , pBounded
      ]

pValue :: Parser Value
pValue =
  label "value" $
    (`Value` mempty)
      <$> choice
        [ try pFloat
        , try pInt
        , try pBytes
        , pText
        , try pBool
        ]
  where
    pSignedNum :: Num a => Parser a -> Parser (Bool, a)
    pSignedNum valParser = do
      sign <- optional "-"
      val <- valParser <* notFollowedBy "*"
      pure (isJust sign, val)
    -- Need to ensure that number values are not actually bounds on a later
    -- value.
    pInt =
      pSignedNum L.decimal >>= \case
        (False, val)
          | val <= fromIntegral (maxBound @Word64) -> pure . VUInt $ fromIntegral val
          | otherwise -> pure $ VBignum val
        (True, val)
          | val <= fromIntegral (maxBound @Word64) -> pure . VNInt $ fromIntegral val
          | otherwise -> pure . VBignum $ -val
    pFloat =
      pSignedNum L.float >>= \case
        (False, val) -> pure $ VFloat64 val
        (True, val) -> pure . VFloat64 $ negate val
    -- Currently this doesn't allow string escaping
    pSChar :: Parser Text
    pSChar = takeWhileP (Just "character") $ \x ->
      or $
        [ charInRange '\x20' '\x21'
        , charInRange '\x23' '\x5b'
        , charInRange '\x5d' '\x7e'
        , charInRange '\x80' '\x10fffd'
        ]
          <*> pure x
    pText = VText <$> ("\"" *> pSChar <* "\"")
    pSByte = takeWhileP (Just "byte character") $ \x ->
      or $
        [ charInRange '\x20' '\x26'
        , charInRange '\x28' '\x5b'
        , charInRange '\x5d' '\x10fffd'
        ]
          <*> pure x
    pBytes = do
      _qualifier <- optional ("h" <|> "b64")
      bytes <- between "'" "'" pSByte
      case Base16.decode $ encodeUtf8 bytes of
        Right x -> pure $ VBytes x
        Left err -> fail err
    pBool = label "boolean" $ do
      b <-
        (True <$ string "true")
          <|> (False <$ string "false")
      pure $ VBool b

pTyOp :: Parser TyOp
pTyOp =
  choice
    [ try $ RangeOp <$> pRangeOp
    , CtrlOp <$> pCtlOp
    ]

pBounded :: Parser OccurrenceIndicator
pBounded = do
  lo <- optional L.decimal
  _ <- char '*'
  hi <- optional L.decimal
  pure $ case (lo, hi) of
    (Nothing, Nothing) -> OIZeroOrMore
    (x, y) -> OIBounded x y

-- | A variant of 'optional' for composite parsers, which will consume no input
-- if it fails.
optcomp :: MonadParsec e s f => f a -> f (Maybe a)
optcomp = optional . try

{-
cddl = S *(rule S)
rule = typename [genericparm] S assignt S type
     / groupname [genericparm] S assigng S grpent

typename = id
groupname = id

assignt = "=" / "/="
assigng = "=" / "//="

genericparm = "<" S id S *("," S id S ) ">"
genericarg = "<" S type1 S *("," S type1 S ) ">"

type = type1 *(S "/" S type1)

type1 = type2 [S (rangeop / ctlop) S type2]
; space may be needed before the operator if type2 ends in a name

type2 = value
      / typename [genericarg]
      / "(" S type S ")"
      / "{" S group S "}"
      / "[" S group S "]"
      / "~" S typename [genericarg]
      / "&" S "(" S group S ")"
      / "&" S groupname [genericarg]
      / "#" "6" ["." head-number] "(" S type S ")"
      / "#" "7" ["." head-number]
      / "#" DIGIT ["." uint]                ; major/ai
      / "#"                                 ; any
head-number = uint / ("<" type ">")

rangeop = "..." / ".."

ctlop = "." id

group = grpchoice *(S "//" S grpchoice)

grpchoice = *(grpent optcom)

grpent = [occur S] [memberkey S] type
       / [occur S] groupname [genericarg]  ; preempted by above
       / [occur S] "(" S group S ")"

memberkey = type1 S ["^" S] "=>"
          / bareword S ":"
          / value S ":"

bareword = id

optcom = S ["," S]

occur = [uint] "*" [uint]
      / "+"
      / "?"

uint = DIGIT1 *DIGIT
     / "0x" 1*HEXDIG
     / "0b" 1*BINDIG
     / "0"

value = number
      / text
      / bytes

int = ["-"] uint

; This is a float if it has fraction or exponent; int otherwise
number = hexfloat / (int ["." fraction] ["e" exponent ])
hexfloat = ["-"] "0x" 1*HEXDIG ["." 1*HEXDIG] "p" exponent
fraction = 1*DIGIT
exponent = ["+"/"-"] 1*DIGIT

text = %x22 *SCHAR %x22
SCHAR = %x20-21 / %x23-5B / %x5D-7E / NONASCII / SESC

SESC = "\" ( %x22 / "/" / "\" /                 ; \" \/ \\
             %x62 / %x66 / %x6E / %x72 / %x74 / ; \b \f \n \r \t
             (%x75 hexchar) )                   ; \uXXXX

hexchar = "{" (1*"0" [ hexscalar ] / hexscalar) "}" /
          non-surrogate / (high-surrogate "\" %x75 low-surrogate)
non-surrogate = ((DIGIT / "A"/"B"/"C" / "E"/"F") 3HEXDIG) /
                ("D" %x30-37 2HEXDIG )
high-surrogate = "D" ("8"/"9"/"A"/"B") 2HEXDIG
low-surrogate = "D" ("C"/"D"/"E"/"F") 2HEXDIG
hexscalar = "10" 4HEXDIG / HEXDIG1 4HEXDIG
          / non-surrogate / 1*3HEXDIG

bytes = [bsqual] %x27 *BCHAR %x27
BCHAR = %x20-26 / %x28-5B / %x5D-7E / NONASCII / SESC / "\'" / CRLF
bsqual = "h" / "b64"

id = EALPHA *(*("-" / ".") (EALPHA / DIGIT))
ALPHA = %x41-5A / %x61-7A
EALPHA = ALPHA / "@" / "_" / "$"
DIGIT = %x30-39
DIGIT1 = %x31-39
HEXDIG = DIGIT / "A" / "B" / "C" / "D" / "E" / "F"
HEXDIG1 = DIGIT1 / "A" / "B" / "C" / "D" / "E" / "F"
BINDIG = %x30-31

S = *WS
WS = SP / NL
SP = %x20
NL = COMMENT / CRLF
COMMENT = ";" *PCHAR CRLF
PCHAR = %x20-7E / NONASCII
NONASCII = %xA0-D7FF / %xE000-10FFFD
CRLF = %x0A / %x0D.0A
-}

-- | Variant on 'NE.sepEndBy1' which doesn't consume the separator
sepBy1' :: MonadParsec e s m => m a -> m sep -> m (NonEmpty a)
sepBy1' p sep = NE.fromList <$> go
  where
    go = liftA2 (:) p (many (try $ sep *> p))