diff --git a/scripts/bytecodecompare/prepare_report.js b/scripts/bytecodecompare/prepare_report.js index 36acfbb35853..c5e8bdf9e449 100755 --- a/scripts/bytecodecompare/prepare_report.js +++ b/scripts/bytecodecompare/prepare_report.js @@ -4,6 +4,10 @@ const fs = require('fs') const compiler = require('solc') +SETTINGS_PRESETS = { + 'legacy-optimize': {optimize: true}, + 'legacy-no-optimize': {optimize: false}, +} function loadSource(sourceFileName, stripSMTPragmas) { @@ -23,96 +27,114 @@ function cleanString(string) return (string !== '' ? string : undefined) } - +let inputFiles = [] let stripSMTPragmas = false -let firstFileArgumentIndex = 2 +let presets = undefined -if (process.argv.length >= 3 && process.argv[2] === '--strip-smt-pragmas') +for (let i = 2; i < process.argv.length; ++i) { - stripSMTPragmas = true - firstFileArgumentIndex = 3 -} - -for (const optimize of [false, true]) -{ - for (const filename of process.argv.slice(firstFileArgumentIndex)) + if (process.argv[i] === '--strip-smt-pragmas') + stripSMTPragmas = true + else if (process.argv[i] === '--preset') { - if (filename !== undefined) - { - let input = { - language: 'Solidity', - sources: { - [filename]: {content: loadSource(filename, stripSMTPragmas)} - }, - settings: { - optimizer: {enabled: optimize}, - outputSelection: {'*': {'*': ['evm.bytecode.object', 'metadata']}} - } - } - if (!stripSMTPragmas) - input['settings']['modelChecker'] = {engine: 'none'} + if (i + 1 === process.argv.length) + throw Error("Option --preset was used, but no preset name given.") - let serializedOutput - let result - const serializedInput = JSON.stringify(input) + if (presets === undefined) + presets = [] + presets.push(process.argv[i + 1]) + ++i; + } + else + inputFiles.push(process.argv[i]) +} - let internalCompilerError = false - try - { - serializedOutput = compiler.compile(serializedInput) - } - catch (exception) - { - internalCompilerError = true - } +if (presets === undefined) + presets = ['legacy-no-optimize', 'legacy-optimize'] - if (!internalCompilerError) - { - result = JSON.parse(serializedOutput) - - if ('errors' in result) - for (const error of result['errors']) - // JSON interface still returns contract metadata in case of an internal compiler error while - // CLI interface does not. To make reports comparable we must force this case to be detected as - // an error in both cases. - if (['UnimplementedFeatureError', 'CompilerError', 'CodeGenerationError'].includes(error['type'])) - { - internalCompilerError = true - break - } - } +for (const preset of presets) + if (!(preset in SETTINGS_PRESETS)) + throw Error(`Invalid preset name: ${preset}.`) - if ( - internalCompilerError || - !('contracts' in result) || - Object.keys(result['contracts']).length === 0 || - Object.keys(result['contracts']).every(file => Object.keys(result['contracts'][file]).length === 0) - ) - // NOTE: do not exit here because this may be run on source which cannot be compiled - console.log(filename + ': ') - else - for (const contractFile in result['contracts']) - for (const contractName in result['contracts'][contractFile]) - { - const contractResults = result['contracts'][contractFile][contractName] +for (const preset of presets) +{ + settings = SETTINGS_PRESETS[preset] - let bytecode = '' - let metadata = '' + for (const filename of inputFiles) + { + let input = { + language: 'Solidity', + sources: { + [filename]: {content: loadSource(filename, stripSMTPragmas)} + }, + settings: { + optimizer: {enabled: settings.optimize}, + outputSelection: {'*': {'*': ['evm.bytecode.object', 'metadata']}} + } + } + if (!stripSMTPragmas) + input['settings']['modelChecker'] = {engine: 'none'} - if ( - 'evm' in contractResults && - 'bytecode' in contractResults['evm'] && - 'object' in contractResults['evm']['bytecode'] && - cleanString(contractResults.evm.bytecode.object) !== undefined - ) - bytecode = cleanString(contractResults.evm.bytecode.object) + let serializedOutput + let result + const serializedInput = JSON.stringify(input) - if ('metadata' in contractResults && cleanString(contractResults.metadata) !== undefined) - metadata = contractResults.metadata + let internalCompilerError = false + try + { + serializedOutput = compiler.compile(serializedInput) + } + catch (exception) + { + internalCompilerError = true + } - console.log(filename + ':' + contractName + ' ' + bytecode) - console.log(filename + ':' + contractName + ' ' + metadata) + if (!internalCompilerError) + { + result = JSON.parse(serializedOutput) + + if ('errors' in result) + for (const error of result['errors']) + // JSON interface still returns contract metadata in case of an internal compiler error while + // CLI interface does not. To make reports comparable we must force this case to be detected as + // an error in both cases. + if (['UnimplementedFeatureError', 'CompilerError', 'CodeGenerationError'].includes(error['type'])) + { + internalCompilerError = true + break } } + + if ( + internalCompilerError || + !('contracts' in result) || + Object.keys(result['contracts']).length === 0 || + Object.keys(result['contracts']).every(file => Object.keys(result['contracts'][file]).length === 0) + ) + // NOTE: do not exit here because this may be run on source which cannot be compiled + console.log(filename + ': ') + else + for (const contractFile in result['contracts']) + for (const contractName in result['contracts'][contractFile]) + { + const contractResults = result['contracts'][contractFile][contractName] + + let bytecode = '' + let metadata = '' + + if ( + 'evm' in contractResults && + 'bytecode' in contractResults['evm'] && + 'object' in contractResults['evm']['bytecode'] && + cleanString(contractResults.evm.bytecode.object) !== undefined + ) + bytecode = cleanString(contractResults.evm.bytecode.object) + + if ('metadata' in contractResults && cleanString(contractResults.metadata) !== undefined) + metadata = contractResults.metadata + + console.log(filename + ':' + contractName + ' ' + bytecode) + console.log(filename + ':' + contractName + ' ' + metadata) + } } } diff --git a/scripts/bytecodecompare/prepare_report.py b/scripts/bytecodecompare/prepare_report.py index 90901e8b79ed..6ec80e753ab5 100755 --- a/scripts/bytecodecompare/prepare_report.py +++ b/scripts/bytecodecompare/prepare_report.py @@ -26,12 +26,29 @@ class CompilerInterface(Enum): STANDARD_JSON = 'standard-json' +class SettingsPreset(Enum): + LEGACY_OPTIMIZE = 'legacy-optimize' + LEGACY_NO_OPTIMIZE = 'legacy-no-optimize' + + class SMTUse(Enum): PRESERVE = 'preserve' DISABLE = 'disable' STRIP_PRAGMAS = 'strip-pragmas' +@dataclass(frozen=True) +class CompilerSettings: + optimize: bool + + @staticmethod + def from_preset(preset: SettingsPreset): + return { + SettingsPreset.LEGACY_OPTIMIZE: CompilerSettings(optimize=True), + SettingsPreset.LEGACY_NO_OPTIMIZE: CompilerSettings(optimize=False), + }[preset] + + @dataclass(frozen=True) class ContractReport: contract_name: str @@ -190,13 +207,15 @@ def parse_cli_output(source_file_name: Path, cli_output: str) -> FileReport: def prepare_compiler_input( compiler_path: Path, source_file_name: Path, - optimize: bool, force_no_optimize_yul: bool, interface: CompilerInterface, + preset: SettingsPreset, smt_use: SMTUse, metadata_option_supported: bool, ) -> Tuple[List[str], str]: + settings = CompilerSettings.from_preset(preset) + if interface == CompilerInterface.STANDARD_JSON: json_input: dict = { 'language': 'Solidity', @@ -204,7 +223,7 @@ def prepare_compiler_input( str(source_file_name): {'content': load_source(source_file_name, smt_use)} }, 'settings': { - 'optimizer': {'enabled': optimize}, + 'optimizer': {'enabled': settings.optimize}, 'outputSelection': {'*': {'*': ['evm.bytecode.object', 'metadata']}}, } } @@ -220,7 +239,7 @@ def prepare_compiler_input( compiler_options = [str(source_file_name), '--bin'] if metadata_option_supported: compiler_options.append('--metadata') - if optimize: + if settings.optimize: compiler_options.append('--optimize') elif force_no_optimize_yul: compiler_options.append('--no-optimize-yul') @@ -259,9 +278,9 @@ def detect_metadata_cli_option_support(compiler_path: Path): def run_compiler( compiler_path: Path, source_file_name: Path, - optimize: bool, force_no_optimize_yul: bool, interface: CompilerInterface, + preset: SettingsPreset, smt_use: SMTUse, metadata_option_supported: bool, tmp_dir: Path, @@ -272,9 +291,9 @@ def run_compiler( (command_line, compiler_input) = prepare_compiler_input( compiler_path, Path(source_file_name.name), - optimize, force_no_optimize_yul, interface, + preset, smt_use, metadata_option_supported, ) @@ -295,9 +314,9 @@ def run_compiler( (command_line, compiler_input) = prepare_compiler_input( compiler_path.absolute(), Path(source_file_name.name), - optimize, force_no_optimize_yul, interface, + preset, smt_use, metadata_option_supported, ) @@ -324,6 +343,7 @@ def generate_report( source_file_names: List[str], compiler_path: Path, interface: CompilerInterface, + presets: List[SettingsPreset], smt_use: SMTUse, force_no_optimize_yul: bool, report_file_path: Path, @@ -335,16 +355,16 @@ def generate_report( try: with open(report_file_path, mode='w', encoding='utf8', newline='\n') as report_file: - for optimize in [False, True]: + for preset in presets: with TemporaryDirectory(prefix='prepare_report-') as tmp_dir: for source_file_name in sorted(source_file_names): try: report = run_compiler( compiler_path, Path(source_file_name), - optimize, force_no_optimize_yul, interface, + preset, smt_use, metadata_option_supported, Path(tmp_dir), @@ -358,7 +378,7 @@ def generate_report( except subprocess.CalledProcessError as exception: print( f"\n\nInterrupted by an exception while processing file " - f"'{source_file_name}' with optimize={optimize}\n\n" + f"'{source_file_name}' with preset={preset}\n\n" f"COMPILER STDOUT:\n{exception.stdout}\n" f"COMPILER STDERR:\n{exception.stderr}\n", file=sys.stderr @@ -367,7 +387,7 @@ def generate_report( except: print( f"\n\nInterrupted by an exception while processing file " - f"'{source_file_name}' with optimize={optimize}\n", + f"'{source_file_name}' with preset={preset}\n\n", file=sys.stderr ) raise @@ -390,6 +410,15 @@ def commandline_parser() -> ArgumentParser: choices=[c.value for c in CompilerInterface], help="Compiler interface to use.", ) + parser.add_argument( + '--preset', + dest='presets', + default=None, + nargs='+', + action='append', + choices=[p.value for p in SettingsPreset], + help="Predefined set of settings to pass to the compiler. More than one can be selected.", + ) parser.add_argument( '--smt-use', dest='smt_use', @@ -418,10 +447,19 @@ def commandline_parser() -> ArgumentParser: if __name__ == "__main__": options = commandline_parser().parse_args() + + if options.presets is None: + # NOTE: Can't put it in add_argument()'s default because then it would be always present. + # See https://github.com/python/cpython/issues/60603 + presets = [[SettingsPreset.LEGACY_NO_OPTIMIZE.value, SettingsPreset.LEGACY_OPTIMIZE.value]] + else: + presets = options.presets + generate_report( glob("*.sol"), Path(options.compiler_path), CompilerInterface(options.interface), + [SettingsPreset(p) for preset_group in presets for p in preset_group], SMTUse(options.smt_use), options.force_no_optimize_yul, Path(options.report_file), diff --git a/test/scripts/test_bytecodecompare_prepare_report.py b/test/scripts/test_bytecodecompare_prepare_report.py index e0a8ca76a6d9..16d73ee39acd 100644 --- a/test/scripts/test_bytecodecompare_prepare_report.py +++ b/test/scripts/test_bytecodecompare_prepare_report.py @@ -9,7 +9,7 @@ # NOTE: This test file file only works with scripts/ added to PYTHONPATH so pylint can't find the imports # pragma pylint: disable=import-error -from bytecodecompare.prepare_report import CompilerInterface, FileReport, ContractReport, SMTUse, Statistics +from bytecodecompare.prepare_report import CompilerInterface, FileReport, ContractReport, SettingsPreset, SMTUse, Statistics from bytecodecompare.prepare_report import load_source, parse_cli_output, parse_standard_json_output, prepare_compiler_input # pragma pylint: enable=import-error @@ -224,7 +224,7 @@ def test_prepare_compiler_input_should_work_with_standard_json_interface(self): (command_line, compiler_input) = prepare_compiler_input( Path('solc'), SMT_SMOKE_TEST_SOL_PATH, - optimize=True, + preset=SettingsPreset.LEGACY_OPTIMIZE, force_no_optimize_yul=False, interface=CompilerInterface.STANDARD_JSON, smt_use=SMTUse.DISABLE, @@ -238,7 +238,7 @@ def test_prepare_compiler_input_should_work_with_cli_interface(self): (command_line, compiler_input) = prepare_compiler_input( Path('solc'), SMT_SMOKE_TEST_SOL_PATH, - optimize=True, + preset=SettingsPreset.LEGACY_OPTIMIZE, force_no_optimize_yul=False, interface=CompilerInterface.CLI, smt_use=SMTUse.DISABLE, @@ -273,7 +273,7 @@ def test_prepare_compiler_input_for_json_preserves_newlines(self): (command_line, compiler_input) = prepare_compiler_input( Path('solc'), SMT_CONTRACT_WITH_MIXED_NEWLINES_SOL_PATH, - optimize=True, + preset=SettingsPreset.LEGACY_OPTIMIZE, force_no_optimize_yul=False, interface=CompilerInterface.STANDARD_JSON, smt_use=SMTUse.DISABLE, @@ -287,7 +287,7 @@ def test_prepare_compiler_input_for_cli_preserves_newlines(self): (_command_line, compiler_input) = prepare_compiler_input( Path('solc'), SMT_CONTRACT_WITH_MIXED_NEWLINES_SOL_PATH, - optimize=True, + preset=SettingsPreset.LEGACY_OPTIMIZE, force_no_optimize_yul=True, interface=CompilerInterface.CLI, smt_use=SMTUse.DISABLE, @@ -300,7 +300,7 @@ def test_prepare_compiler_input_for_cli_should_handle_force_no_optimize_yul_flag (command_line, compiler_input) = prepare_compiler_input( Path('solc'), SMT_SMOKE_TEST_SOL_PATH, - optimize=False, + preset=SettingsPreset.LEGACY_NO_OPTIMIZE, force_no_optimize_yul=True, interface=CompilerInterface.CLI, smt_use=SMTUse.DISABLE, @@ -317,7 +317,7 @@ def test_prepare_compiler_input_for_cli_should_not_use_metadata_option_if_not_su (command_line, compiler_input) = prepare_compiler_input( Path('solc'), SMT_SMOKE_TEST_SOL_PATH, - optimize=True, + preset=SettingsPreset.LEGACY_OPTIMIZE, force_no_optimize_yul=False, interface=CompilerInterface.CLI, smt_use=SMTUse.PRESERVE,