-
-
Notifications
You must be signed in to change notification settings - Fork 872
fix: constructor context for internal functions #3388
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
Changes from all commits
bb1b037
af15d43
8a6fc16
2897ff4
bbfaf2a
c52449c
ddbeed0
a0d43dc
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,49 @@ | ||
| from vyper.compiler.phases import CompilerData | ||
|
|
||
|
|
||
| def test_dead_code_eliminator(): | ||
| code = """ | ||
| s: uint256 | ||
|
|
||
| @internal | ||
| def foo(): | ||
| self.s = 1 | ||
|
|
||
| @internal | ||
| def qux(): | ||
| self.s = 2 | ||
|
|
||
| @external | ||
| def bar(): | ||
| self.foo() | ||
|
|
||
| @external | ||
| def __init__(): | ||
| self.qux() | ||
| """ | ||
|
|
||
| c = CompilerData(code, no_optimize=True) | ||
| initcode_asm = [i for i in c.assembly if not isinstance(i, list)] | ||
| runtime_asm = c.assembly_runtime | ||
|
|
||
| foo_label = "_sym_internal_foo___" | ||
| qux_label = "_sym_internal_qux___" | ||
|
|
||
| # all the labels should be in all the unoptimized asms | ||
| for s in (foo_label, qux_label): | ||
| assert s in initcode_asm | ||
| assert s in runtime_asm | ||
|
|
||
| c = CompilerData(code, no_optimize=False) | ||
| initcode_asm = [i for i in c.assembly if not isinstance(i, list)] | ||
| runtime_asm = c.assembly_runtime | ||
|
|
||
| # qux should not be in runtime code | ||
| for instr in runtime_asm: | ||
| if isinstance(instr, str): | ||
| assert not instr.startswith(qux_label), instr | ||
|
|
||
| # foo should not be in initcode asm | ||
| for instr in initcode_asm: | ||
| if isinstance(instr, str): | ||
| assert not instr.startswith(foo_label), instr |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -67,19 +67,21 @@ def _runtime_ir(runtime_functions, all_sigs, global_ctx): | |
|
|
||
| # create a map of the IR functions since they might live in both | ||
| # runtime and deploy code (if init function calls them) | ||
| internal_functions_map: Dict[str, IRnode] = {} | ||
| internal_functions_ir: list[IRnode] = [] | ||
|
|
||
| for func_ast in internal_functions: | ||
| func_ir = generate_ir_for_function(func_ast, all_sigs, global_ctx, False) | ||
| internal_functions_map[func_ast.name] = func_ir | ||
| internal_functions_ir.append(func_ir) | ||
|
|
||
| # for some reason, somebody may want to deploy a contract with no | ||
| # external functions, or more likely, a "pure data" contract which | ||
| # contains immutables | ||
| if len(external_functions) == 0: | ||
| # TODO: prune internal functions in this case? | ||
| runtime = ["seq"] + list(internal_functions_map.values()) | ||
| return runtime, internal_functions_map | ||
| # TODO: prune internal functions in this case? dead code eliminator | ||
| # might not eliminate them, since internal function jumpdest is at the | ||
| # first instruction in the contract. | ||
| runtime = ["seq"] + internal_functions_ir | ||
| return runtime | ||
|
|
||
| # note: if the user does not provide one, the default fallback function | ||
| # reverts anyway. so it does not hurt to batch the payable check. | ||
|
|
@@ -125,10 +127,10 @@ def _runtime_ir(runtime_functions, all_sigs, global_ctx): | |
| ["label", "fallback", ["var_list"], fallback_ir], | ||
| ] | ||
|
|
||
| # TODO: prune unreachable functions? | ||
| runtime.extend(internal_functions_map.values()) | ||
| # note: dead code eliminator will clean dead functions | ||
| runtime.extend(internal_functions_ir) | ||
|
|
||
| return runtime, internal_functions_map | ||
| return runtime | ||
|
|
||
|
|
||
| # take a GlobalContext, which is basically | ||
|
|
@@ -159,12 +161,15 @@ def generate_ir_for_module(global_ctx: GlobalContext) -> Tuple[IRnode, IRnode, F | |
| runtime_functions = [f for f in function_defs if not _is_init_func(f)] | ||
| init_function = next((f for f in function_defs if _is_init_func(f)), None) | ||
|
|
||
| runtime, internal_functions = _runtime_ir(runtime_functions, all_sigs, global_ctx) | ||
| runtime = _runtime_ir(runtime_functions, all_sigs, global_ctx) | ||
|
|
||
| deploy_code: List[Any] = ["seq"] | ||
| immutables_len = global_ctx.immutable_section_bytes | ||
| if init_function: | ||
| init_func_ir = generate_ir_for_function(init_function, all_sigs, global_ctx, False) | ||
| # TODO might be cleaner to separate this into an _init_ir helper func | ||
| init_func_ir = generate_ir_for_function( | ||
| init_function, all_sigs, global_ctx, skip_nonpayable_check=False, is_ctor_context=True | ||
| ) | ||
| deploy_code.append(init_func_ir) | ||
|
|
||
| # pass the amount of memory allocated for the init function | ||
|
|
@@ -174,8 +179,13 @@ def generate_ir_for_module(global_ctx: GlobalContext) -> Tuple[IRnode, IRnode, F | |
| deploy_code.append(["deploy", init_mem_used, runtime, immutables_len]) | ||
|
|
||
| # internal functions come after everything else | ||
| for f in init_function._metadata["type"].called_functions: | ||
| deploy_code.append(internal_functions[f.name]) | ||
| internal_functions = [f for f in runtime_functions if _is_internal(f)] | ||
| for f in internal_functions: | ||
| func_ir = generate_ir_for_function( | ||
| f, all_sigs, global_ctx, skip_nonpayable_check=False, is_ctor_context=True | ||
| ) | ||
| # note: we depend on dead code eliminator to clean dead function defs | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. So are you 100% sure that if
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. yea i'm 100% sure, i double checked by hand. we could add tests for the dead code eliminator altho it seems a bit out of scope for this PR ps: @internal
def foo():
pass
@internal
def qux():
pass
@external
def bar():
self.foo()
@external
def __init__():
self.qux()produces the following asm: which you can manually verify that qux does not appear in the runtime code and foo does not appear in the initcode.
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ok - i added the dead code eliminator test in 2897ff4
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I also quickly verified it manually. Also, for the record, I tested this morning manually a contract with 10 nested |
||
| deploy_code.append(func_ir) | ||
|
|
||
| else: | ||
| if immutables_len != 0: | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
do you think it's worth adding a separate test that combines both issues? Also, it might make sense to add an additional test where the
immutablevariable is declared aspublicsince this involves the generation of an additional getter that also must read the state correctly.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
added the addl test in ddbeed0