Repository: google/starlark-go (go)

Issue:
`break` inside a nested function results in a panic
I found a code snippet that crashes the interpreter.

```starlark
def fun():
  for a in [1, 2, 3]:
    def nested():
      break

fun()
```

```
﻿﻿panic: runtime error: index out of range [-1]

goroutine 1 [running]:
go.starlark.net/internal/compile.(*fcomp).stmt(0x1400002efd8, {0x102c655e8?, 0x14000010620})
	/Users/Bartek.Pacia/.cache/go/pkg/mod/go.starlark.net@v0.0.0-20250225190231-0d3f41d403af/internal/compile/compile.go:1066 +0xfd4
go.starlark.net/internal/compile.(*fcomp).stmts(...)
	/Users/Bartek.Pacia/.cache/go/pkg/mod/go.starlark.net@v0.0.0-20250225190231-0d3f41d403af/internal/compile/compile.go:1046
go.starlark.net/internal/compile.(*pcomp).function(0x140000106a0, {0x1400000e200, 0x6}, {0x1400002a2b0?, 0x2f1b8?, 0x140?}, {0x1400002a2d0, 0x1, 0x2000b682e?}, {0x0, ...}, ...)
	/Users/Bartek.Pacia/.cache/go/pkg/mod/go.starlark.net@v0.0.0-20250225190231-0d3f41d403af/internal/compile/compile.go:541 +0x440
go.starlark.net/internal/compile.(*fcomp).function(0x1400002f588, 0x140000c6090)
	/Users/Bartek.Pacia/.cache/go/pkg/mod/go.starlark.net@v0.0.0-20250225190231-0d3f41d403af/internal/compile/compile.go:1852 +0x2c0
go.starlark.net/internal/compile.(*fcomp).stmt(0x1400002f588, {0x102c65690?, 0x14000036080})
	/Users/Bartek.Pacia/.cache/go/pkg/mod/go.starlark.net@v0.0.0-20250225190231-0d3f41d403af/internal/compile/compile.go:1170 +0x198
go.starlark.net/internal/compile.(*fcomp).stmts(...)
	/Users/Bartek.Pacia/.cache/go/pkg/mod/go.starlark.net@v0.0.0-20250225190231-0d3f41d403af/internal/compile/compile.go:1046
go.starlark.net/internal/compile.(*fcomp).stmt(0x1400002f588, {0x102c656c8?, 0x140000ba050})
	/Users/Bartek.Pacia/.cache/go/pkg/mod/go.starlark.net@v0.0.0-20250225190231-0d3f41d403af/internal/compile/compile.go:1190 +0xcb0
go.starlark.net/internal/compile.(*fcomp).stmts(...)
	/Users/Bartek.Pacia/.cache/go/pkg/mod/go.starlark.net@v0.0.0-20250225190231-0d3f41d403af/internal/compile/compile.go:1046
go.starlark.net/internal/compile.(*pcomp).function(0x140000106a0, {0x1400000e1f0, 0x3}, {0x1400002a2b0?, 0x2f668?, 0x140?}, {0x1400002a2f0, 0x1, 0x22?}, {0x1400002a330, ...}, ...)
	/Users/Bartek.Pacia/.cache/go/pkg/mod/go.starlark.net@v0.0.0-20250225190231-0d3f41d403af/internal/compile/compile.go:541 +0x440
go.starlark.net/internal/compile.(*fcomp).function(0x1400002f998, 0x140000c6000)
	/Users/Bartek.Pacia/.cache/go/pkg/mod/go.starlark.net@v0.0.0-20250225190231-0d3f41d403af/internal/compile/compile.go:1852 +0x2c0
go.starlark.net/internal/compile.(*fcomp).stmt(0x1400002f998, {0x102c65690?, 0x14000036100})
	/Users/Bartek.Pacia/.cache/go/pkg/mod/go.starlark.net@v0.0.0-20250225190231-0d3f41d403af/internal/compile/compile.go:1170 +0x198
go.starlark.net/internal/compile.(*fcomp).stmts(...)
	/Users/Bartek.Pacia/.cache/go/pkg/mod/go.starlark.net@v0.0.0-20250225190231-0d3f41d403af/internal/compile/compile.go:1046
go.starlark.net/internal/compile.(*pcomp).function(0x140000106a0, {0x102bc1543, 0xa}, {0x1400002a2b0?, 0x2fa88?, 0x140?}, {0x14000010640, 0x2, 0x0?}, {0x0, ...}, ...)
	/Users/Bartek.Pacia/.cache/go/pkg/mod/go.starlark.net@v0.0.0-20250225190231-0d3f41d403af/internal/compile/compile.go:541 +0x440
go.starlark.net/internal/compile.File(0x1400000e1c6, {0x14000010640, 0x2, 0x2}, {0x1400002a2b0?, 0x2c350a0?, 0x1?}, {0x102bc1543, 0xa}, {0x0, ...}, ...)
	/Users/Bartek.Pacia/.cache/go/pkg/mod/go.starlark.net@v0.0.0-20250225190231-0d3f41d403af/internal/compile/compile.go:508 +0x220
go.starlark.net/starlark.FileProgram(0x140000ba0f0, 0x1400002a2a0)
	/Users/Bartek.Pacia/.cache/go/pkg/mod/go.starlark.net@v0.0.0-20250225190231-0d3f41d403af/starlark/eval.go:415 +0x138
go.starlark.net/starlark.SourceProgramOptions(0x14000090c28?, {0x16d3d2841?, 0x14000090c18?}, {0x0?, 0x0?}, 0x1400002a2a0)
	/Users/Bartek.Pacia/.cache/go/pkg/mod/go.starlark.net@v0.0.0-20250225190231-0d3f41d403af/starlark/eval.go:387 +0x5c
go.starlark.net/starlark.ExecFileOptions(0x1400000e1c6, 0x140000b00e0, {0x16d3d2841, 0xa}, {0x0, 0x0}, 0x0)
	/Users/Bartek.Pacia/.cache/go/pkg/mod/go.starlark.net@v0.0.0-20250225190231-0d3f41d403af/starlark/eval.go:355 +0x90
go.starlark.net/starlark.ExecFile(0x140000b00e0, {0x16d3d2841, 0xa}, {0x0, 0x0}, 0x0)
	/Users/Bartek.Pacia/.cache/go/pkg/mod/go.starlark.net@v0.0.0-20250225190231-0d3f41d403af/starlark/eval.go:333 +0xa0
main.doMain()
	/Users/Bartek.Pacia/.cache/go/pkg/mod/go.starlark.net@v0.0.0-20250225190231-0d3f41d403af/cmd/starlark/starlark.go:118 +0x7ec
main.main()
	/Users/Bartek.Pacia/.cache/go/pkg/mod/go.starlark.net@v0.0.0-20250225190231-0d3f41d403af/cmd/starlark/starlark.go:51 +0x1c
```


---

This correctly prints "continue not in a loop" static error:

```starlark
def fun():
  def nested():
    continue

fun()
```

Pull request description:
[resolve] detect invalid break/continue stmts if inside nested function
fix #598

Patch:
diff --git a/resolve/resolve.go b/resolve/resolve.go
index 494811d..e00d2b3 100644
--- a/resolve/resolve.go
+++ b/resolve/resolve.go
@@ -230,7 +230,7 @@ type resolver struct {
 	// isGlobal may be nil.
 	isGlobal, isPredeclared, isUniversal func(name string) bool
 
-	loops   int // number of enclosing for/while loops
+	loops   int // number of enclosing for/while loops in current function (or file, if top-level)
 	ifstmts int // number of enclosing if statements loops
 
 	errors ErrorList
@@ -822,6 +822,10 @@ func (r *resolver) function(function *Function, pos syntax.Position) {
 	b := &block{function: function}
 	r.push(b)
 
+	// Save the current loop count and reset it for the new function
+	outerLoops := r.loops
+	r.loops = 0
+
 	var seenOptional bool
 	var star *syntax.UnaryExpr // * or *args param
 	var starStar *syntax.Ident // **kwargs ident
@@ -905,6 +909,9 @@ func (r *resolver) function(function *Function, pos syntax.Position) {
 	// Leave function block.
 	r.pop()
 
+	// Restore the outer loop count
+	r.loops = outerLoops
+
 	// References within the function body to globals are not
 	// resolved until the end of the module.
 }


Test patch:
diff --git a/resolve/testdata/resolve.star b/resolve/testdata/resolve.star
index cb6385a..55612c8 100644
--- a/resolve/testdata/resolve.star
+++ b/resolve/testdata/resolve.star
@@ -227,6 +227,18 @@ continue ### "continue not in a loop"
 
 pass
 
+---
+# Test for bug #598: break statement inside nested function within a loop
+def fun1():
+  for a in [1, 2, 3]:
+    def nested():
+      break ### "break not in a loop"
+
+def fun2():
+  for a in [1, 2, 3]:
+    def nested():
+      continue ### "continue not in a loop"
+
 ---
 # Positional arguments (and required parameters)
 # must appear before named arguments (and optional parameters).