From 9e4855598fa24ca8eb9947c5265d3807c95faf13 Mon Sep 17 00:00:00 2001 From: LordBaryhobal Date: Fri, 6 Feb 2026 23:51:23 +0100 Subject: [PATCH] feat: add basic inheritance --- docs/grammar.bnf | 2 +- examples/basic/21_super.peb | 37 ++++++++++++++++++++++++++++++++++ main.py | 2 +- src/ast/stmt.py | 3 ++- src/core/klass.py | 11 ++++++++-- src/formatter.py | 6 +++++- src/interpreter/interpreter.py | 8 +++++++- src/interpreter/resolver.py | 5 +++++ src/parser/parser.py | 6 +++++- 9 files changed, 72 insertions(+), 8 deletions(-) create mode 100644 examples/basic/21_super.peb diff --git a/docs/grammar.bnf b/docs/grammar.bnf index e39b516..ba82b2b 100644 --- a/docs/grammar.bnf +++ b/docs/grammar.bnf @@ -61,7 +61,7 @@ root ::= declaration* <> ; declaration ::= classDecl | funDecl | varDecl | statement ; -classDecl ::= KW_CLASS IDENTIFIER PUNC_LBRACE function* PUNC_RBRACE ; +classDecl ::= KW_CLASS IDENTIFIER ( OP_LESS IDENTIFIER )? PUNC_LBRACE function* PUNC_RBRACE ; funDecl ::= KW_FUN function ; varDecl ::= KW_LET IDENTIFIER ( OP_EQUAL expression )? ; diff --git a/examples/basic/21_super.peb b/examples/basic/21_super.peb new file mode 100644 index 0000000..430b370 --- /dev/null +++ b/examples/basic/21_super.peb @@ -0,0 +1,37 @@ +class Employee { + get_salary() { + return 100 + } + + has_responsibilities() { + return false + } +} + +class Manager < Employee { + get_salary() { + return 300 + } + + has_responsibilities() { + return true + } +} + +class Boss < Manager { + get_salary() { + return 500 + } +} + +let employee = Employee() +let manager = Manager() +let boss = Boss() + +print(employee, employee.get_salary()) +print(manager, manager.get_salary()) +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 diff --git a/main.py b/main.py index f5f582c..a7be3a5 100644 --- a/main.py +++ b/main.py @@ -4,7 +4,7 @@ from src.pebble import Pebble def main(): - path: Path = Path("examples/basic/20_init.peb") + path: Path = Path("examples/basic/21_super.peb") Pebble.run_file(path) diff --git a/src/ast/stmt.py b/src/ast/stmt.py index 84ddbdb..6e64837 100644 --- a/src/ast/stmt.py +++ b/src/ast/stmt.py @@ -4,7 +4,7 @@ from abc import ABC, abstractmethod from dataclasses import dataclass from typing import TypeVar, Generic, Optional -from src.ast.expr import Expr +from src.ast.expr import Expr, VariableExpr from src.token import Token T = TypeVar("T") @@ -73,6 +73,7 @@ class BlockStmt(Stmt): @dataclass(frozen=True) class ClassStmt(Stmt): name: Token + superclass: Optional[VariableExpr] methods: list[FunctionStmt] def accept(self, visitor: Stmt.Visitor[T]) -> T: diff --git a/src/core/klass.py b/src/core/klass.py index 0f39bf7..99d5786 100644 --- a/src/core/klass.py +++ b/src/core/klass.py @@ -12,8 +12,9 @@ if TYPE_CHECKING: class PebbleClass(PebbleCallable): - def __init__(self, name: str, methods: dict[str, PebbleFunction]): + def __init__(self, name: str, superclass: Optional[PebbleClass], methods: dict[str, PebbleFunction]): self.name: str = name + self.superclass: Optional[PebbleClass] = superclass self.methods: dict[str, PebbleFunction] = methods def __str__(self): @@ -33,4 +34,10 @@ class PebbleClass(PebbleCallable): return instance def find_method(self, name: str) -> Optional[PebbleFunction]: - return self.methods.get(name) + if name in self.methods: + return self.methods[name] + + if self.superclass is not None: + return self.superclass.find_method(name) + + return None diff --git a/src/formatter.py b/src/formatter.py index 1e6916f..e3a635a 100644 --- a/src/formatter.py +++ b/src/formatter.py @@ -87,7 +87,11 @@ class Formatter(Expr.Visitor[str], Stmt.Visitor[str]): return res def visit_class_stmt(self, stmt: ClassStmt) -> str: - res: str = self.indented(f"class {stmt.name.lexeme} {{\n") + res: str = self.indented("") + res += f"class {stmt.name.lexeme} " + if stmt.superclass is not None: + res += f"< {stmt.superclass.name.lexeme} " + res += "{\n" self.level += 1 enclosing_class: ClassType = self.current_class self.current_class = ClassType.CLASS diff --git a/src/interpreter/interpreter.py b/src/interpreter/interpreter.py index cfe7fa3..3a11b98 100644 --- a/src/interpreter/interpreter.py +++ b/src/interpreter/interpreter.py @@ -177,12 +177,18 @@ class Interpreter(Expr.Visitor[Any], Stmt.Visitor[None]): self.execute_block(stmt.statements, Environment(self.env)) def visit_class_stmt(self, stmt: ClassStmt) -> None: + superclass: Any = None + if stmt.superclass is not None: + superclass = self.evaluate(stmt.superclass) + if not isinstance(superclass, PebbleClass): + raise PebbleRuntimeError(stmt.superclass.name, "Superclass must be a class.") + self.env.define(stmt.name.lexeme, None) 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, methods) + klass: PebbleClass = PebbleClass(stmt.name.lexeme, superclass, methods) 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 8cb3460..f46e9e5 100644 --- a/src/interpreter/resolver.py +++ b/src/interpreter/resolver.py @@ -134,6 +134,11 @@ class Resolver(Expr.Visitor[None], Stmt.Visitor[None]): self.declare(stmt.name) self.define(stmt.name) + if stmt.superclass is not None: + if stmt.name.lexeme == stmt.superclass.name.lexeme: + Pebble.token_error(stmt.superclass.name, "A class cannot inherit from itself.") + self.resolve(stmt.superclass) + self.begin_scope() self.scopes[-1]["this"] = True diff --git a/src/parser/parser.py b/src/parser/parser.py index 529b025..f60c58c 100644 --- a/src/parser/parser.py +++ b/src/parser/parser.py @@ -97,13 +97,17 @@ class Parser: def class_declaration(self) -> Stmt: name: Token = self.consume(TokenType.IDENTIFIER, "Expected class name.") + superclass: Optional[VariableExpr] = None + if self.match(TokenType.LESS): + self.consume(TokenType.IDENTIFIER, "Expected superclass name.") + superclass = VariableExpr(self.previous()) self.consume(TokenType.LEFT_BRACE, "Expected '{' before class body.") methods: list[FunctionStmt] = [] while not self.check(TokenType.RIGHT_BRACE) and not self.is_at_end(): methods.append(self.function("method")) self.consume(TokenType.RIGHT_BRACE, "Expected '}' after class body.") - return ClassStmt(name, methods) + return ClassStmt(name, superclass, methods) def function(self, kind: str) -> FunctionStmt: # TODO: allow anonymous/lambda functions