def scan_token(self) -> None: """ Scan source and append tokens """ char: str = self.get_next() if char in "!=<>" and self.match("="): char = char + "=" elif char == "/" and self.match("/"): while self.peek() != '\n' and not self.is_end(): self.get_next() return elif char == "\n": self.line += 1 if char in Scanner.lex: tok = Scanner.lex.get(char, None) if tok is not None: self.add_token(tok, None, self.line) elif char == '"': self.get_string() elif char.isdigit(): self.get_number() elif self.is_valid_first(char): self.get_identifier() elif not char.isspace(): raise_error(LoxError, self.line, f'Unexpected character: {char}')
def visit_class_stmt(self, stmt: loxStmtAST.Class) -> None: enclosing_class = self.current_class self.current_class = Resolver.ClassType.CLASS self.declare(stmt.name) self.define(stmt.name) if stmt.superclass is not None and stmt.name.lexeme == stmt.superclass.name.lexeme: raise_error(LoxError, stmt.superclass.name, "A class cannot inherit from itself.") if stmt.superclass is not None: self.resolve_expr(stmt.superclass) if stmt.superclass is not None: self.begin_scope() self.scopes.peek()["super"] = True self.begin_scope() self.scopes.peek()["this"] = True for method in stmt.methods: if method.name.lexeme == "init": declaration: Resolver.FunctionType = Resolver.FunctionType.INITIALIZER else: declaration = Resolver.FunctionType.METHOD self.resolve_function(method, declaration) self.end_scope() if stmt.superclass is not None: self.end_scope() self.current_class = enclosing_class return None
def consume(self, tok_type: loxtoken.TokenType, message: str) -> Optional[loxtoken.Token]: """ If token of correct type advance otherwise raise error with message """ if self.check(tok_type): return self.advance() raise_error(LoxParseError, self.peek(), message) return None
def visit_variable_expr(self, expr: loxExprAST.Variable) -> None: if not self.scopes.is_empty() and self.scopes.peek().get( expr.name.lexeme) is False: raise_error(LoxError, expr.name, "Cannot read local variable in its own initializer.") self.resolve_local(expr, expr.name) return None
def function(self, kind: str) -> loxStmtAST.Function: """ funDecl → "fun" function ; function → IDENTIFIER "(" parameters? ")" block ; parameters → IDENTIFIER ( "," IDENTIFIER )* ; """ name: loxtoken.Token = self.consume(Parser.tokentypes.IDENTIFIER, f'Expect {kind} name.') self.consume(Parser.tokentypes.LEFT_PAREN, f"Expect '(' after {kind} name.") parameters: List[loxtoken.Token] = [] if not self.check(Parser.tokentypes.RIGHT_PAREN): while True: if len(parameters) >= 255: raise_error(LoxParseError, self.peek(), "Cannot have more than 255 parameters.") parameters.append( self.consume(Parser.tokentypes.IDENTIFIER, "Expect parameter name.")) if not self.match(Parser.tokentypes.COMMA): break self.consume(Parser.tokentypes.RIGHT_PAREN, "Expect ')' after parameters.") self.consume( Parser.tokentypes.LEFT_BRACE, f'Expect {{ before {kind} body.') # {{ to escape { in string body: List[loxStmtAST.Stmt] = self.block_stmt() return loxStmtAST.Function(name, parameters, body)
def visit_set_expr(self, expr: loxExprAST.Set): set_object = self.evaluate(expr.set_object) if not isinstance(set_object, loxclass.LoxInstance): raise_error(LoxRuntimeError, expr.name, "Only instances have fields.") value = self.evaluate(expr.value) set_object.set(expr.name, value) return value
def get(self, name: loxtoken.Token): """ Get property """ if name.lexeme in self.fields: return self.fields[name.lexeme] method = self.klass.find_method(name.lexeme) if method is not None: return method.bind(self) raise_error(LoxRuntimeError, name, "Undefined property '" + name.lexeme + "'.")
def declare(self, name: Token): """ Declare variable. Set value to False """ if self.scopes.is_empty(): return None if name.lexeme in self.scopes.peek(): raise_error( LoxError, name, "Variable with this name already declared in this scope.") self.scopes.peek()[name.lexeme] = False
def get(self, name: Token) -> object: """ get value of token from nearest environment """ if name.lexeme in self.values: return self.values[name.lexeme] if self.enclosing is not None: return self.enclosing.get(name) raise_error(LoxRuntimeError, name, f'Undefined variable {name.lexeme}.') return None
def visit_return_stmt(self, stmt: loxStmtAST.Return) -> None: if self.current_function == Resolver.FunctionType.NONE: raise_error(LoxError, stmt.keyword, "Cannot return from top-level code.") if stmt.value is not None: if self.current_function == Resolver.FunctionType.INITIALIZER: raise_error(LoxError, stmt.keyword, "Cannot return a value from an initializer.") self.resolve_expr(stmt.value) return None
def visit_super_expr(self, expr: loxExprAST.Super): distance = self.locals[expr] superclass = self.environment.get_at(distance, "super") # "this" is always one level nearer than "super"'s environment. get_object = self.environment.get_at(distance - 1, "this") method = superclass.find_method(expr.method.lexeme) if method is None: raise_error(LoxRuntimeError, expr.method, "Undefined property '" + expr.method.lexeme + "'.") return method.bind(get_object)
def assign(self, name: Token, value: object) -> None: """ set value of name in nearest environment where it exits otherwise return error """ if name.lexeme in self.values: self.values[name.lexeme] = value return if self.enclosing is not None: self.enclosing.assign(name, value) return raise_error(LoxRuntimeError, name, f'Undefined variable {name.lexeme}.')
def check_number_operands(operator: loxtoken.Token, op1: Any, op2: Any = None) -> bool: """ Check if one or two operands are numeric (float) """ if op2 is None: if isinstance(op1, float): return True raise_error(LoxRuntimeError, operator, "Operand must be a number.") else: if isinstance(op1, float) and isinstance(op2, float): return True raise_error(LoxRuntimeError, operator, "Both operands must be a number.") return False
def get_string(self) -> None: """ Process literal string and add token Increase line no if required """ while self.peek() != '"' and not self.is_end(): char: str = self.get_next() if char == "\n": self.line += 1 if self.is_end(): raise_error(LoxError, self.line, "Unterminated string.") return value: str = self.source[self.start + 1:self.current] self.add_token(Scanner.token_types.STRING, value, self.line) self.get_next()
def finish_call(self, callee) -> loxExprAST.Expr: """ arguments → expression ( "," expression )* """ arguments: List[loxExprAST.Expr] = [] if not self.check(Parser.tokentypes.RIGHT_PAREN): while True: if len(arguments) >= 255: raise_error(LoxParseError, self.peek(), "Cannot have more than 255 arguments.") arguments.append(self.expression()) if not self.match(Parser.tokentypes.COMMA): break paren: loxtoken.Token = self.consume(Parser.tokentypes.RIGHT_PAREN, "Expect ')' after arguments.") return loxExprAST.Call(callee, paren, arguments)
def assignment(self) -> loxExprAST.Expr: """ assignment → ( call "." )? IDENTIFIER "=" assignment | logic_or; """ expr: loxExprAST.Expr = self.op_or() if self.match(Parser.tokentypes.EQUAL): equals: loxtoken.Token = self.previous() value: loxExprAST.Expr = self.assignment() if isinstance(expr, loxExprAST.Variable): name: loxtoken.Token = expr.name return loxExprAST.Assign(name, value) elif isinstance(expr, loxExprAST.Get): # get = expr return loxExprAST.Set(expr.get_object, expr.name, value) else: raise_error(LoxParseError, equals, "Invalid assignment target.") return expr
def visit_call_expr(self, expr: loxExprAST.Call) -> object: callee: loxcallable.LoxFunction = self.evaluate(expr.callee) arguments: List[object] = [] for arg in expr.arguments: arguments.append(self.evaluate(arg)) if not isinstance( callee, loxcallable.LoxCallable ) and loxcallable.LoxCallable not in callee.__bases__: raise_error(LoxRuntimeError, expr.paren, "Can only call functions and classes.") # funct: loxcallable.LoxFunction = callee if len(arguments) != callee.arity(): raise_error( LoxRuntimeError, expr.paren, "Expected " + str(callee.arity()) + " arguments but got " + str(len(arguments)) + ".") return callee.call(self, arguments)
def visit_binary_expr(self, expr) -> Optional[Union[float, str, bool]]: left: Union[float, str, bool] = self.evaluate(expr.left) right: Union[float, str, bool] = self.evaluate(expr.right) if expr.operator.tok_type == Interpreter.tokentypes.MINUS: if self.check_number_operands(expr.operator, left, right): return float(left) - float(right) elif expr.operator.tok_type == Interpreter.tokentypes.SLASH: if self.check_number_operands(expr.operator, left, right): return float(left) / float(right) elif expr.operator.tok_type == Interpreter.tokentypes.STAR: if self.check_number_operands(expr.operator, left, right): return float(left) * float(right) elif expr.operator.tok_type == Interpreter.tokentypes.PLUS: if isinstance(left, float) and isinstance(right, float): return left + right elif isinstance(left, str) and isinstance(right, str): return left + right else: raise_error(LoxRuntimeError, expr.operator, "Operands must both be a number or a string.") elif expr.operator.tok_type == Interpreter.tokentypes.GREATER: if self.check_number_operands(expr.operator, left, right): return float(left) > float(right) elif expr.operator.tok_type == Interpreter.tokentypes.GREATER_EQUAL: if self.check_number_operands(expr.operator, left, right): return float(left) >= float(right) elif expr.operator.tok_type == Interpreter.tokentypes.LESS: if self.check_number_operands(expr.operator, left, right): return float(left) < float(right) elif expr.operator.tok_type == Interpreter.tokentypes.LESS_EQUAL: if self.check_number_operands(expr.operator, left, right): return float(left) <= float(right) elif expr.operator.tok_type == Interpreter.tokentypes.EQUAL_EQUAL: return self.is_equal(left, right) elif expr.operator.tok_type == Interpreter.tokentypes.BANG_EQUAL: return not self.is_equal(left, right) return None
def visit_class_stmt(self, stmt: loxStmtAST.Class) -> None: superclass: Optional[loxExprAST.Variable] = None if stmt.superclass is not None: superclass = self.evaluate(stmt.superclass) if not isinstance(superclass, loxclass.LoxClass): raise_error(LoxRuntimeError, stmt.superclass.name, "Superclass must be a class.") self.environment.define(stmt.name.lexeme, None) if stmt.superclass is not None: self.environment = loxenvironment.Environment(self.environment) self.environment.define("super", superclass) methods: Dict[str, loxcallable.LoxFunction] = dict() for method in stmt.methods: function = loxcallable.LoxFunction(method, self.environment, method.name.lexeme == "init") methods[method.name.lexeme] = function klass: loxclass.LoxClass = loxclass.LoxClass(stmt.name.lexeme, superclass, methods) if stmt.superclass is not None: self.environment = self.environment.enclosing self.environment.assign(stmt.name, klass) return None
def primary(self) -> Optional[loxExprAST.Expr]: """ primary → "true" | "false" | "nil" | "this" | NUMBER | STRING | IDENTIFIER | "(" expression ")" | "super" "." IDENTIFIER ; NUMBER → DIGIT+ ( "." DIGIT+ )? ; STRING → '"' <any char except '"'>* '"' ; IDENTIFIER → ALPHA ( ALPHA | DIGIT )* ; ALPHA → 'a' ... 'z' | 'A' ... 'Z' | '_' ; DIGIT → '0' ... '9' ;""" if self.match(Parser.tokentypes.SUPER): keyword: loxtoken.Token = self.previous() self.consume(Parser.tokentypes.DOT, "Expect '.' after 'super'.") method: loxtoken.Token = self.consume( Parser.tokentypes.IDENTIFIER, "Expect superclass method name.") return loxExprAST.Super(keyword, method) if self.match(Parser.tokentypes.THIS): return loxExprAST.This(self.previous()) if self.match(Parser.tokentypes.IDENTIFIER): return loxExprAST.Variable(self.previous()) if self.match(Parser.tokentypes.FALSE): return loxExprAST.Literal(False) if self.match(Parser.tokentypes.TRUE): return loxExprAST.Literal(True) if self.match(Parser.tokentypes.NIL): return loxExprAST.Literal(None) if self.match(Parser.tokentypes.NUMBER, Parser.tokentypes.STRING): return loxExprAST.Literal(self.previous().literal) if self.match(Parser.tokentypes.LEFT_PAREN): expr: loxExprAST.Expr = self.expression() self.consume(Parser.tokentypes.RIGHT_PAREN, "Expect ')' after expression") return loxExprAST.Grouping(expr) raise_error(LoxParseError, self.peek(), "Expect expression.") return None
def visit_get_expr(self, expr: loxExprAST.Get): get_object = self.evaluate(expr.get_object) if isinstance(get_object, loxclass.LoxInstance): return get_object.get(expr.name) raise_error(LoxRuntimeError, expr.name, "Only instances have properties.")
def visit_this_expr(self, expr: loxExprAST.This) -> None: if self.current_class == Resolver.ClassType.NONE: raise_error(LoxError, expr.keyword, "Cannot use 'this' outside of a class.") self.resolve_local(expr, expr.keyword) return None