diff --git a/tests/functional/syntax/modules/test_initializers.py b/tests/functional/syntax/modules/test_initializers.py index 57e847ab58..1ddf6c99cb 100644 --- a/tests/functional/syntax/modules/test_initializers.py +++ b/tests/functional/syntax/modules/test_initializers.py @@ -200,10 +200,10 @@ def foo(): @deploy def __init__(): - lib2.__init__() # demonstrate we can call lib1.__init__ through lib2.lib1 # (not sure this should be allowed, really. lib2.lib1.__init__() + lib2.__init__() """ input_bundle = make_input_bundle({"lib1.vy": lib1, "lib2.vy": lib2}) @@ -1811,3 +1811,1069 @@ def foo() -> uint256: data = CompilerData(main, input_bundle=input_bundle) foo_t = data.function_signatures["foo"] assert len(foo_t._variable_reads) == 0 + + +def test_init_in_both_if_branches(make_input_bundle): + other = """ +counter: uint256 + +@deploy +def __init__(): + pass + """ + main = """ +import other + +initializes: other + +@deploy +def __init__(): + if True: + other.__init__() + else: + other.__init__() + """ + input_bundle = make_input_bundle({"other.vy": other}) + assert compile_code(main, input_bundle=input_bundle) is not None + + +def test_double_init_if_else(make_input_bundle): + other = """ +counter: uint256 + +@deploy +def __init__(): + pass + """ + main = """ +import other + +initializes: other + +@deploy +def __init__(): + other.__init__() + if True: + other.__init__() + else: + other.__init__() + """ + input_bundle = make_input_bundle({"other.vy": other}) + with pytest.raises(InitializerException) as e: + compile_code(main, input_bundle=input_bundle) + assert ( + e.value.message + == "tried to initialize `other`, but its __init__() function was already called!" + ) + + +def test_init_followed_by_for_loop(make_input_bundle): + other = """ +counter: uint256 + +@deploy +def __init__(): + pass + """ + main = """ +import other + +initializes: other + +@deploy +def __init__(): + other.__init__() + for i: uint256 in range(10): + pass + """ + input_bundle = make_input_bundle({"other.vy": other}) + assert compile_code(main, input_bundle=input_bundle) is not None + + +# Shared module sources for the dependency-ordering tests below. +# Each `uses:` declaration must be backed by real state access (otherwise +# BorrowException fires before `is_initialized` ever runs). +_LIB1 = """ +counter: uint256 + +@deploy +def __init__(): + pass +""" + +_LIB2_USES_LIB1 = """ +import lib1 + +uses: lib1 + +counter: uint256 + +@deploy +def __init__(): + pass + +@internal +def touch(): + lib1.counter += 1 +""" + +_LIB1_NO_INIT = """ +counter: uint256 +""" + +_LIB2_NO_DEPS = """ +counter: uint256 + +@deploy +def __init__(): + pass +""" + +_LIB3_USES_BOTH = """ +import lib1 +import lib2 + +uses: (lib1, lib2) + +@deploy +def __init__(): + pass + +@internal +def touch(): + lib1.counter += 1 + lib2.counter += 1 +""" + + +def test_init_before_dependency(make_input_bundle): + main = """ +import lib1 +import lib2 + +initializes: lib2[lib1 := lib1] +initializes: lib1 + +@deploy +def __init__(): + lib2.__init__() + lib1.__init__() + """ + input_bundle = make_input_bundle({"lib1.vy": _LIB1, "lib2.vy": _LIB2_USES_LIB1}) + with pytest.raises(InitializerException) as e: + compile_code(main, input_bundle=input_bundle) + msg = ( + "tried to initialize `lib2`, but it depends on the following modules " + "which have not been initialized: lib1" + ) + assert e.value._message == msg + + +def test_init_before_multiple_dependencies(make_input_bundle): + main = """ +import lib1 +import lib2 +import lib3 + +initializes: lib1 +initializes: lib2 +initializes: lib3[lib1 := lib1, lib2 := lib2] + +@deploy +def __init__(): + lib3.__init__() + lib1.__init__() + lib2.__init__() + """ + input_bundle = make_input_bundle( + {"lib1.vy": _LIB1, "lib2.vy": _LIB2_NO_DEPS, "lib3.vy": _LIB3_USES_BOTH} + ) + with pytest.raises(InitializerException) as e: + compile_code(main, input_bundle=input_bundle) + msg = ( + "tried to initialize `lib3`, but it depends on the following modules " + "which have not been initialized: lib1, lib2" + ) + assert e.value._message == msg + + +def test_init_before_some_dependencies(make_input_bundle): + main = """ +import lib1 +import lib2 +import lib3 + +initializes: lib1 +initializes: lib2 +initializes: lib3[lib1 := lib1, lib2 := lib2] + +@deploy +def __init__(): + lib1.__init__() + lib3.__init__() + lib2.__init__() + """ + input_bundle = make_input_bundle( + {"lib1.vy": _LIB1, "lib2.vy": _LIB2_NO_DEPS, "lib3.vy": _LIB3_USES_BOTH} + ) + with pytest.raises(InitializerException) as e: + compile_code(main, input_bundle=input_bundle) + msg = ( + "tried to initialize `lib3`, but it depends on the following modules " + "which have not been initialized: lib2" + ) + assert e.value._message == msg + + +def test_dep_init_in_only_one_if_branch_then_parent(make_input_bundle): + # The existing "not guaranteed to be reachable" check fires before + # the new ordering check is reached: lib1 is only initialized in + # one branch of the `if`, which is detected when the branches merge. + main = """ +import lib1 +import lib2 + +initializes: lib2[lib1 := lib1] +initializes: lib1 + +@deploy +def __init__(use: bool): + if use: + lib1.__init__() + lib2.__init__() + """ + input_bundle = make_input_bundle({"lib1.vy": _LIB1, "lib2.vy": _LIB2_USES_LIB1}) + with pytest.raises(InitializerException) as e: + compile_code(main, input_bundle=input_bundle) + assert "not guaranteed to be reachable" in e.value._message + + +def test_init_attribute_path_before_parent(make_input_bundle): + # `lib2.lib1.__init__()` reaches lib1 through lib2's namespace; the + # ordering rule must still treat it as a lib1 init and reject the + # reverse order. + main = """ +import lib1 +import lib2 + +initializes: lib2[lib1 := lib1] +initializes: lib1 + +@deploy +def __init__(): + lib2.__init__() + lib2.lib1.__init__() + """ + input_bundle = make_input_bundle({"lib1.vy": _LIB1, "lib2.vy": _LIB2_USES_LIB1}) + with pytest.raises(InitializerException) as e: + compile_code(main, input_bundle=input_bundle) + msg = ( + "tried to initialize `lib2`, but it depends on the following modules " + "which have not been initialized: lib1" + ) + assert e.value._message == msg + + +def test_init_after_dependency(make_input_bundle): + main = """ +import lib1 +import lib2 + +initializes: lib2[lib1 := lib1] +initializes: lib1 + +@deploy +def __init__(): + lib1.__init__() + lib2.__init__() + """ + input_bundle = make_input_bundle({"lib1.vy": _LIB1, "lib2.vy": _LIB2_USES_LIB1}) + assert compile_code(main, input_bundle=input_bundle) is not None + + +def test_init_after_dependency_in_both_branches(make_input_bundle): + main = """ +import lib1 +import lib2 + +initializes: lib2[lib1 := lib1] +initializes: lib1 + +@deploy +def __init__(use: bool): + if use: + lib1.__init__() + lib2.__init__() + else: + lib1.__init__() + lib2.__init__() + """ + input_bundle = make_input_bundle({"lib1.vy": _LIB1, "lib2.vy": _LIB2_USES_LIB1}) + assert compile_code(main, input_bundle=input_bundle) is not None + + +def test_init_chain_of_dependencies(make_input_bundle): + main = """ +import lib1 +import lib2 +import lib3 + +initializes: lib1 +initializes: lib2[lib1 := lib1] +initializes: lib3[lib1 := lib1, lib2 := lib2] + +@deploy +def __init__(): + lib1.__init__() + lib2.__init__() + lib3.__init__() + """ + input_bundle = make_input_bundle( + {"lib1.vy": _LIB1, "lib2.vy": _LIB2_USES_LIB1, "lib3.vy": _LIB3_USES_BOTH} + ) + assert compile_code(main, input_bundle=input_bundle) is not None + + +def test_init_with_dependency_without_init_function(make_input_bundle): + # lib1 has no __init__(); it is filtered out of `initializing_nodes`. + # The dependency loop in `is_initialized` must skip it (otherwise the + # dict lookup would KeyError when processing `lib2.__init__()`). + main = """ +import lib1 +import lib2 + +initializes: lib2[lib1 := lib1] +initializes: lib1 + +@deploy +def __init__(): + lib2.__init__() + """ + input_bundle = make_input_bundle({"lib1.vy": _LIB1_NO_INIT, "lib2.vy": _LIB2_USES_LIB1}) + assert compile_code(main, input_bundle=input_bundle) is not None + + +def test_init_transitive_dependency_unchecked(make_input_bundle): + # The dependency-ordering check is one level deep: when initializing + # lib3 (direct deps: lib1, lib2), only those direct deps' init must + # have run. Correct order compiles cleanly. + main = """ +import lib1 +import lib2 +import lib3 + +initializes: lib1 +initializes: lib2[lib1 := lib1] +initializes: lib3[lib1 := lib1, lib2 := lib2] + +@deploy +def __init__(): + lib1.__init__() + lib2.__init__() + lib3.__init__() + """ + input_bundle = make_input_bundle( + {"lib1.vy": _LIB1, "lib2.vy": _LIB2_USES_LIB1, "lib3.vy": _LIB3_USES_BOTH} + ) + assert compile_code(main, input_bundle=input_bundle) is not None + + +def test_init_then_if_else_no_inner_init(make_input_bundle): + main = """ +import lib1 + +initializes: lib1 + +@deploy +def __init__(): + lib1.__init__() + if True: + x: uint256 = 1 + else: + x: uint256 = 2 + """ + input_bundle = make_input_bundle({"lib1.vy": _LIB1}) + assert compile_code(main, input_bundle=input_bundle) is not None + + +def test_init_then_if_no_else(make_input_bundle): + main = """ +import lib1 + +initializes: lib1 + +@deploy +def __init__(): + lib1.__init__() + if True: + x: uint256 = 1 + """ + input_bundle = make_input_bundle({"lib1.vy": _LIB1}) + assert compile_code(main, input_bundle=input_bundle) is not None + + +def test_init_inside_for_loop(make_input_bundle): + main = """ +import lib1 + +initializes: lib1 + +@deploy +def __init__(): + for i: uint256 in range(10): + lib1.__init__() + """ + input_bundle = make_input_bundle({"lib1.vy": _LIB1}) + with pytest.raises(InitializerException) as e: + compile_code(main, input_bundle=input_bundle) + assert "not guaranteed to be reachable" in e.value._message + assert "present in a for loop" in e.value._message + + +def test_duplicate_init_in_single_branch(make_input_bundle): + main = """ +import lib1 + +initializes: lib1 + +@deploy +def __init__(): + if True: + lib1.__init__() + lib1.__init__() + else: + lib1.__init__() + """ + input_bundle = make_input_bundle({"lib1.vy": _LIB1}) + with pytest.raises(InitializerException) as e: + compile_code(main, input_bundle=input_bundle) + assert ( + e.value._message + == "tried to initialize `lib1`, but its __init__() function was already called!" + ) + + +def test_init_only_in_if_no_else(make_input_bundle): + main = """ +import lib1 + +initializes: lib1 + +@deploy +def __init__(): + if True: + lib1.__init__() + """ + input_bundle = make_input_bundle({"lib1.vy": _LIB1}) + with pytest.raises(InitializerException) as e: + compile_code(main, input_bundle=input_bundle) + assert "not guaranteed to be reachable" in e.value._message + assert "present only in a single branch of an if" in e.value._message + + +def test_nested_if_asymmetric(make_input_bundle): + # Outer if/else is balanced (both branches reach init), but the inner if + # only initializes in one branch — the inner asymmetry must be detected. + main = """ +import lib1 + +initializes: lib1 + +@deploy +def __init__(): + if True: + if True: + lib1.__init__() + else: + lib1.__init__() + """ + input_bundle = make_input_bundle({"lib1.vy": _LIB1}) + with pytest.raises(InitializerException) as e: + compile_code(main, input_bundle=input_bundle) + assert "not guaranteed to be reachable" in e.value._message + assert "present only in a single branch of an if" in e.value._message + + +def test_raise_in_then_init_in_else(make_input_bundle): + main = """ +import lib1 + +initializes: lib1 + +@deploy +def __init__(cond: bool): + if cond: + raise "nope" + else: + lib1.__init__() + """ + input_bundle = make_input_bundle({"lib1.vy": _LIB1}) + assert compile_code(main, input_bundle=input_bundle) is not None + + +def test_init_in_then_raise_in_else(make_input_bundle): + main = """ +import lib1 + +initializes: lib1 + +@deploy +def __init__(cond: bool): + if cond: + lib1.__init__() + else: + raise "nope" + """ + input_bundle = make_input_bundle({"lib1.vy": _LIB1}) + assert compile_code(main, input_bundle=input_bundle) is not None + + +def test_no_init_in_then_raise_in_else(make_input_bundle): + main = """ +import lib1 + +initializes: lib1 + +@deploy +def __init__(cond: bool): + if cond: + pass + else: + raise "nope" + """ + input_bundle = make_input_bundle({"lib1.vy": _LIB1}) + with pytest.raises(InitializerException) as e: + compile_code(main, input_bundle=input_bundle) + assert e.value._message == "not initialized!" + + +def test_both_branches_raise_with_initializer(make_input_bundle): + main = """ +import lib1 + +initializes: lib1 + +@deploy +def __init__(cond: bool): + if cond: + raise "nope" + else: + raise "still nope" + """ + input_bundle = make_input_bundle({"lib1.vy": _LIB1}) + assert compile_code(main, input_bundle=input_bundle) is not None + + +def test_init_then_raise_in_one_branch(make_input_bundle): + main = """ +import lib1 + +initializes: lib1 + +@deploy +def __init__(cond: bool): + if cond: + lib1.__init__() + raise "nope" + else: + lib1.__init__() + """ + input_bundle = make_input_bundle({"lib1.vy": _LIB1}) + assert compile_code(main, input_bundle=input_bundle) is not None + + +def test_nested_if_both_branches_raise_then_outer_else_inits(make_input_bundle): + main = """ +import lib1 + +initializes: lib1 + +@deploy +def __init__(cond1: bool, cond2: bool): + if cond1: + if cond2: + raise "nope" + else: + raise "still nope" + else: + lib1.__init__() + """ + input_bundle = make_input_bundle({"lib1.vy": _LIB1}) + assert compile_code(main, input_bundle=input_bundle) is not None + + +def test_init_body_only_raise_with_initializer(make_input_bundle): + main = """ +import lib1 + +initializes: lib1 + +@deploy +def __init__(): + raise "nope" + """ + input_bundle = make_input_bundle({"lib1.vy": _LIB1}) + assert compile_code(main, input_bundle=input_bundle) is not None + + +def test_init_body_only_raise_no_initializers(make_input_bundle): + main = """ +@deploy +def __init__(): + raise "nope" + """ + assert compile_code(main) is not None + + +def test_raise_in_for_loop_body_alone(make_input_bundle): + main = """ +@deploy +def __init__(): + for i: uint256 in range(10): + raise "nope" + """ + assert compile_code(main) is not None + + +def test_init_then_for_loop_with_raise(make_input_bundle): + main = """ +import lib1 + +initializes: lib1 + +@deploy +def __init__(): + lib1.__init__() + for i: uint256 in range(10): + raise "nope" + """ + input_bundle = make_input_bundle({"lib1.vy": _LIB1}) + assert compile_code(main, input_bundle=input_bundle) is not None + + +def test_dependency_ordering_preserved_with_raise_wildcard(make_input_bundle): + main = """ +import lib1 +import lib2 + +initializes: lib2[lib1 := lib1] +initializes: lib1 + +@deploy +def __init__(cond: bool): + if cond: + raise "nope" + else: + lib2.__init__() + lib1.__init__() + """ + input_bundle = make_input_bundle({"lib1.vy": _LIB1, "lib2.vy": _LIB2_USES_LIB1}) + with pytest.raises(InitializerException) as e: + compile_code(main, input_bundle=input_bundle) + msg = ( + "tried to initialize `lib2`, but it depends on the following modules " + "which have not been initialized: lib1" + ) + assert e.value._message == msg + + +def test_dependency_init_split_across_raise_branches(make_input_bundle): + main = """ +import lib1 +import lib2 + +initializes: lib2[lib1 := lib1] +initializes: lib1 + +@deploy +def __init__(cond: bool): + if cond: + raise "nope" + else: + lib1.__init__() + lib2.__init__() + """ + input_bundle = make_input_bundle({"lib1.vy": _LIB1, "lib2.vy": _LIB2_USES_LIB1}) + assert compile_code(main, input_bundle=input_bundle) is not None + + +def test_dep_init_after_raise_wildcard_if(make_input_bundle): + main = """ +import lib1 +import lib2 + +initializes: lib1 +initializes: lib2[lib1 := lib1] + +@deploy +def __init__(cond: bool): + if cond: + lib1.__init__() + else: + raise "nope" + lib2.__init__() + """ + input_bundle = make_input_bundle({"lib1.vy": _LIB1, "lib2.vy": _LIB2_USES_LIB1}) + assert compile_code(main, input_bundle=input_bundle) is not None + + +def test_nested_if_inner_raise_as_wildcard(make_input_bundle): + main = """ +import lib1 + +initializes: lib1 + +@deploy +def __init__(cond1: bool, cond2: bool): + if cond1: + if cond2: + raise "nope" + else: + lib1.__init__() + else: + lib1.__init__() + """ + input_bundle = make_input_bundle({"lib1.vy": _LIB1}) + assert compile_code(main, input_bundle=input_bundle) is not None + + +def test_assert_false_is_not_a_wildcard(make_input_bundle): + main = """ +import lib1 + +initializes: lib1 + +@deploy +def __init__(cond: bool): + if cond: + assert False, "nope" + else: + lib1.__init__() + """ + input_bundle = make_input_bundle({"lib1.vy": _LIB1}) + with pytest.raises(InitializerException) as e: + compile_code(main, input_bundle=input_bundle) + assert "not guaranteed to be reachable" in e.value._message + assert "present only in a single branch of an if" in e.value._message + + +def test_return_at_end_after_init(make_input_bundle): + main = """ +import lib1 + +initializes: lib1 + +@deploy +def __init__(): + lib1.__init__() + return + """ + input_bundle = make_input_bundle({"lib1.vy": _LIB1}) + assert compile_code(main, input_bundle=input_bundle) is not None + + +def test_bare_return_no_initializers(make_input_bundle): + main = """ +@deploy +def __init__(): + return + """ + assert compile_code(main) is not None + + +def test_return_before_required_init(make_input_bundle): + main = """ +import lib1 + +initializes: lib1 + +@deploy +def __init__(): + return + """ + input_bundle = make_input_bundle({"lib1.vy": _LIB1}) + with pytest.raises(InitializerException) as e: + compile_code(main, input_bundle=input_bundle) + assert e.value._message == "not initialized!" + + +def test_init_after_return_is_dead(make_input_bundle): + main = """ +import lib1 + +initializes: lib1 + +@deploy +def __init__(): + return + lib1.__init__() + """ + input_bundle = make_input_bundle({"lib1.vy": _LIB1}) + with pytest.raises(StructureException) as e: + compile_code(main, input_bundle=input_bundle) + assert e.value._message == "Unreachable code!" + + +def test_return_in_then_after_init_else_inits(make_input_bundle): + main = """ +import lib1 + +initializes: lib1 + +@deploy +def __init__(cond: bool): + if cond: + lib1.__init__() + return + else: + lib1.__init__() + """ + input_bundle = make_input_bundle({"lib1.vy": _LIB1}) + assert compile_code(main, input_bundle=input_bundle) is not None + + +def test_return_in_then_without_init_else_inits(make_input_bundle): + main = """ +import lib1 + +initializes: lib1 + +@deploy +def __init__(cond: bool): + if cond: + return + else: + lib1.__init__() + """ + input_bundle = make_input_bundle({"lib1.vy": _LIB1}) + with pytest.raises(InitializerException) as e: + compile_code(main, input_bundle=input_bundle) + assert e.value._message == "not initialized!" + + +def test_both_branches_return_after_init(make_input_bundle): + main = """ +import lib1 + +initializes: lib1 + +@deploy +def __init__(cond: bool): + if cond: + lib1.__init__() + return + else: + lib1.__init__() + return + """ + input_bundle = make_input_bundle({"lib1.vy": _LIB1}) + assert compile_code(main, input_bundle=input_bundle) is not None + + +def test_both_branches_return_without_init(make_input_bundle): + main = """ +import lib1 + +initializes: lib1 + +@deploy +def __init__(cond: bool): + if cond: + return + else: + return + """ + input_bundle = make_input_bundle({"lib1.vy": _LIB1}) + with pytest.raises(InitializerException) as e: + compile_code(main, input_bundle=input_bundle) + assert e.value._message == "not initialized!" + + +def test_return_in_then_without_else_init_after(make_input_bundle): + main = """ +import lib1 + +initializes: lib1 + +@deploy +def __init__(): + if True: + return + lib1.__init__() + """ + input_bundle = make_input_bundle({"lib1.vy": _LIB1}) + with pytest.raises(InitializerException) as e: + compile_code(main, input_bundle=input_bundle) + assert e.value._message == "not initialized!" + + +def test_return_after_if_validates_total(make_input_bundle): + main = """ +import lib1 + +initializes: lib1 + +@deploy +def __init__(cond: bool): + if cond: + lib1.__init__() + else: + lib1.__init__() + return + """ + input_bundle = make_input_bundle({"lib1.vy": _LIB1}) + assert compile_code(main, input_bundle=input_bundle) is not None + + +def test_nested_if_return_in_inner_branch_without_init(make_input_bundle): + main = """ +import lib1 + +initializes: lib1 + +@deploy +def __init__(cond1: bool, cond2: bool): + if cond1: + if cond2: + return + else: + lib1.__init__() + else: + lib1.__init__() + """ + input_bundle = make_input_bundle({"lib1.vy": _LIB1}) + with pytest.raises(InitializerException) as e: + compile_code(main, input_bundle=input_bundle) + assert e.value._message == "not initialized!" + + +def test_return_in_for_body_no_initializers(make_input_bundle): + main = """ +@deploy +def __init__(): + for i: uint256 in range(10): + return + """ + assert compile_code(main) is not None + + +def test_return_in_for_body_required_init_missing(make_input_bundle): + main = """ +import lib1 + +initializes: lib1 + +@deploy +def __init__(): + for i: uint256 in range(10): + return + """ + input_bundle = make_input_bundle({"lib1.vy": _LIB1}) + with pytest.raises(InitializerException) as e: + compile_code(main, input_bundle=input_bundle) + assert e.value._message == "not initialized!" + + +def test_return_and_init_in_for_body(make_input_bundle): + main = """ +import lib1 + +initializes: lib1 + +@deploy +def __init__(): + for i: uint256 in range(10): + lib1.__init__() + return + """ + input_bundle = make_input_bundle({"lib1.vy": _LIB1}) + with pytest.raises(InitializerException) as e: + compile_code(main, input_bundle=input_bundle) + assert e.value._message == "not initialized!" + + +def test_init_then_for_loop_with_return(make_input_bundle): + main = """ +import lib1 + +initializes: lib1 + +@deploy +def __init__(): + lib1.__init__() + for i: uint256 in range(10): + return + """ + input_bundle = make_input_bundle({"lib1.vy": _LIB1}) + assert compile_code(main, input_bundle=input_bundle) is not None + + +def test_return_after_dependent_before_dependency(make_input_bundle): + main = """ +import lib1 +import lib2 + +initializes: lib2[lib1 := lib1] +initializes: lib1 + +@deploy +def __init__(): + lib2.__init__() + return + """ + input_bundle = make_input_bundle({"lib1.vy": _LIB1, "lib2.vy": _LIB2_USES_LIB1}) + with pytest.raises(InitializerException) as e: + compile_code(main, input_bundle=input_bundle) + msg = ( + "tried to initialize `lib2`, but it depends on the following modules " + "which have not been initialized: lib1" + ) + assert e.value._message == msg + + +def test_return_in_branch_with_correct_dep_order(make_input_bundle): + main = """ +import lib1 +import lib2 + +initializes: lib1 +initializes: lib2[lib1 := lib1] + +@deploy +def __init__(cond: bool): + if cond: + lib1.__init__() + lib2.__init__() + return + else: + lib1.__init__() + lib2.__init__() + return + """ + input_bundle = make_input_bundle({"lib1.vy": _LIB1, "lib2.vy": _LIB2_USES_LIB1}) + assert compile_code(main, input_bundle=input_bundle) is not None + + +def test_for_loop_with_guarded_init_and_return_then_raise(make_input_bundle): + lib = """ +counter: uint256 + +@deploy +def __init__(x: uint256): + self.counter = x + """ + main = """ +import lib + +initializes: lib + +@deploy +def __init__(xs: DynArray[uint256, 10]): + for x: uint256 in xs: + if x > 0: + lib.__init__(x) + return + raise "no valid x in xs" + """ + input_bundle = make_input_bundle({"lib.vy": lib}) + assert compile_code(main, input_bundle=input_bundle) is not None diff --git a/vyper/semantics/analysis/base.py b/vyper/semantics/analysis/base.py index 80c9731853..9698725932 100644 --- a/vyper/semantics/analysis/base.py +++ b/vyper/semantics/analysis/base.py @@ -119,6 +119,11 @@ def set_ownership(self, module_ownership: ModuleOwnership, node: Optional[vy_ast def __hash__(self): return hash(id(self.module_t)) + def __eq__(self, other): + if not isinstance(other, ModuleInfo): + return NotImplemented + return self.module_t is other.module_t + @dataclass class ImportInfo(AnalysisResult): @@ -145,6 +150,8 @@ def to_dict(self): return ret +# TODO: Remove, the dependencies field is never used, +# and the node field is redundant with ModuleInfo.ownership_decl # analysis result of InitializesDecl @dataclass class InitializesInfo(AnalysisResult): @@ -153,6 +160,7 @@ class InitializesInfo(AnalysisResult): node: Optional[vy_ast.VyperNode] = None +# TODO: Remove, the node field is redundant with ModuleInfo.ownership_decl # analysis result of UsesDecl @dataclass class UsesInfo(AnalysisResult): diff --git a/vyper/semantics/analysis/local.py b/vyper/semantics/analysis/local.py index 45698439ba..9879947e9d 100644 --- a/vyper/semantics/analysis/local.py +++ b/vyper/semantics/analysis/local.py @@ -120,11 +120,7 @@ def is_terminated(block: list[vy_ast.VyperNode]) -> bool: terminated = True if isinstance(node, vy_ast.If): - if node.orelse is not None: - terminated = is_terminated(node.body) and is_terminated(node.orelse) - else: - # call is_terminated for its side effects - is_terminated(node.body) + terminated = is_terminated(node.body) and is_terminated(node.orelse) if isinstance(node, vy_ast.For): # call is_terminated for its side effects diff --git a/vyper/semantics/analysis/module.py b/vyper/semantics/analysis/module.py index dfe9b3c70c..5b134d160b 100644 --- a/vyper/semantics/analysis/module.py +++ b/vyper/semantics/analysis/module.py @@ -1,6 +1,7 @@ from __future__ import annotations -from itertools import zip_longest +from collections import defaultdict +from itertools import chain, zip_longest from typing import Optional from vyper import ast as vy_ast @@ -148,7 +149,7 @@ def _analyze_module_bodies(module_ast: vy_ast.Module) -> None: with override_global_namespace(namespace): analyze_functions(module_ast) _validate_exports_uses(module_ast, module_t) - _validate_initialized_modules(module_ast, module_t) + ConstructorValidator(module_t).validate() _validate_used_modules(module_ast, module_t) @@ -192,73 +193,231 @@ def _validate_used_modules(module_ast: vy_ast.Module, module_t: ModuleT) -> None err_list.raise_if_not_empty() -def _validate_initialized_modules(module_ast: vy_ast.Module, module_t: ModuleT) -> None: - """Check all `initializes:` modules have `__init__()` called exactly once.""" - # only call `__init__()` for modules which have an - # `__init__()` function - should_initialize = { - t.module_info.module_t: t - for t in module_t.initialized_modules - if t.module_info.module_t.init_function is not None - } +class ConstructorValidator(VyperNodeVisitorBase): + """ + Check all `initializes:`-ed modules each have `__init__()` executed exactly once. + Also checks that the initializes calls are done in the correct order, + if a uses b, init of b is called before init of a. + + This check handles branching by requiring the set of initialized modules to be + the same in both branches. + (If one branch raises, we act as if it initialized all necessary modules.) + """ + + modules_to_initialize: list[ModuleInfo] + constructor: vy_ast.FunctionDef | None + + def __init__(self, module_t: ModuleT): + + self.modules_to_initialize = [ + t.module_info + for t in module_t.initialized_modules + if t.module_info.module_t.init_function is not None + ] + + init_fun = module_t.init_function + if init_fun is None: + self.constructor = None + else: + assert isinstance(init_fun.ast_def, vy_ast.FunctionDef) # help mypy + + self.constructor = init_fun.ast_def + + def validate(self): + if self.constructor is not None: + body = self.constructor.body + else: + body = [] + + init_calls_by_module = self.visit_block(body, defaultdict(list)) + + # is None when body ends in a revert or return (also considers branches, + # so if it ends with an 'if' where both branches return, it will also be none) + if init_calls_by_module is not None: + self._validate_init_return(init_calls_by_module) + + def _validate_init_return( + self, init_calls_by_module: dict[ModuleInfo, list[vy_ast.VyperNode]] + ) -> None: + """ + Checks that the correct modules have their init called when returning from the constructor + """ + + err_list = ExceptionList() + for module_info in self.modules_to_initialize: + init_calls = init_calls_by_module.get(module_info, []) + if len(init_calls) == 0: + msg = "not initialized!" + hint = f"add `{module_info.alias}.__init__()` to " + hint += "your `__init__()` function" + + err_list.append( + InitializerException( + msg, self.constructor, module_info.ownership_decl, hint=hint + ) + ) + err_list.raise_if_not_empty() + + for module_info in init_calls_by_module: + if module_info not in self.modules_to_initialize: + msg = f"tried to initialize `{module_info.alias}`, " + msg += "but it is not in initializer list!" + hint = f"add `initializes: {module_info.alias}` " + hint += "as a top-level statement to your contract" + raise InitializerException(msg, *init_calls_by_module[module_info], hint=hint) + + def visit_block( + self, block: list[vy_ast.VyperNode], init_calls: dict[ModuleInfo, list[vy_ast.VyperNode]] + ) -> dict[ModuleInfo, list[vy_ast.VyperNode]] | None: + # Make a copy so that branches do not interfere + init_calls = defaultdict(list, {k: v.copy() for k, v in init_calls.items()}) + + # Subset of init_calls that happen in this block + local_init_calls: dict[ModuleInfo, list[vy_ast.VyperNode]] = defaultdict(list) + + for node in block: + node_init_calls = self.visit(node, init_calls) + if node_init_calls is None: + # Return or Raise, terminates block with wildcard + return None + for module_info, calls in node_init_calls.items(): + init_calls[module_info] += calls + local_init_calls[module_info] += calls + + return local_init_calls + + def visit_Raise(self, _: vy_ast.Raise, init_calls: dict[ModuleInfo, list[vy_ast.VyperNode]]): + # If we raise, return wildcard + return None + + def visit_Return(self, _: vy_ast.Return, init_calls: dict[ModuleInfo, list[vy_ast.VyperNode]]): + # If we return, return wildcard + # Instead, move _validate_init_return inside ConstructorValidator + self._validate_init_return(init_calls) + return None - constructor = module_t.init_function + def visit_If(self, node: vy_ast.If, init_calls: dict[ModuleInfo, list[vy_ast.VyperNode]]): + then_nodes = self.visit_block(node.body, init_calls) + else_nodes = self.visit_block(node.orelse, init_calls) + + # If either branch reverts/returns, return the other + # (if both revert, will return None) + if then_nodes is None: + return else_nodes + elif else_nodes is None: + return then_nodes + + # TODO: UX: instead of raising on the first, batch them all together + for module_info in {**then_nodes, **else_nodes}: + if bool(then_nodes[module_info]) != bool(else_nodes[module_info]): + msg = f"`{module_info.alias}`.__init__() is not guaranteed to be reachable: " + msg += "present only in a single branch of an if" + raise InitializerException(msg, node) + + # If the context and the branches had init calls, + # then we would already have errored: "__init__() function was already called!" + assert init_calls[module_info] == [] or ( + then_nodes[module_info] == [] and else_nodes[module_info] == [] + ) - # Methods called by the constructor - init_calls: list[vy_ast.Call] = [] - if constructor is not None: - init_calls = constructor.ast_def.get_descendants(vy_ast.Call) # type: ignore + merged: dict[ModuleInfo, list[vy_ast.VyperNode]] = defaultdict(list) + for module_info, calls in chain(then_nodes.items(), else_nodes.items()): + merged[module_info] += calls + return merged + + def visit_For(self, node: vy_ast.For, init_calls: dict[ModuleInfo, list[vy_ast.VyperNode]]): + loop_nodes = self.visit_block(node.body, init_calls) + if loop_nodes is not None: + for module_info in loop_nodes: + if len(loop_nodes[module_info]) != 0: + msg = f"`{module_info.alias}`.__init__() is not guaranteed to be reachable: " + msg += "present in a for loop" + raise InitializerException(msg, node) + # Note: the above is more fine-grained than simply forbidding init calls in a for loop, + # it allows the following: + # def __init__(xs): + # for x in xs: + # if is_valid(x): + # lib.__init__(x) + # return + # raise "No valid x in xs" + + return {} + + def _validate_call( + self, call: vy_ast.Call, init_calls: dict[ModuleInfo, list[vy_ast.VyperNode]] + ) -> ModuleInfo | None: + + expr_info = call.func._expr_info - seen_initializers: dict[ModuleT, vy_ast.VyperNode] = {} - for call_node in init_calls: - expr_info = call_node.func._expr_info if expr_info is None: # this can happen for range() calls; CMC 2024-02-05 try to # refactor so that range() is properly tagged. - continue + return None call_t = expr_info.typ if not isinstance(call_t, ContractFunctionT): - continue + return None if not call_t.is_constructor: - continue + return None # XXX: check this works as expected for nested attributes - initialized_module = call_node.func.value._expr_info.module_info # type: ignore + other_module_info = call.func.value._expr_info.module_info # type: ignore + + # If A.__init__ is called, make sure it was not called before - if initialized_module.module_t in seen_initializers: - seen_location = seen_initializers[initialized_module.module_t] - msg = f"tried to initialize `{initialized_module.alias}`, " + init_calls_m = init_calls[other_module_info] + + if len(init_calls_m) != 0: + msg = f"tried to initialize `{other_module_info.alias}`, " msg += "but its __init__() function was already called!" - raise InitializerException(msg, call_node.func, seen_location) + raise InitializerException(msg, call.func, init_calls_m) - if initialized_module.module_t not in should_initialize: - msg = f"tried to initialize `{initialized_module.alias}`, " - msg += "but it is not in initializer list!" - hint = f"add `initializes: {initialized_module.alias}` " - hint += "as a top-level statement to your contract" - raise InitializerException(msg, call_node.func, hint=hint) + # If A uses B, make sure B.__init__ is called before A.__init__ - del should_initialize[initialized_module.module_t] - seen_initializers[initialized_module.module_t] = call_node.func + uninitialized_dependents: list[str] = [] + """ + Modules which the other module initializes, but whose init are not called beforehand + """ - if len(should_initialize) > 0: - err_list = ExceptionList() - for s in should_initialize.values(): - msg = "not initialized!" - hint = f"add `{s.module_info.alias}.__init__()` to " - hint += "your `__init__()` function" - - # grab the init function AST node for error message - # (it could be None, it's ok since it's just for diagnostics) - init_func_node = None - if constructor is not None: - init_func_node = constructor.decl_node - err_list.append(InitializerException(msg, init_func_node, s.node, hint=hint)) + for dependent in other_module_info.module_t.used_modules: + if dependent.module_t.init_function is None: + # no constructor to check + continue - err_list.raise_if_not_empty() + dependent_init_calls = init_calls[dependent] + if len(dependent_init_calls) == 0: + uninitialized_dependents.append(dependent.alias) + + if len(uninitialized_dependents) != 0: + msg = f"tried to initialize `{other_module_info.alias}`, " + msg += "but it depends on the following modules " + msg += "which have not been initialized: " + ", ".join(uninitialized_dependents) + hint = "call their `__init__()` methods before " + hint += f"`{other_module_info.alias}.__init__()`." + raise InitializerException(msg, call.func, init_calls, hint=hint) + + return other_module_info + + def visit_VyperNode( + self, node: vy_ast.VyperNode, init_calls: dict[ModuleInfo, list[vy_ast.VyperNode]] + ): + # Regular, non-branching node + + ret: dict[ModuleInfo, list[vy_ast.VyperNode]] = {} + + for call in node.get_descendants(vy_ast.Call): + initialized_module_info = self._validate_call(call, init_calls) + if initialized_module_info is not None: + + # There should always be at most one init call per statement! + assert not ret + + ret[initialized_module_info] = [call] + + return ret def _validate_exports_uses(module_ast: vy_ast.Module, module_t: ModuleT) -> None: diff --git a/vyper/semantics/types/module.py b/vyper/semantics/types/module.py index ab7e814ac7..6454e3d13a 100644 --- a/vyper/semantics/types/module.py +++ b/vyper/semantics/types/module.py @@ -1,3 +1,5 @@ +from __future__ import annotations + from functools import cached_property from typing import TYPE_CHECKING, Optional @@ -10,7 +12,7 @@ StructureException, UnfoldableNode, ) -from vyper.semantics.analysis.base import Modifiability +from vyper.semantics.analysis.base import InitializesInfo, Modifiability from vyper.semantics.analysis.utils import ( check_modifiability, validate_expected_type, @@ -100,7 +102,7 @@ def _try_fold(self, node): return node # when using the type itself (not an instance) in the call position - def _ctor_call_return(self, node: vy_ast.Call) -> "InterfaceT": + def _ctor_call_return(self, node: vy_ast.Call) -> InterfaceT: self._ctor_arg_types(node) return self @@ -173,7 +175,7 @@ def _from_lists( error_list: Optional[list[tuple[str, ErrorT]]] = None, struct_list: Optional[list[tuple[str, StructT]]] = None, flag_list: Optional[list[tuple[str, FlagT]]] = None, - ) -> "InterfaceT": + ) -> InterfaceT: functions: dict[str, ContractFunctionT] = {} events: dict[str, EventT] = {} errors: dict[str, ErrorT] = {} @@ -206,7 +208,7 @@ def _process(dst_dict, items): return cls(interface_name, decl_node, functions, events, errors, structs, flags) @classmethod - def from_json_abi(cls, name: str, abi: dict) -> "InterfaceT": + def from_json_abi(cls, name: str, abi: dict) -> InterfaceT: """ Generate an `InterfaceT` object from an ABI. @@ -274,7 +276,7 @@ def _dedup_default_arg_overloads(cls, abi: dict) -> list: return [(fn_name, fn) for fn_name, fn in parsed_fns.items()] @classmethod - def from_ModuleT(cls, module_t: "ModuleT") -> "InterfaceT": + def from_ModuleT(cls, module_t: ModuleT) -> InterfaceT: """ Generate an `InterfaceT` object from a Vyper ast node. @@ -306,7 +308,7 @@ def from_ModuleT(cls, module_t: "ModuleT") -> "InterfaceT": ) @classmethod - def from_InterfaceDef(cls, node: vy_ast.InterfaceDef) -> "InterfaceT": + def from_InterfaceDef(cls, node: vy_ast.InterfaceDef) -> InterfaceT: functions = [] for func_ast in node.body: if not isinstance(func_ast, vy_ast.FunctionDef): @@ -435,7 +437,7 @@ def __hash__(self): def decl_node(self) -> Optional[vy_ast.VyperNode]: # type: ignore[override] return self._module - def get_type_member(self, key: str, node: vy_ast.VyperNode) -> "VyperType": + def get_type_member(self, key: str, node: vy_ast.VyperNode) -> VyperType: return self._helper.get_member(key, node) @cached_property @@ -499,7 +501,7 @@ def import_stmts(self): return self._module.get_children((vy_ast.Import, vy_ast.ImportFrom)) @cached_property - def imported_modules(self) -> dict[str, "ModuleInfo"]: + def imported_modules(self) -> dict[str, ModuleInfo]: ret = {} for s in self.import_stmts: for info in s._metadata["import_infos"]: @@ -509,7 +511,7 @@ def imported_modules(self) -> dict[str, "ModuleInfo"]: ret[info.alias] = module_info return ret - def find_module_info(self, needle: "ModuleT") -> Optional["ModuleInfo"]: + def find_module_info(self, needle: ModuleT) -> Optional[ModuleInfo]: for s in self.imported_modules.values(): if s.module_t == needle: return s @@ -532,7 +534,7 @@ def exports_decls(self): return self._module.get_children(vy_ast.ExportsDecl) @cached_property - def used_modules(self): + def used_modules(self) -> list[ModuleInfo]: # modules which are written to ret = [] for node in self.uses_decls: @@ -541,7 +543,7 @@ def used_modules(self): return ret @property - def initialized_modules(self): + def initialized_modules(self) -> list[InitializesInfo]: # modules which are initialized to ret = [] for node in self.initializes_decls: