From ccc78148d8457271fba8513471657af1f28d585e Mon Sep 17 00:00:00 2001 From: Productive2 Date: Thu, 22 Oct 2020 10:19:00 +0200 Subject: [PATCH] added missing files --- JAPL/__init__.py | 4 + JAPL/interpreter.py | 704 +++++++++++++++++++------------------- JAPL/lexer.py | 424 +++++++++++------------ JAPL/meta/__init__.py | 0 JAPL/meta/classtype.py | 14 +- JAPL/meta/environment.py | 142 ++++---- JAPL/meta/exceptions.py | 64 ++-- JAPL/meta/expression.py | 334 +++++++++--------- JAPL/meta/functiontype.py | 16 +- JAPL/meta/looptype.py | 14 +- JAPL/meta/statement.py | 346 +++++++++---------- JAPL/meta/tokenobject.py | 26 +- JAPL/meta/tokentype.py | 110 +++--- JAPL/parser.py | 444 ++++++++++++++++++++++++ JAPL/resolver.py | 281 +++++++++++++++ JAPL/types/__init__.py | 0 JAPL/types/callable.py | 11 + JAPL/types/instance.py | 24 ++ JAPL/types/japlclass.py | 35 ++ JAPL/types/native.py | 175 ++++++++++ JAPL/wrapper.py | 69 ++++ README.md | 68 +++- examples/examples.jpl | 125 +++++++ examples/factorial.jpl | 19 + examples/fib.jpl | 12 + japl.py | 9 + requirements.txt | 1 + setup.py | 16 + 28 files changed, 2388 insertions(+), 1099 deletions(-) create mode 100644 JAPL/__init__.py create mode 100644 JAPL/meta/__init__.py create mode 100644 JAPL/parser.py create mode 100644 JAPL/resolver.py create mode 100644 JAPL/types/__init__.py create mode 100644 JAPL/types/callable.py create mode 100644 JAPL/types/instance.py create mode 100644 JAPL/types/japlclass.py create mode 100644 JAPL/types/native.py create mode 100644 JAPL/wrapper.py create mode 100644 examples/examples.jpl create mode 100644 examples/factorial.jpl create mode 100644 examples/fib.jpl create mode 100644 japl.py create mode 100644 requirements.txt create mode 100644 setup.py diff --git a/JAPL/__init__.py b/JAPL/__init__.py new file mode 100644 index 0000000..3c370aa --- /dev/null +++ b/JAPL/__init__.py @@ -0,0 +1,4 @@ +from .lexer import Lexer +from .parser import Parser +from .interpreter import Interpreter + diff --git a/JAPL/interpreter.py b/JAPL/interpreter.py index 4c4a5ea..e5bf596 100644 --- a/JAPL/interpreter.py +++ b/JAPL/interpreter.py @@ -1,352 +1,352 @@ -import operator -from typing import List -from .types.callable import Callable -from .types.japlclass import JAPLClass -from .types.instance import JAPLInstance -from .meta.environment import Environment -from .meta.tokentype import TokenType -from .meta.exceptions import JAPLError, BreakException, ReturnException -from .types.native import Clock, Type, JAPLFunction, Truthy, Stringify, PrintFunction, IsInstance, IsSubclass, IsSuperclass -from .meta.expression import Expression, Variable, Literal, Logical, Binary, Unary, Grouping, Assignment, Call, Get, Set -from .meta.statement import Statement, StatementExpr, If, While, Del, Break, Return, Var, Block, Function, Class - - -class Interpreter(Expression.Visitor, Statement.Visitor): - """ - An interpreter for the JAPL - programming language - """ - - OPS = {TokenType.MINUS: operator.sub, TokenType.PLUS: operator.add, TokenType.SLASH: operator.truediv, - TokenType.STAR: operator.mul, TokenType.DEQ: operator.eq, TokenType.GT: operator.gt, - TokenType.GE: operator.ge, TokenType.LT: operator.lt, TokenType.LE: operator.le, TokenType.EQ: operator.eq, - TokenType.NE: operator.ne, TokenType.MOD: operator.mod, TokenType.POW: operator.pow} - - def __init__(self): - """Object constructor""" - - self.environment = Environment() - self.locals = {} - self.globals = self.environment - self.globals.define("clock", Clock()) - self.globals.define("type", Type()) - self.globals.define("truthy", Truthy()) - self.globals.define("stringify", Stringify()) - self.globals.define("print", PrintFunction()) - self.globals.define("isinstance", IsInstance()) - self.globals.define("issuperclass", IsSuperclass()) - self.globals.define("issubclass", IsSubclass()) - - def number_operand(self, op, operand): - """ - An helper method to check if the operand - to a unary operator is a number - """ - - if isinstance(operand, (int, float)): - return - raise JAPLError(op, - f"Unsupported unary operator '{op.lexeme}' for object of type '{type(operand).__name__}'") - - def compatible_operands(self, op, left, right): - """ - Helper method to check types when doing binary - operations - """ - - if op.kind == TokenType.SLASH and right == 0: - raise JAPLError(op, "Cannot divide by 0") - elif isinstance(left, (bool, type(None))) or isinstance(right, (bool, type(None))): - if op.kind not in (TokenType.DEQ, TokenType.NE): - raise JAPLError(op, f"Unsupported binary operator '{op.lexeme}' for objects of type '{type(left).__name__}' and '{type(right).__name__}'") - return - elif isinstance(left, (int, float)) and isinstance(right, (int, float)): - return - elif op.kind in (TokenType.PLUS, TokenType.STAR, TokenType.DEQ, TokenType.NE): - if isinstance(left, str) and isinstance(right, str): - return - elif isinstance(left, str) and isinstance(right, int): - return - elif isinstance(left, int) and isinstance(right, str): - return - raise JAPLError(operator, f"Unsupported binary operator '{op.lexeme}' for objects of type '{type(left).__name__}' and '{type(right).__name__}'") - - def visit_literal(self, expr: Literal): - """ - Visits a Literal node in the Abstract Syntax Tree, - returning its value to the visitor - """ - - return expr.value - - def visit_logical(self, expr: Logical): - """Visits a logical node""" - - left = self.eval(expr.left) - if expr.operator.kind == TokenType.OR: - if bool(left): - return left - elif not bool(left): - return self.eval(expr.right) - return self.eval(expr.right) - - def eval(self, expr: Expression): - """ - Evaluates an expression by calling its accept() - method and passing self to it. This mechanism is known - as the 'Visitor Pattern': the expression object will - later call the interpreter's appropriate method to - evaluate itself - """ - - return expr.accept(self) - - def visit_grouping(self, grouping: Grouping): - """ - Visits a Grouping node in the Abstract Syntax Tree, - recursively evaluating its subexpressions - """ - - return self.eval(grouping.expr) - - def visit_unary(self, expr: Unary): - """ - Visits a Unary node in the Abstract Syntax Teee, - returning the negation of the given object, if - the operation is supported - """ - - right = self.eval(expr.right) - self.number_operand(expr.operator, right) - if expr.operator.kind == TokenType.NEG: - return not right - return -right - - def visit_binary(self, expr: Binary): - """ - Visits a Binary node in the Abstract Syntax Tree, - recursively evaulating both operands first and then - performing the operation specified by the operator - """ - - left = self.eval(expr.left) - right = self.eval(expr.right) - self.compatible_operands(expr.operator, left, right) - return self.OPS[expr.operator.kind](left, right) - - def visit_statement_expr(self, stmt: StatementExpr): - """ - Visits an expression statement and evaluates it - """ - - self.eval(stmt.expression) - - def visit_if(self, statement: If): - """ - Visits an If node and evaluates it - """ - - if self.eval(statement.condition): - self.exec(statement.then_branch) - elif statement.else_branch: - self.exec(statement.else_branch) - - def visit_class(self, stmt: Class): - """Visits a class declaration""" - - superclass = None - if stmt.superclass: - superclass = self.eval(stmt.superclass) - if not isinstance(superclass, JAPLClass): - raise JAPLError(stmt.superclass.name, "Superclass must be a class") - self.environment.define(stmt.name.lexeme, None) - if superclass: - environment = Environment(self.environment) - environment.define("super", superclass) - else: - environment = self.environment - methods = {} - for method in stmt.methods: - func = JAPLFunction(method, environment) - methods[method.name.lexeme] = func - klass = JAPLClass(stmt.name.lexeme, methods, superclass) - if superclass: - self.environment = environment.enclosing - self.environment.assign(stmt.name, klass) - - def visit_while(self, statement: While): - """ - Visits a while node and executes it - """ - - while self.eval(statement.condition): - try: - self.exec(statement.body) - except BreakException: - break - - def visit_var_stmt(self, stmt: Var): - """ - Visits a var statement - """ - - val = None - if stmt.init: - val = self.eval(stmt.init) - self.environment.define(stmt.name.lexeme, val) - - def lookup(self, name, expr: Expression): - """ - Performs name lookups in the closest scope - """ - - distance = self.locals.get(expr) - if distance is not None: - return self.environment.get_at(distance, name.lexeme) - else: - return self.globals.get(name) - - def visit_var_expr(self, expr: Variable): - """ - Visits a var expression - """ - - return self.lookup(expr.name, expr) - - def visit_del(self, stmt: Del): - """ - Visits a del expression - """ - - return self.environment.delete(stmt.name) - - def visit_assign(self, stmt: Assignment): - """ - Visits an assignment expression - """ - - right = self.eval(stmt.value) - distance = self.locals.get(stmt) - if distance is not None: - self.environment.assign_at(distance, stmt.name, right) - else: - self.globals.assign(stmt.name, right) - return right - - def visit_block(self, stmt: Block): - """ - Visits a new scope block - """ - - return self.execute_block(stmt.statements, Environment(self.environment)) - - def visit_break(self, stmt: Break): - """ - Visits a break statement - """ - - raise BreakException() - - def visit_call_expr(self, expr: Call): - """ - Visits a call expression - """ - - callee = self.eval(expr.callee) - if not isinstance(callee, Callable): - raise JAPLError(expr.paren, f"'{type(callee).__name__}' is not callable") - arguments = [] - for argument in expr.arguments: - arguments.append(self.eval(argument)) - function = callee - if function.arity != len(arguments): - raise JAPLError(expr.paren, f"Expecting {function.arity} arguments, got {len(arguments)}") - return function.call(self, arguments) - - def execute_block(self, statements: List[Statement], scope: Environment): - """ - Executes a block of statements - """ - - prev = self.environment - try: - self.environment = scope - for statement in statements: - self.exec(statement) - finally: - self.environment = prev - - def visit_return(self, statement: Return): - """ - Visits a return statement - """ - - value = None - if statement.value: - value = self.eval(statement.value) - raise ReturnException(value) - - def visit_function(self, statement: Function): - """ - Visits a function - """ - - function = JAPLFunction(statement, self.environment) - self.environment.define(statement.name.lexeme, function) - - def visit_get(self, expr: Get): - """Visits property get expressions and evaluates them""" - - obj = self.eval(expr.object) - if isinstance(obj, JAPLInstance): - return obj.get(expr.name) - raise JAPLError(expr.name, "Only instances have properties") - - def visit_set(self, expr: Set): - """Visits property set expressions and evaluates them""" - - obj = self.eval(expr.object) - if not isinstance(obj, JAPLInstance): - raise JAPLError(expr, "Only instances have fields") - value = self.eval(expr.value) - obj.set(expr.name, value) - - def visit_this(self, expr): - """Evaluates 'this' expressions""" - - return self.lookup(expr.keyword, expr) - - def visit_super(self, expr): - """Evaluates 'super' expressions""" - - distance = self.locals.get(expr) - superclass = self.environment.get_at(distance, "super") - instance = self.environment.get_at(distance - 1, "this") - meth = superclass.get_method(expr.method.lexeme) - if not meth: - raise JAPLError(expr.method, f"Undefined property '{expr.method.lexeme}'") - return meth.bind(instance) - - def exec(self, statement: Statement): - """ - Executes a statement - """ - - statement.accept(self) - - def interpret(self, statements: List[Statement]): - """ - Executes a JAPL program - """ - - for statement in statements: - self.exec(statement) - - def resolve(self, expr: Expression, depth: int): - """ - Stores the result of the name resolution: this - info will be used later to know exactly in which - environment to look up a given variable - """ - - self.locals[expr] = depth # How many environments to skip! +import operator +from typing import List +from .types.callable import Callable +from .types.japlclass import JAPLClass +from .types.instance import JAPLInstance +from .meta.environment import Environment +from .meta.tokentype import TokenType +from .meta.exceptions import JAPLError, BreakException, ReturnException +from .types.native import Clock, Type, JAPLFunction, Truthy, Stringify, PrintFunction, IsInstance, IsSubclass, IsSuperclass +from .meta.expression import Expression, Variable, Literal, Logical, Binary, Unary, Grouping, Assignment, Call, Get, Set +from .meta.statement import Statement, StatementExpr, If, While, Del, Break, Return, Var, Block, Function, Class + + +class Interpreter(Expression.Visitor, Statement.Visitor): + """ + An interpreter for the JAPL + programming language + """ + + OPS = {TokenType.MINUS: operator.sub, TokenType.PLUS: operator.add, TokenType.SLASH: operator.truediv, + TokenType.STAR: operator.mul, TokenType.DEQ: operator.eq, TokenType.GT: operator.gt, + TokenType.GE: operator.ge, TokenType.LT: operator.lt, TokenType.LE: operator.le, TokenType.EQ: operator.eq, + TokenType.NE: operator.ne, TokenType.MOD: operator.mod, TokenType.POW: operator.pow} + + def __init__(self): + """Object constructor""" + + self.environment = Environment() + self.locals = {} + self.globals = self.environment + self.globals.define("clock", Clock()) + self.globals.define("type", Type()) + self.globals.define("truthy", Truthy()) + self.globals.define("stringify", Stringify()) + self.globals.define("print", PrintFunction()) + self.globals.define("isinstance", IsInstance()) + self.globals.define("issuperclass", IsSuperclass()) + self.globals.define("issubclass", IsSubclass()) + + def number_operand(self, op, operand): + """ + An helper method to check if the operand + to a unary operator is a number + """ + + if isinstance(operand, (int, float)): + return + raise JAPLError(op, + f"Unsupported unary operator '{op.lexeme}' for object of type '{type(operand).__name__}'") + + def compatible_operands(self, op, left, right): + """ + Helper method to check types when doing binary + operations + """ + + if op.kind == TokenType.SLASH and right == 0: + raise JAPLError(op, "Cannot divide by 0") + elif isinstance(left, (bool, type(None))) or isinstance(right, (bool, type(None))): + if op.kind not in (TokenType.DEQ, TokenType.NE): + raise JAPLError(op, f"Unsupported binary operator '{op.lexeme}' for objects of type '{type(left).__name__}' and '{type(right).__name__}'") + return + elif isinstance(left, (int, float)) and isinstance(right, (int, float)): + return + elif op.kind in (TokenType.PLUS, TokenType.STAR, TokenType.DEQ, TokenType.NE): + if isinstance(left, str) and isinstance(right, str): + return + elif isinstance(left, str) and isinstance(right, int): + return + elif isinstance(left, int) and isinstance(right, str): + return + raise JAPLError(operator, f"Unsupported binary operator '{op.lexeme}' for objects of type '{type(left).__name__}' and '{type(right).__name__}'") + + def visit_literal(self, expr: Literal): + """ + Visits a Literal node in the Abstract Syntax Tree, + returning its value to the visitor + """ + + return expr.value + + def visit_logical(self, expr: Logical): + """Visits a logical node""" + + left = self.eval(expr.left) + if expr.operator.kind == TokenType.OR: + if bool(left): + return left + elif not bool(left): + return self.eval(expr.right) + return self.eval(expr.right) + + def eval(self, expr: Expression): + """ + Evaluates an expression by calling its accept() + method and passing self to it. This mechanism is known + as the 'Visitor Pattern': the expression object will + later call the interpreter's appropriate method to + evaluate itself + """ + + return expr.accept(self) + + def visit_grouping(self, grouping: Grouping): + """ + Visits a Grouping node in the Abstract Syntax Tree, + recursively evaluating its subexpressions + """ + + return self.eval(grouping.expr) + + def visit_unary(self, expr: Unary): + """ + Visits a Unary node in the Abstract Syntax Teee, + returning the negation of the given object, if + the operation is supported + """ + + right = self.eval(expr.right) + self.number_operand(expr.operator, right) + if expr.operator.kind == TokenType.NEG: + return not right + return -right + + def visit_binary(self, expr: Binary): + """ + Visits a Binary node in the Abstract Syntax Tree, + recursively evaulating both operands first and then + performing the operation specified by the operator + """ + + left = self.eval(expr.left) + right = self.eval(expr.right) + self.compatible_operands(expr.operator, left, right) + return self.OPS[expr.operator.kind](left, right) + + def visit_statement_expr(self, stmt: StatementExpr): + """ + Visits an expression statement and evaluates it + """ + + self.eval(stmt.expression) + + def visit_if(self, statement: If): + """ + Visits an If node and evaluates it + """ + + if self.eval(statement.condition): + self.exec(statement.then_branch) + elif statement.else_branch: + self.exec(statement.else_branch) + + def visit_class(self, stmt: Class): + """Visits a class declaration""" + + superclass = None + if stmt.superclass: + superclass = self.eval(stmt.superclass) + if not isinstance(superclass, JAPLClass): + raise JAPLError(stmt.superclass.name, "Superclass must be a class") + self.environment.define(stmt.name.lexeme, None) + if superclass: + environment = Environment(self.environment) + environment.define("super", superclass) + else: + environment = self.environment + methods = {} + for method in stmt.methods: + func = JAPLFunction(method, environment) + methods[method.name.lexeme] = func + klass = JAPLClass(stmt.name.lexeme, methods, superclass) + if superclass: + self.environment = environment.enclosing + self.environment.assign(stmt.name, klass) + + def visit_while(self, statement: While): + """ + Visits a while node and executes it + """ + + while self.eval(statement.condition): + try: + self.exec(statement.body) + except BreakException: + break + + def visit_var_stmt(self, stmt: Var): + """ + Visits a var statement + """ + + val = None + if stmt.init: + val = self.eval(stmt.init) + self.environment.define(stmt.name.lexeme, val) + + def lookup(self, name, expr: Expression): + """ + Performs name lookups in the closest scope + """ + + distance = self.locals.get(expr) + if distance is not None: + return self.environment.get_at(distance, name.lexeme) + else: + return self.globals.get(name) + + def visit_var_expr(self, expr: Variable): + """ + Visits a var expression + """ + + return self.lookup(expr.name, expr) + + def visit_del(self, stmt: Del): + """ + Visits a del expression + """ + + return self.environment.delete(stmt.name) + + def visit_assign(self, stmt: Assignment): + """ + Visits an assignment expression + """ + + right = self.eval(stmt.value) + distance = self.locals.get(stmt) + if distance is not None: + self.environment.assign_at(distance, stmt.name, right) + else: + self.globals.assign(stmt.name, right) + return right + + def visit_block(self, stmt: Block): + """ + Visits a new scope block + """ + + return self.execute_block(stmt.statements, Environment(self.environment)) + + def visit_break(self, stmt: Break): + """ + Visits a break statement + """ + + raise BreakException() + + def visit_call_expr(self, expr: Call): + """ + Visits a call expression + """ + + callee = self.eval(expr.callee) + if not isinstance(callee, Callable): + raise JAPLError(expr.paren, f"'{type(callee).__name__}' is not callable") + arguments = [] + for argument in expr.arguments: + arguments.append(self.eval(argument)) + function = callee + if function.arity != len(arguments): + raise JAPLError(expr.paren, f"Expecting {function.arity} arguments, got {len(arguments)}") + return function.call(self, arguments) + + def execute_block(self, statements: List[Statement], scope: Environment): + """ + Executes a block of statements + """ + + prev = self.environment + try: + self.environment = scope + for statement in statements: + self.exec(statement) + finally: + self.environment = prev + + def visit_return(self, statement: Return): + """ + Visits a return statement + """ + + value = None + if statement.value: + value = self.eval(statement.value) + raise ReturnException(value) + + def visit_function(self, statement: Function): + """ + Visits a function + """ + + function = JAPLFunction(statement, self.environment) + self.environment.define(statement.name.lexeme, function) + + def visit_get(self, expr: Get): + """Visits property get expressions and evaluates them""" + + obj = self.eval(expr.object) + if isinstance(obj, JAPLInstance): + return obj.get(expr.name) + raise JAPLError(expr.name, "Only instances have properties") + + def visit_set(self, expr: Set): + """Visits property set expressions and evaluates them""" + + obj = self.eval(expr.object) + if not isinstance(obj, JAPLInstance): + raise JAPLError(expr, "Only instances have fields") + value = self.eval(expr.value) + obj.set(expr.name, value) + + def visit_this(self, expr): + """Evaluates 'this' expressions""" + + return self.lookup(expr.keyword, expr) + + def visit_super(self, expr): + """Evaluates 'super' expressions""" + + distance = self.locals.get(expr) + superclass = self.environment.get_at(distance, "super") + instance = self.environment.get_at(distance - 1, "this") + meth = superclass.get_method(expr.method.lexeme) + if not meth: + raise JAPLError(expr.method, f"Undefined property '{expr.method.lexeme}'") + return meth.bind(instance) + + def exec(self, statement: Statement): + """ + Executes a statement + """ + + statement.accept(self) + + def interpret(self, statements: List[Statement]): + """ + Executes a JAPL program + """ + + for statement in statements: + self.exec(statement) + + def resolve(self, expr: Expression, depth: int): + """ + Stores the result of the name resolution: this + info will be used later to know exactly in which + environment to look up a given variable + """ + + self.locals[expr] = depth # How many environments to skip! diff --git a/JAPL/lexer.py b/JAPL/lexer.py index ac24f1a..f30a846 100644 --- a/JAPL/lexer.py +++ b/JAPL/lexer.py @@ -1,212 +1,212 @@ -from .meta.tokenobject import Token -from .meta.tokentype import TokenType -from .meta.exceptions import ParseError -from typing import List - - -class Lexer(object): - """ - A simple tokenizer for the JAPL programming - language, scans a input source file and - produces a list of tokens. Some errors - are caught here as well. - """ - - TOKENS = {"(": TokenType.LP, ")": TokenType.RP, - "{": TokenType.LB, "}": TokenType.RB, - ".": TokenType.DOT, ",": TokenType.COMMA, - "-": TokenType.MINUS, "+": TokenType.PLUS, - ";": TokenType.SEMICOLON, "*": TokenType.STAR, - ">": TokenType.GT, "<": TokenType.LT, - "=": TokenType.EQ, "!": TokenType.NEG, - "/": TokenType.SLASH, "%": TokenType.MOD} - - RESERVED = {"or": TokenType.OR, "and": TokenType.AND, - "class": TokenType.CLASS, "fun": TokenType.FUN, - "if": TokenType.IF, "else": TokenType.ELSE, - "for": TokenType.FOR, "while": TokenType.WHILE, - "var": TokenType.VAR, "nil": TokenType.NIL, - "true": TokenType.TRUE, "false": TokenType.FALSE, - "return": TokenType.RETURN, - "this": TokenType.THIS, "super": TokenType.SUPER, - "del": TokenType.DEL, "break": TokenType.BREAK} - - def __init__(self, source: str): - """Object constructor""" - - self.source = source - self.tokens: List[Token] = [] - self.line: int = 1 # Points to the line being lexed - self.start: int = 0 # The position of the first character of the current lexeme - self.current: int = 0 # The position of the current character being lexed - - def step(self) -> str: - """ - 'Steps' one character in the source code and returns it - """ - - if self.done(): - return "" - self.current += 1 - return self.source[self.current - 1] - - def peek(self) -> str: - """ - Returns the current character without consuming it - or an empty string if all text has been consumed - """ - - if self.done(): - return "" - return self.source[self.current] - - def peek_next(self) -> str: - """ - Returns the next character after self.current - or an empty string if the input has been consumed - """ - - if self.current + 1 >= len(self.source): - return "" - return self.source[self.current + 1] - - def string(self, delimiter: str): - """Parses a string literal""" - - while self.peek() != delimiter and not self.done(): - if self.peek() == "\n": - self.line += 1 - self.step() - if self.done(): - raise ParseError(f"unterminated string literal at line {self.line}") - self.step() # Consume the '"' - value = self.source[self.start + 1:self.current - 1] # Get the actual string - self.tokens.append(self.create_token(TokenType.STR, value)) - - def number(self): - """Parses a number literal""" - - convert = int - while self.peek().isdigit(): - self.step() - if self.peek() == ".": - self.step() # Consume the "." - while self.peek().isdigit(): - self.step() - convert = float - self.tokens.append(self.create_token(TokenType.NUM, - convert(self.source[self.start:self.current]))) - - def identifier(self): - """Parses identifiers and reserved keywords""" - - while self.peek().isalnum() or self.is_identifier(self.peek()): - self.step() - kind = TokenType.ID - value = self.source[self.start:self.current] - if self.RESERVED.get(value, None): - kind = self.RESERVED[value] - self.tokens.append(self.create_token(kind)) - - def comment(self): - """Handles multi-line comments""" - - closed = False - while not self.done(): - end = self.peek() + self.peek_next() - if end == "/*": # Nested comments - self.step() - self.step() - self.comment() - elif end == "*/": - closed = True - self.step() # Consume the two ends - self.step() - break - self.step() - if self.done() and not closed: - raise ParseError(f"Unexpected EOF at line {self.line}") - - def match(self, char: str) -> bool: - """ - Returns True if the current character in self.source matches - the given character - """ - - if self.done(): - return False - elif self.source[self.current] != char: - return False - self.current += 1 - return True - - def done(self) -> bool: - """ - Helper method that's used by the lexer - to know if all source has been consumed - """ - - return self.current >= len(self.source) - - def create_token(self, kind: TokenType, literal: object = None) -> Token: - """ - Creates and returns a token object - """ - - return Token(kind, self.source[self.start:self.current], literal, self.line) - - def is_identifier(self, char: str): - """Returns if a character can be an identifier""" - - if char.isalpha() or char in ("_", ): # More coming soon - return True - - def scan_token(self): - """ - Scans for a single token and adds it to - self.tokens - """ - - char = self.step() - if char in (" ", "\t", "\r"): # Useless characters - return - elif char == "\n": # New line - self.line += 1 - elif char in ("'", '"'): # A string literal - self.string(char) - elif char.isdigit(): - self.number() - elif self.is_identifier(char): # Identifier or reserved keyword - self.identifier() - elif char in self.TOKENS: - if char == "/" and self.match("/"): - while self.peek() != "\n" and not self.done(): - self.step() # Who cares about comments? - elif char == "/" and self.match("*"): - self.comment() - elif char == "=" and self.match("="): - self.tokens.append(self.create_token(TokenType.DEQ)) - elif char == ">" and self.match("="): - self.tokens.append(self.create_token(TokenType.GE)) - elif char == "<" and self.match("="): - self.tokens.append(self.create_token(TokenType.LE)) - elif char == "!" and self.match("="): - self.tokens.append(self.create_token(TokenType.NE)) - elif char == "*" and self.match("*"): - self.tokens.append(self.create_token(TokenType.POW)) - else: - self.tokens.append(self.create_token(self.TOKENS[char])) - else: - raise ParseError(f"unexpected character '{char}' at line {self.line}") - - def lex(self) -> List[Token]: - """ - Performs lexical analysis on self.source - and returns a list of tokens - """ - - while not self.done(): - self.start = self.current - self.scan_token() - self.tokens.append(Token(TokenType.EOF, "", None, self.line)) - return self.tokens +from .meta.tokenobject import Token +from .meta.tokentype import TokenType +from .meta.exceptions import ParseError +from typing import List + + +class Lexer(object): + """ + A simple tokenizer for the JAPL programming + language, scans a input source file and + produces a list of tokens. Some errors + are caught here as well. + """ + + TOKENS = {"(": TokenType.LP, ")": TokenType.RP, + "{": TokenType.LB, "}": TokenType.RB, + ".": TokenType.DOT, ",": TokenType.COMMA, + "-": TokenType.MINUS, "+": TokenType.PLUS, + ";": TokenType.SEMICOLON, "*": TokenType.STAR, + ">": TokenType.GT, "<": TokenType.LT, + "=": TokenType.EQ, "!": TokenType.NEG, + "/": TokenType.SLASH, "%": TokenType.MOD} + + RESERVED = {"or": TokenType.OR, "and": TokenType.AND, + "class": TokenType.CLASS, "fun": TokenType.FUN, + "if": TokenType.IF, "else": TokenType.ELSE, + "for": TokenType.FOR, "while": TokenType.WHILE, + "var": TokenType.VAR, "nil": TokenType.NIL, + "true": TokenType.TRUE, "false": TokenType.FALSE, + "return": TokenType.RETURN, + "this": TokenType.THIS, "super": TokenType.SUPER, + "del": TokenType.DEL, "break": TokenType.BREAK} + + def __init__(self, source: str): + """Object constructor""" + + self.source = source + self.tokens: List[Token] = [] + self.line: int = 1 # Points to the line being lexed + self.start: int = 0 # The position of the first character of the current lexeme + self.current: int = 0 # The position of the current character being lexed + + def step(self) -> str: + """ + 'Steps' one character in the source code and returns it + """ + + if self.done(): + return "" + self.current += 1 + return self.source[self.current - 1] + + def peek(self) -> str: + """ + Returns the current character without consuming it + or an empty string if all text has been consumed + """ + + if self.done(): + return "" + return self.source[self.current] + + def peek_next(self) -> str: + """ + Returns the next character after self.current + or an empty string if the input has been consumed + """ + + if self.current + 1 >= len(self.source): + return "" + return self.source[self.current + 1] + + def string(self, delimiter: str): + """Parses a string literal""" + + while self.peek() != delimiter and not self.done(): + if self.peek() == "\n": + self.line += 1 + self.step() + if self.done(): + raise ParseError(f"unterminated string literal at line {self.line}") + self.step() # Consume the '"' + value = self.source[self.start + 1:self.current - 1] # Get the actual string + self.tokens.append(self.create_token(TokenType.STR, value)) + + def number(self): + """Parses a number literal""" + + convert = int + while self.peek().isdigit(): + self.step() + if self.peek() == ".": + self.step() # Consume the "." + while self.peek().isdigit(): + self.step() + convert = float + self.tokens.append(self.create_token(TokenType.NUM, + convert(self.source[self.start:self.current]))) + + def identifier(self): + """Parses identifiers and reserved keywords""" + + while self.peek().isalnum() or self.is_identifier(self.peek()): + self.step() + kind = TokenType.ID + value = self.source[self.start:self.current] + if self.RESERVED.get(value, None): + kind = self.RESERVED[value] + self.tokens.append(self.create_token(kind)) + + def comment(self): + """Handles multi-line comments""" + + closed = False + while not self.done(): + end = self.peek() + self.peek_next() + if end == "/*": # Nested comments + self.step() + self.step() + self.comment() + elif end == "*/": + closed = True + self.step() # Consume the two ends + self.step() + break + self.step() + if self.done() and not closed: + raise ParseError(f"Unexpected EOF at line {self.line}") + + def match(self, char: str) -> bool: + """ + Returns True if the current character in self.source matches + the given character + """ + + if self.done(): + return False + elif self.source[self.current] != char: + return False + self.current += 1 + return True + + def done(self) -> bool: + """ + Helper method that's used by the lexer + to know if all source has been consumed + """ + + return self.current >= len(self.source) + + def create_token(self, kind: TokenType, literal: object = None) -> Token: + """ + Creates and returns a token object + """ + + return Token(kind, self.source[self.start:self.current], literal, self.line) + + def is_identifier(self, char: str): + """Returns if a character can be an identifier""" + + if char.isalpha() or char in ("_", ): # More coming soon + return True + + def scan_token(self): + """ + Scans for a single token and adds it to + self.tokens + """ + + char = self.step() + if char in (" ", "\t", "\r"): # Useless characters + return + elif char == "\n": # New line + self.line += 1 + elif char in ("'", '"'): # A string literal + self.string(char) + elif char.isdigit(): + self.number() + elif self.is_identifier(char): # Identifier or reserved keyword + self.identifier() + elif char in self.TOKENS: + if char == "/" and self.match("/"): + while self.peek() != "\n" and not self.done(): + self.step() # Who cares about comments? + elif char == "/" and self.match("*"): + self.comment() + elif char == "=" and self.match("="): + self.tokens.append(self.create_token(TokenType.DEQ)) + elif char == ">" and self.match("="): + self.tokens.append(self.create_token(TokenType.GE)) + elif char == "<" and self.match("="): + self.tokens.append(self.create_token(TokenType.LE)) + elif char == "!" and self.match("="): + self.tokens.append(self.create_token(TokenType.NE)) + elif char == "*" and self.match("*"): + self.tokens.append(self.create_token(TokenType.POW)) + else: + self.tokens.append(self.create_token(self.TOKENS[char])) + else: + raise ParseError(f"unexpected character '{char}' at line {self.line}") + + def lex(self) -> List[Token]: + """ + Performs lexical analysis on self.source + and returns a list of tokens + """ + + while not self.done(): + self.start = self.current + self.scan_token() + self.tokens.append(Token(TokenType.EOF, "", None, self.line)) + return self.tokens diff --git a/JAPL/meta/__init__.py b/JAPL/meta/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/JAPL/meta/classtype.py b/JAPL/meta/classtype.py index c257e05..3ce839b 100644 --- a/JAPL/meta/classtype.py +++ b/JAPL/meta/classtype.py @@ -1,7 +1,7 @@ -from enum import Enum, auto - - -class ClassType(Enum): - - NONE = auto() - CLASS = auto() +from enum import Enum, auto + + +class ClassType(Enum): + + NONE = auto() + CLASS = auto() diff --git a/JAPL/meta/environment.py b/JAPL/meta/environment.py index 02f37e5..b6be5d1 100644 --- a/JAPL/meta/environment.py +++ b/JAPL/meta/environment.py @@ -1,71 +1,71 @@ -from .exceptions import JAPLError -from .tokenobject import Token -from .expression import Variable - - -class Environment(object): - """ - A wrapper around a hashmap representing - a scope - """ - - def __init__(self, enclosing=None): - """Object constructor""" - - self.map = {} - self.enclosing = enclosing - - def define(self, name: str, attr: object): - """Defines a new variable in the scope""" - - self.map[name] = attr - - def get(self, name: Token): - """Gets a variable""" - - if name.lexeme in self.map: - return self.map[name.lexeme] - elif self.enclosing: - return self.enclosing.get(name) - raise JAPLError(name, f"Undefined name '{name.lexeme}'") - - def get_at(self, distance, name): - """Gets a variable in a specific scope""" - - return self.ancestor(distance).map.get(name) - - def ancestor(self, distance): - """Finds the scope specified by distance""" - - env = self - for _ in range(distance): - env = env.enclosing - return env - - def assign_at(self, distance, name, value): - """Same as get_at, but assigns instead of retrieving""" - - self.ancestor(distance).map[name.lexeme] = value - - def delete(self, var): - """Deletes a variable""" - - if var.name.lexeme in self.map: - del self.map[var.name.lexeme] - elif self.enclosing: - self.enclosing.delete(var) - else: - raise JAPLError(var.name, f"Undefined name '{var.name.lexeme}'") - - def assign(self, name: Token, value: object): - """Assigns a variable""" - - if name.lexeme in self.map: - if isinstance(value, Variable): - self.map[name.lexeme] = self.get(value.name) - else: - self.map[name.lexeme] = value - elif self.enclosing: - self.enclosing.assign(name, value) - else: - raise JAPLError(name, f"Undefined name '{name.lexeme}'") +from .exceptions import JAPLError +from .tokenobject import Token +from .expression import Variable + + +class Environment(object): + """ + A wrapper around a hashmap representing + a scope + """ + + def __init__(self, enclosing=None): + """Object constructor""" + + self.map = {} + self.enclosing = enclosing + + def define(self, name: str, attr: object): + """Defines a new variable in the scope""" + + self.map[name] = attr + + def get(self, name: Token): + """Gets a variable""" + + if name.lexeme in self.map: + return self.map[name.lexeme] + elif self.enclosing: + return self.enclosing.get(name) + raise JAPLError(name, f"Undefined name '{name.lexeme}'") + + def get_at(self, distance, name): + """Gets a variable in a specific scope""" + + return self.ancestor(distance).map.get(name) + + def ancestor(self, distance): + """Finds the scope specified by distance""" + + env = self + for _ in range(distance): + env = env.enclosing + return env + + def assign_at(self, distance, name, value): + """Same as get_at, but assigns instead of retrieving""" + + self.ancestor(distance).map[name.lexeme] = value + + def delete(self, var): + """Deletes a variable""" + + if var.name.lexeme in self.map: + del self.map[var.name.lexeme] + elif self.enclosing: + self.enclosing.delete(var) + else: + raise JAPLError(var.name, f"Undefined name '{var.name.lexeme}'") + + def assign(self, name: Token, value: object): + """Assigns a variable""" + + if name.lexeme in self.map: + if isinstance(value, Variable): + self.map[name.lexeme] = self.get(value.name) + else: + self.map[name.lexeme] = value + elif self.enclosing: + self.enclosing.assign(name, value) + else: + raise JAPLError(name, f"Undefined name '{name.lexeme}'") diff --git a/JAPL/meta/exceptions.py b/JAPL/meta/exceptions.py index c67ecfc..de9f18f 100644 --- a/JAPL/meta/exceptions.py +++ b/JAPL/meta/exceptions.py @@ -1,32 +1,32 @@ -from .tokentype import TokenType - - -class JAPLError(BaseException): - """JAPL's exceptions base class""" - - def __repr__(self): - return self.args[1] - - -class ParseError(JAPLError): - """An error occurred while parsing""" - - def __repr__(self): - if len(self.args) > 1: - message, token = self.args - if token.kind == TokenType.EOF: - return f"Unexpected error while parsing at line {token.line}, at end: {message}" - else: - return f"Unexpected error while parsing at line {token.line} at '{token.lexeme}': {message}" - return self.args[0] - - def __str__(self): - return self.__repr__() - - -class BreakException(JAPLError): - """Notifies a loop that it's time to break""" - - -class ReturnException(JAPLError): - """Notifies a function that it's time to return""" +from .tokentype import TokenType + + +class JAPLError(BaseException): + """JAPL's exceptions base class""" + + def __repr__(self): + return self.args[1] + + +class ParseError(JAPLError): + """An error occurred while parsing""" + + def __repr__(self): + if len(self.args) > 1: + message, token = self.args + if token.kind == TokenType.EOF: + return f"Unexpected error while parsing at line {token.line}, at end: {message}" + else: + return f"Unexpected error while parsing at line {token.line} at '{token.lexeme}': {message}" + return self.args[0] + + def __str__(self): + return self.__repr__() + + +class BreakException(JAPLError): + """Notifies a loop that it's time to break""" + + +class ReturnException(JAPLError): + """Notifies a function that it's time to return""" diff --git a/JAPL/meta/expression.py b/JAPL/meta/expression.py index e97312c..f62beb6 100644 --- a/JAPL/meta/expression.py +++ b/JAPL/meta/expression.py @@ -1,167 +1,167 @@ -from dataclasses import dataclass -from abc import ABC, abstractmethod -from .tokenobject import Token -from typing import List - - -class Expression(object): - """ - An object representing a JAPL expression. - This class is not meant to be instantiated directly, - inherit from it instead! - """ - - def accept(self, visitor): - raise NotImplementedError - - class Visitor(ABC): - """ - Visitor abstract base class to implement - the Visitor pattern - """ - - @abstractmethod - def visit_literal(self, visitor): - raise NotImplementedError - - @abstractmethod - def visit_binary(self, visitor): - raise NotImplementedError - - @abstractmethod - def visit_grouping(self, visitor): - raise NotImplementedError - - @abstractmethod - def visit_unary(self, visitor): - raise NotImplementedError - - @staticmethod - def visit_get(self, visitor): - raise NotImplementedError - - @staticmethod - def visit_set(self, visitor): - raise NotImplementedError - - -@dataclass -class Binary(Expression): - left: Expression - operator: Token - right: Expression - - def accept(self, visitor): - return visitor.visit_binary(self) - - -@dataclass -class Unary(Expression): - operator: Token - right: Expression - - def accept(self, visitor): - return visitor.visit_unary(self) - - -@dataclass -class Literal(Expression): - value: object - - def accept(self, visitor): - return visitor.visit_literal(self) - - -@dataclass -class Grouping(Expression): - expr: Expression - - def accept(self, visitor): - return visitor.visit_grouping(self) - - -@dataclass -class Variable(Expression): - name: Token - - def accept(self, visitor): - return visitor.visit_var_expr(self) - - def __hash__(self): - return super().__hash__() - - -@dataclass -class Assignment(Expression): - name: Token - value: Expression - - def accept(self, visitor): - return visitor.visit_assign(self) - - def __hash__(self): - return super().__hash__() - - -@dataclass -class Logical(Expression): - left: Expression - operator: Token - right: Expression - - def accept(self, visitor): - return visitor.visit_logical(self) - - -@dataclass -class Call(Expression): - callee: Expression - paren: Token - arguments: List[Expression] = () - - def accept(self, visitor): - return visitor.visit_call_expr(self) - - -@dataclass -class Get(Expression): - object: Expression - name: Token - - def accept(self, visitor): - return visitor.visit_get(self) - - -@dataclass -class Set(Expression): - object: Expression - name: Token - value: Expression - - def accept(self, visitor): - return visitor.visit_set(self) - - -@dataclass -class This(Expression): - keyword: Token - - def accept(self, visitor): - return visitor.visit_this(self) - - def __hash__(self): - return super().__hash__() - - -@dataclass -class Super(Expression): - keyword: Token - method: Token - - def accept(self, visitor): - return visitor.visit_super(self) - - def __hash__(self): - return super().__hash__() - - +from dataclasses import dataclass +from abc import ABC, abstractmethod +from .tokenobject import Token +from typing import List + + +class Expression(object): + """ + An object representing a JAPL expression. + This class is not meant to be instantiated directly, + inherit from it instead! + """ + + def accept(self, visitor): + raise NotImplementedError + + class Visitor(ABC): + """ + Visitor abstract base class to implement + the Visitor pattern + """ + + @abstractmethod + def visit_literal(self, visitor): + raise NotImplementedError + + @abstractmethod + def visit_binary(self, visitor): + raise NotImplementedError + + @abstractmethod + def visit_grouping(self, visitor): + raise NotImplementedError + + @abstractmethod + def visit_unary(self, visitor): + raise NotImplementedError + + @staticmethod + def visit_get(self, visitor): + raise NotImplementedError + + @staticmethod + def visit_set(self, visitor): + raise NotImplementedError + + +@dataclass +class Binary(Expression): + left: Expression + operator: Token + right: Expression + + def accept(self, visitor): + return visitor.visit_binary(self) + + +@dataclass +class Unary(Expression): + operator: Token + right: Expression + + def accept(self, visitor): + return visitor.visit_unary(self) + + +@dataclass +class Literal(Expression): + value: object + + def accept(self, visitor): + return visitor.visit_literal(self) + + +@dataclass +class Grouping(Expression): + expr: Expression + + def accept(self, visitor): + return visitor.visit_grouping(self) + + +@dataclass +class Variable(Expression): + name: Token + + def accept(self, visitor): + return visitor.visit_var_expr(self) + + def __hash__(self): + return super().__hash__() + + +@dataclass +class Assignment(Expression): + name: Token + value: Expression + + def accept(self, visitor): + return visitor.visit_assign(self) + + def __hash__(self): + return super().__hash__() + + +@dataclass +class Logical(Expression): + left: Expression + operator: Token + right: Expression + + def accept(self, visitor): + return visitor.visit_logical(self) + + +@dataclass +class Call(Expression): + callee: Expression + paren: Token + arguments: List[Expression] = () + + def accept(self, visitor): + return visitor.visit_call_expr(self) + + +@dataclass +class Get(Expression): + object: Expression + name: Token + + def accept(self, visitor): + return visitor.visit_get(self) + + +@dataclass +class Set(Expression): + object: Expression + name: Token + value: Expression + + def accept(self, visitor): + return visitor.visit_set(self) + + +@dataclass +class This(Expression): + keyword: Token + + def accept(self, visitor): + return visitor.visit_this(self) + + def __hash__(self): + return super().__hash__() + + +@dataclass +class Super(Expression): + keyword: Token + method: Token + + def accept(self, visitor): + return visitor.visit_super(self) + + def __hash__(self): + return super().__hash__() + + diff --git a/JAPL/meta/functiontype.py b/JAPL/meta/functiontype.py index ed60a00..1ab1777 100644 --- a/JAPL/meta/functiontype.py +++ b/JAPL/meta/functiontype.py @@ -1,8 +1,8 @@ -from enum import Enum, auto - - -class FunctionType(Enum): - NONE = auto() - FUNCTION = auto() - METHOD = auto() - INIT = auto() +from enum import Enum, auto + + +class FunctionType(Enum): + NONE = auto() + FUNCTION = auto() + METHOD = auto() + INIT = auto() diff --git a/JAPL/meta/looptype.py b/JAPL/meta/looptype.py index a946143..53290c6 100644 --- a/JAPL/meta/looptype.py +++ b/JAPL/meta/looptype.py @@ -1,7 +1,7 @@ -from enum import Enum, auto - - -class LoopType(Enum): - - NONE = auto() - WHILE = auto() +from enum import Enum, auto + + +class LoopType(Enum): + + NONE = auto() + WHILE = auto() diff --git a/JAPL/meta/statement.py b/JAPL/meta/statement.py index 0a811c0..ecc1ed8 100644 --- a/JAPL/meta/statement.py +++ b/JAPL/meta/statement.py @@ -1,173 +1,173 @@ -from abc import ABC, abstractmethod -from dataclasses import dataclass -from .expression import Expression, Variable -from .tokenobject import Token -from typing import List, Any - - -class Statement(object): - """ - A Base Class representing JAPL statements - """ - - def accept(self, visitor): - raise NotImplementedError - - class Visitor(ABC): - """Wrapper to implement the Visitor Pattern""" - - - @abstractmethod - def visit_statement_expr(self, visitor): - raise NotImplementedError - - @abstractmethod - def visit_var_stmt(self, visitor): - raise NotImplementedError - - @abstractmethod - def visit_del(self, visitor): - raise NotImplementedError - - @abstractmethod - def visit_block(self, visitor): - raise NotImplementedError - - @abstractmethod - def visit_if(self, visitor): - raise NotImplementedError - - @abstractmethod - def visit_while(self, visitor): - raise NotImplementedError - - @abstractmethod - def visit_break(self, visitor): - raise NotImplementedError - - @abstractmethod - def visit_function(self, visitor): - raise NotImplementedError - - @abstractmethod - def visit_return(self, visitor): - raise NotImplementedError - - @staticmethod - def visit_class(self, visitor): - raise NotImplementedError - - -@dataclass -class StatementExpr(Statement): - """ - An expression statement - """ - - expression: Expression - - def accept(self, visitor): - visitor.visit_statement_expr(self) - - -@dataclass -class Var(Statement): - """ - A var statement - """ - - name: Token - init: Expression = None - - def accept(self, visitor): - visitor.visit_var_stmt(self) - - -@dataclass -class Del(Statement): - """ - A del statement - """ - - name: Any - - def accept(self, visitor): - visitor.visit_del(self) - - -@dataclass -class Block(Statement): - """A block statement""" - - statements: List[Statement] - - def accept(self, visitor): - visitor.visit_block(self) - - -@dataclass -class If(Statement): - """An if statement""" - - condition: Expression - then_branch: Statement - else_branch: Statement - - def accept(self, visitor): - visitor.visit_if(self) - - -@dataclass -class While(Statement): - """A while statement""" - - condition: Expression - body: Statement - - def accept(self, visitor): - visitor.visit_while(self) - - -@dataclass -class Break(Statement): - """A break statement""" - - token: Token - - def accept(self, visitor): - visitor.visit_break(self) - - -@dataclass -class Function(Statement): - """A function statement""" - - name: Token - params: List[Token] - body: List[Statement] - - def accept(self, visitor): - visitor.visit_function(self) - - -@dataclass -class Return(Statement, BaseException): - """A return statement""" - - keyword: Token - value: Expression - - def accept(self, visitor): - visitor.visit_return(self) - - -@dataclass -class Class(Statement): - """A class statement""" - - name: Token - methods: list - superclass: Variable - - def accept(self, visitor): - visitor.visit_class(self) +from abc import ABC, abstractmethod +from dataclasses import dataclass +from .expression import Expression, Variable +from .tokenobject import Token +from typing import List, Any + + +class Statement(object): + """ + A Base Class representing JAPL statements + """ + + def accept(self, visitor): + raise NotImplementedError + + class Visitor(ABC): + """Wrapper to implement the Visitor Pattern""" + + + @abstractmethod + def visit_statement_expr(self, visitor): + raise NotImplementedError + + @abstractmethod + def visit_var_stmt(self, visitor): + raise NotImplementedError + + @abstractmethod + def visit_del(self, visitor): + raise NotImplementedError + + @abstractmethod + def visit_block(self, visitor): + raise NotImplementedError + + @abstractmethod + def visit_if(self, visitor): + raise NotImplementedError + + @abstractmethod + def visit_while(self, visitor): + raise NotImplementedError + + @abstractmethod + def visit_break(self, visitor): + raise NotImplementedError + + @abstractmethod + def visit_function(self, visitor): + raise NotImplementedError + + @abstractmethod + def visit_return(self, visitor): + raise NotImplementedError + + @staticmethod + def visit_class(self, visitor): + raise NotImplementedError + + +@dataclass +class StatementExpr(Statement): + """ + An expression statement + """ + + expression: Expression + + def accept(self, visitor): + visitor.visit_statement_expr(self) + + +@dataclass +class Var(Statement): + """ + A var statement + """ + + name: Token + init: Expression = None + + def accept(self, visitor): + visitor.visit_var_stmt(self) + + +@dataclass +class Del(Statement): + """ + A del statement + """ + + name: Any + + def accept(self, visitor): + visitor.visit_del(self) + + +@dataclass +class Block(Statement): + """A block statement""" + + statements: List[Statement] + + def accept(self, visitor): + visitor.visit_block(self) + + +@dataclass +class If(Statement): + """An if statement""" + + condition: Expression + then_branch: Statement + else_branch: Statement + + def accept(self, visitor): + visitor.visit_if(self) + + +@dataclass +class While(Statement): + """A while statement""" + + condition: Expression + body: Statement + + def accept(self, visitor): + visitor.visit_while(self) + + +@dataclass +class Break(Statement): + """A break statement""" + + token: Token + + def accept(self, visitor): + visitor.visit_break(self) + + +@dataclass +class Function(Statement): + """A function statement""" + + name: Token + params: List[Token] + body: List[Statement] + + def accept(self, visitor): + visitor.visit_function(self) + + +@dataclass +class Return(Statement, BaseException): + """A return statement""" + + keyword: Token + value: Expression + + def accept(self, visitor): + visitor.visit_return(self) + + +@dataclass +class Class(Statement): + """A class statement""" + + name: Token + methods: list + superclass: Variable + + def accept(self, visitor): + visitor.visit_class(self) diff --git a/JAPL/meta/tokenobject.py b/JAPL/meta/tokenobject.py index 998c5e2..5607348 100644 --- a/JAPL/meta/tokenobject.py +++ b/JAPL/meta/tokenobject.py @@ -1,13 +1,13 @@ -from dataclasses import dataclass -from .tokentype import TokenType - -@dataclass -class Token(object): - """The representation of a JAPL token""" - - kind: TokenType - lexeme: str - literal: object - line: int - - +from dataclasses import dataclass +from .tokentype import TokenType + +@dataclass +class Token(object): + """The representation of a JAPL token""" + + kind: TokenType + lexeme: str + literal: object + line: int + + diff --git a/JAPL/meta/tokentype.py b/JAPL/meta/tokentype.py index bbc3178..0036010 100644 --- a/JAPL/meta/tokentype.py +++ b/JAPL/meta/tokentype.py @@ -1,55 +1,55 @@ -from enum import Enum, auto - -class TokenType(Enum): - """ - An enumeration for all JAPL types - """ - - LP = auto() - RP = auto() - LB = auto() - RB = auto() - COMMA = auto() - DOT = auto() - PLUS = auto() - MINUS = auto() - SLASH = auto() - SEMICOLON = auto() - STAR = auto() - - - NEG = auto() - NE = auto() - EQ = auto() - DEQ = auto() - GT = auto() - LT = auto() - GE = auto() - LE = auto() - MOD = auto() - POW = auto() - - ID = auto() - STR = auto() - NUM = auto() - - - AND = auto() - CLASS = auto() - ELSE = auto() - FOR = auto() - FUN = auto() - FALSE = auto() - IF = auto() - NIL = auto() - OR = auto() - RETURN = auto() - SUPER = auto() - THIS = auto() - TRUE = auto() - VAR = auto() - WHILE = auto() - DEL = auto() - BREAK = auto() - - EOF = auto() +from enum import Enum, auto + +class TokenType(Enum): + """ + An enumeration for all JAPL types + """ + + LP = auto() + RP = auto() + LB = auto() + RB = auto() + COMMA = auto() + DOT = auto() + PLUS = auto() + MINUS = auto() + SLASH = auto() + SEMICOLON = auto() + STAR = auto() + + + NEG = auto() + NE = auto() + EQ = auto() + DEQ = auto() + GT = auto() + LT = auto() + GE = auto() + LE = auto() + MOD = auto() + POW = auto() + + ID = auto() + STR = auto() + NUM = auto() + + + AND = auto() + CLASS = auto() + ELSE = auto() + FOR = auto() + FUN = auto() + FALSE = auto() + IF = auto() + NIL = auto() + OR = auto() + RETURN = auto() + SUPER = auto() + THIS = auto() + TRUE = auto() + VAR = auto() + WHILE = auto() + DEL = auto() + BREAK = auto() + + EOF = auto() diff --git a/JAPL/parser.py b/JAPL/parser.py new file mode 100644 index 0000000..3527476 --- /dev/null +++ b/JAPL/parser.py @@ -0,0 +1,444 @@ +from .meta.exceptions import ParseError +from .meta.tokentype import TokenType +from .meta.tokenobject import Token +from typing import List, Union +from .meta.expression import Variable, Assignment, Logical, Call, Binary, Unary, Literal, Grouping, Expression, Get, Set, This, Super +from .meta.statement import StatementExpr, Var, Del, Block, If, While, Break, Function, Return, Class + + +class Parser(object): + """A simple recursive-descent top-down parser""" + + def __init__(self, tokens: List[Token]): + """Object constructor""" + + self.tokens = tokens + self.current: int = 0 + + def check(self, token_type): + """ + Helper method for self.match + """ + + if self.done(): + return False + elif self.peek().kind == token_type: + return True + return False + + def throw(self, token: Token, message: str) -> ParseError: + """Returns ParseError with the given message""" + + return ParseError(token, message) + + def synchronize(self): + """Synchronizes the parser's state to recover after + an error occurred while parsing""" + + self.step() + while not self.done(): + if self.previous().kind == TokenType.SEMICOLON: + break + else: + token_type = self.peek().kind + if token_type in ( + TokenType.IF, TokenType.CLASS, TokenType.VAR, TokenType.FOR, TokenType.WHILE, + TokenType.RETURN, TokenType.FUN + ): + return + self.step() + + def peek(self): + """ + Returns a token without consuming it + """ + + return self.tokens[self.current] + + def previous(self): + """ + Returns the most recently consumed token + """ + + return self.tokens[self.current - 1] + + def done(self): + """ + Returns True if we reached EOF + """ + + return self.peek().kind == TokenType.EOF + + def match(self, *types: Union[TokenType, List[TokenType]]): + """ + Checks if the current token matches + any of the given token type(s) + """ + + for token_type in types: + if self.check(token_type): + self.step() + return True + return False + + def consume(self, token_type, message: str): + """ + Consumes a token, raises an error + with the given message if the current token + differs from the expected one + """ + + if self.check(token_type): + return self.step() + raise self.throw(self.peek(), message) + + def primary(self): + """Parses unary expressions (literals)""" + + if self.match(TokenType.FALSE): + return Literal(False) + elif self.match(TokenType.TRUE): + return Literal(True) + elif self.match(TokenType.NIL): + return Literal(None) + elif self.match(TokenType.NUM, TokenType.STR): + return Literal(self.previous().literal) + elif self.match(TokenType.LP): + expr: Expression = self.expression() + self.consume(TokenType.RP, "Unexpected error while parsing parenthesized expression") + return Grouping(expr) + elif self.match(TokenType.ID): + return Variable(self.previous()) + elif self.match(TokenType.SUPER): + keyword = self.previous() + self.consume(TokenType.DOT, "Expecting '.' after 'super'") + method = self.consume(TokenType.ID, "Expecting superclass method name") + return Super(keyword, method) + elif self.match(TokenType.THIS): + return This(self.previous()) + raise self.throw(self.peek(), "Invalid syntax") + + def finish_call(self, callee): + """Parses a function call""" + + arguments = [] + if not self.check(TokenType.RP): + while True: + if len(arguments) >= 255: + raise self.throw(self.peek(), "Cannot have more than 255 arguments") + arguments.append(self.expression()) + if not self.match(TokenType.COMMA): + break + paren = self.consume(TokenType.RP, "Unexpected error while parsing call") + return Call(callee, paren, arguments) + + def call(self): + """Parses call expressions""" + + expr = self.primary() + while True: + if self.match(TokenType.LP): + expr = self.finish_call(expr) + elif self.match(TokenType.DOT): + name = self.consume(TokenType.ID, "Expecting property after '.'") + expr = Get(expr, name) + else: + break + return expr + + def unary(self): + """Parses unary expressions""" + + if self.match(TokenType.NEG, TokenType.MINUS): + operator: Token = self.previous() + right: Expression = self.unary() + return Unary(operator, right) + return self.call() + + def pow(self): + """Parses pow expressions""" + + expr: Expression = self.unary() + while self.match(TokenType.POW): + operator: Token = self.previous() + right: Expression = self.unary() + expr = Binary(expr, operator, right) + return expr + + def multiplication(self): + """ + Parses multiplications and divisions + """ + + expr: Expression = self.pow() + while self.match(TokenType.STAR, TokenType.SLASH, TokenType.MOD): + operator: Token = self.previous() + right: Expression = self.pow() + expr = Binary(expr, operator, right) + return expr + + def addition(self): + """ + Parses additions and subtractions + """ + + expr: Expression = self.multiplication() + while self.match(TokenType.PLUS, TokenType.MINUS): + operator: Token = self.previous() + right: Expression = self.multiplication() + expr = Binary(expr, operator, right) + return expr + + def comparison(self): + """ + Parses comparison expressions + """ + + expr: Expression = self.addition() + while self.match(TokenType.GT, TokenType.GE, TokenType.LT, TokenType.LE, TokenType.NE): + operator: Token = self.previous() + right: Expression = self.addition() + expr = Binary(expr, operator, right) + return expr + + def equality(self): + """ + Parses equality expressions + """ + + expr: Expression = self.comparison() + while self.match(TokenType.NEG, TokenType.DEQ): + operator: Token = self.previous() + right: Expression = self.comparison() + expr = Binary(expr, operator, right) + return expr + + def logical_and(self): + """Parses a logical and expression""" + + expr = self.equality() + while self.match(TokenType.AND): + operator = self.previous() + right = self.equality() + expr = Logical(expr, operator, right) + return expr + + def logical_or(self): + """Parses a logical or expression""" + + expr = self.logical_and() + while self.match(TokenType.OR): + operator = self.previous() + right = self.logical_and() + expr = Logical(expr, operator, right) + return expr + + def assignment(self): + """ + Parses an assignment expression + """ + + expr = self.logical_or() + if self.match(TokenType.EQ): + eq = self.previous() + value = self.assignment() + if isinstance(expr, Variable): + name = expr.name + return Assignment(name, value) + elif isinstance(expr, Get): + return Set(expr.object, expr.name, value) + raise self.throw(eq, "Invalid syntax") + return expr + + def expression(self): + """ + Parses an expression + """ + + return self.assignment() + + def step(self): + """Steps 1 token forward""" + + if not self.done(): + self.current += 1 + return self.previous() + + def del_statement(self): + """Returns a del AST node""" + + value = self.expression() + self.consume(TokenType.SEMICOLON, "Missing semicolon after statement") + return Del(value) + + def expression_statement(self): + """Returns a StatemenrExpr AST node""" + + value = self.expression() + self.consume(TokenType.SEMICOLON, "Missing semicolon after statement") + return StatementExpr(value) + + def block(self): + """Returns a new environment to enable block scoping""" + + statements = [] + while not self.check(TokenType.RB) and not self.done(): + statements.append(self.declaration()) + self.consume(TokenType.RB, "Unexpected end of block") + return statements + + def if_statement(self): + """Parses an IF statement""" + + self.consume(TokenType.LP, "The if condition must be parenthesized") + cond = self.expression() + self.consume(TokenType.RP, "The if condition must be parenthesized") + then_branch = self.statement() + else_branch = None + if self.match(TokenType.ELSE): + else_branch = self.statement() + return If(cond, then_branch, else_branch) + + def while_statement(self): + """Parses a while statement""" + + self.consume(TokenType.LP, "The while condition must be parenthesized") + cond = self.expression() + self.consume(TokenType.RP, "The while condition must be parenthesized") + body = self.statement() + return While(cond, body) + + def for_statement(self): + """Parses a for statement""" + + self.consume(TokenType.LP, "The for condition must be parenthesized") + if self.match(TokenType.SEMICOLON): + init = None + elif self.match(TokenType.VAR): + init = self.var_declaration() + else: + init = self.expression_statement() + condition = None + if not self.check(TokenType.SEMICOLON): + condition = self.expression() + self.consume(TokenType.SEMICOLON, "Missing semicolon after loop condition") + incr = None + if not self.check(TokenType.RP): + incr = self.expression() + self.consume(TokenType.RP, "The for condition must be parenthesized") + body = self.statement() + if incr: + body = Block([body, StatementExpr(incr)]) + if not condition: + condition = Literal(True) + body = While(condition, body) + if init: + body = Block([init, body]) + return body + + def break_statement(self): + """Parses a break statement""" + + if self.check(TokenType.SEMICOLON): + return self.step() + raise ParseError(self.peek(), "Invalid syntax") + + def return_statement(self): + """Parses a return statement""" + + keyword = self.previous() + value = None + if not self.check(TokenType.SEMICOLON): + value = self.expression() + self.consume(TokenType.SEMICOLON, "Missing semicolon after statement") + return Return(keyword, value) + + def statement(self): + """Parses a statement""" + + if self.match(TokenType.IF): + return self.if_statement() + elif self.match(TokenType.RETURN): + return self.return_statement() + elif self.match(TokenType.FOR): + return self.for_statement() + elif self.match(TokenType.WHILE): + return self.while_statement() + elif self.match(TokenType.BREAK): + return Break(self.break_statement()) + elif self.match(TokenType.LB): + return Block(self.block()) + elif self.match(TokenType.DEL): + return self.del_statement() + return self.expression_statement() + + def var_declaration(self): + """Parses a var declaration""" + + name = self.consume(TokenType.ID, "Expecting a variable name") + init = None + if self.match(TokenType.EQ): + init = self.expression() + self.consume(TokenType.SEMICOLON, "Missing semicolon after declaration") + return Var(name, init) + + def function(self, kind: str): + """Parses a function declaration""" + + name = self.consume(TokenType.ID, f"Expecting {kind} name") + self.consume(TokenType.LP, f"Expecting parenthesis after {kind} name") + parameters = [] + if not self.check(TokenType.RP): + while True: + if len(parameters) >= 255: + raise self.throw(self.peek(), "Cannot have more than 255 arguments") + parameter = self.consume(TokenType.ID, "Expecting parameter name") + if parameter in parameters: + raise self.throw(self.peek(), "Multiple parameters with the same name in function declaration are not allowed") + parameters.append(parameter) + if not self.match(TokenType.COMMA): + break + self.consume(TokenType.RP, "Unexpected error while parsing function declaration") + self.consume(TokenType.LB, f"Expecting '{{' before {kind} body") + body = self.block() + return Function(name, parameters, body) + + def class_declaration(self): + """Parses a class declaration""" + + name = self.consume(TokenType.ID, "Expecting class name") + superclass = None + if self.match(TokenType.LT): + self.consume(TokenType.ID, "Expecting superclass name") + superclass = Variable(self.previous()) + self.consume(TokenType.LB, "Expecting '{' before class body") + methods = [] + while not self.check(TokenType.RB) and not self.done(): + methods.append(self.function("method")) + self.consume(TokenType.RB, "Expecting '}' after class body") + return Class(name, methods, superclass) + + def declaration(self): + """Parses a declaration""" + + try: + if self.match(TokenType.CLASS): + return self.class_declaration() + elif self.match(TokenType.FUN): + return self.function("function") + elif self.match(TokenType.VAR): + return self.var_declaration() + return self.statement() + except ParseError: + self.synchronize() + raise + + def parse(self): + """ + Starts to parse + """ + + statements = [] + while not self.done(): + statements.append(self.declaration()) + return statements diff --git a/JAPL/resolver.py b/JAPL/resolver.py new file mode 100644 index 0000000..478db44 --- /dev/null +++ b/JAPL/resolver.py @@ -0,0 +1,281 @@ +from .meta.exceptions import JAPLError +from .meta.expression import Expression +from .meta.statement import Statement +from .meta.functiontype import FunctionType +from .meta.classtype import ClassType +from .meta.looptype import LoopType +try: + from functools import singledispatchmethod +except ImportError: + from singledispatchmethod import singledispatchmethod # Backport +from typing import List, Union +from collections import deque + + +class Resolver(Expression.Visitor, Statement.Visitor): + """ + This class serves the purpose of correctly resolving + name bindings (even with closures) efficiently + """ + + def __init__(self, interpreter): + """ + Object constructor + """ + + self.interpreter = interpreter + self.scopes = deque() + self.current_function = FunctionType.NONE + self.current_loop = LoopType.NONE + self.current_class = ClassType.NONE + + @singledispatchmethod + def resolve(self, stmt_or_expr: Union[Statement, Expression, List[Statement]]): + """Generic method to dispatch statements/expressions""" + + raise NotImplementedError + + def begin_scope(self): + """ + Opens a new scope + """ + + self.scopes.append({}) + + def end_scope(self): + """ + Ends a scope + """ + + self.scopes.pop() + + @resolve.register + def resolve_statement(self, stmt: Statement): + """ + Resolves names for the given group + of statements + """ + + stmt.accept(self) + + @resolve.register + def resolve_expression(self, expression: Expression): + """ + Resolves an expression + """ + + return expression.accept(self) + + @resolve.register + def resolve_statements(self, stmt: list): + """Resolves multiple statements""" + + for statement in stmt: + self.resolve(statement) + + def declare(self, name): + """ + Declares a new variable + """ + + if not self.scopes: + return + scope = self.scopes[-1] + if name.lexeme in scope: + raise JAPLError(name, "Cannot re-declare the same variable in local scope, use assignment instead") + scope[name.lexeme] = False + + def define(self, name): + """ + Defines a new variable + """ + + if not self.scopes: + return + scope = self.scopes[-1] + scope[name.lexeme] = True + + def visit_block(self, block): + """Starts name resolution on a given block""" + + self.begin_scope() + self.resolve(block.statements) + self.end_scope() + + def visit_var_stmt(self, stmt): + """Visits a var statement node""" + + self.declare(stmt.name) + if stmt.init: + self.resolve(stmt.init) + self.define(stmt.name) + + def visit_var_expr(self, expr): + """Visits a var expression node""" + + if self.scopes and self.scopes[-1].get(expr.name.lexeme) is False: + raise JAPLError(expr.name, f"Cannot read local variable in its own initializer") + self.resolve_local(expr, expr.name) + + def resolve_local(self, expr, name): + """Resolves local variables""" + + i = 0 + for scope in reversed(self.scopes): + if name.lexeme in scope: + self.interpreter.resolve(expr, i) + i += 1 + + def resolve_function(self, function, function_type: FunctionType): + """Resolves function objects""" + + enclosing = self.current_function + self.current_function = function_type + self.begin_scope() + for param in function.params: + self.declare(param) + self.define(param) + self.resolve(function.body) + self.end_scope() + self.current_function = enclosing + + def visit_assign(self, expr): + """Visits an assignment expression""" + + self.resolve(expr.value) + self.resolve_local(expr, expr.name) + + def visit_function(self, stmt): + """Visits a function statement""" + + self.declare(stmt.name) + self.define(stmt.name) + self.resolve_function(stmt, FunctionType.FUNCTION) + + def visit_class(self, stmt): + """Visits a class statement""" + + enclosing = self.current_class + self.current_class = ClassType.CLASS + self.declare(stmt.name) + self.define(stmt.name) + if stmt.superclass: + if stmt.superclass.name.lexeme == stmt.name.lexeme: + raise JAPLError(stmt.name, "A class cannot inherit from itself") + self.resolve(stmt.superclass) + self.begin_scope() + self.scopes[-1]["super"] = True + self.begin_scope() + self.scopes[-1]["this"] = True + for method in stmt.methods: + ftype = FunctionType.METHOD + if method.name.lexeme == "init": + ftype = FunctionType.INIT + self.resolve_function(method, ftype) + self.end_scope() + if stmt.superclass: + self.end_scope() + self.current_class = enclosing + + def visit_statement_expr(self, stmt): + """Visits a statement expression node""" + + self.resolve(stmt.expression) + + def visit_if(self, stmt): + """Visits an if statement node""" + + self.resolve(stmt.condition) + self.resolve(stmt.then_branch) + if stmt.else_branch: + self.resolve(stmt.else_branch) + + def visit_return(self, stmt): + """Visits a return statement node""" + + if self.current_function == FunctionType.NONE: + raise JAPLError(stmt.keyword, "'return' outside function") + elif self.current_function == FunctionType.INIT: + raise JAPLError(stmt.keyword, "Cannot explicitly return from constructor") + elif stmt.value is not None: + self.resolve(stmt.value) + + def visit_while(self, stmt): + """Visits a while statement node""" + + loop = self.current_loop + self.current_loop = LoopType.WHILE + self.resolve(stmt.condition) + self.resolve(stmt.body) + self.current_loop = loop + + def visit_binary(self, expr): + """Visits a binary expression node""" + + self.resolve(expr.left) + self.resolve(expr.right) + + def visit_call_expr(self, expr): + """Visits a call expression node""" + + self.resolve(expr.callee) + for argument in expr.arguments: + self.resolve(argument) + + def visit_grouping(self, expr): + """Visits a grouping expression""" + + self.resolve(expr.expr) + + def visit_literal(self, expr): + """Visits a literal node""" + + return # Literal has no subexpressions and does not reference variables + + def visit_logical(self, expr): + """Visits a logical node""" + + self.visit_binary(expr) # No need to short circuit, so it's the same! + + def visit_unary(self, expr): + """Visits a unary node""" + + self.resolve(expr.right) + + def visit_del(self, stmt): + """Visits a del statement""" + + self.resolve(stmt.name) + + def visit_break(self, stmt): + """Visits a break statement""" + + if self.current_loop == LoopType.NONE: + raise JAPLError("'break' outside loop") + + def visit_get(self, expr): + """Visits a property get expression""" + + self.resolve(expr.object) + + def visit_set(self, expr): + """Visits a property set expression""" + + self.resolve(expr.value) + self.resolve(expr.object) + + def visit_this(self, expr): + """Visits a 'this' expression""" + + if self.current_class == ClassType.NONE: + raise JAPLError(expr.keyword, "'this' outside class") + self.resolve_local(expr, expr.keyword) + + def visit_super(self, expr): + """Visits a 'super' expression""" + + if self.current_class == ClassType.NONE: + raise JAPLError(expr.keyword, "'super' outside class") + self.resolve_local(expr, expr.keyword) + + diff --git a/JAPL/types/__init__.py b/JAPL/types/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/JAPL/types/callable.py b/JAPL/types/callable.py new file mode 100644 index 0000000..46bada0 --- /dev/null +++ b/JAPL/types/callable.py @@ -0,0 +1,11 @@ +class Callable(object): + """A generic callable""" + + def call(self, interpreter, arguments): + raise NotImplementedError + + def __init__(self, arity): + """Object constructor""" + + self.arity = arity + diff --git a/JAPL/types/instance.py b/JAPL/types/instance.py new file mode 100644 index 0000000..04b7049 --- /dev/null +++ b/JAPL/types/instance.py @@ -0,0 +1,24 @@ +from ..meta.exceptions import JAPLError +from ..meta.tokenobject import Token + + +class JAPLInstance: + """A class instance""" + + def __init__(self, klass): + self.klass = klass + self.fields = {} + + def __repr__(self): + return f"" + + def get(self, name: Token): + if name.lexeme in self.fields: + return self.fields[name.lexeme] + meth = self.klass.get_method(name.lexeme) + if meth: + return meth.bind(self) + raise JAPLError(name, f"Undefined property '{name.lexeme}'") + + def set(self, name: Token, value: object): + self.fields[name.lexeme] = value diff --git a/JAPL/types/japlclass.py b/JAPL/types/japlclass.py new file mode 100644 index 0000000..7e51104 --- /dev/null +++ b/JAPL/types/japlclass.py @@ -0,0 +1,35 @@ +from .callable import Callable +from .instance import JAPLInstance + + +class JAPLClass(Callable): + """A JAPL class""" + + def __init__(self, name: str, methods: dict, superclass): + self.name = name + self.methods = methods + self.superclass = superclass + if self.get_method("init"): + self.arity = self.get_method("init").arity + else: + self.arity = 0 + + def get_method(self, name: str): + if name in self.methods: + return self.methods[name] + superclass = self.superclass + while superclass: + if name in superclass.methods: + return superclass.methods[name] + superclass = superclass.superclass + + def __repr__(self): + return f"" + + def call(self, interpreter, arguments): + instance = JAPLInstance(self) + constructor = self.get_method("init") + if constructor: + constructor.bind(instance).call(interpreter, arguments) + return instance + diff --git a/JAPL/types/native.py b/JAPL/types/native.py new file mode 100644 index 0000000..8390827 --- /dev/null +++ b/JAPL/types/native.py @@ -0,0 +1,175 @@ +from .callable import Callable +import time +from ..meta.environment import Environment +from ..meta.exceptions import ReturnException +from .instance import JAPLInstance +from .japlclass import JAPLClass + + +class Clock(Callable): + """JAPL's wrapper around time.time""" + + def __init__(self, *_): + """Object constructor""" + + self.arity = 0 + + def call(self, *args): + return time.time() + + def __repr__(self): + return f"" + + +class Type(Callable): + """JAPL's wrapper around type""" + + def __init__(self, *_): + """Object constructor""" + + self.arity = 1 + + def call(self, _, obj): + return type(obj[0]) + + def __repr__(self): + return f"" + + +class Truthy(Callable): + """JAPL's wrapper around bool""" + + def __init__(self, *_): + """Object constructor""" + + self.arity = 1 + + def call(self, _, obj): + return bool(obj[0]) + + def __repr__(self): + return f"" + + +class Stringify(Callable): + """JAPL's wrapper around str()""" + + def __init__(self, *_): + """Object constructor""" + + self.arity = 1 + + def call(self, _, obj): + return str(obj[0]) + + def __repr__(self): + return f"" + + +class PrintFunction(Callable): + """The print function""" + + def __init__(self, *_): + """Object constructor""" + + self.arity = 1 + + def call(self, _, *args): + print(*args[0]) + + def __repr__(self): + return "" + + +class IsInstance(Callable): + """The isinstance function""" + + def __init__(self, *_): + """Object constructor""" + + self.arity = 2 + + def call(self, _, args): + instance, klass = args + if not isinstance(instance, JAPLInstance): + return False + elif not isinstance(klass, JAPLClass): + return False + return instance.klass == klass + + def __repr__(self): + return "" + + +class IsSubclass(Callable): + """The isinstance function""" + + def __init__(self, *_): + """Object constructor""" + + self.arity = 2 + + def call(self, _, args): + first, second = args + if not isinstance(first, JAPLClass): + return False + elif not isinstance(second, JAPLClass): + return False + return first.superclass == second + + def __repr__(self): + return "" + + +class IsSuperclass(Callable): + """The isinstance function""" + + def __init__(self, *_): + """Object constructor""" + + self.arity = 2 + + def call(self, _, args): + first, second = args + if not isinstance(first, JAPLClass): + return False + elif not isinstance(second, JAPLClass): + return False + return second.superclass == first + + def __repr__(self): + return "" + + +class JAPLFunction(Callable): + """A generic wrapper for user-defined functions""" + + def __init__(self, declaration, closure): + """Object constructor""" + + self.declaration = declaration + self._repr = f"" + self.arity = len(self.declaration.params) + self.closure = closure + + def bind(self, obj: object): + """Binds a method to an object""" + + env = Environment(self.closure) + env.define("this", obj) + func = type(self)(self.declaration, env) + func._repr = f"" + return func + + def call(self, interpreter, arguments): + scope = Environment(self.closure) + for name, value in zip(self.declaration.params, arguments): + scope.define(name.lexeme, value) + try: + interpreter.execute_block(self.declaration.body, scope) + except ReturnException as error: + return error.args[0] + + def __repr__(self): + return self._repr + diff --git a/JAPL/wrapper.py b/JAPL/wrapper.py new file mode 100644 index 0000000..6b8dc50 --- /dev/null +++ b/JAPL/wrapper.py @@ -0,0 +1,69 @@ +from JAPL.lexer import Lexer +from JAPL.meta.exceptions import ParseError, JAPLError +from JAPL.resolver import Resolver +from JAPL.parser import Parser +from JAPL.interpreter import Interpreter + + +class JAPL(object): + """Wrapper around JAPL's interpreter, lexer and parser""" + + interpreter = Interpreter() + resolver = Resolver(interpreter) + + def run(self, file: str): + """Runs a file""" + + if not file: + self.repl() + else: + try: + with open(file) as source_file: + source_code = source_file.read() + lexer = Lexer(source_code) + tokens = lexer.lex() + parser = Parser(tokens) + ast = parser.parse() + self.resolver.resolve(ast) + self.interpreter.interpret(ast) + except FileNotFoundError: + print(f"Error: '{file}', no such file or directory") + except PermissionError: + print(f"Error' '{file}', permission denied") + except JAPLError as err: + if len(err.args) == 2: + token, message = err.args + print(f"An exception occurred at line {token.line}, file '{file}' at '{token.lexeme}': {message}") + else: + print(f"An exception occurred, details below\n\n{type(err).__name__}: {err}") + + def repl(self): + """Starts an interactive REPL""" + + print("[JAPL 0.1.1 - Interactive REPL]") + while True: + try: + source = input(">>> ") + except (EOFError, KeyboardInterrupt): + print() + return + if not source: + continue + lexer = Lexer(source) + try: + tokens = lexer.lex() + ast = Parser(tokens).parse() + self.resolver.resolve(ast) + result = self.interpreter.interpret(ast) + except ParseError as err: + if len(err.args) == 2: + token, message = err.args + print(f"An exception occurred at line {token.line} at '{token.lexeme}': {message}") + else: + print(f"\nAn exception occurred, details below\n\nParseError: {err.args[0]}") + except JAPLError as error: + if len(error.args) == 2: + token, message = error.args + print(f"An exception occurred at line {token.line}, file 'stdin' at '{token.lexeme}': {message}") + else: + print(f"An exception occurred, details below\n\n{type(error).__name__}: {error}") diff --git a/README.md b/README.md index 722e6c1..2f58e7a 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,66 @@ -# japl-python -A POC (proof of concept) of the JAPL language written in Python 3 +# japl +JAPL is an interpreted, dynamically-typed, garbage-collected and minimalistic programming language with C- and Java-like syntax. + +# J.. what? + +You may wonder what's the meaning of JAPL: well, it turns out to be an acronym +for __Just Another Programming Language__, but beware! Despite the name, the pronounciation is actually the same as "JPL". + +## Some backstory + +JAPL is born thanks to the amazing work of Bob Nystrom that wrote a book available completely for free +at [this](https://craftinginterpreters.com) link, where he describes the implementation of a simple language called Lox. + + +### What has been (or will be) added from Lox + +- Possibility to delete variables with the `del` statement (Currently being reworked) +- `break` statement +- `continue` statement +- Multi-line comments (`/* like this */`) +- Nested comments +- Modulo division (`%`) and exponentiation (`**`) +- `OP_CONSTANT_LONG` is implemented +- Differentiation between integers and floating point numbers +- `inf` and `nan` types +- Possibility to have more than 255 locals in scope at any given time +- String slicing, with start:end syntax as well +- Strings are not interned (may change in the future) +- All entities are actually objects, even builtins +- Bitwise operators (AND, OR, XOR, NOT) +- Functions default and keyword arguments (__WIP__) +- A proper import system (__Coming soon__) +- Native asynchronous (`await`/`async fun`) support (__Coming soon__) +- Multiple inheritance (__Coming Soon__) +- Bytecode optimizations such as constant folding and stack caching (__Coming Soon__) +- Arbitrary-precision arithmetic (__Coming soon__) +- Generators (__Coming soon__) +- A standard library with collections, I/O utilities, scientific modules, etc (__Coming soon__) +- Multithreading and multiprocessing support with a global VM Lock like CPython (__Coming soon__) +- Multiple GC implementations which can be chosen at runtime or via CLI: bare refcount, refcount + generational GC, M&S (__Coming soon__) +- Exceptions (__Coming soon__) +- Optional JIT Compilation (__Coming soon__) +- Some syntax changes (maybe), e.g. get rid of semicolons +- Prototypes based system instead of classes (maybe) + +Other than that, JAPL features closures, function definitions, classes, inheritance and static scoping. You can check +the provided example `.jpl` files in the repo to find out more about its syntax. + +### Disclaimer + +This project is currently a WIP (Work in Progress) and is not optimized nor complete. +The first version of the interpreter is written in Python, but a bytecode stack-based VM written in nim is being developed right now. + +Also, the design of the language may change at any moment and all the source inside this repo +is alpha code quality, for now. + +For other useful information, check the LICENSE file in this repo. + +### Contributing + +If you want to contribute, feel free to send a PR! + +Right now there are some major issues with the virtual machine which need to be addressed +before the development can proceed, and some help is ~~desperately needed~~ greatly appreciated! + +You can also contact me using the information available [here](https://github.com/nocturn9x) diff --git a/examples/examples.jpl b/examples/examples.jpl new file mode 100644 index 0000000..6f3c138 --- /dev/null +++ b/examples/examples.jpl @@ -0,0 +1,125 @@ +// Example file to test JAPL's syntax + +// Mathematical expressions + +2 + 2; +-1 * 6; +3 * (9 / 2); // Parentheses for grouping +8 % 2; // Modulo division +6 ** 9; // Exponentiation + +// Variable definition and assignment + +var name = "bob"; // Dynamically typed +name = "joe"; // Can only be assigned if it's defined +del name; // Delete a variable +var foo; // Unitialized variables are equal to nil + +// Scoping + +var a = "global"; +var b = "global1"; +{ // open a new scope + var b = "local"; // Shadow the global variable + print a; // This falls back to the global scope + print b; +} +print a; +print b; // The outer scope isn't affected + +/* +A multiline comment +yay! +*/ + +// Control flow statements + +var n = 0; +while (n <= 10) { // While loops + if (n <= 5) { // If statements + print n; + } + n = n + 1; +} + +for (var i = 0; i < 10; i = i + 1) { // For loops + print i; +} + + +// Functions + +print clock(); // Function calls + +fun count(n) { // Function definitions + if (n > 1) count(n - 1); // Recursion works + print n; +} + +count(3); + +// Closures work too! + +var a = "global"; +{ + fun showA() { + print a; + } + + showA(); + var a = "block"; + showA(); +} + +// Nested functions + +fun makeCounter() { + var i = 0; + fun count() { + i = i + 1; + print i; + } + + return count; +} + +var counter = makeCounter(); +counter(); // "1". +counter(); // "2". + + +// Classes + +class Person { + + init(name) { // Class initializer + + this.name = name; + } + + greet() { // Methods don't use the 'fun' keyword! + print "Hello, " + this.name; + } +} + +var bob = Person("Bob"); // Object creation +bob.greet(); // Prints Hello, Bob +var greetbob = bob.greet; // Functions and methods are first-class objects! (classes are too) +greetbob(); + + +class Male < Person { // Male inherits from person + + init(name) { + super.init(name); // Inherits constructor behavior + this.sex = "male"; + + } + + greet() { + super.greet(); // Inherits behavior from superclass + } +} + +var mark = Male("Mark"); +mark.greet(); diff --git a/examples/factorial.jpl b/examples/factorial.jpl new file mode 100644 index 0000000..8ab3127 --- /dev/null +++ b/examples/factorial.jpl @@ -0,0 +1,19 @@ +fun factorial(n) { + // Computes the factorial of n iteratively + + if (n < 0) return nil; + if (n == 1 or n == 2) return n; + var fact = 1; + for (var i = 1; i < n + 1; i = i + 1) { + fact = fact * i; + } + return fact; +} + + +var start = clock(); +print "Computing factorials from 0 to 200"; +for (var i = 0; i < 201; i = i + 1) factorial(i); +var result = clock() - start; +print "Computed factorials in " + stringify(result) + " seconds"; + diff --git a/examples/fib.jpl b/examples/fib.jpl new file mode 100644 index 0000000..0a97934 --- /dev/null +++ b/examples/fib.jpl @@ -0,0 +1,12 @@ +// A recursive implementation of the fibonacci sequence + +fun fib(n) { + if (n <= 1) return n; + return fib(n - 2) + fib(n - 1); +} + + +var start = clock(); +fib(30); +var end = clock() - start; +print(end); diff --git a/japl.py b/japl.py new file mode 100644 index 0000000..f22af9d --- /dev/null +++ b/japl.py @@ -0,0 +1,9 @@ +import sys +from JAPL.wrapper import JAPL + + +if __name__ == "__main__": + if len(sys.argv) == 1: + JAPL().repl() + else: + JAPL().run(sys.argv[1]) diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..e6ab583 --- /dev/null +++ b/requirements.txt @@ -0,0 +1 @@ +singledispatchmethod \ No newline at end of file diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..8ace899 --- /dev/null +++ b/setup.py @@ -0,0 +1,16 @@ +import setuptools + +setuptools.setup( + name="JAPL - Just another programming language", + version="0.1.1", + author="nocturn9x", + author_email="nocturn9x@intellivoid.net", + description="The JAPL programming language", + packages=setuptools.find_packages(), + classifiers=[ + "Programming Language :: Python :: 3", + "License :: OSI Approved :: MIT License", + "Operating System :: OS Independent", + ], + python_requires='>=3.6', +)