Skip to content

Commit

Permalink
Implement the break statement (#3320)
Browse files Browse the repository at this point in the history
* Implement the 'break' statement

* Track whether we are within a loop or not
  • Loading branch information
peterebden authored Dec 19, 2024
1 parent 1b2a187 commit 24b4a3d
Show file tree
Hide file tree
Showing 8 changed files with 64 additions and 7 deletions.
2 changes: 1 addition & 1 deletion docs/grammar.txt
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
file_input = { statement };

# Any single statement. Must occur on its own line.
statement = ( "pass" | "continue" | func_def | for | if | return |
statement = ( "pass" | "continue" | "break" | func_def | for | if | return |
assert | ident_statement | expression ) EOL;
return = "return" [ expression { "," expression } ];
assert = "assert" expression [ "," expression ];
Expand Down
1 change: 1 addition & 0 deletions src/parse/asp/grammar.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ type Statement struct {
Literal *Expression
Pass bool
Continue bool
Break bool
}

// An AssertStatement implements the 'assert' statement.
Expand Down
13 changes: 13 additions & 0 deletions src/parse/asp/grammar_parse.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ var keywords = map[string]struct{}{
type parser struct {
l *lex
endPos Position
inFor bool
}

// parseFileInput is the only external entry point to this class, it parses a file into a FileInput structure.
Expand Down Expand Up @@ -153,13 +154,25 @@ func (p *parser) parseStatement() *Statement {
p.endPos = p.l.Next().EndPos()
p.next(EOL)
case "continue":
p.assert(p.inFor, tok, "'continue' outside loop")
s.Continue = true
p.endPos = p.l.Next().EndPos()
p.next(EOL)
case "break":
p.assert(p.inFor, tok, "'break' outside loop")
s.Break = true
p.endPos = p.l.Next().EndPos()
p.next(EOL)
case "def":
before := p.inFor
p.inFor = false
s.FuncDef = p.parseFuncDef()
p.inFor = before
case "for":
before := p.inFor
p.inFor = true
s.For = p.parseFor()
p.inFor = before
case "if":
s.If = p.parseIf()
case "return":
Expand Down
8 changes: 7 additions & 1 deletion src/parse/asp/interpreter.go
Original file line number Diff line number Diff line change
Expand Up @@ -555,6 +555,9 @@ func (s *scope) interpretStatements(statements []*Statement) pyObject {
} else if stmt.Continue {
// This is definitely awkward since we need to control a for loop that's happening in a function outside this scope.
return continueIteration
} else if stmt.Break {
// Similar to above, although CPython does do this the same way...
return stopIteration
} else if stmt.Pass {
continue // Nothing to do...
} else {
Expand All @@ -580,8 +583,11 @@ func (s *scope) interpretFor(stmt *ForStatement) pyObject {
for li := range s.iterable(&stmt.Expr) {
s.unpackNames(stmt.Names, li)
if ret := s.interpretStatements(stmt.Statements); ret != nil {
if s, ok := ret.(pySentinel); ok && s == continueIteration {
switch ret {
case continueIteration:
continue
case stopIteration:
break
}
return ret
}
Expand Down
6 changes: 6 additions & 0 deletions src/parse/asp/interpreter_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -709,3 +709,9 @@ func TestListConcatenation(t *testing.T) {
pyString("haribo"),
}, s.Lookup("fruit_veg_canned_food_and_sweets"))
}

func TestBreakLoop(t *testing.T) {
s, err := parseFile("src/parse/asp/test_data/interpreter/break_loop.build")
assert.NoError(t, err)
assert.EqualValues(t, 1, s.Lookup("i"))
}
13 changes: 8 additions & 5 deletions src/parse/asp/objects.go
Original file line number Diff line number Diff line change
Expand Up @@ -134,13 +134,16 @@ func (n pyNone) MarshalJSON() ([]byte, error) {

// A pySentinel is an internal implementation detail used in some cases. It should never be
// exposed to users.
type pySentinel struct{}
type pySentinel string

// continueIteration is used to implement the "continue" statement.
var continueIteration = pySentinel{}
var continueIteration = pySentinel("ContinueIteration")

// stopIteration is used to implement the "break" statement.
var stopIteration = pySentinel("StopIteration")

func (s pySentinel) Type() string {
return "sentinel"
return string(s)
}

func (s pySentinel) TypeTag() int32 {
Expand All @@ -152,11 +155,11 @@ func (s pySentinel) IsTruthy() bool {
}

func (s pySentinel) String() string {
panic("non stringable type sentinel")
panic("non stringable type " + string(s))
}

func (s pySentinel) MarshalJSON() ([]byte, error) {
panic("non serialisable type sentinel")
panic("non serialisable type " + string(s))
}

type pyInt int
Expand Down
26 changes: 26 additions & 0 deletions src/parse/asp/parser_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -831,3 +831,29 @@ func TestFStringIncompleteError(t *testing.T) {
require.Error(t, err)
assert.Contains(t, err.Error(), "Unterminated brace in fstring")
}

// Continue shouldn't be allowed outside a loop
func TestContinueOutsideLoop(t *testing.T) {
_, err := newParser().parseAndHandleErrors(strings.NewReader("continue"))
require.Error(t, err)
assert.Contains(t, err.Error(), "'continue' outside loop")
}

// Break shouldn't be allowed outside a loop
func TestBreakOutsideLoop(t *testing.T) {
_, err := newParser().parseAndHandleErrors(strings.NewReader("break"))
require.Error(t, err)
assert.Contains(t, err.Error(), "'break' outside loop")
}

// Functions have a new scope that doesn't count as within the enclosing loop
func TestBreakWithinFunctionWithinLoop(t *testing.T) {
const code = `
for i in [1,2,3]:
def foo():
break
`
_, err := newParser().parseAndHandleErrors(strings.NewReader(code))
require.Error(t, err)
assert.Contains(t, err.Error(), "'break' outside loop")
}
2 changes: 2 additions & 0 deletions src/parse/asp/test_data/interpreter/break_loop.build
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
for i in range(1, 3):
break

0 comments on commit 24b4a3d

Please sign in to comment.