File size: 13,019 Bytes
a10b189 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 | {-|
Module : Assertica.Surface.Lexer
Description : Lexer for Haskell-like surface syntax
Copyright : (c) 2026 Ahmad Ali Parr
License : BSL-1.0
Tokenizes Haskell-like source code into a stream of tokens,
preserving source location information for error reporting.
Token categories:
- Keywords: let, in, forall, assert, proof, data, type, class, instance
- Identifiers: simple names and qualified names (Mod.name)
- Operators: +, -, *, /, ==, /=, ->, =>, :=, etc.
- Literals: integers, strings, booleans
- Delimiters: ( ) [ ] { } , ; :
- Comments: -- single-line, {- multi-line -}
-}
module Assertica.Surface.Lexer
( Token (..)
, TokenType (..)
, Lexer.SourceLoc (..)
, lexer
, lexerWithLoc
, prettyToken
) where
import Data.Text (Text)
import qualified Data.Text as T
import Data.Char (isAlphaNum, isAlpha, isDigit, isSpace)
import qualified Assertica.Surface.AST as Lexer (SourceLoc (..))
import Assertica.Surface.AST (SourceLoc (..))
-- ============================================================================
-- TOKEN TYPES
-- ============================================================================
{-| Token type classification.
-}
data TokenType
-- Keywords
= KwLet | KwIn | KwForall | KwAssert | KwProof
| KwData | KwType | KwClass | KwInstance | KwWhere
| KwCase | KwOf | KwIf | KwThen | KwElse
| KwBy | KwRefl | KwSymm | KwTrans
| KwTrue | KwFalse
-- Identifiers and literals
| Ident Text -- ^ Simple identifier
| QualIdent [Text] -- ^ Qualified identifier (Mod.name)
| IntLiteral Integer
| StringLiteral String
-- Operators
| OpArrow -- ^ ->
| OpFatArrow -- ^ =>
| OpColonEq -- ^ :=
| OpEq -- ^ ==
| OpNeq -- ^ /= or !=
| OpLt | OpLe | OpGt | OpGe
| OpPlus | OpMinus | OpMul | OpDiv | OpMod
| OpAnd | OpOr -- ^ /\ and \/
| OpNot
| OpConcat -- ^ ++
| OpColon -- ^ :
| OpDot -- ^ .
-- Delimiters
| LParen | RParen
| LBracket | RBracket
| LBrace | RBrace
| Comma | Semicolon | Pipe
-- Special
| Underscore
| Backslash -- ^ \
| EOF
deriving (Eq, Show)
{-| A token with source location.
-}
data Token = Token
{ tokenType :: TokenType
, tokenLoc :: SourceLoc
}
deriving (Eq, Show)
-- ============================================================================
-- LEXER STATE AND HELPERS
-- ============================================================================
{-| Lexer state tracking position.
-}
data LexState = LexState
{ lsInput :: String
, lsPos :: Int
, lsLine :: Int
, lsCol :: Int
, lsFile :: String
}
deriving (Show)
{-| Initialize lexer state.
-}
initLexState :: String -> String -> LexState
initLexState file input = LexState input 0 1 1 file
{-| Get current source location.
-}
currentLoc :: LexState -> SourceLoc
currentLoc ls = SourceLoc (lsFile ls) (lsLine ls) (lsCol ls)
{-| Peek at the current character without consuming it.
-}
peek :: LexState -> Maybe Char
peek ls =
if lsPos ls < length (lsInput ls)
then Just (lsInput ls !! lsPos ls)
else Nothing
{-| Consume a character and update position tracking.
-}
consume :: LexState -> LexState
consume ls = case peek ls of
Nothing -> ls
Just '\n' -> ls { lsPos = lsPos ls + 1, lsLine = lsLine ls + 1, lsCol = 1 }
Just _ -> ls { lsPos = lsPos ls + 1, lsCol = lsCol ls + 1 }
{-| Peek n characters ahead.
-}
peekN :: Int -> LexState -> String
peekN n ls = take n (drop (lsPos ls) (lsInput ls))
-- ============================================================================
-- LEXER
-- ============================================================================
{-| Lex source code into a stream of tokens.
Returns either an error message or a list of tokens.
-}
lexer :: String -> Either String [Token]
lexer input = lexerWithLoc "<input>" input
{-| Lex source code with explicit file name.
-}
lexerWithLoc :: String -> String -> Either String [Token]
lexerWithLoc file input = do
tokens <- lexTokens (initLexState file input)
return tokens
{-| Lexically analyze tokens.
-}
lexTokens :: LexState -> Either String [Token]
lexTokens ls = case peek ls of
Nothing -> Right [Token EOF (currentLoc ls)]
Just c
| isSpace c -> lexTokens (skipWhitespace ls)
| c == '-' && peekN 2 ls == "--" -> lexTokens (skipLineComment ls)
| c == '{' && peekN 2 ls == "{-" -> do
ls' <- skipBlockComment ls
lexTokens ls'
| otherwise -> do
(tok, ls') <- lexToken ls
rest <- lexTokens ls'
return (tok : rest)
{-| Skip whitespace.
-}
skipWhitespace :: LexState -> LexState
skipWhitespace ls = case peek ls of
Just c | isSpace c -> skipWhitespace (consume ls)
_ -> ls
{-| Skip single-line comment (-- ...)
-}
skipLineComment :: LexState -> LexState
skipLineComment ls = case peek ls of
Just '\n' -> consume ls
Just _ -> skipLineComment (consume ls)
Nothing -> ls
{-| Skip multi-line comment ({- ... -})
Handles nested comments.
-}
skipBlockComment :: LexState -> Either String LexState
skipBlockComment ls0 = go 1 (consume (consume ls0))
where
go :: Int -> LexState -> Either String LexState
go depth ls
| depth == 0 = Right ls
| otherwise = case peekN 2 ls of
"{-" -> go (depth + 1) (consume (consume ls))
"-}" -> go (depth - 1) (consume (consume ls))
_ -> case peek ls of
Nothing -> Left "Unterminated block comment"
Just _ -> go depth (consume ls)
{-| Lex a single token.
-}
lexToken :: LexState -> Either String (Token, LexState)
lexToken ls =
let loc = currentLoc ls
in case peek ls of
Nothing -> Right (Token EOF loc, ls)
Just '(' -> Right (Token LParen loc, consume ls)
Just ')' -> Right (Token RParen loc, consume ls)
Just '[' -> Right (Token LBracket loc, consume ls)
Just ']' -> Right (Token RBracket loc, consume ls)
Just '{' -> Right (Token LBrace loc, consume ls)
Just '}' -> Right (Token RBrace loc, consume ls)
Just ',' -> Right (Token Comma loc, consume ls)
Just ';' -> Right (Token Semicolon loc, consume ls)
Just '|' -> Right (Token Pipe loc, consume ls)
Just '\\' -> Right (Token Backslash loc, consume ls)
Just '_' ->
if isIdNext (peekN 2 ls)
then lexIdent ls
else Right (Token Underscore loc, consume ls)
Just '"' -> do
(s, ls') <- lexString ls
Right (Token (StringLiteral s) loc, ls')
Just c
| isDigit c -> do
(n, ls') <- lexNumber ls
Right (Token (IntLiteral n) loc, ls')
| isAlpha c || c == '_' -> lexIdent ls
| otherwise -> lexOperator ls
{-| Check if the next character would continue an identifier.
-}
isIdNext :: String -> Bool
isIdNext (c:_) = isAlphaNum c || c == '_' || c == '\''
isIdNext [] = False
{-| Lex an identifier or keyword.
-}
lexIdent :: LexState -> Either String (Token, LexState)
lexIdent ls =
let loc = currentLoc ls
(ident, ls') = span (\c -> isAlphaNum c || c == '_' || c == '\'') (drop (lsPos ls) (lsInput ls))
ls'' = ls { lsPos = lsPos ls + length ident, lsCol = lsCol ls + length ident }
txt = T.pack ident
in case T.unpack txt of
"let" -> Right (Token KwLet loc, ls'')
"in" -> Right (Token KwIn loc, ls'')
"forall" -> Right (Token KwForall loc, ls'')
"assert" -> Right (Token KwAssert loc, ls'')
"proof" -> Right (Token KwProof loc, ls'')
"data" -> Right (Token KwData loc, ls'')
"type" -> Right (Token KwType loc, ls'')
"class" -> Right (Token KwClass loc, ls'')
"instance"-> Right (Token KwInstance loc, ls'')
"where" -> Right (Token KwWhere loc, ls'')
"case" -> Right (Token KwCase loc, ls'')
"of" -> Right (Token KwOf loc, ls'')
"if" -> Right (Token KwIf loc, ls'')
"then" -> Right (Token KwThen loc, ls'')
"else" -> Right (Token KwElse loc, ls'')
"by" -> Right (Token KwBy loc, ls'')
"by_refl" -> Right (Token KwRefl loc, ls'')
"refl" -> Right (Token KwRefl loc, ls'')
"by_symm" -> Right (Token KwSymm loc, ls'')
"symm" -> Right (Token KwSymm loc, ls'')
"by_trans"-> Right (Token KwTrans loc, ls'')
"trans" -> Right (Token KwTrans loc, ls'')
"True" -> Right (Token KwTrue loc, ls'')
"False" -> Right (Token KwFalse loc, ls'')
_ ->
-- Check for qualified identifiers (Mod.name)
if '.' `elem` ident
then
let parts = T.splitOn (T.pack ".") txt
in Right (Token (QualIdent (map T.unpack parts)) loc, ls'')
else
Right (Token (Ident txt) loc, ls'')
{-| Lex a numeric literal.
-}
lexNumber :: LexState -> Either String (Integer, LexState)
lexNumber ls =
let numStr = takeWhile isDigit (drop (lsPos ls) (lsInput ls))
num = read numStr :: Integer
ls' = ls { lsPos = lsPos ls + length numStr, lsCol = lsCol ls + length numStr }
in Right (num, ls')
{-| Lex a string literal.
-}
lexString :: LexState -> Either String (String, LexState)
lexString ls0 =
let ls = consume ls0 -- skip opening quote
go acc ls' = case peek ls' of
Nothing -> Left "Unterminated string"
Just '"' -> Right (reverse acc, consume ls')
Just '\\' ->
case peekN 2 ls' of
"\\n" -> go ('\n' : acc) (consume (consume ls'))
"\\t" -> go ('\t' : acc) (consume (consume ls'))
"\\\"" -> go ('"' : acc) (consume (consume ls'))
"\\\\" -> go ('\\' : acc) (consume (consume ls'))
_ -> go (head (drop 1 (peekN 2 ls')) : acc) (consume (consume ls'))
Just c -> go (c : acc) (consume ls')
in go "" ls
{-| Lex an operator.
-}
lexOperator :: LexState -> Either String (Token, LexState)
lexOperator ls =
let loc = currentLoc ls
in case peekN 3 ls of
(':' : '=' : _) -> Right (Token OpColonEq loc, consume (consume ls))
('=' : '>' : _) -> Right (Token OpFatArrow loc, consume (consume ls))
('-' : '>' : _) -> Right (Token OpArrow loc, consume (consume ls))
('=' : '=' : _) -> Right (Token OpEq loc, consume (consume ls))
('/' : '=' : _) -> Right (Token OpNeq loc, consume (consume ls))
('!' : '=' : _) -> Right (Token OpNeq loc, consume (consume ls))
('<' : '=' : _) -> Right (Token OpLe loc, consume (consume ls))
('>' : '=' : _) -> Right (Token OpGe loc, consume (consume ls))
('/' : '\\' : _) -> Right (Token OpAnd loc, consume (consume ls))
('\\' : '/' : _) -> Right (Token OpOr loc, consume (consume ls))
('+' : '+' : _) -> Right (Token OpConcat loc, consume (consume ls))
_ -> case peek ls of
Just '+' -> Right (Token OpPlus loc, consume ls)
Just '-' -> Right (Token OpMinus loc, consume ls)
Just '*' -> Right (Token OpMul loc, consume ls)
Just '/' -> Right (Token OpDiv loc, consume ls)
Just '%' -> Right (Token OpMod loc, consume ls)
Just '<' -> Right (Token OpLt loc, consume ls)
Just '>' -> Right (Token OpGt loc, consume ls)
Just ':' -> Right (Token OpColon loc, consume ls)
Just '.' -> Right (Token OpDot loc, consume ls)
Just c -> Left $ "Unexpected character: " ++ [c]
Nothing -> Right (Token EOF loc, ls)
-- ============================================================================
-- PRETTY PRINTING
-- ============================================================================
{-| Pretty-print a token for debugging.
-}
prettyToken :: Token -> String
prettyToken (Token tt loc) =
let loc_str = show (lsLine loc) ++ ":" ++ show (lsCol loc)
in loc_str ++ " " ++ prettyTokenType tt
{-| Pretty-print a token type.
-}
prettyTokenType :: TokenType -> String
prettyTokenType = \case
KwLet -> "KwLet"
KwIn -> "KwIn"
KwForall -> "KwForall"
KwAssert -> "KwAssert"
KwProof -> "KwProof"
KwData -> "KwData"
KwType -> "KwType"
KwClass -> "KwClass"
KwInstance -> "KwInstance"
KwWhere -> "KwWhere"
KwCase -> "KwCase"
KwOf -> "KwOf"
KwIf -> "KwIf"
KwThen -> "KwThen"
KwElse -> "KwElse"
KwBy -> "KwBy"
KwRefl -> "KwRefl"
KwSymm -> "KwSymm"
KwTrans -> "KwTrans"
KwTrue -> "KwTrue"
KwFalse -> "KwFalse"
Ident x -> "Ident(" ++ T.unpack x ++ ")"
QualIdent xs -> "QualIdent(" ++ intercalate "." xs ++ ")"
IntLiteral n -> "IntLit(" ++ show n ++ ")"
StringLiteral s -> "StringLit(" ++ show s ++ ")"
OpArrow -> "->"
OpFatArrow -> "=>"
OpColonEq -> ":="
OpEq -> "=="
OpNeq -> "/="
OpLt -> "<"
OpLe -> "<="
OpGt -> ">"
OpGe -> ">="
OpPlus -> "+"
OpMinus -> "-"
OpMul -> "*"
OpDiv -> "/"
OpMod -> "%"
OpAnd -> "/\\"
OpOr -> "\\/"
OpNot -> "not"
OpConcat -> "++"
OpColon -> ":"
OpDot -> "."
LParen -> "("
RParen -> ")"
LBracket -> "["
RBracket -> "]"
LBrace -> "{"
RBrace -> "}"
Comma -> ","
Semicolon -> ";"
Pipe -> "|"
Underscore -> "_"
Backslash -> "\\"
EOF -> "EOF"
|