Repository: vyperlang/vyper

Issue:
VIP: Add min and max type
## Simple Summary
Add the ability to get the minimum and maximum values for a given type via `min_value(type)` and `max_value(type)`. Remove value constants like `ZERO_ADDRESS`, `MIN_UINT256`, and `EMPTY_BYTES32`

## Motivation
#1675 adds `empty(type)` to specify an empty section of memory filling the size of `type`. This can be used to simulate "default" values that the EVM provides all types. Some types deserve the ability to also specify their edge-case values, so functionality should be added to ensure this is possible. After this featureset is fully implemented, it makes sense to eliminate the standard constants that are provided with Vyper, as they can be replicated with this more advanced featureset

## Specification
### `min_value(type)`
Returns the minimum representation value of the given type (the most negative value for a given type). Raises a compilation error if the type does not have a minimum representation type (like `bytes32` or `address`)

Throws for more complex types.

### `max_value(type)`
Returns the maximum representation value of the given type (the most positive value for a given type). Raises a compilation error if the type does not have a maximum representation type (like `bytes32` or `address`)

Throws for more complex types.

### Built-in Constants
All built-in constants can be replicated as follows:
```
ZERO_ADDRESS: address = empty(address)
EMPTY_BYTES32: bytes32 = empty(bytes32)
MAX_INT128: int128 = max_value(int128)
MIN_INT128: int128 = min_value(int128)
MAX_DECIMAL: decimal = max_value(decimal)
MIN_DECIMAL: decimal = min_value(decimal)
MAX_UINT256: uint256 = max_value(uint256)
MIN_UINT256: uint256 = min_value(uint256)  # Same as empty(uint256)
```

## Backwards Compatibility
Removes built-in constants

## Dependencies
No dependencies

## Copyright
Copyright and related rights waived via [CC0](https://creativecommons.org/publicdomain/zero/1.0/)

---

EDIT: Modified `min` and `max` to `min_value` and `max_value`


Discussion:
great VIP!
I like the functionality but I'm not a fan of the names. Vyper already uses `min` and `max` as arithmetic functions returning the lower or higher of two numeric values. As described, this VIP will give these functions two completely different functionalities depending on the input(s) given.

Is there another set of function names that are just as descriptive and not already in use?
We can just do `min_value(type)` and `max_value(type)`, which are much more descriptive
:+1: for `min_value` and `max_value`

Resolving patch:
diff --git a/vyper/ast/folding.py b/vyper/ast/folding.py
--- a/vyper/ast/folding.py
+++ b/vyper/ast/folding.py
@@ -1,3 +1,4 @@
+import warnings
 from typing import Union
 
 from vyper.ast import nodes as vy_ast
@@ -11,13 +12,14 @@
     "EMPTY_BYTES32": (
         vy_ast.Hex,
         "0x0000000000000000000000000000000000000000000000000000000000000000",
+        "empty(bytes32)",
     ),  # NOQA: E501
-    "ZERO_ADDRESS": (vy_ast.Hex, "0x0000000000000000000000000000000000000000"),
-    "MAX_INT128": (vy_ast.Int, 2 ** 127 - 1),
-    "MIN_INT128": (vy_ast.Int, -(2 ** 127)),
-    "MAX_DECIMAL": (vy_ast.Decimal, SizeLimits.MAX_AST_DECIMAL),
-    "MIN_DECIMAL": (vy_ast.Decimal, SizeLimits.MIN_AST_DECIMAL),
-    "MAX_UINT256": (vy_ast.Int, 2 ** 256 - 1),
+    "ZERO_ADDRESS": (vy_ast.Hex, "0x0000000000000000000000000000000000000000", "empty(address)"),
+    "MAX_INT128": (vy_ast.Int, 2 ** 127 - 1, "max_value(int128)"),
+    "MIN_INT128": (vy_ast.Int, -(2 ** 127), "min_value(int128)"),
+    "MAX_DECIMAL": (vy_ast.Decimal, SizeLimits.MAX_AST_DECIMAL, "max_value(decimal)"),
+    "MIN_DECIMAL": (vy_ast.Decimal, SizeLimits.MIN_AST_DECIMAL, "min_value(decimal)"),
+    "MAX_UINT256": (vy_ast.Int, 2 ** 256 - 1, "max_value(uint256)"),
 }
 
 
@@ -145,8 +147,10 @@ def replace_builtin_constants(vyper_module: vy_ast.Module) -> None:
     vyper_module : Module
         Top-level Vyper AST node.
     """
-    for name, (node, value) in BUILTIN_CONSTANTS.items():
-        replace_constant(vyper_module, name, node(value=value), True)  # type: ignore
+    for name, (node, value, replacement) in BUILTIN_CONSTANTS.items():
+        found = replace_constant(vyper_module, name, node(value=value), True)
+        if found > 0:
+            warnings.warn(f"{name} is deprecated. Please use `{replacement}` instead.")
 
 
 def replace_user_defined_constants(vyper_module: vy_ast.Module) -> int:
diff --git a/vyper/builtin_functions/functions.py b/vyper/builtin_functions/functions.py
--- a/vyper/builtin_functions/functions.py
+++ b/vyper/builtin_functions/functions.py
@@ -49,6 +49,7 @@
     TupleType,
     get_type_for_exact_size,
     is_base_type,
+    parse_decimal_info,
     parse_integer_typeinfo,
 )
 from vyper.codegen.types.convert import new_type_to_old_type
@@ -2505,6 +2506,62 @@ def build_IR(self, expr, args, kwargs, context):
             )
 
 
+class _MinMaxValue(BuiltinFunction):
+    _inputs = [("typename", "TYPE_DEFINITION")]
+
+    def evaluate(self, node):
+        self._validate_arg_types(node)
+        input_type = get_type_from_annotation(node.args[0], DataLocation.UNSET)
+
+        if not isinstance(input_type, NumericAbstractType):
+            raise InvalidType(f"Expected numeric type but got {input_type} instead", node)
+
+        if isinstance(input_type, DecimalDefinition):
+            val = self._eval_decimal(input_type)
+            return vy_ast.Decimal.from_node(node, value=val)
+
+        if isinstance(input_type, IntegerAbstractType):
+            val = self._eval_int(input_type)
+            return vy_ast.Int.from_node(node, value=val)
+
+    def fetch_call_return(self, node):  # pragma: no cover
+        raise CompilerPanic(f"{self._id} should always be folded")
+
+    def infer_arg_types(self, node):  # pragma: no cover
+        raise CompilerPanic(f"{self._id} should always be folded")
+
+    def infer_kwarg_types(self, node):  # pragma: no cover
+        raise CompilerPanic(f"{self._id} should always be folded")
+
+    # TODO we may want to provide this as the default impl on the base class
+    def build_IR(self, *args, **kwargs):  # pragma: no cover
+        raise CompilerPanic(f"{self._id} should always be folded")
+
+
+class MinValue(_MinMaxValue):
+    _id = "min_value"
+
+    def _eval_int(self, type_):
+        typinfo = parse_integer_typeinfo(str(type_))
+        return typinfo.bounds[0]
+
+    def _eval_decimal(self, type_):
+        typinfo = parse_decimal_info(str(type_))
+        return typinfo.decimal_bounds[0]
+
+
+class MaxValue(_MinMaxValue):
+    _id = "max_value"
+
+    def _eval_int(self, type_):
+        typinfo = parse_integer_typeinfo(str(type_))
+        return typinfo.bounds[1]
+
+    def _eval_decimal(self, type_):
+        typinfo = parse_decimal_info(str(type_))
+        return typinfo.decimal_bounds[1]
+
+
 DISPATCH_TABLE = {
     "_abi_encode": ABIEncode(),
     "_abi_decode": ABIDecode(),
@@ -2546,6 +2603,8 @@ def build_IR(self, expr, args, kwargs, context):
     "max": Max(),
     "empty": Empty(),
     "abs": Abs(),
+    "min_value": MinValue(),
+    "max_value": MaxValue(),
 }
 
 STMT_DISPATCH_TABLE = {
diff --git a/vyper/semantics/namespace.py b/vyper/semantics/namespace.py
--- a/vyper/semantics/namespace.py
+++ b/vyper/semantics/namespace.py
@@ -202,8 +202,6 @@ def validate_identifier(attr):
     "empty_bytes32",
     "max_int128",
     "min_int128",
-    "max_int256",
-    "min_int256",
     "max_decimal",
     "min_decimal",
     "max_uint256",


Test patch:
diff --git a/tests/ast/test_folding.py b/tests/ast/test_folding.py
--- a/tests/ast/test_folding.py
+++ b/tests/ast/test_folding.py
@@ -133,15 +133,15 @@ def test_replace_constant_no(source):
 
 
 builtins_modified = [
-    "MAX_INT128",
-    "foo = MAX_INT128",
-    "foo: int128[MAX_INT128] = 42",
-    "foo = [MAX_INT128]",
-    "def foo(bar: int128 = MAX_INT128): pass",
-    "def foo(): bar = MAX_INT128",
-    "def foo(): return MAX_INT128",
-    "log foo(MAX_INT128)",
-    "log foo(42, MAX_INT128)",
+    "ZERO_ADDRESS",
+    "foo = ZERO_ADDRESS",
+    "foo: int128[ZERO_ADDRESS] = 42",
+    "foo = [ZERO_ADDRESS]",
+    "def foo(bar: address = ZERO_ADDRESS): pass",
+    "def foo(): bar = ZERO_ADDRESS",
+    "def foo(): return ZERO_ADDRESS",
+    "log foo(ZERO_ADDRESS)",
+    "log foo(42, ZERO_ADDRESS)",
 ]
 
 
@@ -156,12 +156,12 @@ def test_replace_builtin_constant(source):
 
 
 builtins_unmodified = [
-    "MAX_INT128 = 2",
-    "MAX_INT128()",
-    "def foo(MAX_INT128: int128 = 42): pass",
-    "def foo(): MAX_INT128 = 42",
-    "def MAX_INT128(): pass",
-    "log MAX_INT128(42)",
+    "ZERO_ADDRESS = 2",
+    "ZERO_ADDRESS()",
+    "def foo(ZERO_ADDRESS: int128 = 42): pass",
+    "def foo(): ZERO_ADDRESS = 42",
+    "def ZERO_ADDRESS(): pass",
+    "log ZERO_ADDRESS(42)",
 ]
 
 
diff --git a/tests/parser/features/iteration/test_for_in_list.py b/tests/parser/features/iteration/test_for_in_list.py
--- a/tests/parser/features/iteration/test_for_in_list.py
+++ b/tests/parser/features/iteration/test_for_in_list.py
@@ -754,7 +754,7 @@ def foo():
 @external
 def test_for() -> int128:
     a: int128 = 0
-    for i in range(MAX_INT128, MAX_INT128+2):
+    for i in range(max_value(int128), max_value(int128)+2):
         a = i
     return a
     """,
diff --git a/tests/parser/functions/test_abi_decode.py b/tests/parser/functions/test_abi_decode.py
--- a/tests/parser/functions/test_abi_decode.py
+++ b/tests/parser/functions/test_abi_decode.py
@@ -277,7 +277,7 @@ def _foo(bs: Bytes[160]):
 
 @external
 def foo(bs: Bytes[160]) -> (uint256, DynArray[uint256, 3]):
-    dont_clobber_me: uint256 = MAX_UINT256
+    dont_clobber_me: uint256 = max_value(uint256)
     self._foo(bs)
     return dont_clobber_me, self.bytez
     """
@@ -297,7 +297,7 @@ def _foo(bs: Bytes[1696]):
 
 @external
 def foo(bs: Bytes[1696]) -> (uint256, DynArray[DynArray[DynArray[uint256, 3], 3], 3]):
-    dont_clobber_me: uint256 = MAX_UINT256
+    dont_clobber_me: uint256 = max_value(uint256)
     self._foo(bs)
     return dont_clobber_me, self.bytez
     """
diff --git a/tests/parser/functions/test_abi_encode.py b/tests/parser/functions/test_abi_encode.py
--- a/tests/parser/functions/test_abi_encode.py
+++ b/tests/parser/functions/test_abi_encode.py
@@ -303,7 +303,7 @@ def _foo(bs: Bytes[32]):
 
 @external
 def foo(bs: Bytes[32]) -> (uint256, Bytes[96]):
-    dont_clobber_me: uint256 = MAX_UINT256
+    dont_clobber_me: uint256 = max_value(uint256)
     self._foo(bs)
     return dont_clobber_me, self.bytez
     """
@@ -320,7 +320,7 @@ def _foo(bs: DynArray[uint256, 3]):
     self.bytez = _abi_encode(bs)
 @external
 def foo(bs: DynArray[uint256, 3]) -> (uint256, Bytes[160]):
-    dont_clobber_me: uint256 = MAX_UINT256
+    dont_clobber_me: uint256 = max_value(uint256)
     self._foo(bs)
     return dont_clobber_me, self.bytez
     """
@@ -338,7 +338,7 @@ def _foo(bs: DynArray[DynArray[DynArray[uint256, 3], 3], 3]):
 
 @external
 def foo(bs: DynArray[DynArray[DynArray[uint256, 3], 3], 3]) -> (uint256, Bytes[1696]):
-    dont_clobber_me: uint256 = MAX_UINT256
+    dont_clobber_me: uint256 = max_value(uint256)
     self._foo(bs)
     return dont_clobber_me, self.bytez
     """
diff --git a/tests/parser/functions/test_default_parameters.py b/tests/parser/functions/test_default_parameters.py
--- a/tests/parser/functions/test_default_parameters.py
+++ b/tests/parser/functions/test_default_parameters.py
@@ -197,18 +197,6 @@ def callMeMaybe() -> (Bytes[100], uint256, Bytes[20]):
     assert c.callMeMaybe() == [b"here is my number", 555123456, b"baby"]
 
 
-def test_builtin_constants_as_default(get_contract):
-    code = """
-@external
-def foo(a: int128 = MIN_INT128, b: int128 = MAX_INT128) -> (int128, int128):
-    return a, b
-    """
-    c = get_contract(code)
-    assert c.foo() == [-(2 ** 127), 2 ** 127 - 1]
-    assert c.foo(31337) == [31337, 2 ** 127 - 1]
-    assert c.foo(13, 42) == [13, 42]
-
-
 def test_environment_vars_as_default(get_contract):
     code = """
 xx: uint256
diff --git a/tests/parser/functions/test_minmax.py b/tests/parser/functions/test_minmax.py
--- a/tests/parser/functions/test_minmax.py
+++ b/tests/parser/functions/test_minmax.py
@@ -239,19 +239,19 @@ def test_signed(get_contract_with_gas_estimation):
     code = """
 @external
 def foo1() -> int128:
-    return min(MIN_INT128, MAX_INT128)
+    return min(min_value(int128), max_value(int128))
 
 @external
 def foo2() -> int128:
-    return min(MAX_INT128, MIN_INT128)
+    return min(max_value(int128), min_value(int128))
 
 @external
 def foo3() -> int128:
-    return max(MIN_INT128, MAX_INT128)
+    return max(min_value(int128), max_value(int128))
 
 @external
 def foo4() -> int128:
-    return max(MAX_INT128, MIN_INT128)
+    return max(max_value(int128), min_value(int128))
     """
 
     c = get_contract_with_gas_estimation(code)
diff --git a/tests/parser/functions/test_minmax_value.py b/tests/parser/functions/test_minmax_value.py
new file mode 100644
--- /dev/null
+++ b/tests/parser/functions/test_minmax_value.py
@@ -0,0 +1,89 @@
+import pytest
+
+from vyper.codegen.types import (
+    DECIMAL_TYPES,
+    INTEGER_TYPES,
+    parse_decimal_info,
+    parse_integer_typeinfo,
+)
+from vyper.exceptions import InvalidType, OverflowException
+from vyper.utils import int_bounds
+
+
+@pytest.mark.parametrize("typ", sorted(INTEGER_TYPES))
+@pytest.mark.parametrize("op", ("min_value", "max_value"))
+def test_minmax_value_int(get_contract, op, typ):
+    code = f"""
+@external
+def foo() -> {typ}:
+    return {op}({typ})
+    """
+    c = get_contract(code)
+
+    typ_info = parse_integer_typeinfo(typ)
+    (lo, hi) = int_bounds(typ_info.is_signed, typ_info.bits)
+    if op == "min_value":
+        assert c.foo() == lo
+    elif op == "max_value":
+        assert c.foo() == hi
+
+
+@pytest.mark.parametrize("typ", sorted(INTEGER_TYPES))
+def test_minmax_value_int_oob(get_contract, assert_compile_failed, typ):
+    upper = f"""
+@external
+def foo():
+    a: {typ} = max_value({typ}) + 1
+    """
+
+    lower = f"""
+@external
+def foo():
+    a: {typ} = min_value({typ}) - 1
+    """
+
+    if typ == "uint256":
+        assert_compile_failed(lambda: get_contract(upper), OverflowException)
+    else:
+        assert_compile_failed(lambda: get_contract(upper), InvalidType)
+
+    if typ == "int256":
+        assert_compile_failed(lambda: get_contract(lower), OverflowException)
+    else:
+        assert_compile_failed(lambda: get_contract(lower), InvalidType)
+
+
+@pytest.mark.parametrize("typ", sorted(DECIMAL_TYPES))
+@pytest.mark.parametrize("op", ("min_value", "max_value"))
+def test_minmax_value_decimal(get_contract, op, typ):
+    code = f"""
+@external
+def foo() -> {typ}:
+    return {op}({typ})
+    """
+    c = get_contract(code)
+
+    typ_info = parse_decimal_info(typ)
+    (lo, hi) = typ_info.decimal_bounds
+    if op == "min_value":
+        assert c.foo() == lo
+    elif op == "max_value":
+        assert c.foo() == hi
+
+
+@pytest.mark.parametrize("typ", sorted(DECIMAL_TYPES))
+def test_minmax_value_decimal_oob(get_contract, assert_compile_failed, typ):
+    upper = f"""
+@external
+def foo():
+    a: {typ} = max_value({typ}) + 1e-10
+    """
+
+    lower = f"""
+@external
+def foo():
+    a: {typ} = min_value({typ}) - 1e-10
+    """
+
+    assert_compile_failed(lambda: get_contract(upper), OverflowException)
+    assert_compile_failed(lambda: get_contract(lower), OverflowException)
diff --git a/tests/parser/functions/test_slice.py b/tests/parser/functions/test_slice.py
--- a/tests/parser/functions/test_slice.py
+++ b/tests/parser/functions/test_slice.py
@@ -158,7 +158,7 @@ def _slice(start: uint256, length: uint256):
 @external
 def foo(x: uint256, y: uint256) -> (uint256, String[12]):
     self.bytez = "hello, world"
-    dont_clobber_me: uint256 = MAX_UINT256
+    dont_clobber_me: uint256 = max_value(uint256)
     self._slice(x, y)
     return dont_clobber_me, self.bytez
     """
diff --git a/tests/parser/syntax/test_constants.py b/tests/parser/syntax/test_constants.py
--- a/tests/parser/syntax/test_constants.py
+++ b/tests/parser/syntax/test_constants.py
@@ -144,7 +144,7 @@ def test() -> int128:
 test_a : constant(int128) = 2188824287183927522224640574525
     """,
     """
-test_a: constant(uint256) = MAX_UINT256
+test_a: constant(uint256) = max_value(uint256)
     """,
     """
 TEST_C: constant(uint256) = 1
diff --git a/tests/parser/syntax/test_minmax_value.py b/tests/parser/syntax/test_minmax_value.py
new file mode 100644
--- /dev/null
+++ b/tests/parser/syntax/test_minmax_value.py
@@ -0,0 +1,22 @@
+import pytest
+
+from vyper.exceptions import InvalidType
+
+fail_list = [
+    """
+@external
+def foo():
+    a: address = min_value(address)
+    """,
+    """
+@external
+def foo():
+    a: address = max_value(address)
+    """,
+]
+
+
+@pytest.mark.parametrize("bad_code", fail_list)
+def test_block_fail(assert_compile_failed, get_contract_with_gas_estimation, bad_code):
+
+    assert_compile_failed(lambda: get_contract_with_gas_estimation(bad_code), InvalidType)
diff --git a/tests/parser/syntax/test_msg_data.py b/tests/parser/syntax/test_msg_data.py
--- a/tests/parser/syntax/test_msg_data.py
+++ b/tests/parser/syntax/test_msg_data.py
@@ -65,9 +65,9 @@ def test_memory_pointer_advances_appropriately(get_contract, keccak):
     code = """
 @external
 def foo() -> (uint256, Bytes[4], uint256):
-    a: uint256 = MAX_UINT256
+    a: uint256 = max_value(uint256)
     b: Bytes[4] = slice(msg.data, 0, 4)
-    c: uint256 = MAX_UINT256
+    c: uint256 = max_value(uint256)
 
     return (a, b, c)
 """
diff --git a/tests/parser/types/numbers/test_constants.py b/tests/parser/types/numbers/test_constants.py
--- a/tests/parser/types/numbers/test_constants.py
+++ b/tests/parser/types/numbers/test_constants.py
@@ -22,22 +22,22 @@ def test_empty_bytes32(a: bytes32) -> bool:
 
 @external
 def test_int128(a: int128) -> (bool, bool):
-    return a == MAX_INT128, a == MIN_INT128
+    return a == max_value(int128), a == min_value(int128)
 
 
 @external
 def test_decimal(a: decimal) -> (bool, bool):
-    return a == MAX_DECIMAL, a == MIN_DECIMAL
+    return a == max_value(decimal), a == min_value(decimal)
 
 
 @external
 def test_uint256(a: uint256) -> bool:
-    return a == MAX_UINT256
+    return a == max_value(uint256)
 
 
 @external
 def test_arithmetic(a: int128) -> int128:
-    return MAX_INT128 - a
+    return max_value(int128) - a
     """
 
     c = get_contract_with_gas_estimation(code)
@@ -71,12 +71,12 @@ def test_builtin_constants_assignment(get_contract_with_gas_estimation):
     code = """
 @external
 def foo() -> int128:
-    bar: int128 = MAX_INT128
+    bar: int128 = max_value(int128)
     return bar
 
 @external
 def goo() -> int128:
-    bar: int128 = MIN_INT128
+    bar: int128 = min_value(int128)
     return bar
 
 @external
@@ -91,17 +91,17 @@ def joo() -> address:
 
 @external
 def koo() -> decimal:
-    bar: decimal = MAX_DECIMAL
+    bar: decimal = max_value(decimal)
     return bar
 
 @external
 def loo() -> decimal:
-    bar: decimal = MIN_DECIMAL
+    bar: decimal = min_value(decimal)
     return bar
 
 @external
 def zoo() -> uint256:
-    bar: uint256 = MAX_UINT256
+    bar: uint256 = max_value(uint256)
     return bar
     """