Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat[ux]: allow "compiling" .vyi files #4290

Merged
merged 47 commits into from
Dec 27, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
47 commits
Select commit Hold shift + click to select a range
fb72306
set is_interface based on the suffix of an input file
sandbubbles Oct 11, 2024
8bc61ed
construct ir_runtime only if input isn't interface
sandbubbles Oct 11, 2024
112c76f
exclude some output formats when compiling interface
sandbubbles Oct 11, 2024
fc05efe
add test for interface outputs
sandbubbles Oct 11, 2024
e5fc9a7
iterate through all unsupported formats
sandbubbles Oct 12, 2024
328f7fd
tag modules with is_interface
sandbubbles Oct 12, 2024
36c620c
add is_interface tag to ModuleT
sandbubbles Oct 13, 2024
d181da1
add is_interface to parse_to_ast_with_settings
sandbubbles Oct 14, 2024
5d4e55b
adjust tests
sandbubbles Oct 14, 2024
6641092
lint
sandbubbles Oct 14, 2024
87bc04e
merge master
sandbubbles Oct 14, 2024
0908bdf
Merge branch 'master' into feat/compile-solely-vyi
sandbubbles Oct 15, 2024
afd8f8e
adjust solution due to merge
sandbubbles Oct 15, 2024
8048d4f
reduce a multiline expression
charles-cooper Oct 15, 2024
2651d89
fix mypy
charles-cooper Oct 15, 2024
1134c3a
style: factor out an expression
charles-cooper Oct 15, 2024
b582f8f
Merge branch 'master' into feat/compile-solely-vyi
charles-cooper Oct 15, 2024
5cdb2da
Merge branch 'master' into feat/compile-solely-vyi
charles-cooper Oct 17, 2024
324b598
add flags into interface output
sandbubbles Oct 25, 2024
50692a1
add test for presence of flags in interface output
sandbubbles Oct 25, 2024
fcfc2ec
lint
sandbubbles Oct 25, 2024
60dddd4
capitalize the first letter but leave the rest the same
sandbubbles Oct 25, 2024
77d62a5
Merge branch 'master' into feat/compile-solely-vyi
sandbubbles Oct 29, 2024
3bf9389
fix flake8 errors
sandbubbles Oct 29, 2024
6806134
Merge branch 'master' into feat/compile-solely-vyi
sandbubbles Nov 7, 2024
5d954d8
remove duplicate list
sandbubbles Nov 7, 2024
49385b7
Merge branch 'master' into feat/compile-solely-vyi
charles-cooper Nov 21, 2024
a21dc18
handle edgecases of filenames
sandbubbles Dec 7, 2024
af655be
add str function for FlagT
sandbubbles Dec 7, 2024
bef8725
test function with flag as return type
sandbubbles Dec 7, 2024
59101c0
merge master
sandbubbles Dec 7, 2024
4c476ad
take file_input from compiler_data after merge
sandbubbles Dec 7, 2024
3bc7ef8
use the OUTPUT_FORMATS instead of listing all options
sandbubbles Dec 12, 2024
d851c73
replace capitilising by a cleaner solution
sandbubbles Dec 12, 2024
92c883f
Merge branch 'master' into feat/compile-solely-vyi
sandbubbles Dec 12, 2024
a392898
remove unused list
sandbubbles Dec 12, 2024
e46ccb7
remove repeated code
charles-cooper Dec 13, 2024
bce2a08
test compilation round trip
sandbubbles Dec 15, 2024
e37f4f7
add comment
sandbubbles Dec 15, 2024
53b4626
test weird interface name
sandbubbles Dec 15, 2024
cea1f21
add comment
sandbubbles Dec 15, 2024
d48b137
Merge branch 'master' into feat/compile-solely-vyi
sandbubbles Dec 18, 2024
be7318b
add parameter to inner function after merge with master
sandbubbles Dec 18, 2024
5f04c0e
Merge branch 'master' into feat/compile-solely-vyi
charles-cooper Dec 19, 2024
37666bc
explain test in comment
sandbubbles Dec 26, 2024
574bee1
Merge branch 'master' into feat/compile-solely-vyi
sandbubbles Dec 26, 2024
99a7605
Merge branch 'master' into feat/compile-solely-vyi
charles-cooper Dec 27, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
105 changes: 105 additions & 0 deletions tests/functional/codegen/test_interfaces.py
Original file line number Diff line number Diff line change
Expand Up @@ -876,3 +876,108 @@ def bar() -> uint256:
input_bundle = make_input_bundle({"lib1.vy": lib1})
c = get_contract(main, input_bundle=input_bundle)
assert c.bar() == 1


def test_interface_with_flags():
code = """
struct MyStruct:
a: address

flag Foo:
BOO
MOO
POO

event Transfer:
sender: indexed(address)

@external
def bar():
pass
flag BAR:
BIZ
BAZ
BOO

@external
@view
def foo(s: MyStruct) -> MyStruct:
return s
"""

out = compile_code(code, contract_path="code.vy", output_formats=["interface"])["interface"]

assert "# Flags" in out
assert "flag Foo:" in out
assert "flag BAR" in out
assert "BOO" in out
assert "MOO" in out

compile_code(out, contract_path="code.vyi", output_formats=["interface"])


vyi_filenames = [
"test__test.vyi",
"test__t.vyi",
"t__test.vyi",
"t__t.vyi",
"t_t.vyi",
"test_test.vyi",
"t_test.vyi",
"test_t.vyi",
"_test_t__t_tt_.vyi",
"foo_bar_baz.vyi",
]


@pytest.mark.parametrize("vyi_filename", vyi_filenames)
def test_external_interface_names(vyi_filename):
code = """
@external
def foo():
...
"""

compile_code(code, contract_path=vyi_filename, output_formats=["external_interface"])


def test_external_interface_with_flag():
code = """
flag Foo:
Blah

@external
def foo() -> Foo:
...
"""

out = compile_code(code, contract_path="test__test.vyi", output_formats=["external_interface"])[
"external_interface"
]
assert "-> Foo:" in out
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe we should also check that the output compiles again (similar to a round trip)

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've added that check for primitive types, for external interface it does not work with user defined ones.



def test_external_interface_compiles_again():
code = """
@external
def foo() -> uint256:
...
@external
def bar(a:int32) -> uint256:
...
"""

out = compile_code(code, contract_path="test.vyi", output_formats=["external_interface"])[
"external_interface"
]
compile_code(out, contract_path="test.vyi", output_formats=["external_interface"])


@pytest.mark.xfail
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

let's please link: #4290 (comment)

def test_weird_interface_name():
# based on comment https://github.com/vyperlang/vyper/pull/4290#discussion_r1884137428
# we replace "_" for "" which results in an interface without name
out = compile_code("", contract_path="_.vyi", output_formats=["external_interface"])[
"external_interface"
]
assert "interface _:" in out
2 changes: 2 additions & 0 deletions tests/unit/ast/test_ast_dict.py
Original file line number Diff line number Diff line change
Expand Up @@ -399,6 +399,7 @@ def foo():
"node_id": 0,
"path": "main.vy",
"source_id": 1,
"is_interface": False,
"type": {
"name": "main.vy",
"type_decl_node": {"node_id": 0, "source_id": 1},
Expand Down Expand Up @@ -1175,6 +1176,7 @@ def foo():
"node_id": 0,
"path": "lib1.vy",
"source_id": 0,
"is_interface": False,
"type": {
"name": "lib1.vy",
"type_decl_node": {"node_id": 0, "source_id": 0},
Expand Down
29 changes: 29 additions & 0 deletions tests/unit/cli/vyper_compile/test_compile_files.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

from vyper.cli.vyper_compile import compile_files
from vyper.cli.vyper_json import compile_json
from vyper.compiler import INTERFACE_OUTPUT_FORMATS, OUTPUT_FORMATS
from vyper.compiler.input_bundle import FilesystemInputBundle
from vyper.compiler.output_bundle import OutputBundle
from vyper.compiler.phases import CompilerData
Expand Down Expand Up @@ -425,3 +426,31 @@ def test_archive_search_path(tmp_path_factory, make_file, chdir_tmp_path):

used_dir = search_paths[-1].stem # either dir1 or dir2
assert output_bundle.used_search_paths == [".", "0/" + used_dir]


def test_compile_interface_file(make_file):
interface = """
@view
@external
def foo() -> String[1]:
...

@view
@external
def bar() -> String[1]:
...

@external
def baz() -> uint8:
...

"""
file = make_file("interface.vyi", interface)
compile_files([file], INTERFACE_OUTPUT_FORMATS)

# check unallowed output formats
for f in OUTPUT_FORMATS:
charles-cooper marked this conversation as resolved.
Show resolved Hide resolved
if f in INTERFACE_OUTPUT_FORMATS:
continue
with pytest.raises(ValueError):
compile_files([file], [f])
2 changes: 1 addition & 1 deletion vyper/ast/nodes.py
Original file line number Diff line number Diff line change
Expand Up @@ -638,7 +638,7 @@ class TopLevel(VyperNode):

class Module(TopLevel):
# metadata
__slots__ = ("path", "resolved_path", "source_id")
__slots__ = ("path", "resolved_path", "source_id", "is_interface")

def to_dict(self):
return dict(source_sha256sum=self.source_sha256sum, **super().to_dict())
Expand Down
1 change: 1 addition & 0 deletions vyper/ast/nodes.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ class Module(TopLevel):
path: str = ...
resolved_path: str = ...
source_id: int = ...
is_interface: bool = ...
def namespace(self) -> Any: ... # context manager

class FunctionDef(TopLevel):
Expand Down
8 changes: 7 additions & 1 deletion vyper/ast/parse.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,11 @@ def parse_to_ast_with_settings(
module_path: Optional[str] = None,
resolved_path: Optional[str] = None,
add_fn_node: Optional[str] = None,
is_interface: bool = False,
) -> tuple[Settings, vy_ast.Module]:
try:
return _parse_to_ast_with_settings(
vyper_source, source_id, module_path, resolved_path, add_fn_node
vyper_source, source_id, module_path, resolved_path, add_fn_node, is_interface
)
except SyntaxException as e:
e.resolved_path = resolved_path
Expand All @@ -39,6 +40,7 @@ def _parse_to_ast_with_settings(
module_path: Optional[str] = None,
resolved_path: Optional[str] = None,
add_fn_node: Optional[str] = None,
is_interface: bool = False,
) -> tuple[Settings, vy_ast.Module]:
"""
Parses a Vyper source string and generates basic Vyper AST nodes.
Expand All @@ -62,6 +64,9 @@ def _parse_to_ast_with_settings(
resolved_path: str, optional
The resolved path of the source code
Corresponds to FileInput.resolved_path
is_interface: bool
Indicates whether the source code should
be parsed as an interface file.

Returns
-------
Expand Down Expand Up @@ -106,6 +111,7 @@ def _parse_to_ast_with_settings(
# Convert to Vyper AST.
module = vy_ast.get_node(py_ast)
assert isinstance(module, vy_ast.Module) # mypy hint
module.is_interface = is_interface

return pre_parser.settings, module

Expand Down
15 changes: 15 additions & 0 deletions vyper/compiler/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,13 @@
"opcodes_runtime": output.build_opcodes_runtime_output,
}

INTERFACE_OUTPUT_FORMATS = [
"ast_dict",
"annotated_ast_dict",
"interface",
"external_interface",
"abi",
]

UNKNOWN_CONTRACT_NAME = "<unknown>"

Expand Down Expand Up @@ -121,10 +128,18 @@ def outputs_from_compiler_data(
output_formats = ("bytecode",)

ret = {}

with anchor_settings(compiler_data.settings):
for output_format in output_formats:
if output_format not in OUTPUT_FORMATS:
raise ValueError(f"Unsupported format type {repr(output_format)}")

is_vyi = compiler_data.file_input.resolved_path.suffix == ".vyi"
charles-cooper marked this conversation as resolved.
Show resolved Hide resolved
if is_vyi and output_format not in INTERFACE_OUTPUT_FORMATS:
raise ValueError(
f"Unsupported format for compiling interface: {repr(output_format)}"
)

try:
formatter = OUTPUT_FORMATS[output_format]
ret[output_format] = formatter(compiler_data)
Expand Down
16 changes: 12 additions & 4 deletions vyper/compiler/output.py
Original file line number Diff line number Diff line change
Expand Up @@ -108,9 +108,8 @@ def build_integrity(compiler_data: CompilerData) -> str:
def build_external_interface_output(compiler_data: CompilerData) -> str:
interface = compiler_data.annotated_vyper_module._metadata["type"].interface
stem = PurePath(compiler_data.contract_path).stem
# capitalize words separated by '_'
# ex: test_interface.vy -> TestInterface
name = "".join([x.capitalize() for x in stem.split("_")])

name = stem.title().replace("_", "")
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what about _.vyi

out = f"\n# External Interfaces\ninterface {name}:\n"

for func in interface.functions.values():
Expand All @@ -136,6 +135,14 @@ def build_interface_output(compiler_data: CompilerData) -> str:
out += f" {member_name}: {member_type}\n"
out += "\n\n"

if len(interface.flags) > 0:
out += "# Flags\n\n"
for flag in interface.flags.values():
out += f"flag {flag.name}:\n"
for flag_value in flag._flag_members:
out += f" {flag_value}\n"
out += "\n\n"
charles-cooper marked this conversation as resolved.
Show resolved Hide resolved

if len(interface.events) > 0:
out += "# Events\n\n"
for event in interface.events.values():
Expand Down Expand Up @@ -282,7 +289,8 @@ def build_method_identifiers_output(compiler_data: CompilerData) -> dict:

def build_abi_output(compiler_data: CompilerData) -> list:
module_t = compiler_data.annotated_vyper_module._metadata["type"]
_ = compiler_data.ir_runtime # ensure _ir_info is generated
if not compiler_data.annotated_vyper_module.is_interface:
_ = compiler_data.ir_runtime # ensure _ir_info is generated

abi = module_t.interface.to_toplevel_abi_dict()
if module_t.init_function:
Expand Down
3 changes: 3 additions & 0 deletions vyper/compiler/phases.py
Original file line number Diff line number Diff line change
Expand Up @@ -113,11 +113,14 @@ def contract_path(self):

@cached_property
def _generate_ast(self):
is_vyi = self.contract_path.suffix == ".vyi"

settings, ast = vy_ast.parse_to_ast_with_settings(
self.source_code,
self.source_id,
module_path=self.contract_path.as_posix(),
resolved_path=self.file_input.resolved_path.as_posix(),
is_interface=is_vyi,
)

if self.original_settings:
Expand Down
2 changes: 1 addition & 1 deletion vyper/semantics/analysis/module.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ def analyze_module(module_ast: vy_ast.Module) -> ModuleT:
add all module-level objects to the namespace, type-check/validate
semantics and annotate with type and analysis info
"""
return _analyze_module_r(module_ast)
return _analyze_module_r(module_ast, module_ast.is_interface)


def _analyze_module_r(module_ast: vy_ast.Module, is_interface: bool = False):
Expand Down
3 changes: 3 additions & 0 deletions vyper/semantics/types/user.py
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,9 @@ def get_type_member(self, key: str, node: vy_ast.VyperNode) -> "VyperType":
self._helper.get_member(key, node)
return self

def __str__(self):
return f"{self.name}"

def __repr__(self):
arg_types = ",".join(repr(a) for a in self._flag_members)
return f"flag {self.name}({arg_types})"
Expand Down
Loading