-
-
Notifications
You must be signed in to change notification settings - Fork 870
Description
Simple Summary
add abstract methods to the vyper language. use a top-level resolution block as an explicit, in-text declaration of programmer intent.
the text below was co-created with claude code and is a starting point for the final specification.
Motivation
This document outlines the design for adding abstract methods to the Vyper programming language. The goal is to enable library authors to provide reusable contract patterns while maintaining Vyper's core principles of readability, explicitness, and security.
Library authors currently face limitations when trying to provide reusable contract implementations. They often need to implement 95% of a pattern's functionality while leaving specific customization points for users. Without abstract methods, this requires either:
- Copy-pasting entire implementations
- Complex delegation patterns
- Sacrificing type safety
Abstract methods allow library authors to define contracts with "holes" that users fill in, similar to the template method pattern in object-oriented programming.
Design Goals
- No spooky action at a distance - Module behavior must be understandable by reading the module itself
- Explicit over implicit - All abstract method resolutions must be explicit
- Compile-time resolution - No runtime dispatch or polymorphism
- Prevent ambiguity - The diamond problem must be impossible
- Maintain simplicity - The mental model must remain simple
Specification
Core Concepts
- Libraries can define abstract methods that must be implemented by users
- Libraries can provide implementations for other libraries' abstract methods
- Compilation targets must explicitly resolve all abstract methods using a
resolutionsblock - All resolution happens at compile time through code inlining
Syntax
Defining Abstract Methods in Libraries
# base_token.vy
@library
@abstract
def before_transfer(sender: address, recipient: address, amount: uint256): ...
@abstract
def after_transfer(sender: address, recipient: address, amount: uint256): ...
@external
def transfer(recipient: address, amount: uint256) -> bool:
self.before_transfer(msg.sender, recipient, amount)
# ... transfer logic ...
return TrueProviding Implementations in Libraries
# pausable_token.vy
import base_token
@library
uses: base_token
provides:
base_token.before_transfer -> self._check_pause
base_token.after_transfer -> self._after_transfer
@internal
def _check_pause(sender: address, recipient: address, amount: uint256):
assert not self.paused, "Token is paused"
@internal
def _after_transfer(sender: address, recipient: address, amount: uint256):
pass # no-opResolution in Compilation Targets
# my_token.vy
import base_token
import pausable_token
resolutions:
base_token.before_transfer: accept pausable_token._check_pause
base_token.after_transfer: override pausable_token._log -> self._my_log
base_token.on_mint -> self._mint_checks
uses: pausable_token
@internal
def _my_log(sender: address, recipient: address, amount: uint256):
# Custom implementation
pass
@internal
def _mint_checks(recipient: address, amount: uint256):
# Implementation for abstract method
passResolution Syntax
accept <provider>.<method>- Use the provided implementationoverride <provider>.<method> -> <implementation>- Replace provided implementation-> <implementation>- Provide fresh implementation (when no provider exists)
Key Rules
- Libraries cannot override - Only provide or pass through
- Explicit conflict resolution - Multiple providers require explicit choice
- Complete resolution required - All reachable abstract methods must be in
resolutions - Single library rule - A module can only use one library with abstract methods (prevents complexity)
Design Process and Rejected Alternatives
1. Inheritance-Based Approaches (Rejected)
Considered: Traditional OOP-style inheritance with method overriding
Rejected because:
- Violates "no spooky action at a distance" principle
- Inheritance chains make reasoning about behavior difficult
- Vyper explicitly avoids inheritance
2. Implicit Resolution (Rejected)
Considered: Automatically using the "most recent" or "most specific" implementation
Rejected because:
- Too implicit for Vyper's philosophy
- Can lead to surprising behavior
- Makes code review and auditing harder
3. Runtime Dispatch (Rejected)
Considered: Virtual method tables or dynamic dispatch
Rejected because:
- Performance overhead
- Complexity in understanding execution flow
- Against Vyper's principle of static verification
4. Multiple Template Instantiation (Rejected)
Considered: Allowing multiple templates/libraries with abstract methods per contract
Rejected because:
- Creates complex diamond problems
- Difficult to reason about method resolution order
- Significantly increases complexity
5. Cross-Module Abstract Methods Only (Rejected)
Considered: Abstract methods that can only be implemented across module boundaries
Rejected because:
- The diamond problem becomes unavoidable
- Complex resolution rules needed
- Libraries couldn't compose easily
6. Single-File Abstract Methods Only (Rejected)
Considered: Abstract methods only for forward declarations within one file
Rejected because:
- Doesn't solve the library author use case
- Too limited in functionality
- Doesn't enable code reuse
7. Strategy Pattern / Explicit Delegation (Rejected)
Considered: Using interfaces and explicit delegation instead of abstract methods
# Example of rejected pattern
@external
def transfer(recipient: address, amount: uint256, validator: IValidator) -> bool:
validator.validate_transfer(msg.sender, recipient, amount)
# ...Rejected because:
- Changes contract APIs
- Less elegant than abstract methods
- Requires deploying separate contracts
8. Event-like Hook System (Rejected)
Considered: Multiple implementations that all get called
Rejected because:
- Ordering problems
- Performance overhead
- Too different from traditional abstract methods
9. Implicit accept (Considered, then Rejected)
Considered: Making accept the default, not requiring explicit keyword
Rejected because:
- Explicit is better than implicit
acceptmakes intent clearer- Helps with code review and auditing
Diamond Problem Resolution
The diamond problem is completely prevented through:
- Explicit resolution requirement - When multiple libraries provide the same abstract method, compilation fails unless explicitly resolved
- Single source of truth - Only the compilation target's
resolutionsblock determines final bindings - No library override - Libraries cannot override other libraries' provisions
Example:
# If lib_a and lib_b both provide base.foo
resolutions:
base.foo -> lib_a._foo # Must explicitly choose
# OR
base.foo -> lib_b._foo
# OR
base.foo -> self._my_fooBenefits
- Library reusability - Authors can provide mostly-complete implementations
- Clear composition - All wiring visible at contract top
- Type safety - Compile-time verification of all resolutions
- No ambiguity - Diamond problem impossible
- Performance - Zero runtime overhead
- Auditability - Easy to understand what code does
Risks and Mitigations
- Complexity - Mitigated by keeping rules simple and explicit
- Learning curve - Mitigated by clear documentation and examples
- Tooling updates - Mitigated by phased rollout
Conclusion
This design provides abstraction capabilities for library authors while maintaining Vyper's core philosophy. By requiring explicit resolution and preventing library overrides, we avoid the drawbacks of inheritance while enabling useful code reuse patterns.
The resolutions block serves as a single source of truth at the top-level compilation target, making contracts simpler to audit. All method dispatch is resolved at compile time, maintaining Vyper's performance characteristics and security properties.
Backwards Compatibility
No known backwards compatibility issues
Dependencies
References
Add any references that this VIP might reference (other VIPs/issues, links to blog posts, etc.)
Copyright
Copyright and related rights waived via CC0