feat: add for loops

This commit is contained in:
2026-02-06 03:53:55 +01:00
parent be1b03bb1f
commit bf750748e3
7 changed files with 148 additions and 6 deletions

View File

@@ -1,15 +1,24 @@
print("1,2,3,4,5")
for i from 1 to 5 {
print(i)
}
print("0,2,4,6,8")
for j from 0 until 10 by 2 {
print(j)
}
print("0,1,2,3,4")
for k to 4 {
print(k)
}
print("5,4,3,2,1")
for k from 5 to 1 by -1 {
print(k)
}
print("0,3,6,9")
let l = 0
while l < 10 {
print(l)

View File

@@ -13,7 +13,7 @@ def main():
123
"This is
another string" """
path: str = "examples/10_while.peb"
path: str = "examples/05_loop.peb"
with open(path, "r") as f:
source = f.read()
lexer: Lexer = Lexer()

View File

@@ -41,6 +41,10 @@ class Stmt(ABC):
def visit_while_stmt(self, stmt: WhileStmt) -> T:
...
@abstractmethod
def visit_for_stmt(self, stmt: ForStmt) -> T:
...
@dataclass(frozen=True)
class BlockStmt(Stmt):
@@ -85,7 +89,6 @@ class LetStmt(Stmt):
return visitor.visit_let_stmt(self)
@dataclass(frozen=True)
class WhileStmt(Stmt):
condition: Expr
@@ -93,3 +96,18 @@ class WhileStmt(Stmt):
def accept(self, visitor: Stmt.Visitor[T]) -> T:
return visitor.visit_while_stmt(self)
@dataclass(frozen=True)
class ForStmt(Stmt):
variable: Token
start_token: Optional[Token]
start: Optional[Expr]
end_token: Optional[Token]
end: Optional[Expr]
step_token: Optional[Token]
step: Optional[Expr]
body: Stmt
def accept(self, visitor: Stmt.Visitor[T]) -> T:
return visitor.visit_for_stmt(self)

View File

@@ -1,7 +1,7 @@
from typing import Any
from typing import Any, Optional
from src.ast.expr import LiteralExpr, GroupingExpr, UnaryExpr, BinaryExpr, Expr, VariableExpr, AssignExpr, LogicalExpr
from src.ast.stmt import Stmt, PrintStmt, ExpressionStmt, LetStmt, BlockStmt, IfStmt, WhileStmt
from src.ast.stmt import Stmt, PrintStmt, ExpressionStmt, LetStmt, BlockStmt, IfStmt, WhileStmt, ForStmt
from src.interpreter.environment import Environment
from src.interpreter.error import PebbleRuntimeError
from src.pebble import Pebble
@@ -137,6 +137,54 @@ class Interpreter(Expr.Visitor[Any], Stmt.Visitor[None]):
while self.is_truthy(self.evaluate(stmt.condition)):
self.execute(stmt.body)
def visit_for_stmt(self, stmt: ForStmt) -> None:
previous_env: Environment = self.env
self.env = Environment(self.env)
start_value: float = 0
if stmt.start is not None:
start: Any = self.evaluate(stmt.start)
self.check_number_clause(stmt.start_token, start)
start_value = start
end_value: Optional[float] = None
inclusive: Optional[bool] = None
if stmt.end is not None:
end: Any = self.evaluate(stmt.end)
self.check_number_clause(stmt.end_token, end)
end_value = end
inclusive = stmt.end_token.type == TokenType.TO
step_value: float = 1
if stmt.step is not None:
step: Any = self.evaluate(stmt.step)
self.check_number_clause(stmt.step_token, step)
step_value = step
value: float = start_value
def condition() -> bool:
if end_value is None:
return False
match (step_value < 0, inclusive):
case (False, False):
return value < end_value
case (False, True):
return value <= end_value
case (True, False):
return value > end_value
case (True, True):
return value >= end_value
# Unreachable
return True
while condition():
self.env.define(stmt.variable.lexeme, value)
self.execute(stmt.body)
value += step_value
self.env = previous_env
def visit_let_stmt(self, stmt: LetStmt) -> None:
value: Any = None
if stmt.initializer is not None:
@@ -166,3 +214,9 @@ class Interpreter(Expr.Visitor[Any], Stmt.Visitor[None]):
if isinstance(left, float) and isinstance(right, float):
return
raise PebbleRuntimeError(operator, "Operands must be numbers.")
@staticmethod
def check_number_clause(clause: Token, value: Any):
if isinstance(value, float):
return
raise PebbleRuntimeError(clause, "For loop clauses must be numbers.")

View File

@@ -10,6 +10,7 @@ KEYWORDS: dict[str, TokenType] = {
"while": TokenType.WHILE,
"from": TokenType.FROM,
"to": TokenType.TO,
"until": TokenType.UNTIL,
"by": TokenType.BY,
"false": TokenType.FALSE,
"true": TokenType.TRUE,

View File

@@ -1,7 +1,7 @@
from typing import Optional
from src.ast.expr import Expr, BinaryExpr, UnaryExpr, LiteralExpr, GroupingExpr, VariableExpr, AssignExpr, LogicalExpr
from src.ast.stmt import Stmt, PrintStmt, ExpressionStmt, LetStmt, BlockStmt, IfStmt, WhileStmt
from src.ast.stmt import Stmt, PrintStmt, ExpressionStmt, LetStmt, BlockStmt, IfStmt, WhileStmt, ForStmt
from src.parser.error import ParsingError
from src.pebble import Pebble
from src.token import Token, TokenType
@@ -34,8 +34,8 @@ class Parser:
statements: list[Stmt] = []
self.skip_newlines()
while not self.is_at_end():
self.skip_newlines()
statements.append(self.declaration())
self.skip_newlines()
return statements
def skip_newlines(self):
@@ -107,6 +107,8 @@ class Parser:
return LetStmt(name, initializer)
def statement(self) -> Stmt:
if self.match(TokenType.FOR):
return self.for_stmt()
if self.match(TokenType.IF):
return self.if_stmt()
if self.match(TokenType.PRINT):
@@ -117,6 +119,61 @@ class Parser:
return self.block_stmt()
return self.expression_stmt()
def for_stmt(self) -> Stmt:
var: Token = self.consume(TokenType.IDENTIFIER, "Missing loop variable.")
end: Optional[Expr] = None
from_clause: Optional[Expr] = None
to_clause: Optional[Expr] = None
until_clause: Optional[Expr] = None
by_clause: Optional[Expr] = None
from_token: Optional[Token] = None
end_token: Optional[Token] = None
by_token: Optional[Token] = None
while self.match(TokenType.FROM, TokenType.TO, TokenType.UNTIL, TokenType.BY):
previous: Token = self.previous()
match previous.type:
case TokenType.FROM:
if from_clause is not None:
raise self.error(previous, "From clause already defined.")
from_clause = self.expression()
from_token = previous
case TokenType.TO:
if to_clause is not None:
raise self.error(previous, "To clause already defined.")
if until_clause is not None:
raise self.error(previous, "Until clause already defined.")
to_clause = self.expression()
end = to_clause
end_token = previous
case TokenType.UNTIL:
if until_clause is not None:
raise self.error(previous, "Until clause already defined.")
if to_clause is not None:
raise self.error(previous, "To clause already defined.")
until_clause = self.expression()
end = until_clause
end_token = previous
case TokenType.BY:
if by_clause is not None:
raise self.error(previous, "By clause already defined.")
by_clause = self.expression()
by_token = previous
body: Stmt = self.statement()
loop: Stmt = ForStmt(
variable=var,
start_token=from_token,
start=from_clause,
end_token=end_token,
end=end,
step_token=by_token,
step=by_clause,
body=body
)
return loop
def if_stmt(self) -> Stmt:
condition: Expr = self.expression()
then_branch: Stmt = self.statement()

View File

@@ -52,6 +52,7 @@ class TokenType(Enum):
WHILE = auto()
FROM = auto()
TO = auto()
UNTIL = auto()
BY = auto()
# Misc
@@ -73,5 +74,7 @@ class Token:
res: str = f"[{self.type.name}"
if self.value is not None:
res += f" ({self.value!r})"
elif self.type == TokenType.IDENTIFIER:
res += f" ({self.lexeme})"
res += "]"
return res