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"