Skip to content

Commit 952af36

Browse files
committed
add support of break statement
1 parent ff55c97 commit 952af36

File tree

9 files changed

+96
-20
lines changed

9 files changed

+96
-20
lines changed

pylox/interpreter.py

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,10 @@
99
from pylox.stmt import Stmt, StmtVisitor
1010

1111

12+
class BreakException(RuntimeError):
13+
pass
14+
15+
1216
class Interpreter(ExprVisitor, StmtVisitor):
1317
def __init__(self):
1418
self.environment = Environment()
@@ -35,11 +39,17 @@ def visit_if_stmt(self, stmt: stmt_ast.If) -> typing.Any:
3539
return None
3640

3741
def visit_while_stmt(self, stmt: stmt_ast.While) -> typing.Any:
38-
while self.is_truthy(self.evaluate(stmt.condition)):
39-
self.execute(stmt.body)
42+
try:
43+
while self.is_truthy(self.evaluate(stmt.condition)):
44+
self.execute(stmt.body)
45+
except BreakException:
46+
pass # Do nothing.
4047

4148
return None
4249

50+
def visit_break_stmt(self, stmt) -> typing.Any:
51+
raise BreakException()
52+
4353
def visit_block_stmt(self, stmt: stmt_ast.Block) -> typing.Any:
4454
self.execute_block(stmt.statements, Environment(self.environment))
4555
return None
@@ -206,4 +216,10 @@ def stringify(value: typing.Any) -> str:
206216

207217
@staticmethod
208218
def equals(left: typing.Any, right: typing.Any) -> bool:
209-
return type(left) == type(right) and left == right
219+
try:
220+
Interpreter.check_number_operands(
221+
Token(TokenType.EQUAL_EQUAL, "", "", -1), left, right
222+
)
223+
return left == right
224+
except LoxRuntimeError:
225+
return type(left) == type(right) and left == right

pylox/parser.py

Lines changed: 37 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ def __init__(
1919
self.report_error = report_error
2020
self.allow_expressions = False
2121
self.found_expression = False
22+
self.loop_depth = 0
2223

2324
# program → declaration * EOF;
2425
def parse(self) -> list[Stmt]:
@@ -113,6 +114,8 @@ def synchronize(self) -> None:
113114
return
114115
elif self.peek().token_type == TokenType.RETURN:
115116
return
117+
elif self.peek().token_type == TokenType.BREAK:
118+
return
116119
self.advance()
117120

118121
# statements
@@ -149,6 +152,7 @@ def var_declaration(self) -> Stmt:
149152
# | printStmt
150153
# | whileStmt
151154
# | block;
155+
# | break;
152156
def statement(self) -> Stmt:
153157
if self.match(TokenType.IF):
154158
return self.if_statement()
@@ -160,17 +164,32 @@ def statement(self) -> Stmt:
160164
return self.for_statement()
161165
if self.match(TokenType.LEFT_BRACE):
162166
return stmt_ast.Block(self.block())
167+
if self.match(TokenType.BREAK):
168+
return self.break_statement()
163169

164170
return self.expression_statement()
165171

172+
# break -> 'break' ;
173+
def break_statement(self) -> Stmt:
174+
if self.loop_depth == 0:
175+
self.error(
176+
self.previous(), "Must be inside a loop to use 'break'."
177+
)
178+
179+
self.consume(TokenType.SEMICOLON, "Expect ';' after 'break'.")
180+
return stmt_ast.Break()
181+
166182
# whileStmt → "while" "(" expression ")" statement ;
167183
def while_statement(self) -> Stmt:
168184
self.consume(TokenType.LEFT_PAREN, "Expect '(' after 'while'.")
169185
condition = self.expression()
170186
self.consume(TokenType.RIGHT_PAREN, "Expect ')' after condition.")
171-
body = self.statement()
172-
173-
return stmt_ast.While(condition, body)
187+
try:
188+
self.loop_depth += 1
189+
body = self.statement()
190+
return stmt_ast.While(condition, body)
191+
finally:
192+
self.loop_depth -= 1
174193

175194
# forStmt → "for" "(" ( varDecl | exprStmt | ";" )
176195
# expression? ";"
@@ -196,21 +215,25 @@ def for_statement(self) -> Stmt:
196215
increment = self.expression()
197216
self.consume(TokenType.RIGHT_PAREN, "Expect ')' after for clauses.")
198217

199-
body = self.statement()
218+
try:
219+
self.loop_depth += 1
220+
body = self.statement()
200221

201-
if increment is not None:
202-
body = stmt_ast.Block([body, stmt_ast.Expression(increment)])
222+
if increment is not None:
223+
body = stmt_ast.Block([body, stmt_ast.Expression(increment)])
203224

204-
if condition is None:
205-
condition = expr_ast.Literal(True)
225+
if condition is None:
226+
condition = expr_ast.Literal(True)
206227

207-
result: stmt_ast.While | stmt_ast.Block = stmt_ast.While(
208-
condition, body
209-
)
210-
if initializer is not None:
211-
result = stmt_ast.Block([initializer, result])
228+
result: stmt_ast.While | stmt_ast.Block = stmt_ast.While(
229+
condition, body
230+
)
231+
if initializer is not None:
232+
result = stmt_ast.Block([initializer, result])
212233

213-
return result
234+
return result
235+
finally:
236+
self.loop_depth -= 1
214237

215238
# ifStmt → "if" "(" expression ")" statement
216239
# ( "else" statement )? ;

pylox/scanner.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ def is_alnum(char: str) -> bool:
4242
"true": TokenType.TRUE,
4343
"var": TokenType.VAR,
4444
"while": TokenType.WHILE,
45+
"break": TokenType.BREAK,
4546
}
4647

4748

pylox/stmt.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,10 @@ def visit_var_stmt(self, stmt) -> typing.Any:
3232
def visit_while_stmt(self, stmt) -> typing.Any:
3333
pass
3434

35+
@abstractmethod
36+
def visit_break_stmt(self, stmt) -> typing.Any:
37+
pass
38+
3539

3640
class Stmt:
3741
def __init__(self):
@@ -103,3 +107,11 @@ def __init__(self, condition: Expr, body: Stmt):
103107

104108
def accept(self, visitor: StmtVisitor) -> typing.Any:
105109
return visitor.visit_while_stmt(self)
110+
111+
112+
class Break(Stmt):
113+
def __init__(self):
114+
super().__init__()
115+
116+
def accept(self, visitor: StmtVisitor) -> typing.Any:
117+
return visitor.visit_break_stmt(self)

pylox/tokens.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ class TokenType(Enum):
5050
TRUE = auto()
5151
VAR = auto()
5252
WHILE = auto()
53+
BREAK = auto()
5354

5455
EOF = auto()
5556

tests/data/bool/equality.lox

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,3 +21,5 @@ print false != 0; // expect: true
2121
print true != "true"; // expect: true
2222
print false != "false"; // expect: true
2323
print false != ""; // expect: true
24+
25+
print 42.0 == 42; // expect: true

tests/data/custom/loop.lox

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,17 +3,24 @@ var i = 0;
33

44
while (i < 10) {
55
i = i + 1;
6-
}
76

8-
print i; // expect: 10
7+
if (i == 7) {
8+
break;
9+
}
10+
}
911

12+
print i; // expect: 7
1013

1114
var a = 0;
1215
var temp;
1316

1417
for (var b = 1; a < 10000; b = temp + b) {
1518
temp = a;
1619
a = b;
20+
21+
while (true) {
22+
break;
23+
}
1724
}
1825

1926
print a; // expect: 10946

tests/test_parser.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,3 +83,16 @@ def handler(err: LoxParseError):
8383
# THEN
8484
assert "Expect ')' after expression." in err.value.message
8585
assert cnt == 2 # both errors have been handled
86+
87+
88+
def test_if_parser_handles_break_outside_of_the_loop():
89+
# GIVEN
90+
src = "break;"
91+
92+
def handler(err: LoxParseError):
93+
# THEN
94+
assert "Must be inside a loop to use 'break'." in err.message
95+
96+
# WHEN
97+
tokens = Scanner(src).scan_tokens()
98+
Parser(tokens, handler).parse()

tools/generate_ast.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,8 @@ def define_visitor(file, base_name, types):
9393
"If": ('condition : Expr', 'then_branch : Stmt', 'else_branch : typing.Optional[Stmt]'),
9494
"Print": 'expr : Expr',
9595
"Var": ('name : Token', "initializer : typing.Optional[Expr]"),
96-
"While": ('condition : Expr', 'body : Stmt')
96+
"While": ('condition : Expr', 'body : Stmt'),
97+
"Break": ()
9798
}
9899

99100
define_ast("Expr", expressions)

0 commit comments

Comments
 (0)