japl-python/JAPL/interpreter.py

353 lines
12 KiB
Python

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!