packages feed

c-expr-dsl-0.1.0.0: test/Test/CExpr/Parse/Type.hs

-- | Unit tests for 'C.Expr.Parse.Type.parseMacroType'
module Test.CExpr.Parse.Type (tests) where

import Data.Either (isLeft)
import Data.Vec.Lazy (Vec (..))
import Test.Tasty
import Test.Tasty.HUnit

import C.Expr.Syntax

import Clang.CStandard

import Test.CExpr.Parse.Infra
import Test.CExpr.Typecheck.Infra (mtagged, mvar)

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

tests :: TestTree
tests = testGroup "Parse.Type" [
      testWithCStd cStd | cStd <- [minBound .. maxBound :: CStandard]
    ]

testWithCStd :: CStandard -> TestTree
testWithCStd cStd = testGroup (show cStd) [
      testGroup "void and bool"       $ tests_voidBool     std
    , testGroup "integer types"       $ tests_int          std
    , testGroup "char"                $ tests_char         std
    , testGroup "float and double"    $ tests_float        std
    , testGroup "named types"         $ tests_named        std
    , testGroup "tagged types"        $ tests_tagged       std
    , testGroup "const qualifier"     $ tests_const        std
    , testGroup "pointer indirection" $ tests_pointer      std
    , testGroup "combined"            $ tests_combined     std
    , testGroup "keyword order"       $ tests_keywordOrder std
    , testGroup "failures"            $ tests_failures     std
    ]
  where
    std = ClangCStandard cStd DisableGnu

{-------------------------------------------------------------------------------
  void and bool
-------------------------------------------------------------------------------}

tests_voidBool :: ClangCStandard -> [TestTree]
tests_voidBool cStd = [
      testCase "void" $
        -- void
        checkType cStd [kw "void"]
          @?= Right (tyLit TypeVoid)
    , testCase "_Bool" $
        -- _Bool
        checkType cStd [kw "_Bool"]
          @?= Right (tyLit TypeBool)
      -- 'bool' as CXToken_Keyword (Clang >= 16)
    , testCase "bool (keyword)" $
        -- bool
        let res = checkType cStd [kw "bool"]
        in  case cStd of
              ClangCStandard std _ | std >= C23 ->
                res @?= Right (tyLit TypeBool)
              _ ->
                assertBool "bool not a kw" $ isLeft res
      -- 'bool' as CXToken_Identifier (older Clang): treated as a named type
    , testCase "bool (identifier)" $
        -- bool
        checkType cStd [ident "bool"]
          @?= Right (mvar "bool")
    ]

{-------------------------------------------------------------------------------
  Integer types
-------------------------------------------------------------------------------}

tests_int :: ClangCStandard -> [TestTree]
tests_int cStd = [
      testCase "int" $
        -- int
        checkType cStd  [kw "int"]
          @?= Right (tyLit (TypeInt Nothing (Just SizeInt)))
    , testCase "signed" $
        -- signed
        checkType cStd [kw "signed"]
          @?= Right (tyLit (TypeInt (Just Signed) Nothing))
    , testCase "unsigned" $
        -- unsigned
        checkType cStd [kw "unsigned"]
          @?= Right (tyLit (TypeInt (Just Unsigned) Nothing))
    , testCase "short" $
        -- short
        checkType cStd [kw "short"]
          @?= Right (tyLit (TypeInt Nothing (Just SizeShort)))
    , testCase "long" $
        -- long
        checkType cStd [kw "long"]
          @?= Right (tyLit (TypeInt Nothing (Just SizeLong)))
    , testCase "long long" $
        -- long long
        checkType cStd [kw "long", kw "long"]
          @?= Right (tyLit (TypeInt Nothing (Just SizeLongLong)))
    , testCase "unsigned int" $
        -- unsigned int
        checkType cStd [kw "unsigned", kw "int"]
          @?= Right (tyLit (TypeInt (Just Unsigned) (Just SizeInt)))
    , testCase "signed int" $
        -- signed int
        checkType cStd [kw "signed", kw "int"]
          @?= Right (tyLit (TypeInt (Just Signed) (Just SizeInt)))
    , testCase "unsigned short" $
        -- unsigned short
        checkType cStd [kw "unsigned", kw "short"]
          @?= Right (tyLit (TypeInt (Just Unsigned) (Just SizeShort)))
    , testCase "unsigned long" $
        -- unsigned long
        checkType cStd [kw "unsigned", kw "long"]
          @?= Right (tyLit (TypeInt (Just Unsigned) (Just SizeLong)))
    , testCase "unsigned long long" $
        -- unsigned long long
        checkType cStd [kw "unsigned", kw "long", kw "long"]
          @?= Right (tyLit (TypeInt (Just Unsigned) (Just SizeLongLong)))
    , testCase "long long int" $
        -- long long int
        checkType cStd [kw "long", kw "long", kw "int"]
          @?= Right (tyLit (TypeInt Nothing (Just SizeLongLong)))
    ]

{-------------------------------------------------------------------------------
  Char
-------------------------------------------------------------------------------}

tests_char :: ClangCStandard -> [TestTree]
tests_char cStd = [
      testCase "char" $
        -- char
        checkType cStd [kw "char"]
          @?= Right (tyLit (TypeChar Nothing))
    , testCase "signed char" $
        -- signed char
        checkType cStd [kw "signed", kw "char"]
          @?= Right (tyLit (TypeChar (Just Signed)))
    , testCase "unsigned char" $
        -- unsigned char
        checkType cStd [kw "unsigned", kw "char"]
          @?= Right (tyLit (TypeChar (Just Unsigned)))
    ]

{-------------------------------------------------------------------------------
  Float and double
-------------------------------------------------------------------------------}

tests_float :: ClangCStandard -> [TestTree]
tests_float cStd = [
      testCase "float" $
        -- float
        checkType cStd [kw "float"]
          @?= Right (tyLit (TypeFloat SizeFloat))
    , testCase "double" $
        -- double
        checkType cStd [kw "double"]
          @?= Right (tyLit (TypeFloat SizeDouble))
    ]

{-------------------------------------------------------------------------------
  Named types (identifiers)
-------------------------------------------------------------------------------}

-- After parsing, we cannot tell if these are types or value expressions.
-- However, they are parsed by the type branch of the parser ('parseMacroType').

tests_named :: ClangCStandard -> [TestTree]
tests_named cStd = [
      testCase "size_t" $
        -- size_t
        checkType cStd [ident "size_t"]
          @?= Right (mvar "size_t")
    , testCase "uint32_t" $
        -- uint32_t
        checkType cStd [ident "uint32_t"]
          @?= Right (mvar "uint32_t")
      -- An identifier token spelled "int" is treated as a named type,
      -- not as the built-in int keyword (libclang always tokenizes keywords
      -- as CXToken_Keyword, so this case is mainly for documentation)
    , testCase "int as identifier" $
        -- int  (tokenised as CXToken_Identifier, not CXToken_Keyword)
        checkType cStd [ident "int"]
          @?= Right (mvar "int")
    ]

{-------------------------------------------------------------------------------
  Tagged types
-------------------------------------------------------------------------------}

tests_tagged :: ClangCStandard -> [TestTree]
tests_tagged cStd = [
      testCase "struct Foo" $
        -- struct Foo
        checkType cStd [kw "struct", ident "Foo"]
          @?= Right (mtagged "Foo" TagStruct)
    , testCase "union Bar" $
        -- union Bar
        checkType cStd [kw "union", ident "Bar"]
          @?= Right (mtagged "Bar" TagUnion)
    , testCase "enum Baz" $
        -- enum Baz
        checkType cStd [kw "enum", ident "Baz"]
          @?= Right (mtagged "Baz" TagEnum)
    ]

{-------------------------------------------------------------------------------
  Const qualifier
-------------------------------------------------------------------------------}

tests_const :: ClangCStandard -> [TestTree]
tests_const cStd = [
      testCase "const int (leading)" $
        -- const int
        checkType cStd [kw "const", kw "int"]
          @?= Right (TyApp Const (tyLit (TypeInt Nothing (Just SizeInt)) ::: VNil))
    , testCase "int const (trailing)" $
        -- int const
        checkType cStd [kw "int", kw "const"]
          @?= Right (TyApp Const (tyLit (TypeInt Nothing (Just SizeInt)) ::: VNil))
    , testCase "const void" $
        -- const void
        checkType cStd [kw "const", kw "void"]
          @?= Right (TyApp Const (tyLit TypeVoid ::: VNil))
    , testCase "const size_t" $
        -- const size_t
        checkType cStd [kw "const", ident "size_t"]
          @?= Right (TyApp Const (mvar "size_t" ::: VNil))
    , testCase "const struct Foo" $
        -- const struct Foo
        checkType cStd [kw "const", kw "struct", ident "Foo"]
          @?= Right (TyApp Const (mtagged "Foo" TagStruct ::: VNil))
    ]

{-------------------------------------------------------------------------------
  Pointer indirection
-------------------------------------------------------------------------------}

tests_pointer :: ClangCStandard -> [TestTree]
tests_pointer cStd = [
      testCase "int*" $
        -- int *
        checkType cStd [kw "int", punc "*"]
          @?= Right (TyApp Pointer (tyLit (TypeInt Nothing (Just SizeInt)) ::: VNil))
    , testCase "int**" $
        -- int **
        checkType cStd [kw "int", punc "*", punc "*"]
          @?= Right (TyApp Pointer (TyApp Pointer (tyLit (TypeInt Nothing (Just SizeInt)) ::: VNil) ::: VNil))
    , testCase "void*" $
        -- void *
        checkType cStd [kw "void", punc "*"]
          @?= Right (TyApp Pointer (tyLit TypeVoid ::: VNil))
    , testCase "size_t*" $
        -- size_t *
        checkType cStd [ident "size_t", punc "*"]
          @?= Right (TyApp Pointer (mvar "size_t" ::: VNil))
    , testCase "struct Foo*" $
        -- struct Foo *
        checkType cStd [kw "struct", ident "Foo", punc "*"]
          @?= Right (TyApp Pointer (mtagged "Foo" TagStruct ::: VNil))
    ]

{-------------------------------------------------------------------------------
  Combined complex types
-------------------------------------------------------------------------------}

tests_combined :: ClangCStandard -> [TestTree]
tests_combined cStd = [
      testCase "const int * (pointer to const int, const left-hand side)" $
        checkType cStd [kw "const", kw "int", punc "*"]
          @?= Right (TyApp Pointer (TyApp Const (tyLit (TypeInt Nothing (Just SizeInt)) ::: VNil) ::: VNil))
    , testCase "int const * (pointer to const int, const right-hand side)" $
        checkType cStd [kw "int", kw "const", punc "*"]
          @?= Right (TyApp Pointer (TyApp Const (tyLit (TypeInt Nothing (Just SizeInt)) ::: VNil) ::: VNil))
    , testCase "int * const (const pointer to int)" $
        checkType cStd [kw "int", punc "*", kw "const"]
          @?= Right (TyApp Const (TyApp Pointer (tyLit (TypeInt Nothing (Just SizeInt)) ::: VNil) ::: VNil))
    , testCase "const int * const (const pointer to const int, const left-hand side)" $
        checkType cStd [kw "const", kw "int", punc "*", kw "const"]
          @?= Right (TyApp Const (TyApp Pointer (TyApp Const (tyLit (TypeInt Nothing (Just SizeInt)) ::: VNil) ::: VNil) ::: VNil))
    , testCase "int const * const (const pointer to const int, const right-hand side)" $
        checkType cStd [kw "int", kw "const", punc "*", kw "const"]
          @?= Right (TyApp Const (TyApp Pointer (TyApp Const (tyLit (TypeInt Nothing (Just SizeInt)) ::: VNil) ::: VNil) ::: VNil))
    , testCase "const unsigned long*" $
        -- const unsigned long *
        checkType cStd [kw "const", kw "unsigned", kw "long", punc "*"]
          @?= Right (TyApp Pointer (TyApp Const (tyLit (TypeInt (Just Unsigned) (Just SizeLong)) ::: VNil) ::: VNil))
    , testCase "const struct Foo**" $
        -- const struct Foo **
        checkType cStd [kw "const", kw "struct", ident "Foo", punc "*", punc "*"]
          @?= Right (TyApp Pointer (TyApp Pointer (TyApp Const (mtagged "Foo" TagStruct ::: VNil) ::: VNil) ::: VNil))
    , testCase "unsigned long long int" $
        -- unsigned long long int
        checkType cStd [kw "unsigned", kw "long", kw "long", kw "int"]
          @?= Right (tyLit (TypeInt (Just Unsigned) (Just SizeLongLong)))
    ]

{-------------------------------------------------------------------------------
  Keyword order independence
-------------------------------------------------------------------------------}

tests_keywordOrder :: ClangCStandard -> [TestTree]
tests_keywordOrder cStd = [
      -- C allows type specifier keywords in any order
      testCase "long unsigned == unsigned long" $
        checkType cStd [kw "long", kw "unsigned"]
          @?= checkType cStd [kw "unsigned", kw "long"]
    , testCase "int unsigned == unsigned int" $
        checkType cStd [kw "int", kw "unsigned"]
          @?= checkType cStd [kw "unsigned", kw "int"]
    , testCase "int long unsigned == unsigned long int" $
        checkType cStd [kw "int", kw "long", kw "unsigned"]
          @?= checkType cStd [kw "unsigned", kw "long", kw "int"]
    ]

{-------------------------------------------------------------------------------
  Failure cases
-------------------------------------------------------------------------------}

tests_failures :: ClangCStandard -> [TestTree]
tests_failures cStd = [
      testCase "bare punctuation" $
        -- *  (no preceding type specifier)
        assertBool "expected failure" $ isLeft (checkType cStd [punc "*"])
    , testCase "struct without name" $
        -- struct  (tag keyword not followed by a name)
        assertBool "expected failure" $ isLeft (checkType cStd [kw "struct"])
    , testCase "void void" $
        -- void void  (duplicate specifier)
        assertBool "expected failure" $ isLeft (checkType cStd [kw "void", kw "void"])
    , testCase "float double" $
        -- float double  (conflicting float specifiers)
        assertBool "expected failure" $ isLeft (checkType cStd [kw "float", kw "double"])
    , testCase "int char" $
        -- int char  (conflicting specifiers)
        assertBool "expected failure" $ isLeft (checkType cStd [kw "int", kw "char"])
    , testCase "literal token" $
        -- 42  (integer literal is not a type token)
        assertBool "expected failure" $ isLeft (checkType cStd [lit "42"])
    , testCase "trailing tokens rejected" $
        -- int * extra  (parseMacroType alone would succeed on [int, *], but
        -- checkType adds eof so the trailing identifier causes failure)
        assertBool "expected failure" $
          isLeft (checkType cStd [kw "int", punc "*", ident "extra"])
    ]