diff --git a/docs/grammar.bnf b/docs/grammar.bnf index ba82b2b..280b2ed 100644 --- a/docs/grammar.bnf +++ b/docs/grammar.bnf @@ -48,6 +48,7 @@ KW_CONTINUE="continue" KW_CLASS="class" KW_THIS="this" + KW_SUPER="super" WHITE_SPACE="regexp:\s+" @@ -98,7 +99,7 @@ factor ::= unary ( ( OP_SLASH | OP_STAR ) unary )* ; unary ::= ( OP_BANG | OP_MINUS ) unary | call ; call ::= primary ( PUNC_LPAREN arguments? PUNC_RPAREN | PUNC_DOT IDENTIFIER )* ; -primary ::= KW_TRUE | KW_FALSE | KW_NULL | KW_THIS | NUMBER | STRING | IDENTIFIER | PUNC_LPAREN expression PUNC_RPAREN ; +primary ::= KW_TRUE | KW_FALSE | KW_NULL | KW_THIS | NUMBER | STRING | IDENTIFIER | PUNC_LPAREN expression PUNC_RPAREN | KW_SUPER PUNC_DOT IDENTIFIER ; function ::= IDENTIFIER PUNC_LPAREN parameters? PUNC_RPAREN block ; parameters ::= IDENTIFIER ( PUNC_COMMA IDENTIFIER )* ; diff --git a/examples/basic/21_super.peb b/examples/basic/21_super.peb index 430b370..c9b6bf8 100644 --- a/examples/basic/21_super.peb +++ b/examples/basic/21_super.peb @@ -6,6 +6,10 @@ class Employee { has_responsibilities() { return false } + + get_title() { + return "Employee" + } } class Manager < Employee { @@ -22,6 +26,10 @@ class Boss < Manager { get_salary() { return 500 } + + get_title() { + return super.get_title() + ", CEO" + } } let employee = Employee() @@ -34,4 +42,8 @@ print(boss, boss.get_salary()) print(employee, employee.has_responsibilities()) print(manager, manager.has_responsibilities()) -print(boss, boss.has_responsibilities()) \ No newline at end of file +print(boss, boss.has_responsibilities()) + +print(employee, employee.get_title()) +print(manager, manager.get_title()) +print(boss, boss.get_title()) \ No newline at end of file diff --git a/src/ast/expr.py b/src/ast/expr.py index ed52ce1..002353c 100644 --- a/src/ast/expr.py +++ b/src/ast/expr.py @@ -61,6 +61,10 @@ class Expr(ABC): def visit_this_expr(self, expr: ThisExpr) -> T: ... + @abstractmethod + def visit_super_expr(self, expr: SuperExpr) -> T: + ... + @dataclass(frozen=True) class AssignExpr(Expr): @@ -159,3 +163,12 @@ class ThisExpr(Expr): def accept(self, visitor: Expr.Visitor[T]) -> T: return visitor.visit_this_expr(self) + + +@dataclass(frozen=True) +class SuperExpr(Expr): + keyword: Token + method: Token + + def accept(self, visitor: Expr.Visitor[T]) -> T: + return visitor.visit_super_expr(self) diff --git a/src/formatter.py b/src/formatter.py index e3a635a..b34d852 100644 --- a/src/formatter.py +++ b/src/formatter.py @@ -2,7 +2,7 @@ from enum import Enum, auto from typing import Any from src.ast.expr import Expr, VariableExpr, LiteralExpr, GroupingExpr, UnaryExpr, BinaryExpr, AssignExpr, LogicalExpr, \ - CallExpr, SetExpr, GetExpr, ThisExpr, T + CallExpr, SetExpr, GetExpr, ThisExpr, T, SuperExpr from src.ast.stmt import Stmt, LetStmt, IfStmt, ExpressionStmt, BlockStmt, WhileStmt, ForStmt, FunctionStmt, \ ReturnStmt, BreakStmt, ContinueStmt, ClassStmt @@ -46,6 +46,9 @@ class Formatter(Expr.Visitor[str], Stmt.Visitor[str]): def visit_this_expr(self, expr: ThisExpr) -> str: return expr.keyword.lexeme + def visit_super_expr(self, expr: SuperExpr) -> str: + return f"{expr.keyword.lexeme}.{expr.method.lexeme}" + def visit_unary_expr(self, expr: UnaryExpr) -> str: return f"{expr.operator.lexeme}{self.format(expr.right)}" diff --git a/src/interpreter/interpreter.py b/src/interpreter/interpreter.py index 3a11b98..afac1fd 100644 --- a/src/interpreter/interpreter.py +++ b/src/interpreter/interpreter.py @@ -1,7 +1,7 @@ from typing import Any, Optional from src.ast.expr import LiteralExpr, GroupingExpr, UnaryExpr, BinaryExpr, Expr, VariableExpr, AssignExpr, LogicalExpr, \ - CallExpr, GetExpr, T, SetExpr, ThisExpr + CallExpr, GetExpr, T, SetExpr, ThisExpr, SuperExpr from src.ast.stmt import Stmt, ExpressionStmt, LetStmt, BlockStmt, IfStmt, WhileStmt, ForStmt, FunctionStmt, \ ReturnStmt, BreakStmt, ContinueStmt, ClassStmt from src.consts import CONSTRUCTOR_NAME @@ -91,6 +91,15 @@ class Interpreter(Expr.Visitor[Any], Stmt.Visitor[None]): def visit_this_expr(self, expr: ThisExpr) -> Any: return self.look_up_variable(expr.keyword, expr) + def visit_super_expr(self, expr: SuperExpr) -> Any: + distance: int = self.locals.get(expr) + superclass: PebbleClass = self.env.get_at(distance, "super") + obj: PebbleInstance = self.env.get_at(distance - 1, "this") + method: Optional[PebbleFunction] = superclass.find_method(expr.method.lexeme) + if method is None: + raise PebbleRuntimeError(expr.method, f"Undefined property '{expr.method.lexeme}'.") + return method.bind(obj) + def visit_binary_expr(self, expr: BinaryExpr) -> Any: left: Any = self.evaluate(expr.left) right: Any = self.evaluate(expr.right) @@ -184,11 +193,20 @@ class Interpreter(Expr.Visitor[Any], Stmt.Visitor[None]): raise PebbleRuntimeError(stmt.superclass.name, "Superclass must be a class.") self.env.define(stmt.name.lexeme, None) + + if stmt.superclass is not None: + self.env = Environment(self.env) + self.env.define("super", superclass) + methods: dict[str, PebbleFunction] = {} for method in stmt.methods: func: PebbleFunction = PebbleFunction(method, self.env, method.name.lexeme == CONSTRUCTOR_NAME) methods[method.name.lexeme] = func klass: PebbleClass = PebbleClass(stmt.name.lexeme, superclass, methods) + + if superclass is not None: + self.env = self.env.enclosing + self.env.assign(stmt.name, klass) def visit_expression_stmt(self, stmt: ExpressionStmt) -> None: diff --git a/src/interpreter/resolver.py b/src/interpreter/resolver.py index f46e9e5..c60844a 100644 --- a/src/interpreter/resolver.py +++ b/src/interpreter/resolver.py @@ -4,7 +4,7 @@ from enum import Enum, auto from typing import TYPE_CHECKING from src.ast.expr import Expr, LogicalExpr, VariableExpr, LiteralExpr, GroupingExpr, CallExpr, UnaryExpr, BinaryExpr, \ - AssignExpr, GetExpr, SetExpr, ThisExpr + AssignExpr, GetExpr, SetExpr, ThisExpr, SuperExpr, T from src.ast.stmt import Stmt, ForStmt, WhileStmt, LetStmt, ReturnStmt, IfStmt, FunctionStmt, \ ExpressionStmt, BlockStmt, BreakStmt, ContinueStmt, ClassStmt from src.consts import CONSTRUCTOR_NAME @@ -123,6 +123,9 @@ class Resolver(Expr.Visitor[None], Stmt.Visitor[None]): Pebble.token_error(expr.keyword, "Cannot use 'this' outside of a class.") self.resolve_local(expr, expr.keyword) + def visit_super_expr(self, expr: SuperExpr) -> None: + self.resolve_local(expr, expr.keyword) + def visit_block_stmt(self, stmt: BlockStmt) -> None: self.begin_scope() self.resolve(*stmt.statements) @@ -139,6 +142,9 @@ class Resolver(Expr.Visitor[None], Stmt.Visitor[None]): Pebble.token_error(stmt.superclass.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 @@ -149,6 +155,10 @@ class Resolver(Expr.Visitor[None], Stmt.Visitor[None]): self.resolve_function(method, declaration) self.end_scope() + + if stmt.superclass is not None: + self.end_scope() + self.current_class = enclosing_class def visit_expression_stmt(self, stmt: ExpressionStmt) -> None: diff --git a/src/keyword.py b/src/keyword.py index ac01f58..cec6276 100644 --- a/src/keyword.py +++ b/src/keyword.py @@ -21,4 +21,5 @@ KEYWORDS: dict[str, TokenType] = { "continue": TokenType.CONTINUE, "class": TokenType.CLASS, "this": TokenType.THIS, + "super": TokenType.SUPER, } diff --git a/src/parser/parser.py b/src/parser/parser.py index f60c58c..ceb692c 100644 --- a/src/parser/parser.py +++ b/src/parser/parser.py @@ -1,7 +1,7 @@ from typing import Optional from src.ast.expr import Expr, BinaryExpr, UnaryExpr, LiteralExpr, GroupingExpr, VariableExpr, AssignExpr, LogicalExpr, \ - CallExpr, GetExpr, SetExpr, ThisExpr + CallExpr, GetExpr, SetExpr, ThisExpr, SuperExpr from src.ast.stmt import Stmt, ExpressionStmt, LetStmt, BlockStmt, IfStmt, WhileStmt, ForStmt, FunctionStmt, \ ReturnStmt, BreakStmt, ContinueStmt, ClassStmt from src.consts import MAX_FUNCTION_ARGS @@ -367,6 +367,12 @@ class Parser: if self.match(TokenType.NUMBER, TokenType.STRING): return LiteralExpr(self.previous().value) + if self.match(TokenType.SUPER): + keyword: Token = self.previous() + self.consume(TokenType.DOT, "Expected '.' after 'super'.") + method: Token = self.consume(TokenType.IDENTIFIER, "Expected superclass method name.") + return SuperExpr(keyword, method) + if self.match(TokenType.THIS): return ThisExpr(self.previous()) diff --git a/src/token.py b/src/token.py index 407297c..e83ec10 100644 --- a/src/token.py +++ b/src/token.py @@ -60,6 +60,7 @@ class TokenType(Enum): CONTINUE = auto() CLASS = auto() THIS = auto() + SUPER = auto() # Misc COMMENT = auto()