c-expr-dsl-0.1.0.0: test/Test/CExpr/Parse/Macro.hs
{-# LANGUAGE CPP #-}
#if __GLASGOW_HASKELL__ >=908
{-# LANGUAGE TypeAbstractions #-}
#endif
-- | Unit tests for 'C.Expr.Parse.Expr.parseMacro'
--
-- Tests the full macro parser, focusing on:
--
-- * Type bodies vs expression bodies (disambiguation)
-- * Object-like and function-like expression macros
module Test.CExpr.Parse.Macro (tests) where
import Data.Either (isLeft, isRight)
import Data.Nat (Nat (..))
import Data.Type.Equality ((:~:) (..))
import Data.Type.Nat qualified as Nat
import Data.Vec.Lazy (Vec (..))
import Data.Vec.Lazy qualified as Vec
import DeBruijn (Idx (..))
import Test.Tasty
import Test.Tasty.HUnit
import C.Expr.Syntax
import Clang.CStandard
import Clang.HighLevel.Types
import Test.CExpr.Parse.Infra
import Test.CExpr.Typecheck.Infra (mtagged, mvar)
{-------------------------------------------------------------------------------
Top-level
-------------------------------------------------------------------------------}
tests :: TestTree
tests = testGroup "Parse.Macro" [
testsWithCStd cStd | cStd <- [minBound .. maxBound :: CStandard]
]
testsWithCStd :: CStandard -> TestTree
testsWithCStd cStd = testGroup (show cStd) [
testGroup "type bodies" $ tests_typeBody std
, testGroup "function-like type bodies" $ tests_funcLikeTypeBody std
, testGroup "expression bodies" $ tests_exprBody std
, testGroup "disambiguation" $ tests_disambiguation std
]
where
std = ClangCStandard cStd DisableGnu
{-------------------------------------------------------------------------------
Helpers
-------------------------------------------------------------------------------}
-- | A fixed macro name token used in all tests
macroNameTok :: Token TokenSpelling
macroNameTok = ident "FOO"
-- | True when the macro expression looks like a type: it has an 'Type' or
-- 'TyApp' at its core (bare identifier cases are intentionally excluded
-- because a bare name is structurally identical in both type and expression
-- position after the refactor).
isTypeBody :: Either e (Macro ann) -> Bool
isTypeBody (Right Macro{macroExpr}) = case macroExpr of
Term (Literal (TypeLit _)) -> True
TyApp {} -> True
_ -> False
isTypeBody _ = False
-- | True when the macro expression is unambiguously an expression (a literal
-- or an operator application), not a type.
isExprBody :: Either e (Macro ann) -> Bool
isExprBody (Right Macro{macroExpr}) = case macroExpr of
Term (Literal (ValueLit (ValueInt _))) -> True
Term (Literal (ValueLit (ValueFloat _))) -> True
Term (Literal (ValueLit (ValueChar _))) -> True
Term (Literal (ValueLit (ValueString _))) -> True
VaApp {} -> True
_ -> False
isExprBody _ = False
getMacroExpr ::
forall e ctx ann. Nat.SNatI ctx
=> Either e (Macro ann)
-> Maybe (Expr ctx (Ps ann))
getMacroExpr (Right (Macro @_ @ctx1 _ _ macroParams macroExpr)) =
Vec.withDict macroParams $
case Nat.eqNat @ctx @ctx1 of
Just Refl -> Just macroExpr
Nothing -> Nothing
getMacroExpr _ =
Nothing
-- | Extract the expression body from an object-like (0-arg) macro.
getObjExpr :: forall e ann. Either e (Macro ann) -> Maybe (Expr Z (Ps ann))
getObjExpr = getMacroExpr
-- | Extract the expression body from a function-like macro with one parameter.
getFn1Expr :: forall e ann. Either e (Macro ann) -> Maybe (Expr (S Z) (Ps ann))
getFn1Expr = getMacroExpr
{-------------------------------------------------------------------------------
Type bodies
-------------------------------------------------------------------------------}
tests_typeBody :: ClangCStandard -> [TestTree]
tests_typeBody cStd = [
testCase "int" $
-- #define FOO int
getObjExpr (checkMacro cStd [macroNameTok, kw "int"])
@?= Just (tyLit (TypeInt Nothing (Just SizeInt)))
, testCase "unsigned long" $
-- #define FOO unsigned long
getObjExpr (checkMacro cStd [macroNameTok, kw "unsigned", kw "long"])
@?= Just (tyLit (TypeInt (Just Unsigned) (Just SizeLong)))
, testCase "const int*" $
-- #define FOO const int *
getObjExpr (checkMacro cStd [macroNameTok, kw "const", kw "int", punc "*"])
@?= Just (TyApp Pointer (TyApp Const (tyLit (TypeInt Nothing (Just SizeInt)) ::: VNil) ::: VNil))
, testCase "void*" $
-- #define FOO void *
getObjExpr (checkMacro cStd [macroNameTok, kw "void", punc "*"])
@?= Just (TyApp Pointer (tyLit TypeVoid ::: VNil))
, testCase "struct Foo" $
-- #define FOO struct Foo
getObjExpr (checkMacro cStd [macroNameTok, kw "struct", ident "Foo"])
@?= Just (mtagged "Foo" TagStruct)
, testCase "size_t" $
-- #define FOO size_t (bare identifier; typechecker decides it's a type)
getObjExpr (checkMacro cStd [macroNameTok, ident "size_t"])
@?= Just (mvar "size_t")
, testCase "_Bool" $
-- #define FOO _Bool
getObjExpr (checkMacro cStd [macroNameTok, kw "_Bool"])
@?= Just (tyLit TypeBool)
, testCase "_Bool" $
-- #define FOO size_t const * const
getObjExpr (checkMacro cStd [macroNameTok, ident "size_t", kw "const", punc "*", kw "const" ])
@?= Just (TyApp Const (TyApp Pointer (TyApp Const (mvar "size_t" ::: VNil) ::: VNil) ::: VNil))
]
{-------------------------------------------------------------------------------
Function-like type bodies (local args)
-------------------------------------------------------------------------------}
tests_funcLikeTypeBody :: ClangCStandard -> [TestTree]
tests_funcLikeTypeBody cStd = [
testCase "PTR(T) = T*" $
-- #define PTR(T) T*
-- T is a local arg; the body is a pointer type parameterised by T.
getFn1Expr (checkMacro cStd
[ macroNameTok, punc "(", ident "T", punc ")"
, ident "T", punc "*"
])
@?= Just (TyApp Pointer (Term (LocalParam IZ) ::: VNil))
, testCase "CONST_PTR(T) = const T*" $
-- #define CONST_PTR(T) const T*
getFn1Expr (checkMacro cStd
[ macroNameTok, punc "(", ident "T", punc ")"
, kw "const", ident "T", punc "*"
])
@?= Just (TyApp Pointer (TyApp Const (Term (LocalParam IZ) ::: VNil) ::: VNil))
, testCase "free var is not a local arg" $
-- #define PTR(T) size_t*
-- size_t is not a formal parameter, so it stays as Var, not LocalParam.
getFn1Expr (checkMacro cStd
[ macroNameTok, punc "(", ident "T", punc ")"
, ident "size_t", punc "*"
])
@?= Just (TyApp Pointer (mvar "size_t" ::: VNil))
]
{-------------------------------------------------------------------------------
Expression bodies
-------------------------------------------------------------------------------}
tests_exprBody :: ClangCStandard -> [TestTree]
tests_exprBody cStd = [
-- Object-like macros
testCase "integer literal" $
-- #define FOO 42
assertBool "expected expression body" $
isExprBody (checkMacro cStd [macroNameTok, lit "42"])
, testCase "negative literal" $
-- #define FOO -1
assertBool "expected expression body" $
isExprBody (checkMacro cStd [macroNameTok, punc "-", lit "1"])
, testCase "arithmetic expression" $
-- #define FOO 1 + 2
assertBool "expected expression body" $
isExprBody (checkMacro cStd [macroNameTok, lit "1", punc "+", lit "2"])
-- Function-like macros
-- A bare identifier body (e.g. x) is structurally identical for type and
-- expression positions after the Expr unification; we just check it parses.
, testCase "identity function" $
-- #define FOO(x) x
assertBool "expected parse success" $
isRight $
checkMacro cStd [
macroNameTok, punc "(", ident "x", punc ")"
, ident "x"
]
, testCase "two-argument function" $
-- #define FOO(a, b) a + b
assertBool "expected expression body" $
isExprBody $
checkMacro cStd [
macroNameTok
, punc "(", ident "a", punc ",", ident "b", punc ")"
, ident "a", punc "+", ident "b"
]
-- Zero-argument function-like macro (#define FOO() 0) is
-- parsed as objectLike since empty parens are not valid formalArgs;
-- the result is still an expression body
]
{-------------------------------------------------------------------------------
Disambiguation: types vs. expressions
-------------------------------------------------------------------------------}
tests_disambiguation :: ClangCStandard -> [TestTree]
tests_disambiguation cStd = [
-- A bare identifier like 'size_t' is now structurally identical whether
-- it came from the type parser or the expression parser (both produce
-- Term (Var ...)). We just verify that parsing succeeds.
testCase "bare name parses successfully" $
-- #define FOO size_t
assertBool "expected parse success" $
isRight (checkMacro cStd [macroNameTok, ident "size_t"])
, testCase "void is a type body, not an identifier expression" $
-- #define FOO void
assertBool "expected type body" $
isTypeBody (checkMacro cStd [macroNameTok, kw "void"])
-- An integer literal cannot be a type, so it falls through to expression.
, testCase "literal falls through to expression" $
-- #define FOO 0
assertBool "expected expression body" $
isExprBody (checkMacro cStd [macroNameTok, lit "0"])
-- An expression that starts with parenthesised identifiers could look
-- like formal arguments.
, testCase "parenthesised expression is not a type" $
-- #define FOO (1)
assertBool "expected expression body" $
isExprBody (checkMacro cStd [macroNameTok, punc "(", lit "1", punc ")"])
-- Completely unparseable input
, testCase "bare comma fails" $
-- #define FOO ,
assertBool "expected failure" $
isLeft (checkMacro cStd [macroNameTok, punc ","])
, testCase "empty body fails" $
-- #define FOO
assertBool "expected failure" $
isLeft (checkMacro cStd [macroNameTok])
]