diff --git a/AST.py b/AST.py index c5a1ebc..912616a 100644 --- a/AST.py +++ b/AST.py @@ -1,8 +1,9 @@ from enum import Enum class Node: - def __init__(self): - self.children = [] + def __init__(self, pos): + self.children = [] + self.pos = pos def __repr__(self): return self.__str__() @@ -11,7 +12,7 @@ class Node: return len(self.children) def __getitem__(self, index): - return self.children[index] + return self.children[index] def append(self, node): self.children.append(node) @@ -21,36 +22,38 @@ class Node: class Program(Node): def __init__(self): - Node.__init__(self) + Node.__init__(self, (-1, -1)) def __str__(self): return "Program:\n" + "\n".join([str(e) for e in self.children]) class BlockNode(Node): - def __init__(self): - Node.__init__(self) + def __init__(self, pos): + Node.__init__(self, pos) def __str__(self): return "B{\n" + "\n".join([str(e) for e in self.children]) + "\n}" class ListNode(Node): - def __init__(self): - Node.__init__(self) + def __init__(self, pos): + Node.__init__(self, pos) def __str__(self): return "@(" + ", ".join([str(e) for e in self.children]) + ")" class IdentifierNode(Node): - def __init__(self, identifier): + def __init__(self, identifier, pos): + Node.__init__(self, pos) self.identifier = identifier def __str__(self): return f"L'{self.identifier}'" class AssignExpression(Node): - def __init__(self, target, value): + def __init__(self, target, value, pos): + Node.__init__(self, pos) self.target = target self.value = value @@ -58,7 +61,8 @@ class AssignExpression(Node): return f"A[{self.target} = {self.value}]" class AsteriskStatementNode(Node): - def __init__(self, iterator, statement): + def __init__(self, iterator, statement, pos): + Node.__init__(self, pos) self.iterator = iterator self.statement = statement @@ -66,7 +70,8 @@ class AsteriskStatementNode(Node): return f"*({self.iterator}: {self.statement})" class ColonNode(Node): - def __init__(self, a, b): + def __init__(self, a, b, pos): + Node.__init__(self, pos) self.a = a self.b = b @@ -74,33 +79,40 @@ class ColonNode(Node): return f":({self.a}, {self.b})" class ExpressionNode(Node): + def __init__(self, pos): + Node.__init__(self, pos) + def __str__(self): return f"{self.__class__.__name__}('{self.value}')" class IntegerLiteralNode(ExpressionNode): - def __init__(self, value): + def __init__(self, value, pos): + Node.__init__(self, pos) self.value = value def __str__(self): return f"i'{self.value}'" class StringLiteralNode(ExpressionNode): - def __init__(self, value): + def __init__(self, value, pos): + Node.__init__(self, pos) self.value = value def __str__(self): return f"s'{self.value}'" class NoteLiteralNode(ExpressionNode): - def __init__(self, value): + def __init__(self, value, pos): + Node.__init__(self, pos) self.value = value def __str__(self): return f"n'{self.value.note}[{self.value.octave}, {self.value.duration}]'" class FunctionCallNode(Node): - def __init__(self, identifier, arguments): + def __init__(self, identifier, arguments, pos): + Node.__init__(self, pos) self.identifier = identifier self.arguments = arguments @@ -108,11 +120,14 @@ class FunctionCallNode(Node): return f"F({self.identifier}: {self.arguments})" class CommaNode(Node): + def __init__(self, pos): + Node.__init__(self, pos) def __str__(self): return "[,]" class PercentNode(Node): - def __init__(self, value): + def __init__(self, value, pos): + Node.__init__(self, pos) self.value = value def __str__(self): diff --git a/Environment.py b/Environment.py index d7ff598..49efdd1 100644 --- a/Environment.py +++ b/Environment.py @@ -1,9 +1,10 @@ import sys -from Evaluator import RuntimeException, objectString +from Evaluator import objectString from Note import * import random import Synth import time +from Error import RuntimeException types = { int: 'integer', @@ -28,7 +29,7 @@ class Environment(): return value else: return value - raise RuntimeException(f"Variable '{name}' is not declared" + ("" if type is None else f" (expected type: {types[type]})")) + raise RuntimeException(None, f"Variable '{name}' is not declared" + ("" if type is None else f" (expected type: {types[type]})")) def findVariableScope(self, name, type=None): for scope in reversed(self.scopes): diff --git a/Error.py b/Error.py index 831cca9..c1b588e 100644 --- a/Error.py +++ b/Error.py @@ -1,2 +1,9 @@ -class ParseError(Exception): - pass +class SyntaxException(Exception): + def __init__(self, pos, msg): + posStr = "" if pos is None else f"[line {pos[0]+1}, col {pos[1]+1}]" + self.msg = f"Syntax error {posStr}:\n{msg}" + +class RuntimeException(Exception): + def __init__(self, pos, msg): + posStr = "" if pos is None else f"[line {pos[0]+1}, col {pos[1]+1}]" + self.msg = f"Syntax error {posStr}:\n{msg}" diff --git a/Evaluator.py b/Evaluator.py index e086752..7346ea2 100644 --- a/Evaluator.py +++ b/Evaluator.py @@ -2,9 +2,7 @@ from Tokenizer import tokenize, TokenType from Parser import parse from AST import * from Note import Note - -class RuntimeException(Exception): - pass +from Error import RuntimeException def evaluateProgram(program, environment): for node in program.children: @@ -39,8 +37,8 @@ def objectString(obj): if isinstance(obj, float): return f"{int(obj*100)}%" if obj is None: - raise RuntimeException(f"Trying to interpret void") - raise RuntimeException(f"Don't know how to interpret {str(obj)}") + raise RuntimeException(None, f"Trying to interpret void") + raise RuntimeException(None, f"Don't know how to interpret {str(obj)}") def evaluateNote(note, environment): return note.value @@ -51,7 +49,7 @@ def evaluateFunctionCall(functionCall, environment): for name, definition in environment.functions.items(): if name == function: return definition(arguments, environment) - raise RuntimeException(f"Function '{function}' does not exist") + raise RuntimeException(functionCall.pos, f"Function '{function}' does not exist") def evaluateComma(comma, environment): @@ -109,7 +107,7 @@ def evaluateColon(colon, environment): return Note.range(colon.a.value, colon.b.value) elif isinstance(colon.a, IntegerLiteralNode) and isinstance(colon.b, IntegerLiteralNode): return list(range(colon.a.value, colon.b.value+1)) - raise RuntimeException("Invalid colon arguments") + raise RuntimeException(colon.pos, "Invalid colon arguments") def evaluate(input, environment): if isinstance(input, Program): diff --git a/Note.py b/Note.py index c46825c..eedd2ed 100644 --- a/Note.py +++ b/Note.py @@ -1,6 +1,6 @@ from enum import Enum -from Error import ParseError import math +from Error import SyntaxException class NotePitch(Enum): C = 0 @@ -52,7 +52,7 @@ class NotePitch(Enum): } return map[string.lower()] except KeyError as e: - raise ParseError(f"Note '{string}' does not exist") + raise SyntaxException(None, f"Note '{string}' does not exist") class Note: def __init__(self, note, octave = 4, duration = 4): diff --git a/Parser.py b/Parser.py index 9f6c053..4520f11 100644 --- a/Parser.py +++ b/Parser.py @@ -1,19 +1,22 @@ from Tokenizer import * from Note import * from AST import * -from Error import ParseError +from Error import SyntaxException def expectedFound(expected, found): - raise ParseError(f"Expected: {expected}, found: {found}") + raise SyntaxException(None, f"Expected: {expected}, found: {found}") def parseInteger(input, parent): - return IntegerLiteralNode(int(input.pop(0).value)) + token = input.pop(0) + return IntegerLiteralNode(int(token.value), token.pos) -def parseString(input, parent): - return StringLiteralNode(input.pop(0).value[1:-1]) +def parseString(input, parent): + token = input.pop(0) + return StringLiteralNode(token.value[1:-1], token.pos) def parseNote(input, parent): - value = input.pop(0).value + token = input.pop(0) + value = token.value consumedChars = 1 notePitch = value[consumedChars] consumedChars += 1 @@ -33,21 +36,21 @@ def parseNote(input, parent): consumedChars += 1 duration = int(durationString) - return NoteLiteralNode(Note(notePitch, octave, duration)) + return NoteLiteralNode(Note(notePitch, octave, duration), token.pos) def parseComma(input, parent): - input.pop(0) - return CommaNode() + token = input.pop(0) + return CommaNode(token.pos) def parseList(input, parent): - input.pop(0) + token = input.pop(0) - node = ListNode() + node = ListNode(token.pos) while input[0].type != TokenType.CLOSE_PAREN: element = parseArrayElement(input, node) if element is None: - raise ParseError(f"Line: {input[0].pos[0]+1}, col: {input[0].pos[1]+1}: Invalid element '{input[0].value}'") + raise SyntaxException(input[0].pos, "Invalid element '{input[0].value}'") node.append(element) if input[0].type != TokenType.CLOSE_PAREN: @@ -57,9 +60,9 @@ def parseList(input, parent): return node def parseBlock(input, parent): - input.pop(0) + token = input.pop(0) - block = BlockNode() + block = BlockNode(token.pos) while input[0].type != TokenType.CLOSE_BRACKET: block.append(parseToken(input, block)) @@ -72,62 +75,63 @@ def parseBlock(input, parent): def parseAsterisk(input, parent): - input.pop(0) + token = input.pop(0) iterator = parent.pop(-1) value = parseStatement(input, parent) #TODO: only statements! (?) - return AsteriskStatementNode(iterator, value) + return AsteriskStatementNode(iterator, value, token.pos) def parseNoteOrColon(input, parent): note = parseNote(input, parent) if len(input) > 1 and input[0].type == TokenType.COLON: - input.pop(0) + token = input.pop(0) b = parseNote(input, parent) #TODO: only expressions! if b is None: - raise ParseError(f"Line {input[0].pos[0]+1}, col {input[0].pos[1]+1}: Invalid colon argument '{input[0].value}'") - return ColonNode(note, b) + raise SyntaxException(input[0].pos, f"Invalid colon argument '{input[0].value}'") + return ColonNode(note, b, token.pos) return note def parseIntegerOrColon(input, parent): integer = parseInteger(input, parent) if len(input) > 1 and input[0].type == TokenType.COLON: - input.pop(0) + token = input.pop(0) b = parseInteger(input, parent) #TODO: only expressions! if b is None: - raise ParseError(f"Line {input[0].pos[0]+1}, col {input[0].pos[1]+1}: Invalid colon argument '{input[0].value}'") - return ColonNode(integer, b) + raise SyntaxException(input[0].pos, f"Invalid colon argument '{input[0].value}'") + return ColonNode(integer, b, token.pos) return integer -def parseFunctionCallOrAssignOrIdentifier(input, parent): - identifier = IdentifierNode(input.pop(0).value) +def parseFunctionCallOrAssignOrIdentifier(input, parent): + token = input.pop(0) + identifier = IdentifierNode(token.value, token.pos) # Function call if len(input) > 0 and input[0].type == TokenType.OPEN_PAREN: arguments = parseList(input, parent) - return FunctionCallNode(identifier, arguments) + return FunctionCallNode(identifier, arguments, token.pos) # Assign if len(input) > 1 and input[0].type == TokenType.ASSIGN: - input.pop(0) + token = input.pop(0) value = parseExpression(input, parent) #TODO: only expressions! - return AssignExpression(identifier, value) + return AssignExpression(identifier, value, token.pos) return identifier def parsePercent(input, parent): - input.pop(0) + token = input.pop(0) value = parent.pop(-1) - return PercentNode(value) + return PercentNode(value, token.pos) def parseMinus(input, parent): - input.pop(0) + token = input.pop(0) value = parseInteger(input, parent) - return IntegerLiteralNode(-value.value) + return IntegerLiteralNode(-value.value, token.pos) def parseExpression(input, parent): type = input[0].type @@ -145,7 +149,7 @@ def parseExpression(input, parent): return parsePercent(input, parent) if type == TokenType.OPEN_PAREN: return parseList(input, parent) - raise ParseError(f"Line {input[0].pos[0]+1}, col {input[0].pos[1]+1}: Unexpected character '{input[0].value}'") + raise SyntaxException(input[0].pos, f"Unexpected character '{input[0].value}'") def parseArrayElement(input, parent): type = input[0].type @@ -172,20 +176,3 @@ def parse(input): while len(input) > 0: root.append(parseToken(input, root)) return root - -if __name__ == "__main__": - try: - with open(sys.argv[1], 'r') as source: - lines = [line.rstrip('\n') for line in source.readlines()] - - tokens = [token for token in tokenize(lines) if token.type != TokenType.COMMENT] - - ast = parse(tokens) - - print(ast) - except TokenizerError as e: - print(str(e)) - - except ParseError as e: - print(str(e)) - diff --git a/Tokenizer.py b/Tokenizer.py index 2426933..dd2799d 100644 --- a/Tokenizer.py +++ b/Tokenizer.py @@ -2,6 +2,7 @@ from enum import Enum import time import re import sys +from Error import SyntaxException class TokenType(Enum): OPEN_PAREN = 1 @@ -19,10 +20,6 @@ class TokenType(Enum): COMMENT = 13 PERCENT = 14 MINUS = 15 - -class TokenizerError(Exception): - pass - class Token: def __init__(self, type, value, pos): @@ -188,7 +185,7 @@ def doTokenize(lines): break if not tokenized: - raise TokenizerError(f"Line {lineNumber+1}, col {current+1}: unknown symbol '{line[current]}'") + raise SyntaxException((lineNumber, current), f"Unknown symbol '{line[current]}'") return [token for token in tokens if token.type is not None] diff --git a/main.py b/main.py index c96519a..0dc0ff0 100644 --- a/main.py +++ b/main.py @@ -2,16 +2,22 @@ from Tokenizer import tokenize from Parser import parse from Evaluator import evaluate from Environment import createEnvironment +from Error import SyntaxException, RuntimeException import sys if __name__ == "__main__": - with open(sys.argv[1], 'r') as source: - lines = [line.rstrip('\n') for line in source.readlines()] - - env = createEnvironment() - - tokens = tokenize(lines) - - ast = parse(tokens) - - evaluate(ast, env) + try: + with open(sys.argv[1], 'r') as source: + lines = [line.rstrip('\n') for line in source.readlines()] + + env = createEnvironment() + + tokens = tokenize(lines) + + ast = parse(tokens) + + evaluate(ast, env) + except SyntaxException as e: + print(e.msg) + except RuntimeException as e: + print(e.msg)