Repository: vyperlang/vyper

Issue:
`Break` outside of loop leads to Compiler Panic
### Version Information

* vyper Version (output of `vyper --version`): 0.3.8+commit.046ea166
* OS: OSX
* Python Version (output of `python --version`): 3.8.0

### What's your issue about?

Having a `break` statement outside of a loop leads the compiler to panicking while the same situation with a `continue` statement would raise a ``StructureException: `continue` must be enclosed in a `for` loop``
```Python
@external
def test():
    break
```
```
Error compiling: tests/customs/code.vy
Traceback (most recent call last):
  File "/Users/trocher/Documents/thesis/vyper/venv/bin/vyper", line 11, in <module>
    load_entry_point('vyper==0.3.8', 'console_scripts', 'vyper')()
  File "/Users/trocher/Documents/thesis/vyper/vyper/cli/vyper_compile.py", line 57, in _parse_cli_args
    return _parse_args(sys.argv[1:])
  File "/Users/trocher/Documents/thesis/vyper/vyper/cli/vyper_compile.py", line 154, in _parse_args
    compiled = compile_files(
  File "/Users/trocher/Documents/thesis/vyper/vyper/cli/vyper_compile.py", line 294, in compile_files
    compiler_data = vyper.compile_codes(
  File "/Users/trocher/Documents/thesis/vyper/vyper/evm/opcodes.py", line 226, in _wrapper
    return fn(*args, **kwargs)
  File "/Users/trocher/Documents/thesis/vyper/vyper/compiler/__init__.py", line 141, in compile_codes
    exc_handler(contract_name, exc)
  File "/Users/trocher/Documents/thesis/vyper/vyper/cli/vyper_compile.py", line 189, in exc_handler
    raise exception
  File "/Users/trocher/Documents/thesis/vyper/vyper/compiler/__init__.py", line 138, in compile_codes
    out[contract_name][output_format] = OUTPUT_FORMATS[output_format](compiler_data)
  File "/Users/trocher/Documents/thesis/vyper/vyper/compiler/output.py", line 248, in build_bytecode_output
    return f"0x{compiler_data.bytecode.hex()}"
  File "/Library/Frameworks/Python.framework/Versions/3.8/lib/python3.8/functools.py", line 966, in __get__
    val = self.func(instance)
  File "/Users/trocher/Documents/thesis/vyper/vyper/compiler/phases.py", line 150, in bytecode
    self.assembly, is_runtime=False, no_bytecode_metadata=self.no_bytecode_metadata
  File "/Library/Frameworks/Python.framework/Versions/3.8/lib/python3.8/functools.py", line 966, in __get__
    val = self.func(instance)
  File "/Users/trocher/Documents/thesis/vyper/vyper/compiler/phases.py", line 141, in assembly
    return generate_assembly(self.ir_nodes, self.no_optimize)
  File "/Users/trocher/Documents/thesis/vyper/vyper/compiler/phases.py", line 300, in generate_assembly
    assembly = compile_ir.compile_to_assembly(ir_nodes, no_optimize=no_optimize)
  File "/Users/trocher/Documents/thesis/vyper/vyper/ir/compile_ir.py", line 189, in apply_line_no_wrapper
    ret = func(*args, **kwargs)
  File "/Users/trocher/Documents/thesis/vyper/vyper/ir/compile_ir.py", line 211, in compile_to_assembly
    res = _compile_to_assembly(code)
  File "/Users/trocher/Documents/thesis/vyper/vyper/ir/compile_ir.py", line 189, in apply_line_no_wrapper
    ret = func(*args, **kwargs)
  File "/Users/trocher/Documents/thesis/vyper/vyper/ir/compile_ir.py", line 502, in _compile_to_assembly
    subcode = _compile_to_assembly(ir)
  File "/Users/trocher/Documents/thesis/vyper/vyper/ir/compile_ir.py", line 189, in apply_line_no_wrapper
    ret = func(*args, **kwargs)
  File "/Users/trocher/Documents/thesis/vyper/vyper/ir/compile_ir.py", line 535, in _compile_to_assembly
    o.extend(_compile_to_assembly(arg, withargs, existing_labels, break_dest, height))
  File "/Users/trocher/Documents/thesis/vyper/vyper/ir/compile_ir.py", line 189, in apply_line_no_wrapper
    ret = func(*args, **kwargs)
  File "/Users/trocher/Documents/thesis/vyper/vyper/ir/compile_ir.py", line 480, in _compile_to_assembly
    _compile_to_assembly(code.args[2], withargs, existing_labels, break_dest, height + 1)
  File "/Users/trocher/Documents/thesis/vyper/vyper/ir/compile_ir.py", line 189, in apply_line_no_wrapper
    ret = func(*args, **kwargs)
  File "/Users/trocher/Documents/thesis/vyper/vyper/ir/compile_ir.py", line 535, in _compile_to_assembly
    o.extend(_compile_to_assembly(arg, withargs, existing_labels, break_dest, height))
  File "/Users/trocher/Documents/thesis/vyper/vyper/ir/compile_ir.py", line 189, in apply_line_no_wrapper
    ret = func(*args, **kwargs)
  File "/Users/trocher/Documents/thesis/vyper/vyper/ir/compile_ir.py", line 342, in _compile_to_assembly
    o.extend(_compile_to_assembly(code.args[1], withargs, existing_labels, break_dest, height))
  File "/Users/trocher/Documents/thesis/vyper/vyper/ir/compile_ir.py", line 189, in apply_line_no_wrapper
    ret = func(*args, **kwargs)
  File "/Users/trocher/Documents/thesis/vyper/vyper/ir/compile_ir.py", line 535, in _compile_to_assembly
    o.extend(_compile_to_assembly(arg, withargs, existing_labels, break_dest, height))
  File "/Users/trocher/Documents/thesis/vyper/vyper/ir/compile_ir.py", line 189, in apply_line_no_wrapper
    ret = func(*args, **kwargs)
  File "/Users/trocher/Documents/thesis/vyper/vyper/ir/compile_ir.py", line 535, in _compile_to_assembly
    o.extend(_compile_to_assembly(arg, withargs, existing_labels, break_dest, height))
  File "/Users/trocher/Documents/thesis/vyper/vyper/ir/compile_ir.py", line 189, in apply_line_no_wrapper
    ret = func(*args, **kwargs)
  File "/Users/trocher/Documents/thesis/vyper/vyper/ir/compile_ir.py", line 705, in _compile_to_assembly
    body_asm = _compile_to_assembly(
  File "/Users/trocher/Documents/thesis/vyper/vyper/ir/compile_ir.py", line 189, in apply_line_no_wrapper
    ret = func(*args, **kwargs)
  File "/Users/trocher/Documents/thesis/vyper/vyper/ir/compile_ir.py", line 535, in _compile_to_assembly
    o.extend(_compile_to_assembly(arg, withargs, existing_labels, break_dest, height))
  File "/Users/trocher/Documents/thesis/vyper/vyper/ir/compile_ir.py", line 189, in apply_line_no_wrapper
    ret = func(*args, **kwargs)
  File "/Users/trocher/Documents/thesis/vyper/vyper/ir/compile_ir.py", line 454, in _compile_to_assembly
    raise CompilerPanic("Invalid break")
vyper.exceptions.CompilerPanic: Invalid break

This is an unhandled internal compiler error. Please create an issue on Github to notify the developers.
https://github.com/vyperlang/vyper/issues/new?template=bug.md
```

### How can it be fixed?

`vy_ast.Break` should not be a ignored type of the `FunctionNodeVisitor` and rather a function very similar to `visit_Continue` should ensure that `break` statements are enclosed in loops.
https://github.com/vyperlang/vyper/blob/ac2fa147fdfed8590a1eee1d27ceb1b26d6a62b2/vyper/semantics/analysis/local.py#L287-L290


Discussion:

Resolving patch:
diff --git a/vyper/semantics/analysis/local.py b/vyper/semantics/analysis/local.py
--- a/vyper/semantics/analysis/local.py
+++ b/vyper/semantics/analysis/local.py
@@ -160,7 +160,7 @@ def _validate_msg_data_attribute(node: vy_ast.Attribute) -> None:
 
 class FunctionNodeVisitor(VyperNodeVisitorBase):
 
-    ignored_types = (vy_ast.Break, vy_ast.Constant, vy_ast.Pass)
+    ignored_types = (vy_ast.Constant, vy_ast.Pass)
     scope_name = "function"
 
     def __init__(
@@ -289,6 +289,11 @@ def visit_Continue(self, node):
         if for_node is None:
             raise StructureException("`continue` must be enclosed in a `for` loop", node)
 
+    def visit_Break(self, node):
+        for_node = node.get_ancestor(vy_ast.For)
+        if for_node is None:
+            raise StructureException("`break` must be enclosed in a `for` loop", node)
+
     def visit_Return(self, node):
         values = node.value
         if values is None:


Test patch:
diff --git a/tests/parser/features/iteration/test_break.py b/tests/parser/features/iteration/test_break.py
--- a/tests/parser/features/iteration/test_break.py
+++ b/tests/parser/features/iteration/test_break.py
@@ -1,5 +1,9 @@
 from decimal import Decimal
 
+import pytest
+
+from vyper.exceptions import StructureException
+
 
 def test_break_test(get_contract_with_gas_estimation):
     break_test = """
@@ -79,3 +83,41 @@ def foo(n: int128) -> int128:
     assert c.foo(200) == 23
     assert c.foo(4000000) == 66
     print("Passed aug-assignment break composite test")
+
+
+fail_list = [
+    (
+        """
+@external
+def foo():
+    a: uint256 = 3
+    break
+    """,
+        StructureException,
+    ),
+    (
+        """
+@external
+def foo():
+    if True:
+        break
+    """,
+        StructureException,
+    ),
+    (
+        """
+@external
+def foo():
+    for i in [1, 2, 3]:
+        b: uint256 = i
+    if True:
+        break
+    """,
+        StructureException,
+    ),
+]
+
+
+@pytest.mark.parametrize("bad_code,exc", fail_list)
+def test_block_fail(assert_compile_failed, get_contract_with_gas_estimation, bad_code, exc):
+    assert_compile_failed(lambda: get_contract_with_gas_estimation(bad_code), exc)
diff --git a/tests/parser/features/iteration/test_continue.py b/tests/parser/features/iteration/test_continue.py
--- a/tests/parser/features/iteration/test_continue.py
+++ b/tests/parser/features/iteration/test_continue.py
@@ -1,5 +1,7 @@
 import pytest
 
+from vyper.exceptions import StructureException
+
 
 def test_continue1(get_contract_with_gas_estimation):
     code = """
@@ -59,19 +61,26 @@ def foo() -> int128:
 
 
 fail_list = [
-    """
+    (
+        """
 @external
 def foo():
     a: uint256 = 3
     continue
     """,
-    """
+        StructureException,
+    ),
+    (
+        """
 @external
 def foo():
     if True:
         continue
     """,
-    """
+        StructureException,
+    ),
+    (
+        """
 @external
 def foo():
     for i in [1, 2, 3]:
@@ -79,9 +88,11 @@ def foo():
     if True:
         continue
     """,
+        StructureException,
+    ),
 ]
 
 
-@pytest.mark.parametrize("bad_code", fail_list)
-def test_block_fail(assert_compile_failed, get_contract_with_gas_estimation, bad_code):
-    assert_compile_failed(lambda: get_contract_with_gas_estimation(bad_code))
+@pytest.mark.parametrize("bad_code,exc", fail_list)
+def test_block_fail(assert_compile_failed, get_contract_with_gas_estimation, bad_code, exc):
+    assert_compile_failed(lambda: get_contract_with_gas_estimation(bad_code), exc)