Merge pull request #14350 from ethereum/bytecode-report-presets

Bytecode report presets
This commit is contained in:
Kamil Śliwak 2023-06-23 19:49:07 +02:00 committed by GitHub
commit aca4c86a23
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 153 additions and 95 deletions

View File

@ -4,6 +4,10 @@ const fs = require('fs')
const compiler = require('solc') const compiler = require('solc')
SETTINGS_PRESETS = {
'legacy-optimize': {optimize: true},
'legacy-no-optimize': {optimize: false},
}
function loadSource(sourceFileName, stripSMTPragmas) function loadSource(sourceFileName, stripSMTPragmas)
{ {
@ -23,96 +27,112 @@ function cleanString(string)
return (string !== '' ? string : undefined) return (string !== '' ? string : undefined)
} }
let inputFiles = []
let stripSMTPragmas = false let stripSMTPragmas = false
let firstFileArgumentIndex = 2 let presets = []
if (process.argv.length >= 3 && process.argv[2] === '--strip-smt-pragmas') for (let i = 2; i < process.argv.length; ++i)
{ {
stripSMTPragmas = true if (process.argv[i] === '--strip-smt-pragmas')
firstFileArgumentIndex = 3 stripSMTPragmas = true
else if (process.argv[i] === '--preset')
{
if (i + 1 === process.argv.length)
throw Error("Option --preset was used, but no preset name given.")
presets.push(process.argv[i + 1])
++i;
}
else
inputFiles.push(process.argv[i])
} }
for (const optimize of [false, true]) if (presets.length === 0)
presets = ['legacy-no-optimize', 'legacy-optimize']
for (const preset of presets)
if (!(preset in SETTINGS_PRESETS))
throw Error(`Invalid preset name: ${preset}.`)
for (const preset of presets)
{ {
for (const filename of process.argv.slice(firstFileArgumentIndex)) settings = SETTINGS_PRESETS[preset]
for (const filename of inputFiles)
{ {
if (filename !== undefined) 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'}
let serializedOutput
let result
const serializedInput = JSON.stringify(input)
let internalCompilerError = false
try
{ {
let input = { serializedOutput = compiler.compile(serializedInput)
language: 'Solidity', }
sources: { catch (exception)
[filename]: {content: loadSource(filename, stripSMTPragmas)} {
}, internalCompilerError = true
settings: { }
optimizer: {enabled: optimize},
outputSelection: {'*': {'*': ['evm.bytecode.object', 'metadata']}}
}
}
if (!stripSMTPragmas)
input['settings']['modelChecker'] = {engine: 'none'}
let serializedOutput if (!internalCompilerError)
let result {
const serializedInput = JSON.stringify(input) result = JSON.parse(serializedOutput)
let internalCompilerError = false if ('errors' in result)
try for (const error of result['errors'])
{ // JSON interface still returns contract metadata in case of an internal compiler error while
serializedOutput = compiler.compile(serializedInput) // CLI interface does not. To make reports comparable we must force this case to be detected as
} // an error in both cases.
catch (exception) if (['UnimplementedFeatureError', 'CompilerError', 'CodeGenerationError'].includes(error['type']))
{
internalCompilerError = true
}
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 + ': <ERROR>')
else
for (const contractFile in result['contracts'])
for (const contractName in result['contracts'][contractFile])
{ {
const contractResults = result['contracts'][contractFile][contractName] internalCompilerError = true
break
let bytecode = '<NO BYTECODE>'
let metadata = '<NO 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)
} }
} }
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 + ': <ERROR>')
else
for (const contractFile in result['contracts'])
for (const contractName in result['contracts'][contractFile])
{
const contractResults = result['contracts'][contractFile][contractName]
let bytecode = '<NO BYTECODE>'
let metadata = '<NO 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)
}
} }
} }

View File

@ -26,12 +26,29 @@ class CompilerInterface(Enum):
STANDARD_JSON = 'standard-json' STANDARD_JSON = 'standard-json'
class SettingsPreset(Enum):
LEGACY_OPTIMIZE = 'legacy-optimize'
LEGACY_NO_OPTIMIZE = 'legacy-no-optimize'
class SMTUse(Enum): class SMTUse(Enum):
PRESERVE = 'preserve' PRESERVE = 'preserve'
DISABLE = 'disable' DISABLE = 'disable'
STRIP_PRAGMAS = 'strip-pragmas' 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) @dataclass(frozen=True)
class ContractReport: class ContractReport:
contract_name: str contract_name: str
@ -190,13 +207,15 @@ def parse_cli_output(source_file_name: Path, cli_output: str) -> FileReport:
def prepare_compiler_input( def prepare_compiler_input(
compiler_path: Path, compiler_path: Path,
source_file_name: Path, source_file_name: Path,
optimize: bool,
force_no_optimize_yul: bool, force_no_optimize_yul: bool,
interface: CompilerInterface, interface: CompilerInterface,
preset: SettingsPreset,
smt_use: SMTUse, smt_use: SMTUse,
metadata_option_supported: bool, metadata_option_supported: bool,
) -> Tuple[List[str], str]: ) -> Tuple[List[str], str]:
settings = CompilerSettings.from_preset(preset)
if interface == CompilerInterface.STANDARD_JSON: if interface == CompilerInterface.STANDARD_JSON:
json_input: dict = { json_input: dict = {
'language': 'Solidity', 'language': 'Solidity',
@ -204,7 +223,7 @@ def prepare_compiler_input(
str(source_file_name): {'content': load_source(source_file_name, smt_use)} str(source_file_name): {'content': load_source(source_file_name, smt_use)}
}, },
'settings': { 'settings': {
'optimizer': {'enabled': optimize}, 'optimizer': {'enabled': settings.optimize},
'outputSelection': {'*': {'*': ['evm.bytecode.object', 'metadata']}}, 'outputSelection': {'*': {'*': ['evm.bytecode.object', 'metadata']}},
} }
} }
@ -220,7 +239,7 @@ def prepare_compiler_input(
compiler_options = [str(source_file_name), '--bin'] compiler_options = [str(source_file_name), '--bin']
if metadata_option_supported: if metadata_option_supported:
compiler_options.append('--metadata') compiler_options.append('--metadata')
if optimize: if settings.optimize:
compiler_options.append('--optimize') compiler_options.append('--optimize')
elif force_no_optimize_yul: elif force_no_optimize_yul:
compiler_options.append('--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( def run_compiler(
compiler_path: Path, compiler_path: Path,
source_file_name: Path, source_file_name: Path,
optimize: bool,
force_no_optimize_yul: bool, force_no_optimize_yul: bool,
interface: CompilerInterface, interface: CompilerInterface,
preset: SettingsPreset,
smt_use: SMTUse, smt_use: SMTUse,
metadata_option_supported: bool, metadata_option_supported: bool,
tmp_dir: Path, tmp_dir: Path,
@ -272,9 +291,9 @@ def run_compiler(
(command_line, compiler_input) = prepare_compiler_input( (command_line, compiler_input) = prepare_compiler_input(
compiler_path, compiler_path,
Path(source_file_name.name), Path(source_file_name.name),
optimize,
force_no_optimize_yul, force_no_optimize_yul,
interface, interface,
preset,
smt_use, smt_use,
metadata_option_supported, metadata_option_supported,
) )
@ -295,9 +314,9 @@ def run_compiler(
(command_line, compiler_input) = prepare_compiler_input( (command_line, compiler_input) = prepare_compiler_input(
compiler_path.absolute(), compiler_path.absolute(),
Path(source_file_name.name), Path(source_file_name.name),
optimize,
force_no_optimize_yul, force_no_optimize_yul,
interface, interface,
preset,
smt_use, smt_use,
metadata_option_supported, metadata_option_supported,
) )
@ -324,6 +343,7 @@ def generate_report(
source_file_names: List[str], source_file_names: List[str],
compiler_path: Path, compiler_path: Path,
interface: CompilerInterface, interface: CompilerInterface,
presets: List[SettingsPreset],
smt_use: SMTUse, smt_use: SMTUse,
force_no_optimize_yul: bool, force_no_optimize_yul: bool,
report_file_path: Path, report_file_path: Path,
@ -335,16 +355,16 @@ def generate_report(
try: try:
with open(report_file_path, mode='w', encoding='utf8', newline='\n') as report_file: 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: with TemporaryDirectory(prefix='prepare_report-') as tmp_dir:
for source_file_name in sorted(source_file_names): for source_file_name in sorted(source_file_names):
try: try:
report = run_compiler( report = run_compiler(
compiler_path, compiler_path,
Path(source_file_name), Path(source_file_name),
optimize,
force_no_optimize_yul, force_no_optimize_yul,
interface, interface,
preset,
smt_use, smt_use,
metadata_option_supported, metadata_option_supported,
Path(tmp_dir), Path(tmp_dir),
@ -358,7 +378,7 @@ def generate_report(
except subprocess.CalledProcessError as exception: except subprocess.CalledProcessError as exception:
print( print(
f"\n\nInterrupted by an exception while processing file " 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 STDOUT:\n{exception.stdout}\n"
f"COMPILER STDERR:\n{exception.stderr}\n", f"COMPILER STDERR:\n{exception.stderr}\n",
file=sys.stderr file=sys.stderr
@ -367,7 +387,7 @@ def generate_report(
except: except:
print( print(
f"\n\nInterrupted by an exception while processing file " 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 file=sys.stderr
) )
raise raise
@ -390,6 +410,15 @@ def commandline_parser() -> ArgumentParser:
choices=[c.value for c in CompilerInterface], choices=[c.value for c in CompilerInterface],
help="Compiler interface to use.", 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( parser.add_argument(
'--smt-use', '--smt-use',
dest='smt_use', dest='smt_use',
@ -418,10 +447,19 @@ def commandline_parser() -> ArgumentParser:
if __name__ == "__main__": if __name__ == "__main__":
options = commandline_parser().parse_args() 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( generate_report(
glob("*.sol"), glob("*.sol"),
Path(options.compiler_path), Path(options.compiler_path),
CompilerInterface(options.interface), CompilerInterface(options.interface),
[SettingsPreset(p) for preset_group in presets for p in preset_group],
SMTUse(options.smt_use), SMTUse(options.smt_use),
options.force_no_optimize_yul, options.force_no_optimize_yul,
Path(options.report_file), Path(options.report_file),

View File

@ -9,7 +9,7 @@ from unittest_helpers import FIXTURE_DIR, LIBSOLIDITY_TEST_DIR, load_fixture, lo
# NOTE: This test file file only works with scripts/ added to PYTHONPATH so pylint can't find the imports # NOTE: This test file file only works with scripts/ added to PYTHONPATH so pylint can't find the imports
# pragma pylint: disable=import-error # 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 from bytecodecompare.prepare_report import load_source, parse_cli_output, parse_standard_json_output, prepare_compiler_input
# pragma pylint: enable=import-error # pragma pylint: enable=import-error
@ -224,7 +224,7 @@ class TestPrepareCompilerInput(PrepareReportTestBase):
(command_line, compiler_input) = prepare_compiler_input( (command_line, compiler_input) = prepare_compiler_input(
Path('solc'), Path('solc'),
SMT_SMOKE_TEST_SOL_PATH, SMT_SMOKE_TEST_SOL_PATH,
optimize=True, preset=SettingsPreset.LEGACY_OPTIMIZE,
force_no_optimize_yul=False, force_no_optimize_yul=False,
interface=CompilerInterface.STANDARD_JSON, interface=CompilerInterface.STANDARD_JSON,
smt_use=SMTUse.DISABLE, smt_use=SMTUse.DISABLE,
@ -238,7 +238,7 @@ class TestPrepareCompilerInput(PrepareReportTestBase):
(command_line, compiler_input) = prepare_compiler_input( (command_line, compiler_input) = prepare_compiler_input(
Path('solc'), Path('solc'),
SMT_SMOKE_TEST_SOL_PATH, SMT_SMOKE_TEST_SOL_PATH,
optimize=True, preset=SettingsPreset.LEGACY_OPTIMIZE,
force_no_optimize_yul=False, force_no_optimize_yul=False,
interface=CompilerInterface.CLI, interface=CompilerInterface.CLI,
smt_use=SMTUse.DISABLE, smt_use=SMTUse.DISABLE,
@ -273,7 +273,7 @@ class TestPrepareCompilerInput(PrepareReportTestBase):
(command_line, compiler_input) = prepare_compiler_input( (command_line, compiler_input) = prepare_compiler_input(
Path('solc'), Path('solc'),
SMT_CONTRACT_WITH_MIXED_NEWLINES_SOL_PATH, SMT_CONTRACT_WITH_MIXED_NEWLINES_SOL_PATH,
optimize=True, preset=SettingsPreset.LEGACY_OPTIMIZE,
force_no_optimize_yul=False, force_no_optimize_yul=False,
interface=CompilerInterface.STANDARD_JSON, interface=CompilerInterface.STANDARD_JSON,
smt_use=SMTUse.DISABLE, smt_use=SMTUse.DISABLE,
@ -287,7 +287,7 @@ class TestPrepareCompilerInput(PrepareReportTestBase):
(_command_line, compiler_input) = prepare_compiler_input( (_command_line, compiler_input) = prepare_compiler_input(
Path('solc'), Path('solc'),
SMT_CONTRACT_WITH_MIXED_NEWLINES_SOL_PATH, SMT_CONTRACT_WITH_MIXED_NEWLINES_SOL_PATH,
optimize=True, preset=SettingsPreset.LEGACY_OPTIMIZE,
force_no_optimize_yul=True, force_no_optimize_yul=True,
interface=CompilerInterface.CLI, interface=CompilerInterface.CLI,
smt_use=SMTUse.DISABLE, smt_use=SMTUse.DISABLE,
@ -300,7 +300,7 @@ class TestPrepareCompilerInput(PrepareReportTestBase):
(command_line, compiler_input) = prepare_compiler_input( (command_line, compiler_input) = prepare_compiler_input(
Path('solc'), Path('solc'),
SMT_SMOKE_TEST_SOL_PATH, SMT_SMOKE_TEST_SOL_PATH,
optimize=False, preset=SettingsPreset.LEGACY_NO_OPTIMIZE,
force_no_optimize_yul=True, force_no_optimize_yul=True,
interface=CompilerInterface.CLI, interface=CompilerInterface.CLI,
smt_use=SMTUse.DISABLE, smt_use=SMTUse.DISABLE,
@ -317,7 +317,7 @@ class TestPrepareCompilerInput(PrepareReportTestBase):
(command_line, compiler_input) = prepare_compiler_input( (command_line, compiler_input) = prepare_compiler_input(
Path('solc'), Path('solc'),
SMT_SMOKE_TEST_SOL_PATH, SMT_SMOKE_TEST_SOL_PATH,
optimize=True, preset=SettingsPreset.LEGACY_OPTIMIZE,
force_no_optimize_yul=False, force_no_optimize_yul=False,
interface=CompilerInterface.CLI, interface=CompilerInterface.CLI,
smt_use=SMTUse.PRESERVE, smt_use=SMTUse.PRESERVE,