diff --git a/examples/05_loop.peb b/examples/05_loop.peb index f2b7a9d..4fc8eae 100644 --- a/examples/05_loop.peb +++ b/examples/05_loop.peb @@ -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) diff --git a/main.py b/main.py index 0a4cb36..7ee7e42 100644 --- a/main.py +++ b/main.py @@ -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() diff --git a/src/ast/stmt.py b/src/ast/stmt.py index 1cb5be6..a3e9551 100644 --- a/src/ast/stmt.py +++ b/src/ast/stmt.py @@ -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) diff --git a/src/interpreter/interpreter.py b/src/interpreter/interpreter.py index 6f99d24..1bdb1f0 100644 --- a/src/interpreter/interpreter.py +++ b/src/interpreter/interpreter.py @@ -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.") diff --git a/src/keyword.py b/src/keyword.py index fb369a1..2b39d4e 100644 --- a/src/keyword.py +++ b/src/keyword.py @@ -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, diff --git a/src/parser/parser.py b/src/parser/parser.py index 21d8638..3c4ccdd 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 -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() diff --git a/src/token.py b/src/token.py index 1176d19..99c04f5 100644 --- a/src/token.py +++ b/src/token.py @@ -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