Repository: vyperlang/vyper

Issue:
VIP: `epsilon(decimal)` builtin for decimal types
## Simple Summary
Add an `epsilon()` builtin which returns the smallest non-zero value for a decimal type.

## Motivation
Similar to https://github.com/vyperlang/vyper/issues/1923, it is nice to not have to type out these special constants (as is done for instance here: https://github.com/vyperlang/vyper/pull/2935/files#diff-86a0306fe1e101309bd0992f2780ec4f0c3dd319940131270904157845c9d9beR79)

## Specification
Add an `epsilon()` builtin which takes a `decimal` typedef as its argument, and returns the smallest non-zero value for that type. Currently as we only have one decimal type, this would be `epsilon(decimal) == 1e-10`. In general as we add more decimal types, `epsilon(Decimal[N, ...])` would return `1 / 10**N`. This is already in the typeinfo for decimals, it's just a matter of exposing as a builtin
https://github.com/vyperlang/vyper/blob/9a02a44d7b18ef3a618d2d1903283fd61aa71c88/vyper/codegen/types/types.py#L110-L112


## Copyright
Copyright and related rights waived via [CC0](https://creativecommons.org/publicdomain/zero/1.0/)



Discussion:
note: consider for future VIP to expose a `scale(decimal)` builtin which returns `1 / epsilon(decimal)`. This could be useful for instance for scaling raw integers for manual implementations of fixed point math.
meeting notes: approved

Resolving patch:
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
@@ -75,6 +75,7 @@
 from vyper.semantics.types.abstract import (
     ArrayValueAbstractType,
     BytesAbstractType,
+    FixedAbstractType,
     IntegerAbstractType,
     NumericAbstractType,
     SignedIntegerAbstractType,
@@ -121,6 +122,22 @@
 SHA256_PER_WORD_GAS = 12
 
 
+class FoldedFunction(BuiltinFunction):
+    # Base class for nodes which should always be folded
+
+    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")
+
+    def build_IR(self, *args, **kwargs):  # pragma: no cover
+        raise CompilerPanic(f"{self._id} should always be folded")
+
+
 class Floor(BuiltinFunction):
 
     _id = "floor"
@@ -701,7 +718,7 @@ def build_IR(self, expr, args, kwargs, context):
         )
 
 
-class MethodID(BuiltinFunction):
+class MethodID(FoldedFunction):
 
     _id = "method_id"
 
@@ -733,18 +750,6 @@ def evaluate(self, node):
         else:
             return vy_ast.Bytes.from_node(node, value=value.to_bytes(4, "big"))
 
-    def fetch_call_return(self, node):
-        raise CompilerPanic("method_id should always be folded")
-
-    def infer_arg_types(self, node):
-        raise CompilerPanic("method_id should always be folded")
-
-    def infer_kwarg_types(self, node):
-        raise CompilerPanic("method_id should always be folded")
-
-    def build_IR(self, *args, **kwargs):
-        raise CompilerPanic("method_id should always be folded")
-
 
 class ECRecover(BuiltinFunction):
 
@@ -2520,7 +2525,7 @@ def build_IR(self, expr, args, kwargs, context):
             )
 
 
-class _MinMaxValue(BuiltinFunction):
+class _MinMaxValue(FoldedFunction):
     _inputs = [("typename", "TYPE_DEFINITION")]
 
     def evaluate(self, node):
@@ -2538,19 +2543,6 @@ def evaluate(self, node):
             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"
@@ -2576,6 +2568,27 @@ def _eval_decimal(self, type_):
         return typinfo.decimal_bounds[1]
 
 
+class Epsilon(FoldedFunction):
+
+    _inputs = [("typename", "TYPE_DEFINITION")]
+    _id = "epsilon"
+
+    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, FixedAbstractType):
+            raise InvalidType(f"Expected decimal type but got {input_type} instead", node)
+
+        # this check seems redundant, but sets a pattern to be followed
+        # when new decimal types are created
+        if isinstance(input_type, DecimalDefinition):
+            typinfo = parse_decimal_info(str(input_type))
+            return vy_ast.Decimal.from_node(node, value=typinfo.epsilon)
+
+        raise CompilerPanic("unreachable")  # pragma: notest
+
+
 DISPATCH_TABLE = {
     "_abi_encode": ABIEncode(),
     "_abi_decode": ABIDecode(),
@@ -2619,6 +2632,7 @@ def _eval_decimal(self, type_):
     "abs": Abs(),
     "min_value": MinValue(),
     "max_value": MaxValue(),
+    "epsilon": Epsilon(),
 }
 
 STMT_DISPATCH_TABLE = {


Test patch:
diff --git a/tests/functions/folding/test_epsilon.py b/tests/functions/folding/test_epsilon.py
new file mode 100644
--- /dev/null
+++ b/tests/functions/folding/test_epsilon.py
@@ -0,0 +1,20 @@
+import pytest
+
+from vyper import ast as vy_ast
+from vyper import builtin_functions as vy_fn
+
+
+@pytest.mark.parametrize("typ_name", ["decimal"])
+def test_epsilon(get_contract, typ_name):
+    source = f"""
+@external
+def foo() -> {typ_name}:
+    return epsilon({typ_name})
+    """
+    contract = get_contract(source)
+
+    vyper_ast = vy_ast.parse_to_ast(f"epsilon({typ_name})")
+    old_node = vyper_ast.body[0].value
+    new_node = vy_fn.DISPATCH_TABLE["epsilon"].evaluate(old_node)
+
+    assert contract.foo() == new_node.value