diff --git a/docs/grammar.bnf b/docs/grammar.bnf index ede3967..e39b516 100644 --- a/docs/grammar.bnf +++ b/docs/grammar.bnf @@ -47,6 +47,7 @@ KW_BREAK="break" KW_CONTINUE="continue" KW_CLASS="class" + KW_THIS="this" WHITE_SPACE="regexp:\s+" @@ -97,7 +98,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 | 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 ; function ::= IDENTIFIER PUNC_LPAREN parameters? PUNC_RPAREN block ; parameters ::= IDENTIFIER ( PUNC_COMMA IDENTIFIER )* ; diff --git a/examples/basic/19_this.peb b/examples/basic/19_this.peb new file mode 100644 index 0000000..06b78fc --- /dev/null +++ b/examples/basic/19_this.peb @@ -0,0 +1,19 @@ +class Person { + get_fullname() { + return this.firstname + " " + this.lastname + } + + greet(person) { + print("Hello " + person.get_fullname()) + } +} + +let alice = Person() +alice.firstname = "Alice" +alice.lastname = "Foo" +let bob = Person() +bob.firstname = "Bob" +bob.lastname = "Bar" + +alice.greet(bob) +bob.greet(alice) \ No newline at end of file diff --git a/main.py b/main.py index f0421da..97f30e3 100644 --- a/main.py +++ b/main.py @@ -4,7 +4,7 @@ from src.pebble import Pebble def main(): - path: Path = Path("examples/basic/18_fields.peb") + path: Path = Path("examples/basic/19_this.peb") Pebble.run_file(path) diff --git a/src/ast/expr.py b/src/ast/expr.py index d2e7209..ed52ce1 100644 --- a/src/ast/expr.py +++ b/src/ast/expr.py @@ -57,6 +57,10 @@ class Expr(ABC): def visit_set_expr(self, expr: SetExpr) -> T: ... + @abstractmethod + def visit_this_expr(self, expr: ThisExpr) -> T: + ... + @dataclass(frozen=True) class AssignExpr(Expr): @@ -147,3 +151,11 @@ class SetExpr(Expr): def accept(self, visitor: Expr.Visitor[T]) -> T: return visitor.visit_set_expr(self) + + +@dataclass(frozen=True) +class ThisExpr(Expr): + keyword: Token + + def accept(self, visitor: Expr.Visitor[T]) -> T: + return visitor.visit_this_expr(self) diff --git a/src/core/function.py b/src/core/function.py index 929d269..5edd8ba 100644 --- a/src/core/function.py +++ b/src/core/function.py @@ -8,6 +8,7 @@ from src.interpreter.environment import Environment from src.interpreter.exceptions import ReturnException if TYPE_CHECKING: + from src.core.instance import PebbleInstance from src.interpreter.interpreter import Interpreter @@ -32,3 +33,8 @@ class PebbleFunction(PebbleCallable): def __str__(self): return f"" + + def bind(self, instance: PebbleInstance): + env: Environment = Environment(self.closure) + env.define("this", instance) + return PebbleFunction(self.declaration, env) diff --git a/src/core/instance.py b/src/core/instance.py index a78cb61..36e4780 100644 --- a/src/core/instance.py +++ b/src/core/instance.py @@ -24,7 +24,7 @@ class PebbleInstance: except KeyError: method: Optional[PebbleFunction] = self.klass.find_method(name.lexeme) if method is not None: - return method + return method.bind(self) raise PebbleRuntimeError(name, f"Undefined property '{name.lexeme}'.") def set(self, name: Token, value: Any) -> None: diff --git a/src/formatter.py b/src/formatter.py index b2ca8e6..1e6916f 100644 --- a/src/formatter.py +++ b/src/formatter.py @@ -1,15 +1,22 @@ +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 + CallExpr, SetExpr, GetExpr, ThisExpr, T from src.ast.stmt import Stmt, LetStmt, IfStmt, ExpressionStmt, BlockStmt, WhileStmt, ForStmt, FunctionStmt, \ ReturnStmt, BreakStmt, ContinueStmt, ClassStmt +class ClassType(Enum): + NONE = auto() + CLASS = auto() + + class Formatter(Expr.Visitor[str], Stmt.Visitor[str]): def __init__(self, indent: int = 4): self.indent: int = indent self.level: int = 0 + self.current_class: ClassType = ClassType.NONE def indented(self, text: str) -> str: return " " * (self.level * self.indent) + text @@ -36,6 +43,9 @@ class Formatter(Expr.Visitor[str], Stmt.Visitor[str]): def visit_set_expr(self, expr: SetExpr) -> str: return f"{self.format(expr.object)}.{expr.name.lexeme} = {self.format(expr.value)}" + def visit_this_expr(self, expr: ThisExpr) -> str: + return expr.keyword.lexeme + def visit_unary_expr(self, expr: UnaryExpr) -> str: return f"{expr.operator.lexeme}{self.format(expr.right)}" @@ -79,8 +89,11 @@ class Formatter(Expr.Visitor[str], Stmt.Visitor[str]): def visit_class_stmt(self, stmt: ClassStmt) -> str: res: str = self.indented(f"class {stmt.name.lexeme} {{\n") self.level += 1 + enclosing_class: ClassType = self.current_class + self.current_class = ClassType.CLASS for method in stmt.methods: res += self.format(method) + self.current_class = enclosing_class self.level -= 1 res += self.indented("}\n") return res @@ -89,12 +102,18 @@ class Formatter(Expr.Visitor[str], Stmt.Visitor[str]): return self.indented(self.format(stmt.expression) + "\n") def visit_function_stmt(self, stmt: FunctionStmt) -> str: - res: str = self.indented(f"fun {stmt.name.lexeme}") + res: str = self.indented("") + if self.current_class != ClassType.CLASS: + res += "fun " + res += stmt.name.lexeme res += f"({', '.join(param.lexeme for param in stmt.params)}) " res += "{\n" self.level += 1 + enclosing_class: ClassType = self.current_class + self.current_class = ClassType.NONE for sub_stmt in stmt.body: res += self.format(sub_stmt) + self.current_class = enclosing_class self.level -= 1 res += self.indented("}\n") return res diff --git a/src/interpreter/interpreter.py b/src/interpreter/interpreter.py index ba19a5e..a3f9b64 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 + CallExpr, GetExpr, T, SetExpr, ThisExpr from src.ast.stmt import Stmt, ExpressionStmt, LetStmt, BlockStmt, IfStmt, WhileStmt, ForStmt, FunctionStmt, \ ReturnStmt, BreakStmt, ContinueStmt, ClassStmt from src.core.callable import PebbleCallable @@ -87,6 +87,9 @@ class Interpreter(Expr.Visitor[Any], Stmt.Visitor[None]): obj.set(expr.name, value) return value + def visit_this_expr(self, expr: ThisExpr) -> Any: + return self.look_up_variable(expr.keyword, expr) + def visit_binary_expr(self, expr: BinaryExpr) -> Any: left: Any = self.evaluate(expr.left) right: Any = self.evaluate(expr.right) diff --git a/src/interpreter/resolver.py b/src/interpreter/resolver.py index 187769d..a743b60 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 + AssignExpr, GetExpr, SetExpr, ThisExpr from src.ast.stmt import Stmt, ForStmt, WhileStmt, LetStmt, ReturnStmt, IfStmt, FunctionStmt, \ ExpressionStmt, BlockStmt, BreakStmt, ContinueStmt, ClassStmt from src.pebble import Pebble @@ -110,6 +110,9 @@ class Resolver(Expr.Visitor[None], Stmt.Visitor[None]): self.resolve(expr.value) self.resolve(expr.object) + def visit_this_expr(self, expr: ThisExpr) -> None: + self.resolve_local(expr, expr.keyword) + def visit_block_stmt(self, stmt: BlockStmt) -> None: self.begin_scope() self.resolve(*stmt.statements) @@ -117,9 +120,15 @@ class Resolver(Expr.Visitor[None], Stmt.Visitor[None]): def visit_class_stmt(self, stmt: ClassStmt) -> None: self.declare(stmt.name) + self.define(stmt.name) + + self.begin_scope() + self.scopes[-1]["this"] = True + for method in stmt.methods: self.resolve_function(method, FunctionType.METHOD) - self.define(stmt.name) + + self.end_scope() def visit_expression_stmt(self, stmt: ExpressionStmt) -> None: self.resolve(stmt.expression) diff --git a/src/keyword.py b/src/keyword.py index d2df6fc..ac01f58 100644 --- a/src/keyword.py +++ b/src/keyword.py @@ -20,4 +20,5 @@ KEYWORDS: dict[str, TokenType] = { "break": TokenType.BREAK, "continue": TokenType.CONTINUE, "class": TokenType.CLASS, + "this": TokenType.THIS, } diff --git a/src/parser/parser.py b/src/parser/parser.py index cc0ada6..529b025 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 + CallExpr, GetExpr, SetExpr, ThisExpr from src.ast.stmt import Stmt, ExpressionStmt, LetStmt, BlockStmt, IfStmt, WhileStmt, ForStmt, FunctionStmt, \ ReturnStmt, BreakStmt, ContinueStmt, ClassStmt from src.consts import MAX_FUNCTION_ARGS @@ -363,6 +363,9 @@ class Parser: if self.match(TokenType.NUMBER, TokenType.STRING): return LiteralExpr(self.previous().value) + if self.match(TokenType.THIS): + return ThisExpr(self.previous()) + if self.match(TokenType.IDENTIFIER): return VariableExpr(self.previous()) diff --git a/src/token.py b/src/token.py index 57695fd..407297c 100644 --- a/src/token.py +++ b/src/token.py @@ -59,6 +59,7 @@ class TokenType(Enum): BREAK = auto() CONTINUE = auto() CLASS = auto() + THIS = auto() # Misc COMMENT = auto()