Tracebacks and SyntaxErrs and RuntimeErrs OH MY!

This commit is contained in:
2023-04-03 19:32:02 -04:00
parent dc433d407c
commit b7f3867196
2 changed files with 367 additions and 37 deletions

View File

@@ -12,4 +12,6 @@ expr : term ((PLUS|MINUS) term)*
term : factor ((MUL|DIV) factor)* term : factor ((MUL|DIV) factor)*
factor : INT | FLOAT factor : INT | FLOAT
``` : (PLUS|MINUS) factor
: LPAREN expr RPAREN
```

400
aurora.py
View File

@@ -10,15 +10,18 @@ d88P aaa Y88888 888 Y88P 888 Y888888
Purpose: Main implementation of Aurora Purpose: Main implementation of Aurora
""" """
############# # Imports
# CONSTANTS # from internal.term_utils import point_at
#############
####################
# CONSTANTS
####################
DIGITS = "0123456789" DIGITS = "0123456789"
############# ####################
# ERRORS # # ERRORS
############# ####################
class Error: class Error:
def __init__(self, pos_start, pos_end, error_name, details): def __init__(self, pos_start, pos_end, error_name, details):
self.pos_start = pos_start self.pos_start = pos_start
@@ -29,17 +32,48 @@ class Error:
def as_string(self): def as_string(self):
result = f'{self.error_name}: {self.details}\n' result = f'{self.error_name}: {self.details}\n'
result += f'File {self.pos_start.fn}, line {self.pos_start.ln + 1}' result += f'File {self.pos_start.fn}, line {self.pos_start.ln + 1}'
result += f'\n{point_at(self.pos_start.ftxt, self.pos_start, self.pos_end)}'
return result return result
class IllegalCharError(Error): class IllegalCharErr(Error):
def __init__(self, pos_start, pos_end, details): def __init__(self, pos_start, pos_end, details):
super().__init__(pos_start, pos_end, 'IllegalCharacter', details) super().__init__(pos_start, pos_end, 'IllegalCharacter', details)
####################################### class InvalidSyntaxErr(Error):
def __init__(self, pos_start, pos_end, details=''):
super().__init__(pos_start, pos_end, 'InvalidSyntax', details)
class RuntimeErr(Error):
def __init__(self, pos_start, pos_end, details, context):
super().__init__(pos_start, pos_end, 'RuntimeErr', details)
self.context = context
def as_string(self):
result = self.generate_traceback()
result += f'{self.error_name}: {self.details}\n'
result += f'\n{point_at(self.pos_start.ftxt, self.pos_start, self.pos_end)}'
return result
def generate_traceback(self):
result = ''
pos = self.pos_start
context = self.context
while context:
result = f' File {pos.fn} line {str(pos.ln + 1)}, in {context.display_name}\n' + result
pos = context.parent_entry_pos
context = context.parent
return f"Traceback (most recent call last):\n" \
f"{result}"
####################
# POSITION # POSITION
####################################### ####################
class Position: class Position:
def __init__(self, idx, ln, col, fn, ftxt): def __init__(self, idx, ln, col, fn, ftxt):
self.idx = idx self.idx = idx
@@ -48,7 +82,7 @@ class Position:
self.fn = fn self.fn = fn
self.ftxt = ftxt self.ftxt = ftxt
def advance(self, current_char): def advance(self, current_char=None):
self.idx += 1 self.idx += 1
self.col += 1 self.col += 1
@@ -62,9 +96,9 @@ class Position:
return Position(self.idx, self.ln, self.col, self.fn, self.ftxt) return Position(self.idx, self.ln, self.col, self.fn, self.ftxt)
############# ####################
# Tokens # # TOKENS
############# ####################
# Token types # Token types
TT_INT = "INT" TT_INT = "INT"
TT_FLOAT = "FLOAT" TT_FLOAT = "FLOAT"
@@ -74,13 +108,22 @@ TT_MUL = "MUL"
TT_DIV = "DIV" TT_DIV = "DIV"
TT_LPAREN = "LPAREN" TT_LPAREN = "LPAREN"
TT_RPAREN = "RPAREN" TT_RPAREN = "RPAREN"
TT_EOF = "EOF"
class Token: class Token:
def __init__(self, type_, value=None): def __init__(self, type_, value=None, pos_start=None, pos_end=None):
self.type = type_ self.type = type_
self.value = value self.value = value
if pos_start:
self.pos_start = pos_start.copy()
self.pos_end = pos_start.copy()
self.pos_end.advance()
if pos_end:
self.pos_end = pos_end
def __repr__(self): def __repr__(self):
""" """
Will return "type:value" if a value exists. Otherwise, just "type". Will return "type:value" if a value exists. Otherwise, just "type".
@@ -88,9 +131,9 @@ class Token:
return f"<{self.type}:{self.value}>" if self.value else f"<{self.type}>" return f"<{self.type}:{self.value}>" if self.value else f"<{self.type}>"
############# ####################
# Lexer # # LEXER
############# ####################
class Lexer: class Lexer:
def __init__(self, fn, text): def __init__(self, fn, text):
self.fn = fn self.fn = fn
@@ -112,35 +155,37 @@ class Lexer:
self.advance() self.advance()
elif self.current_char in DIGITS: elif self.current_char in DIGITS:
tokens.append(self.make_number()) tokens.append(self.make_number())
elif self.current_char == "+": elif self.current_char == '+':
tokens.append(Token(TT_PLUS)) tokens.append(Token(TT_PLUS, pos_start=self.pos))
self.advance() self.advance()
elif self.current_char == "-": elif self.current_char == '-':
tokens.append(Token(TT_MINUS)) tokens.append(Token(TT_MINUS, pos_start=self.pos))
self.advance() self.advance()
elif self.current_char == "*": elif self.current_char == '*':
tokens.append(Token(TT_MUL)) tokens.append(Token(TT_MUL, pos_start=self.pos))
self.advance() self.advance()
elif self.current_char == "/": elif self.current_char == '/':
tokens.append(Token(TT_DIV)) tokens.append(Token(TT_DIV, pos_start=self.pos))
self.advance() self.advance()
elif self.current_char == "(": elif self.current_char == '(':
tokens.append(Token(TT_LPAREN)) tokens.append(Token(TT_LPAREN, pos_start=self.pos))
self.advance() self.advance()
elif self.current_char == ")": elif self.current_char == ')':
tokens.append(Token(TT_RPAREN)) tokens.append(Token(TT_RPAREN, pos_start=self.pos))
self.advance() self.advance()
else: else:
pos_start = self.pos.copy() pos_start = self.pos.copy()
char = self.current_char char = self.current_char
self.advance() self.advance()
return [], IllegalCharError(pos_start, self.pos, "'" + char + "'") return [], IllegalCharErr(pos_start, self.pos, "'" + char + "'")
tokens.append(Token(TT_EOF, pos_start=self.pos))
return tokens, None return tokens, None
def make_number(self): def make_number(self):
num_str = '' num_str = ''
dot_count = 0 dot_count = 0
pos_start = self.pos.copy()
while self.current_char is not None and self.current_char in (DIGITS + '.'): while self.current_char is not None and self.current_char in (DIGITS + '.'):
if self.current_char == '.': if self.current_char == '.':
@@ -153,16 +198,299 @@ class Lexer:
self.advance() self.advance()
if dot_count == 0: if dot_count == 0:
return Token(TT_INT, int(num_str)) return Token(TT_INT, int(num_str), pos_start, self.pos)
else: else:
return Token(TT_FLOAT, float(num_str)) return Token(TT_FLOAT, float(num_str), pos_start, self.pos)
############# ####################
# Runner # # NODES
############# ####################
class NumberNode:
def __init__(self, token):
self.token = token
self.pos_start = self.token.pos_start
self.pos_end = self.token.pos_end
def __repr__(self):
return f"{self.token}"
class BinaryOperationNode:
def __init__(self, left_node, operation, right_node):
self.left_node = left_node
self.operation = operation
self.right_node = right_node
self.pos_start = self.left_node.pos_start
self.pos_end = self.right_node.pos_end
def __repr__(self):
return f"({self.left_node}, {self.operation}, {self.right_node})"
class UnaryOperationNode:
def __init__(self, operation, node):
self.operation = operation
self.node = node
self.pos_start = self.operation.pos_start
self.pos_end = self.node.pos_end
def __repr__(self):
return f'({self.operation}, {self.node})'
####################
# PARSER RESULT
####################
class ParsedResult:
def __init__(self):
self.error = None
self.node = None
def register(self, result):
if isinstance(result, ParsedResult):
if result.error:
self.error = result.error
return result.node
return result
def success(self, node):
self.node = node
return self
def failure(self, error):
self.error = error
return self
####################
# RUNTIME RESULT
####################
class RuntimeResult:
def __init__(self):
self.value = None
self.error = None
def register(self, result):
if result.error:
self.error = result.error
return result.value
def success(self, value):
self.value = value
return self
def failure(self, error):
self.error = error
return self
####################
# PARSER
####################
class Parser:
def __init__(self, tokens):
self.tokens = tokens
self.token_idx = -1
self.current_token = None
self.advance()
def advance(self):
self.token_idx += 1
if self.token_idx < len(self.tokens):
self.current_token = self.tokens[self.token_idx]
return self.current_token
def parse(self):
result = self.expr()
if not result.error and self.current_token.type != TT_EOF:
return result.failure(InvalidSyntaxErr(
self.current_token.pos_start,
self.current_token.pos_end,
"Expected '+', '-', '*', or '/'"
))
return result
def factor(self):
result = ParsedResult()
token = self.current_token
if token.type in (TT_PLUS, TT_MINUS):
result.register(self.advance())
factor = result.register(self.factor())
if result.error:
return result
return result.success(UnaryOperationNode(token, factor))
elif token.type in (TT_INT, TT_FLOAT):
result.register(self.advance())
return result.success(NumberNode(token))
elif token.type == TT_LPAREN:
result.register(self.advance())
expr = result.register(self.expr())
if result.error:
return result
if self.current_token.type == TT_RPAREN:
result.register(self.advance())
return result.success(expr)
else:
return result.failure(InvalidSyntaxErr(
self.current_token.pos_start, self.current_token.pos_end,
"Expected ')'"
))
return result.failure(InvalidSyntaxErr(token.pos_start, token.pos_end, "Expected int or float"))
def term(self):
return self.binary_operation(self.factor, (TT_MUL, TT_DIV))
def expr(self):
return self.binary_operation(self.term, (TT_PLUS, TT_MINUS))
def binary_operation(self, func, operations):
result = ParsedResult()
left = result.register(func())
if result.error:
return result
while self.current_token.type in operations:
operation = self.current_token
result.register(self.advance())
right = result.register(func())
if result.error:
return result
left = BinaryOperationNode(left, operation, right)
return result.success(left)
####################
# VALUES
####################
class Number:
def __init__(self, value):
self.value = value
self.pos_start = None
self.pos_end = None
self.context = None
def set_pos(self, pos_start=None, pos_end=None):
self.pos_start = pos_start
self.pos_end = pos_end
return self
def set_context(self, context=None):
self.context = context
return self
def added_to(self, other):
if isinstance(other, Number):
return Number(self.value + other.value).set_context(self.context), None
def subtracted_by(self, other):
if isinstance(other, Number):
return Number(self.value - other.value).set_context(self.context), None
def multiplied_by(self, other):
if isinstance(other, Number):
return Number(self.value * other.value).set_context(self.context), None
def divided_by(self, other):
if isinstance(other, Number):
if other.value == 0:
return None, RuntimeErr(
other.pos_start,
other.pos_end,
"Division by zero",
self.context
)
return Number(self.value / other.value).set_context(self.context), None
def __repr__(self):
return str(self.value)
####################
# CONTEXT
####################
class Context:
def __init__(self, display_name, parent=None, parent_entry_pos=None):
self.display_name = display_name
self.parent = parent
self.parent_entry_pos = parent_entry_pos
####################
# INTERPRETER
####################
class Interpreter:
def visit(self, node, context):
method_name = f'visit_{type(node).__name__}'
method = getattr(self, method_name, self.no_visit_method)
return method(node, context)
def no_visit_method(self, node, context):
raise Exception(f"No visit_{type(node).__name__} method defined")
def visit_NumberNode(self, node, context):
return RuntimeResult().success(Number(node.token.value).set_context(context).set_pos(node.pos_start, node.pos_end))
def visit_BinaryOperationNode(self, node, context):
rt_result = RuntimeResult()
left = rt_result.register(self.visit(node.left_node, context))
if rt_result.error:
return rt_result
right = rt_result.register(self.visit(node.right_node, context))
if rt_result.error:
return rt_result
if node.operation.type == TT_PLUS:
result, error = left.added_to(right)
if node.operation.type == TT_MINUS:
result, error = left.subtracted_by(right)
if node.operation.type == TT_MUL:
result, error = left.multiplied_by(right)
if node.operation.type == TT_DIV:
result, error = left.divided_by(right)
if error:
return rt_result.failure(error)
return rt_result.success(result.set_pos(node.pos_start, node.pos_end))
def visit_UnaryOperationNode(self, node: UnaryOperationNode, context):
result = RuntimeResult()
number = result.register(self.visit(node.node, context))
if result.error:
return result
if node.operation.type == TT_MINUS:
number, error = number.multiplied_by(Number(-1))
if error:
return result.failure(error)
return number.set_pos(node.pos_start, node.pos_end)
####################
# RUNNER
####################
def evaluate(fn, text): def evaluate(fn, text):
lexer = Lexer(fn, text) lexer = Lexer(fn, text)
tokens, error = lexer.make_tokens() tokens, error = lexer.make_tokens()
if error:
return None, error
return tokens, error if error:
return None, error
# Generate the abstract syntax tree
parser = Parser(tokens)
ast = parser.parse()
if ast.error:
return None, ast.error
# Run the program
interpreter = Interpreter()
context = Context("<program>")
result = interpreter.visit(ast.node, context)
return result.value, result.error