From 9d5fbc8c4589106691602426baabb1b86fbddf32 Mon Sep 17 00:00:00 2001 From: LordBaryhobal Date: Fri, 6 Feb 2026 13:02:21 +0100 Subject: [PATCH] feat: add function calls --- src/ast/expr.py | 14 ++++++++++++++ src/consts.py | 1 + src/core/__init__.py | 0 src/core/callable.py | 17 +++++++++++++++++ src/interpreter/interpreter.py | 17 ++++++++++++++++- src/parser/parser.py | 28 ++++++++++++++++++++++++++-- 6 files changed, 74 insertions(+), 3 deletions(-) create mode 100644 src/consts.py create mode 100644 src/core/__init__.py create mode 100644 src/core/callable.py diff --git a/src/ast/expr.py b/src/ast/expr.py index 63f5883..5a60189 100644 --- a/src/ast/expr.py +++ b/src/ast/expr.py @@ -29,6 +29,10 @@ class Expr(ABC): def visit_unary_expr(self, expr: UnaryExpr) -> T: ... + @abstractmethod + def visit_call_expr(self, expr: CallExpr) -> T: + ... + @abstractmethod def visit_grouping_expr(self, expr: GroupingExpr) -> T: ... @@ -74,6 +78,16 @@ class UnaryExpr(Expr): return visitor.visit_unary_expr(self) +@dataclass(frozen=True) +class CallExpr(Expr): + callee: Expr + paren: Token + arguments: list[Expr] + + def accept(self, visitor: Expr.Visitor[T]) -> T: + return visitor.visit_call_expr(self) + + @dataclass(frozen=True) class GroupingExpr(Expr): expression: Expr diff --git a/src/consts.py b/src/consts.py new file mode 100644 index 0000000..22fa3d8 --- /dev/null +++ b/src/consts.py @@ -0,0 +1 @@ +MAX_FUNCTION_ARGS = 255 diff --git a/src/core/__init__.py b/src/core/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/core/callable.py b/src/core/callable.py new file mode 100644 index 0000000..99d0770 --- /dev/null +++ b/src/core/callable.py @@ -0,0 +1,17 @@ +from __future__ import annotations + +from abc import ABC, abstractmethod +from typing import Any, TYPE_CHECKING + +if TYPE_CHECKING: + from src.interpreter.interpreter import Interpreter + + +class PebbleCallable(ABC): + @abstractmethod + def arity(self) -> int: + ... + + @abstractmethod + def call(self, interpreter: Interpreter, arguments: list[Any]) -> Any: + ... diff --git a/src/interpreter/interpreter.py b/src/interpreter/interpreter.py index 1bdb1f0..96dd95d 100644 --- a/src/interpreter/interpreter.py +++ b/src/interpreter/interpreter.py @@ -1,7 +1,9 @@ from typing import Any, Optional -from src.ast.expr import LiteralExpr, GroupingExpr, UnaryExpr, BinaryExpr, Expr, VariableExpr, AssignExpr, LogicalExpr +from src.ast.expr import LiteralExpr, GroupingExpr, UnaryExpr, BinaryExpr, Expr, VariableExpr, AssignExpr, LogicalExpr, \ + CallExpr from src.ast.stmt import Stmt, PrintStmt, ExpressionStmt, LetStmt, BlockStmt, IfStmt, WhileStmt, ForStmt +from src.core.callable import PebbleCallable from src.interpreter.environment import Environment from src.interpreter.error import PebbleRuntimeError from src.pebble import Pebble @@ -108,6 +110,19 @@ class Interpreter(Expr.Visitor[Any], Stmt.Visitor[None]): # Unreachable return None + def visit_call_expr(self, expr: CallExpr) -> Any: + callee: Any = self.evaluate(expr.callee) + arguments: list[Any] = [ + self.evaluate(arg) + for arg in expr.arguments + ] + if not isinstance(callee, PebbleCallable): + raise PebbleRuntimeError(expr.paren, "Can only call functions and classes.") + function: PebbleCallable = callee + if len(arguments) != function.arity(): + raise PebbleRuntimeError(expr.paren, f"Expected {function.arity()} arguments but got {len(arguments)}.") + return function.call(self, arguments) + def visit_grouping_expr(self, expr: GroupingExpr) -> Any: return self.evaluate(expr.expression) diff --git a/src/parser/parser.py b/src/parser/parser.py index 3c4ccdd..1bc4e70 100644 --- a/src/parser/parser.py +++ b/src/parser/parser.py @@ -1,7 +1,9 @@ from typing import Optional -from src.ast.expr import Expr, BinaryExpr, UnaryExpr, LiteralExpr, GroupingExpr, VariableExpr, AssignExpr, LogicalExpr +from src.ast.expr import Expr, BinaryExpr, UnaryExpr, LiteralExpr, GroupingExpr, VariableExpr, AssignExpr, LogicalExpr, \ + CallExpr from src.ast.stmt import Stmt, PrintStmt, ExpressionStmt, LetStmt, BlockStmt, IfStmt, WhileStmt, ForStmt +from src.consts import MAX_FUNCTION_ARGS from src.parser.error import ParsingError from src.pebble import Pebble from src.token import Token, TokenType @@ -286,7 +288,29 @@ class Parser: operator: Token = self.previous() right: Expr = self.unary() return UnaryExpr(operator, right) - return self.primary() + return self.call() + + def call(self) -> Expr: + expr: Expr = self.primary() + while True: + if self.match(TokenType.LEFT_PAREN): + expr = self.finish_call(expr) + else: + break + return expr + + def finish_call(self, callee: Expr) -> Expr: + arguments: list[Expr] = [] + if not self.check(TokenType.RIGHT_PAREN): + while True: + if len(arguments) >= MAX_FUNCTION_ARGS: + self.error(self.peek(), f"Cannot have more than {MAX_FUNCTION_ARGS} arguments.") + arguments.append(self.expression()) + if not self.match(TokenType.COMMA): + break + + paren: Token = self.consume(TokenType.RIGHT_PAREN, "Expected ')' after arguments.") + return CallExpr(callee, paren, arguments) def primary(self) -> Expr: if self.match(TokenType.FALSE):