Repository: vyperlang/vyper

Issue:
VIP: Implement short circuiting
## Simple Summary
Add short circuiting logic to binary operations.

## Motivation
1. This is pretty standard behavior in most programming languages - I believe the majority of users will assume that it already happens and write their code accordingly.
2. By not doing it, we make transactions less efficient through unnecessary evaluation.
3. We introduce a risk for exploits due to unexpected behavior:

```python
@private
def a() -> bool:
    return True

@private
def b() -> bool:
    steal_the_money()
    return True

@public
def foo():
    if a() or b():
        # something mostly harmless
```

## Specification
Treat `and` and `or` as short-circuit operators: evaluate them left to right, and stop evaluation as soon as an outcome is determined.

## Backwards Compatibility
This will alter behavior in contracts, but should not introduce any breaking changes.

## Copyright
Copyright and related rights waived via [CC0](https://creativecommons.org/publicdomain/zero/1.0/)



Discussion:
In hindsight, I don't think this needs to be on the v0.2.0 milestone

Resolving patch:
diff --git a/vyper/parser/expr.py b/vyper/parser/expr.py
--- a/vyper/parser/expr.py
+++ b/vyper/parser/expr.py
@@ -651,39 +651,59 @@ def load_bytearray(side):
             return LLLnode.from_list([op, left, right], typ="bool", pos=getpos(self.expr))
 
     def parse_BoolOp(self):
-        # Iterate through values
         for value in self.expr.values:
             # Check for boolean operations with non-boolean inputs
             _expr = Expr.parse_value_expr(value, self.context)
             if not is_base_type(_expr.typ, "bool"):
                 return
 
-        # Check for valid ops
+        def _build_if_lll(condition, true, false):
+            # generate a basic if statement in LLL
+            o = ["if", condition, true, false]
+            return o
+
+        jump_label = f"_boolop_{self.expr.src}"
         if isinstance(self.expr.op, vy_ast.And):
-            op = "and"
+            if len(self.expr.values) == 2:
+                # `x and y` is a special case, it doesn't require jumping
+                lll_node = _build_if_lll(
+                    Expr.parse_value_expr(self.expr.values[0], self.context),
+                    Expr.parse_value_expr(self.expr.values[1], self.context),
+                    [0],
+                )
+                return LLLnode.from_list(lll_node, typ="bool")
+
+            # create the initial `x and y` from the final two values
+            lll_node = _build_if_lll(
+                Expr.parse_value_expr(self.expr.values[-2], self.context),
+                Expr.parse_value_expr(self.expr.values[-1], self.context),
+                [0],
+            )
+            # iterate backward through the remaining values
+            for node in self.expr.values[-3::-1]:
+                lll_node = _build_if_lll(
+                    Expr.parse_value_expr(node, self.context), lll_node, [0, ["goto", jump_label]]
+                )
+
         elif isinstance(self.expr.op, vy_ast.Or):
-            op = "or"
-        else:
-            return
+            # create the initial `x or y` from the final two values
+            lll_node = _build_if_lll(
+                Expr.parse_value_expr(self.expr.values[-2], self.context),
+                [1, ["goto", jump_label]],
+                Expr.parse_value_expr(self.expr.values[-1], self.context),
+            )
 
-        # Handle different numbers of inputs
-        count = len(self.expr.values)
-        if count == 2:
-            left = Expr.parse_value_expr(self.expr.values[0], self.context)
-            right = Expr.parse_value_expr(self.expr.values[1], self.context)
-            return LLLnode.from_list([op, left, right], typ="bool", pos=getpos(self.expr))
+            # iterate backward through the remaining values
+            for node in self.expr.values[-3::-1]:
+                lll_node = _build_if_lll(
+                    Expr.parse_value_expr(node, self.context), [1, ["goto", jump_label]], lll_node,
+                )
         else:
-            left = Expr.parse_value_expr(self.expr.values[0], self.context)
-            right = Expr.parse_value_expr(self.expr.values[1], self.context)
-
-            p = ["seq", [op, left, right]]
-            values = self.expr.values[2:]
-            while len(values) > 0:
-                value = Expr.parse_value_expr(values[0], self.context)
-                p = [op, value, p]
-                values = values[1:]
+            raise TypeCheckFailure(f"Unexpected boolean operator: {type(self.expr.op).__name__}")
 
-            return LLLnode.from_list(p, typ="bool", pos=getpos(self.expr))
+        # add `jump_label` at the end of the boolop
+        lll_node = ["seq_unchecked", lll_node, ["label", jump_label]]
+        return LLLnode.from_list(lll_node, typ="bool")
 
     # Unary operations (only "not" supported)
     def parse_UnaryOp(self):


Test patch:
diff --git a/tests/parser/features/test_short_circuiting.py b/tests/parser/features/test_short_circuiting.py
new file mode 100644
--- /dev/null
+++ b/tests/parser/features/test_short_circuiting.py
@@ -0,0 +1,175 @@
+import itertools
+
+import pytest
+
+
+def test_short_circuit_and_left_is_false(w3, get_contract):
+    code = """
+
+called_left: public(bool)
+called_right: public(bool)
+
+@private
+def left() -> bool:
+    self.called_left = True
+    return False
+
+@private
+def right() -> bool:
+    self.called_right = True
+    return False
+
+@public
+def foo() -> bool:
+    return self.left() and self.right()
+"""
+    c = get_contract(code)
+    assert not c.foo()
+
+    c.foo(transact={})
+    assert c.called_left()
+    assert not c.called_right()
+
+
+def test_short_circuit_and_left_is_true(w3, get_contract):
+    code = """
+
+called_left: public(bool)
+called_right: public(bool)
+
+@private
+def left() -> bool:
+    self.called_left = True
+    return True
+
+@private
+def right() -> bool:
+    self.called_right = True
+    return True
+
+@public
+def foo() -> bool:
+    return self.left() and self.right()
+"""
+    c = get_contract(code)
+    assert c.foo()
+
+    c.foo(transact={})
+    assert c.called_left()
+    assert c.called_right()
+
+
+def test_short_circuit_or_left_is_true(w3, get_contract):
+    code = """
+
+called_left: public(bool)
+called_right: public(bool)
+
+@private
+def left() -> bool:
+    self.called_left = True
+    return True
+
+@private
+def right() -> bool:
+    self.called_right = True
+    return True
+
+@public
+def foo() -> bool:
+    return self.left() or self.right()
+"""
+    c = get_contract(code)
+    assert c.foo()
+
+    c.foo(transact={})
+    assert c.called_left()
+    assert not c.called_right()
+
+
+def test_short_circuit_or_left_is_false(w3, get_contract):
+    code = """
+
+called_left: public(bool)
+called_right: public(bool)
+
+@private
+def left() -> bool:
+    self.called_left = True
+    return False
+
+@private
+def right() -> bool:
+    self.called_right = True
+    return False
+
+@public
+def foo() -> bool:
+    return self.left() or self.right()
+"""
+    c = get_contract(code)
+    assert not c.foo()
+
+    c.foo(transact={})
+    assert c.called_left()
+    assert c.called_right()
+
+
+@pytest.mark.parametrize("op", ["and", "or"])
+@pytest.mark.parametrize("a, b", itertools.product([True, False], repeat=2))
+def test_from_memory(w3, get_contract, a, b, op):
+    code = f"""
+@public
+def foo(a: bool, b: bool) -> bool:
+    c: bool = a
+    d: bool = b
+    return c {op} d
+"""
+    c = get_contract(code)
+    assert c.foo(a, b) == eval(f"{a} {op} {b}")
+
+
+@pytest.mark.parametrize("op", ["and", "or"])
+@pytest.mark.parametrize("a, b", itertools.product([True, False], repeat=2))
+def test_from_storage(w3, get_contract, a, b, op):
+    code = f"""
+c: bool
+d: bool
+
+@public
+def foo(a: bool, b: bool) -> bool:
+    self.c = a
+    self.d = b
+    return self.c {op} self.d
+"""
+    c = get_contract(code)
+    assert c.foo(a, b) == eval(f"{a} {op} {b}")
+
+
+@pytest.mark.parametrize("op", ["and", "or"])
+@pytest.mark.parametrize("a, b", itertools.product([True, False], repeat=2))
+def test_from_calldata(w3, get_contract, a, b, op):
+    code = f"""
+@public
+def foo(a: bool, b: bool) -> bool:
+    return a {op} b
+"""
+    c = get_contract(code)
+    assert c.foo(a, b) == eval(f"{a} {op} {b}")
+
+
+@pytest.mark.parametrize("a, b, c, d", itertools.product([True, False], repeat=4))
+@pytest.mark.parametrize("ops", itertools.product(["and", "or"], repeat=3))
+def test_complex_combination(w3, get_contract, a, b, c, d, ops):
+    boolop = f"a {ops[0]} b {ops[1]} c {ops[2]} d"
+
+    code = f"""
+@public
+def foo(a: bool, b: bool, c: bool, d: bool) -> bool:
+    return {boolop}
+"""
+    contract = get_contract(code)
+    if eval(boolop):
+        assert contract.foo(a, b, c, d)
+    else:
+        assert not contract.foo(a, b, c, d)