Support operator overloading by operand types for user-defined value types
- Dominant language
- C++
- Stars
- 25.7k
- Forks
- 6.2k
- Avg merge
- 2d 19h
- Merged PRs (30d)
- 29
Description
## Abstract
Enable operator overloading based on operand types for user-defined value types to achieve zero-cost type-safe abstractions in Solidity.
## Motivation
Solidity's user-defined value types provide excellent zero-cost abstractions - they compile to the underlying primitive type with no runtime overhead while providing compile-time type safety. However, the current limitation that prevents mapping multiple functions with different signatures to the same operator significantly reduces their utility for creating domain-specific types.
## Current Behavior
Currently, attempting to define multiple operations for the same operator results in a compilation error:
```solidity
type Price is uint256;
using {
sub as -, // Price - Price = uint256
subU256 as - // ERROR: Duplicate operator definition
} for Price global;
function sub(Price a, Price b) pure returns (uint256) {
return Price.unwrap(a) - Price.unwrap(b);
}
function subU256(Price a, uint256 b) pure returns (Price) {
return Price.wrap(Price.unwrap(a) - b);
}
```
## Use Case
When modeling domain-specific types like `Price`, `Percentage`, or `Amount`, different operations with the same operator have different semantic meanings:
- `Price - Price → uint256`: Calculate price difference (not a Price!)
- `Price - uint256 → Price`: Adjust price downward
- `Price + uint256 → Price`: Adjust price upward
Without operator overloading by operand types, developers must choose between:
1. **Type safety with poor ergonomics**: `price1.sub(price2)` and `price1.subAmount(100)`
2. **Good ergonomics without type safety**: Using raw `uint256` everywhere
3. **Inconsistent APIs**: Mix of operators and method calls
## Proposed Solution
Allow the `using` directive to map multiple functions to the same operator, with the compiler selecting the appropriate function based on operand types:
```solidity
type Price is uint256;
using {
sub as -, // Price - Price = uint256
subU256 as -, // Price - uint256 = Price
addU256 as + // Price + uint256 = Price
} for Price global;
// Usage would be type-safe and intuitive:
Price p1 = Price.wrap(100e18);
Price p2 = Price.wrap(80e18);
uint256 diff = p1 - p2; // Calls sub(), returns uint256
Price p3 = p1 - 10e18; // Calls subU256(), returns Price
```
## Benefits
1. **Zero-cost abstraction**: No runtime overhead compared to raw uint256
2. **Type safety**: Prevents semantic errors at compile time
3. **Intuitive APIs**: Natural mathematical notation for domain types
4. **Code clarity**: Self-documenting operations through types
## Backwards Compatibility
This change would be backwards compatible as it only extends the existing functionality without breaking current behavior.
## References
Similar operator overloading based on operand types exists in many languages (C++, Rust, Swift) and has proven valuable for creating type-safe domain-specific abstractions.
Contributor guide
Research direction
The issue names no files, tests, or entry points. Start by locating the compiler's handling of user-defined value types and operator resolution, then determine how overload selection should use operand types; done means the examples compile with the stated return types while existing operator mappings continue to work.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- cpp, solidity
- Domain
- compilers
- Issue type
- Feature
- Difficulty
- 5/5
- Estimated time
- Over a week
- Activity status
- Stale
- Clarity
- Mostly clear
- Newbie friendliness
- 30/100