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

VIP: instantiable stateful modules #3723

Closed
charles-cooper opened this issue Jan 9, 2024 · 2 comments
Closed

VIP: instantiable stateful modules #3723

charles-cooper opened this issue Jan 9, 2024 · 2 comments

Comments

@charles-cooper
Copy link
Member

charles-cooper commented Jan 9, 2024

Simple Summary

extend the import system by allowing "stateful modules" (that is, modules with top-level state variables). allow multiple instantiation of modules.

this is the second of two proposals exploring the stateful module design space; the other is #3722.

Motivation

the analysis here is substantially similar to the motivation presented in #3722. it is omitted here so that there is only one master copy of the analysis in case it undergoes edits or revision, but the discussion in #3722 should be considered requisite background for this proposal.

this proposal proposes an alternative design, which allows multiple instantiations of any given module, and solves the state sharing problem by allowing the programmer to share specific instances between modules. dependencies which are intended to be shared are marked as such at declaration time, and an instance needs to be provided before it can be compiled.

Specification

some examples, with a tentative syntax:

###
# dep1.vy

counter: uint256

###

###
# dep2.vy
import dep1

# mark d as being something which will be initialized by some other module
# note that dep2.vy is not a compilable until self.d is resolved!
d: dep1[noinit=True]

def __init__():
    pass  # self.d.__init__() is *not* allowed, because `noinit` is set to `True`

@internal
def bar():
    self.d.counter += 1

###

###
# contract.vy

import dep1
import dep2

d1: dep1
d2: dep2[d=dep1]  # resolve d2.d to our d1

def __init__():
    self.d1.__init__()
    self.d2.__init__()

@external
def foo():
    self.d1.counter += 1

@external
def foo1():
    self.d1.update_counter()

@external
def foo2():
    t: uint256 = self.d1.counter
    self.d1.counter += 1
    self.d2.bar()
    assert self.d1.counter == t + 2
###
# Foo.vy

import Lock

counter: uint256
lock: Lock[noinit=True]

@external
def foo():
    self.lock.acquire()
    self.counter += 1
    raw_call(...)
    self.lock.release()
###

###
# Bar.vy

import Lock
import Foo

foo: Foo[lock=l]

x: uint256
# this statement also controls the location of Lock in the storage layout -- it comes after `x`.
l: Lock

exports: Foo.foo

def __init__():
    self.l.__init__(...)  # omitting this would be an error!

@external
def bar():
    self.l.acquire()
    ...  # do stuff, maybe call an external contract
    self.l.release()

an obligatory token example:

###
# Owned.vy
owner: address

def __init__():
    self.owner = msg.sender

def check_owner():
    assert msg.sender == self.owner
###

###
# BaseToken.vy
totalSupply: uint256
balances: HashMap[address, uint256]

def __init__(initial_supply: uint256):
    self.totalSupply += initial_supply
    self.balances[msg.sender] += initial_supply

@external
def transfer(recipient: address, amount: uint256):
    self.balances[msg.sender] -= amount  # safesub
    self.balances[recipient] += amount
###

###
# Mint.vy
import BaseToken
import Owned

ownership: Owned[noinit=True]
base_token: BaseToken[noinit=True]

@external
def mint(recipient: address, amount: uint256):
    self.ownership.check_owner()
    self._mint_to(recipient, amount)

@internal
def _mint_to(recipient: address, amount: uint256):
    self.base_token.totalSupply += amount
    self.base_token.balances[recipient] += amount
###

###
# Contract.vy
import Owned
import Mint
import BaseToken

_base: BaseToken
acl: Owned
token: Mint[ownership=acl, base_token=_base]

def __init__():
    self._base.__init__(100)
    self.acl.__init__()
    self.token.__init__()

export: token.mint
export: _base.transfer

note an alternative design for this hypothetical project could be for Mint to instantiate Owned and BaseToken directly. then Contract.vy would use minter.acl. this would be a design decision left up to the library. for illustration, this is what that design would look like:

# Owned and BaseToken look the same.
###
# Mint.vy
import Owned
import BaseToken

acl: Owned
base_token: BaseToken

def __init__(initial_supply: uint256):
    self.acl.__init__()
    self.base_token.__init__(initial_supply)

@external
def mint(recipient: address, amount: uint256):
    self.acl.check_owner()
    self._mint_to(recipient, amount)

@internal
def _mint_to(recipient: address, amount: uint256):
    self.base_token.totalSupply += amount
    self.base_token.balances[recipient] += amount
###

###
# Contract.vy
import Mint

token: Mint

export: token.mint, token.base_token.transfer

def __init__():
    self.token.base_token.__init__()  # error! Mint already initializes base_token
    self.token.acl.__init__()  # error! Mint already initializes acl

    self.token.__init__(100)  # that's better

Backwards Compatibility

does not change any existing language features, fully backwards compatible

Dependencies

References

Copyright

Copyright and related rights waived via CC0

@charles-cooper
Copy link
Member Author

for future reference -- i started work on this in #3707. however, after side-by-side comparison, i am beginning to favor #3722 (with some modifications).

@charles-cooper
Copy link
Member Author

superseded by #3722

@charles-cooper charles-cooper closed this as not planned Won't fix, can't repro, duplicate, stale Apr 28, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant