Skip to content

Commit

Permalink
feat[lang]!: add feature flag for decimals (#3930)
Browse files Browse the repository at this point in the history
hide decimals behind a feature flag. this informally lets us "break the
compatibility contract" with users by moving decimals into
quasi-experimental status; this way we can iterate on the decimals
design faster in the 0.4.x series instead of needing to wait for
breaking releases to make changes.
  • Loading branch information
charles-cooper authored Apr 10, 2024
1 parent e4f1c24 commit 44a593a
Show file tree
Hide file tree
Showing 10 changed files with 67 additions and 7 deletions.
2 changes: 2 additions & 0 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
from web3.contract import Contract
from web3.providers.eth_tester import EthereumTesterProvider

import vyper.compiler.settings as compiler_settings
import vyper.evm.opcodes as evm
from tests.utils import working_directory
from vyper import compiler
Expand Down Expand Up @@ -119,6 +120,7 @@ def evm_version(pytestconfig):
@pytest.fixture(scope="session", autouse=True)
def global_settings(evm_version, experimental_codegen, optimize, debug):
evm.DEFAULT_EVM_VERSION = evm_version
compiler_settings.DEFAULT_ENABLE_DECIMALS = True
settings = Settings(
optimize=optimize,
evm_version=evm_version,
Expand Down
21 changes: 21 additions & 0 deletions tests/functional/codegen/types/numbers/test_decimals.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,12 @@

import pytest

import vyper.compiler.settings as compiler_settings
from tests.utils import decimal_to_int
from vyper import compile_code
from vyper.exceptions import (
DecimalOverrideException,
FeatureException,
InvalidOperation,
OverflowException,
TypeMismatch,
Expand Down Expand Up @@ -317,3 +319,22 @@ def foo():
compile_code(code)

assert e.value._hint == "did you mean `5.0 / 9.0`?"


def test_decimals_blocked():
code = """
@external
def foo(x: decimal):
pass
"""
# enable_decimals default to False normally, but defaults to True in the
# test suite. this test manually overrides the default value to test the
# "normal" behavior of enable_decimals outside of the test suite.
try:
assert compiler_settings.DEFAULT_ENABLE_DECIMALS is True
compiler_settings.DEFAULT_ENABLE_DECIMALS = False
with pytest.raises(FeatureException) as e:
compile_code(code)
assert e.value._message == "decimals are not allowed unless `--enable-decimals` is set"
finally:
compiler_settings.DEFAULT_ENABLE_DECIMALS = True
2 changes: 1 addition & 1 deletion tests/unit/ast/test_pre_parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -185,7 +185,7 @@ def test_parse_pragmas(code, pre_parse_settings, compiler_data_settings, mock_ve
# None is sentinel here meaning that nothing changed
compiler_data_settings = pre_parse_settings

# cannot be set via pragma, don't check
# experimental_codegen is False by default
compiler_data_settings.experimental_codegen = False

assert compiler_data.settings == compiler_data_settings
Expand Down
7 changes: 7 additions & 0 deletions vyper/ast/pre_parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,7 @@ def pre_parse(code: str) -> tuple[Settings, ModificationOffsets, dict, str]:
validate_version_pragma(compiler_version, code, start)
settings.compiler_version = compiler_version

# TODO: refactor these to something like Settings.from_pragma
elif pragma.startswith("optimize "):
if settings.optimize is not None:
raise StructureException("pragma optimize specified twice!", start)
Expand All @@ -212,6 +213,12 @@ def pre_parse(code: str) -> tuple[Settings, ModificationOffsets, dict, str]:
"pragma experimental-codegen specified twice!", start
)
settings.experimental_codegen = True
elif pragma.startswith("enable-decimals"):
if settings.enable_decimals is not None:
raise StructureException(
"pragma enable_decimals specified twice!", start
)
settings.enable_decimals = True

else:
raise StructureException(f"Unknown pragma `{pragma.split()[0]}`")
Expand Down
4 changes: 4 additions & 0 deletions vyper/cli/vyper_compile.py
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,7 @@ def _parse_args(argv):
action="store_true",
dest="experimental_codegen",
)
parser.add_argument("--enable-decimals", help="Enable decimals", action="store_true")

args = parser.parse_args(argv)

Expand Down Expand Up @@ -186,6 +187,9 @@ def _parse_args(argv):
if args.debug:
settings.debug = args.debug

if args.enable_decimals:
settings.enable_decimals = args.enable_decimals

if args.verbose:
print(f"cli specified: `{settings}`", file=sys.stderr)

Expand Down
1 change: 1 addition & 0 deletions vyper/compiler/phases.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ def _merge_settings(cli: Settings, pragma: Settings):
ret.experimental_codegen = _merge_one(
cli.experimental_codegen, pragma.experimental_codegen, "experimental codegen"
)
ret.enable_decimals = _merge_one(cli.enable_decimals, pragma.enable_decimals, "enable-decimals")

return ret

Expand Down
10 changes: 10 additions & 0 deletions vyper/compiler/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,13 +38,23 @@ def default(cls):
return cls.GAS


DEFAULT_ENABLE_DECIMALS = False


@dataclass
class Settings:
compiler_version: Optional[str] = None
optimize: Optional[OptimizationLevel] = None
evm_version: Optional[str] = None
experimental_codegen: Optional[bool] = None
debug: Optional[bool] = None
enable_decimals: Optional[bool] = None

# CMC 2024-04-10 consider hiding the `enable_decimals` member altogether
def get_enable_decimals(self) -> bool:
if self.enable_decimals is None:
return DEFAULT_ENABLE_DECIMALS
return self.enable_decimals


# CMC 2024-04-10 do we need it to be Optional?
Expand Down
4 changes: 4 additions & 0 deletions vyper/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -354,6 +354,10 @@ class UnimplementedException(VyperException):
"""Some feature is known to be not implemented"""


class FeatureException(VyperException):
"""Some feature flag is not enabled"""


class StaticAssertionException(VyperException):
"""An assertion is proven to fail at compile-time."""

Expand Down
4 changes: 2 additions & 2 deletions vyper/semantics/analysis/local.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@
VariableDeclarationException,
VyperException,
)

# TODO consolidate some of these imports
from vyper.semantics.analysis.base import (
Modifiability,
ModuleInfo,
Expand All @@ -36,8 +38,6 @@
validate_expected_type,
)
from vyper.semantics.data_locations import DataLocation

# TODO consolidate some of these imports
from vyper.semantics.environment import CONSTANT_ENVIRONMENT_VARS
from vyper.semantics.namespace import get_namespace
from vyper.semantics.types import (
Expand Down
19 changes: 15 additions & 4 deletions vyper/semantics/types/utils.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
from vyper import ast as vy_ast
from vyper.compiler.settings import get_global_settings
from vyper.exceptions import (
ArrayIndexException,
CompilerPanic,
FeatureException,
InstantiationException,
InvalidType,
StructureException,
Expand Down Expand Up @@ -83,13 +85,22 @@ def type_from_annotation(
VyperType
Type definition object.
"""
typ_ = _type_from_annotation(node)
typ = _type_from_annotation(node)

if location in typ_._invalid_locations:
if location in typ._invalid_locations:
location_str = "" if location is DataLocation.UNSET else f"in {location.name.lower()}"
raise InstantiationException(f"{typ_} is not instantiable {location_str}", node)
raise InstantiationException(f"{typ} is not instantiable {location_str}", node)

return typ_
# TODO: cursed import cycle!
from vyper.semantics.types.primitives import DecimalT

if isinstance(typ, DecimalT):
# is there a better place to put this check?
settings = get_global_settings()
if settings and not settings.get_enable_decimals():
raise FeatureException("decimals are not allowed unless `--enable-decimals` is set")

return typ


def _type_from_annotation(node: vy_ast.VyperNode) -> VyperType:
Expand Down

0 comments on commit 44a593a

Please sign in to comment.