vyper icon indicating copy to clipboard operation
vyper copied to clipboard

Request for Comment: `safe` and `unsafe` math operations

Open bout3fiddy opened this issue 2 years ago • 4 comments

Simple Summary

This VIP introduces an alternative syntax for unsafe math operations. The goal is to improve the readability of complex mathematical expressions without compromising auditability. To that extent we propose safe and unsafe builtin methods for safe as well as unsafe operations.

Motivation

Simple expressions like a: uint256 = unsafe_add(b, c) are straightforward in Vyper. However, readability decreases when complex expressions involve multiple unsafe operations (code snippet taken from CurveCryptoMathOptimized3.vy):

K0 = unsafe_div(
    unsafe_mul(
        unsafe_mul(
            unsafe_div(
                unsafe_mul(
                    unsafe_mul(
                        unsafe_div(
                            unsafe_mul(
                                unsafe_mul(10**18, x[0]), N_COINS
                            ),
                            D,
                        ),
                        x[1],
                    ),
                    N_COINS,
                ),
                D,
            ),
            x[2],
        ),
        N_COINS,
    ),
    D,
)

In such situations, Solidity's inline assembly blocks can be more readable than Vyper's unsafe_ops(...).

Finally, expressions with a mix of safe and unsafe operations are particularly challenging to read (code snippet from CurveCryptoMathOptimized3.vy):

GK0: uint256 = (
    unsafe_div(unsafe_div(2 * K0 * K0, 10**36) * K0, 10**36)
    + pow_mod256(unsafe_add(_A_gamma[1], 10**18), 2)
    - unsafe_div(
        unsafe_div(pow_mod256(K0, 2), 10**36) * unsafe_add(unsafe_mul(2, _A_gamma[1]), 3 * 10**18),
        10**18
    )
)

While long-term compiler optimizations can eventually address these readability issues, we propose a short-term solution here.

Specification

Readable Expressions with Unsafe Operations

For straightforward expressions involving unsafe operations, we retain the existing unsafe_ops syntax for its readability:

a: uint256 = unsafe_add(b, c)

but unsafe can also be used here:

a: uint256 = unsafe(b + c)

Entirely Unsafe Expression

When an entire expression is unsafe, we suggest borrowing Solidity's approach:

K0 = unsafe(10**18 * x[0] * N_COINS / D * x[1] * N_COINS / D * x[2] * N_COINS / D)

such that it is clear the entire expression is evaluated using unsafe math operations.

This would then introduce unsafe builtin that does only unsafe operations with the expression inside it.

Expressions Combining Safe and Unsafe Operations

We propose a safe builtin method that can be used inside unsafe expressions.

GK0: uint256 = (
    unsafe(
        safe(unsafe(safe(2 * K0 * K0) / 10**36) * K0) / 10**36
    )
    + unsafe((_A_gamma[1] + 10**18) ** 2)
    - unsafe(
        unsafe(K0 ** 2 / 10**36) * unsafe(2 * _A_gamma[1] + safe(3 * 10**18)) / 10**18
    )
)

Backwards Compatibility

Syntactically, this would not be a breaking change since it does not change how unsafe_mul etc. are handled, and only introduces unsafe(...expression * safe (...expression)).

bout3fiddy avatar Jun 05 '23 16:06 bout3fiddy

Yes ser, bring it!

michwill avatar Jun 05 '23 16:06 michwill

Not sure how it will translate to Python syntax.

Alternatively:

with unsafe_math():
    K0 = 10**18 * x[0] * N_COINS / D * x[1] * N_COINS / D * x[2] * N_COINS / D

The __enter__() and __exit__() method for the unsafe_math() object may be used to turn off the safe math checks for the operator overloads.

It can also allow for optional arguments:

with unsafe_math(allow_zero_denominator=True):
    K0 = 10**18 * x[0] * N_COINS / D * x[1] * N_COINS / D * x[2] * N_COINS / D

Vectorized avatar Jun 05 '23 18:06 Vectorized

what about +!, -!, /!, and *! as aliases for the unsafe version of each of these operators

so that

GK0: uint256 = (
    unsafe(
        safe(unsafe(safe(2 * K0 * K0) / 10**36) * K0) / 10**36
    )
    + unsafe((_A_gamma[1] + 10**18) ** 2)
    - unsafe(
        unsafe(K0 ** 2 / 10**36) * unsafe(2 * _A_gamma[1] + safe(3 * 10**18)) / 10**18
    )
)

becomes

GK0: uint256 = (((2 * K0 * K0) /! 10**36) * K0) /! 10**36) 
    + ((_A_gamma[1] +! 10**18) ** 2) 
    - ((K0 ** 2 /! 10 ** 36) * (2 *! _A_gamma[1] +! (3 * 10**18) /! 10**18)

z80dev avatar Jun 28 '23 19:06 z80dev

@z80dev Not bad! This looks more readable! Question: why would the ! come before or after the operator? Does that matter?

bout3fiddy avatar Jul 23 '23 15:07 bout3fiddy