diff --git a/.github/ISSUE_TEMPLATE/bug.md b/.github/ISSUE_TEMPLATE/bug.md index a48e240a23..88f418fff4 100644 --- a/.github/ISSUE_TEMPLATE/bug.md +++ b/.github/ISSUE_TEMPLATE/bug.md @@ -1,6 +1,7 @@ --- name: Bug Report about: Any general feedback or bug reports about the Vyper Compiler. No new features proposals. +labels: ["needs triage"] --- ### Version Information diff --git a/.github/ISSUE_TEMPLATE/vip.md b/.github/ISSUE_TEMPLATE/vip.md index b35a1e7c23..d32a8ac3de 100644 --- a/.github/ISSUE_TEMPLATE/vip.md +++ b/.github/ISSUE_TEMPLATE/vip.md @@ -1,6 +1,7 @@ --- name: Vyper Improvement Proposal (VIP) about: This is the suggested template for new VIPs. +labels: ["needs triage"] --- ## Simple Summary "If you can't explain it simply, you don't understand it well enough." Provide a simplified and layman-accessible explanation of the VIP. diff --git a/docs/types.rst b/docs/types.rst index 752e06b14f..807c83848f 100644 --- a/docs/types.rst +++ b/docs/types.rst @@ -359,11 +359,12 @@ A byte array with a max size. The syntax being ``Bytes[maxLen]``, where ``maxLen`` is an integer which denotes the maximum number of bytes. On the ABI level the Fixed-size bytes array is annotated as ``bytes``. -Bytes literals may be given as bytes strings. +Bytes literals may be given as bytes strings or as hex strings. .. code-block:: vyper bytes_string: Bytes[100] = b"\x01" + bytes_string: Bytes[100] = x"01" .. index:: !string diff --git a/tests/conftest.py b/tests/conftest.py index 31c72246bd..76ebc2df22 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -14,7 +14,7 @@ from tests.utils import working_directory from vyper import compiler from vyper.codegen.ir_node import IRnode -from vyper.compiler.input_bundle import FilesystemInputBundle, InputBundle +from vyper.compiler.input_bundle import FilesystemInputBundle from vyper.compiler.settings import OptimizationLevel, Settings, set_global_settings from vyper.exceptions import EvmVersionException from vyper.ir import compile_ir, optimizer @@ -166,12 +166,6 @@ def fn(sources_dict): return fn -# for tests which just need an input bundle, doesn't matter what it is -@pytest.fixture -def dummy_input_bundle(): - return InputBundle([]) - - @pytest.fixture(scope="module") def gas_limit(): # set absurdly high gas limit so that london basefee never adjusts diff --git a/tests/functional/codegen/features/test_clampers.py b/tests/functional/codegen/features/test_clampers.py index 1adffcf29a..b82a771962 100644 --- a/tests/functional/codegen/features/test_clampers.py +++ b/tests/functional/codegen/features/test_clampers.py @@ -429,7 +429,7 @@ def foo(b: int128[6][1][2]) -> int128[6][1][2]: c = get_contract(code) with tx_failed(): - _make_tx(env, c.address, "foo(int128[6][1][2]])", values) + _make_tx(env, c.address, "foo(int128[6][1][2])", values) @pytest.mark.parametrize("value", [0, 1, -1, 2**127 - 1, -(2**127)]) @@ -453,7 +453,7 @@ def test_int128_dynarray_clamper_failing(env, tx_failed, get_contract, bad_value # ensure the invalid value is detected at all locations in the array code = """ @external -def foo(b: int128[5]) -> int128[5]: +def foo(b: DynArray[int128, 5]) -> DynArray[int128, 5]: return b """ diff --git a/tests/functional/codegen/modules/test_interface_imports.py b/tests/functional/codegen/modules/test_interface_imports.py index c0fae6496c..3f0f8cb010 100644 --- a/tests/functional/codegen/modules/test_interface_imports.py +++ b/tests/functional/codegen/modules/test_interface_imports.py @@ -58,3 +58,31 @@ def foo() -> bool: c = get_contract(main, input_bundle=input_bundle) assert c.foo() is True + + +def test_import_interface_flags(make_input_bundle, get_contract): + ifaces = """ +flag Foo: + BOO + MOO + POO + +interface IFoo: + def foo() -> Foo: nonpayable + """ + + contract = """ +import ifaces + +implements: ifaces + +@external +def foo() -> ifaces.Foo: + return ifaces.Foo.POO + """ + + input_bundle = make_input_bundle({"ifaces.vyi": ifaces}) + + c = get_contract(contract, input_bundle=input_bundle) + + assert c.foo() == 4 diff --git a/tests/functional/codegen/test_interfaces.py b/tests/functional/codegen/test_interfaces.py index 9ea0b58d89..8887bf07cb 100644 --- a/tests/functional/codegen/test_interfaces.py +++ b/tests/functional/codegen/test_interfaces.py @@ -13,6 +13,7 @@ ) +# TODO CMC 2024-10-13: this should probably be in tests/unit/compiler/ def test_basic_extract_interface(): code = """ # Events @@ -22,6 +23,7 @@ def test_basic_extract_interface(): _to: address _value: uint256 + # Functions @view @@ -37,6 +39,7 @@ def allowance(_owner: address, _spender: address) -> (uint256, uint256): assert code_pass.strip() == out.strip() +# TODO CMC 2024-10-13: this should probably be in tests/unit/compiler/ def test_basic_extract_external_interface(): code = """ @view @@ -68,6 +71,7 @@ def test(_owner: address): nonpayable assert interface.strip() == out.strip() +# TODO CMC 2024-10-13: should probably be in syntax tests def test_basic_interface_implements(assert_compile_failed): code = """ from ethereum.ercs import IERC20 @@ -82,6 +86,7 @@ def test() -> bool: assert_compile_failed(lambda: compile_code(code), InterfaceViolation) +# TODO CMC 2024-10-13: should probably be in syntax tests def test_external_interface_parsing(make_input_bundle, assert_compile_failed): interface_code = """ @external @@ -126,6 +131,7 @@ def foo() -> uint256: compile_code(not_implemented_code, input_bundle=input_bundle) +# TODO CMC 2024-10-13: should probably be in syntax tests def test_log_interface_event(make_input_bundle, assert_compile_failed): interface_code = """ event Foo: @@ -160,6 +166,7 @@ def bar() -> uint256: ] +# TODO CMC 2024-10-13: should probably be in syntax tests @pytest.mark.parametrize("code,filename", VALID_IMPORT_CODE) def test_extract_file_interface_imports(code, filename, make_input_bundle): input_bundle = make_input_bundle({filename: ""}) @@ -177,6 +184,7 @@ def test_extract_file_interface_imports(code, filename, make_input_bundle): ] +# TODO CMC 2024-10-13: should probably be in syntax tests @pytest.mark.parametrize("code,exception_type", BAD_IMPORT_CODE) def test_extract_file_interface_imports_raises( code, exception_type, assert_compile_failed, make_input_bundle @@ -726,3 +734,43 @@ def bar() -> uint256: c = get_contract(code, input_bundle=input_bundle) assert c.foo() == c.bar() == 1 + + +def test_interface_with_structures(): + code = """ +struct MyStruct: + a: address + b: uint256 + +event Transfer: + sender: indexed(address) + receiver: indexed(address) + value: uint256 + +struct Voter: + weight: int128 + voted: bool + delegate: address + vote: int128 + +@external +def bar(): + pass + +event Buy: + buyer: indexed(address) + buy_order: uint256 + +@external +@view +def foo(s: MyStruct) -> MyStruct: + return s + """ + + out = compile_code(code, contract_path="code.vy", output_formats=["interface"])["interface"] + + assert "# Structs" in out + assert "struct MyStruct:" in out + assert "b: uint256" in out + assert "struct Voter:" in out + assert "voted: bool" in out diff --git a/tests/functional/codegen/types/numbers/test_decimals.py b/tests/functional/codegen/types/numbers/test_decimals.py index 36c14f804d..ad8bf74b0d 100644 --- a/tests/functional/codegen/types/numbers/test_decimals.py +++ b/tests/functional/codegen/types/numbers/test_decimals.py @@ -299,7 +299,7 @@ def foo(): compile_code(code) -def test_replace_decimal_nested_intermediate_underflow(dummy_input_bundle): +def test_replace_decimal_nested_intermediate_underflow(): code = """ @external def foo(): diff --git a/tests/functional/codegen/types/test_bytes.py b/tests/functional/codegen/types/test_bytes.py index a5b119f143..6473be4348 100644 --- a/tests/functional/codegen/types/test_bytes.py +++ b/tests/functional/codegen/types/test_bytes.py @@ -259,6 +259,28 @@ def test2(l: bytes{m} = {vyper_literal}) -> bool: assert c.test2(vyper_literal) is True +@pytest.mark.parametrize("m,val", [(2, "ab"), (3, "ab"), (4, "abcd")]) +def test_native_hex_literals(get_contract, m, val): + vyper_literal = bytes.fromhex(val) + code = f""" +@external +def test() -> bool: + l: Bytes[{m}] = x"{val}" + return l == {vyper_literal} + +@external +def test2(l: Bytes[{m}] = x"{val}") -> bool: + return l == {vyper_literal} + """ + print(code) + + c = get_contract(code) + + assert c.test() is True + assert c.test2() is True + assert c.test2(vyper_literal) is True + + def test_zero_padding_with_private(get_contract): code = """ counter: uint256 diff --git a/tests/functional/grammar/test_grammar.py b/tests/functional/grammar/test_grammar.py index 2af5385b3d..0ff8c23477 100644 --- a/tests/functional/grammar/test_grammar.py +++ b/tests/functional/grammar/test_grammar.py @@ -102,6 +102,6 @@ def has_no_docstrings(c): max_examples=500, suppress_health_check=[HealthCheck.too_slow, HealthCheck.filter_too_much] ) def test_grammar_bruteforce(code): - _, _, _, reformatted_code = pre_parse(code + "\n") - tree = parse_to_ast(reformatted_code) + pre_parse_result = pre_parse(code + "\n") + tree = parse_to_ast(pre_parse_result.reformatted_code) assert isinstance(tree, Module) diff --git a/tests/functional/syntax/test_bytes.py b/tests/functional/syntax/test_bytes.py index 0ca3b27fee..9df2962f2e 100644 --- a/tests/functional/syntax/test_bytes.py +++ b/tests/functional/syntax/test_bytes.py @@ -80,6 +80,15 @@ def test() -> Bytes[1]: ( """ @external +def test() -> Bytes[2]: + a: Bytes[2] = x"abc" + return a + """, + SyntaxException, + ), + ( + """ +@external def foo(): a: Bytes = b"abc" """, diff --git a/tests/functional/syntax/test_interfaces.py b/tests/functional/syntax/test_interfaces.py index 20813c48d1..86ea4bcfd0 100644 --- a/tests/functional/syntax/test_interfaces.py +++ b/tests/functional/syntax/test_interfaces.py @@ -381,13 +381,22 @@ def test_interfaces_success(good_code): def test_imports_and_implements_within_interface(make_input_bundle): - interface_code = """ + ibar_code = """ @external def foobar(): ... """ + ifoo_code = """ +import bar - input_bundle = make_input_bundle({"foo.vyi": interface_code}) +implements: bar + +@external +def foobar(): + ... +""" + + input_bundle = make_input_bundle({"foo.vyi": ifoo_code, "bar.vyi": ibar_code}) code = """ import foo as Foo diff --git a/tests/unit/ast/nodes/test_hex.py b/tests/unit/ast/nodes/test_hex.py index 7168defa99..6d82b1d2ab 100644 --- a/tests/unit/ast/nodes/test_hex.py +++ b/tests/unit/ast/nodes/test_hex.py @@ -40,7 +40,7 @@ def foo(): @pytest.mark.parametrize("code", code_invalid_checksum) -def test_invalid_checksum(code, dummy_input_bundle): +def test_invalid_checksum(code): with pytest.raises(InvalidLiteral): vyper_module = vy_ast.parse_to_ast(code) - semantics.analyze_module(vyper_module, dummy_input_bundle) + semantics.analyze_module(vyper_module) diff --git a/tests/unit/ast/test_annotate_and_optimize_ast.py b/tests/unit/ast/test_annotate_and_optimize_ast.py index 7e1641e49e..39ea899bd9 100644 --- a/tests/unit/ast/test_annotate_and_optimize_ast.py +++ b/tests/unit/ast/test_annotate_and_optimize_ast.py @@ -28,12 +28,12 @@ def foo() -> int128: def get_contract_info(source_code): - _, loop_var_annotations, class_types, reformatted_code = pre_parse(source_code) - py_ast = python_ast.parse(reformatted_code) + pre_parse_result = pre_parse(source_code) + py_ast = python_ast.parse(pre_parse_result.reformatted_code) - annotate_python_ast(py_ast, reformatted_code, loop_var_annotations, class_types) + annotate_python_ast(py_ast, pre_parse_result.reformatted_code, pre_parse_result) - return py_ast, reformatted_code + return py_ast, pre_parse_result.reformatted_code def test_it_annotates_ast_with_source_code(): diff --git a/tests/unit/ast/test_ast_dict.py b/tests/unit/ast/test_ast_dict.py index 07da3c0ace..c9d7248809 100644 --- a/tests/unit/ast/test_ast_dict.py +++ b/tests/unit/ast/test_ast_dict.py @@ -1,3 +1,4 @@ +import copy import json from vyper import compiler @@ -216,24 +217,27 @@ def foo(): input_bundle = make_input_bundle({"lib1.vy": lib1, "main.vy": main}) lib1_file = input_bundle.load_file("lib1.vy") - out = compiler.compile_from_file_input( + lib1_out = compiler.compile_from_file_input( lib1_file, input_bundle=input_bundle, output_formats=["annotated_ast_dict"] ) - lib1_ast = out["annotated_ast_dict"]["ast"] + + lib1_ast = copy.deepcopy(lib1_out["annotated_ast_dict"]["ast"]) lib1_sha256sum = lib1_ast.pop("source_sha256sum") assert lib1_sha256sum == lib1_file.sha256sum to_strip = NODE_SRC_ATTRIBUTES + ("resolved_path", "variable_reads", "variable_writes") _strip_source_annotations(lib1_ast, to_strip=to_strip) main_file = input_bundle.load_file("main.vy") - out = compiler.compile_from_file_input( + main_out = compiler.compile_from_file_input( main_file, input_bundle=input_bundle, output_formats=["annotated_ast_dict"] ) - main_ast = out["annotated_ast_dict"]["ast"] + main_ast = main_out["annotated_ast_dict"]["ast"] main_sha256sum = main_ast.pop("source_sha256sum") assert main_sha256sum == main_file.sha256sum _strip_source_annotations(main_ast, to_strip=to_strip) + assert main_out["annotated_ast_dict"]["imports"][0] == lib1_out["annotated_ast_dict"]["ast"] + # TODO: would be nice to refactor this into bunch of small test cases assert main_ast == { "ast_type": "Module", @@ -1776,3 +1780,49 @@ def qux2(): }, } ] + + +def test_annotated_ast_export_recursion(make_input_bundle): + sources = { + "main.vy": """ +import lib1 + +@external +def foo(): + lib1.foo() + """, + "lib1.vy": """ +import lib2 + +def foo(): + lib2.foo() + """, + "lib2.vy": """ +def foo(): + pass + """, + } + + input_bundle = make_input_bundle(sources) + + def compile_and_get_ast(file_name): + file = input_bundle.load_file(file_name) + output = compiler.compile_from_file_input( + file, input_bundle=input_bundle, output_formats=["annotated_ast_dict"] + ) + return output["annotated_ast_dict"] + + lib1_ast = compile_and_get_ast("lib1.vy")["ast"] + lib2_ast = compile_and_get_ast("lib2.vy")["ast"] + main_out = compile_and_get_ast("main.vy") + + lib1_import_ast = main_out["imports"][1] + lib2_import_ast = main_out["imports"][0] + + # path is once virtual, once libX.vy + # type contains name which is based on path + keys = [s for s in lib1_import_ast.keys() if s not in {"path", "type"}] + + for key in keys: + assert lib1_ast[key] == lib1_import_ast[key] + assert lib2_ast[key] == lib2_import_ast[key] diff --git a/tests/unit/ast/test_pre_parser.py b/tests/unit/ast/test_pre_parser.py index da7d72b8ec..4190725f7e 100644 --- a/tests/unit/ast/test_pre_parser.py +++ b/tests/unit/ast/test_pre_parser.py @@ -174,9 +174,9 @@ def test_prerelease_invalid_version_pragma(file_version, mock_version): @pytest.mark.parametrize("code, pre_parse_settings, compiler_data_settings", pragma_examples) def test_parse_pragmas(code, pre_parse_settings, compiler_data_settings, mock_version): mock_version("0.3.10") - settings, _, _, _ = pre_parse(code) + pre_parse_result = pre_parse(code) - assert settings == pre_parse_settings + assert pre_parse_result.settings == pre_parse_settings compiler_data = CompilerData(code) diff --git a/tests/unit/cli/vyper_json/test_compile_json.py b/tests/unit/cli/vyper_json/test_compile_json.py index ef3284cd15..5da98cf20f 100644 --- a/tests/unit/cli/vyper_json/test_compile_json.py +++ b/tests/unit/cli/vyper_json/test_compile_json.py @@ -295,3 +295,27 @@ def test_relative_import_paths(input_json): input_json["sources"]["contracts/potato/baz/potato.vy"] = {"content": "from . import baz"} input_json["sources"]["contracts/potato/footato.vy"] = {"content": "from baz import baz"} compile_from_input_dict(input_json) + + +def test_compile_json_with_abi_top(make_input_bundle): + stream = """ +{ + "abi": [ + { + "name": "validate", + "inputs": [ + { "name": "creator", "type": "address" }, + { "name": "token", "type": "address" }, + { "name": "amount_per_second", "type": "uint256" }, + { "name": "reason", "type": "bytes" } + ], + "outputs": [{ "name": "max_stream_life", "type": "uint256" }] + } + ] +} + """ + code = """ +from . import stream + """ + input_bundle = make_input_bundle({"stream.json": stream, "code.vy": code}) + vyper.compiler.compile_code(code, input_bundle=input_bundle) diff --git a/tests/unit/compiler/venom/test_algebraic_optimizer.py b/tests/unit/compiler/venom/test_algebraic_optimizer.py index b5d55efbdc..39008649ea 100644 --- a/tests/unit/compiler/venom/test_algebraic_optimizer.py +++ b/tests/unit/compiler/venom/test_algebraic_optimizer.py @@ -1,11 +1,9 @@ import pytest -from vyper.venom.analysis.analysis import IRAnalysesCache +from vyper.venom.analysis import IRAnalysesCache from vyper.venom.basicblock import IRBasicBlock, IRLabel from vyper.venom.context import IRContext -from vyper.venom.passes.algebraic_optimization import AlgebraicOptimizationPass -from vyper.venom.passes.make_ssa import MakeSSA -from vyper.venom.passes.remove_unused_variables import RemoveUnusedVariablesPass +from vyper.venom.passes import AlgebraicOptimizationPass, MakeSSA, RemoveUnusedVariablesPass @pytest.mark.parametrize("iszero_count", range(5)) diff --git a/tests/unit/compiler/venom/test_branch_optimizer.py b/tests/unit/compiler/venom/test_branch_optimizer.py index b6e806e217..82dff4777d 100644 --- a/tests/unit/compiler/venom/test_branch_optimizer.py +++ b/tests/unit/compiler/venom/test_branch_optimizer.py @@ -1,9 +1,7 @@ -from vyper.venom.analysis.analysis import IRAnalysesCache -from vyper.venom.analysis.dfg import DFGAnalysis +from vyper.venom.analysis import DFGAnalysis, IRAnalysesCache from vyper.venom.basicblock import IRBasicBlock, IRLabel from vyper.venom.context import IRContext -from vyper.venom.passes.branch_optimization import BranchOptimizationPass -from vyper.venom.passes.make_ssa import MakeSSA +from vyper.venom.passes import BranchOptimizationPass, MakeSSA def test_simple_jump_case(): diff --git a/tests/unit/compiler/venom/test_dominator_tree.py b/tests/unit/compiler/venom/test_dominator_tree.py index 29f86df221..30a2e4564e 100644 --- a/tests/unit/compiler/venom/test_dominator_tree.py +++ b/tests/unit/compiler/venom/test_dominator_tree.py @@ -2,12 +2,11 @@ from vyper.exceptions import CompilerPanic from vyper.utils import OrderedSet -from vyper.venom.analysis.analysis import IRAnalysesCache -from vyper.venom.analysis.dominators import DominatorTreeAnalysis +from vyper.venom.analysis import DominatorTreeAnalysis, IRAnalysesCache from vyper.venom.basicblock import IRBasicBlock, IRInstruction, IRLabel, IRLiteral, IRVariable from vyper.venom.context import IRContext from vyper.venom.function import IRFunction -from vyper.venom.passes.make_ssa import MakeSSA +from vyper.venom.passes import MakeSSA def _add_bb( diff --git a/tests/unit/compiler/venom/test_duplicate_operands.py b/tests/unit/compiler/venom/test_duplicate_operands.py index ab55649dae..89b06796e3 100644 --- a/tests/unit/compiler/venom/test_duplicate_operands.py +++ b/tests/unit/compiler/venom/test_duplicate_operands.py @@ -1,8 +1,8 @@ from vyper.compiler.settings import OptimizationLevel from vyper.venom import generate_assembly_experimental -from vyper.venom.analysis.analysis import IRAnalysesCache +from vyper.venom.analysis import IRAnalysesCache from vyper.venom.context import IRContext -from vyper.venom.passes.store_expansion import StoreExpansionPass +from vyper.venom.passes import StoreExpansionPass def test_duplicate_operands(): diff --git a/tests/unit/compiler/venom/test_make_ssa.py b/tests/unit/compiler/venom/test_make_ssa.py index 9cea1a20a4..aa3fead6bf 100644 --- a/tests/unit/compiler/venom/test_make_ssa.py +++ b/tests/unit/compiler/venom/test_make_ssa.py @@ -1,7 +1,7 @@ -from vyper.venom.analysis.analysis import IRAnalysesCache +from vyper.venom.analysis import IRAnalysesCache from vyper.venom.basicblock import IRBasicBlock, IRLabel from vyper.venom.context import IRContext -from vyper.venom.passes.make_ssa import MakeSSA +from vyper.venom.passes import MakeSSA def test_phi_case(): diff --git a/tests/unit/compiler/venom/test_multi_entry_block.py b/tests/unit/compiler/venom/test_multi_entry_block.py index 313fbb3ebd..a38e4b4158 100644 --- a/tests/unit/compiler/venom/test_multi_entry_block.py +++ b/tests/unit/compiler/venom/test_multi_entry_block.py @@ -1,8 +1,7 @@ -from vyper.venom.analysis.analysis import IRAnalysesCache -from vyper.venom.analysis.cfg import CFGAnalysis +from vyper.venom.analysis import CFGAnalysis, IRAnalysesCache from vyper.venom.context import IRContext from vyper.venom.function import IRBasicBlock, IRLabel -from vyper.venom.passes.normalization import NormalizationPass +from vyper.venom.passes import NormalizationPass def test_multi_entry_block_1(): diff --git a/tests/unit/compiler/venom/test_sccp.py b/tests/unit/compiler/venom/test_sccp.py index 478acc1079..375dfd5dac 100644 --- a/tests/unit/compiler/venom/test_sccp.py +++ b/tests/unit/compiler/venom/test_sccp.py @@ -1,11 +1,10 @@ import pytest from vyper.exceptions import StaticAssertionException -from vyper.venom.analysis.analysis import IRAnalysesCache +from vyper.venom.analysis import IRAnalysesCache from vyper.venom.basicblock import IRBasicBlock, IRLabel, IRLiteral, IRVariable from vyper.venom.context import IRContext -from vyper.venom.passes.make_ssa import MakeSSA -from vyper.venom.passes.sccp import SCCP +from vyper.venom.passes import SCCP, MakeSSA from vyper.venom.passes.sccp.sccp import LatticeEnum diff --git a/tests/unit/compiler/venom/test_simplify_cfg.py b/tests/unit/compiler/venom/test_simplify_cfg.py index c4bdbb263b..3de6a77cc9 100644 --- a/tests/unit/compiler/venom/test_simplify_cfg.py +++ b/tests/unit/compiler/venom/test_simplify_cfg.py @@ -1,8 +1,7 @@ -from vyper.venom.analysis.analysis import IRAnalysesCache +from vyper.venom.analysis import IRAnalysesCache from vyper.venom.basicblock import IRBasicBlock, IRLabel, IRLiteral from vyper.venom.context import IRContext -from vyper.venom.passes.sccp import SCCP -from vyper.venom.passes.simplify_cfg import SimplifyCFGPass +from vyper.venom.passes import SCCP, SimplifyCFGPass def test_phi_reduction_after_block_pruning(): diff --git a/tests/unit/compiler/venom/test_stack_reorder.py b/tests/unit/compiler/venom/test_stack_reorder.py index a15dd4d540..8f38e00cdb 100644 --- a/tests/unit/compiler/venom/test_stack_reorder.py +++ b/tests/unit/compiler/venom/test_stack_reorder.py @@ -1,7 +1,7 @@ from vyper.venom import generate_assembly_experimental -from vyper.venom.analysis.analysis import IRAnalysesCache +from vyper.venom.analysis import IRAnalysesCache from vyper.venom.context import IRContext -from vyper.venom.passes.store_expansion import StoreExpansionPass +from vyper.venom.passes import StoreExpansionPass def test_stack_reorder(): diff --git a/tests/unit/semantics/analysis/test_array_index.py b/tests/unit/semantics/analysis/test_array_index.py index b5bf86494d..aa9a702be3 100644 --- a/tests/unit/semantics/analysis/test_array_index.py +++ b/tests/unit/semantics/analysis/test_array_index.py @@ -11,7 +11,7 @@ @pytest.mark.parametrize("value", ["address", "Bytes[10]", "decimal", "bool"]) -def test_type_mismatch(namespace, value, dummy_input_bundle): +def test_type_mismatch(namespace, value): code = f""" a: uint256[3] @@ -22,11 +22,11 @@ def foo(b: {value}): """ vyper_module = parse_to_ast(code) with pytest.raises(TypeMismatch): - analyze_module(vyper_module, dummy_input_bundle) + analyze_module(vyper_module) @pytest.mark.parametrize("value", ["1.0", "0.0", "'foo'", "0x00", "b'\x01'", "False"]) -def test_invalid_literal(namespace, value, dummy_input_bundle): +def test_invalid_literal(namespace, value): code = f""" a: uint256[3] @@ -37,11 +37,11 @@ def foo(): """ vyper_module = parse_to_ast(code) with pytest.raises(TypeMismatch): - analyze_module(vyper_module, dummy_input_bundle) + analyze_module(vyper_module) @pytest.mark.parametrize("value", [-1, 3, -(2**127), 2**127 - 1, 2**256 - 1]) -def test_out_of_bounds(namespace, value, dummy_input_bundle): +def test_out_of_bounds(namespace, value): code = f""" a: uint256[3] @@ -52,11 +52,11 @@ def foo(): """ vyper_module = parse_to_ast(code) with pytest.raises(ArrayIndexException): - analyze_module(vyper_module, dummy_input_bundle) + analyze_module(vyper_module) @pytest.mark.parametrize("value", ["b", "self.b"]) -def test_undeclared_definition(namespace, value, dummy_input_bundle): +def test_undeclared_definition(namespace, value): code = f""" a: uint256[3] @@ -67,11 +67,11 @@ def foo(): """ vyper_module = parse_to_ast(code) with pytest.raises(UndeclaredDefinition): - analyze_module(vyper_module, dummy_input_bundle) + analyze_module(vyper_module) @pytest.mark.parametrize("value", ["a", "foo", "int128"]) -def test_invalid_reference(namespace, value, dummy_input_bundle): +def test_invalid_reference(namespace, value): code = f""" a: uint256[3] @@ -82,4 +82,4 @@ def foo(): """ vyper_module = parse_to_ast(code) with pytest.raises(InvalidReference): - analyze_module(vyper_module, dummy_input_bundle) + analyze_module(vyper_module) diff --git a/tests/unit/semantics/analysis/test_cyclic_function_calls.py b/tests/unit/semantics/analysis/test_cyclic_function_calls.py index 406adc00ab..da2e63c5fc 100644 --- a/tests/unit/semantics/analysis/test_cyclic_function_calls.py +++ b/tests/unit/semantics/analysis/test_cyclic_function_calls.py @@ -5,7 +5,7 @@ from vyper.semantics.analysis import analyze_module -def test_self_function_call(dummy_input_bundle): +def test_self_function_call(): code = """ @internal def foo(): @@ -13,12 +13,12 @@ def foo(): """ vyper_module = parse_to_ast(code) with pytest.raises(CallViolation) as e: - analyze_module(vyper_module, dummy_input_bundle) + analyze_module(vyper_module) assert e.value.message == "Contract contains cyclic function call: foo -> foo" -def test_self_function_call2(dummy_input_bundle): +def test_self_function_call2(): code = """ @external def foo(): @@ -30,12 +30,12 @@ def bar(): """ vyper_module = parse_to_ast(code) with pytest.raises(CallViolation) as e: - analyze_module(vyper_module, dummy_input_bundle) + analyze_module(vyper_module) assert e.value.message == "Contract contains cyclic function call: foo -> bar -> bar" -def test_cyclic_function_call(dummy_input_bundle): +def test_cyclic_function_call(): code = """ @internal def foo(): @@ -47,12 +47,12 @@ def bar(): """ vyper_module = parse_to_ast(code) with pytest.raises(CallViolation) as e: - analyze_module(vyper_module, dummy_input_bundle) + analyze_module(vyper_module) assert e.value.message == "Contract contains cyclic function call: foo -> bar -> foo" -def test_multi_cyclic_function_call(dummy_input_bundle): +def test_multi_cyclic_function_call(): code = """ @internal def foo(): @@ -72,14 +72,14 @@ def potato(): """ vyper_module = parse_to_ast(code) with pytest.raises(CallViolation) as e: - analyze_module(vyper_module, dummy_input_bundle) + analyze_module(vyper_module) expected_message = "Contract contains cyclic function call: foo -> bar -> baz -> potato -> foo" assert e.value.message == expected_message -def test_multi_cyclic_function_call2(dummy_input_bundle): +def test_multi_cyclic_function_call2(): code = """ @internal def foo(): @@ -99,14 +99,14 @@ def potato(): """ vyper_module = parse_to_ast(code) with pytest.raises(CallViolation) as e: - analyze_module(vyper_module, dummy_input_bundle) + analyze_module(vyper_module) expected_message = "Contract contains cyclic function call: foo -> bar -> baz -> potato -> bar" assert e.value.message == expected_message -def test_global_ann_assign_callable_no_crash(dummy_input_bundle): +def test_global_ann_assign_callable_no_crash(): code = """ balanceOf: public(HashMap[address, uint256]) @@ -116,5 +116,5 @@ def foo(to : address): """ vyper_module = parse_to_ast(code) with pytest.raises(StructureException) as excinfo: - analyze_module(vyper_module, dummy_input_bundle) + analyze_module(vyper_module) assert excinfo.value.message == "HashMap[address, uint256] is not callable" diff --git a/tests/unit/semantics/analysis/test_for_loop.py b/tests/unit/semantics/analysis/test_for_loop.py index d7d4f7083b..810ff0a8b9 100644 --- a/tests/unit/semantics/analysis/test_for_loop.py +++ b/tests/unit/semantics/analysis/test_for_loop.py @@ -5,7 +5,7 @@ from vyper.semantics.analysis import analyze_module -def test_modify_iterator_function_outside_loop(dummy_input_bundle): +def test_modify_iterator_function_outside_loop(): code = """ a: uint256[3] @@ -21,10 +21,10 @@ def bar(): pass """ vyper_module = parse_to_ast(code) - analyze_module(vyper_module, dummy_input_bundle) + analyze_module(vyper_module) -def test_pass_memory_var_to_other_function(dummy_input_bundle): +def test_pass_memory_var_to_other_function(): code = """ @internal @@ -41,10 +41,10 @@ def bar(): self.foo(a) """ vyper_module = parse_to_ast(code) - analyze_module(vyper_module, dummy_input_bundle) + analyze_module(vyper_module) -def test_modify_iterator(dummy_input_bundle): +def test_modify_iterator(): code = """ a: uint256[3] @@ -56,10 +56,10 @@ def bar(): """ vyper_module = parse_to_ast(code) with pytest.raises(ImmutableViolation): - analyze_module(vyper_module, dummy_input_bundle) + analyze_module(vyper_module) -def test_bad_keywords(dummy_input_bundle): +def test_bad_keywords(): code = """ @internal @@ -70,10 +70,10 @@ def bar(n: uint256): """ vyper_module = parse_to_ast(code) with pytest.raises(ArgumentException): - analyze_module(vyper_module, dummy_input_bundle) + analyze_module(vyper_module) -def test_bad_bound(dummy_input_bundle): +def test_bad_bound(): code = """ @internal @@ -84,10 +84,10 @@ def bar(n: uint256): """ vyper_module = parse_to_ast(code) with pytest.raises(StructureException): - analyze_module(vyper_module, dummy_input_bundle) + analyze_module(vyper_module) -def test_modify_iterator_function_call(dummy_input_bundle): +def test_modify_iterator_function_call(): code = """ a: uint256[3] @@ -103,10 +103,10 @@ def bar(): """ vyper_module = parse_to_ast(code) with pytest.raises(ImmutableViolation): - analyze_module(vyper_module, dummy_input_bundle) + analyze_module(vyper_module) -def test_modify_iterator_recursive_function_call(dummy_input_bundle): +def test_modify_iterator_recursive_function_call(): code = """ a: uint256[3] @@ -126,10 +126,10 @@ def baz(): """ vyper_module = parse_to_ast(code) with pytest.raises(ImmutableViolation): - analyze_module(vyper_module, dummy_input_bundle) + analyze_module(vyper_module) -def test_modify_iterator_recursive_function_call_topsort(dummy_input_bundle): +def test_modify_iterator_recursive_function_call_topsort(): # test the analysis works no matter the order of functions code = """ a: uint256[3] @@ -149,12 +149,12 @@ def foo(): """ vyper_module = parse_to_ast(code) with pytest.raises(ImmutableViolation) as e: - analyze_module(vyper_module, dummy_input_bundle) + analyze_module(vyper_module) assert e.value._message == "Cannot modify loop variable `a`" -def test_modify_iterator_through_struct(dummy_input_bundle): +def test_modify_iterator_through_struct(): # GH issue 3429 code = """ struct A: @@ -170,12 +170,12 @@ def foo(): """ vyper_module = parse_to_ast(code) with pytest.raises(ImmutableViolation) as e: - analyze_module(vyper_module, dummy_input_bundle) + analyze_module(vyper_module) assert e.value._message == "Cannot modify loop variable `a`" -def test_modify_iterator_complex_expr(dummy_input_bundle): +def test_modify_iterator_complex_expr(): # GH issue 3429 # avoid false positive! code = """ @@ -189,10 +189,10 @@ def foo(): self.b[self.a[1]] = i """ vyper_module = parse_to_ast(code) - analyze_module(vyper_module, dummy_input_bundle) + analyze_module(vyper_module) -def test_modify_iterator_siblings(dummy_input_bundle): +def test_modify_iterator_siblings(): # test we can modify siblings in an access tree code = """ struct Foo: @@ -207,10 +207,10 @@ def foo(): self.f.b += i """ vyper_module = parse_to_ast(code) - analyze_module(vyper_module, dummy_input_bundle) + analyze_module(vyper_module) -def test_modify_subscript_barrier(dummy_input_bundle): +def test_modify_subscript_barrier(): # test that Subscript nodes are a barrier for analysis code = """ struct Foo: @@ -229,7 +229,7 @@ def foo(): """ vyper_module = parse_to_ast(code) with pytest.raises(ImmutableViolation) as e: - analyze_module(vyper_module, dummy_input_bundle) + analyze_module(vyper_module) assert e.value._message == "Cannot modify loop variable `b`" @@ -269,7 +269,7 @@ def foo(): @pytest.mark.parametrize("code", iterator_inference_codes) -def test_iterator_type_inference_checker(code, dummy_input_bundle): +def test_iterator_type_inference_checker(code): vyper_module = parse_to_ast(code) with pytest.raises(TypeMismatch): - analyze_module(vyper_module, dummy_input_bundle) + analyze_module(vyper_module) diff --git a/vyper/ast/grammar.lark b/vyper/ast/grammar.lark index 97f9f70e24..bc2f9ba77c 100644 --- a/vyper/ast/grammar.lark +++ b/vyper/ast/grammar.lark @@ -318,7 +318,7 @@ COMMENT: /#[^\n\r]*/ _NEWLINE: ( /\r?\n[\t ]*/ | COMMENT )+ -STRING: /b?("(?!"").*?(? Any: ... # context manager class FunctionDef(TopLevel): @@ -109,9 +110,7 @@ class ExprNode(VyperNode): class Constant(ExprNode): value: Any = ... -class Num(Constant): - @property - def n(self): ... +class Num(Constant): ... class Int(Num): value: int = ... @@ -122,14 +121,9 @@ class Hex(Num): @property def n_bytes(self): ... -class Str(Constant): - @property - def s(self): ... - -class Bytes(Constant): - @property - def s(self): ... - +class Str(Constant): ... +class Bytes(Constant): ... +class HexBytes(Constant): ... class NameConstant(Constant): ... class Ellipsis(Constant): ... diff --git a/vyper/ast/parse.py b/vyper/ast/parse.py index d4569dd644..1e88241186 100644 --- a/vyper/ast/parse.py +++ b/vyper/ast/parse.py @@ -6,10 +6,9 @@ import asttokens from vyper.ast import nodes as vy_ast -from vyper.ast.pre_parser import pre_parse +from vyper.ast.pre_parser import PreParseResult, pre_parse from vyper.compiler.settings import Settings from vyper.exceptions import CompilerPanic, ParserException, SyntaxException -from vyper.typing import ModificationOffsets from vyper.utils import sha256sum, vyper_warn @@ -55,9 +54,9 @@ def parse_to_ast_with_settings( """ if "\x00" in vyper_source: raise ParserException("No null bytes (\\x00) allowed in the source code.") - settings, class_types, for_loop_annotations, python_source = pre_parse(vyper_source) + pre_parse_result = pre_parse(vyper_source) try: - py_ast = python_ast.parse(python_source) + py_ast = python_ast.parse(pre_parse_result.reformatted_code) except SyntaxError as e: # TODO: Ensure 1-to-1 match of source_code:reformatted_code SyntaxErrors raise SyntaxException(str(e), vyper_source, e.lineno, e.offset) from None @@ -73,21 +72,20 @@ def parse_to_ast_with_settings( annotate_python_ast( py_ast, vyper_source, - class_types, - for_loop_annotations, + pre_parse_result, source_id=source_id, module_path=module_path, resolved_path=resolved_path, ) # postcondition: consumed all the for loop annotations - assert len(for_loop_annotations) == 0 + assert len(pre_parse_result.for_loop_annotations) == 0 # Convert to Vyper AST. module = vy_ast.get_node(py_ast) assert isinstance(module, vy_ast.Module) # mypy hint - return settings, module + return pre_parse_result.settings, module def ast_to_dict(ast_struct: Union[vy_ast.VyperNode, List]) -> Union[Dict, List]: @@ -118,8 +116,7 @@ def dict_to_ast(ast_struct: Union[Dict, List]) -> Union[vy_ast.VyperNode, List]: def annotate_python_ast( parsed_ast: python_ast.AST, vyper_source: str, - modification_offsets: ModificationOffsets, - for_loop_annotations: dict, + pre_parse_result: PreParseResult, source_id: int = 0, module_path: Optional[str] = None, resolved_path: Optional[str] = None, @@ -133,11 +130,8 @@ def annotate_python_ast( The AST to be annotated and optimized. vyper_source: str The original vyper source code - 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. + pre_parse_result: PreParseResult + Outputs from pre-parsing. Returns ------- @@ -148,8 +142,7 @@ def annotate_python_ast( tokens.mark_tokens(parsed_ast) visitor = AnnotatingVisitor( vyper_source, - modification_offsets, - for_loop_annotations, + pre_parse_result, tokens, source_id, module_path=module_path, @@ -162,14 +155,12 @@ def annotate_python_ast( class AnnotatingVisitor(python_ast.NodeTransformer): _source_code: str - _modification_offsets: ModificationOffsets - _loop_var_annotations: dict[int, dict[str, Any]] + _pre_parse_result: PreParseResult def __init__( self, source_code: str, - modification_offsets: ModificationOffsets, - for_loop_annotations: dict, + pre_parse_result: PreParseResult, tokens: asttokens.ASTTokens, source_id: int, module_path: Optional[str] = None, @@ -180,8 +171,7 @@ def __init__( self._module_path = module_path self._resolved_path = resolved_path self._source_code = source_code - self._modification_offsets = modification_offsets - self._for_loop_annotations = for_loop_annotations + self._pre_parse_result = pre_parse_result self.counter: int = 0 @@ -275,7 +265,7 @@ def visit_ClassDef(self, node): """ self.generic_visit(node) - node.ast_type = self._modification_offsets[(node.lineno, node.col_offset)] + node.ast_type = self._pre_parse_result.modification_offsets[(node.lineno, node.col_offset)] return node def visit_For(self, node): @@ -283,7 +273,8 @@ def visit_For(self, node): Visit a For node, splicing in the loop variable annotation provided by the pre-parser """ - annotation_tokens = self._for_loop_annotations.pop((node.lineno, node.col_offset)) + key = (node.lineno, node.col_offset) + annotation_tokens = self._pre_parse_result.for_loop_annotations.pop(key) if not annotation_tokens: # a common case for people migrating to 0.4.0, provide a more @@ -350,14 +341,15 @@ def visit_Expr(self, node): if isinstance(node.value, python_ast.Yield): # CMC 2024-03-03 consider unremoving this from the enclosing Expr node = node.value - node.ast_type = self._modification_offsets[(node.lineno, node.col_offset)] + key = (node.lineno, node.col_offset) + node.ast_type = self._pre_parse_result.modification_offsets[key] return node def visit_Await(self, node): start_pos = node.lineno, node.col_offset # grab these before generic_visit modifies them self.generic_visit(node) - node.ast_type = self._modification_offsets[start_pos] + node.ast_type = self._pre_parse_result.modification_offsets[start_pos] return node def visit_Call(self, node): @@ -401,7 +393,18 @@ def visit_Constant(self, node): if node.value is None or isinstance(node.value, bool): node.ast_type = "NameConstant" elif isinstance(node.value, str): - node.ast_type = "Str" + key = (node.lineno, node.col_offset) + if key in self._pre_parse_result.native_hex_literal_locations: + if len(node.value) % 2 != 0: + raise SyntaxException( + "Native hex string must have an even number of characters", + self._source_code, + node.lineno, + node.col_offset, + ) + node.ast_type = "HexBytes" + else: + node.ast_type = "Str" elif isinstance(node.value, bytes): node.ast_type = "Bytes" elif isinstance(node.value, Ellipsis.__class__): diff --git a/vyper/ast/pre_parser.py b/vyper/ast/pre_parser.py index b12aecd0bf..07ba1d2d0d 100644 --- a/vyper/ast/pre_parser.py +++ b/vyper/ast/pre_parser.py @@ -2,7 +2,7 @@ import io import re from collections import defaultdict -from tokenize import COMMENT, NAME, OP, TokenError, TokenInfo, tokenize, untokenize +from tokenize import COMMENT, NAME, OP, STRING, TokenError, TokenInfo, tokenize, untokenize from packaging.specifiers import InvalidSpecifier, SpecifierSet @@ -12,7 +12,7 @@ # evm-version pragma from vyper.evm.opcodes import EVM_VERSIONS from vyper.exceptions import StructureException, SyntaxException, VersionException -from vyper.typing import ModificationOffsets, ParserPosition +from vyper.typing import ParserPosition def validate_version_pragma(version_str: str, full_source_code: str, start: ParserPosition) -> None: @@ -48,7 +48,7 @@ def validate_version_pragma(version_str: str, full_source_code: str, start: Pars ) -class ForParserState(enum.Enum): +class ParserState(enum.Enum): NOT_RUNNING = enum.auto() START_SOON = enum.auto() RUNNING = enum.auto() @@ -63,7 +63,7 @@ def __init__(self, code): self.annotations = {} self._current_annotation = None - self._state = ForParserState.NOT_RUNNING + self._state = ParserState.NOT_RUNNING self._current_for_loop = None def consume(self, token): @@ -71,15 +71,15 @@ def consume(self, token): if token.type == NAME and token.string == "for": # 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._state = ParserState.START_SOON self._current_for_loop = token.start - if self._state == ForParserState.NOT_RUNNING: + if self._state == ParserState.NOT_RUNNING: return False # state machine: start slurping tokens if token.type == OP and token.string == ":": - self._state = ForParserState.RUNNING + self._state = ParserState.RUNNING # sanity check -- this should never really happen, but if it does, # try to raise an exception which pinpoints the source. @@ -93,12 +93,12 @@ def consume(self, token): # state machine: end slurping tokens if token.type == NAME and token.string == "in": - self._state = ForParserState.NOT_RUNNING + self._state = ParserState.NOT_RUNNING self.annotations[self._current_for_loop] = self._current_annotation or [] self._current_annotation = None return False - if self._state != ForParserState.RUNNING: + if self._state != ParserState.RUNNING: return False # slurp the token @@ -106,6 +106,42 @@ def consume(self, token): return True +class HexStringParser: + def __init__(self): + self.locations = [] + self._current_x = None + self._state = ParserState.NOT_RUNNING + + def consume(self, token, result): + # prepare to check if the next token is a STRING + if token.type == NAME and token.string == "x": + self._state = ParserState.RUNNING + self._current_x = token + return True + + if self._state == ParserState.NOT_RUNNING: + return False + + if self._state == ParserState.RUNNING: + current_x = self._current_x + self._current_x = None + self._state = ParserState.NOT_RUNNING + + toks = [current_x] + + # drop the leading x token if the next token is a STRING to avoid a python + # parser error + if token.type == STRING: + self.locations.append(current_x.start) + toks = [TokenInfo(STRING, token.string, current_x.start, token.end, token.line)] + result.extend(toks) + return True + + result.extend(toks) + + return False + + # compound statements that are replaced with `class` # TODO remove enum in favor of flag VYPER_CLASS_TYPES = { @@ -122,7 +158,34 @@ def consume(self, token): CUSTOM_EXPRESSION_TYPES = {"extcall": "ExtCall", "staticcall": "StaticCall"} -def pre_parse(code: str) -> tuple[Settings, ModificationOffsets, dict, str]: +class PreParseResult: + # Compilation settings based on the directives in the source code + settings: Settings + # A mapping of class names to their original class types. + modification_offsets: dict[tuple[int, int], str] + # A mapping of line/column offsets of `For` nodes to the annotation of the for loop target + for_loop_annotations: dict[tuple[int, int], list[TokenInfo]] + # A list of line/column offsets of native hex literals + native_hex_literal_locations: list[tuple[int, int]] + # Reformatted python source string. + reformatted_code: str + + def __init__( + self, + settings, + modification_offsets, + for_loop_annotations, + native_hex_literal_locations, + reformatted_code, + ): + self.settings = settings + self.modification_offsets = modification_offsets + self.for_loop_annotations = for_loop_annotations + self.native_hex_literal_locations = native_hex_literal_locations + self.reformatted_code = reformatted_code + + +def pre_parse(code: str) -> PreParseResult: """ Re-formats a vyper source string into a python source string and performs some validation. More specifically, @@ -144,19 +207,14 @@ def pre_parse(code: str) -> tuple[Settings, ModificationOffsets, dict, str]: Returns ------- - Settings - Compilation settings based on the directives in the source code - 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 annotation of the for loop target - str - Reformatted python source string. + PreParseResult + Outputs for transforming the python AST to vyper AST """ - result = [] - modification_offsets: ModificationOffsets = {} + result: list[TokenInfo] = [] + modification_offsets: dict[tuple[int, int], str] = {} settings = Settings() for_parser = ForParser(code) + native_hex_parser = HexStringParser() _col_adjustments: dict[int, int] = defaultdict(lambda: 0) @@ -264,7 +322,7 @@ def pre_parse(code: str) -> tuple[Settings, ModificationOffsets, dict, str]: if (typ, string) == (OP, ";"): raise SyntaxException("Semi-colon statements not allowed", code, start[0], start[1]) - if not for_parser.consume(token): + if not for_parser.consume(token) and not native_hex_parser.consume(token, result): result.extend(toks) except TokenError as e: @@ -274,4 +332,10 @@ def pre_parse(code: str) -> tuple[Settings, ModificationOffsets, dict, str]: for k, v in for_parser.annotations.items(): for_loop_annotations[k] = v.copy() - return settings, modification_offsets, for_loop_annotations, untokenize(result).decode("utf-8") + return PreParseResult( + settings, + modification_offsets, + for_loop_annotations, + native_hex_parser.locations, + untokenize(result).decode("utf-8"), + ) diff --git a/vyper/codegen/expr.py b/vyper/codegen/expr.py index 0b3b29b9d0..cd51966710 100644 --- a/vyper/codegen/expr.py +++ b/vyper/codegen/expr.py @@ -140,6 +140,12 @@ def parse_Str(self): # Byte literals def parse_Bytes(self): + return self._parse_bytes() + + def parse_HexBytes(self): + return self._parse_bytes() + + def _parse_bytes(self): bytez = self.expr.value bytez_length = len(self.expr.value) typ = BytesT(bytez_length) diff --git a/vyper/compiler/input_bundle.py b/vyper/compiler/input_bundle.py index a928989393..c9eeded3cf 100644 --- a/vyper/compiler/input_bundle.py +++ b/vyper/compiler/input_bundle.py @@ -52,6 +52,8 @@ class ABIInput(CompilerInput): def try_parse_abi(file_input: FileInput) -> CompilerInput: try: s = json.loads(file_input.source_code) + if isinstance(s, dict) and "abi" in s: + s = s["abi"] return ABIInput(**asdict(file_input), abi=s) except (ValueError, TypeError): return file_input diff --git a/vyper/compiler/output.py b/vyper/compiler/output.py index 09d299b90d..f5f99a0bc3 100644 --- a/vyper/compiler/output.py +++ b/vyper/compiler/output.py @@ -3,7 +3,8 @@ from collections import deque from pathlib import PurePath -from vyper.ast import ast_to_dict +import vyper.ast as vy_ast +from vyper.ast.utils import ast_to_dict from vyper.codegen.ir_node import IRnode from vyper.compiler.output_bundle import SolcJSONWriter, VyperArchiveWriter from vyper.compiler.phases import CompilerData @@ -11,7 +12,9 @@ from vyper.evm import opcodes from vyper.exceptions import VyperException from vyper.ir import compile_ir +from vyper.semantics.analysis.base import ModuleInfo from vyper.semantics.types.function import FunctionVisibility, StateMutability +from vyper.semantics.types.module import InterfaceT from vyper.typing import StorageLayout from vyper.utils import vyper_warn from vyper.warnings import ContractSizeLimitWarning @@ -26,9 +29,32 @@ def build_ast_dict(compiler_data: CompilerData) -> dict: def build_annotated_ast_dict(compiler_data: CompilerData) -> dict: + module_t = compiler_data.annotated_vyper_module._metadata["type"] + # get all reachable imports including recursion + imported_module_infos = module_t.reachable_imports + unique_modules: dict[str, vy_ast.Module] = {} + for info in imported_module_infos: + if isinstance(info.typ, InterfaceT): + ast = info.typ.decl_node + if ast is None: # json abi + continue + else: + assert isinstance(info.typ, ModuleInfo) + ast = info.typ.module_t._module + + assert isinstance(ast, vy_ast.Module) # help mypy + # use resolved_path for uniqueness, since Module objects can actually + # come from multiple InputBundles (particularly builtin interfaces), + # so source_id is not guaranteed to be unique. + if ast.resolved_path in unique_modules: + # sanity check -- objects must be identical + assert unique_modules[ast.resolved_path] is ast + unique_modules[ast.resolved_path] = ast + annotated_ast_dict = { "contract_name": str(compiler_data.contract_path), "ast": ast_to_dict(compiler_data.annotated_vyper_module), + "imports": [ast_to_dict(ast) for ast in unique_modules.values()], } return annotated_ast_dict @@ -76,7 +102,7 @@ def build_archive_b64(compiler_data: CompilerData) -> str: def build_integrity(compiler_data: CompilerData) -> str: - return compiler_data.compilation_target._metadata["type"].integrity_sum + return compiler_data.resolved_imports.integrity_sum def build_external_interface_output(compiler_data: CompilerData) -> str: @@ -102,22 +128,33 @@ def build_interface_output(compiler_data: CompilerData) -> str: interface = compiler_data.annotated_vyper_module._metadata["type"].interface out = "" - if interface.events: - out = "# Events\n\n" + if len(interface.structs) > 0: + out += "# Structs\n\n" + for struct in interface.structs.values(): + out += f"struct {struct.name}:\n" + for member_name, member_type in struct.members.items(): + out += f" {member_name}: {member_type}\n" + out += "\n\n" + + if len(interface.events) > 0: + out += "# Events\n\n" for event in interface.events.values(): encoded_args = "\n ".join(f"{name}: {typ}" for name, typ in event.arguments.items()) - out = f"{out}event {event.name}:\n {encoded_args if event.arguments else 'pass'}\n" + out += f"event {event.name}:\n {encoded_args if event.arguments else 'pass'}\n\n\n" - if interface.functions: - out = f"{out}\n# Functions\n\n" + if len(interface.functions) > 0: + out += "# Functions\n\n" for func in interface.functions.values(): if func.visibility == FunctionVisibility.INTERNAL or func.name == "__init__": continue if func.mutability != StateMutability.NONPAYABLE: - out = f"{out}@{func.mutability.value}\n" + out += f"@{func.mutability.value}\n" args = ", ".join([f"{arg.name}: {arg.typ}" for arg in func.arguments]) return_value = f" -> {func.return_type}" if func.return_type is not None else "" - out = f"{out}@external\ndef {func.name}({args}){return_value}:\n ...\n\n" + out += f"@external\ndef {func.name}({args}){return_value}:\n ...\n\n\n" + + out = out.rstrip("\n") + out += "\n" return out diff --git a/vyper/compiler/output_bundle.py b/vyper/compiler/output_bundle.py index 06a84064a1..24a0d070cc 100644 --- a/vyper/compiler/output_bundle.py +++ b/vyper/compiler/output_bundle.py @@ -11,7 +11,7 @@ from vyper.compiler.phases import CompilerData from vyper.compiler.settings import Settings from vyper.exceptions import CompilerPanic -from vyper.semantics.analysis.module import _is_builtin +from vyper.semantics.analysis.imports import _is_builtin from vyper.utils import get_long_version, safe_relpath # data structures and routines for constructing "output bundles", @@ -158,7 +158,7 @@ def write(self): self.write_compilation_target([self.bundle.compilation_target_path]) self.write_search_paths(self.bundle.used_search_paths) self.write_settings(self.compiler_data.original_settings) - self.write_integrity(self.bundle.compilation_target.integrity_sum) + self.write_integrity(self.compiler_data.resolved_imports.integrity_sum) self.write_sources(self.bundle.compiler_inputs) diff --git a/vyper/compiler/phases.py b/vyper/compiler/phases.py index 97df73cdae..d9b6b13b48 100644 --- a/vyper/compiler/phases.py +++ b/vyper/compiler/phases.py @@ -13,6 +13,7 @@ from vyper.ir import compile_ir, optimizer from vyper.semantics import analyze_module, set_data_positions, validate_compilation_target from vyper.semantics.analysis.data_positions import generate_layout_export +from vyper.semantics.analysis.imports import resolve_imports from vyper.semantics.types.function import ContractFunctionT from vyper.semantics.types.module import ModuleT from vyper.typing import StorageLayout @@ -145,9 +146,34 @@ def vyper_module(self): _, ast = self._generate_ast return ast + @cached_property + def _resolve_imports(self): + # deepcopy so as to not interfere with `-f ast` output + vyper_module = copy.deepcopy(self.vyper_module) + with self.input_bundle.search_path(Path(vyper_module.resolved_path).parent): + return vyper_module, resolve_imports(vyper_module, self.input_bundle) + + @cached_property + def resolved_imports(self): + imports = self._resolve_imports[1] + + expected = self.expected_integrity_sum + + if expected is not None and imports.integrity_sum != expected: + # warn for now. strict/relaxed mode was considered but it costs + # interface and testing complexity to add another feature flag. + vyper_warn( + f"Mismatched integrity sum! Expected {expected}" + f" but got {imports.integrity_sum}." + " (This likely indicates a corrupted archive)" + ) + + return imports + @cached_property def _annotate(self) -> tuple[natspec.NatspecOutput, vy_ast.Module]: - module = generate_annotated_ast(self.vyper_module, self.input_bundle) + module = self._resolve_imports[0] + analyze_module(module) nspec = natspec.parse_natspec(module) return nspec, module @@ -167,17 +193,6 @@ def compilation_target(self): """ module_t = self.annotated_vyper_module._metadata["type"] - expected = self.expected_integrity_sum - - if expected is not None and module_t.integrity_sum != expected: - # warn for now. strict/relaxed mode was considered but it costs - # interface and testing complexity to add another feature flag. - vyper_warn( - f"Mismatched integrity sum! Expected {expected}" - f" but got {module_t.integrity_sum}." - " (This likely indicates a corrupted archive)" - ) - validate_compilation_target(module_t) return self.annotated_vyper_module @@ -251,8 +266,7 @@ def assembly_runtime(self) -> list: def bytecode(self) -> bytes: metadata = None if not self.no_bytecode_metadata: - module_t = self.compilation_target._metadata["type"] - metadata = bytes.fromhex(module_t.integrity_sum) + metadata = bytes.fromhex(self.resolved_imports.integrity_sum) return generate_bytecode(self.assembly, compiler_metadata=metadata) @cached_property @@ -270,28 +284,6 @@ def blueprint_bytecode(self) -> bytes: return deploy_bytecode + blueprint_bytecode -def generate_annotated_ast(vyper_module: vy_ast.Module, input_bundle: InputBundle) -> vy_ast.Module: - """ - Validates and annotates the Vyper AST. - - Arguments - --------- - vyper_module : vy_ast.Module - Top-level Vyper AST node - - Returns - ------- - vy_ast.Module - Annotated Vyper AST - """ - vyper_module = copy.deepcopy(vyper_module) - with input_bundle.search_path(Path(vyper_module.resolved_path).parent): - # note: analyze_module does type inference on the AST - analyze_module(vyper_module, input_bundle) - - return vyper_module - - def generate_ir_nodes(global_ctx: ModuleT, settings: Settings) -> tuple[IRnode, IRnode]: """ Generate the intermediate representation (IR) from the contextualized AST. diff --git a/vyper/semantics/analysis/base.py b/vyper/semantics/analysis/base.py index 982b6eb01d..e275930fa0 100644 --- a/vyper/semantics/analysis/base.py +++ b/vyper/semantics/analysis/base.py @@ -1,7 +1,7 @@ import enum from dataclasses import dataclass, fields from functools import cached_property -from typing import TYPE_CHECKING, Any, ClassVar, Dict, Optional, Union +from typing import TYPE_CHECKING, Any, ClassVar, Dict, Optional from vyper import ast as vy_ast from vyper.compiler.input_bundle import CompilerInput @@ -13,7 +13,7 @@ if TYPE_CHECKING: from vyper.semantics.types.function import ContractFunctionT - from vyper.semantics.types.module import InterfaceT, ModuleT + from vyper.semantics.types.module import ModuleT class FunctionVisibility(StringEnum): @@ -119,13 +119,19 @@ def __hash__(self): return hash(id(self.module_t)) -@dataclass(frozen=True) +@dataclass class ImportInfo(AnalysisResult): - typ: Union[ModuleInfo, "InterfaceT"] alias: str # the name in the namespace qualified_module_name: str # for error messages compiler_input: CompilerInput # to recover file info for ast export - node: vy_ast.VyperNode + parsed: Any # (json) abi | AST + _typ: Any = None # type to be filled in during analysis + + @property + def typ(self): + if self._typ is None: # pragma: nocover + raise CompilerPanic("unreachable!") + return self._typ def to_dict(self): ret = {"alias": self.alias, "qualified_module_name": self.qualified_module_name} diff --git a/vyper/semantics/analysis/import_graph.py b/vyper/semantics/analysis/import_graph.py deleted file mode 100644 index e406878194..0000000000 --- a/vyper/semantics/analysis/import_graph.py +++ /dev/null @@ -1,37 +0,0 @@ -import contextlib -from dataclasses import dataclass, field -from typing import Iterator - -from vyper import ast as vy_ast -from vyper.exceptions import CompilerPanic, ImportCycle - -""" -data structure for collecting import statements and validating the -import graph -""" - - -@dataclass -class ImportGraph: - # the current path in the import graph traversal - _path: list[vy_ast.Module] = field(default_factory=list) - - def push_path(self, module_ast: vy_ast.Module) -> None: - if module_ast in self._path: - cycle = self._path + [module_ast] - raise ImportCycle(" imports ".join(f'"{t.path}"' for t in cycle)) - - self._path.append(module_ast) - - def pop_path(self, expected: vy_ast.Module) -> None: - popped = self._path.pop() - if expected != popped: - raise CompilerPanic("unreachable") - - @contextlib.contextmanager - def enter_path(self, module_ast: vy_ast.Module) -> Iterator[None]: - self.push_path(module_ast) - try: - yield - finally: - self.pop_path(module_ast) diff --git a/vyper/semantics/analysis/imports.py b/vyper/semantics/analysis/imports.py new file mode 100644 index 0000000000..3268f12e94 --- /dev/null +++ b/vyper/semantics/analysis/imports.py @@ -0,0 +1,333 @@ +import contextlib +from dataclasses import dataclass, field +from pathlib import Path, PurePath +from typing import Any, Iterator + +import vyper.builtins.interfaces +from vyper import ast as vy_ast +from vyper.compiler.input_bundle import ( + ABIInput, + CompilerInput, + FileInput, + FilesystemInputBundle, + InputBundle, + PathLike, +) +from vyper.exceptions import ( + CompilerPanic, + DuplicateImport, + ImportCycle, + ModuleNotFound, + StructureException, +) +from vyper.semantics.analysis.base import ImportInfo +from vyper.utils import safe_relpath, sha256sum + +""" +collect import statements and validate the import graph. +this module is separated into its own pass so that we can resolve the import +graph quickly (without doing semantic analysis) and for cleanliness, to +segregate the I/O portion of semantic analysis into its own pass. +""" + + +@dataclass +class _ImportGraph: + # the current path in the import graph traversal + _path: list[vy_ast.Module] = field(default_factory=list) + + # stack of dicts, each item in the stack is a dict keeping + # track of imports in the current module + _imports: list[dict] = field(default_factory=list) + + @property + def imported_modules(self): + return self._imports[-1] + + @property + def current_module(self): + return self._path[-1] + + def push_path(self, module_ast: vy_ast.Module) -> None: + if module_ast in self._path: + cycle = self._path + [module_ast] + raise ImportCycle(" imports ".join(f'"{t.path}"' for t in cycle)) + + self._path.append(module_ast) + self._imports.append({}) + + def pop_path(self, expected: vy_ast.Module) -> None: + popped = self._path.pop() + if expected != popped: + raise CompilerPanic("unreachable") + self._imports.pop() + + @contextlib.contextmanager + def enter_path(self, module_ast: vy_ast.Module) -> Iterator[None]: + self.push_path(module_ast) + try: + yield + finally: + self.pop_path(module_ast) + + +class ImportAnalyzer: + def __init__(self, input_bundle: InputBundle, graph: _ImportGraph): + self.input_bundle = input_bundle + self.graph = graph + self._ast_of: dict[int, vy_ast.Module] = {} + + self.seen: set[int] = set() + + self.integrity_sum = None + + def resolve_imports(self, module_ast: vy_ast.Module): + self._resolve_imports_r(module_ast) + self.integrity_sum = self._calculate_integrity_sum_r(module_ast) + + def _calculate_integrity_sum_r(self, module_ast: vy_ast.Module): + acc = [sha256sum(module_ast.full_source_code)] + for s in module_ast.get_children((vy_ast.Import, vy_ast.ImportFrom)): + info = s._metadata["import_info"] + + if info.compiler_input.path.suffix in (".vyi", ".json"): + # NOTE: this needs to be redone if interfaces can import other interfaces + acc.append(info.compiler_input.sha256sum) + else: + acc.append(self._calculate_integrity_sum_r(info.parsed)) + + return sha256sum("".join(acc)) + + def _resolve_imports_r(self, module_ast: vy_ast.Module): + if id(module_ast) in self.seen: + return + with self.graph.enter_path(module_ast): + for node in module_ast.body: + if isinstance(node, vy_ast.Import): + self._handle_Import(node) + elif isinstance(node, vy_ast.ImportFrom): + self._handle_ImportFrom(node) + self.seen.add(id(module_ast)) + + def _handle_Import(self, node: vy_ast.Import): + # import x.y[name] as y[alias] + + alias = node.alias + + if alias is None: + alias = node.name + + # don't handle things like `import x.y` + if "." in alias: + msg = "import requires an accompanying `as` statement" + suggested_alias = node.name[node.name.rfind(".") :] + hint = f"try `import {node.name} as {suggested_alias}`" + raise StructureException(msg, node, hint=hint) + + self._add_import(node, 0, node.name, alias) + + def _handle_ImportFrom(self, node: vy_ast.ImportFrom): + # from m.n[module] import x[name] as y[alias] + + alias = node.alias + + if alias is None: + alias = node.name + + module = node.module or "" + if module: + module += "." + + qualified_module_name = module + node.name + self._add_import(node, node.level, qualified_module_name, alias) + + def _add_import( + self, node: vy_ast.VyperNode, level: int, qualified_module_name: str, alias: str + ) -> None: + compiler_input, ast = self._load_import(node, level, qualified_module_name, alias) + node._metadata["import_info"] = ImportInfo( + alias, qualified_module_name, compiler_input, ast + ) + + # load an InterfaceT or ModuleInfo from an import. + # raises FileNotFoundError + def _load_import(self, node: vy_ast.VyperNode, level: int, module_str: str, alias: str) -> Any: + # the directory this (currently being analyzed) module is in + ast = self.graph.current_module + self_search_path = Path(ast.resolved_path).parent + + with self.input_bundle.poke_search_path(self_search_path): + return self._load_import_helper(node, level, module_str, alias) + + def _load_import_helper( + self, node: vy_ast.VyperNode, level: int, module_str: str, alias: str + ) -> tuple[CompilerInput, Any]: + if _is_builtin(module_str): + return _load_builtin_import(level, module_str) + + path = _import_to_path(level, module_str) + + if path in self.graph.imported_modules: + previous_import_stmt = self.graph.imported_modules[path] + raise DuplicateImport(f"{alias} imported more than once!", previous_import_stmt, node) + + self.graph.imported_modules[path] = node + + err = None + + try: + path_vy = path.with_suffix(".vy") + file = self.input_bundle.load_file(path_vy) + assert isinstance(file, FileInput) # mypy hint + + module_ast = self._ast_from_file(file) + self.resolve_imports(module_ast) + + return file, module_ast + + except FileNotFoundError as e: + # escape `e` from the block scope, it can make things + # easier to debug. + err = e + + try: + file = self.input_bundle.load_file(path.with_suffix(".vyi")) + assert isinstance(file, FileInput) # mypy hint + module_ast = self._ast_from_file(file) + self.resolve_imports(module_ast) + + # language does not yet allow recursion for vyi files + # self.resolve_imports(module_ast) + + return file, module_ast + + except FileNotFoundError: + pass + + try: + file = self.input_bundle.load_file(path.with_suffix(".json")) + assert isinstance(file, ABIInput) # mypy hint + return file, file.abi + except FileNotFoundError: + pass + + hint = None + if module_str.startswith("vyper.interfaces"): + hint = "try renaming `vyper.interfaces` to `ethereum.ercs`" + + # copy search_paths, makes debugging a bit easier + search_paths = self.input_bundle.search_paths.copy() # noqa: F841 + raise ModuleNotFound(module_str, hint=hint) from err + + def _ast_from_file(self, file: FileInput) -> vy_ast.Module: + # cache ast if we have seen it before. + # this gives us the additional property of object equality on + # two ASTs produced from the same source + ast_of = self._ast_of + if file.source_id not in ast_of: + ast_of[file.source_id] = _parse_ast(file) + + return ast_of[file.source_id] + + +def _parse_ast(file: FileInput) -> vy_ast.Module: + module_path = file.resolved_path # for error messages + try: + # try to get a relative path, to simplify the error message + cwd = Path(".") + if module_path.is_absolute(): + cwd = cwd.resolve() + module_path = module_path.relative_to(cwd) + except ValueError: + # we couldn't get a relative path (cf. docs for Path.relative_to), + # use the resolved path given to us by the InputBundle + pass + + ret = vy_ast.parse_to_ast( + file.source_code, + source_id=file.source_id, + module_path=module_path.as_posix(), + resolved_path=file.resolved_path.as_posix(), + ) + return ret + + +# convert an import to a path (without suffix) +def _import_to_path(level: int, module_str: str) -> PurePath: + base_path = "" + if level > 1: + base_path = "../" * (level - 1) + elif level == 1: + base_path = "./" + return PurePath(f"{base_path}{module_str.replace('.', '/')}/") + + +# can add more, e.g. "vyper.builtins.interfaces", etc. +BUILTIN_PREFIXES = ["ethereum.ercs"] + + +# TODO: could move this to analysis/common.py or something +def _is_builtin(module_str): + return any(module_str.startswith(prefix) for prefix in BUILTIN_PREFIXES) + + +_builtins_cache: dict[PathLike, tuple[CompilerInput, vy_ast.Module]] = {} + + +def _load_builtin_import(level: int, module_str: str) -> tuple[CompilerInput, vy_ast.Module]: + if not _is_builtin(module_str): # pragma: nocover + raise CompilerPanic("unreachable!") + + builtins_path = vyper.builtins.interfaces.__path__[0] + # hygiene: convert to relpath to avoid leaking user directory info + # (note Path.relative_to cannot handle absolute to relative path + # conversion, so we must use the `os` module). + builtins_path = safe_relpath(builtins_path) + + search_path = Path(builtins_path).parent.parent.parent + # generate an input bundle just because it knows how to build paths. + input_bundle = FilesystemInputBundle([search_path]) + + # remap builtins directory -- + # ethereum/ercs => vyper/builtins/interfaces + remapped_module = module_str + if remapped_module.startswith("ethereum.ercs"): + remapped_module = remapped_module.removeprefix("ethereum.ercs") + remapped_module = vyper.builtins.interfaces.__package__ + remapped_module + + path = _import_to_path(level, remapped_module).with_suffix(".vyi") + + # builtins are globally the same, so we can safely cache them + # (it is also *correct* to cache them, so that types defined in builtins + # compare correctly using pointer-equality.) + if path in _builtins_cache: + file, ast = _builtins_cache[path] + return file, ast + + try: + file = input_bundle.load_file(path) + assert isinstance(file, FileInput) # mypy hint + except FileNotFoundError as e: + hint = None + components = module_str.split(".") + # common issue for upgrading codebases from v0.3.x to v0.4.x - + # hint: rename ERC20 to IERC20 + if components[-1].startswith("ERC"): + module_prefix = components[-1] + hint = f"try renaming `{module_prefix}` to `I{module_prefix}`" + raise ModuleNotFound(module_str, hint=hint) from e + + interface_ast = _parse_ast(file) + + # no recursion needed since builtins don't have any imports + + _builtins_cache[path] = file, interface_ast + return file, interface_ast + + +def resolve_imports(module_ast: vy_ast.Module, input_bundle: InputBundle): + graph = _ImportGraph() + analyzer = ImportAnalyzer(input_bundle, graph) + analyzer.resolve_imports(module_ast) + + return analyzer diff --git a/vyper/semantics/analysis/module.py b/vyper/semantics/analysis/module.py index 6816fbed98..8a2beb61e6 100644 --- a/vyper/semantics/analysis/module.py +++ b/vyper/semantics/analysis/module.py @@ -1,22 +1,11 @@ -from pathlib import Path, PurePath from typing import Any, Optional -import vyper.builtins.interfaces from vyper import ast as vy_ast -from vyper.compiler.input_bundle import ( - ABIInput, - CompilerInput, - FileInput, - FilesystemInputBundle, - InputBundle, - PathLike, -) from vyper.evm.opcodes import version_check from vyper.exceptions import ( BorrowException, CallViolation, CompilerPanic, - DuplicateImport, EvmVersionException, ExceptionList, ImmutableViolation, @@ -24,7 +13,6 @@ InterfaceViolation, InvalidLiteral, InvalidType, - ModuleNotFound, StateAccessViolation, StructureException, UndeclaredDefinition, @@ -44,7 +32,6 @@ from vyper.semantics.analysis.common import VyperNodeVisitorBase from vyper.semantics.analysis.constant_folding import constant_fold from vyper.semantics.analysis.getters import generate_public_variable_getters -from vyper.semantics.analysis.import_graph import ImportGraph from vyper.semantics.analysis.local import ExprVisitor, analyze_functions, check_module_uses from vyper.semantics.analysis.utils import ( check_modifiability, @@ -57,32 +44,19 @@ from vyper.semantics.types.function import ContractFunctionT from vyper.semantics.types.module import ModuleT from vyper.semantics.types.utils import type_from_annotation -from vyper.utils import OrderedSet, safe_relpath +from vyper.utils import OrderedSet -def analyze_module( - module_ast: vy_ast.Module, - input_bundle: InputBundle, - import_graph: ImportGraph = None, - is_interface: bool = False, -) -> ModuleT: +def analyze_module(module_ast: vy_ast.Module) -> ModuleT: """ Analyze a Vyper module AST node, recursively analyze all its imports, add all module-level objects to the namespace, type-check/validate semantics and annotate with type and analysis info """ - if import_graph is None: - import_graph = ImportGraph() - - return _analyze_module_r(module_ast, input_bundle, import_graph, is_interface) + return _analyze_module_r(module_ast) -def _analyze_module_r( - module_ast: vy_ast.Module, - input_bundle: InputBundle, - import_graph: ImportGraph, - is_interface: bool = False, -): +def _analyze_module_r(module_ast: vy_ast.Module, is_interface: bool = False): if "type" in module_ast._metadata: # we don't need to analyse again, skip out assert isinstance(module_ast._metadata["type"], ModuleT) @@ -91,8 +65,8 @@ def _analyze_module_r( # validate semantics and annotate AST with type/semantics information namespace = get_namespace() - with namespace.enter_scope(), import_graph.enter_path(module_ast): - analyzer = ModuleAnalyzer(module_ast, input_bundle, namespace, import_graph, is_interface) + with namespace.enter_scope(): + analyzer = ModuleAnalyzer(module_ast, namespace, is_interface) analyzer.analyze_module_body() _analyze_call_graph(module_ast) @@ -175,22 +149,12 @@ class ModuleAnalyzer(VyperNodeVisitorBase): scope_name = "module" def __init__( - self, - module_node: vy_ast.Module, - input_bundle: InputBundle, - namespace: Namespace, - import_graph: ImportGraph, - is_interface: bool = False, + self, module_node: vy_ast.Module, namespace: Namespace, is_interface: bool = False ) -> None: self.ast = module_node - self.input_bundle = input_bundle self.namespace = namespace - self._import_graph = import_graph self.is_interface = is_interface - # keep track of imported modules to prevent duplicate imports - self._imported_modules: dict[PurePath, vy_ast.VyperNode] = {} - # keep track of exported functions to prevent duplicate exports self._all_functions: dict[ContractFunctionT, vy_ast.VyperNode] = {} @@ -389,16 +353,6 @@ def validate_initialized_modules(self): err_list.raise_if_not_empty() - def _ast_from_file(self, file: FileInput) -> vy_ast.Module: - # cache ast if we have seen it before. - # this gives us the additional property of object equality on - # two ASTs produced from the same source - ast_of = self.input_bundle._cache._ast_of - if file.source_id not in ast_of: - ast_of[file.source_id] = _parse_ast(file) - - return ast_of[file.source_id] - def visit_ImplementsDecl(self, node): type_ = type_from_annotation(node.annotation) @@ -739,32 +693,44 @@ def visit_FunctionDef(self, node): self._add_exposed_function(func_t, node) def visit_Import(self, node): - # import x.y[name] as y[alias] + self._add_import(node) - alias = node.alias + def visit_ImportFrom(self, node): + self._add_import(node) - if alias is None: - alias = node.name + def _add_import(self, node: vy_ast.VyperNode) -> None: + import_info = node._metadata["import_info"] + # similar structure to import analyzer + module_info = self._load_import(import_info) - # don't handle things like `import x.y` - if "." in alias: - msg = "import requires an accompanying `as` statement" - suggested_alias = node.name[node.name.rfind(".") :] - hint = f"try `import {node.name} as {suggested_alias}`" - raise StructureException(msg, node, hint=hint) + import_info._typ = module_info - self._add_import(node, 0, node.name, alias) + self.namespace[import_info.alias] = module_info - def visit_ImportFrom(self, node): - # from m.n[module] import x[name] as y[alias] - alias = node.alias or node.name + def _load_import(self, import_info: ImportInfo) -> Any: + path = import_info.compiler_input.path + if path.suffix == ".vy": + module_ast = import_info.parsed + with override_global_namespace(Namespace()): + module_t = _analyze_module_r(module_ast, is_interface=False) + return ModuleInfo(module_t, import_info.alias) + + if path.suffix == ".vyi": + module_ast = import_info.parsed + with override_global_namespace(Namespace()): + module_t = _analyze_module_r(module_ast, is_interface=True) - module = node.module or "" - if module: - module += "." + # NOTE: might be cleaner to return the whole module, so we + # have a ModuleInfo, that way we don't need to have different + # code paths for InterfaceT vs ModuleInfo + return module_t.interface - qualified_module_name = module + node.name - self._add_import(node, node.level, qualified_module_name, alias) + if path.suffix == ".json": + abi = import_info.parsed + path = import_info.compiler_input.path + return InterfaceT.from_json_abi(str(path), abi) + + raise CompilerPanic("unreachable") # pragma: nocover def visit_InterfaceDef(self, node): interface_t = InterfaceT.from_InterfaceDef(node) @@ -775,190 +741,3 @@ def visit_StructDef(self, node): struct_t = StructT.from_StructDef(node) node._metadata["struct_type"] = struct_t self.namespace[node.name] = struct_t - - def _add_import( - self, node: vy_ast.VyperNode, level: int, qualified_module_name: str, alias: str - ) -> None: - compiler_input, module_info = self._load_import(node, level, qualified_module_name, alias) - node._metadata["import_info"] = ImportInfo( - module_info, alias, qualified_module_name, compiler_input, node - ) - self.namespace[alias] = module_info - - # load an InterfaceT or ModuleInfo from an import. - # raises FileNotFoundError - def _load_import(self, node: vy_ast.VyperNode, level: int, module_str: str, alias: str) -> Any: - # the directory this (currently being analyzed) module is in - self_search_path = Path(self.ast.resolved_path).parent - - with self.input_bundle.poke_search_path(self_search_path): - return self._load_import_helper(node, level, module_str, alias) - - def _load_import_helper( - self, node: vy_ast.VyperNode, level: int, module_str: str, alias: str - ) -> tuple[CompilerInput, Any]: - if _is_builtin(module_str): - return _load_builtin_import(level, module_str) - - path = _import_to_path(level, module_str) - - # this could conceivably be in the ImportGraph but no need at this point - if path in self._imported_modules: - previous_import_stmt = self._imported_modules[path] - raise DuplicateImport(f"{alias} imported more than once!", previous_import_stmt, node) - - self._imported_modules[path] = node - - err = None - - try: - path_vy = path.with_suffix(".vy") - file = self.input_bundle.load_file(path_vy) - assert isinstance(file, FileInput) # mypy hint - - module_ast = self._ast_from_file(file) - - with override_global_namespace(Namespace()): - module_t = _analyze_module_r( - module_ast, - self.input_bundle, - import_graph=self._import_graph, - is_interface=False, - ) - - return file, ModuleInfo(module_t, alias) - - except FileNotFoundError as e: - # escape `e` from the block scope, it can make things - # easier to debug. - err = e - - try: - file = self.input_bundle.load_file(path.with_suffix(".vyi")) - assert isinstance(file, FileInput) # mypy hint - module_ast = self._ast_from_file(file) - - with override_global_namespace(Namespace()): - _analyze_module_r( - module_ast, - self.input_bundle, - import_graph=self._import_graph, - is_interface=True, - ) - module_t = module_ast._metadata["type"] - - return file, module_t.interface - - except FileNotFoundError: - pass - - try: - file = self.input_bundle.load_file(path.with_suffix(".json")) - assert isinstance(file, ABIInput) # mypy hint - return file, InterfaceT.from_json_abi(str(file.path), file.abi) - except FileNotFoundError: - pass - - hint = None - if module_str.startswith("vyper.interfaces"): - hint = "try renaming `vyper.interfaces` to `ethereum.ercs`" - - # copy search_paths, makes debugging a bit easier - search_paths = self.input_bundle.search_paths.copy() # noqa: F841 - raise ModuleNotFound(module_str, hint=hint) from err - - -def _parse_ast(file: FileInput) -> vy_ast.Module: - module_path = file.resolved_path # for error messages - try: - # try to get a relative path, to simplify the error message - cwd = Path(".") - if module_path.is_absolute(): - cwd = cwd.resolve() - module_path = module_path.relative_to(cwd) - except ValueError: - # we couldn't get a relative path (cf. docs for Path.relative_to), - # use the resolved path given to us by the InputBundle - pass - - ret = vy_ast.parse_to_ast( - file.source_code, - source_id=file.source_id, - module_path=module_path.as_posix(), - resolved_path=file.resolved_path.as_posix(), - ) - return ret - - -# convert an import to a path (without suffix) -def _import_to_path(level: int, module_str: str) -> PurePath: - base_path = "" - if level > 1: - base_path = "../" * (level - 1) - elif level == 1: - base_path = "./" - return PurePath(f"{base_path}{module_str.replace('.', '/')}/") - - -# can add more, e.g. "vyper.builtins.interfaces", etc. -BUILTIN_PREFIXES = ["ethereum.ercs"] - - -# TODO: could move this to analysis/common.py or something -def _is_builtin(module_str): - return any(module_str.startswith(prefix) for prefix in BUILTIN_PREFIXES) - - -_builtins_cache: dict[PathLike, tuple[CompilerInput, ModuleT]] = {} - - -def _load_builtin_import(level: int, module_str: str) -> tuple[CompilerInput, InterfaceT]: - if not _is_builtin(module_str): # pragma: nocover - raise CompilerPanic("unreachable!") - - builtins_path = vyper.builtins.interfaces.__path__[0] - # hygiene: convert to relpath to avoid leaking user directory info - # (note Path.relative_to cannot handle absolute to relative path - # conversion, so we must use the `os` module). - builtins_path = safe_relpath(builtins_path) - - search_path = Path(builtins_path).parent.parent.parent - # generate an input bundle just because it knows how to build paths. - input_bundle = FilesystemInputBundle([search_path]) - - # remap builtins directory -- - # ethereum/ercs => vyper/builtins/interfaces - remapped_module = module_str - if remapped_module.startswith("ethereum.ercs"): - remapped_module = remapped_module.removeprefix("ethereum.ercs") - remapped_module = vyper.builtins.interfaces.__package__ + remapped_module - - path = _import_to_path(level, remapped_module).with_suffix(".vyi") - - # builtins are globally the same, so we can safely cache them - # (it is also *correct* to cache them, so that types defined in builtins - # compare correctly using pointer-equality.) - if path in _builtins_cache: - file, module_t = _builtins_cache[path] - return file, module_t.interface - - try: - file = input_bundle.load_file(path) - assert isinstance(file, FileInput) # mypy hint - except FileNotFoundError as e: - hint = None - components = module_str.split(".") - # common issue for upgrading codebases from v0.3.x to v0.4.x - - # hint: rename ERC20 to IERC20 - if components[-1].startswith("ERC"): - module_prefix = components[-1] - hint = f"try renaming `{module_prefix}` to `I{module_prefix}`" - raise ModuleNotFound(module_str, hint=hint) from e - - interface_ast = _parse_ast(file) - - with override_global_namespace(Namespace()): - module_t = _analyze_module_r(interface_ast, input_bundle, ImportGraph(), is_interface=True) - - _builtins_cache[path] = file, module_t - return file, module_t.interface diff --git a/vyper/semantics/types/bytestrings.py b/vyper/semantics/types/bytestrings.py index cd330681cf..02e3bb213f 100644 --- a/vyper/semantics/types/bytestrings.py +++ b/vyper/semantics/types/bytestrings.py @@ -159,7 +159,7 @@ class BytesT(_BytestringT): typeclass = "bytes" _id = "Bytes" - _valid_literal = (vy_ast.Bytes,) + _valid_literal = (vy_ast.Bytes, vy_ast.HexBytes) @property def abi_type(self) -> ABIType: diff --git a/vyper/semantics/types/module.py b/vyper/semantics/types/module.py index ba72842c65..dabeaf21b6 100644 --- a/vyper/semantics/types/module.py +++ b/vyper/semantics/types/module.py @@ -21,8 +21,8 @@ from vyper.semantics.types.base import TYPE_T, VyperType, is_type_t from vyper.semantics.types.function import ContractFunctionT from vyper.semantics.types.primitives import AddressT -from vyper.semantics.types.user import EventT, StructT, _UserType -from vyper.utils import OrderedSet, sha256sum +from vyper.semantics.types.user import EventT, FlagT, StructT, _UserType +from vyper.utils import OrderedSet if TYPE_CHECKING: from vyper.semantics.analysis.base import ImportInfo, ModuleInfo @@ -45,27 +45,29 @@ def __init__( functions: dict, events: dict, structs: dict, + flags: dict, ) -> None: validate_unique_method_ids(list(functions.values())) - members = functions | events | structs + members = functions | events | structs | flags # sanity check: by construction, there should be no duplicates. - assert len(members) == len(functions) + len(events) + len(structs) + assert len(members) == len(functions) + len(events) + len(structs) + len(flags) super().__init__(functions) - self._helper = VyperType(events | structs) + self._helper = VyperType(events | structs | flags) self._id = _id self._helper._id = _id self.functions = functions self.events = events self.structs = structs + self.flags = flags self.decl_node = decl_node def get_type_member(self, attr, node): - # get an event or struct from this interface + # get an event, struct or flag from this interface return TYPE_T(self._helper.get_member(attr, node)) @property @@ -159,12 +161,14 @@ def _from_lists( interface_name: str, decl_node: Optional[vy_ast.VyperNode], function_list: list[tuple[str, ContractFunctionT]], - event_list: list[tuple[str, EventT]], - struct_list: list[tuple[str, StructT]], + event_list: Optional[list[tuple[str, EventT]]] = None, + struct_list: Optional[list[tuple[str, StructT]]] = None, + flag_list: Optional[list[tuple[str, FlagT]]] = None, ) -> "InterfaceT": - functions = {} - events = {} - structs = {} + functions: dict[str, ContractFunctionT] = {} + events: dict[str, EventT] = {} + structs: dict[str, StructT] = {} + flags: dict[str, FlagT] = {} seen_items: dict = {} @@ -175,19 +179,20 @@ def _mark_seen(name, item): raise NamespaceCollision(msg, item.decl_node, prev_decl=prev_decl) seen_items[name] = item - for name, function in function_list: - _mark_seen(name, function) - functions[name] = function + def _process(dst_dict, items): + if items is None: + return - for name, event in event_list: - _mark_seen(name, event) - events[name] = event + for name, item in items: + _mark_seen(name, item) + dst_dict[name] = item - for name, struct in struct_list: - _mark_seen(name, struct) - structs[name] = struct + _process(functions, function_list) + _process(events, event_list) + _process(structs, struct_list) + _process(flags, flag_list) - return cls(interface_name, decl_node, functions, events, structs) + return cls(interface_name, decl_node, functions, events, structs, flags) @classmethod def from_json_abi(cls, name: str, abi: dict) -> "InterfaceT": @@ -214,8 +219,7 @@ def from_json_abi(cls, name: str, abi: dict) -> "InterfaceT": for item in [i for i in abi if i.get("type") == "event"]: events.append((item["name"], EventT.from_abi(item))) - structs: list = [] # no structs in json ABI (as of yet) - return cls._from_lists(name, None, functions, events, structs) + return cls._from_lists(name, None, functions, events) @classmethod def from_ModuleT(cls, module_t: "ModuleT") -> "InterfaceT": @@ -247,8 +251,9 @@ def from_ModuleT(cls, module_t: "ModuleT") -> "InterfaceT": # these are accessible via import, but they do not show up # in the ABI json structs = [(node.name, node._metadata["struct_type"]) for node in module_t.struct_defs] + flags = [(node.name, node._metadata["flag_type"]) for node in module_t.flag_defs] - return cls._from_lists(module_t._id, module_t.decl_node, funcs, events, structs) + return cls._from_lists(module_t._id, module_t.decl_node, funcs, events, structs, flags) @classmethod def from_InterfaceDef(cls, node: vy_ast.InterfaceDef) -> "InterfaceT": @@ -265,11 +270,7 @@ def from_InterfaceDef(cls, node: vy_ast.InterfaceDef) -> "InterfaceT": ) functions.append((func_ast.name, ContractFunctionT.from_InterfaceDef(func_ast))) - # no structs or events in InterfaceDefs - events: list = [] - structs: list = [] - - return cls._from_lists(node.name, node, functions, events, structs) + return cls._from_lists(node.name, node, functions) # Datatype to store all module information. @@ -437,21 +438,6 @@ def reachable_imports(self) -> list["ImportInfo"]: return ret - @cached_property - def integrity_sum(self) -> str: - acc = [sha256sum(self._module.full_source_code)] - for s in self.import_stmts: - info = s._metadata["import_info"] - - if isinstance(info.typ, InterfaceT): - # NOTE: this needs to be redone if interfaces can import other interfaces - acc.append(info.compiler_input.sha256sum) - else: - assert isinstance(info.typ.typ, ModuleT) - acc.append(info.typ.typ.integrity_sum) - - return sha256sum("".join(acc)) - def find_module_info(self, needle: "ModuleT") -> Optional["ModuleInfo"]: for s in self.imported_modules.values(): if s.module_t == needle: diff --git a/vyper/typing.py b/vyper/typing.py index ad3964dff9..108c0605bb 100644 --- a/vyper/typing.py +++ b/vyper/typing.py @@ -1,7 +1,6 @@ from typing import Dict, Optional, Sequence, Tuple, Union # Parser -ModificationOffsets = Dict[Tuple[int, int], str] ParserPosition = Tuple[int, int] # Compiler diff --git a/vyper/venom/__init__.py b/vyper/venom/__init__.py index a5f51b787d..310147baa7 100644 --- a/vyper/venom/__init__.py +++ b/vyper/venom/__init__.py @@ -9,16 +9,18 @@ from vyper.venom.context import IRContext from vyper.venom.function import IRFunction from vyper.venom.ir_node_to_venom import ir_node_to_venom -from vyper.venom.passes.algebraic_optimization import AlgebraicOptimizationPass -from vyper.venom.passes.branch_optimization import BranchOptimizationPass -from vyper.venom.passes.dft import DFTPass -from vyper.venom.passes.make_ssa import MakeSSA -from vyper.venom.passes.mem2var import Mem2Var -from vyper.venom.passes.remove_unused_variables import RemoveUnusedVariablesPass -from vyper.venom.passes.sccp import SCCP -from vyper.venom.passes.simplify_cfg import SimplifyCFGPass -from vyper.venom.passes.store_elimination import StoreElimination -from vyper.venom.passes.store_expansion import StoreExpansionPass +from vyper.venom.passes import ( + SCCP, + AlgebraicOptimizationPass, + BranchOptimizationPass, + DFTPass, + MakeSSA, + Mem2Var, + RemoveUnusedVariablesPass, + SimplifyCFGPass, + StoreElimination, + StoreExpansionPass, +) from vyper.venom.venom_to_assembly import VenomCompiler DEFAULT_OPT_LEVEL = OptimizationLevel.default() diff --git a/vyper/venom/analysis/__init__.py b/vyper/venom/analysis/__init__.py index e69de29bb2..4870de3fb7 100644 --- a/vyper/venom/analysis/__init__.py +++ b/vyper/venom/analysis/__init__.py @@ -0,0 +1,6 @@ +from .analysis import IRAnalysesCache, IRAnalysis +from .cfg import CFGAnalysis +from .dfg import DFGAnalysis +from .dominators import DominatorTreeAnalysis +from .equivalent_vars import VarEquivalenceAnalysis +from .liveness import LivenessAnalysis diff --git a/vyper/venom/analysis/cfg.py b/vyper/venom/analysis/cfg.py index bd2ae34b68..e4f130bc18 100644 --- a/vyper/venom/analysis/cfg.py +++ b/vyper/venom/analysis/cfg.py @@ -1,5 +1,5 @@ from vyper.utils import OrderedSet -from vyper.venom.analysis.analysis import IRAnalysis +from vyper.venom.analysis import IRAnalysis from vyper.venom.basicblock import CFG_ALTERING_INSTRUCTIONS @@ -32,8 +32,7 @@ def analyze(self) -> None: in_bb.add_cfg_out(bb) def invalidate(self): - from vyper.venom.analysis.dominators import DominatorTreeAnalysis - from vyper.venom.analysis.liveness import LivenessAnalysis + from vyper.venom.analysis import DominatorTreeAnalysis, LivenessAnalysis self.analyses_cache.invalidate_analysis(DominatorTreeAnalysis) self.analyses_cache.invalidate_analysis(LivenessAnalysis) diff --git a/vyper/venom/analysis/dominators.py b/vyper/venom/analysis/dominators.py index 129d1d0f22..e360df36b9 100644 --- a/vyper/venom/analysis/dominators.py +++ b/vyper/venom/analysis/dominators.py @@ -1,7 +1,6 @@ from vyper.exceptions import CompilerPanic from vyper.utils import OrderedSet -from vyper.venom.analysis.analysis import IRAnalysis -from vyper.venom.analysis.cfg import CFGAnalysis +from vyper.venom.analysis import CFGAnalysis, IRAnalysis from vyper.venom.basicblock import IRBasicBlock from vyper.venom.function import IRFunction diff --git a/vyper/venom/analysis/equivalent_vars.py b/vyper/venom/analysis/equivalent_vars.py index 9b0c03e3d1..895895651a 100644 --- a/vyper/venom/analysis/equivalent_vars.py +++ b/vyper/venom/analysis/equivalent_vars.py @@ -1,5 +1,4 @@ -from vyper.venom.analysis.analysis import IRAnalysis -from vyper.venom.analysis.dfg import DFGAnalysis +from vyper.venom.analysis import DFGAnalysis, IRAnalysis from vyper.venom.basicblock import IRVariable diff --git a/vyper/venom/analysis/liveness.py b/vyper/venom/analysis/liveness.py index 2a471bc8be..b5d65961b7 100644 --- a/vyper/venom/analysis/liveness.py +++ b/vyper/venom/analysis/liveness.py @@ -2,8 +2,7 @@ from vyper.exceptions import CompilerPanic from vyper.utils import OrderedSet -from vyper.venom.analysis.analysis import IRAnalysis -from vyper.venom.analysis.cfg import CFGAnalysis +from vyper.venom.analysis import CFGAnalysis, IRAnalysis from vyper.venom.basicblock import IRBasicBlock, IRVariable diff --git a/vyper/venom/passes/__init__.py b/vyper/venom/passes/__init__.py new file mode 100644 index 0000000000..83098234c1 --- /dev/null +++ b/vyper/venom/passes/__init__.py @@ -0,0 +1,11 @@ +from .algebraic_optimization import AlgebraicOptimizationPass +from .branch_optimization import BranchOptimizationPass +from .dft import DFTPass +from .make_ssa import MakeSSA +from .mem2var import Mem2Var +from .normalization import NormalizationPass +from .remove_unused_variables import RemoveUnusedVariablesPass +from .sccp import SCCP +from .simplify_cfg import SimplifyCFGPass +from .store_elimination import StoreElimination +from .store_expansion import StoreExpansionPass diff --git a/vyper/venom/passes/algebraic_optimization.py b/vyper/venom/passes/algebraic_optimization.py index 1d375ea988..5d4291667e 100644 --- a/vyper/venom/passes/algebraic_optimization.py +++ b/vyper/venom/passes/algebraic_optimization.py @@ -1,5 +1,4 @@ -from vyper.venom.analysis.dfg import DFGAnalysis -from vyper.venom.analysis.liveness import LivenessAnalysis +from vyper.venom.analysis import DFGAnalysis, LivenessAnalysis from vyper.venom.basicblock import IRInstruction, IRLabel, IRLiteral, IROperand from vyper.venom.passes.base_pass import IRPass diff --git a/vyper/venom/passes/base_pass.py b/vyper/venom/passes/base_pass.py index 4d1bfe9647..3951ac4455 100644 --- a/vyper/venom/passes/base_pass.py +++ b/vyper/venom/passes/base_pass.py @@ -1,4 +1,4 @@ -from vyper.venom.analysis.analysis import IRAnalysesCache +from vyper.venom.analysis import IRAnalysesCache from vyper.venom.function import IRFunction diff --git a/vyper/venom/passes/branch_optimization.py b/vyper/venom/passes/branch_optimization.py index 354aab7900..d5b0ed9809 100644 --- a/vyper/venom/passes/branch_optimization.py +++ b/vyper/venom/passes/branch_optimization.py @@ -1,4 +1,4 @@ -from vyper.venom.analysis.dfg import DFGAnalysis +from vyper.venom.analysis import DFGAnalysis from vyper.venom.passes.base_pass import IRPass diff --git a/vyper/venom/passes/dft.py b/vyper/venom/passes/dft.py index f45a60079c..85f27867a7 100644 --- a/vyper/venom/passes/dft.py +++ b/vyper/venom/passes/dft.py @@ -1,5 +1,5 @@ from vyper.utils import OrderedSet -from vyper.venom.analysis.dfg import DFGAnalysis +from vyper.venom.analysis import DFGAnalysis from vyper.venom.basicblock import IRBasicBlock, IRInstruction, IRVariable from vyper.venom.function import IRFunction from vyper.venom.passes.base_pass import IRPass diff --git a/vyper/venom/passes/make_ssa.py b/vyper/venom/passes/make_ssa.py index a803514d8b..56d3e1b7d3 100644 --- a/vyper/venom/passes/make_ssa.py +++ b/vyper/venom/passes/make_ssa.py @@ -1,7 +1,5 @@ from vyper.utils import OrderedSet -from vyper.venom.analysis.cfg import CFGAnalysis -from vyper.venom.analysis.dominators import DominatorTreeAnalysis -from vyper.venom.analysis.liveness import LivenessAnalysis +from vyper.venom.analysis import CFGAnalysis, DominatorTreeAnalysis, LivenessAnalysis from vyper.venom.basicblock import IRBasicBlock, IRInstruction, IROperand, IRVariable from vyper.venom.passes.base_pass import IRPass diff --git a/vyper/venom/passes/mem2var.py b/vyper/venom/passes/mem2var.py index 74977d6279..f93924d449 100644 --- a/vyper/venom/passes/mem2var.py +++ b/vyper/venom/passes/mem2var.py @@ -1,6 +1,4 @@ -from vyper.venom.analysis.cfg import CFGAnalysis -from vyper.venom.analysis.dfg import DFGAnalysis -from vyper.venom.analysis.liveness import LivenessAnalysis +from vyper.venom.analysis import CFGAnalysis, DFGAnalysis, LivenessAnalysis from vyper.venom.basicblock import IRInstruction, IRVariable from vyper.venom.function import IRFunction from vyper.venom.passes.base_pass import IRPass diff --git a/vyper/venom/passes/normalization.py b/vyper/venom/passes/normalization.py index cf44c3cf89..7ca242c74e 100644 --- a/vyper/venom/passes/normalization.py +++ b/vyper/venom/passes/normalization.py @@ -1,5 +1,5 @@ from vyper.exceptions import CompilerPanic -from vyper.venom.analysis.cfg import CFGAnalysis +from vyper.venom.analysis import CFGAnalysis from vyper.venom.basicblock import IRBasicBlock, IRLabel from vyper.venom.passes.base_pass import IRPass diff --git a/vyper/venom/passes/remove_unused_variables.py b/vyper/venom/passes/remove_unused_variables.py index be9c1ed535..3ce5bdf2d3 100644 --- a/vyper/venom/passes/remove_unused_variables.py +++ b/vyper/venom/passes/remove_unused_variables.py @@ -1,6 +1,5 @@ from vyper.utils import OrderedSet -from vyper.venom.analysis.dfg import DFGAnalysis -from vyper.venom.analysis.liveness import LivenessAnalysis +from vyper.venom.analysis import DFGAnalysis, LivenessAnalysis from vyper.venom.basicblock import IRInstruction from vyper.venom.passes.base_pass import IRPass @@ -27,6 +26,7 @@ def run_pass(self): self._process_instruction(inst) self.analyses_cache.invalidate_analysis(LivenessAnalysis) + self.analyses_cache.invalidate_analysis(DFGAnalysis) def _process_instruction(self, inst): if inst.output is None: diff --git a/vyper/venom/passes/sccp/sccp.py b/vyper/venom/passes/sccp/sccp.py index 8596bc8405..7966863081 100644 --- a/vyper/venom/passes/sccp/sccp.py +++ b/vyper/venom/passes/sccp/sccp.py @@ -5,9 +5,7 @@ from vyper.exceptions import CompilerPanic, StaticAssertionException from vyper.utils import OrderedSet -from vyper.venom.analysis.analysis import IRAnalysesCache -from vyper.venom.analysis.cfg import CFGAnalysis -from vyper.venom.analysis.dominators import DominatorTreeAnalysis +from vyper.venom.analysis import CFGAnalysis, DominatorTreeAnalysis, IRAnalysesCache from vyper.venom.basicblock import ( IRBasicBlock, IRInstruction, diff --git a/vyper/venom/passes/simplify_cfg.py b/vyper/venom/passes/simplify_cfg.py index 1409f43947..acf37376e0 100644 --- a/vyper/venom/passes/simplify_cfg.py +++ b/vyper/venom/passes/simplify_cfg.py @@ -1,6 +1,6 @@ from vyper.exceptions import CompilerPanic from vyper.utils import OrderedSet -from vyper.venom.analysis.cfg import CFGAnalysis +from vyper.venom.analysis import CFGAnalysis from vyper.venom.basicblock import IRBasicBlock, IRLabel from vyper.venom.passes.base_pass import IRPass diff --git a/vyper/venom/passes/stack_reorder.py b/vyper/venom/passes/stack_reorder.py deleted file mode 100644 index a92fe0e626..0000000000 --- a/vyper/venom/passes/stack_reorder.py +++ /dev/null @@ -1,23 +0,0 @@ -from vyper.utils import OrderedSet -from vyper.venom.basicblock import IRBasicBlock -from vyper.venom.passes.base_pass import IRPass - - -class StackReorderPass(IRPass): - visited: OrderedSet - - def _reorder_stack(self): - pass - - def _visit(self, bb: IRBasicBlock): - if bb in self.visited: - return - self.visited.add(bb) - - for bb_out in bb.cfg_out: - self._visit(bb_out) - - def _run_pass(self): - entry = self.function.entry - self.visited = OrderedSet() - self._visit(entry) diff --git a/vyper/venom/passes/store_elimination.py b/vyper/venom/passes/store_elimination.py index 17b9ce995a..0ecd324e26 100644 --- a/vyper/venom/passes/store_elimination.py +++ b/vyper/venom/passes/store_elimination.py @@ -1,6 +1,4 @@ -from vyper.venom.analysis.cfg import CFGAnalysis -from vyper.venom.analysis.dfg import DFGAnalysis -from vyper.venom.analysis.liveness import LivenessAnalysis +from vyper.venom.analysis import CFGAnalysis, DFGAnalysis, LivenessAnalysis from vyper.venom.basicblock import IRVariable from vyper.venom.passes.base_pass import IRPass diff --git a/vyper/venom/passes/store_expansion.py b/vyper/venom/passes/store_expansion.py index 7718e67d33..be5eb3d95d 100644 --- a/vyper/venom/passes/store_expansion.py +++ b/vyper/venom/passes/store_expansion.py @@ -1,5 +1,4 @@ -from vyper.venom.analysis.dfg import DFGAnalysis -from vyper.venom.analysis.liveness import LivenessAnalysis +from vyper.venom.analysis import DFGAnalysis, LivenessAnalysis from vyper.venom.basicblock import IRInstruction, IRLiteral, IRVariable from vyper.venom.passes.base_pass import IRPass diff --git a/vyper/venom/venom_to_assembly.py b/vyper/venom/venom_to_assembly.py index 32b22a8608..264ec35eee 100644 --- a/vyper/venom/venom_to_assembly.py +++ b/vyper/venom/venom_to_assembly.py @@ -10,9 +10,7 @@ optimize_assembly, ) from vyper.utils import MemoryPositions, OrderedSet -from vyper.venom.analysis.analysis import IRAnalysesCache -from vyper.venom.analysis.equivalent_vars import VarEquivalenceAnalysis -from vyper.venom.analysis.liveness import LivenessAnalysis +from vyper.venom.analysis import IRAnalysesCache, LivenessAnalysis, VarEquivalenceAnalysis from vyper.venom.basicblock import ( IRBasicBlock, IRInstruction, @@ -22,7 +20,7 @@ IRVariable, ) from vyper.venom.context import IRContext -from vyper.venom.passes.normalization import NormalizationPass +from vyper.venom.passes import NormalizationPass from vyper.venom.stack_model import StackModel DEBUG_SHOW_COST = False