assertica / test /Test /Surface /Lexer.hs
SNAPKITTYWEST's picture
October 2026 main drop: mirror from GitHub
a10b189 verified
Raw History Blame Contribute Delete
7.55 kB
{-|
Module : Test.Surface.Lexer
Description : Unit tests for the lexer
Copyright : (c) 2026 Ahmad Ali Parr
License : BSL-1.0
-}
module Test.Surface.Lexer (tests) where
import Test.Tasty (TestTree, testGroup)
import Test.Tasty.HUnit (testCase, (@?=), Assertion)
import Assertica.Surface.Lexer
import Assertica.Surface.AST (SourceLoc(..))
-- ============================================================================
-- LEXER TESTS
-- ============================================================================
tests :: TestTree
tests = testGroup "Assertica.Surface.Lexer"
[ testGroup "Keywords"
[ testCase "lex 'let'" $
lexKeyword "let" KwLet
, testCase "lex 'in'" $
lexKeyword "in" KwIn
, testCase "lex 'forall'" $
lexKeyword "forall" KwForall
, testCase "lex 'assert'" $
lexKeyword "assert" KwAssert
, testCase "lex 'proof'" $
lexKeyword "proof" KwProof
, testCase "lex 'data'" $
lexKeyword "data" KwData
, testCase "lex 'type'" $
lexKeyword "type" KwType
]
, testGroup "Identifiers"
[ testCase "lex simple identifier" $
lexIdent "x" (Ident "x")
, testCase "lex longer identifier" $
lexIdent "join" (Ident "join")
, testCase "lex identifier with underscores" $
lexIdent "my_var" (Ident "my_var")
, testCase "lex qualified name" $
lexIdent "Prelude.Bool" (QualIdent ["Prelude", "Bool"])
]
, testGroup "Operators"
[ testCase "lex '->' arrow" $
lexOp "->" OpArrow
, testCase "lex '=>' fat arrow" $
lexOp "=>" OpFatArrow
, testCase "lex ':=' colon equals" $
lexOp ":=" OpColonEq
, testCase "lex '==' equality" $
lexOp "==" OpEq
, testCase "lex '/=' not equal" $
lexOp "/=" OpNeq
, testCase "lex '+' plus" $
lexOp "+" OpPlus
, testCase "lex '-' minus" $
lexOp "-" OpMinus
, testCase "lex '*' multiply" $
lexOp "*" OpMul
, testCase "lex '/' divide" $
lexOp "/" OpDiv
, testCase "lex '/\\' and" $
lexOp "/\\" OpAnd
, testCase "lex '\\/' or" $
lexOp "\\/" OpOr
]
, testGroup "Literals"
[ testCase "lex integer literal" $
lexNum "42" (IntLiteral 42)
, testCase "lex large integer" $
lexNum "123456789" (IntLiteral 123456789)
, testCase "lex string literal" $
lexStr "\"hello\"" (StringLiteral "hello")
, testCase "lex string with spaces" $
lexStr "\"hello world\"" (StringLiteral "hello world")
]
, testGroup "Delimiters"
[ testCase "lex '('" $
lexDelim "(" LParen
, testCase "lex ')'" $
lexDelim ")" RParen
, testCase "lex '['" $
lexDelim "[" LBracket
, testCase "lex ']'" $
lexDelim "]" RBracket
, testCase "lex '{'" $
lexDelim "{" LBrace
, testCase "lex '}'" $
lexDelim "}" RBrace
, testCase "lex ','" $
lexDelim "," Comma
, testCase "lex ';'" $
lexDelim ";" Semicolon
]
, testGroup "Comments"
[ testCase "skip line comment" $
lexSkipLineComment "x -- comment\ny" [Ident "x", Ident "y"]
, testCase "skip block comment" $
lexSkipBlockComment "x {- comment -} y" [Ident "x", Ident "y"]
, testCase "skip nested block comments" $
lexSkipBlockComment "x {- a {- b -} c -} y" [Ident "x", Ident "y"]
]
, testGroup "Complex programs"
[ testCase "lex lambda expression" $
lexProgram "\\x -> x + 1"
[Backslash, Ident "x", OpArrow, Ident "x", OpPlus, IntLiteral 1]
, testCase "lex assertion" $
lexProgram "assert comm: forall x y. x == y"
[KwAssert, Ident "comm", OpColon, KwForall, Ident "x", Ident "y", OpDot, Ident "x", OpEq, Ident "y"]
, testCase "lex let binding" $
lexProgram "let x = 42 in x"
[KwLet, Ident "x", OpEq, IntLiteral 42, KwIn, Ident "x"]
]
, testGroup "Error cases"
[ testCase "unterminated string" $
case lexer "\"hello" of
Left _ -> return ()
Right _ -> fail "Should fail on unterminated string"
, testCase "unterminated block comment" $
case lexer "x {- comment" of
Left _ -> return ()
Right _ -> fail "Should fail on unterminated block comment"
]
]
-- ============================================================================
-- HELPER FUNCTIONS
-- ============================================================================
{-| Test lexing a single keyword.
-}
lexKeyword :: String -> TokenType -> Assertion
lexKeyword input expected = do
case lexer input of
Right tokens ->
case filter (\t -> tokenType t /= EOF) tokens of
[Token tt _] -> tt @?= expected
_ -> fail "Expected exactly one non-EOF token"
Left err -> fail err
{-| Test lexing a single identifier.
-}
lexIdent :: String -> TokenType -> Assertion
lexIdent input expected = do
case lexer input of
Right tokens ->
case filter (\t -> tokenType t /= EOF) tokens of
[Token tt _] -> tt @?= expected
_ -> fail "Expected exactly one non-EOF token"
Left err -> fail err
{-| Test lexing a single operator.
-}
lexOp :: String -> TokenType -> Assertion
lexOp input expected = do
case lexer input of
Right tokens ->
case filter (\t -> tokenType t /= EOF) tokens of
[Token tt _] -> tt @?= expected
_ -> fail "Expected exactly one non-EOF token"
Left err -> fail err
{-| Test lexing a number.
-}
lexNum :: String -> TokenType -> Assertion
lexNum input expected = do
case lexer input of
Right tokens ->
case filter (\t -> tokenType t /= EOF) tokens of
[Token tt _] -> tt @?= expected
_ -> fail "Expected exactly one non-EOF token"
Left err -> fail err
{-| Test lexing a string.
-}
lexStr :: String -> TokenType -> Assertion
lexStr input expected = do
case lexer input of
Right tokens ->
case filter (\t -> tokenType t /= EOF) tokens of
[Token tt _] -> tt @?= expected
_ -> fail "Expected exactly one non-EOF token"
Left err -> fail err
{-| Test lexing a delimiter.
-}
lexDelim :: String -> TokenType -> Assertion
lexDelim input expected = do
case lexer input of
Right tokens ->
case filter (\t -> tokenType t /= EOF) tokens of
[Token tt _] -> tt @?= expected
_ -> fail "Expected exactly one non-EOF token"
Left err -> fail err
{-| Test skipping line comments.
-}
lexSkipLineComment :: String -> [TokenType] -> Assertion
lexSkipLineComment input expectedTypes = do
case lexer input of
Right tokens ->
let types = map tokenType (filter (\t -> tokenType t /= EOF) tokens)
in types @?= expectedTypes
Left err -> fail err
{-| Test skipping block comments.
-}
lexSkipBlockComment :: String -> [TokenType] -> Assertion
lexSkipBlockComment input expectedTypes = do
case lexer input of
Right tokens ->
let types = map tokenType (filter (\t -> tokenType t /= EOF) tokens)
in types @?= expectedTypes
Left err -> fail err
{-| Test lexing a complete program.
-}
lexProgram :: String -> [TokenType] -> Assertion
lexProgram input expectedTypes = do
case lexer input of
Right tokens ->
let types = map tokenType (filter (\t -> tokenType t /= EOF) tokens)
in types @?= expectedTypes
Left err -> fail err