feat: add basic lexer for punctuation
This commit is contained in:
12
main.py
Normal file
12
main.py
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
from src.lexer import Lexer
|
||||||
|
from src.token import Token
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
source: str = """(),{;:}.."""
|
||||||
|
lexer: Lexer = Lexer()
|
||||||
|
tokens: list[Token] = lexer.process(source)
|
||||||
|
print(tokens)
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
main()
|
||||||
0
src/__init__.py
Normal file
0
src/__init__.py
Normal file
91
src/lexer.py
Normal file
91
src/lexer.py
Normal file
@@ -0,0 +1,91 @@
|
|||||||
|
from typing import Optional, Any
|
||||||
|
|
||||||
|
from src.position import Position
|
||||||
|
from src.token import Token, TokenType
|
||||||
|
|
||||||
|
|
||||||
|
class Lexer:
|
||||||
|
def __init__(self):
|
||||||
|
self.path: str = "<main>"
|
||||||
|
self.source: str = ""
|
||||||
|
self.tokens: list[Token] = []
|
||||||
|
self.start: int = 0
|
||||||
|
self.idx: int = 0
|
||||||
|
self.length: int = 0
|
||||||
|
self.line: int = 0
|
||||||
|
self.column: int = 0
|
||||||
|
self.start_pos: Position = self.get_position()
|
||||||
|
|
||||||
|
def process(self, source: str, path: Optional[str] = None) -> list[Token]:
|
||||||
|
self.path = path or "<main>"
|
||||||
|
self.source = source
|
||||||
|
self.tokens = []
|
||||||
|
self.start = 0
|
||||||
|
self.idx = 0
|
||||||
|
self.length = len(self.source)
|
||||||
|
self.line = 0
|
||||||
|
self.column = 0
|
||||||
|
|
||||||
|
while not self.is_at_end():
|
||||||
|
self.start_pos = self.get_position()
|
||||||
|
self.start = self.idx
|
||||||
|
self.scan_token()
|
||||||
|
|
||||||
|
self.tokens.append(Token(TokenType.EOF, "", None, self.get_position()))
|
||||||
|
|
||||||
|
return self.tokens
|
||||||
|
|
||||||
|
def is_at_end(self) -> bool:
|
||||||
|
return self.idx >= self.length
|
||||||
|
|
||||||
|
def get_position(self) -> Position:
|
||||||
|
return Position(self.path, self.line, self.column)
|
||||||
|
|
||||||
|
def peek(self) -> str:
|
||||||
|
if self.idx < self.length:
|
||||||
|
return self.source[self.idx]
|
||||||
|
return ""
|
||||||
|
|
||||||
|
def advance(self) -> str:
|
||||||
|
char: str = self.peek()
|
||||||
|
self.idx += 1
|
||||||
|
self.column += 1
|
||||||
|
if char == "\n":
|
||||||
|
self.line += 1
|
||||||
|
self.column = 0
|
||||||
|
return char
|
||||||
|
|
||||||
|
def match(self, expected: str) -> bool:
|
||||||
|
if self.peek() == expected:
|
||||||
|
self.advance()
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
def add_token(self, token_type: TokenType, value: Optional[Any] = None):
|
||||||
|
lexeme: str = self.source[self.start:self.idx]
|
||||||
|
self.tokens.append(
|
||||||
|
Token(
|
||||||
|
position=self.start_pos,
|
||||||
|
type=token_type,
|
||||||
|
lexeme=lexeme,
|
||||||
|
value=value
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
def scan_token(self):
|
||||||
|
char: str = self.advance()
|
||||||
|
match char:
|
||||||
|
case "(": self.add_token(TokenType.LEFT_PAREN)
|
||||||
|
case ")": self.add_token(TokenType.RIGHT_PAREN)
|
||||||
|
case "{": self.add_token(TokenType.LEFT_BRACE)
|
||||||
|
case "}": self.add_token(TokenType.RIGHT_BRACE)
|
||||||
|
case ",": self.add_token(TokenType.COMMA)
|
||||||
|
case ".": self.add_token(TokenType.DOT)
|
||||||
|
case ";": self.add_token(TokenType.SEMICOLON)
|
||||||
|
case ":": self.add_token(TokenType.COLON)
|
||||||
|
case _: self.error("Unexpected character")
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
def error(self, msg: str):
|
||||||
|
raise SyntaxError(f"[ERROR] Error at {self.start_pos}: {msg}")
|
||||||
11
src/position.py
Normal file
11
src/position.py
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
from dataclasses import dataclass
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class Position:
|
||||||
|
path: str
|
||||||
|
line: int
|
||||||
|
column: int
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return f"{self.path}#{self.line}:{self.column}"
|
||||||
75
src/token.py
Normal file
75
src/token.py
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
from dataclasses import dataclass
|
||||||
|
from enum import Enum, auto
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from src.position import Position
|
||||||
|
|
||||||
|
|
||||||
|
class TokenType(Enum):
|
||||||
|
# Punctuation
|
||||||
|
LEFT_PAREN = auto()
|
||||||
|
RIGHT_PAREN = auto()
|
||||||
|
LEFT_BRACE = auto()
|
||||||
|
RIGHT_BRACE = auto()
|
||||||
|
COMMA = auto()
|
||||||
|
DOT = auto()
|
||||||
|
SEMICOLON = auto()
|
||||||
|
COLON = auto()
|
||||||
|
|
||||||
|
# Operators
|
||||||
|
PLUS = auto()
|
||||||
|
PLUS_EQUAL = auto()
|
||||||
|
MINUS = auto()
|
||||||
|
MINUS_EQUAL = auto()
|
||||||
|
SLASH = auto()
|
||||||
|
SLASH_EQUAL = auto()
|
||||||
|
STAR = auto()
|
||||||
|
STAR_EQUAL = auto()
|
||||||
|
EQUAL = auto()
|
||||||
|
EQUAL_EQUAL = auto()
|
||||||
|
BANG = auto()
|
||||||
|
BANG_EQUAL = auto()
|
||||||
|
GREATER = auto()
|
||||||
|
GREATER_EQUAL = auto()
|
||||||
|
LESS = auto()
|
||||||
|
LESS_EQUAL = auto()
|
||||||
|
|
||||||
|
# Literals
|
||||||
|
IDENTIFIER = auto()
|
||||||
|
STRING = auto()
|
||||||
|
NUMBER = auto()
|
||||||
|
TRUE = auto()
|
||||||
|
FALSE = auto()
|
||||||
|
|
||||||
|
# Keywords
|
||||||
|
LET = auto()
|
||||||
|
AND = auto()
|
||||||
|
OR = auto()
|
||||||
|
IF = auto()
|
||||||
|
ELSE = auto()
|
||||||
|
FOR = auto()
|
||||||
|
WHILE = auto()
|
||||||
|
FROM = auto()
|
||||||
|
TO = auto()
|
||||||
|
BY = auto()
|
||||||
|
|
||||||
|
# Misc
|
||||||
|
PRINT = auto()
|
||||||
|
COMMENT = auto()
|
||||||
|
WHITESPACE = auto()
|
||||||
|
EOF = auto()
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class Token:
|
||||||
|
type: TokenType
|
||||||
|
lexeme: str
|
||||||
|
value: Any
|
||||||
|
position: Position
|
||||||
|
|
||||||
|
def __repr__(self) -> str:
|
||||||
|
res: str = f"[{self.type.name}"
|
||||||
|
if self.value is not None:
|
||||||
|
res += f" ({self.value})"
|
||||||
|
res += "]"
|
||||||
|
return res
|
||||||
Reference in New Issue
Block a user