Repository: vyperlang/vyper

Issue:
Importing interfaces not as docs describe
### Version Information

* vyper Version (output of `vyper --version`): 0.3.3
* OS: osx
* Python Version (output of `python --version`): 3.9.5

### What's your issue about?

Please include information like:

* full output of the error you received
* what command you ran
* the code that caused the failure (see [this link](https://help.github.com/articles/basic-writing-and-formatting-syntax/) for help with formatting code)
* please try running your example with the --debug flag turned on

I have an interface that looks like such:

```python
# @version ^0.3.3
interface IFace:
    def read_stuff() -> uint256: view
```

And a contract that looks as such:

```python
# @version ^0.3.3
from interfaces.IFace import IFace

@external
@view
def read_contract(some_address: address) -> uint256:
    myContract: IFace = IFace(some_address)
    return myContract.read_stuff()
```

However, when I run `vyper contracts/MyContract.vy` I get:

```
Error compiling: contracts/MyContract.vy
vyper.exceptions.UnknownAttribute: IFace has no member 'read_stuff'. 
  contract "contracts/MyContract.vy", function "read_contract", line 11:11 
       10     myContract: IFace = IFace(some_address)
  ---> 11     return myContract.read_stuff()
  -------------------^
       12
```

[According to the documentation](https://vyper.readthedocs.io/en/stable/interfaces.html?highlight=interface#importing-interfaces), when importing interfaces, you need to use regular vyper syntax. However, adjusting my interface as such:

```python
# @version ^0.3.3

interface IFace:
    @external
    def read_stuff() -> uint256: 
        pass
```

Results in this error:

```
vyper.exceptions.StructureException: Body must only contain state mutability label
  contract "contracts/interfaces/IFace.vy", function "read_stuff", line 6:8 
       5     def read_stuff() -> uint256:
  ---> 6         pass
  ---------------^
       7
```

### How can it be fixed?

I'm not sure


Discussion:

Patch:
diff --git a/vyper/ast/nodes.py b/vyper/ast/nodes.py
index 90365c63..fa1fb636 100644
--- a/vyper/ast/nodes.py
+++ b/vyper/ast/nodes.py
@@ -1372,8 +1372,8 @@ class ImplementsDecl(Stmt):
     def __init__(self, *args, **kwargs):
         super().__init__(*args, **kwargs)
 
-        if not isinstance(self.annotation, Name):
-            raise StructureException("not an identifier", self.annotation)
+        if not isinstance(self.annotation, (Name, Attribute)):
+            raise StructureException("invalid implements", self.annotation)
 
 
 class If(Stmt):
diff --git a/vyper/ast/parse.py b/vyper/ast/parse.py
index b1b9a8d9..cc0a4782 100644
--- a/vyper/ast/parse.py
+++ b/vyper/ast/parse.py
@@ -1,5 +1,4 @@
 import ast as python_ast
-import string
 import tokenize
 from decimal import Decimal
 from typing import Any, Dict, List, Optional, Union, cast
@@ -115,6 +114,50 @@ def dict_to_ast(ast_struct: Union[Dict, List]) -> Union[vy_ast.VyperNode, List]:
     raise CompilerPanic(f'Unknown ast_struct provided: "{type(ast_struct)}".')
 
 
+def annotate_python_ast(
+    parsed_ast: python_ast.AST,
+    source_code: str,
+    modification_offsets: ModificationOffsets,
+    for_loop_annotations: dict,
+    source_id: int = 0,
+    module_path: Optional[str] = None,
+    resolved_path: Optional[str] = None,
+) -> python_ast.AST:
+    """
+    Annotate and optimize a Python AST in preparation conversion to a Vyper AST.
+
+    Parameters
+    ----------
+    parsed_ast : AST
+        The AST to be annotated and optimized.
+    source_code : str
+        The originating source code of the AST.
+    loop_var_annotations: dict
+        A mapping of line numbers of `For` nodes to the tokens of the type
+        annotation of the iterator extracted during pre-parsing.
+    modification_offsets : dict
+        A mapping of class names to their original class types.
+
+    Returns
+    -------
+        The annotated and optimized AST.
+    """
+
+    tokens = asttokens.ASTTokens(source_code, tree=cast(Optional[python_ast.Module], parsed_ast))
+    visitor = AnnotatingVisitor(
+        source_code,
+        modification_offsets,
+        for_loop_annotations,
+        tokens,
+        source_id,
+        module_path=module_path,
+        resolved_path=resolved_path,
+    )
+    visitor.visit(parsed_ast)
+
+    return parsed_ast
+
+
 class AnnotatingVisitor(python_ast.NodeTransformer):
     _source_code: str
     _modification_offsets: ModificationOffsets
@@ -170,6 +213,7 @@ class AnnotatingVisitor(python_ast.NodeTransformer):
         if hasattr(node, "last_token"):
             start_pos = node.first_token.startpos
             end_pos = node.last_token.endpos
+
             if node.last_token.type == 4:
                 # ignore trailing newline once more
                 end_pos -= 1
@@ -241,52 +285,42 @@ class AnnotatingVisitor(python_ast.NodeTransformer):
                 node.col_offset,
             )
 
-        self.generic_visit(node)
+        # some kind of black magic. untokenize preserves the line and column
+        # offsets, giving us something like `\
+        # \
+        # \
+        #   uint8`
+        # that's not a valid python Expr because it is indented.
+        # but it's good because the code is indented to exactly the same
+        # offset as it did in the original source!
+        # (to best understand this, print out annotation_str and
+        # self._source_code and compare them side-by-side).
+        #
+        # what we do here is add in a dummy target which we will remove
+        # in a bit, but for now lets us keep the line/col offset, and
+        # *also* gives us a valid AST. it doesn't matter what the dummy
+        # target name is, since it gets removed in a few lines.
+        annotation_str = tokenize.untokenize(annotation_tokens)
+        annotation_str = "dummy_target:" + annotation_str
 
         try:
-            annotation_str = tokenize.untokenize(annotation_tokens).strip(string.whitespace + "\\")
-            annotation = python_ast.parse(annotation_str)
+            fake_node = python_ast.parse(annotation_str).body[0]
         except SyntaxError as e:
             raise SyntaxException(
                 "invalid type annotation", self._source_code, node.lineno, node.col_offset
             ) from e
 
-        annotation = annotation.body[0]
-        og_target = node.target
-
-        # annotate with token and source code information. `first_token`
-        # and `last_token` attributes are accessed in `generic_visit`.
-        tokens = asttokens.ASTTokens(annotation_str)
-        tokens.mark_tokens(annotation)
-
-        # decrease line offset by 1 because annotation is on the same line as `For` node
-        # but the spliced expression also starts at line 1
-        adjustment = og_target.first_token.start[0] - 1, og_target.first_token.start[1]
-
-        def _add_pair(x, y):
-            return x[0] + y[0], x[1] + y[1]
-
-        for n in python_ast.walk(annotation):
-            # adjust all offsets
-            if hasattr(n, "first_token"):
-                n.first_token = n.first_token._replace(
-                    start=_add_pair(n.first_token.start, adjustment),
-                    end=_add_pair(n.first_token.end, adjustment),
-                    startpos=n.first_token.startpos + og_target.first_token.startpos,
-                    endpos=n.first_token.startpos + og_target.first_token.startpos,
-                )
-            if hasattr(n, "last_token"):
-                n.last_token = n.last_token._replace(
-                    start=_add_pair(n.last_token.start, adjustment),
-                    end=_add_pair(n.last_token.end, adjustment),
-                    startpos=n.last_token.startpos + og_target.first_token.startpos,
-                    endpos=n.last_token.endpos + og_target.first_token.startpos,
-                )
+        # fill in with asttokens info. note we can use `self._tokens` because
+        # it is indented to exactly the same position where it appeared
+        # in the original source!
+        self._tokens.mark_tokens(fake_node)
 
-        node.target = annotation
-        node.target = self.generic_visit(node.target)
+        # replace the dummy target name with the real target name.
+        fake_node.target = node.target
+        # replace the For node target with the new ann_assign
+        node.target = fake_node
 
-        return node
+        return self.generic_visit(node)
 
     def visit_Expr(self, node):
         """
@@ -425,47 +459,3 @@ class AnnotatingVisitor(python_ast.NodeTransformer):
             return node.operand
         else:
             return node
-
-
-def annotate_python_ast(
-    parsed_ast: python_ast.AST,
-    source_code: str,
-    modification_offsets: ModificationOffsets,
-    for_loop_annotations: dict,
-    source_id: int = 0,
-    module_path: Optional[str] = None,
-    resolved_path: Optional[str] = None,
-) -> python_ast.AST:
-    """
-    Annotate and optimize a Python AST in preparation conversion to a Vyper AST.
-
-    Parameters
-    ----------
-    parsed_ast : AST
-        The AST to be annotated and optimized.
-    source_code : str
-        The originating source code of the AST.
-    loop_var_annotations: dict, optional
-        A mapping of line numbers of `For` nodes to the tokens of the type annotation
-        of the iterator extracted during pre-parsing.
-    modification_offsets : dict, optional
-        A mapping of class names to their original class types.
-
-    Returns
-    -------
-        The annotated and optimized AST.
-    """
-
-    tokens = asttokens.ASTTokens(source_code, tree=cast(Optional[python_ast.Module], parsed_ast))
-    visitor = AnnotatingVisitor(
-        source_code,
-        modification_offsets,
-        for_loop_annotations,
-        tokens,
-        source_id,
-        module_path=module_path,
-        resolved_path=resolved_path,
-    )
-    visitor.visit(parsed_ast)
-
-    return parsed_ast
diff --git a/vyper/ast/pre_parser.py b/vyper/ast/pre_parser.py
index f7d2df20..159dfc0a 100644
--- a/vyper/ast/pre_parser.py
+++ b/vyper/ast/pre_parser.py
@@ -65,32 +65,27 @@ class ForParser:
     def consume(self, token):
         # state machine: we can start slurping tokens soon
         if token.type == NAME and token.string == "for":
-            # sanity check -- this should never really happen, but if it does,
-            # try to raise an exception which pinpoints the source.
-            if self._current_annotation is not None:
-                raise SyntaxException(
-                    "for loop parse error", self._code, token.start[0], token.start[1]
-                )
-            self._current_annotation = []
-
-            assert self._state == ForParserState.NOT_RUNNING
+            # note: self._state should be NOT_RUNNING here, but we don't sanity
+            # check here as that should be an error the parser will handle.
             self._state = ForParserState.START_SOON
             self._current_for_loop = token.start
-            return False
 
         if self._state == ForParserState.NOT_RUNNING:
             return False
 
-        if self._state == ForParserState.START_SOON:
-            # state machine: start slurping tokens
+        # state machine: start slurping tokens
+        if token.type == OP and token.string == ":":
+            self._state = ForParserState.RUNNING
 
-            self._current_annotation.append(token)
-
-            if token.type == OP and token.string == ":":
-                self._state = ForParserState.RUNNING
-                return True  # do not add ":" to global tokens.
+            # sanity check -- this should never really happen, but if it does,
+            # try to raise an exception which pinpoints the source.
+            if self._current_annotation is not None:
+                raise SyntaxException(
+                    "for loop parse error", self._code, token.start[0], token.start[1]
+                )
 
-            return False  # add everything before ":" to tokens
+            self._current_annotation = []
+            return True  # do not add ":" to tokens.
 
         # state machine: end slurping tokens
         if token.type == NAME and token.string == "in":
@@ -142,8 +137,7 @@ def pre_parse(code: str) -> tuple[Settings, ModificationOffsets, dict, str]:
     ModificationOffsets
         A mapping of class names to their original class types.
     dict[tuple[int, int], list[TokenInfo]]
-        A mapping of line/column offsets of `For` nodes to the tokens of the annotation of the
-        for loop target
+        A mapping of line/column offsets of `For` nodes to the annotation of the for loop target
     str
         Reformatted python source string.
     """
diff --git a/vyper/semantics/analysis/module.py b/vyper/semantics/analysis/module.py
index 4a7e33e8..2972ed29 100644
--- a/vyper/semantics/analysis/module.py
+++ b/vyper/semantics/analysis/module.py
@@ -383,8 +383,9 @@ class ModuleAnalyzer(VyperNodeVisitorBase):
         self._add_import(node, node.level, qualified_module_name, alias)
 
     def visit_InterfaceDef(self, node):
-        obj = InterfaceT.from_InterfaceDef(node)
-        self.namespace[node.name] = obj
+        interface_t = InterfaceT.from_InterfaceDef(node)
+        node._metadata["interface_type"] = interface_t
+        self.namespace[node.name] = interface_t
 
     def visit_StructDef(self, node):
         struct_t = StructT.from_StructDef(node)
diff --git a/vyper/semantics/types/module.py b/vyper/semantics/types/module.py
index 8f1a5cc0..f2c3d745 100644
--- a/vyper/semantics/types/module.py
+++ b/vyper/semantics/types/module.py
@@ -39,6 +39,7 @@ class InterfaceT(_UserType):
 
         self._helper = VyperType(events | structs)
         self._id = _id
+        self._helper._id = _id
         self.functions = functions
         self.events = events
         self.structs = structs
@@ -267,6 +268,8 @@ class InterfaceT(_UserType):
 
 # Datatype to store all module information.
 class ModuleT(VyperType):
+    _attribute_in_annotation = True
+
     def __init__(self, module: vy_ast.Module, name: Optional[str] = None):
         super().__init__()
 
@@ -276,7 +279,10 @@ class ModuleT(VyperType):
 
         # compute the interface, note this has the side effect of checking
         # for function collisions
-        self._helper = self.interface
+        _ = self.interface
+
+        self._helper = VyperType()
+        self._helper._id = self._id
 
         for f in self.function_defs:
             # note: this checks for collisions
@@ -289,6 +295,12 @@ class ModuleT(VyperType):
         for s in self.struct_defs:
             # add the type of the struct so it can be used in call position
             self.add_member(s.name, TYPE_T(s._metadata["struct_type"]))  # type: ignore
+            self._helper.add_member(s.name, TYPE_T(s._metadata["struct_type"]))  # type: ignore
+
+        for i in self.interface_defs:
+            # add the type of the interface so it can be used in call position
+            self.add_member(i.name, TYPE_T(i._metadata["interface_type"]))  # type: ignore
+            self._helper.add_member(i.name, TYPE_T(i._metadata["interface_type"]))  # type: ignore
 
         for v in self.variable_decls:
             self.add_member(v.target.id, v.target._metadata["varinfo"])
@@ -322,6 +334,10 @@ class ModuleT(VyperType):
     def struct_defs(self):
         return self._module.get_children(vy_ast.StructDef)
 
+    @property
+    def interface_defs(self):
+        return self._module.get_children(vy_ast.InterfaceDef)
+
     @property
     def import_stmts(self):
         return self._module.get_children((vy_ast.Import, vy_ast.ImportFrom))
diff --git a/vyper/semantics/types/utils.py b/vyper/semantics/types/utils.py
index eb963754..c82eb73a 100644
--- a/vyper/semantics/types/utils.py
+++ b/vyper/semantics/types/utils.py
@@ -127,14 +127,16 @@ def _type_from_annotation(node: vy_ast.VyperNode) -> VyperType:
         except UndeclaredDefinition:
             raise InvalidType(err_msg, node) from None
 
-        interface = module_or_interface
         if hasattr(module_or_interface, "module_t"):  # i.e., it's a ModuleInfo
-            interface = module_or_interface.module_t.interface
+            module_or_interface = module_or_interface.module_t
 
-        if not interface._attribute_in_annotation:
+        if not isinstance(module_or_interface, VyperType):
             raise InvalidType(err_msg, node)
 
-        type_t = interface.get_type_member(node.attr, node)
+        if not module_or_interface._attribute_in_annotation:
+            raise InvalidType(err_msg, node)
+
+        type_t = module_or_interface.get_type_member(node.attr, node)  # type: ignore
         assert isinstance(type_t, TYPE_T)  # sanity check
         return type_t.typedef
 


Test patch:
diff --git a/tests/functional/codegen/modules/test_interface_imports.py b/tests/functional/codegen/modules/test_interface_imports.py
new file mode 100644
index 00000000..084ad26e
--- /dev/null
+++ b/tests/functional/codegen/modules/test_interface_imports.py
@@ -0,0 +1,31 @@
+def test_import_interface_types(make_input_bundle, get_contract):
+    ifaces = """
+interface IFoo:
+    def foo() -> uint256: nonpayable
+    """
+
+    foo_impl = """
+import ifaces
+
+implements: ifaces.IFoo
+
+@external
+def foo() -> uint256:
+    return block.number
+    """
+
+    contract = """
+import ifaces
+
+@external
+def test_foo(s: ifaces.IFoo) -> bool:
+    assert s.foo() == block.number
+    return True
+    """
+
+    input_bundle = make_input_bundle({"ifaces.vy": ifaces})
+
+    foo = get_contract(foo_impl, input_bundle=input_bundle)
+    c = get_contract(contract, input_bundle=input_bundle)
+
+    assert c.test_foo(foo.address) is True
diff --git a/tests/functional/codegen/test_stateless_modules.py b/tests/functional/codegen/modules/test_stateless_functions.py
similarity index 100%
rename from tests/functional/codegen/test_stateless_modules.py
rename to tests/functional/codegen/modules/test_stateless_functions.py
diff --git a/tests/functional/syntax/test_for_range.py b/tests/functional/syntax/test_for_range.py
index e807e12d..a486d117 100644
--- a/tests/functional/syntax/test_for_range.py
+++ b/tests/functional/syntax/test_for_range.py
@@ -247,9 +247,43 @@ def foo():
         "No builtin or user-defined type named 'DynArra'. Did you mean 'DynArray'?",
         "DynArra",
     ),
+    (
+        # test for loop target broken into multiple lines
+        """
+@external
+def foo():
+    for i: \\
+      \\
+        \\
+        \\
+        \\
+        \\
+        uint9 in [1,2,3]:
+        pass
+    """,
+        UnknownType,
+        "No builtin or user-defined type named 'uint9'. Did you mean 'uint96', or maybe 'uint8'?",
+        "uint9",
+    ),
+    (
+        # test an even more deranged example
+        """
+@external
+def foo():
+    for i: \\
+        \\
+          DynArray[\\
+        uint9, 3\\
+        ] in [1,2,3]:
+        pass
+    """,
+        UnknownType,
+        "No builtin or user-defined type named 'uint9'. Did you mean 'uint96', or maybe 'uint8'?",
+        "uint9",
+    ),
 ]
 
-for_code_regex = re.compile(r"for .+ in (.*):")
+for_code_regex = re.compile(r"for .+ in (.*):", re.DOTALL)
 fail_test_names = [
     (
         f"{i:02d}: {for_code_regex.search(code).group(1)}"  # type: ignore[union-attr]
diff --git a/tests/functional/syntax/test_interfaces.py b/tests/functional/syntax/test_interfaces.py
index a672ed7b..ca96adca 100644
--- a/tests/functional/syntax/test_interfaces.py
+++ b/tests/functional/syntax/test_interfaces.py
@@ -90,7 +90,7 @@ interface A:
         """
 implements: self.x
     """,
-        StructureException,
+        InvalidType,
     ),
     (
         """