diff --git a/tests/functional/codegen/calling_convention/test_internal_call.py b/tests/functional/codegen/calling_convention/test_internal_call.py index 4f58680e12..84b11192ea 100644 --- a/tests/functional/codegen/calling_convention/test_internal_call.py +++ b/tests/functional/codegen/calling_convention/test_internal_call.py @@ -142,7 +142,7 @@ def return_goose2() -> Bytes[10]: print("Passed multi-argument self-call test") -@pytest.mark.hevm("--max-iterations", "10") +@pytest.mark.hevm("--max-iterations", "100") def test_selfcall_code_5(get_contract): selfcall_code_5 = """ counter: int128 @@ -661,7 +661,7 @@ def bar(i: uint256) -> uint256: assert c.foo() == [2, 1] -@pytest.mark.hevm +@pytest.mark.hevm("--max-iterations", "100") def test_make_setter_internal_call2(get_contract): # cf. GH #3503 code = """ diff --git a/tests/functional/venom/test_empty_liveness_guard.py b/tests/functional/venom/test_empty_liveness_guard.py index 13ac6b5e90..bcafbe9f69 100644 --- a/tests/functional/venom/test_empty_liveness_guard.py +++ b/tests/functional/venom/test_empty_liveness_guard.py @@ -1,5 +1,5 @@ from tests.venom_utils import parse_venom -from vyper.compiler.settings import OptimizationLevel +from vyper.compiler.settings import OptimizationLevel, VenomOptimizationFlags from vyper.venom import generate_assembly_experimental, run_passes_on @@ -13,7 +13,8 @@ def test_empty_liveness_at_function_entry_param_then_stop(): } """ ctx = parse_venom(venom) - run_passes_on(ctx, OptimizationLevel.GAS) + flags = VenomOptimizationFlags(level=OptimizationLevel.GAS) + run_passes_on(ctx, flags) generate_assembly_experimental(ctx) @@ -38,5 +39,6 @@ def test_empty_liveness_param_then_revert_immediates2(): } """ ctx = parse_venom(venom) - run_passes_on(ctx, OptimizationLevel.GAS) + flags = VenomOptimizationFlags(level=OptimizationLevel.GAS) + run_passes_on(ctx, flags) generate_assembly_experimental(ctx) diff --git a/tests/functional/venom/test_venom_label_variables.py b/tests/functional/venom/test_venom_label_variables.py index 0f34f073d6..7b35c6223f 100644 --- a/tests/functional/venom/test_venom_label_variables.py +++ b/tests/functional/venom/test_venom_label_variables.py @@ -1,5 +1,5 @@ from vyper.compiler.phases import generate_bytecode -from vyper.compiler.settings import OptimizationLevel +from vyper.compiler.settings import OptimizationLevel, VenomOptimizationFlags from vyper.venom import generate_assembly_experimental, run_passes_on from vyper.venom.check_venom import check_venom_ctx from vyper.venom.parser import parse_venom @@ -80,6 +80,8 @@ def test_labels_as_variables(): check_venom_ctx(ctx) - run_passes_on(ctx, OptimizationLevel.default()) + opt_level = OptimizationLevel.default() + flags = VenomOptimizationFlags(level=opt_level) + run_passes_on(ctx, flags) asm = generate_assembly_experimental(ctx) generate_bytecode(asm) diff --git a/tests/functional/venom/test_venom_repr.py b/tests/functional/venom/test_venom_repr.py index c8bfc16229..8c664f1388 100644 --- a/tests/functional/venom/test_venom_repr.py +++ b/tests/functional/venom/test_venom_repr.py @@ -7,7 +7,7 @@ from tests.venom_utils import assert_ctx_eq, parse_venom from vyper.compiler import compile_code from vyper.compiler.phases import generate_bytecode -from vyper.compiler.settings import OptimizationLevel +from vyper.compiler.settings import OptimizationLevel, VenomOptimizationFlags from vyper.venom import generate_assembly_experimental, run_passes_on from vyper.venom.context import IRContext @@ -100,7 +100,8 @@ def _helper1(vyper_source, optimize): # check it's valid to run venom passes+analyses # (note this breaks bytecode equality, in the future we should # test that separately) - run_passes_on(ctx, optimize) + flags = VenomOptimizationFlags(level=optimize) + run_passes_on(ctx, flags) # test we can generate assembly+bytecode asm = generate_assembly_experimental(ctx) diff --git a/tests/hevm.py b/tests/hevm.py index 50847fc362..3be791464b 100644 --- a/tests/hevm.py +++ b/tests/hevm.py @@ -5,15 +5,13 @@ from tests.venom_utils import parse_from_basic_block from vyper.ir.compile_ir import assembly_to_evm -from vyper.venom import ( - CFGNormalization, - LowerDloadPass, - SimplifyCFGPass, - SingleUseExpansion, - VenomCompiler, -) +from vyper.venom import VenomCompiler from vyper.venom.analysis import IRAnalysesCache from vyper.venom.basicblock import IRInstruction, IRLiteral +from vyper.venom.passes.cfg_normalization import CFGNormalization +from vyper.venom.passes.lower_dload import LowerDloadPass +from vyper.venom.passes.simplify_cfg import SimplifyCFGPass +from vyper.venom.passes.single_use_expansion import SingleUseExpansion HAS_HEVM: bool = False diff --git a/tests/unit/compiler/venom/test_inliner.py b/tests/unit/compiler/venom/test_inliner.py index 1cfc65e9a0..0cfc5c8117 100644 --- a/tests/unit/compiler/venom/test_inliner.py +++ b/tests/unit/compiler/venom/test_inliner.py @@ -1,5 +1,5 @@ from tests.venom_utils import parse_venom -from vyper.compiler.settings import OptimizationLevel +from vyper.compiler.settings import OptimizationLevel, VenomOptimizationFlags from vyper.venom.analysis.analysis import IRAnalysesCache from vyper.venom.check_venom import check_venom_ctx from vyper.venom.passes import FunctionInlinerPass, SimplifyCFGPass @@ -45,7 +45,8 @@ def test_inliner_phi_invalidation(): for fn in ctx.functions.values(): ir_analyses[fn] = IRAnalysesCache(fn) - FunctionInlinerPass(ir_analyses, ctx, OptimizationLevel.CODESIZE).run_pass() + flags = VenomOptimizationFlags(level=OptimizationLevel.CODESIZE) + FunctionInlinerPass(ir_analyses, ctx, flags).run_pass() for fn in ctx.get_functions(): ac = IRAnalysesCache(fn) @@ -100,7 +101,8 @@ def test_inliner_phi_invalidation_inner(): for fn in ctx.functions.values(): ir_analyses[fn] = IRAnalysesCache(fn) - FunctionInlinerPass(ir_analyses, ctx, OptimizationLevel.CODESIZE).run_pass() + flags = VenomOptimizationFlags(level=OptimizationLevel.CODESIZE) + FunctionInlinerPass(ir_analyses, ctx, flags).run_pass() for fn in ctx.get_functions(): ac = IRAnalysesCache(fn) diff --git a/vyper/ast/nodes.py b/vyper/ast/nodes.py index 4ddb02c058..4009592cbb 100644 --- a/vyper/ast/nodes.py +++ b/vyper/ast/nodes.py @@ -449,7 +449,7 @@ def to_dict(self) -> dict: if isinstance(value, list): ast_dict[key] = [_to_dict(i) for i in value] elif isinstance(value, Settings): - ast_dict[key] = value.as_dict() + ast_dict[key] = value.as_dict(include_venom_flags=False) else: ast_dict[key] = _to_dict(value) diff --git a/vyper/ast/pre_parser.py b/vyper/ast/pre_parser.py index 40c907347d..1b20ab6cd4 100644 --- a/vyper/ast/pre_parser.py +++ b/vyper/ast/pre_parser.py @@ -66,6 +66,7 @@ def _parse_pragma(comment_contents, settings, is_interface, code, start): try: mode = pragma.removeprefix("optimize").strip() settings.optimize = OptimizationLevel.from_string(mode) + settings.venom_flags.set_level(settings.optimize) except ValueError: raise PragmaException(f"Invalid optimization mode `{mode}`", *location) return diff --git a/vyper/cli/venom_main.py b/vyper/cli/venom_main.py index 0ceeae73f6..cdfdba6e0f 100755 --- a/vyper/cli/venom_main.py +++ b/vyper/cli/venom_main.py @@ -5,7 +5,12 @@ import vyper import vyper.evm.opcodes as evm from vyper.compiler.phases import generate_bytecode -from vyper.compiler.settings import OptimizationLevel, Settings, set_global_settings +from vyper.compiler.settings import ( + OptimizationLevel, + Settings, + VenomOptimizationFlags, + set_global_settings, +) from vyper.venom import generate_assembly_experimental, run_passes_on from vyper.venom.check_venom import check_venom_ctx from vyper.venom.parser import parse_venom @@ -59,7 +64,8 @@ def _parse_args(argv: list[str]): check_venom_ctx(ctx) - run_passes_on(ctx, OptimizationLevel.default()) + flags = VenomOptimizationFlags(level=OptimizationLevel.default()) + run_passes_on(ctx, flags) asm = generate_assembly_experimental(ctx) bytecode, _ = generate_bytecode(asm) print(f"0x{bytecode.hex()}") diff --git a/vyper/cli/vyper_compile.py b/vyper/cli/vyper_compile.py index eadabf302f..077ee99fd3 100755 --- a/vyper/cli/vyper_compile.py +++ b/vyper/cli/vyper_compile.py @@ -132,7 +132,7 @@ def _parse_args(argv): choices=list(evm.EVM_VERSIONS), dest="evm_version", ) - parser.add_argument("--no-optimize", help="Do not optimize", action="store_true") + parser.add_argument("--disable-optimize", help="Do not optimize", action="store_true") parser.add_argument( "--base64", help="Base64 encode the output (only valid in conjunction with `-f archive`", @@ -141,12 +141,37 @@ def _parse_args(argv): parser.add_argument( "-O", "--optimize", - help="Optimization flag (defaults to 'gas')", - choices=["gas", "codesize", "none"], + help="Optimization level (defaults to 'gas'). Valid options: " + "1 (basic), 2 (gas/default), 3 (aggressive - experimental), " + "s (size), or legacy names: none (alias for 1), gas, codesize", + metavar="LEVEL", + dest="optimize", ) + parser.add_argument( + "--disable-inlining", help="Disable function inlining optimization", action="store_true" + ) + parser.add_argument( + "--disable-cse", help="Disable common subexpression elimination", action="store_true" + ) + parser.add_argument( + "--disable-sccp", + help="Disable sparse conditional constant propagation", + action="store_true", + ) + parser.add_argument( + "--disable-load-elimination", + help="Disable load elimination optimization", + action="store_true", + ) + parser.add_argument( + "--disable-dead-store-elimination", + help="Disable dead store elimination", + action="store_true", + ) + parser.add_argument("--inline-threshold", help="Function inlining cost threshold", type=int) parser.add_argument("--debug", help="Compile in debug mode", action="store_true") parser.add_argument( - "--no-bytecode-metadata", help="Do not add metadata to bytecode", action="store_true" + "--disable-bytecode-metadata", help="Do not add metadata to bytecode", action="store_true" ) parser.add_argument( "--traceback-limit", @@ -219,16 +244,32 @@ def _parse_args(argv): if args.base64: output_formats = ("archive_b64",) - if args.no_optimize and args.optimize: - raise ValueError("Cannot use `--no-optimize` and `--optimize` at the same time!") - - settings = Settings() + if args.disable_optimize and args.optimize: + raise ValueError("Cannot use `--disable-optimize` and `-O/--optimize` at the same time!") - # TODO: refactor to something like Settings.from_args() - if args.no_optimize: - settings.optimize = OptimizationLevel.NONE + optimize = None + if args.disable_optimize: + optimize = OptimizationLevel.NONE elif args.optimize is not None: - settings.optimize = OptimizationLevel.from_string(args.optimize) + # Handle both old-style (none, gas, codesize) and numeric (1, 2, 3, s) arguments + opt_level = args.optimize.lower() + if opt_level in ["1", "2", "3"]: + opt_level = "O" + opt_level + elif opt_level == "s": + opt_level = "Os" + optimize = OptimizationLevel.from_string(opt_level) + + settings = Settings(optimize=optimize) + + # Apply individual flag overrides + flags = settings.venom_flags + flags.disable_inlining |= args.disable_inlining + flags.disable_cse |= args.disable_cse + flags.disable_sccp |= args.disable_sccp + flags.disable_load_elimination |= args.disable_load_elimination + flags.disable_dead_store_elimination |= args.disable_dead_store_elimination + if args.inline_threshold is not None: + flags.inline_threshold = args.inline_threshold if args.evm_version: settings.evm_version = args.evm_version @@ -255,7 +296,7 @@ def _parse_args(argv): args.show_gas_estimates, settings, args.storage_layout, - args.no_bytecode_metadata, + args.disable_bytecode_metadata, args.warnings_control, ) diff --git a/vyper/cli/vyper_json.py b/vyper/cli/vyper_json.py index 865a9bd173..a784fd06dc 100755 --- a/vyper/cli/vyper_json.py +++ b/vyper/cli/vyper_json.py @@ -9,7 +9,7 @@ import vyper from vyper.compiler.input_bundle import FileInput, JSONInput, JSONInputBundle, _normpath -from vyper.compiler.settings import OptimizationLevel, Settings +from vyper.compiler.settings import OptimizationLevel, Settings, VenomOptimizationFlags from vyper.evm.opcodes import EVM_VERSIONS from vyper.exceptions import JSONError from vyper.utils import OrderedSet, keccak256 @@ -304,6 +304,10 @@ def get_settings(input_dict: dict) -> Settings: evm_version = get_evm_version(input_dict) optimize = input_dict["settings"].get("optimize") + opt_level = input_dict["settings"].get("optLevel") + + if optimize is not None and opt_level is not None: + raise JSONError("both 'optimize' and 'optLevel' cannot be set") experimental_codegen = input_dict["settings"].get("experimentalCodegen") if experimental_codegen is None: @@ -311,7 +315,9 @@ def get_settings(input_dict: dict) -> Settings: elif input_dict["settings"].get("venomExperimental") is not None: raise JSONError("both experimentalCodegen and venomExperimental cannot be set") - if isinstance(optimize, bool): + if opt_level is not None: + optimize = OptimizationLevel.from_string(opt_level) + elif isinstance(optimize, bool): # bool optimization level for backwards compatibility vyper_warn( Deprecation( @@ -329,12 +335,47 @@ def get_settings(input_dict: dict) -> Settings: # TODO: maybe change these to camelCase for consistency enable_decimals = input_dict["settings"].get("enable_decimals", None) + # Create Venom optimization flags with the optimization level + venom_flags = VenomOptimizationFlags(level=optimize) + + # Check for Venom-specific settings + venom_settings = input_dict["settings"].get("venom", {}) + if venom_settings: + if venom_flags is None: + venom_flags = VenomOptimizationFlags() + + flag_mapping = { + "disableInlining": ("disable_inlining", bool), + "disableCSE": ("disable_cse", bool), + "disableSCCP": ("disable_sccp", bool), + "disableLoadElimination": ("disable_load_elimination", bool), + "disableDeadStoreElimination": ("disable_dead_store_elimination", bool), + "disableAlgebraicOptimization": ("disable_algebraic_optimization", bool), + "disableBranchOptimization": ("disable_branch_optimization", bool), + "disableMem2Var": ("disable_mem2var", bool), + "disableSimplifyCFG": ("disable_simplify_cfg", bool), + "disableRemoveUnusedVariables": ("disable_remove_unused_variables", bool), + "inlineThreshold": ("inline_threshold", int), + } + + # Apply settings from venom_settings + for json_field, (attr_name, expected_type) in flag_mapping.items(): + if json_field in venom_settings: + value = venom_settings[json_field] + if not isinstance(value, expected_type): + raise JSONError( + f"venom.{json_field} must be {expected_type.__name__}, " + f"got {type(value).__name__}" + ) + setattr(venom_flags, attr_name, value) + return Settings( evm_version=evm_version, optimize=optimize, experimental_codegen=experimental_codegen, debug=debug, enable_decimals=enable_decimals, + venom_flags=venom_flags, ) diff --git a/vyper/compiler/settings.py b/vyper/compiler/settings.py index bd6d28a198..8cbf34bd45 100644 --- a/vyper/compiler/settings.py +++ b/vyper/compiler/settings.py @@ -28,16 +28,25 @@ class OptimizationLevel(Enum): NONE = 1 GAS = 2 CODESIZE = 3 + # TODO: O1 (minimal passes) is currently disabled because it can cause + # "stack too deep" errors. Re-enable once stack spilling machinery is + # implemented to allow compilation with minimal optimization passes. + O2 = 6 # Standard "stable" optimizations (default) + O3 = 7 # Aggressive optimizations -- experimental possibly unsafe + Os = 8 # Optimize for size @classmethod def from_string(cls, val): match val: case "none": return cls.NONE - case "gas": + # O1 maps to O2 for now until stack spilling is implemented + case "O1" | "O2" | "gas": return cls.GAS - case "codesize": + case "codesize" | "Os": return cls.CODESIZE + case "O3": + return cls.O3 raise ValueError(f"unrecognized optimization level: {val}") @classmethod @@ -45,11 +54,79 @@ def default(cls): return cls.GAS def __str__(self): - return self._name_.lower() + return self._name_ if self._name_.startswith("O") else self._name_.lower() DEFAULT_ENABLE_DECIMALS = False +# Inlining threshold constants +INLINE_THRESHOLD_SIZE = 5 # Conservative for size optimization +INLINE_THRESHOLD_DEFAULT = 15 # Standard inlining +INLINE_THRESHOLD_AGGRESSIVE = 30 # Aggressive inlining for O3 + + +@dataclass +class VenomOptimizationFlags: + level: OptimizationLevel = OptimizationLevel.default() + + # Disable flags - default False means optimization is enabled + # These are used to override the defaults for the optimization level + disable_inlining: bool = False + disable_cse: bool = False + disable_sccp: bool = False + disable_load_elimination: bool = False + disable_dead_store_elimination: bool = False + disable_algebraic_optimization: bool = False + disable_branch_optimization: bool = False + disable_mem2var: bool = False + disable_simplify_cfg: bool = False + disable_remove_unused_variables: bool = False + + # Tuning parameters + inline_threshold: Optional[int] = None + + def __post_init__(self): + # Set default optimization level if not provided + if self.level is None: + self.level = OptimizationLevel.default() + + # Always set inline_threshold based on level + if self.inline_threshold is None: + self.inline_threshold = self._get_inline_threshold_for_level(self.level) + + def _get_inline_threshold_for_level(self, level: OptimizationLevel) -> int: + if level == OptimizationLevel.O3: + return INLINE_THRESHOLD_AGGRESSIVE + elif level in (OptimizationLevel.Os, OptimizationLevel.CODESIZE): + return INLINE_THRESHOLD_SIZE + elif level == OptimizationLevel.NONE: + return INLINE_THRESHOLD_DEFAULT + else: + return INLINE_THRESHOLD_DEFAULT + + def _update_inline_threshold(self): + self.inline_threshold = self._get_inline_threshold_for_level(self.level) + + def set_level(self, level: OptimizationLevel): + """Set optimization level and update dependent parameters.""" + self.level = level + self._update_inline_threshold() + + def as_dict(self): + ret = dataclasses.asdict(self) + # Convert OptimizationLevel to string for JSON serialization + if ret.get("level") is not None: + ret["level"] = str(ret["level"]) + return ret + + @classmethod + def from_dict(cls, data): + data = data.copy() + # Convert string back to OptimizationLevel + if "level" in data and data["level"] is not None: + data["level"] = OptimizationLevel.from_string(data["level"]) + return cls(**data) + @dataclass class Settings: @@ -60,6 +137,7 @@ class Settings: debug: Optional[bool] = None enable_decimals: Optional[bool] = None nonreentrancy_by_default: Optional[bool] = None + venom_flags: Optional[VenomOptimizationFlags] = None def __post_init__(self): # sanity check inputs @@ -74,6 +152,16 @@ def __post_init__(self): if self.nonreentrancy_by_default is not None: assert isinstance(self.nonreentrancy_by_default, bool) + # Initialize venom_flags if not provided + if self.venom_flags is None: + self.venom_flags = VenomOptimizationFlags(level=self.optimize) + else: + assert isinstance(self.venom_flags, VenomOptimizationFlags) + # Ensure consistency + if self.optimize is not None and self.venom_flags.level != self.optimize: + self.venom_flags.level = self.optimize + self.venom_flags._update_inline_threshold() + # CMC 2024-04-10 consider hiding the `enable_decimals` member altogether def get_enable_decimals(self) -> bool: if self.enable_decimals is None: @@ -95,14 +183,25 @@ def as_cli(self): return "".join(ret) - def as_dict(self): - ret = dataclasses.asdict(self) - # compiler_version is not a compiler input, it can only come from - # source code pragma. - ret.pop("compiler_version", None) - ret = {k: v for (k, v) in ret.items() if v is not None} - if "optimize" in ret: - ret["optimize"] = str(ret["optimize"]) + def as_dict(self, include_venom_flags=True): + ret = {} + for field in dataclasses.fields(self): + value = getattr(self, field.name) + if value is None: + continue + if field.name == "compiler_version": + # compiler_version is not a compiler input, it can only come from + # source code pragma. + continue + if field.name == "venom_flags" and not include_venom_flags: + # Skip venom_flags for AST output - it's an internal optimization detail + continue + if field.name == "optimize": + ret["optimize"] = str(value) + elif field.name == "venom_flags": + ret["venom_flags"] = value.as_dict() + else: + ret[field.name] = value return ret @classmethod @@ -110,6 +209,8 @@ def from_dict(cls, data): data = data.copy() if "optimize" in data: data["optimize"] = OptimizationLevel.from_string(data["optimize"]) + if "venom_flags" in data and data["venom_flags"] is not None: + data["venom_flags"] = VenomOptimizationFlags.from_dict(data["venom_flags"]) return cls(**data) @@ -136,15 +237,32 @@ def _merge_one(lhs, rhs, helpstr): ) return lhs if rhs is None else rhs - ret = Settings() - for field in dataclasses.fields(ret): + # Collect all values first before creating Settings to avoid double initialization + values = {} + for field in dataclasses.fields(Settings): if field.name == "compiler_version": continue - pretty_name = field.name.replace("_", "-") # e.g. evm_version -> evm-version - val = _merge_one(getattr(one, field.name), getattr(two, field.name), pretty_name) - setattr(ret, field.name, val) - - return ret + if field.name != "venom_flags": + pretty_name = field.name.replace("_", "-") # e.g. evm_version -> evm-version + val = _merge_one(getattr(one, field.name), getattr(two, field.name), pretty_name) + if val is not None: + values[field.name] = val + + # Now handle venom_flags based on the merged optimize value + # If either source has explicit venom_flags with customizations, use it + # Otherwise let Settings.__post_init__ create the default based on optimize + venom_one = getattr(one, "venom_flags", None) + venom_two = getattr(two, "venom_flags", None) + + # Pick the venom_flags that matches the merged optimize level, if any + merged_optimize = values.get("optimize") + if venom_two and venom_two.level == merged_optimize: + values["venom_flags"] = venom_two + elif venom_one and venom_one.level == merged_optimize: + values["venom_flags"] = venom_one + # Otherwise don't set it - let __post_init__ create the right one + + return Settings(**values) # CMC 2024-04-10 do we need it to be Optional? diff --git a/vyper/venom/__init__.py b/vyper/venom/__init__.py index 62c63653b1..35e71e62f8 100644 --- a/vyper/venom/__init__.py +++ b/vyper/venom/__init__.py @@ -1,140 +1,110 @@ # maybe rename this `main.py` or `venom.py` # (can have an `__init__.py` which exposes the API). -from typing import Optional +from typing import Any, Dict, List from vyper.codegen.ir_node import IRnode -from vyper.compiler.settings import OptimizationLevel, Settings -from vyper.evm.address_space import MEMORY, STORAGE, TRANSIENT -from vyper.exceptions import CompilerPanic +from vyper.compiler.settings import OptimizationLevel, Settings, VenomOptimizationFlags from vyper.ir.compile_ir import AssemblyInstruction -from vyper.venom.analysis import MemSSA from vyper.venom.analysis.analysis import IRAnalysesCache from vyper.venom.basicblock import IRLabel, IRLiteral from vyper.venom.check_venom import check_calling_convention from vyper.venom.context import IRContext from vyper.venom.function import IRFunction from vyper.venom.ir_node_to_venom import ir_node_to_venom -from vyper.venom.passes import ( - CSE, - SCCP, - AlgebraicOptimizationPass, - AssignElimination, - BranchOptimizationPass, - CFGNormalization, - DFTPass, - FloatAllocas, - FunctionInlinerPass, - LoadElimination, - LowerDloadPass, - MakeSSA, - Mem2Var, - MemMergePass, - PhiEliminationPass, - ReduceLiteralsCodesize, - RemoveUnusedVariablesPass, - RevertToAssert, - SimplifyCFGPass, - SingleUseExpansion, -) -from vyper.venom.passes.dead_store_elimination import DeadStoreElimination +from vyper.venom.optimization_levels import PASSES_O2, PASSES_O3, PASSES_Os +from vyper.venom.optimization_levels.types import PassConfig +from vyper.venom.passes import FunctionInlinerPass from vyper.venom.venom_to_assembly import VenomCompiler DEFAULT_OPT_LEVEL = OptimizationLevel.default() +# Pass configuration for each optimization level +# TODO: O1 (minimal passes) is currently disabled because it can cause +# "stack too deep" errors. Re-enable once stack spilling machinery is +# implemented to allow compilation with minimal optimization passes. +OPTIMIZATION_PASSES: Dict[OptimizationLevel, List[PassConfig]] = { + OptimizationLevel.O2: PASSES_O2, + OptimizationLevel.O3: PASSES_O3, + OptimizationLevel.Os: PASSES_Os, +} + +# Legacy aliases for backwards compatibility +OPTIMIZATION_PASSES[OptimizationLevel.NONE] = OPTIMIZATION_PASSES[ + OptimizationLevel.O2 +] # none -> O2 +OPTIMIZATION_PASSES[OptimizationLevel.GAS] = OPTIMIZATION_PASSES[OptimizationLevel.O2] # gas -> O2 +OPTIMIZATION_PASSES[OptimizationLevel.CODESIZE] = OPTIMIZATION_PASSES[ + OptimizationLevel.Os +] # codesize -> Os + def generate_assembly_experimental( venom_ctx: IRContext, optimize: OptimizationLevel = DEFAULT_OPT_LEVEL ) -> list[AssemblyInstruction]: compiler = VenomCompiler(venom_ctx) - return compiler.generate_evm_assembly(optimize == OptimizationLevel.NONE) - - -def _run_passes(fn: IRFunction, optimize: OptimizationLevel, ac: IRAnalysesCache) -> None: - # Run passes on Venom IR - # TODO: Add support for optimization levels - - FloatAllocas(ac, fn).run_pass() - - SimplifyCFGPass(ac, fn).run_pass() - - MakeSSA(ac, fn).run_pass() - PhiEliminationPass(ac, fn).run_pass() - - # run constant folding before mem2var to reduce some pointer arithmetic - AlgebraicOptimizationPass(ac, fn).run_pass() - SCCP(ac, fn, remove_allocas=False).run_pass() - SimplifyCFGPass(ac, fn).run_pass() - - AssignElimination(ac, fn).run_pass() - Mem2Var(ac, fn).run_pass() - MakeSSA(ac, fn).run_pass() - PhiEliminationPass(ac, fn).run_pass() - SCCP(ac, fn).run_pass() - - SimplifyCFGPass(ac, fn).run_pass() - AssignElimination(ac, fn).run_pass() - AlgebraicOptimizationPass(ac, fn).run_pass() - - LoadElimination(ac, fn).run_pass() - PhiEliminationPass(ac, fn).run_pass() - AssignElimination(ac, fn).run_pass() - - SCCP(ac, fn).run_pass() - AssignElimination(ac, fn).run_pass() - RevertToAssert(ac, fn).run_pass() - - SimplifyCFGPass(ac, fn).run_pass() - MemMergePass(ac, fn).run_pass() - RemoveUnusedVariablesPass(ac, fn).run_pass() - - DeadStoreElimination(ac, fn).run_pass(addr_space=MEMORY) - DeadStoreElimination(ac, fn).run_pass(addr_space=STORAGE) - DeadStoreElimination(ac, fn).run_pass(addr_space=TRANSIENT) - LowerDloadPass(ac, fn).run_pass() + return compiler.generate_evm_assembly(False) - BranchOptimizationPass(ac, fn).run_pass() - AlgebraicOptimizationPass(ac, fn).run_pass() +# Mapping of pass names to their disable flag names +# Passes not in this map are considered essential and always run +PASS_FLAG_MAP = { + "AlgebraicOptimizationPass": "disable_algebraic_optimization", + "SCCP": "disable_sccp", + "Mem2Var": "disable_mem2var", + "LoadElimination": "disable_load_elimination", + "RemoveUnusedVariablesPass": "disable_remove_unused_variables", + "DeadStoreElimination": "disable_dead_store_elimination", + "BranchOptimizationPass": "disable_branch_optimization", + "CSE": "disable_cse", + "SimplifyCFGPass": "disable_simplify_cfg", +} - # This improves the performance of cse - RemoveUnusedVariablesPass(ac, fn).run_pass() - PhiEliminationPass(ac, fn).run_pass() - AssignElimination(ac, fn).run_pass() - CSE(ac, fn).run_pass() +def _run_passes(fn: IRFunction, flags: VenomOptimizationFlags, ac: IRAnalysesCache) -> None: + passes = OPTIMIZATION_PASSES[flags.level] - AssignElimination(ac, fn).run_pass() - RemoveUnusedVariablesPass(ac, fn).run_pass() - SingleUseExpansion(ac, fn).run_pass() + for pass_config in passes: + if isinstance(pass_config, tuple): + pass_cls, kwargs = pass_config + else: + pass_cls = pass_config + kwargs = {} - if optimize == OptimizationLevel.CODESIZE: - ReduceLiteralsCodesize(ac, fn).run_pass() + # Check if pass should be skipped based on user flags + pass_name = pass_cls.__name__ + flag_name = PASS_FLAG_MAP.get(pass_name) - DFTPass(ac, fn).run_pass() + if flag_name and getattr(flags, flag_name): + continue - CFGNormalization(ac, fn).run_pass() + # Run the pass + pass_instance = pass_cls(ac, fn) + pass_instance.run_pass(**kwargs) -def _run_global_passes(ctx: IRContext, optimize: OptimizationLevel, ir_analyses: dict) -> None: - FunctionInlinerPass(ir_analyses, ctx, optimize).run_pass() +def _run_global_passes( + ctx: IRContext, flags: VenomOptimizationFlags, ir_analyses: dict[IRFunction, IRAnalysesCache] +) -> None: + if not flags.disable_inlining: + FunctionInlinerPass(ir_analyses, ctx, flags).run_pass() -def run_passes_on(ctx: IRContext, optimize: OptimizationLevel) -> None: +def run_passes_on(ctx: IRContext, flags: VenomOptimizationFlags) -> None: ir_analyses = {} # Validate calling convention invariants before running passes check_calling_convention(ctx) for fn in ctx.functions.values(): ir_analyses[fn] = IRAnalysesCache(fn) - _run_global_passes(ctx, optimize, ir_analyses) + _run_global_passes(ctx, flags, ir_analyses) ir_analyses = {} for fn in ctx.functions.values(): ir_analyses[fn] = IRAnalysesCache(fn) for fn in ctx.functions.values(): - _run_passes(fn, optimize, ir_analyses[fn]) + _run_passes(fn, flags, ir_analyses[fn]) def generate_venom( @@ -156,8 +126,7 @@ def generate_venom( for constname, value in constants.items(): ctx.add_constant(constname, value) - optimize = settings.optimize - assert optimize is not None # help mypy - run_passes_on(ctx, optimize) + assert settings.venom_flags is not None + run_passes_on(ctx, settings.venom_flags) return ctx diff --git a/vyper/venom/optimization_levels/O2.py b/vyper/venom/optimization_levels/O2.py new file mode 100644 index 0000000000..e705c8c878 --- /dev/null +++ b/vyper/venom/optimization_levels/O2.py @@ -0,0 +1,66 @@ +from typing import List + +from vyper.evm.address_space import MEMORY, STORAGE, TRANSIENT +from vyper.venom.optimization_levels.types import PassConfig +from vyper.venom.passes.algebraic_optimization import AlgebraicOptimizationPass +from vyper.venom.passes.assign_elimination import AssignElimination +from vyper.venom.passes.branch_optimization import BranchOptimizationPass +from vyper.venom.passes.cfg_normalization import CFGNormalization +from vyper.venom.passes.common_subexpression_elimination import CSE +from vyper.venom.passes.dead_store_elimination import DeadStoreElimination +from vyper.venom.passes.dft import DFTPass +from vyper.venom.passes.float_allocas import FloatAllocas +from vyper.venom.passes.load_elimination import LoadElimination +from vyper.venom.passes.lower_dload import LowerDloadPass +from vyper.venom.passes.make_ssa import MakeSSA +from vyper.venom.passes.mem2var import Mem2Var +from vyper.venom.passes.memmerging import MemMergePass +from vyper.venom.passes.phi_elimination import PhiEliminationPass +from vyper.venom.passes.remove_unused_variables import RemoveUnusedVariablesPass +from vyper.venom.passes.revert_to_assert import RevertToAssert +from vyper.venom.passes.sccp.sccp import SCCP +from vyper.venom.passes.simplify_cfg import SimplifyCFGPass +from vyper.venom.passes.single_use_expansion import SingleUseExpansion + +# Standard optimizations (default) +PASSES_O2: List[PassConfig] = [ + FloatAllocas, + SimplifyCFGPass, + MakeSSA, + PhiEliminationPass, + AlgebraicOptimizationPass, + (SCCP, {"remove_allocas": False}), + SimplifyCFGPass, + AssignElimination, + Mem2Var, + MakeSSA, + PhiEliminationPass, + SCCP, + SimplifyCFGPass, + AssignElimination, + AlgebraicOptimizationPass, + LoadElimination, + PhiEliminationPass, + AssignElimination, + SCCP, + AssignElimination, + RevertToAssert, + SimplifyCFGPass, + MemMergePass, + RemoveUnusedVariablesPass, + (DeadStoreElimination, {"addr_space": MEMORY}), + (DeadStoreElimination, {"addr_space": STORAGE}), + (DeadStoreElimination, {"addr_space": TRANSIENT}), + LowerDloadPass, + BranchOptimizationPass, + AlgebraicOptimizationPass, + RemoveUnusedVariablesPass, + PhiEliminationPass, + AssignElimination, + CSE, + AssignElimination, + RemoveUnusedVariablesPass, + SingleUseExpansion, + DFTPass, + CFGNormalization, +] diff --git a/vyper/venom/optimization_levels/O3.py b/vyper/venom/optimization_levels/O3.py new file mode 100644 index 0000000000..25d724b02a --- /dev/null +++ b/vyper/venom/optimization_levels/O3.py @@ -0,0 +1,66 @@ +from typing import List + +from vyper.evm.address_space import MEMORY, STORAGE, TRANSIENT +from vyper.venom.optimization_levels.types import PassConfig +from vyper.venom.passes.algebraic_optimization import AlgebraicOptimizationPass +from vyper.venom.passes.assign_elimination import AssignElimination +from vyper.venom.passes.branch_optimization import BranchOptimizationPass +from vyper.venom.passes.cfg_normalization import CFGNormalization +from vyper.venom.passes.common_subexpression_elimination import CSE +from vyper.venom.passes.dead_store_elimination import DeadStoreElimination +from vyper.venom.passes.dft import DFTPass +from vyper.venom.passes.float_allocas import FloatAllocas +from vyper.venom.passes.load_elimination import LoadElimination +from vyper.venom.passes.lower_dload import LowerDloadPass +from vyper.venom.passes.make_ssa import MakeSSA +from vyper.venom.passes.mem2var import Mem2Var +from vyper.venom.passes.memmerging import MemMergePass +from vyper.venom.passes.phi_elimination import PhiEliminationPass +from vyper.venom.passes.remove_unused_variables import RemoveUnusedVariablesPass +from vyper.venom.passes.revert_to_assert import RevertToAssert +from vyper.venom.passes.sccp.sccp import SCCP +from vyper.venom.passes.simplify_cfg import SimplifyCFGPass +from vyper.venom.passes.single_use_expansion import SingleUseExpansion + +# Aggressive optimizations +PASSES_O3: List[PassConfig] = [ + FloatAllocas, + SimplifyCFGPass, + MakeSSA, + PhiEliminationPass, + AlgebraicOptimizationPass, + (SCCP, {"remove_allocas": False}), + SimplifyCFGPass, + AssignElimination, + Mem2Var, + MakeSSA, + PhiEliminationPass, + SCCP, + SimplifyCFGPass, + AssignElimination, + AlgebraicOptimizationPass, + LoadElimination, + PhiEliminationPass, + AssignElimination, + SCCP, + AssignElimination, + RevertToAssert, + SimplifyCFGPass, + MemMergePass, + RemoveUnusedVariablesPass, + (DeadStoreElimination, {"addr_space": MEMORY}), + (DeadStoreElimination, {"addr_space": STORAGE}), + (DeadStoreElimination, {"addr_space": TRANSIENT}), + LowerDloadPass, + BranchOptimizationPass, + AlgebraicOptimizationPass, + RemoveUnusedVariablesPass, + PhiEliminationPass, + AssignElimination, + CSE, + AssignElimination, + RemoveUnusedVariablesPass, + SingleUseExpansion, + DFTPass, + CFGNormalization, +] diff --git a/vyper/venom/optimization_levels/Os.py b/vyper/venom/optimization_levels/Os.py new file mode 100644 index 0000000000..8a52ac14e8 --- /dev/null +++ b/vyper/venom/optimization_levels/Os.py @@ -0,0 +1,68 @@ +from typing import List + +from vyper.evm.address_space import MEMORY, STORAGE, TRANSIENT +from vyper.venom.optimization_levels.types import PassConfig +from vyper.venom.passes.algebraic_optimization import AlgebraicOptimizationPass +from vyper.venom.passes.assign_elimination import AssignElimination +from vyper.venom.passes.branch_optimization import BranchOptimizationPass +from vyper.venom.passes.cfg_normalization import CFGNormalization +from vyper.venom.passes.common_subexpression_elimination import CSE +from vyper.venom.passes.dead_store_elimination import DeadStoreElimination +from vyper.venom.passes.dft import DFTPass +from vyper.venom.passes.float_allocas import FloatAllocas +from vyper.venom.passes.literals_codesize import ReduceLiteralsCodesize +from vyper.venom.passes.load_elimination import LoadElimination +from vyper.venom.passes.lower_dload import LowerDloadPass +from vyper.venom.passes.make_ssa import MakeSSA +from vyper.venom.passes.mem2var import Mem2Var +from vyper.venom.passes.memmerging import MemMergePass +from vyper.venom.passes.phi_elimination import PhiEliminationPass +from vyper.venom.passes.remove_unused_variables import RemoveUnusedVariablesPass +from vyper.venom.passes.revert_to_assert import RevertToAssert +from vyper.venom.passes.sccp.sccp import SCCP +from vyper.venom.passes.simplify_cfg import SimplifyCFGPass +from vyper.venom.passes.single_use_expansion import SingleUseExpansion + +# Optimize for size +PASSES_Os: List[PassConfig] = [ + FloatAllocas, + SimplifyCFGPass, + MakeSSA, + PhiEliminationPass, + AlgebraicOptimizationPass, + (SCCP, {"remove_allocas": False}), + SimplifyCFGPass, + AssignElimination, + Mem2Var, + MakeSSA, + PhiEliminationPass, + SCCP, + SimplifyCFGPass, + AssignElimination, + AlgebraicOptimizationPass, + LoadElimination, + PhiEliminationPass, + AssignElimination, + SCCP, + AssignElimination, + RevertToAssert, + SimplifyCFGPass, + MemMergePass, + RemoveUnusedVariablesPass, + (DeadStoreElimination, {"addr_space": MEMORY}), + (DeadStoreElimination, {"addr_space": STORAGE}), + (DeadStoreElimination, {"addr_space": TRANSIENT}), + LowerDloadPass, + BranchOptimizationPass, + AlgebraicOptimizationPass, + RemoveUnusedVariablesPass, + PhiEliminationPass, + AssignElimination, + CSE, + AssignElimination, + RemoveUnusedVariablesPass, + SingleUseExpansion, + ReduceLiteralsCodesize, + DFTPass, + CFGNormalization, +] diff --git a/vyper/venom/optimization_levels/__init__.py b/vyper/venom/optimization_levels/__init__.py new file mode 100644 index 0000000000..783a905067 --- /dev/null +++ b/vyper/venom/optimization_levels/__init__.py @@ -0,0 +1,9 @@ +# TODO: O1 (minimal passes) is currently disabled because it can cause +# "stack too deep" errors. Re-enable once stack spilling machinery is +# implemented to allow compilation with minimal optimization passes. +# from vyper.venom.optimization_levels.O1 import PASSES_O1 +from vyper.venom.optimization_levels.O2 import PASSES_O2 +from vyper.venom.optimization_levels.O3 import PASSES_O3 +from vyper.venom.optimization_levels.Os import PASSES_Os + +__all__ = ["PASSES_O2", "PASSES_O3", "PASSES_Os"] diff --git a/vyper/venom/optimization_levels/types.py b/vyper/venom/optimization_levels/types.py new file mode 100644 index 0000000000..9667d0aee6 --- /dev/null +++ b/vyper/venom/optimization_levels/types.py @@ -0,0 +1,7 @@ +from typing import Any, Dict, Tuple, Union + +from vyper.venom.passes.base_pass import IRPass + +PassConfig = Union[type[IRPass], Tuple[type[IRPass], Dict[str, Any]]] + +__all__ = ["PassConfig"] diff --git a/vyper/venom/passes/function_inliner.py b/vyper/venom/passes/function_inliner.py index 963a5eb72a..e6d0eb4449 100644 --- a/vyper/venom/passes/function_inliner.py +++ b/vyper/venom/passes/function_inliner.py @@ -1,6 +1,6 @@ from typing import List, Optional -from vyper.compiler.settings import OptimizationLevel +from vyper.compiler.settings import VenomOptimizationFlags from vyper.exceptions import CompilerPanic from vyper.utils import OrderedSet from vyper.venom.analysis import CFGAnalysis, DFGAnalysis, FCGAnalysis, IRAnalysesCache @@ -25,16 +25,16 @@ class FunctionInlinerPass(IRGlobalPass): inline_count: int fcg: FCGAnalysis - optimize: OptimizationLevel + flags: VenomOptimizationFlags def __init__( self, analyses_caches: dict[IRFunction, IRAnalysesCache], ctx: IRContext, - optimize: OptimizationLevel, + flags: VenomOptimizationFlags, ): super().__init__(analyses_caches, ctx) - self.optimize = optimize + self.flags = flags or VenomOptimizationFlags() def run_pass(self): entry = self.ctx.entry_function @@ -70,16 +70,9 @@ def _select_inline_candidate(self) -> Optional[IRFunction]: if call_count == 1: return func - # Decide whether to inline based on the optimization level. - if self.optimize == OptimizationLevel.CODESIZE: - continue - elif self.optimize == OptimizationLevel.GAS: - if func.code_size_cost <= 15: - return func - elif self.optimize == OptimizationLevel.NONE: - continue - else: # pragma: nocover - raise CompilerPanic(f"Unsupported inlining optimization level: {self.optimize}") + # Use the inline threshold from flags + if func.code_size_cost <= self.flags.inline_threshold: + return func return None diff --git a/vyper/venom/passes/sccp/sccp.py b/vyper/venom/passes/sccp/sccp.py index 5c13b1e563..2ce2841b0d 100644 --- a/vyper/venom/passes/sccp/sccp.py +++ b/vyper/venom/passes/sccp/sccp.py @@ -58,16 +58,13 @@ class SCCP(IRPass): cfg_dirty: bool - def __init__( - self, analyses_cache: IRAnalysesCache, function: IRFunction, /, remove_allocas=True - ): + def __init__(self, analyses_cache: IRAnalysesCache, function: IRFunction): super().__init__(analyses_cache, function) - self.remove_allocas = remove_allocas - self.lattice = {} self.work_list: list[WorkListItem] = [] - def run_pass(self): + def run_pass(self, remove_allocas=True): + self.remove_allocas = remove_allocas self.fn = self.function self.dfg = self.analyses_cache.request_analysis(DFGAnalysis) self.cfg = self.analyses_cache.request_analysis(CFGAnalysis)