packages feed

c-expr-dsl-0.1.0.0: src/C/Expr/Parse/Expr.hs

{-# LANGUAGE OverloadedRecordDot #-}

module C.Expr.Parse.Expr (parseMacro, parseMacroType) where

import Control.Monad
import Data.Foldable qualified as Foldable
import Data.Functor.Identity
import Data.Text (Text)
import Data.Type.Nat
import Data.Vec.Lazy (Vec (..))
import Data.Vec.Lazy qualified as Vec
import DeBruijn (Idx (..))
import Text.Parsec hiding (parseTest, token)
import Text.Parsec.Expr

import C.Expr.Parse.Identifier
import C.Expr.Parse.Infra
import C.Expr.Parse.Literal
import C.Expr.Syntax

import Clang.CStandard
import Clang.Enum.Simple
import Clang.HighLevel.Types
import Clang.LowLevel.Core

{-------------------------------------------------------------------------------
  Top-level

  Some useful references:

  - Section 6.10 "Preprocessing directives" of the C standard
    <https://fog.misty.com/perry/osp/standard/preproc.pdf>
  - Section 3 "Macros" of the @cpp@ documentation
    <https://gcc.gnu.org/onlinedocs/cpp/Macros.html>
  - "C operator precedence"
    <https://en.cppreference.com/w/c/language/operator_precedence>
-------------------------------------------------------------------------------}

-- | Parse a macro definition (type or value expression)
--
-- Tries to parse the body as a type expression first. A valid type token
-- sequence always produces @'Term' ('Type' …)@; everything else parses as an
-- expression. Only when typechecking macros, we can fully discriminate type and
-- value expressions.
parseMacro :: ClangCStandard -> Parser (Macro ())
parseMacro cStd = do
    (macroLocRange, macroName) <- parseLocIdentifier
    let
        macroLoc :: MultiLoc
        macroLoc = macroLocRange.rangeStart

        functionLike :: Parser (Macro ())
        functionLike = do
          noWhitespace macroLocRange
          paramNames <- formalParams
          Vec.reifyList (reverse paramNames) $ \macroParams -> do
            macroExpr <- bodyExpr macroParams
            pure $ Macro macroLoc macroName (Vec.reverse macroParams) macroExpr

        objectLike :: Parser (Macro ())
        objectLike = do
          macroExpr <- bodyExpr VNil
          pure $ Macro macroLoc macroName VNil macroExpr
    m <- choice [try functionLike, objectLike]
    eof
    return m
  where
    -- Try the body as a type expression first. The @'eof'@ inside the @'try'@
    -- is essential: if @'parseMacroType'@ succeeds on a prefix (e.g. the bare
    -- identifier in @size_t + 1@) but leaves tokens unconsumed, the whole
    -- attempt is abandoned and we fall back to the expression parser.
    bodyExpr :: Vec ctx Identifier -> Parser (Expr ctx (Ps ()))
    bodyExpr macroParams = do
      rejectPragma
      try (parseMacroType cStd macroParams <* eof) <|> exprTuple cStd macroParams

-- | Reject macro bodies that begin with the @_Pragma@ operator
--
-- @_Pragma@ is the C99 preprocessor operator (C standard §6.10.9), equivalent
-- to @#pragma@. A macro whose body uses it, such as
--
-- > #define PACK_START _Pragma("pack(1)")
--
-- expands to a preprocessing directive, not a C value or type expression. We
-- reject such macros here rather than misparsing @_Pragma@ as a reference to a
-- variable of that name, which would otherwise survive parsing and only fail
-- later (during name resolution or typechecking) with a misleading message.
rejectPragma :: Parser ()
rejectPragma =
    notFollowedBy (token isPragma) <?> "C expression (not a _Pragma operator)"
  where
    isPragma :: Token TokenSpelling -> Maybe ()
    isPragma t
      | getTokenSpelling (tokenSpelling t) == "_Pragma" = Just ()
      | otherwise                                       = Nothing

formalParams :: Parser [Identifier]
formalParams = parens $ parseIdentifier `sepBy` comma

lookupParam :: Identifier -> Vec ctx Identifier -> Maybe (Idx ctx)
lookupParam _ VNil    = Nothing
lookupParam n (m ::: vec)
  | n == m    = Just IZ
  | otherwise = IS <$> lookupParam n vec

-- | Check that there is no whitespace between the previous token and the current
-- token
--
-- Function-like macros are only function-like if there is /no/ whitespace
-- between the macro name and the opening parenthesis of the parameter list.
--
-- We used to not check whitespace, which was the source of a bug. See issue
-- #1903: <https://github.com/well-typed/hs-bindgen/issues/1903>
noWhitespace ::
     -- | Source range for the previous token
     Range MultiLoc
  -> Parser ()
noWhitespace prevRange = lookAhead $ do
    tok <- anyToken
    let prev    = prevRange.rangeEnd.multiLocExpansion
        current = tok.tokenExtent.rangeStart.multiLocExpansion
        p       = prev.singleLocPath   == current.singleLocPath &&
                  prev.singleLocLine   == current.singleLocLine &&
                  prev.singleLocColumn == current.singleLocColumn
    unless p $
      parserFail "unexpected whitespace"

{-------------------------------------------------------------------------------
  Types

-------------------------------------------------------------------------------}

-- | Parse a macro body as a C type expression
--
-- Recognizes the following grammar (informally):
--
-- @
--   type           ::= const? type_base const? pointer_layers?
--
--   type_base      ::= sign_specifier? int_size_keyword? 'int'?
--                    | sign_specifier? 'char'
--                    | 'float' | 'double'
--                    | 'void'
--                    | '_Bool' | 'bool'
--                    | ('struct' | 'union' | 'enum') identifier
--                    | identifier
--
--   pointer_layers ::= ('*' const?)+
-- @
--
-- Returns an @'Expr' ctx ('Ps' ())@ where:
--
-- * A keyword base type becomes @'Term' ('Literal' ('TypeLit' …))@.
-- * A tagged base type (e.g. @struct Foo@) becomes @'Term' ('Var' …)@ with a
--   'NameTagged' name; the typechecker resolves it.
-- * A bare identifier that is a local macro parameter becomes @'Term' ('LocalParam' …)@.
-- * A bare identifier that is a free variable becomes @'Term' ('Var' …)@;
--   the typechecker decides whether it names a type or a value.
-- * Each @const@ qualifier wraps the expression in @'TyApp' 'Const'@.
-- * Each @*@ pointer layer wraps the expression in @'TyApp' 'Pointer'@.
parseMacroType :: ClangCStandard -> Vec ctx Identifier -> Parser (Expr ctx (Ps ()))
parseMacroType cStd macroParams = do
    constBefore <- option False (True <$ keyword "const")
    base        <- typeBase cStd macroParams
    constAfter  <- option False (True <$ keyword "const")
    ptrs        <- pointerLayers
    -- In C, @const@ is idempotent: @const int const@ is valid but equivalent
    -- to @const int@. We therefore wrap with at most one 'Const' layer,
    -- regardless of whether the qualifier appeared before or after the base.
    let withConst
          | constBefore || constAfter = TyApp Const (base ::: VNil)
          | otherwise                 = base
    return (Foldable.foldl' apPtr withConst ptrs)
  where
    apPtr acc ptrConst =
      let withPtr = TyApp Pointer (acc ::: VNil)
      in if ptrConst then TyApp Const (withPtr ::: VNil) else withPtr

-- | Base of a type expression (without const/pointer layers)
--
-- Returns:
--
-- * @'Term' ('Literal' ('TypeLit' …))@ for keyword base types.
-- * @'Term' ('Var' …)@ with a 'NameTagged' name for a tagged base type (e.g. @struct Foo@).
-- * @'Term' ('LocalParam' …)@ for a bare identifier that is a local macro parameter.
-- * @'Term' ('Var' …)@ for any other bare identifier; the typechecker decides
--   whether it names a type or a value.
typeBase :: forall ctx. ClangCStandard -> Vec ctx Identifier -> Parser (Expr ctx (Ps ()))
typeBase cStd macroParams =
    choice [
        -- Type literal.
        Term . Literal . TypeLit <$> typeLiteral cStd
        -- Tagged type (e.g., @struct Foo@)
      , Term . mkTagged <$> taggedTypeLit
        -- The bare identifier (typedef name, type macro, or expression
        -- variable) is needed to parse pointer-qualified typedef references
        -- such as @size_t *@: without it, @parseMacroType@ would reject the
        -- identifier base and the expression parser would then fail on @*@ (a
        -- binary operator without a right-hand side). Attempting to detangle
        -- the two by restricting @parseMacroType@ to keyword\/tagged bases only
        -- does not help (both paths produce the same @'Var'@ node) while
        -- adding backtracking overhead.
      , Term . mkVar <$> parseIdentifier
      ]
  where
    mkTagged :: (TagKind, Identifier) -> Term ctx (Ps ())
    mkTagged (tag, ident) = Var (XVarPs ()) (NameTagged ident tag) []

    mkVar :: Identifier -> Term ctx (Ps ())
    mkVar n = case lookupParam n macroParams of
      Just i  -> LocalParam i
      Nothing -> Var (XVarPs ()) (NameOrdinary n) []

-- | Parse a sequence of type-literal keywords and combine them
--
-- C type literal keywords can appear in various orders:
--
-- @
--   unsigned long int
--   long unsigned int
--   int long unsigned
-- @
--
-- are all the same type.
typeLiteral :: ClangCStandard -> Parser TypeLit
typeLiteral cStd = do
    kws <- many1 (typeKeyword cStd)
    case interpretKeywords kws of
      Just lit -> return lit
      Nothing  -> fail "unrecognised type literal"

-- | Parse an elaborated type literal
--
-- @
--   struct tag
--   union  tag
--   enum   tag
-- @
taggedTypeLit :: Parser (TagKind, Identifier)
taggedTypeLit = do
    tag <- choice [
        TagStruct <$ keyword "struct"
      , TagUnion  <$ keyword "union"
      , TagEnum   <$ keyword "enum"
      ]
    name <- parseIdentifier
    return (tag, name)

data TypeKeyword =
    KwSigned | KwUnsigned
  | KwShort | KwInt | KwLong | KwChar
  | KwFloat | KwDouble
  | KwVoid | KwBool
  deriving stock (Eq)

typeKeyword :: ClangCStandard -> Parser TypeKeyword
typeKeyword cStd = choice $
      [ KwSigned   <$ keyword "signed"
      , KwUnsigned <$ keyword "unsigned"
      , KwShort    <$ keyword "short"
      , KwInt      <$ keyword "int"
      , KwLong     <$ keyword "long"
      , KwChar     <$ keyword "char"
      , KwFloat    <$ keyword "float"
      , KwDouble   <$ keyword "double"
      , KwVoid     <$ keyword "void"
      , KwBool     <$ keyword "_Bool"
      ]
      ++
      -- @bool@ is a keyword in C23 and later.
      case cStd of
        ClangCStandard std _ | std >= C23 -> [bool]
        _                                 -> []
  where
    bool = KwBool <$ keyword "bool"

-- | Combine a list of type keywords into a type literal
--
-- Returns 'Nothing' if the combination is invalid.
--
-- Duplicate keywords (e.g. @signed signed int@) are accepted, since input
-- tokens come from libclang-validated C source and such constructs are
-- rejected by the C compiler long before we see them.
interpretKeywords :: [TypeKeyword] -> Maybe TypeLit
interpretKeywords kws
  -- void
  | kws == [KwVoid]
  = Just TypeVoid

  -- _Bool / bool
  | kws == [KwBool]
  = Just TypeBool

  -- float
  | kws == [KwFloat]
  = Just $ TypeFloat SizeFloat

  -- double
  | kws == [KwDouble]
  = Just $ TypeFloat SizeDouble

  -- char with optional sign
  | KwChar `elem` kws
  , let sign = extractSign kws
  , all (\k -> k `elem` [KwChar, KwSigned, KwUnsigned]) kws
  = Just $ TypeChar sign

  -- integral types: combinations of sign, size, and int
  | all (\k -> k `elem` [KwSigned, KwUnsigned, KwShort, KwInt, KwLong]) kws
  = Just $ TypeInt (extractSign kws) (extractIntSize kws)

  | otherwise
  = Nothing

extractSign :: [TypeKeyword] -> Maybe Sign
extractSign kws
  | KwSigned   `elem` kws = Just Signed
  | KwUnsigned `elem` kws = Just Unsigned
  | otherwise              = Nothing

extractIntSize :: [TypeKeyword] -> Maybe IntSize
extractIntSize kws
  | KwShort `elem` kws                   = Just SizeShort
  | length (filter (== KwLong) kws) >= 2 = Just SizeLongLong
  | KwLong `elem` kws                    = Just SizeLong
  | KwInt `elem` kws                     = Just SizeInt
  | otherwise                            = Nothing
  -- NB: @signed@ alone (no size keyword, no @int@) means @signed int@.
  -- We return Nothing here; the caller interprets (Just sign, Nothing) as int.

-- | Parse zero or more pointer indirections, optionally followed by @const@
pointerLayers :: Parser [Bool]
pointerLayers = many pointerLayer

-- | Parse a pointer indirection, optionally followed by @const@
pointerLayer :: Parser Bool
pointerLayer = do
    punctuation "*"
    option False (True <$ keyword "const")

-- | Match a keyword token with the given spelling
keyword :: Text -> Parser ()
keyword expected = token $ \t ->
    if fromSimpleEnum (tokenKind t) == Right CXToken_Keyword
       && getTokenSpelling (tokenSpelling t) == expected
      then Just ()
      else Nothing

{-------------------------------------------------------------------------------
  Simple expressions
-------------------------------------------------------------------------------}

term :: forall ctx. ClangCStandard -> Vec ctx Identifier -> Parser (Term ctx (Ps ()))
term cStd macroParams =
    buildExpressionParser ops trm <?> "simple expression"
  where
    trm :: Parser (Term ctx (Ps ()))
    trm = choice [
        Literal <$> lit
      , localParamOrVar
      ]

    localParamOrVar :: Parser (Term ctx (Ps ()))
    localParamOrVar = do
      varName <- parseIdentifier
      case lookupParam varName macroParams of
        Just i ->
          pure $ LocalParam i
        Nothing ->
          Var (XVarPs ()) (NameOrdinary varName) <$>
            option [] (actualArgs cStd macroParams)

    lit :: Parser Literal
    lit = ValueLit <$> choice [
        ValueInt    <$> literalInteger
      , ValueFloat  <$> literalFloat
      , ValueChar   <$> literalChar
      , ValueString <$> literalString
      ]

    ops :: OperatorTable [Token TokenSpelling] () Identity (Term ctx (Ps ()))
    ops = []


-- | Parse integer literal
literalInteger :: Parser IntegerLiteral
literalInteger = do
  (val, ty) <- parseTokenOfKind CXToken_Literal parseLiteralInteger
  return $
    IntegerLiteral
      { integerLiteralType  = ty
      , integerLiteralValue = val
      }

-- | Parse floating point literal
literalFloat :: Parser FloatingLiteral
literalFloat = do
  (fltVal, dblVal, ty) <- parseTokenOfKind CXToken_Literal parseLiteralFloating
  return $
    FloatingLiteral
      { floatingLiteralType = ty
      , floatingLiteralFloatValue = fltVal
      , floatingLiteralDoubleValue = dblVal
      }

-- | Parse character literal
literalChar :: Parser CharLiteral
literalChar = do
  val <- parseTokenOfKind CXToken_Literal parseLiteralChar
  return $ CharLiteral val

-- | Parse string literal
literalString :: Parser StringLiteral
literalString = do
  val <- parseTokenOfKind CXToken_Literal parseLiteralString
  return $ StringLiteral val

actualArgs :: ClangCStandard -> Vec ctx Identifier -> Parser [Expr ctx (Ps ())]
actualArgs cStd macroParams = parens $ expr cStd macroParams `sepBy` comma

{-------------------------------------------------------------------------------
  Expressions

  This is currently only a subset of the operators described in
  <https://en.cppreference.com/w/c/language/operator_precedence>, but we do
  follow the same structure.
-------------------------------------------------------------------------------}

exprTuple :: ClangCStandard -> Vec ctx Identifier -> Parser (Expr ctx (Ps ()))
exprTuple cStd macroParams = try tuple <|> expr cStd macroParams
  where
    tuple = do
      openParen <- optionMaybe $ punctuation "("
      (e1, e2, es) <- expr cStd macroParams `sepBy2` comma
      case openParen of
        Nothing -> return ()
        Just {} -> punctuation ")"
      return $
        Vec.reifyList es $ \es' ->
           VaApp NoXApp MTuple ( e1 ::: e2 ::: es' )

expr :: forall ctx. ClangCStandard -> Vec ctx Identifier -> Parser (Expr ctx (Ps ()))
expr cStd macroParams = buildExpressionParser ops trm <?> "expression"
  where

    trm :: Parser (Expr ctx (Ps ()))
    trm = choice [
          parens (expr cStd macroParams)
        , Term <$> term cStd macroParams
        ]

    -- 'OperatorTable' expects the list in descending precedence
    ops = [
        -- Precedence 1 (all left-to-right)
        []

        -- Precedence 2 (all right-to-left)
      , [ Prefix (ap1 MUnaryPlus  <$ punctuation "+")
        , Prefix (ap1 MUnaryMinus <$ punctuation "-")
        , Prefix (ap1 MLogicalNot <$ punctuation "!")
        , Prefix (ap1 MBitwiseNot <$ punctuation "~")
        ]

        -- Precedence 3 (precedence 3 .. 12 are all left-to-right)
      , [ Infix (ap2 MMult <$ punctuation "*") AssocLeft
        , Infix (ap2 MDiv  <$ punctuation "/") AssocLeft
        , Infix (ap2 MRem  <$ punctuation "%") AssocLeft
        ]

        -- Precedence 4
      , [ Infix (ap2 MAdd <$ punctuation "+") AssocLeft
        , Infix (ap2 MSub <$ punctuation "-") AssocLeft
        ]

        -- Precedence 5
      , [ Infix (ap2 MShiftLeft  <$ punctuation "<<") AssocLeft
        , Infix (ap2 MShiftRight <$ punctuation ">>") AssocLeft
        ]

        -- Precedence 6
      , [ Infix (ap2 MRelLT <$ punctuation "<")  AssocLeft
        , Infix (ap2 MRelLE <$ punctuation "<=") AssocLeft
        , Infix (ap2 MRelGT <$ punctuation ">")  AssocLeft
        , Infix (ap2 MRelGE <$ punctuation ">=") AssocLeft
        ]

        -- Precedence 7
      , [ Infix (ap2 MRelEQ <$ punctuation "==") AssocLeft
        , Infix (ap2 MRelNE <$ punctuation "!=") AssocLeft
        ]

        -- Precedence 8 .. 12
      , [ Infix (ap2 MBitwiseAnd <$ punctuation "&")  AssocLeft ]
      , [ Infix (ap2 MBitwiseXor <$ punctuation "^")  AssocLeft ]
      , [ Infix (ap2 MBitwiseOr  <$ punctuation "|")  AssocLeft ]
      , [ Infix (ap2 MLogicalAnd <$ punctuation "&&") AssocLeft ]
      , [ Infix (ap2 MLogicalOr  <$ punctuation "||") AssocLeft ]
      ]

    ap1 :: VaFun (S Z) -> Expr ctx (Ps ()) -> Expr ctx (Ps ())
    ap1 op arg = VaApp NoXApp op ( arg ::: VNil )

    ap2 :: VaFun (S (S Z)) -> Expr ctx (Ps ()) -> Expr ctx (Ps ()) -> Expr ctx (Ps ())
    ap2 op arg1 arg2 = VaApp NoXApp op ( arg1 ::: arg2 ::: VNil )

sepBy2 :: ParsecT s u m a -> ParsecT s u m sep -> ParsecT s u m (a, a, [a])
{-# INLINEABLE sepBy2 #-}
sepBy2 p sep = do
  x1 <- p
  void sep
  x2 <- p
  xs <- many $ sep >> p
  return (x1, x2, xs)