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')
SETTINGS_PRESETS = {
'legacy-optimize': {optimize: true},
'legacy-no-optimize': {optimize: false},
}
function loadSource(sourceFileName, stripSMTPragmas)
{
@ -23,96 +27,112 @@ function cleanString(string)
return (string !== '' ? string : undefined)
}
let inputFiles = []
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
firstFileArgumentIndex = 3
if (process.argv[i] === '--strip-smt-pragmas')
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 = {
language: 'Solidity',
sources: {
[filename]: {content: loadSource(filename, stripSMTPragmas)}
},
settings: {
optimizer: {enabled: optimize},
outputSelection: {'*': {'*': ['evm.bytecode.object', 'metadata']}}
}
}
if (!stripSMTPragmas)
input['settings']['modelChecker'] = {engine: 'none'}
serializedOutput = compiler.compile(serializedInput)
}
catch (exception)
{
internalCompilerError = true
}
let serializedOutput
let result
const serializedInput = JSON.stringify(input)
if (!internalCompilerError)
{
result = JSON.parse(serializedOutput)
let internalCompilerError = false
try
{
serializedOutput = compiler.compile(serializedInput)
}
catch (exception)
{
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])
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']))
{
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)
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]
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'
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),

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
# 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 @@ class TestPrepareCompilerInput(PrepareReportTestBase):
(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 @@ class TestPrepareCompilerInput(PrepareReportTestBase):
(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 @@ class TestPrepareCompilerInput(PrepareReportTestBase):
(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 @@ class TestPrepareCompilerInput(PrepareReportTestBase):
(_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 @@ class TestPrepareCompilerInput(PrepareReportTestBase):
(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 @@ class TestPrepareCompilerInput(PrepareReportTestBase):
(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,