Repository: vyperlang/vyper

Issue:
Compiler error when function returns tuple containing structs
In Ethereum Python Community Discord group, fubuloubu asked me to create this issue.

### Version Information

* vyper: 0.2.12
* OS: OSX
* Python: 3.9.6

### What's your issue about?

When a function in a contract is returning a tuple of two structs, It will not compile. Here is the error: 

`AttributeError: 'StructType' object has no attribute 'typ'`

Here is sample code if you want to replicate the error:
```
struct Animal:
    location: address
    fur: uint256

struct Human:
    location: address
    height: uint256

@external
def thisDoesNotWork(_animal: Animal, _human: Human) -> (Animal, Human):
    animal: Animal = _animal
    human: Human = _human

    # do stuff, edit the structs
    animal.fur += 1
    human.height += 1

    return animal, human
```

### How can it be fixed?

fubuloubu says it's a compiler error. I don't know exactly how to fix it.



Discussion:
@CapUndyPants can you print the full error you are seeing?
```
Unhandled exception in 'contracts/test/FooBar.vy':
  File "brownie/_cli/__main__.py", line 64, in main
    importlib.import_module(f"brownie._cli.{cmd}").main()
  File "brownie/_cli/compile.py", line 50, in main
    proj = project.load()
  File "brownie/project/main.py", line 745, in load
    return Project(name, project_path)
  File "brownie/project/main.py", line 180, in __init__
    self.load()
  File "brownie/project/main.py", line 235, in load
    self._compile(changed, self._compiler_config, False)
  File "brownie/project/main.py", line 92, in _compile
    build_json = compiler.compile_and_format(
  File "brownie/project/compiler/__init__.py", line 141, in compile_and_format
    output_json = compile_from_input_json(input_json, silent, allow_paths)
  File "brownie/project/compiler/__init__.py", line 248, in compile_from_input_json
    return vyper.compile_from_input_json(input_json, silent, allow_paths)
  File "brownie/project/compiler/vyper.py", line 235, in compile_from_input_json
    return vyper_json.compile_json(input_json, root_path=allow_paths)
  File "vyper/cli/vyper_json.py", line 394, in compile_json
    compiler_data, warn_data = compile_from_input_dict(input_dict, exc_handler, root_path)
  File "vyper/cli/vyper_json.py", line 326, in compile_from_input_dict
    return exc_handler(contract_path, exc, "compiler"), {}
  File "vyper/cli/vyper_json.py", line 102, in exc_handler_raises
    raise exception
  File "vyper/cli/vyper_json.py", line 318, in compile_from_input_dict
    data = vyper.compile_codes(
  File "vyper/opcodes.py", line 223, in _wrapper
    return fn(*args, **kwargs)
  File "vyper/compiler/__init__.py", line 110, in compile_codes
    raise exc
  File "vyper/compiler/__init__.py", line 105, in compile_codes
    out[contract_name][output_format] = OUTPUT_FORMATS[output_format](compiler_data)
  File "vyper/compiler/output.py", line 88, in build_abi_output
    gas_estimates = build_gas_estimates(compiler_data.lll_nodes)
  File "vyper/compiler/phases.py", line 101, in lll_nodes
    self._gen_lll()
  File "vyper/compiler/phases.py", line 96, in _gen_lll
    self._lll_nodes, self._lll_runtime = generate_lll_nodes(self.global_ctx)
  File "vyper/compiler/phases.py", line 224, in generate_lll_nodes
    lll_nodes, lll_runtime = parser.parse_tree_to_lll(global_ctx)
  File "vyper/parser/parser.py", line 207, in parse_tree_to_lll
    o, runtime = parse_other_functions(
  File "vyper/parser/parser.py", line 112, in parse_other_functions
    func_lll = parse_function(
  File "vyper/parser/function_definitions/parse_function.py", line 58, in parse_function
    o = parse_external_function(
  File "vyper/parser/function_definitions/parse_external_function.py", line 229, in parse_external_function
    + [parse_body(c, context) for c in code.body]
  File "vyper/parser/function_definitions/parse_external_function.py", line 229, in <listcomp>
    + [parse_body(c, context) for c in code.body]
  File "vyper/parser/stmt.py", line 573, in parse_body
    return parse_stmt(code, context)
  File "vyper/parser/stmt.py", line 567, in parse_stmt
    return Stmt(stmt, context).lll_node
  File "vyper/parser/stmt.py", line 40, in __init__
    self.lll_node = fn()
  File "vyper/parser/stmt.py", line 546, in parse_Return
    return gen_tuple_return(self.stmt, self.context, sub)
  File "vyper/codegen/return_.py", line 100, in gen_tuple_return
    base_types.add(typ.typ)
AttributeError: 'StructType' object has no attribute 'typ'
```
@CapUndyPants thanks!

Resolving patch:
diff --git a/vyper/old_codegen/return_.py b/vyper/old_codegen/return_.py
--- a/vyper/old_codegen/return_.py
+++ b/vyper/old_codegen/return_.py
@@ -1,12 +1,7 @@
 from vyper import ast as vy_ast
 from vyper.old_codegen.lll_node import LLLnode
 from vyper.old_codegen.parser_utils import getpos, make_setter
-from vyper.old_codegen.types import (
-    BaseType,
-    ListType,
-    TupleType,
-    get_size_of_type,
-)
+from vyper.old_codegen.types import BaseType, TupleType, get_size_of_type
 from vyper.old_codegen.types.check import check_assign
 from vyper.utils import MemoryPositions
 
@@ -100,31 +95,26 @@ def gen_tuple_return(stmt, context, sub):
 
         if isinstance(context.return_type, TupleType) and not abi_typ.dynamic_size_bound():
             # for tuples where every value is of the same type and a fixed length,
-            # we can simplify the encoding by treating it as though it were an array
-            base_types = set()
-            for typ in context.return_type.members:
-                while isinstance(typ, ListType):
-                    typ = typ.subtype
-                base_types.add(typ.typ)
-
-            if len(base_types) == 1:
-                new_sub = LLLnode.from_list(
-                    context.new_internal_variable(context.return_type),
-                    typ=context.return_type,
-                    location="memory",
-                )
-                setter = make_setter(new_sub, sub, "memory", pos=getpos(stmt))
-                return LLLnode.from_list(
-                    [
-                        "seq",
-                        setter,
-                        make_return_stmt(
-                            stmt, context, new_sub, get_size_of_type(context.return_type) * 32,
-                        ),
-                    ],
-                    typ=None,
-                    pos=getpos(stmt),
-                )
+            # we can simplify the encoding by using make_setter, since
+            # our memory encoding happens to be identical to the ABI
+            # encoding.
+            new_sub = LLLnode.from_list(
+                context.new_internal_variable(context.return_type),
+                typ=context.return_type,
+                location="memory",
+            )
+            setter = make_setter(new_sub, sub, "memory", pos=getpos(stmt))
+            return LLLnode.from_list(
+                [
+                    "seq",
+                    setter,
+                    make_return_stmt(
+                        stmt, context, new_sub, get_size_of_type(context.return_type) * 32,
+                    ),
+                ],
+                typ=None,
+                pos=getpos(stmt),
+            )
 
         # in case of multi we can't create a variable to store location of the return expression
         # as multi can have data from multiple location like store, calldata etc


Test patch:
diff --git a/tests/functional/codegen/test_struct_return.py b/tests/functional/codegen/test_struct_return.py
--- a/tests/functional/codegen/test_struct_return.py
+++ b/tests/functional/codegen/test_struct_return.py
@@ -1,6 +1,39 @@
 import pytest
 
 
+def test_nested_tuple(get_contract):
+    code = """
+struct Animal:
+    location: address
+    fur: uint256
+
+struct Human:
+    location: address
+    height: uint256
+
+@external
+def return_nested_tuple() -> (Animal, Human):
+    animal: Animal = Animal({
+        location: 0x1234567890123456789012345678901234567890,
+        fur: 123
+    })
+    human: Human = Human({
+        location: 0x1234567890123456789012345678900000000000,
+        height: 456
+    })
+
+    # do stuff, edit the structs
+    animal.fur += 1
+    human.height += 1
+
+    return animal, human
+    """
+    c = get_contract(code)
+    addr1 = "0x1234567890123456789012345678901234567890"
+    addr2 = "0x1234567890123456789012345678900000000000"
+    assert c.return_nested_tuple() == [(addr1, 124), (addr2, 457)]
+
+
 @pytest.mark.parametrize("string", ["a", "abc", "abcde", "potato"])
 def test_string_inside_tuple(get_contract, string):
     code = f"""