Repository: vyperlang/vyper

Issue:
Python level vs. Vyper-level Type Errors for Improper Uses of decimal
### Version Information

* vyper Version (output of `vyper --version`): 0.2.8+commit.069936f
* OS: osx
* Python Version (output of `python --version`):  Python 3.7.7
* Environment (output of `pip freeze`):  
apipkg==1.5
appnope==0.1.0
asttokens==2.0.3
atomicwrites==1.3.0
attrdict==2.0.1
attrs==19.3.0
backcall==0.1.0
base58==1.0.3
certifi==2019.9.11
cffi==1.13.1
chardet==3.0.4
coverage==4.5.2
coveralls==1.6.0
cryptography==2.8
cycler==0.10.0
cytoolz==0.10.0
decorator==4.4.1
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
ipython==7.9.0
ipython-genutils==0.2.0
isort==4.3.21
jedi==0.15.1
jsonschema==2.6.0
jupyter-core==4.6.1
kiwisolver==1.1.0
lru-dict==1.1.6
matplotlib==3.1.2
mccabe==0.6.1
more-itertools==7.2.0
multiaddr==0.0.8
mypy==0.701
mypy-extensions==0.4.3
nbformat==4.4.0
netaddr==0.7.19
numpy==1.17.3
packaging==19.2
parsimonious==0.8.1
parso==0.5.1
pexpect==4.7.0
pickleshare==0.7.5
pluggy==0.13.0
prompt-toolkit==2.0.10
protobuf==3.10.0
ptyprocess==0.6.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
Pygments==2.4.2
pyparsing==2.4.2
pytest==5.2.1
pytest-cov==2.4.0
pytest-xdist==1.18.1
python-dateutil==2.8.1
PyYAML==5.1.2
requests==2.22.0
rlp==1.1.0
scipy==1.3.1
semantic-version==2.8.2
six==1.12.0
toml==0.10.0
toolz==0.10.0
tox==3.14.0
traitlets==4.3.3
trie==1.4.0
# Editable install with no version control (tstl==1.2.34)
-e /Users/adg326/vyper-venv/lib/python3.7/site-packages/tstl-1.2.34-py3.7.egg
typed-ast==1.3.5
urllib3==1.25.6
varint==1.0.2
virtualenv==16.7.7
vyper==0.2.8
wcwidth==0.1.7
web3==5.0.0b2
websockets==7.0
zipp==0.6.0

### What's your issue about?

This is basically cosmetic, and includes two bugs.  I'm starting up fuzzing vyper again, and not sure if you all care to see this kind of thing.

First:

This code:
```
cv8: constant(uint256) = "alpha"
cv6: constant(decimal) = block.timestamp or -MAX_DECIMAL * -MAX_DECIMAL <= block.timestamp
```

produces

```
Error compiling: vfile.55739.vy
decimal.InvalidOperation: [<class 'decimal.InvalidOperation'>]
```

Probably related (handling using decimal where not supported as a Python-level error, not a vyper-level type error:

```
cv8: constant(bool) = bitwise_or(0.6, 0.6)
```

produces

```
Error compiling: vfile.55844.vy
TypeError: unsupported operand type(s) for |: 'decimal.Decimal' and 'decimal.Decimal'
```

I'm not sure if you care about this kind of "cosmetic" issue but I believe I reported some in the past you fixed.  This is starting up the fuzzing effort based on this model:   https://github.com/agroce/tstl/tree/master/examples/vyper and using the TSTL testing tool.

### How can it be fixed?

Not sure in code, but just detect the type problem that or and bitwise_or don't like decimal inputs.



Discussion:
bitwise_xor (and I would assume bitwise_and) do the same.
yes, bitwise_and is the same
Yes, this is very much appreciated!  Keep it coming :)

Resolving patch:
diff --git a/vyper/ast/nodes.py b/vyper/ast/nodes.py
--- a/vyper/ast/nodes.py
+++ b/vyper/ast/nodes.py
@@ -26,7 +26,14 @@
     VyperException,
     ZeroDivisionException,
 )
-from vyper.utils import MAX_DECIMAL_PLACES, SizeLimits, annotate_source_code, evm_div, sha256sum
+from vyper.utils import (
+    MAX_DECIMAL_PLACES,
+    SizeLimits,
+    annotate_source_code,
+    evm_div,
+    quantize,
+    sha256sum,
+)
 
 NODE_BASE_ATTRIBUTES = (
     "_children",
@@ -824,6 +831,7 @@ def to_dict(self):
         return ast_dict
 
     def validate(self):
+        # note: maybe use self.value == quantize(self.value) for this check
         if self.value.as_tuple().exponent < -MAX_DECIMAL_PLACES:
             raise InvalidLiteral("Vyper supports a maximum of ten decimal points", self)
         if self.value < SizeLimits.MIN_AST_DECIMAL:
@@ -1010,9 +1018,15 @@ def _op(self, left, right):
         value = left * right
         if isinstance(left, decimal.Decimal):
             # ensure that the result is truncated to MAX_DECIMAL_PLACES
-            return value.quantize(
-                decimal.Decimal(f"{1:0.{MAX_DECIMAL_PLACES}f}"), decimal.ROUND_DOWN
-            )
+            try:
+                # if the intermediate result requires too many decimal places,
+                # decimal will puke - catch the error and raise an
+                # OverflowException
+                return quantize(value)
+            except decimal.InvalidOperation:
+                msg = f"{self._description} requires too many decimal places:"
+                msg += f"\n  {left} * {right} => {value}"
+                raise OverflowException(msg, self) from None
         else:
             return value
 
@@ -1036,7 +1050,12 @@ def _op(self, left, right):
             # the EVM always truncates toward zero
             value = -(-left / right)
         # ensure that the result is truncated to MAX_DECIMAL_PLACES
-        return value.quantize(decimal.Decimal(f"{1:0.{MAX_DECIMAL_PLACES}f}"), decimal.ROUND_DOWN)
+        try:
+            return quantize(value)
+        except decimal.InvalidOperation:
+            msg = f"{self._description} requires too many decimal places:"
+            msg += f"\n  {left} {self._pretty} {right} => {value}"
+            raise OverflowException(msg, self) from None
 
 
 class FloorDiv(VyperNode):
diff --git a/vyper/utils.py b/vyper/utils.py
--- a/vyper/utils.py
+++ b/vyper/utils.py
@@ -407,6 +407,11 @@ class SizeLimits:
     MAX_UINT256 = 2**256 - 1
 
 
+def quantize(d: decimal.Decimal, places=MAX_DECIMAL_PLACES, rounding_mode=decimal.ROUND_DOWN):
+    quantizer = decimal.Decimal(f"{1:0.{places}f}")
+    return d.quantize(quantizer, rounding_mode)
+
+
 # List of valid IR macros.
 # TODO move this somewhere else, like ir_node.py
 VALID_IR_MACROS = {


Test patch:
diff --git a/tests/functional/builtins/codegen/test_convert.py b/tests/functional/builtins/codegen/test_convert.py
--- a/tests/functional/builtins/codegen/test_convert.py
+++ b/tests/functional/builtins/codegen/test_convert.py
@@ -17,6 +17,7 @@
     checksum_encode,
     int_bounds,
     is_checksum_encoded,
+    quantize,
     round_towards_zero,
     unsigned_to_signed,
 )
@@ -414,7 +415,7 @@ def _vyper_literal(val, typ):
         return "0x" + val.hex()
     if isinstance(typ, DecimalT):
         tmp = val
-        val = val.quantize(DECIMAL_EPSILON)
+        val = quantize(val)
         assert tmp == val
     return str(val)
 
diff --git a/tests/functional/codegen/types/numbers/test_decimals.py b/tests/functional/codegen/types/numbers/test_decimals.py
--- a/tests/functional/codegen/types/numbers/test_decimals.py
+++ b/tests/functional/codegen/types/numbers/test_decimals.py
@@ -1,5 +1,5 @@
 import warnings
-from decimal import ROUND_DOWN, Decimal, getcontext
+from decimal import Decimal, getcontext
 
 import pytest
 
@@ -10,7 +10,7 @@
     OverflowException,
     TypeMismatch,
 )
-from vyper.utils import DECIMAL_EPSILON, SizeLimits
+from vyper.utils import DECIMAL_EPSILON, SizeLimits, quantize
 
 
 def test_decimal_override():
@@ -51,10 +51,6 @@ def foo(x: decimal) -> decimal:
         compile_code(code)
 
 
-def quantize(x: Decimal) -> Decimal:
-    return x.quantize(DECIMAL_EPSILON, rounding=ROUND_DOWN)
-
-
 def test_decimal_test(get_contract_with_gas_estimation):
     decimal_test = """
 @external
diff --git a/tests/unit/ast/nodes/test_fold_binop_decimal.py b/tests/unit/ast/nodes/test_fold_binop_decimal.py
--- a/tests/unit/ast/nodes/test_fold_binop_decimal.py
+++ b/tests/unit/ast/nodes/test_fold_binop_decimal.py
@@ -6,9 +6,17 @@
 
 from tests.utils import parse_and_fold
 from vyper.exceptions import OverflowException, TypeMismatch, ZeroDivisionException
+from vyper.semantics.analysis.local import ExprVisitor
+from vyper.semantics.types import DecimalT
+
+DECIMAL_T = DecimalT()
 
 st_decimals = st.decimals(
-    min_value=-(2**32), max_value=2**32, allow_nan=False, allow_infinity=False, places=10
+    min_value=DECIMAL_T.decimal_bounds[0],
+    max_value=DECIMAL_T.decimal_bounds[1],
+    allow_nan=False,
+    allow_infinity=False,
+    places=DECIMAL_T._decimal_places,
 )
 
 
@@ -30,10 +38,11 @@ def foo(a: decimal, b: decimal) -> decimal:
 
     try:
         vyper_ast = parse_and_fold(f"{left} {op} {right}")
-        old_node = vyper_ast.body[0].value
-        new_node = old_node.get_folded_value()
+        expr = vyper_ast.body[0].value
+        ExprVisitor().visit(expr, DecimalT())
+        new_node = expr.get_folded_value()
         is_valid = True
-    except ZeroDivisionException:
+    except (OverflowException, ZeroDivisionException):
         is_valid = False
 
     if is_valid:
@@ -71,9 +80,12 @@ def foo({input_value}) -> decimal:
     literal_op = literal_op.rsplit(maxsplit=1)[0]
     try:
         vyper_ast = parse_and_fold(literal_op)
-        new_node = vyper_ast.body[0].value.get_folded_value()
+        expr = vyper_ast.body[0].value
+        ExprVisitor().visit(expr, DecimalT())
+        new_node = expr.get_folded_value()
         expected = new_node.value
-        is_valid = -(2**127) <= expected < 2**127
+        lo, hi = DecimalT().decimal_bounds
+        is_valid = lo <= expected < hi
     except (OverflowException, ZeroDivisionException):
         # for overflow or division/modulus by 0, expect the contract call to revert
         is_valid = False