@@ -134,6 +134,14 @@ def tokenize(data: str) -> list[Token]:
134134 return Lexer (data ).tokenize ()
135135
136136
137+ def format_error_context (data : str , lineno : int , window : int = 1 ) -> str :
138+ """Format a snippet of the input data around a given line number for error messages."""
139+ start = max (0 , lineno - window - 2 )
140+ context = data .splitlines ()[start :lineno ]
141+ no_len = len (str (lineno )) + 2
142+ return "\n " .join (f"{ start + i + 1 :>{no_len }} : { line } " for i , line in enumerate (context ))
143+
144+
137145class Lexer :
138146 """Lexer compatible with C-like syntax for struct definitions and preprocessor directives."""
139147
@@ -213,8 +221,10 @@ def _expect(self, *chars: str) -> str:
213221
214222 return self ._take ()
215223
216- def _error (self , msg : str , * , line : int | None = None ) -> LexerError :
217- return LexerError (f"line { line if line is not None else self ._line } : { msg } " )
224+ def _error (self , msg : str , * , lineno : int | None = None ) -> LexerError :
225+ lineno = lineno if lineno is not None else self ._line
226+ content = format_error_context (self .data , lineno )
227+ return LexerError (f"line { lineno } : { msg } \n { content } " )
218228
219229 def _emit (self , type : TokenType , value : str , line : int , column : int = 0 ) -> None :
220230 """Emit a token with the given type and value at the specified line and column."""
@@ -402,15 +412,15 @@ def _read_preprocessor(self) -> None:
402412 keyword = self ._read_identifier ()
403413
404414 if (token_type := _PP_KEYWORDS .get (keyword )) is None :
405- raise self ._error (f"unknown preprocessor directive '#{ keyword } '" , line = line )
415+ raise self ._error (f"unknown preprocessor directive '#{ keyword } '" , lineno = line )
406416
407417 self ._emit (token_type , keyword , line , col )
408418
409419 if token_type == TokenType .PP_DEFINE :
410420 self ._skip_whitespace_and_comments ()
411421
412422 if not (name := self ._read_identifier ()):
413- raise self ._error ("expected identifier after '#define'" , line = line )
423+ raise self ._error ("expected identifier after '#define'" , lineno = line )
414424 self ._emit (TokenType .IDENTIFIER , name , line )
415425
416426 self ._skip_whitespace_and_comments ()
@@ -434,7 +444,7 @@ def _read_preprocessor(self) -> None:
434444 self ._expect (">" ) # Consume closing `>`
435445 value = f"<{ value } >"
436446 else :
437- raise self ._error ("expected include path after '#include'" , line = line )
447+ raise self ._error ("expected include path after '#include'" , lineno = line )
438448
439449 self ._emit (TokenType .STRING , value , line )
440450
@@ -486,7 +496,7 @@ def tokenize(self) -> list[Token]:
486496 self ._emit (_SINGLE_CHARS [ch ], self ._take (), line , col )
487497
488498 else :
489- raise self ._error (f"unexpected character { ch !r} " , line = line )
499+ raise self ._error (f"unexpected character { ch !r} " , lineno = line )
490500
491501 self ._emit (TokenType .EOF , "" , self ._line , self ._column )
492502 return self ._tokens
0 commit comments