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: constructor context for internal functions #3388

Merged
merged 8 commits into from
May 15, 2023

Conversation

charles-cooper
Copy link
Member

@charles-cooper charles-cooper commented May 8, 2023

What I did

fix #3206, fix #3292

How I did it

add constructor context to codegen, skip cleverness with called_functions

How to verify it

Commit message

this commit fixes two related issues with initcode generation:

- nested internal functions called from the constructor would cause a
  compiler panic
- internal functions called from the constructor would not read/write
  from the correct immutables space

the relevant examples reproducing each issue are in the tests. this
commit fixes the issue by

- not trying to traverse the call graph to figure out which internal
  functions to include in the initcode. instead, all internal functions
  are included, and we rely on the dead code eliminator to remove unused
  functions
- adding a "constructor" flag to the codegen, so we can distinguish
  between internal calls which are being generated to include in
  initcode or runtime code.

Description for the changelog

Cute Animal Picture

Put a link to a cute animal picture inside the parenthesis-->

fix incorrect codegen (using dload instead of iload)
fix non-recursing for called functions from ctor
@codecov-commenter
Copy link

codecov-commenter commented May 8, 2023

Codecov Report

Merging #3388 (a0d43dc) into master (3c83947) will decrease coverage by 0.14%.
The diff coverage is 100.00%.

❗ Your organization is not using the GitHub App Integration. As a result you may experience degraded service beginning May 15th. Please install the Github App Integration for your organization. Read more.

@@            Coverage Diff             @@
##           master    #3388      +/-   ##
==========================================
- Coverage   88.92%   88.79%   -0.14%     
==========================================
  Files          86       86              
  Lines       10726    10771      +45     
  Branches     2440     2447       +7     
==========================================
+ Hits         9538     9564      +26     
- Misses        787      809      +22     
+ Partials      401      398       -3     
Impacted Files Coverage Δ
vyper/codegen/context.py 91.85% <100.00%> (-0.06%) ⬇️
vyper/codegen/expr.py 87.50% <100.00%> (-0.10%) ⬇️
vyper/codegen/function_definitions/common.py 100.00% <100.00%> (ø)
vyper/codegen/module.py 97.75% <100.00%> (+0.05%) ⬆️

... and 13 files with indirect coverage changes

📣 We’re building smart automated test selection to slash your CI/CD build times. Learn more

this commit fixes two issues in constructor codegen:
- nested calls to internal functions (i.e. the constructor calls a()
  which then calls b()) were not compiling properly, only a single level
  of internal calls were tracked correctly, and
- internal functions which used immutables were being produced with the
  incorrect `dload` instead of `iload` instruction.

it fixes the first issue by simply including the code for all functions
in the generated IR for the constructor and depending on the dead code
eliminator to eliminate unused functions - instead of trying to traverse
the call graph and only including called functions.

the second issue is fixed by producing a separate compilation context
for codegen which indicates whether the function is being called from
the constrcutor or not - and depending on the context, the correct
instruction can be emitted.
@pcaversaccio
Copy link
Collaborator

@charles-cooper can confirm that this fixes my issue. I tested it using my snekmate ERC20 implementation by replacing:

    if (initial_supply != empty(uint256)):
        self._before_token_transfer(empty(address), msg.sender, initial_supply)
        self.totalSupply = initial_supply
        self.balanceOf[msg.sender] = initial_supply
        log Transfer(empty(address), msg.sender, initial_supply)
        self._after_token_transfer(empty(address), msg.sender, initial_supply)

with

    if (initial_supply != empty(uint256)):
        self._mint(msg.sender, initial_supply)

where _mint itself calls another internal functions:

@internal
def _mint(owner: address, amount: uint256):
    """
    @dev Creates `amount` tokens and assigns
         them to `owner`, increasing the
         total supply.
    @notice This is an `internal` function without
            access restriction. Note that `owner`
            cannot be the zero address.
    @param owner The 20-byte owner address.
    @param amount The 32-byte token amount to be created.
    """
    assert owner != empty(address), "ERC20: mint to the zero address"

    self._before_token_transfer(empty(address), owner, amount)

    self.totalSupply += amount
    self.balanceOf[owner] = unsafe_add(self.balanceOf[owner], amount)
    log Transfer(empty(address), owner, amount)

    self._after_token_transfer(empty(address), owner, amount)

Everything compiles smoothly :- D! thx.

@charles-cooper charles-cooper changed the title fix: ctor issues fix: initcode codegen bugs May 12, 2023
tests/parser/features/test_init.py Outdated Show resolved Hide resolved
tests/parser/features/test_init.py Outdated Show resolved Hide resolved
Copy link
Collaborator

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 immutable variable is declared as public since this involves the generation of an additional getter that also must read the state correctly.

Copy link
Member Author

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

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
Copy link
Collaborator

Choose a reason for hiding this comment

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

So are you 100% sure that if internal functions run only within ctor are eliminated by the dead code eliminator or does this need further investigations? Can we maybe add here an additional test? Just to ensure to no dead code is deployed (this happened to Solidity already btw).

Copy link
Member Author

@charles-cooper charles-cooper May 15, 2023

Choose a reason for hiding this comment

The 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:

CALLVALUE _sym_revert1 JUMPI _sym_external___init______cleanup _sym_internal_qux____cleanup JUMP _sym_external___init______cleanup JUMPDEST _sym_subcode_size _sym_runtime_begin2 _mem_deploy_start CODECOPY _OFST _sym_subcode_size 0 _mem_deploy_start RETURN _sym_runtime_begin2 BLANK { _DEPLOY_MEM_OFST_64 PUSH1 0x00 CALLDATALOAD PUSH1 0xe0 SHR CALLVALUE _sym_revert1 JUMPI PUSH4 0xfebb0f7e DUP2 XOR _sym_join3 JUMPI PUSH1 0x04 CALLDATASIZE LT _sym_revert1 JUMPI _sym_external_bar____cleanup _sym_internal_foo____cleanup JUMP _sym_external_bar____cleanup JUMPDEST STOP _sym_join3 JUMPDEST POP PUSH1 0x00 PUSH1 0x00 REVERT _sym_internal_foo____cleanup JUMPDEST JUMP _sym_revert1 JUMPDEST PUSH1 0x00 DUP1 REVERT } _sym_internal_qux____cleanup JUMPDEST JUMP _sym_revert1 JUMPDEST PUSH1 0x00 DUP1 REVERT 

which you can manually verify that qux does not appear in the runtime code and foo does not appear in the initcode.

Copy link
Member Author

Choose a reason for hiding this comment

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

ok - i added the dead code eliminator test in 2897ff4

Copy link
Collaborator

Choose a reason for hiding this comment

The 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 internal calls. +1 for the code eliminator test.

@pcaversaccio
Copy link
Collaborator

LGTM now!

Copy link
Member

@fubuloubu fubuloubu left a comment

Choose a reason for hiding this comment

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

Is there a test for an internal that's in both the constructor and a public function?

@charles-cooper
Copy link
Member Author

Is there a test for an internal that's in both the constructor and a public function?

will add

@charles-cooper
Copy link
Member Author

Is there a test for an internal that's in both the constructor and a public function?

see a0d43dc

@charles-cooper charles-cooper enabled auto-merge (squash) May 15, 2023 18:31
@charles-cooper charles-cooper changed the title fix: initcode codegen bugs fix: immutables behavior in internal functions called from constructor May 15, 2023
@charles-cooper charles-cooper changed the title fix: immutables behavior in internal functions called from constructor fix: constructor context for internal functions May 15, 2023
@charles-cooper charles-cooper enabled auto-merge (squash) May 15, 2023 18:37
@charles-cooper charles-cooper merged commit c202c4e into vyperlang:master May 15, 2023
@charles-cooper charles-cooper deleted the fix/ctor_issues branch May 15, 2023 19:06
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
4 participants