Skip to content

Commit

Permalink
prepare_report: Replace hard-coded optimize setting with selectable p…
Browse files Browse the repository at this point in the history
…resets
  • Loading branch information
cameel committed Jun 23, 2023
1 parent f9a3c09 commit 07e7fd7
Show file tree
Hide file tree
Showing 3 changed files with 154 additions and 94 deletions.
176 changes: 99 additions & 77 deletions scripts/bytecodecompare/prepare_report.js
Original file line number Diff line number Diff line change
Expand Up @@ -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)
{
Expand All @@ -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 + ': <ERROR>')
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 = '<NO BYTECODE>'
let metadata = '<NO 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 + ': <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)
}
}
}
58 changes: 48 additions & 10 deletions scripts/bytecodecompare/prepare_report.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -190,21 +207,23 @@ 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',
'sources': {
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']}},
}
}
Expand All @@ -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')
Expand Down Expand Up @@ -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,
Expand All @@ -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,
)
Expand All @@ -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,
)
Expand All @@ -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,
Expand All @@ -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),
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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',
Expand Down Expand Up @@ -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),
Expand Down
Loading

0 comments on commit 07e7fd7

Please sign in to comment.