Repository: vyperlang/vyper

Issue:
Only allow one function call in assignment expressions
### What's your issue about?

An expression should only allow constant expression to be used.

```python
x: decimal

@constant
@private
def a () -> num:
    return 1

@private
def b () -> num:
    self.x = 333.33
    return 2

@public
def test():
    a: num = 3 * self.a()  # allowed
    b: num = self.a() or 4 * self.b()  # not allowed 
```

### How can it be fixed?

Let's discuss any downsides, and then implement it.


#### Cute Animal Picture

![](http://www.dogster.com/wp-content/uploads/2017/03/shutterstock_puppy_paws_thumb.jpeg)


Discussion:
I don't think there's necessarily a problem having one state changing function call another, so I choose to interpret this as one state changing function calling another that returns stuff.

I think in general, we should move to discouraging returns from state changing functions in order to stop design patterns from developing around this poor practice (in my opinion). This looks like a great corner case for that paradigm, so I agree with this issue given that context.

I sort of picture "returns" for a state changing function (from a usage perspective) being receipts, which match the asynchronous nature of transactions in Ethereum.

Resolving patch:
diff --git a/vyper/parser/expr.py b/vyper/parser/expr.py
--- a/vyper/parser/expr.py
+++ b/vyper/parser/expr.py
@@ -415,6 +415,9 @@ def compare(self):
     def boolean_operations(self):
         if len(self.expr.values) != 2:
             raise StructureException("Expected two arguments for a bool op", self.expr)
+        if self.context.in_assignment and (isinstance(self.expr.values[0], ast.Call) or isinstance(self.expr.values[1], ast.Call)):
+            raise StructureException("Boolean operations with calls may not be performed on assignment", self.expr)
+
         left = Expr.parse_value_expr(self.expr.values[0], self.context)
         right = Expr.parse_value_expr(self.expr.values[1], self.context)
         if not is_base_type(left.typ, 'bool') or not is_base_type(right.typ, 'bool'):
@@ -468,7 +471,8 @@ def call(self):
             sig = self.context.sigs['self'][method_name]
             if self.context.is_constant and not sig.const:
                 raise ConstancyViolationException(
-                    "May not call non-constant function '%s' within a constant function." % (method_name)
+                    "May not call non-constant function '%s' within a constant function." % (method_name),
+                    getpos(self.expr)
                 )
             add_gas = self.context.sigs['self'][method_name].gas  # gas of call
             inargs, inargsize = pack_arguments(sig, [Expr(arg, self.context).lll_node for arg in self.expr.args], self.context)
diff --git a/vyper/parser/parser.py b/vyper/parser/parser.py
--- a/vyper/parser/parser.py
+++ b/vyper/parser/parser.py
@@ -282,6 +282,11 @@ def __init__(self, vars=None, globals=None, sigs=None, forvars=None, return_type
         self.function_return_count = 0
         # Current block scope
         self.blockscopes = set()
+        # In assignment. Whether expressiong is currently evaluating an assignment expression.
+        self.in_assignment = False
+
+    def set_in_assignment(self, state: bool):
+        self.in_assignment = state
 
     def set_in_for_loop(self, name_of_list):
         self.in_for_loop.add(name_of_list)
diff --git a/vyper/parser/stmt.py b/vyper/parser/stmt.py
--- a/vyper/parser/stmt.py
+++ b/vyper/parser/stmt.py
@@ -91,6 +91,7 @@ def ann_assign(self):
         from .parser import (
             make_setter,
         )
+        self.context.set_in_assignment(True)
         typ = parse_type(self.stmt.annotation, location='memory')
         if isinstance(self.stmt.target, ast.Attribute) and self.stmt.target.value.id == 'self':
             raise TypeMismatchException('May not redefine storage variables.', self.stmt)
@@ -102,6 +103,7 @@ def ann_assign(self):
             self._check_valid_assign(sub)
             variable_loc = LLLnode.from_list(pos, typ=sub.typ, location='memory', pos=getpos(self.stmt))
             o = make_setter(variable_loc, sub, 'memory', pos=getpos(self.stmt))
+        self.context.set_in_assignment(False)
         return o
 
     def assign(self):
@@ -111,6 +113,7 @@ def assign(self):
         # Assignment (eg. x[4] = y)
         if len(self.stmt.targets) != 1:
             raise StructureException("Assignment statement must have one target", self.stmt)
+        self.context.set_in_assignment(True)
         sub = Expr(self.stmt.value, self.context).lll_node
         # Determine if it's an RLPList assignment.
         if isinstance(self.stmt.value, ast.Call) and getattr(self.stmt.value.func, 'id', '') is 'RLPList':
@@ -125,6 +128,7 @@ def assign(self):
             target = self.get_target(self.stmt.targets[0])
             o = make_setter(target, sub, target.location, pos=getpos(self.stmt))
         o.pos = getpos(self.stmt)
+        self.context.set_in_assignment(False)
         return o
 
     def parse_if(self):


Test patch:
diff --git a/tests/parser/syntax/test_ann_assign.py b/tests/parser/syntax/test_ann_assign.py
--- a/tests/parser/syntax/test_ann_assign.py
+++ b/tests/parser/syntax/test_ann_assign.py
@@ -2,7 +2,11 @@
 from pytest import raises
 
 from vyper import compiler
-from vyper.exceptions import VariableDeclarationException, TypeMismatchException
+from vyper.exceptions import (
+    VariableDeclarationException,
+    TypeMismatchException,
+    StructureException
+)
 
 
 fail_list = [
@@ -25,7 +29,25 @@ def test():
 @public
 def test():
     a: int128 = 33.33
-    """, TypeMismatchException)
+    """, TypeMismatchException),
+    ("""
+@private
+def do_stuff() -> bool:
+    return True
+
+@public
+def test():
+    a: bool = self.do_stuff() or self.do_stuff()
+    """, StructureException),
+    ("""
+@private
+def do_stuff() -> bool:
+    return True
+
+@public
+def test():
+    a: bool = False or self.do_stuff()
+    """, StructureException)
 ]
 
 
@@ -45,6 +67,15 @@ def test_as_wei_fail(bad_code):
 def test():
     a: int128 = 1
     """,
+    """
+@private
+def do_stuff() -> bool:
+    return True
+
+@public
+def test():
+    a: bool = self.do_stuff()
+    """
 ]