Repository: vyperlang/vyper

Issue:
Inconsistent type-checking depending on Assign vs AnnAssign
### Version Information

* vyper Version: master / ~d934b291f53756b47cf0775c73742b48e3fafb58~ 921bb80cf59c9be0067ed20e1a7b6ad0b59598b3
* OS: linux
* Python Version (python --version):
```
$ python --version
Python 3.6.7
```
* Environment (output of `pip freeze`):
```
$ pip freeze
flake8==3.6.0
mccabe==0.6.1
pycodestyle==2.4.0
pycryptodome==3.7.2
pyflakes==2.0.0
vyper==0.1.0b4
```

### What's your issue about?
```python
# Fails with vyper.exceptions.TypeMismatchException: Invalid type, expected: decimal
#@private
#def _assign1():
#    y: int128 = 1
#    z: decimal = y

# Passes:
@private
def _assign2():
    y: int128 = 1
    z: decimal
    z = y

# By analogy with _assign1 this should fail but it does not.
@private
def _assign3():
    y: {a: {c: int128}, b: int128}
    z: {a: {c: decimal}, b: int128} = y
```

Ran into this while working on type-checking in #1102. This is actually in the test suite - https://github.com/ethereum/vyper/blob/921bb80cf59c9be0067ed20e1a7b6ad0b59598b3/tests/parser/syntax/test_maps.py#L135-L141.

### How can it be fixed?
I don't know whether `_assign1` is correct is or `_assign2` (I think the rejection in `_assign1` is correct), but basically one needs to reify the type-checking logic between `_check_valid_assign` (used by `ann_assign`) and `make_setter` (used by `assign`).


Discussion:
Man, all three of those should fail. They all look pretty dubious!
Yeah they all definitely should fail. I feel like this type of behavior should require being explicit with the `convert()` function.
Pushing a fix shortly (or at least partial fix as for me `_assign3` already fails) that enforces throwing an error for implicit type conversions.

Resolving patch:
diff --git a/vyper/parser/stmt.py b/vyper/parser/stmt.py
--- a/vyper/parser/stmt.py
+++ b/vyper/parser/stmt.py
@@ -176,6 +176,13 @@ def ann_assign(self):
         self.context.set_in_assignment(False)
         return o
 
+    def _check_implicit_conversion(self, var_id, sub):
+        target_typ = self.context.vars[var_id].typ
+        assign_typ = sub.typ
+        if isinstance(target_typ, BaseType) and isinstance(assign_typ, BaseType):
+            if not assign_typ.is_literal and assign_typ.typ != target_typ.typ:
+                raise TypeMismatchException('Invalid type {}, expected: {}'.format(assign_typ.typ, target_typ.typ, self.stmt))
+
     def assign(self):
         # Assignment (e.g. x[4] = y)
         if len(self.stmt.targets) != 1:
@@ -188,18 +195,24 @@ def assign(self):
             pos = self.context.new_variable(self.stmt.targets[0].id, sub.typ)
             variable_loc = LLLnode.from_list(pos, typ=sub.typ, location='memory', pos=getpos(self.stmt), annotation=self.stmt.targets[0].id)
             o = make_setter(variable_loc, sub, 'memory', pos=getpos(self.stmt))
+        else:
+            # Error check when assigning to declared variable
+            if isinstance(self.stmt.targets[0], ast.Name):
+                # Do not allow assignment to undefined variables without annotation
+                if self.stmt.targets[0].id not in self.context.vars:
+                    raise VariableDeclarationException("Variable type not defined", self.stmt)
 
-        # All other assignments are forbidden.
-        elif isinstance(self.stmt.targets[0], ast.Name) and self.stmt.targets[0].id not in self.context.vars:
-            raise VariableDeclarationException("Variable type not defined", self.stmt)
+                # Check against implicit conversion
+                self._check_implicit_conversion(self.stmt.targets[0].id, sub)
 
-        elif isinstance(self.stmt.targets[0], ast.Tuple) and isinstance(self.stmt.value, ast.Tuple):
-            raise VariableDeclarationException("Tuple to tuple assignment not supported", self.stmt)
+            # Do no allow tuple-to-tuple assignment
+            if isinstance(self.stmt.targets[0], ast.Tuple) and isinstance(self.stmt.value, ast.Tuple):
+                raise VariableDeclarationException("Tuple to tuple assignment not supported", self.stmt)
 
-        else:
             # Checks to see if assignment is valid
             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


Test patch:
diff --git a/tests/parser/features/test_assignment.py b/tests/parser/features/test_assignment.py
--- a/tests/parser/features/test_assignment.py
+++ b/tests/parser/features/test_assignment.py
@@ -2,6 +2,7 @@
     ParserException,
     ConstancyViolationException,
     InvalidLiteralException,
+    TypeMismatchException,
 )
 
 
@@ -179,3 +180,79 @@ def get() -> int128:
     c = get_contract_with_gas_estimation(code)
     assert c.set(transact={})
     assert c.get() == 111
+
+
+def test_invalid_implicit_conversions(assert_compile_failed, get_contract_with_gas_estimation):
+    contracts = [
+"""
+@public
+def foo():
+   y: int128 = 1
+   z: decimal = y
+""",
+"""
+@public
+def foo():
+    y: int128 = 1
+    z: decimal
+    z = y
+""",
+"""
+@public
+def foo():
+   y: bool = False
+   z: decimal = y
+""",
+"""
+@public
+def foo():
+    y: bool = False
+    z: decimal
+    z = y
+""",
+"""
+@public
+def foo():
+   y: uint256 = 1
+   z: int128 = y
+""",
+"""
+@public
+def foo():
+    y: uint256 = 1
+    z: int128
+    z = y
+""",
+"""
+@public
+def foo():
+   y: int128 = 1
+   z: bytes32 = y
+""",
+"""
+@public
+def foo():
+    y: int128 = 1
+    z: bytes32
+    z = y
+""",
+"""
+@public
+def foo():
+   y: uint256 = 1
+   z: bytes32 = y
+""",
+"""
+@public
+def foo():
+    y: uint256 = 1
+    z: bytes32
+    z = y
+"""
+    ]
+
+    for contract in contracts:
+        assert_compile_failed(
+            lambda: get_contract_with_gas_estimation(contract),
+            TypeMismatchException
+        )