From 44a593a0bb87b0d6d18df1437ee6625cb5934003 Mon Sep 17 00:00:00 2001 From: Charles Cooper Date: Wed, 10 Apr 2024 13:12:42 -0400 Subject: [PATCH] feat[lang]!: add feature flag for decimals (#3930) 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. --- tests/conftest.py | 2 ++ .../codegen/types/numbers/test_decimals.py | 21 +++++++++++++++++++ tests/unit/ast/test_pre_parser.py | 2 +- vyper/ast/pre_parser.py | 7 +++++++ vyper/cli/vyper_compile.py | 4 ++++ vyper/compiler/phases.py | 1 + vyper/compiler/settings.py | 10 +++++++++ vyper/exceptions.py | 4 ++++ vyper/semantics/analysis/local.py | 4 ++-- vyper/semantics/types/utils.py | 19 +++++++++++++---- 10 files changed, 67 insertions(+), 7 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index 0dd54b65c0..824fa795ab 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -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 @@ -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, diff --git a/tests/functional/codegen/types/numbers/test_decimals.py b/tests/functional/codegen/types/numbers/test_decimals.py index e66d2f001f..59810776eb 100644 --- a/tests/functional/codegen/types/numbers/test_decimals.py +++ b/tests/functional/codegen/types/numbers/test_decimals.py @@ -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, @@ -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 diff --git a/tests/unit/ast/test_pre_parser.py b/tests/unit/ast/test_pre_parser.py index cd1a91f210..da7d72b8ec 100644 --- a/tests/unit/ast/test_pre_parser.py +++ b/tests/unit/ast/test_pre_parser.py @@ -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 diff --git a/vyper/ast/pre_parser.py b/vyper/ast/pre_parser.py index 645c857a2f..535618b2e2 100644 --- a/vyper/ast/pre_parser.py +++ b/vyper/ast/pre_parser.py @@ -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) @@ -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]}`") diff --git a/vyper/cli/vyper_compile.py b/vyper/cli/vyper_compile.py index 1aac5455b0..fc5477d3ac 100755 --- a/vyper/cli/vyper_compile.py +++ b/vyper/cli/vyper_compile.py @@ -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) @@ -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) diff --git a/vyper/compiler/phases.py b/vyper/compiler/phases.py index 8fe55a2016..a9a003f80a 100644 --- a/vyper/compiler/phases.py +++ b/vyper/compiler/phases.py @@ -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 diff --git a/vyper/compiler/settings.py b/vyper/compiler/settings.py index f416cf4c95..0e232472ea 100644 --- a/vyper/compiler/settings.py +++ b/vyper/compiler/settings.py @@ -38,6 +38,9 @@ def default(cls): return cls.GAS +DEFAULT_ENABLE_DECIMALS = False + + @dataclass class Settings: compiler_version: Optional[str] = None @@ -45,6 +48,13 @@ class Settings: 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? diff --git a/vyper/exceptions.py b/vyper/exceptions.py index 7fb7e33da5..183dd63b76 100644 --- a/vyper/exceptions.py +++ b/vyper/exceptions.py @@ -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.""" diff --git a/vyper/semantics/analysis/local.py b/vyper/semantics/analysis/local.py index b0a6e38d10..e23a267a15 100644 --- a/vyper/semantics/analysis/local.py +++ b/vyper/semantics/analysis/local.py @@ -20,6 +20,8 @@ VariableDeclarationException, VyperException, ) + +# TODO consolidate some of these imports from vyper.semantics.analysis.base import ( Modifiability, ModuleInfo, @@ -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 ( diff --git a/vyper/semantics/types/utils.py b/vyper/semantics/types/utils.py index 93cf85d5f8..7b0b43990f 100644 --- a/vyper/semantics/types/utils.py +++ b/vyper/semantics/types/utils.py @@ -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, @@ -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: