Repository: vyperlang/vyper

Issue:
Decimals can't be the base for exponentiation
This might be an intentional restriction, but the error messages are confusing:

```python
@public
def test(x: decimal) -> decimal:
    return x**convert(2, decimal) # Error: Only whole number exponents are supported
    return x**2 # Error: Cannot implicitly convert decimal to int128.
```


Discussion:
It is a restriction due to how we handle math for exponents, but agree the error message could also be better

Resolving patch:
diff --git a/vyper/exceptions.py b/vyper/exceptions.py
--- a/vyper/exceptions.py
+++ b/vyper/exceptions.py
@@ -109,6 +109,10 @@ class ArrayIndexException(ParserException):
     pass
 
 
+class ZeroDivisionException(ParserException):
+    pass
+
+
 class EvmVersionException(ParserException):
     pass
 
diff --git a/vyper/parser/expr.py b/vyper/parser/expr.py
--- a/vyper/parser/expr.py
+++ b/vyper/parser/expr.py
@@ -9,6 +9,7 @@
     StructureException,
     TypeMismatchException,
     VariableDeclarationException,
+    ZeroDivisionException,
 )
 from vyper.opcodes import (
     version_check,
@@ -463,15 +464,24 @@ def arithmetic(self):
                 val = left.value - right.value
             elif isinstance(self.expr.op, ast.Mult):
                 val = left.value * right.value
-            elif isinstance(self.expr.op, ast.Div):
-                val = left.value // right.value
-            elif isinstance(self.expr.op, ast.Mod):
-                val = left.value % right.value
             elif isinstance(self.expr.op, ast.Pow):
                 val = left.value ** right.value
+            elif isinstance(self.expr.op, (ast.Div, ast.Mod)):
+                if right.value == 0:
+                    raise ZeroDivisionException(
+                        "integer division or modulo by zero",
+                        self.expr,
+                    )
+                if isinstance(self.expr.op, ast.Div):
+                    val = left.value // right.value
+                elif isinstance(self.expr.op, ast.Mod):
+                    # modified modulo logic to remain consistent with EVM behaviour
+                    val = abs(left.value) % abs(right.value)
+                    if left.value < 0:
+                        val = -val
             else:
                 raise ParserException(
-                    f'Unsupported literal operator: {str(type(self.expr.op))}',
+                    f'Unsupported literal operator: {type(self.expr.op)}',
                     self.expr,
                 )
 
@@ -504,6 +514,12 @@ def arithmetic(self):
                     pos=pos,
                 )
 
+        if left.typ.typ == "decimal" and isinstance(self.expr.op, ast.Pow):
+            raise TypeMismatchException(
+                "Cannot perform exponentiation on decimal values.",
+                self.expr,
+            )
+
         # Only allow explicit conversions to occur.
         if left.typ.typ != right.typ.typ:
             raise TypeMismatchException(
@@ -582,6 +598,8 @@ def arithmetic(self):
                 raise Exception(f"Unsupported Operation 'mul({ltyp}, {rtyp})'")
 
         elif isinstance(self.expr.op, ast.Div):
+            if right.typ.is_literal and right.value == 0:
+                raise ZeroDivisionException("Cannot divide by 0.", self.expr)
             if left.typ.positional or right.typ.positional:
                 raise TypeMismatchException("Cannot divide positional values!", self.expr)
             new_unit = combine_units(left.typ.unit, right.typ.unit, div=True)
@@ -602,6 +620,8 @@ def arithmetic(self):
                 raise Exception(f"Unsupported Operation 'div({ltyp}, {rtyp})'")
 
         elif isinstance(self.expr.op, ast.Mod):
+            if right.typ.is_literal and right.value == 0:
+                raise ZeroDivisionException("Cannot calculate modulus of 0.", self.expr)
             if left.typ.positional or right.typ.positional:
                 raise TypeMismatchException(
                     "Cannot use positional values as modulus arguments!",


Test patch:
diff --git a/tests/parser/features/arithmetic/test_division.py b/tests/parser/features/arithmetic/test_division.py
new file mode 100644
--- /dev/null
+++ b/tests/parser/features/arithmetic/test_division.py
@@ -0,0 +1,43 @@
+import pytest
+
+from vyper.exceptions import (
+    ZeroDivisionException,
+)
+
+BAD_CODE = [
+    """
+@public
+def foo() -> uint256:
+    return 2 / 0
+    """,
+    """
+@public
+def foo() -> int128:
+    return -2 / 0
+    """,
+    """
+@public
+def foo() -> decimal:
+    return 2.22 / 0.0
+    """,
+    """
+@public
+def foo(a: uint256) -> uint256:
+    return a / 0
+    """,
+    """
+@public
+def foo(a: int128) -> int128:
+    return a / 0
+    """,
+    """
+@public
+def foo(a: decimal) -> decimal:
+    return a / 0.0
+    """
+]
+
+
+@pytest.mark.parametrize("code", BAD_CODE)
+def test_divide_by_zero(code, assert_compile_failed, get_contract):
+    assert_compile_failed(lambda: get_contract(code), ZeroDivisionException)
diff --git a/tests/parser/features/arithmetic/test_modulo.py b/tests/parser/features/arithmetic/test_modulo.py
--- a/tests/parser/features/arithmetic/test_modulo.py
+++ b/tests/parser/features/arithmetic/test_modulo.py
@@ -2,8 +2,11 @@
     Decimal,
 )
 
+import pytest
+
 from vyper.exceptions import (
     TypeMismatchException,
+    ZeroDivisionException,
 )
 
 
@@ -62,3 +65,61 @@ def foo(a: decimal, b: decimal) -> decimal:
 """
     c = get_contract_with_gas_estimation(code)
     assert_tx_failed(lambda: c.foo(Decimal('1'), Decimal('0')))
+
+
+def test_literals_vs_evm(get_contract):
+    code = """
+@public
+@constant
+def foo() -> (int128, int128, int128, int128):
+    return 5%2, 5%-2, -5%2, -5%-2
+
+@public
+@constant
+def bar(a: int128) -> bool:
+    assert -5%2 == a%2
+    return True
+"""
+
+    c = get_contract(code)
+    assert c.foo() == [1, 1, -1, -1]
+    assert c.bar(-5) is True
+
+
+BAD_CODE = [
+    """
+@public
+def foo() -> uint256:
+    return 2 % 0
+    """,
+    """
+@public
+def foo() -> int128:
+    return -2 % 0
+    """,
+    """
+@public
+def foo() -> decimal:
+    return 2.22 % 0.0
+    """,
+    """
+@public
+def foo(a: uint256) -> uint256:
+    return a % 0
+    """,
+    """
+@public
+def foo(a: int128) -> int128:
+    return a % 0
+    """,
+    """
+@public
+def foo(a: decimal) -> decimal:
+    return a % 0.0
+    """
+]
+
+
+@pytest.mark.parametrize("code", BAD_CODE)
+def test_modulo_by_zero(code, assert_compile_failed, get_contract):
+    assert_compile_failed(lambda: get_contract(code), ZeroDivisionException)
diff --git a/tests/parser/types/numbers/test_decimals.py b/tests/parser/types/numbers/test_decimals.py
--- a/tests/parser/types/numbers/test_decimals.py
+++ b/tests/parser/types/numbers/test_decimals.py
@@ -3,6 +3,10 @@
     getcontext,
 )
 
+from vyper.exceptions import (
+    TypeMismatchException,
+)
+
 getcontext().prec = 78  # MAX_UINT256 < 1e78
 
 
@@ -161,3 +165,13 @@ def bar(num: decimal) -> decimal:
 
     assert c.foo() == Decimal('1e-10')  # Smallest possible decimal
     assert c.bar(Decimal('1e37')) == Decimal('-9e37')  # Math lines up
+
+
+def test_exponents(assert_compile_failed, get_contract):
+    code = """
+@public
+def foo() -> decimal:
+    return 2.2 ** 2.0
+    """
+
+    assert_compile_failed(lambda: get_contract(code), TypeMismatchException)