feat: add break statements

This commit is contained in:
2026-02-06 15:51:23 +01:00
parent a1bc85f3fa
commit a3a8068d61
11 changed files with 76 additions and 17 deletions

6
examples/16_break.peb Normal file
View File

@@ -0,0 +1,6 @@
for i to 10 {
if i == 5 {
break
}
print(i)
}

View File

@@ -4,7 +4,7 @@ from src.pebble import Pebble
def main(): def main():
path: Path = Path("examples/15_resolution.peb") path: Path = Path("examples/16_break.peb")
Pebble.run_file(path) Pebble.run_file(path)

View File

@@ -53,6 +53,10 @@ class Stmt(ABC):
def visit_for_stmt(self, stmt: ForStmt) -> T: def visit_for_stmt(self, stmt: ForStmt) -> T:
... ...
@abstractmethod
def visit_break_stmt(self, stmt: BreakStmt) -> T:
...
@dataclass(frozen=True) @dataclass(frozen=True)
class BlockStmt(Stmt): class BlockStmt(Stmt):
@@ -138,3 +142,11 @@ class ForStmt(Stmt):
def accept(self, visitor: Stmt.Visitor[T]) -> T: def accept(self, visitor: Stmt.Visitor[T]) -> T:
return visitor.visit_for_stmt(self) return visitor.visit_for_stmt(self)
@dataclass(frozen=True)
class BreakStmt(Stmt):
keyword: Token
def accept(self, visitor: Stmt.Visitor[T]) -> T:
return visitor.visit_break_stmt(self)

View File

@@ -1,6 +1,5 @@
from __future__ import annotations from __future__ import annotations
from enum import Enum, auto
from typing import Any, TYPE_CHECKING from typing import Any, TYPE_CHECKING
from src.ast.stmt import FunctionStmt from src.ast.stmt import FunctionStmt
@@ -12,11 +11,6 @@ if TYPE_CHECKING:
from src.interpreter.interpreter import Interpreter from src.interpreter.interpreter import Interpreter
class FunctionType(Enum):
NONE = auto()
FUNCTION = auto()
class PebbleFunction(PebbleCallable): class PebbleFunction(PebbleCallable):
def __init__(self, declaration: FunctionStmt, closure: Environment): def __init__(self, declaration: FunctionStmt, closure: Environment):
self.declaration: FunctionStmt = declaration self.declaration: FunctionStmt = declaration

View File

@@ -3,7 +3,7 @@ from typing import Any
from src.ast.expr import Expr, VariableExpr, LiteralExpr, GroupingExpr, UnaryExpr, BinaryExpr, AssignExpr, LogicalExpr, \ from src.ast.expr import Expr, VariableExpr, LiteralExpr, GroupingExpr, UnaryExpr, BinaryExpr, AssignExpr, LogicalExpr, \
CallExpr CallExpr
from src.ast.stmt import Stmt, LetStmt, PrintStmt, IfStmt, ExpressionStmt, BlockStmt, WhileStmt, ForStmt, FunctionStmt, \ from src.ast.stmt import Stmt, LetStmt, PrintStmt, IfStmt, ExpressionStmt, BlockStmt, WhileStmt, ForStmt, FunctionStmt, \
ReturnStmt ReturnStmt, BreakStmt
class Formatter(Expr.Visitor[str], Stmt.Visitor[str]): class Formatter(Expr.Visitor[str], Stmt.Visitor[str]):
@@ -85,10 +85,10 @@ class Formatter(Expr.Visitor[str], Stmt.Visitor[str]):
return res return res
def visit_if_stmt(self, stmt: IfStmt) -> str: def visit_if_stmt(self, stmt: IfStmt) -> str:
res: str = self.indented(f"if {self.format(stmt.condition)} {self.format(stmt.then_branch)}") res: str = self.indented(f"if {self.format(stmt.condition)} {self.format(stmt.then_branch).lstrip()}")
res = res.rstrip("\n") res = res.rstrip("\n")
if stmt.else_branch is not None: if stmt.else_branch is not None:
res += f" else {self.format(stmt.else_branch)}" res += f" else {self.format(stmt.else_branch).lstrip()}"
res = res.rstrip("\n") res = res.rstrip("\n")
res += "\n" res += "\n"
return res return res
@@ -128,3 +128,6 @@ class Formatter(Expr.Visitor[str], Stmt.Visitor[str]):
res += f" = {stmt.initializer.accept(self)}" res += f" = {stmt.initializer.accept(self)}"
res += "\n" res += "\n"
return res return res
def visit_break_stmt(self, stmt: BreakStmt) -> str:
return self.indented(f"{stmt.keyword.lexeme}\n")

View File

@@ -5,3 +5,7 @@ class ReturnException(RuntimeError):
def __init__(self, value: Any): def __init__(self, value: Any):
super().__init__() super().__init__()
self.value: Any = value self.value: Any = value
class BreakException(RuntimeError):
pass

View File

@@ -4,12 +4,12 @@ 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 CallExpr
from src.ast.stmt import Stmt, PrintStmt, ExpressionStmt, LetStmt, BlockStmt, IfStmt, WhileStmt, ForStmt, FunctionStmt, \ from src.ast.stmt import Stmt, PrintStmt, ExpressionStmt, LetStmt, BlockStmt, IfStmt, WhileStmt, ForStmt, FunctionStmt, \
ReturnStmt ReturnStmt, BreakStmt
from src.core.callable import PebbleCallable from src.core.callable import PebbleCallable
from src.core.function import PebbleFunction from src.core.function import PebbleFunction
from src.interpreter.environment import Environment from src.interpreter.environment import Environment
from src.interpreter.error import PebbleRuntimeError from src.interpreter.error import PebbleRuntimeError
from src.interpreter.exceptions import ReturnException from src.interpreter.exceptions import ReturnException, BreakException
from src.interpreter.globals import GlobalEnvironment from src.interpreter.globals import GlobalEnvironment
from src.pebble import Pebble from src.pebble import Pebble
from src.token import TokenType, Token from src.token import TokenType, Token
@@ -179,7 +179,10 @@ class Interpreter(Expr.Visitor[Any], Stmt.Visitor[None]):
def visit_while_stmt(self, stmt: WhileStmt) -> None: def visit_while_stmt(self, stmt: WhileStmt) -> None:
while self.is_truthy(self.evaluate(stmt.condition)): while self.is_truthy(self.evaluate(stmt.condition)):
try:
self.execute(stmt.body) self.execute(stmt.body)
except BreakException:
break
def visit_for_stmt(self, stmt: ForStmt) -> None: def visit_for_stmt(self, stmt: ForStmt) -> None:
previous_env: Environment = self.env previous_env: Environment = self.env
@@ -224,7 +227,10 @@ class Interpreter(Expr.Visitor[Any], Stmt.Visitor[None]):
while condition(): while condition():
self.env.define(stmt.variable.lexeme, value) self.env.define(stmt.variable.lexeme, value)
try:
self.execute(stmt.body) self.execute(stmt.body)
except BreakException:
break
value += step_value value += step_value
self.env = previous_env self.env = previous_env
@@ -235,6 +241,9 @@ class Interpreter(Expr.Visitor[Any], Stmt.Visitor[None]):
value = self.evaluate(stmt.initializer) value = self.evaluate(stmt.initializer)
self.env.define(stmt.name.lexeme, value) self.env.define(stmt.name.lexeme, value)
def visit_break_stmt(self, stmt: BreakStmt) -> None:
raise BreakException()
@staticmethod @staticmethod
def is_truthy(value: Any) -> bool: def is_truthy(value: Any) -> bool:
if value is None or value is False: if value is None or value is False:

View File

@@ -1,12 +1,12 @@
from __future__ import annotations from __future__ import annotations
from enum import Enum, auto
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
from src.ast.expr import Expr, LogicalExpr, VariableExpr, LiteralExpr, GroupingExpr, CallExpr, UnaryExpr, BinaryExpr, \ from src.ast.expr import Expr, LogicalExpr, VariableExpr, LiteralExpr, GroupingExpr, CallExpr, UnaryExpr, BinaryExpr, \
AssignExpr AssignExpr
from src.ast.stmt import Stmt, ForStmt, WhileStmt, LetStmt, ReturnStmt, PrintStmt, IfStmt, FunctionStmt, \ from src.ast.stmt import Stmt, ForStmt, WhileStmt, LetStmt, ReturnStmt, PrintStmt, IfStmt, FunctionStmt, \
ExpressionStmt, BlockStmt ExpressionStmt, BlockStmt, BreakStmt, T
from src.core.function import FunctionType
from src.pebble import Pebble from src.pebble import Pebble
from src.token import Token from src.token import Token
@@ -14,11 +14,23 @@ if TYPE_CHECKING:
from src.interpreter.interpreter import Interpreter from src.interpreter.interpreter import Interpreter
class FunctionType(Enum):
NONE = auto()
FUNCTION = auto()
class LoopType(Enum):
NONE = auto()
WHILE = auto()
FOR = auto()
class Resolver(Expr.Visitor[None], Stmt.Visitor[None]): class Resolver(Expr.Visitor[None], Stmt.Visitor[None]):
def __init__(self, interpreter: Interpreter): def __init__(self, interpreter: Interpreter):
self.interpreter: Interpreter = interpreter self.interpreter: Interpreter = interpreter
self.scopes: list[dict[str, bool]] = [] self.scopes: list[dict[str, bool]] = []
self.current_func: FunctionType = FunctionType.NONE self.current_func: FunctionType = FunctionType.NONE
self.current_loop: LoopType = LoopType.NONE
def resolve(self, *objects: Expr | Stmt) -> None: def resolve(self, *objects: Expr | Stmt) -> None:
for obj in objects: for obj in objects:
@@ -126,10 +138,15 @@ class Resolver(Expr.Visitor[None], Stmt.Visitor[None]):
self.define(stmt.name) self.define(stmt.name)
def visit_while_stmt(self, stmt: WhileStmt) -> None: def visit_while_stmt(self, stmt: WhileStmt) -> None:
enclosing_loop: LoopType = self.current_loop
self.current_loop = LoopType.WHILE
self.resolve(stmt.condition) self.resolve(stmt.condition)
self.resolve(stmt.body) self.resolve(stmt.body)
self.current_loop = enclosing_loop
def visit_for_stmt(self, stmt: ForStmt) -> None: def visit_for_stmt(self, stmt: ForStmt) -> None:
enclosing_loop: LoopType = self.current_loop
self.current_loop = LoopType.FOR
self.begin_scope() self.begin_scope()
self.declare(stmt.variable) self.declare(stmt.variable)
self.define(stmt.variable) self.define(stmt.variable)
@@ -141,3 +158,8 @@ class Resolver(Expr.Visitor[None], Stmt.Visitor[None]):
self.resolve(stmt.step) self.resolve(stmt.step)
self.resolve(stmt.body) self.resolve(stmt.body)
self.end_scope() self.end_scope()
self.current_loop = enclosing_loop
def visit_break_stmt(self, stmt: BreakStmt) -> None:
if self.current_loop == LoopType.NONE:
Pebble.token_error(stmt.keyword, "Cannot break outside a loop")

View File

@@ -18,4 +18,5 @@ KEYWORDS: dict[str, TokenType] = {
"null": TokenType.NULL, "null": TokenType.NULL,
"print": TokenType.PRINT, "print": TokenType.PRINT,
"return": TokenType.RETURN, "return": TokenType.RETURN,
"break": TokenType.BREAK,
} }

View File

@@ -3,7 +3,7 @@ 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 CallExpr
from src.ast.stmt import Stmt, PrintStmt, ExpressionStmt, LetStmt, BlockStmt, IfStmt, WhileStmt, ForStmt, FunctionStmt, \ from src.ast.stmt import Stmt, PrintStmt, ExpressionStmt, LetStmt, BlockStmt, IfStmt, WhileStmt, ForStmt, FunctionStmt, \
ReturnStmt ReturnStmt, BreakStmt
from src.consts import MAX_FUNCTION_ARGS from src.consts import MAX_FUNCTION_ARGS
from src.parser.error import ParsingError from src.parser.error import ParsingError
from src.pebble import Pebble from src.pebble import Pebble
@@ -134,6 +134,8 @@ class Parser:
return self.print_stmt() return self.print_stmt()
if self.match(TokenType.RETURN): if self.match(TokenType.RETURN):
return self.return_stmt() return self.return_stmt()
if self.match(TokenType.BREAK):
return self.break_stmt()
if self.match(TokenType.WHILE): if self.match(TokenType.WHILE):
return self.while_stmt() return self.while_stmt()
if self.match(TokenType.LEFT_BRACE): if self.match(TokenType.LEFT_BRACE):
@@ -218,6 +220,11 @@ class Parser:
self.expect_eol("Expected end of line after return statement.") self.expect_eol("Expected end of line after return statement.")
return ReturnStmt(keyword, value) return ReturnStmt(keyword, value)
def break_stmt(self) -> Stmt:
keyword: Token = self.previous()
self.expect_eol("Expected end of line after break statement.")
return BreakStmt(keyword)
def while_stmt(self) -> Stmt: def while_stmt(self) -> Stmt:
condition: Expr = self.expression() condition: Expr = self.expression()
body: Stmt = self.statement() body: Stmt = self.statement()

View File

@@ -56,6 +56,7 @@ class TokenType(Enum):
UNTIL = auto() UNTIL = auto()
BY = auto() BY = auto()
RETURN = auto() RETURN = auto()
BREAK = auto()
# Misc # Misc
PRINT = auto() PRINT = auto()