From e4f1c2479ee8a82777aecb929945f641750b1536 Mon Sep 17 00:00:00 2001 From: Charles Cooper Date: Wed, 10 Apr 2024 11:57:36 -0400 Subject: [PATCH] refactor[codegen]: make settings into a global object (#3929) this commit adds a global settings object, unifying various functions which modify global settings: anchor_opt_level, anchor_evm_version, _set_debug --- tests/conftest.py | 53 ++++++++++++------- .../unit/cli/vyper_json/test_get_settings.py | 12 ++--- tests/unit/compiler/ir/test_optimize_ir.py | 11 ++-- tests/unit/compiler/test_default_settings.py | 9 ++-- tests/unit/compiler/test_opcodes.py | 10 ---- vyper/cli/vyper_compile.py | 13 ++--- vyper/codegen/core.py | 39 +------------- vyper/compiler/__init__.py | 6 +-- vyper/compiler/phases.py | 11 ++-- vyper/compiler/settings.py | 53 ++++++++++++++++--- vyper/evm/opcodes.py | 32 +++++------ 11 files changed, 119 insertions(+), 130 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index 45d1b99b6c..0dd54b65c0 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -21,8 +21,13 @@ from vyper.ast.grammar import parse_vyper_source from vyper.codegen.ir_node import IRnode from vyper.compiler.input_bundle import FilesystemInputBundle, InputBundle -from vyper.compiler.settings import OptimizationLevel, Settings, _set_debug_mode -from vyper.evm.opcodes import version_check +from vyper.compiler.settings import ( + OptimizationLevel, + Settings, + get_global_settings, + set_global_settings, +) +from vyper.evm.opcodes import EVM_VERSIONS, version_check from vyper.exceptions import EvmVersionException from vyper.ir import compile_ir, optimizer from vyper.utils import ERC5202_PREFIX, keccak256 @@ -65,7 +70,7 @@ def pytest_addoption(parser): parser.addoption( "--evm-version", - choices=list(evm.EVM_VERSIONS.keys()), + choices=list(EVM_VERSIONS.keys()), default="shanghai", help="set evm version", ) @@ -81,17 +86,17 @@ def output_formats(): return output_formats -@pytest.fixture(scope="module") +@pytest.fixture(scope="session") def optimize(pytestconfig): flag = pytestconfig.getoption("optimize") return OptimizationLevel.from_string(flag) -@pytest.fixture(scope="session", autouse=True) +@pytest.fixture(scope="session") def debug(pytestconfig): debug = pytestconfig.getoption("enable_compiler_debug_mode") assert isinstance(debug, bool) - _set_debug_mode(debug) + return debug @pytest.fixture(scope="session") @@ -101,6 +106,28 @@ def experimental_codegen(pytestconfig): return ret +@pytest.fixture(scope="session") +def evm_version(pytestconfig): + # note: we configure the evm version that we emit code for, + # but eth-tester is only configured with the latest mainnet + # version. luckily, evms are backwards compatible. + evm_version_str = pytestconfig.getoption("evm_version") + assert isinstance(evm_version_str, str) + return evm_version_str + + +@pytest.fixture(scope="session", autouse=True) +def global_settings(evm_version, experimental_codegen, optimize, debug): + evm.DEFAULT_EVM_VERSION = evm_version + settings = Settings( + optimize=optimize, + evm_version=evm_version, + experimental_codegen=experimental_codegen, + debug=debug, + ) + set_global_settings(settings) + + @pytest.fixture(autouse=True) def check_venom_xfail(request, experimental_codegen): if not experimental_codegen: @@ -124,18 +151,6 @@ def _xfail(*args, **kwargs): return _xfail -@pytest.fixture(scope="session", autouse=True) -def evm_version(pytestconfig): - # note: we configure the evm version that we emit code for, - # but eth-tester is only configured with the latest mainnet - # version. - evm_version_str = pytestconfig.getoption("evm_version") - evm.DEFAULT_EVM_VERSION = evm_version_str - # this should get overridden by anchor_evm_version, - # but set it anyway - evm.active_evm_version = evm.EVM_VERSIONS[evm_version_str] - - @pytest.fixture def chdir_tmp_path(tmp_path): # this is useful for when you want imports to have relpaths @@ -364,7 +379,7 @@ def _get_contract( input_bundle=None, **kwargs, ): - settings = Settings() + settings = get_global_settings() settings.optimize = override_opt_level or optimize settings.experimental_codegen = experimental_codegen out = compiler.compile_code( diff --git a/tests/unit/cli/vyper_json/test_get_settings.py b/tests/unit/cli/vyper_json/test_get_settings.py index 975cb9d143..540a26f062 100644 --- a/tests/unit/cli/vyper_json/test_get_settings.py +++ b/tests/unit/cli/vyper_json/test_get_settings.py @@ -10,7 +10,7 @@ def test_unknown_evm(): @pytest.mark.parametrize( - "evm_version", + "evm_version_str", [ "homestead", "tangerineWhistle", @@ -22,11 +22,11 @@ def test_unknown_evm(): "berlin", ], ) -def test_early_evm(evm_version): +def test_early_evm(evm_version_str): with pytest.raises(JSONError): - get_evm_version({"settings": {"evmVersion": evm_version}}) + get_evm_version({"settings": {"evmVersion": evm_version_str}}) -@pytest.mark.parametrize("evm_version", ["london", "paris", "shanghai", "cancun"]) -def test_valid_evm(evm_version): - assert evm_version == get_evm_version({"settings": {"evmVersion": evm_version}}) +@pytest.mark.parametrize("evm_version_str", ["london", "paris", "shanghai", "cancun"]) +def test_valid_evm(evm_version_str): + assert evm_version_str == get_evm_version({"settings": {"evmVersion": evm_version_str}}) diff --git a/tests/unit/compiler/ir/test_optimize_ir.py b/tests/unit/compiler/ir/test_optimize_ir.py index cb46ba238d..caeba797a0 100644 --- a/tests/unit/compiler/ir/test_optimize_ir.py +++ b/tests/unit/compiler/ir/test_optimize_ir.py @@ -1,13 +1,10 @@ import pytest from vyper.codegen.ir_node import IRnode -from vyper.evm.opcodes import EVM_VERSIONS, anchor_evm_version +from vyper.evm.opcodes import version_check from vyper.exceptions import StaticAssertionException from vyper.ir import optimizer -POST_CANCUN = {k: v for k, v in EVM_VERSIONS.items() if v >= EVM_VERSIONS["cancun"]} - - optimize_list = [ (["eq", 1, 2], [0]), (["lt", 1, 2], [1]), @@ -368,9 +365,9 @@ def test_operator_set_values(): @pytest.mark.parametrize("ir", mload_merge_list) -@pytest.mark.parametrize("evm_version", list(POST_CANCUN.keys())) def test_mload_merge(ir, evm_version): - with anchor_evm_version(evm_version): + # TODO: use something like pytest.mark.post_cancun + if version_check(begin="cancun"): optimized = optimizer.optimize(IRnode.from_list(ir[0])) if ir[1] is None: # no-op, assert optimizer does nothing @@ -379,3 +376,5 @@ def test_mload_merge(ir, evm_version): expected = IRnode.from_list(ir[1]) assert optimized == expected + else: + pytest.skip("no mcopy available") diff --git a/tests/unit/compiler/test_default_settings.py b/tests/unit/compiler/test_default_settings.py index ca05170b61..5466f2cd40 100644 --- a/tests/unit/compiler/test_default_settings.py +++ b/tests/unit/compiler/test_default_settings.py @@ -15,11 +15,10 @@ def test_default_opt_level(): assert OptimizationLevel.default() == OptimizationLevel.GAS -def test_codegen_opt_level(): - assert core._opt_level == OptimizationLevel.GAS - assert core._opt_gas() is True - assert core._opt_none() is False - assert core._opt_codesize() is False +def test_codegen_opt_level(optimize): + assert core._opt_gas() == (optimize == OptimizationLevel.GAS) + assert core._opt_none() == (optimize == OptimizationLevel.NONE) + assert core._opt_codesize() == (optimize == OptimizationLevel.CODESIZE) def test_debug_mode(pytestconfig): diff --git a/tests/unit/compiler/test_opcodes.py b/tests/unit/compiler/test_opcodes.py index 710348a274..6cd88777ca 100644 --- a/tests/unit/compiler/test_opcodes.py +++ b/tests/unit/compiler/test_opcodes.py @@ -6,16 +6,6 @@ from vyper.exceptions import CompilerPanic -@pytest.fixture(params=list(opcodes.EVM_VERSIONS)) -def evm_version(request): - default = opcodes.active_evm_version - try: - opcodes.active_evm_version = opcodes.EVM_VERSIONS[request.param] - yield request.param - finally: - opcodes.active_evm_version = default - - def test_opcodes(): code = """ @external diff --git a/vyper/cli/vyper_compile.py b/vyper/cli/vyper_compile.py index 7a3aa800f6..1aac5455b0 100755 --- a/vyper/cli/vyper_compile.py +++ b/vyper/cli/vyper_compile.py @@ -11,12 +11,7 @@ import vyper.evm.opcodes as evm from vyper.cli import vyper_json from vyper.compiler.input_bundle import FileInput, FilesystemInputBundle -from vyper.compiler.settings import ( - VYPER_TRACEBACK_LIMIT, - OptimizationLevel, - Settings, - _set_debug_mode, -) +from vyper.compiler.settings import VYPER_TRACEBACK_LIMIT, OptimizationLevel, Settings from vyper.typing import ContractPath, OutputFormats T = TypeVar("T") @@ -172,9 +167,6 @@ def _parse_args(argv): output_formats = tuple(uniq(args.format.split(","))) - if args.debug: - _set_debug_mode(True) - if args.no_optimize and args.optimize: raise ValueError("Cannot use `--no-optimize` and `--optimize` at the same time!") @@ -191,6 +183,9 @@ def _parse_args(argv): if args.experimental_codegen: settings.experimental_codegen = args.experimental_codegen + if args.debug: + settings.debug = args.debug + if args.verbose: print(f"cli specified: `{settings}`", file=sys.stderr) diff --git a/vyper/codegen/core.py b/vyper/codegen/core.py index 2cb2876088..c087e0b8d5 100644 --- a/vyper/codegen/core.py +++ b/vyper/codegen/core.py @@ -1,8 +1,5 @@ -import contextlib -from typing import Generator - from vyper.codegen.ir_node import Encoding, IRnode -from vyper.compiler.settings import OptimizationLevel +from vyper.compiler.settings import _opt_codesize, _opt_gas, _opt_none from vyper.evm.address_space import ( CALLDATA, DATA, @@ -916,40 +913,6 @@ def make_setter(left, right): return _complex_make_setter(left, right) -_opt_level = OptimizationLevel.GAS - - -# FIXME: this is to get around the fact that we don't have a -# proper context object in the IR generation phase. -@contextlib.contextmanager -def anchor_opt_level(new_level: OptimizationLevel) -> Generator: - """ - Set the global optimization level variable for the duration of this - context manager. - """ - assert isinstance(new_level, OptimizationLevel) - - global _opt_level - try: - tmp = _opt_level - _opt_level = new_level - yield - finally: - _opt_level = tmp - - -def _opt_codesize(): - return _opt_level == OptimizationLevel.CODESIZE - - -def _opt_gas(): - return _opt_level == OptimizationLevel.GAS - - -def _opt_none(): - return _opt_level == OptimizationLevel.NONE - - def _complex_make_setter(left, right): if right.value == "~empty" and left.location == MEMORY: # optimized memzero diff --git a/vyper/compiler/__init__.py b/vyper/compiler/__init__.py index 84aea73071..835877e124 100644 --- a/vyper/compiler/__init__.py +++ b/vyper/compiler/__init__.py @@ -7,8 +7,7 @@ import vyper.compiler.output as output from vyper.compiler.input_bundle import FileInput, InputBundle, PathLike from vyper.compiler.phases import CompilerData -from vyper.compiler.settings import Settings -from vyper.evm.opcodes import anchor_evm_version +from vyper.compiler.settings import Settings, anchor_settings from vyper.typing import ContractPath, OutputFormats, StorageLayout OUTPUT_FORMATS = { @@ -96,7 +95,6 @@ def compile_from_file_input( Dict Compiler output as `{'output key': "output data"}` """ - settings = settings or Settings() if output_formats is None: @@ -117,7 +115,7 @@ def compile_from_file_input( ) ret = {} - with anchor_evm_version(compiler_data.settings.evm_version): + with anchor_settings(compiler_data.settings): for output_format in output_formats: if output_format not in OUTPUT_FORMATS: raise ValueError(f"Unsupported format type {repr(output_format)}") diff --git a/vyper/compiler/phases.py b/vyper/compiler/phases.py index d794185195..8fe55a2016 100644 --- a/vyper/compiler/phases.py +++ b/vyper/compiler/phases.py @@ -6,10 +6,9 @@ from vyper import ast as vy_ast from vyper.codegen import module -from vyper.codegen.core import anchor_opt_level from vyper.codegen.ir_node import IRnode from vyper.compiler.input_bundle import FileInput, FilesystemInputBundle, InputBundle -from vyper.compiler.settings import OptimizationLevel, Settings +from vyper.compiler.settings import OptimizationLevel, Settings, anchor_settings from vyper.exceptions import StructureException from vyper.ir import compile_ir, optimizer from vyper.semantics import analyze_module, set_data_positions, validate_compilation_target @@ -178,7 +177,7 @@ def global_ctx(self) -> ModuleT: @cached_property def _ir_output(self): # fetch both deployment and runtime IR - return generate_ir_nodes(self.global_ctx, self.settings.optimize) + return generate_ir_nodes(self.global_ctx, self.settings) @property def ir_nodes(self) -> IRnode: @@ -268,7 +267,7 @@ def generate_annotated_ast(vyper_module: vy_ast.Module, input_bundle: InputBundl return vyper_module -def generate_ir_nodes(global_ctx: ModuleT, optimize: OptimizationLevel) -> tuple[IRnode, IRnode]: +def generate_ir_nodes(global_ctx: ModuleT, settings: Settings) -> tuple[IRnode, IRnode]: """ Generate the intermediate representation (IR) from the contextualized AST. @@ -288,9 +287,9 @@ def generate_ir_nodes(global_ctx: ModuleT, optimize: OptimizationLevel) -> tuple IR to generate deployment bytecode IR to generate runtime bytecode """ - with anchor_opt_level(optimize): + with anchor_settings(settings): ir_nodes, ir_runtime = module.generate_ir_for_module(global_ctx) - if optimize != OptimizationLevel.NONE: + if settings.optimize != OptimizationLevel.NONE: ir_nodes = optimizer.optimize(ir_nodes) ir_runtime = optimizer.optimize(ir_runtime) return ir_nodes, ir_runtime diff --git a/vyper/compiler/settings.py b/vyper/compiler/settings.py index 51c8d64e41..f416cf4c95 100644 --- a/vyper/compiler/settings.py +++ b/vyper/compiler/settings.py @@ -1,7 +1,8 @@ +import contextlib import os from dataclasses import dataclass from enum import Enum -from typing import Optional +from typing import Generator, Optional VYPER_COLOR_OUTPUT = os.environ.get("VYPER_COLOR_OUTPUT", "0") == "1" VYPER_ERROR_CONTEXT_LINES = int(os.environ.get("VYPER_ERROR_CONTEXT_LINES", "1")) @@ -43,16 +44,52 @@ class Settings: optimize: Optional[OptimizationLevel] = None evm_version: Optional[str] = None experimental_codegen: Optional[bool] = None + debug: Optional[bool] = None -_DEBUG = False +# CMC 2024-04-10 do we need it to be Optional? +_settings = None -def _is_debug_mode(): - global _DEBUG - return _DEBUG +def get_global_settings() -> Optional[Settings]: + return _settings + + +def set_global_settings(new_settings: Optional[Settings]) -> None: + assert isinstance(new_settings, Settings) or new_settings is None + + global _settings + _settings = new_settings + + +# could maybe refactor this, but it is easier for now than threading settings +# around everywhere. +@contextlib.contextmanager +def anchor_settings(new_settings: Settings) -> Generator: + """ + Set the globally available settings for the duration of this context manager + """ + assert new_settings is not None + global _settings + try: + tmp = get_global_settings() + set_global_settings(new_settings) + yield + finally: + set_global_settings(tmp) + +def _opt_codesize(): + return _settings.optimize == OptimizationLevel.CODESIZE -def _set_debug_mode(dbg: bool = False) -> None: - global _DEBUG - _DEBUG = dbg + +def _opt_gas(): + return _settings.optimize == OptimizationLevel.GAS + + +def _opt_none(): + return _settings.optimize == OptimizationLevel.NONE + + +def _is_debug_mode(): + return get_global_settings().debug diff --git a/vyper/evm/opcodes.py b/vyper/evm/opcodes.py index 48edf48f19..023b3b5fe0 100644 --- a/vyper/evm/opcodes.py +++ b/vyper/evm/opcodes.py @@ -1,6 +1,6 @@ -import contextlib -from typing import Dict, Generator, Optional +from typing import Dict, Optional +from vyper.compiler.settings import get_global_settings from vyper.exceptions import CompilerPanic from vyper.typing import OpcodeGasCost, OpcodeMap, OpcodeRulesetMap, OpcodeRulesetValue, OpcodeValue @@ -14,9 +14,7 @@ _evm_versions = ("london", "paris", "shanghai", "cancun") EVM_VERSIONS: dict[str, int] = dict((v, i) for i, v in enumerate(_evm_versions)) - -DEFAULT_EVM_VERSION: str = "shanghai" -active_evm_version: int = EVM_VERSIONS[DEFAULT_EVM_VERSION] +DEFAULT_EVM_VERSION = "shanghai" # opcode as hex value @@ -208,18 +206,6 @@ IR_OPCODES: OpcodeMap = {**OPCODES, **PSEUDO_OPCODES} -@contextlib.contextmanager -def anchor_evm_version(evm_version: Optional[str] = None) -> Generator: - global active_evm_version - anchor = active_evm_version - evm_version = evm_version or DEFAULT_EVM_VERSION - active_evm_version = EVM_VERSIONS[evm_version] - try: - yield - finally: - active_evm_version = anchor - - def _gas(value: OpcodeValue, idx: int) -> Optional[OpcodeRulesetValue]: gas: OpcodeGasCost = value[3] if isinstance(gas, int): @@ -245,15 +231,23 @@ def _mk_version_opcodes(opcodes: OpcodeMap, idx: int) -> OpcodeRulesetMap: } +def get_active_evm_version(): + settings = get_global_settings() + evm_version_str = settings and settings.evm_version or DEFAULT_EVM_VERSION + return EVM_VERSIONS[evm_version_str] + + def get_opcodes() -> OpcodeRulesetMap: - return _evm_opcodes[active_evm_version] + return _evm_opcodes[get_active_evm_version()] def get_ir_opcodes() -> OpcodeRulesetMap: - return _ir_opcodes[active_evm_version] + return _ir_opcodes[get_active_evm_version()] def version_check(begin: Optional[str] = None, end: Optional[str] = None) -> bool: + active_evm_version = get_active_evm_version() + if begin is None and end is None: raise CompilerPanic("Either beginning or end fork ruleset must be set.") if begin is None: