From b06bce25584c3e49f9adc779fd9a3098f70e1c09 Mon Sep 17 00:00:00 2001 From: LordBaryhobal Date: Fri, 6 Feb 2026 21:25:24 +0100 Subject: [PATCH] feat: add instance field getter --- docs/grammar.bnf | 2 +- src/ast/expr.py | 13 +++++++++++++ src/core/instance.py | 12 +++++++++++- src/interpreter/interpreter.py | 9 ++++++++- src/interpreter/resolver.py | 5 ++++- src/parser/parser.py | 5 ++++- 6 files changed, 41 insertions(+), 5 deletions(-) diff --git a/docs/grammar.bnf b/docs/grammar.bnf index 6d8d6ae..9d4f175 100644 --- a/docs/grammar.bnf +++ b/docs/grammar.bnf @@ -96,7 +96,7 @@ term ::= factor ( ( OP_MINUS | OP_PLUS ) factor )* ; factor ::= unary ( ( OP_SLASH | OP_STAR ) unary )* ; unary ::= ( OP_BANG | OP_MINUS ) unary | call ; -call ::= primary ( PUNC_LPAREN arguments? PUNC_RPAREN )* ; +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 ; function ::= IDENTIFIER PUNC_LPAREN parameters? PUNC_RPAREN block ; diff --git a/src/ast/expr.py b/src/ast/expr.py index 5a60189..b5ee0d7 100644 --- a/src/ast/expr.py +++ b/src/ast/expr.py @@ -33,6 +33,10 @@ class Expr(ABC): def visit_call_expr(self, expr: CallExpr) -> T: ... + @abstractmethod + def visit_get_expr(self, expr: GetExpr) -> T: + ... + @abstractmethod def visit_grouping_expr(self, expr: GroupingExpr) -> T: ... @@ -88,6 +92,15 @@ class CallExpr(Expr): return visitor.visit_call_expr(self) +@dataclass(frozen=True) +class GetExpr(Expr): + object: Expr + name: Token + + def accept(self, visitor: Expr.Visitor[T]) -> T: + return visitor.visit_get_expr(self) + + @dataclass(frozen=True) class GroupingExpr(Expr): expression: Expr diff --git a/src/core/instance.py b/src/core/instance.py index cdf87cc..195309a 100644 --- a/src/core/instance.py +++ b/src/core/instance.py @@ -1,6 +1,9 @@ from __future__ import annotations -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, Any + +from src.interpreter.error import PebbleRuntimeError +from src.token import Token if TYPE_CHECKING: from src.core.klass import PebbleClass @@ -9,6 +12,13 @@ if TYPE_CHECKING: class PebbleInstance: def __init__(self, klass: PebbleClass): self.klass: PebbleClass = klass + self.fields: dict[str, Any] = {} def __str__(self): return f"" + + def get(self, name: Token) -> Any: + try: + return self.fields[name.lexeme] + except KeyError: + raise PebbleRuntimeError(name, f"Undefined property '{name.lexeme}'.") diff --git a/src/interpreter/interpreter.py b/src/interpreter/interpreter.py index 92583ed..7a453d5 100644 --- a/src/interpreter/interpreter.py +++ b/src/interpreter/interpreter.py @@ -1,11 +1,12 @@ from typing import Any, Optional from src.ast.expr import LiteralExpr, GroupingExpr, UnaryExpr, BinaryExpr, Expr, VariableExpr, AssignExpr, LogicalExpr, \ - CallExpr + CallExpr, GetExpr, T from src.ast.stmt import Stmt, ExpressionStmt, LetStmt, BlockStmt, IfStmt, WhileStmt, ForStmt, FunctionStmt, \ ReturnStmt, BreakStmt, ContinueStmt, ClassStmt from src.core.callable import PebbleCallable from src.core.function import PebbleFunction +from src.core.instance import PebbleInstance from src.core.klass import PebbleClass from src.interpreter.environment import Environment from src.interpreter.error import PebbleRuntimeError @@ -143,6 +144,12 @@ class Interpreter(Expr.Visitor[Any], Stmt.Visitor[None]): raise PebbleRuntimeError(expr.paren, f"Expected {arity} arguments but got {len(arguments)}.") return function.call(self, arguments) + def visit_get_expr(self, expr: GetExpr) -> Any: + obj: Any = self.evaluate(expr.object) + if isinstance(obj, PebbleInstance): + return obj.get(expr.name) + raise PebbleRuntimeError(expr.name, "Only class instances have properties.") + def visit_grouping_expr(self, expr: GroupingExpr) -> Any: return self.evaluate(expr.expression) diff --git a/src/interpreter/resolver.py b/src/interpreter/resolver.py index c904760..8b8b5a1 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 + AssignExpr, GetExpr from src.ast.stmt import Stmt, ForStmt, WhileStmt, LetStmt, ReturnStmt, IfStmt, FunctionStmt, \ ExpressionStmt, BlockStmt, BreakStmt, ContinueStmt, ClassStmt from src.pebble import Pebble @@ -87,6 +87,9 @@ class Resolver(Expr.Visitor[None], Stmt.Visitor[None]): for arg in expr.arguments: self.resolve(arg) + def visit_get_expr(self, expr: GetExpr) -> None: + self.resolve(expr.object) + def visit_grouping_expr(self, expr: GroupingExpr) -> None: self.resolve(expr.expression) diff --git a/src/parser/parser.py b/src/parser/parser.py index e8dccc3..b78fdb1 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 + CallExpr, GetExpr from src.ast.stmt import Stmt, ExpressionStmt, LetStmt, BlockStmt, IfStmt, WhileStmt, ForStmt, FunctionStmt, \ ReturnStmt, BreakStmt, ContinueStmt, ClassStmt from src.consts import MAX_FUNCTION_ARGS @@ -333,6 +333,9 @@ class Parser: while True: if self.match(TokenType.LEFT_PAREN): expr = self.finish_call(expr) + elif self.match(TokenType.DOT): + name: Token = self.consume(TokenType.IDENTIFIER, "Expected property name after '.'.") + expr = GetExpr(expr, name) else: break return expr