Repository: vyperlang/vyper

Issue:
CompilerPanic on invalid range
### Version Information

* vyper Version (output of `vyper --version`): 0.1.0b13+commit.da6d2a3
* OS: osx
* Python Version (output of `python --version`): Python 3.7.4
* Environment (output of `pip freeze`):
apipkg==1.5
asttokens==1.1.13
atomicwrites==1.3.0
attrdict==2.0.1
attrs==19.3.0
base58==1.0.3
certifi==2019.9.11
cffi==1.13.1
chardet==3.0.4
coverage==4.5.4
coveralls==1.6.0
cryptography==2.8
cytoolz==0.10.0
docopt==0.6.2
entrypoints==0.3
eth-abi==2.0.0b9
eth-account==0.4.0
eth-bloom==1.0.3
eth-hash==0.2.0
eth-keyfile==0.5.1
eth-keys==0.3.1
eth-rlp==0.1.2
eth-tester==0.1.0b39
eth-typing==2.1.0
eth-utils==1.7.0
ethpm==0.1.4a19
execnet==1.7.1
filelock==3.0.12
flake8==3.7.8
flake8-bugbear==18.8.0
hexbytes==0.2.0
hypothesis==4.11.7
idna==2.8
importlib-metadata==0.23
ipfshttpclient==0.4.12
isort==4.3.21
jsonschema==2.6.0
lru-dict==1.1.6
mccabe==0.6.1
more-itertools==7.2.0
multiaddr==0.0.8
mypy==0.701
mypy-extensions==0.4.3
netaddr==0.7.19
packaging==19.2
parsimonious==0.8.1
pluggy==0.13.0
protobuf==3.10.0
py==1.8.0
py-ecc==1.7.1
py-evm==0.2.0a42
pycodestyle==2.5.0
pycparser==2.19
pycryptodome==3.9.0
pyethash==0.1.27
pyflakes==2.1.1
pyparsing==2.4.2
pytest==5.2.1
pytest-cov==2.4.0
pytest-xdist==1.18.1
PyYAML==5.1.2
requests==2.22.0
rlp==1.1.0
semantic-version==2.8.2
six==1.12.0
toml==0.10.0
toolz==0.10.0
tox==3.14.0
trie==1.4.0
typed-ast==1.3.5
urllib3==1.25.6
varint==1.0.2
virtualenv==16.7.7
vyper==0.1.0b13
wcwidth==0.1.7
web3==5.0.0b2
websockets==7.0
zipp==0.6.0


### What's your issue about?

A rather simple file produces a compiler panic, when a simple compile error seems more reasonable.

```
@public
def f0():
    for v0 in range(ZERO_WEI):
        continue
```

Produces:
```
$ vyper v.vy 
Error compiling: v.vy
vyper.exceptions.CompilerPanic: Number of times repeated must be a constant nonzero positive integer: 0 Please create an issue.
```

Generated using TSTL and https://github.com/agroce/tstl/tree/master/examples/vyper

### How can it be fixed?

Fill this in if you know how to fix it.

Can't loop over a nested array
`for` loops in Vyper can loop over arrays, but not if they're nested. This code fails to compile with the surprising error `'Subscript' object has no attribute 'func'`:

```python
@public
def __init__():
    x: uint256[5][2] = [[0, 1, 2, 3, 4], [2, 4, 6, 8, 10]]
    for i in x[1]:
        pass
```


Discussion:
Cool, thanks for the find! Also, thanks for the very interesting tool!
Sure!  I'll keep adding language elements, and report anything else interesting that comes up.
Seems weird, buy maybe we should allow. Should also check to see if it affects arrays in structs
Meeting Notes: This should be allowed _eventually_ but currently should throw a cleaner error.

Resolving patch:
diff --git a/vyper/parser/stmt.py b/vyper/parser/stmt.py
--- a/vyper/parser/stmt.py
+++ b/vyper/parser/stmt.py
@@ -504,40 +504,42 @@ def _check_valid_range_constant(self, arg_ast_node, raise_exception=True):
             arg_expr = Expr.parse_value_expr(arg_ast_node, self.context)
 
         is_integer_literal = (
-            isinstance(arg_expr.typ, BaseType) and arg_expr.typ.is_literal
-        ) and arg_expr.typ.typ in {'uint256', 'int128'}
-
-        if is_integer_literal:
-            return True, arg_expr
-        else:
-            if raise_exception:
-                raise StructureException("Range only accepts literal (constant) values", arg_expr)
-            return False, arg_expr
+            isinstance(arg_expr.typ, BaseType) and
+            arg_expr.typ.is_literal and
+            arg_expr.typ.typ in {'uint256', 'int128'}
+        )
+        if not is_integer_literal and raise_exception:
+            raise StructureException(
+                "Range only accepts literal (constant) values of type uint256 or int128",
+                arg_ast_node
+            )
+        return is_integer_literal, arg_expr
 
     def _get_range_const_value(self, arg_ast_node):
         _, arg_expr = self._check_valid_range_constant(arg_ast_node)
         return arg_expr.value
 
     def parse_for(self):
-        # from .parser import (
-        #     parse_body,
-        # )
         # Type 0 for, e.g. for i in list(): ...
         if self._is_list_iter():
             return self.parse_for_list()
 
-        is_invalid_for_statement = any((
-            not isinstance(self.stmt.iter, ast.Call),
-            not isinstance(self.stmt.iter.func, ast.Name),
-            not isinstance(self.stmt.target, ast.Name),
-            self.stmt.iter.func.id != "range",
-            len(self.stmt.iter.args) not in {1, 2},
-        ))
-        if is_invalid_for_statement:
-            raise StructureException((
-                "For statements must be of the form `for i in range(rounds): "
-                "..` or `for i in range(start, start + rounds): ..`"
-            ), self.stmt.iter)
+        if not isinstance(self.stmt.iter, ast.Call):
+            if isinstance(self.stmt.iter, ast.Subscript):
+                raise StructureException("Cannot iterate over a nested list", self.stmt.iter)
+            raise StructureException(
+                f"Cannot iterate over '{type(self.stmt.iter).__name__}' object",
+                self.stmt.iter
+            )
+        if getattr(self.stmt.iter.func, 'id', None) != "range":
+            raise StructureException(
+                "Non-literals cannot be used as loop range", self.stmt.iter.func
+            )
+        if len(self.stmt.iter.args) not in {1, 2}:
+            raise StructureException(
+                f"Range expects between 1 and 2 arguments, got {len(self.stmt.iter.args)}",
+                self.stmt.iter.func
+            )
 
         block_scope_id = id(self.stmt)
         with self.context.make_blockscope(block_scope_id):
@@ -583,6 +585,14 @@ def parse_for(self):
                 rounds = self._get_range_const_value(arg1.right)
                 start = Expr.parse_value_expr(arg0, self.context)
 
+            r = rounds if isinstance(rounds, int) else rounds.value
+            if r < 1:
+                raise StructureException(
+                    f"For loop has invalid number of iterations ({r}),"
+                    " the value must be greater than zero",
+                    self.stmt.iter
+                )
+
             varname = self.stmt.target.id
             pos = self.context.new_variable(varname, BaseType('int128'), pos=getpos(self.stmt))
             self.context.forvars[varname] = True


Test patch:
diff --git a/tests/parser/features/iteration/test_for_in_list.py b/tests/parser/features/iteration/test_for_in_list.py
--- a/tests/parser/features/iteration/test_for_in_list.py
+++ b/tests/parser/features/iteration/test_for_in_list.py
@@ -2,41 +2,54 @@
     Decimal,
 )
 
+import pytest
+
 from vyper.exceptions import (
     StructureException,
     VariableDeclarationException,
 )
 
-
-def test_basic_for_in_list(get_contract_with_gas_estimation):
-    code = """
+BASIC_FOR_LOOP_CODE = [
+    # basic for-in-list memory
+    ("""
 @public
 def data() -> int128:
     s: int128[5] = [1, 2, 3, 4, 5]
     for i in s:
         if i >= 3:
             return i
-    return -1
-    """
-
-    c = get_contract_with_gas_estimation(code)
-
-    assert c.data() == 3
-
-
-def test_basic_for_list_liter(get_contract_with_gas_estimation):
-    code = """
+    return -1""", 3),
+    # basic for-in-list literal
+    ("""
 @public
 def data() -> int128:
     for i in [3, 5, 7, 9]:
         if i > 5:
             return i
-    return -1
-    """
+    return -1""", 7),
+    # basic for-in-list addresses
+    ("""
+@public
+def data() -> address:
+    addresses: address[3] = [
+        0x7d577a597B2742b498Cb5Cf0C26cDCD726d39E6e,
+        0x82A978B3f5962A5b0957d9ee9eEf472EE55B42F1,
+        0xDCEceAF3fc5C0a63d195d69b1A90011B7B19650D
+    ]
+    count: int128 = 0
+    for i in addresses:
+        count += 1
+        if count == 2:
+            return i
+    return 0x0000000000000000000000000000000000000000
+    """, "0x82A978B3f5962A5b0957d9ee9eEf472EE55B42F1"),
+]
 
-    c = get_contract_with_gas_estimation(code)
 
-    assert c.data() == 7
+@pytest.mark.parametrize('code, data', BASIC_FOR_LOOP_CODE)
+def test_basic_for_in_lists(code, data, get_contract):
+    c = get_contract(code)
+    assert c.data() == data
 
 
 def test_basic_for_list_storage(get_contract_with_gas_estimation):
@@ -62,91 +75,6 @@ def data() -> int128:
     assert c.data() == 7
 
 
-def test_basic_for_list_address(get_contract_with_gas_estimation):
-    code = """
-@public
-def data() -> address:
-    addresses: address[3] = [
-        0x7d577a597B2742b498Cb5Cf0C26cDCD726d39E6e,
-        0x82A978B3f5962A5b0957d9ee9eEf472EE55B42F1,
-        0xDCEceAF3fc5C0a63d195d69b1A90011B7B19650D
-    ]
-    count: int128 = 0
-    for i in addresses:
-        count += 1
-        if count == 2:
-            return i
-    return 0x0000000000000000000000000000000000000000
-    """
-
-    c = get_contract_with_gas_estimation(code)
-
-    assert c.data() == "0x82A978B3f5962A5b0957d9ee9eEf472EE55B42F1"
-
-
-def test_multiple_for_loops_1(get_contract_with_gas_estimation):
-    code = """
-@public
-def foo(x: int128):
-    p: int128 = 0
-    for i in range(3):
-        p += i
-    for i in range(4):
-        p += i
-"""
-    get_contract_with_gas_estimation(code)
-
-
-def test_multiple_for_loops_2(get_contract_with_gas_estimation):
-    code = """
-@public
-def foo(x: int128):
-    p: int128 = 0
-    for i in range(3):
-        p += i
-    for i in [1, 2, 3, 4]:
-        p += i
-"""
-    get_contract_with_gas_estimation(code)
-
-
-def test_multiple_for_loops_3(get_contract_with_gas_estimation):
-    code = """
-@public
-def foo(x: int128):
-    p: int128 = 0
-    for i in [1, 2, 3, 4]:
-        p += i
-    for i in [1, 2, 3, 4]:
-        p += i
-"""
-    get_contract_with_gas_estimation(code)
-
-
-def test_multiple_loops_4(get_contract_with_gas_estimation):
-    code = """
-@public
-def foo():
-    for i in range(10):
-        pass
-    for i in range(20):
-        pass
-"""
-    get_contract_with_gas_estimation(code)
-
-
-def test_using_index_variable_after_loop(get_contract_with_gas_estimation):
-    code = """
-@public
-def foo():
-    for i in range(10):
-        pass
-    i: int128 = 100  # create new variable i
-    i = 200  # look up the variable i and check whether it is in forvars
-"""
-    get_contract_with_gas_estimation(code)
-
-
 def test_basic_for_list_storage_address(get_contract_with_gas_estimation):
     code = """
 addresses: address[3]
@@ -211,9 +139,130 @@ def i_return(break_count: int128) -> decimal:
     assert c.ret(0) == c.i_return(0) == Decimal('0.0001')
 
 
-def test_altering_list_within_for_loop_1(assert_compile_failed, get_contract_with_gas_estimation):
+def test_for_in_list_iter_type(get_contract_with_gas_estimation):
     code = """
 @public
+@constant
+def func(amounts: wei_value[3]) -> wei_value:
+    total: wei_value = as_wei_value(0, "wei")
+
+    # calculate total
+    for amount in amounts:
+        total += amount
+
+    return total
+    """
+
+    c = get_contract_with_gas_estimation(code)
+
+    assert c.func([100, 200, 300]) == 600
+
+
+GOOD_CODE = [
+    # multiple for loops
+    """
+@public
+def foo(x: int128):
+    p: int128 = 0
+    for i in range(3):
+        p += i
+    for i in range(4):
+        p += i
+    """,
+    """
+@public
+def foo(x: int128):
+    p: int128 = 0
+    for i in range(3):
+        p += i
+    for i in [1, 2, 3, 4]:
+        p += i
+    """,
+    """
+@public
+def foo(x: int128):
+    p: int128 = 0
+    for i in [1, 2, 3, 4]:
+        p += i
+    for i in [1, 2, 3, 4]:
+        p += i
+    """,
+    """
+@public
+def foo():
+    for i in range(10):
+        pass
+    for i in range(20):
+        pass
+    """,
+    # using index variable after loop
+    """
+@public
+def foo():
+    for i in range(10):
+        pass
+    i: int128 = 100  # create new variable i
+    i = 200  # look up the variable i and check whether it is in forvars
+    """,
+]
+
+
+@pytest.mark.parametrize('code', GOOD_CODE)
+def test_good_code(code, get_contract):
+    get_contract(code)
+
+
+RANGE_CONSTANT_CODE = [
+    ("""
+TREE_FIDDY: constant(int128)  = 350
+
+
+@public
+def a() -> uint256:
+    x: uint256 = 0
+    for i in range(TREE_FIDDY):
+        x += 1
+    return x""", 350),
+    ("""
+ONE_HUNDRED: constant(int128)  = 100
+
+@public
+def a() -> uint256:
+    x: uint256 = 0
+    for i in range(1, 1 + ONE_HUNDRED):
+        x += 1
+    return x""", 100),
+    ("""
+START: constant(int128)  = 100
+END: constant(int128)  = 199
+
+@public
+def a() -> uint256:
+    x: uint256 = 0
+    for i in range(START, END):
+        x += 1
+    return x""", 99),
+    ("""
+@public
+def a() -> int128:
+    x: int128 = 0
+    for i in range(-5, -1):
+        x += i
+    return x""", -14)
+]
+
+
+@pytest.mark.parametrize('code, result', RANGE_CONSTANT_CODE)
+def test_range_constant(get_contract, code, result):
+    c = get_contract(code)
+
+    assert c.a() == result
+
+
+BAD_CODE = [
+    # altering list within loop
+    """
+@public
 def data() -> int128:
     s: int128[6] = [1, 2, 3, 4, 5, 6]
     count: int128 = 0
@@ -223,26 +272,17 @@ def data() -> int128:
             return i
         count += 1
     return -1
+    """,
     """
-
-    assert_compile_failed(lambda: get_contract_with_gas_estimation(code), StructureException)
-
-
-def test_altering_list_within_for_loop_2(assert_compile_failed, get_contract_with_gas_estimation):
-    code = """
 @public
 def foo():
     s: int128[6] = [1, 2, 3, 4, 5, 6]
     count: int128 = 0
     for i in s:
         s[count] += 1
-"""
-    assert_compile_failed(lambda: get_contract_with_gas_estimation(code), StructureException)
-
-
-def test_altering_list_within_for_loop_storage(assert_compile_failed,
-                                               get_contract_with_gas_estimation):
-    code = """
+    """,
+    # alter storage list within for loop
+    """
 s: int128[6]
 
 @public
@@ -258,120 +298,133 @@ def data() -> int128:
             return i
         count += 1
     return -1
-    """
-
-    assert_compile_failed(lambda: get_contract_with_gas_estimation(code), StructureException)
-
-
-def test_invalid_nested_for_loop_1(assert_compile_failed, get_contract_with_gas_estimation):
-    code = """
+    """,
+    # invalid nested loop
+    ("""
 @public
 def foo(x: int128):
     for i in range(4):
         for i in range(5):
             pass
-"""
-    assert_compile_failed(
-        lambda: get_contract_with_gas_estimation(code), VariableDeclarationException
-    )
-
-
-def test_invalid_nested_for_loop_2(assert_compile_failed, get_contract_with_gas_estimation):
-    code = """
+    """, VariableDeclarationException),
+    ("""
 @public
 def foo(x: int128):
     for i in [1,2]:
         for i in [1,2]:
             pass
-"""
-    assert_compile_failed(
-        lambda: get_contract_with_gas_estimation(code), VariableDeclarationException,
-    )
-
-
-def test_invalid_iterator_assignment_1(assert_compile_failed, get_contract_with_gas_estimation):
-    code = """
+     """, VariableDeclarationException),
+    # invalid iterator assignment
+    """
 @public
 def foo(x: int128):
     for i in [1,2]:
         i = 2
-"""
-    assert_compile_failed(lambda: get_contract_with_gas_estimation(code), StructureException)
-
-
-def test_invalid_iterator_assignment_2(assert_compile_failed, get_contract_with_gas_estimation):
-    code = """
+    """,
+    """
 @public
 def foo(x: int128):
     for i in [1,2]:
         i += 2
-"""
-    assert_compile_failed(lambda: get_contract_with_gas_estimation(code), StructureException)
-
-
-def test_range_constant(get_contract_with_gas_estimation):
-    code = """
-TREE_FIDDY: constant(int128)  = 350
-
-
+    """,
+    # range of < 1
+    """
 @public
-def a() -> uint256:
-    x: uint256 = 0
-    for i in range(TREE_FIDDY):
-        x += 1
-    return x
+def foo():
+    for i in range(-3):
+        pass
+    """,
     """
-
-    c = get_contract_with_gas_estimation(code)
-
-    assert c.a() == 350
-
-    code = """
-ONE_HUNDRED: constant(int128)  = 100
-
 @public
-def a() -> uint256:
-    x: uint256 = 0
-    for i in range(1, 1 + ONE_HUNDRED):
-        x += 1
-    return x
+def foo():
+    for i in range(0):
+        pass
+    """,
     """
-
-    c = get_contract_with_gas_estimation(code)
-
-    assert c.a() == 100
-
-    code = """
-START: constant(int128)  = 100
-END: constant(int128)  = 199
-
 @public
-def a() -> uint256:
-    x: uint256 = 0
-    for i in range(START, END):
-        x += 1
-    return x
+def foo():
+    for i in range(5,3):
+        pass
+    """,
     """
-
-    c = get_contract_with_gas_estimation(code)
-
-    assert c.a() == 99
-
-
-def test_for_in_list_iter_type(get_contract_with_gas_estimation):
-    code = """
 @public
-@constant
-def func(amounts: wei_value[3]) -> wei_value:
-    total: wei_value = as_wei_value(0, "wei")
-
-    # calculate total
-    for amount in amounts:
-        total += amount
-
-    return total
+def foo():
+    for i in range(5,3,-1):
+        pass
+    """,
+    """
+@public
+def foo():
+    a: uint256 = 2
+    for i in range(a):
+        pass
+    """,
     """
+@public
+def foo():
+    a: int128 = 6
+    for i in range(a,a-3):
+        pass
+    """,
+    # invalid argument length
+    """
+@public
+def foo():
+    for i in range():
+        pass
+    """,
+    """
+@public
+def foo():
+    for i in range(0,1,2):
+        pass
+    """,
+    # non-iterables
+    """
+@public
+def foo():
+    for i in b"asdf":
+        pass
+    """,
+    """
+@public
+def foo():
+    for i in 31337:
+        pass
+    """,
+    """
+@public
+def foo():
+    for i in bar():
+        pass
+    """,
+    """
+@public
+def foo():
+    for i in self.bar():
+        pass
+    """,
+    # nested lists
+    """
+@public
+def foo():
+    x: uint256[5][2] = [[0, 1, 2, 3, 4], [2, 4, 6, 8, 10]]
+    for i in x:
+        pass
+    """,
+    """
+@public
+def foo():
+    x: uint256[5][2] = [[0, 1, 2, 3, 4], [2, 4, 6, 8, 10]]
+    for i in x[1]:
+        pass
+    """
+]
 
-    c = get_contract_with_gas_estimation(code)
 
-    assert c.func([100, 200, 300]) == 600
+@pytest.mark.parametrize('code', BAD_CODE)
+def test_bad_code(assert_compile_failed, get_contract, code):
+    err = StructureException
+    if not isinstance(code, str):
+        code, err = code
+    assert_compile_failed(lambda: get_contract(code), err)