Repository: vyperlang/vyper

Issue:
Impure range expressions
### Version Information

* vyper Version: 0.1.0b9
* Tested with [Remix IDE](https://remix.ethereum.org/)

Currently Vyper allows range expressions of the form `range(x, x + n)` where `n` is a constant. A weird case happens when `x` is impure:

```
glob: int128

@public
def foo() -> int128:
	self.glob += 1
	return 5


@public
def bar():
	for i in range(self.foo(), self.foo() + 1):
		pass
```

Even though the range expression contains two calls to `self.foo()` it is only executed once, which feels very unintuitive at best. In general, it would be nice to have some documentation for loops/range expressions (especially if this is the intended behavior).



Discussion:
I agree, I don't think this should be allowed at all, ie. functions called in range() have to be @constant.

Resolving patch:
diff --git a/vyper/parser/context.py b/vyper/parser/context.py
--- a/vyper/parser/context.py
+++ b/vyper/parser/context.py
@@ -50,6 +50,8 @@ def __init__(self,
         self.constancy = constancy
         # Whether body is currently in an assert statement
         self.in_assertion = False
+        # Whether we are currently parsing a range expression
+        self.in_range_expr = False
         # Is the function payable?
         self.is_payable = is_payable
         # Number of placeholders generated (used to generate random names)
@@ -79,7 +81,9 @@ def __init__(self,
         self.global_ctx = global_ctx
 
     def is_constant(self):
-        return self.constancy is Constancy.Constant or self.in_assertion
+        return self.constancy is Constancy.Constant or \
+                self.in_assertion or \
+                self.in_range_expr
 
     #
     # Context Managers
@@ -104,6 +108,13 @@ def assertion_scope(self):
         yield
         self.in_assertion = prev_value
 
+    @contextlib.contextmanager
+    def range_scope(self):
+        prev_value = self.in_range_expr
+        self.in_range_expr = True
+        yield
+        self.in_range_expr = prev_value
+
     @contextlib.contextmanager
     def make_blockscope(self, blockscope_id):
         self.blockscopes.add(blockscope_id)
@@ -161,6 +172,8 @@ def parse_type(self, ast_node, location):
     def pp_constancy(self):
         if self.in_assertion:
             return 'an assertion'
+        elif self.in_range_expr:
+            return 'a range expression'
         elif self.constancy == Constancy.Constant:
             return 'a constant function'
         raise ValueError('Compiler error: unknown constancy in pp_constancy: %r' % self.constancy)
diff --git a/vyper/parser/stmt.py b/vyper/parser/stmt.py
--- a/vyper/parser/stmt.py
+++ b/vyper/parser/stmt.py
@@ -481,7 +481,10 @@ def parse_assert(self):
             return LLLnode.from_list(['assert', test_expr], typ=None, pos=getpos(self.stmt))
 
     def _check_valid_range_constant(self, arg_ast_node, raise_exception=True):
-        arg_expr = Expr.parse_value_expr(arg_ast_node, self.context)
+        with self.context.range_scope():
+            # TODO should catch if raise_exception == False?
+            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'}
@@ -601,7 +604,8 @@ def _is_list_iter(self):
         return False
 
     def parse_for_list(self):
-        iter_list_node = Expr(self.stmt.iter, self.context).lll_node
+        with self.context.range_scope():
+            iter_list_node = Expr(self.stmt.iter, self.context).lll_node
         if not isinstance(iter_list_node.typ.subtype, BaseType):  # Sanity check on list subtype.
             raise StructureException('For loops allowed only on basetype lists.', self.stmt.iter)
         iter_var_type = (


Test patch:
diff --git a/tests/parser/exceptions/test_constancy_exception.py b/tests/parser/exceptions/test_constancy_exception.py
--- a/tests/parser/exceptions/test_constancy_exception.py
+++ b/tests/parser/exceptions/test_constancy_exception.py
@@ -84,6 +84,62 @@ def a (x:int128):
 @public
 def b():
     self.a(10)
+    """,
+    # test constancy in range expressions
+    """
+glob: int128
+@public
+def foo() -> int128:
+    self.glob += 1
+    return 5
+@public
+def bar():
+    for i in range(self.foo(), self.foo() + 1):
+        pass
+    """,
+    """
+glob: int128
+@public
+def foo() -> int128:
+    self.glob += 1
+    return 5
+@public
+def bar():
+    for i in range(self.foo()):
+        pass
+    """,
+    """
+glob: int128
+@public
+def foo() -> int128:
+    self.glob += 1
+    return 5
+@public
+def bar():
+    for i in [1,2,3,4,self.foo()]:
+        pass
+    """,
+    """
+glob: int128
+@public
+def foo() -> int128:
+    self.glob += 1
+    return 5
+@public
+def bar():
+    for i in range(self.foo(), 7):
+        pass
+    """,
+    """
+glob: int128
+@public
+def foo() -> int128:
+    self.glob += 1
+    return 5
+@public
+def bar():
+    for i in range(3, self.foo()):
+        pass
     """
 ]