Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

fix[lang]: stateless modules should not be initialized #4347

Open
wants to merge 32 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 23 commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
1b1ac45
forbid initialization of a stateless module
sandbubbles Nov 3, 2024
3a9e29d
fix tests so they do not initialize a stateless module
sandbubbles Nov 3, 2024
5527244
lint
sandbubbles Nov 3, 2024
3c695da
set immutables as stateless
sandbubbles Nov 4, 2024
5000efc
add tests
sandbubbles Nov 4, 2024
e3d7582
fix stateless test
sandbubbles Nov 4, 2024
c92e1ad
make immutables state
sandbubbles Nov 4, 2024
cf91784
fix test
sandbubbles Nov 4, 2024
1ab6433
test transient in cancun
sandbubbles Nov 5, 2024
28c9be7
make init function as state
sandbubbles Nov 5, 2024
0c5f645
remove one wrong test
sandbubbles Nov 5, 2024
47efc8e
refactor is_stateless into ModuleT method
sandbubbles Nov 9, 2024
5fe6c84
simplify is_stateless
charles-cooper Nov 20, 2024
fececb7
promote some properties to cached properties
charles-cooper Nov 20, 2024
4cc01bb
add comment to explain a test
sandbubbles Dec 5, 2024
ba19783
rename function to is_initializable
sandbubbles Dec 5, 2024
b668f41
remove redundant storage variables
sandbubbles Dec 5, 2024
6c7a30f
make modules with nonreentrant initializable
sandbubbles Dec 5, 2024
948d047
test that nonreentrant function use state
sandbubbles Dec 5, 2024
3dbbeb6
update comment
sandbubbles Dec 5, 2024
1f4a2d6
remove uses decl from initializable
sandbubbles Dec 6, 2024
be5aa0b
rename the function so the boolean matches
sandbubbles Dec 6, 2024
7b2f38b
refactor comment
sandbubbles Dec 7, 2024
bb4944f
change is_not_initializable to is_initializable
sandbubbles Dec 12, 2024
b41bc28
rename variable
sandbubbles Dec 15, 2024
2bcba89
remove unnecessary phonies and initializes
sandbubbles Dec 15, 2024
7f2f452
clean up tests
sandbubbles Dec 15, 2024
f64109f
remove phony variables to fix tests with "uses"
sandbubbles Dec 30, 2024
bf7307c
modify test to expect a successful compilation
sandbubbles Dec 30, 2024
a075979
mark uses declaration initializable
sandbubbles Dec 30, 2024
b67ab3f
Merge branch 'master' into fix/stateless-modules-can-be-initialized
sandbubbles Dec 30, 2024
ef0c49d
add comments to tests
sandbubbles Dec 30, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions tests/functional/codegen/features/test_transient.py
Original file line number Diff line number Diff line change
Expand Up @@ -570,3 +570,17 @@ def foo() -> (uint256[3], uint256, uint256, uint256):

c = get_contract(main, input_bundle=input_bundle)
assert c.foo() == ([1, 2, 3], 1, 2, 42)


# Testing the `initializes` statement to verify how transient variables
# interact with and relate to state.
def test_transient_is_state(make_input_bundle):
Copy link
Collaborator

Choose a reason for hiding this comment

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

let's please add a comment here that we're testing the initialization and how it relates to state

lib = """
message: transient(bool)
"""
main = """
import lib
initializes: lib
"""
input_bundle = make_input_bundle({"lib.vy": lib, "main.vy": main})
compile_code(main, input_bundle=input_bundle)
2 changes: 2 additions & 0 deletions tests/functional/codegen/modules/test_module_variables.py
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,8 @@ def increment_counter():

uses: lib1

phony: uint32

@internal
def get_lib1_counter() -> uint256:
return lib1.counter
Expand Down
149 changes: 149 additions & 0 deletions tests/functional/syntax/modules/test_initializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -277,6 +277,8 @@ def test_initializer_list_module_mismatch(make_input_bundle):

uses: lib1

phony: uint32

@internal
def foo():
lib1.counter += 1
Expand Down Expand Up @@ -835,6 +837,8 @@ def test_uses_skip_import(make_input_bundle):
lib2 = """
import lib1

phony: uint32

@internal
def foo():
pass
Expand Down Expand Up @@ -1399,6 +1403,7 @@ def test_global_initialize_missed_import_hint(make_input_bundle, chdir_tmp_path)
import lib3

uses: lib3
phony: uint32

@external
def set_some_mod():
Expand All @@ -1418,3 +1423,147 @@ def set_some_mod():
compile_code(main, input_bundle=input_bundle)
assert e.value._message == "module `lib3.vy` is used but never initialized!"
assert e.value._hint is None


initializable_modules = [
"""
phony: uint32
""",
"""
ended: public(bool)
""",
"""
@external
@nonreentrant
def foo():
pass
""",
"""
@internal
@nonreentrant
def foo():
pass
""",
]


@pytest.mark.parametrize("module", initializable_modules)
def test_initializes_on_modules_with_state_related_vars(module, make_input_bundle):
main = """
import lib
initializes: lib
"""
input_bundle = make_input_bundle({"lib.vy": module, "main.vy": main})
compile_code(main, input_bundle=input_bundle)


def test_initializes_on_modules_with_immutables(make_input_bundle):
lib = """
foo: immutable(int128)

@deploy
def __init__():
foo = 2
"""

main = """
import lib
initializes: lib

@deploy
def __init__():
lib.__init__()
"""
input_bundle = make_input_bundle({"lib.vy": lib, "main.vy": main})
compile_code(main, input_bundle=input_bundle)


stateless_modules = [
"""
""",
"""
@internal
@pure
def foo(x: uint256, y: uint256) -> uint256:
return unsafe_add(x & y, (x ^ y) >> 1)
""",
"""
FOO: constant(int128) = 128
""",
]


@pytest.mark.parametrize("module", stateless_modules)
def test_forbids_initializes_on_stateless_modules(module, make_input_bundle):
main = """
import lib
initializes: lib
"""
input_bundle = make_input_bundle({"lib.vy": module, "main.vy": main})
with pytest.raises(StructureException):
compile_code(main, input_bundle=input_bundle)


def test_initializes_on_modules_with_uses(make_input_bundle):
lib0 = """
import lib1
uses: lib1

@external
def foo() -> uint32:
return lib1.phony
"""
lib1 = """
phony: uint32
"""
main = """
import lib1
initializes: lib1

import lib0
initializes: lib0[lib1 := lib1]
"""
input_bundle = make_input_bundle({"lib1.vy": lib1, "lib0.vy": lib0, "main.vy": main})
with pytest.raises(StructureException):
compile_code(main, input_bundle=input_bundle)


def test_initializes_on_modules_with_initializes(make_input_bundle):
lib0 = """
import lib1
initializes: lib1
"""
lib1 = """
phony: uint32
"""
main = """
import lib0
initializes: lib0
"""
input_bundle = make_input_bundle({"lib1.vy": lib1, "lib0.vy": lib0, "main.vy": main})
compile_code(main, input_bundle=input_bundle)


def test_initializes_on_modules_with_init_function(make_input_bundle):
lib = """
interface Foo:
def foo(): payable

@deploy
def __init__():
extcall Foo(self).foo()
"""
main = """
import lib
initializes: lib

@deploy
def __init__():
lib.__init__()

@external
def foo():
pass
"""
input_bundle = make_input_bundle({"lib.vy": lib, "main.vy": main})
compile_code(main, input_bundle=input_bundle)
8 changes: 8 additions & 0 deletions tests/unit/compiler/test_abi.py
Original file line number Diff line number Diff line change
Expand Up @@ -218,6 +218,8 @@ def bar(x: {type}):

def test_exports_abi(make_input_bundle):
lib1 = """
phony: uint32

@external
def foo():
pass
Expand Down Expand Up @@ -361,6 +363,8 @@ def __init__():
def test_event_export_from_function_export(make_input_bundle):
# test events used in exported functions are exported
lib1 = """
phony: uint32
Copy link
Member

Choose a reason for hiding this comment

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

maybe we shouldn't add phony here -- the code change is telling us to remove initializes: lib1


event MyEvent:
pass

Expand Down Expand Up @@ -396,6 +400,8 @@ def foo():
def test_event_export_unused_function(make_input_bundle):
# test events in unused functions are not exported
lib1 = """
phony: uint32

event MyEvent:
pass

Expand Down Expand Up @@ -639,6 +645,8 @@ def update_counter():
import lib1
uses: lib1

phony: uint32

@internal
def use_lib1():
lib1.update_counter()
Expand Down
4 changes: 4 additions & 0 deletions vyper/semantics/analysis/module.py
Original file line number Diff line number Diff line change
Expand Up @@ -409,6 +409,10 @@ def visit_InitializesDecl(self, node):
module_info = get_expr_info(module_ref).module_info
if module_info is None:
raise StructureException("Not a module!", module_ref)
if module_info.module_t.is_not_initializable:
raise StructureException(
f"Cannot initialize a stateless module {module_info.alias}!", module_ref
)

used_modules = {i.module_t: i for i in module_info.module_t.used_modules}

Expand Down
31 changes: 26 additions & 5 deletions vyper/semantics/types/module.py
Original file line number Diff line number Diff line change
Expand Up @@ -444,19 +444,19 @@ def find_module_info(self, needle: "ModuleT") -> Optional["ModuleInfo"]:
return s
return None

@property
@cached_property
def variable_decls(self):
return self._module.get_children(vy_ast.VariableDecl)

@property
@cached_property
def uses_decls(self):
return self._module.get_children(vy_ast.UsesDecl)

@property
@cached_property
def initializes_decls(self):
return self._module.get_children(vy_ast.InitializesDecl)

@property
@cached_property
def exports_decls(self):
return self._module.get_children(vy_ast.ExportsDecl)

Expand All @@ -469,7 +469,7 @@ def used_modules(self):
ret.append(used_module)
return ret

@property
@cached_property
def initialized_modules(self):
# modules which are initialized to
ret = []
Expand Down Expand Up @@ -559,3 +559,24 @@ def immutable_section_bytes(self):
@cached_property
def interface(self):
return InterfaceT.from_ModuleT(self)

@cached_property
def is_not_initializable(self):
Copy link
Member

Choose a reason for hiding this comment

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

maybe we can flip the logic and rename to is_initializable ?

"""
Determine whether ModuleT is stateless by examining its top-level
declarations. A module has state if it contains storage variables,
transient variables, or immutables, or if it includes a "initializes"
declaration, or any nonreentrancy locks.
"""
if len(self.initializes_decls) > 0:
return False
if any(not v.is_constant for v in self.variable_decls):
return False
if self.init_function is not None:
charles-cooper marked this conversation as resolved.
Show resolved Hide resolved
return False

for fun in self.functions.values():
if fun.nonreentrant:
Copy link
Member

Choose a reason for hiding this comment

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

convention: ContractFunctionT is usually named fn_t in loops (or other places with small scope)

Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
for fun in self.functions.values():
if fun.nonreentrant:
for fn_t in self.functions.values():
if fn_t.nonreentrant:

Copy link
Member

@charles-cooper charles-cooper Dec 12, 2024

Choose a reason for hiding this comment

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

actually maybe this should be a loop over exposed functions (per @cyberthirst )?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I think that would skip internal functions with nonreentrancy locks - is that intentional?

Copy link
Member

Choose a reason for hiding this comment

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

no, not intentional

Copy link
Member

Choose a reason for hiding this comment

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

hmm, maybe the exposed_functions thing is a red herring -- we don't care about exposed_functions per se, we care about state in the recursion, but that is handled by checking if there are any initializes decls.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

yup, i think as you said - we dont need to recurse in the function as the state of the imported modules is already handled. So the self.functions.values() is okay?

Copy link
Member

Choose a reason for hiding this comment

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

yes i think so, unless @cyberthirst thinks otherwise.

return False

return True
Loading