1313from typing import List , Optional , Tuple , Union
1414
1515
16- CONTRACT_SEPARATOR_PATTERN = re .compile (r'^======= (?P<file_name>.+):(?P<contract_name>[^:]+) =======$' , re .MULTILINE )
17- BYTECODE_REGEX = re .compile (r'^Binary:\n(?P<bytecode>.*)$' , re .MULTILINE )
18- METADATA_REGEX = re .compile (r'^Metadata:\n(?P<metadata>\{.*\})$' , re .MULTILINE )
16+ CONTRACT_SEPARATOR_PATTERN = re .compile (
17+ r'^ *======= +(?:(?P<file_name>.+) *:)? *(?P<contract_name>[^:]+) +======= *$' ,
18+ re .MULTILINE
19+ )
20+ BYTECODE_REGEX = re .compile (r'^ *Binary: *\n(?P<bytecode>.*[0-9a-f$_]+.*)$' , re .MULTILINE )
21+ METADATA_REGEX = re .compile (r'^ *Metadata: *\n *(?P<metadata>\{.*\}) *$' , re .MULTILINE )
1922
2023
2124class CompilerInterface (Enum ):
2225 CLI = 'cli'
2326 STANDARD_JSON = 'standard-json'
2427
2528
29+ class SMTUse (Enum ):
30+ PRESERVE = 'preserve'
31+ DISABLE = 'disable'
32+ STRIP_PRAGMAS = 'strip-pragmas'
33+
34+
2635@dataclass (frozen = True )
2736class ContractReport :
2837 contract_name : str
29- file_name : Path
38+ file_name : Optional [ Path ]
3039 bytecode : Optional [str ]
3140 metadata : Optional [str ]
3241
@@ -54,15 +63,23 @@ def format_report(self) -> str:
5463 return report
5564
5665
57- def load_source (path : Union [Path , str ]) -> str :
66+ def load_source (path : Union [Path , str ], smt_use : SMTUse ) -> str :
5867 # NOTE: newline='' disables newline conversion.
5968 # We want the file exactly as is because changing even a single byte in the source affects metadata.
6069 with open (path , mode = 'r' , encoding = 'utf8' , newline = '' ) as source_file :
6170 file_content = source_file .read ()
6271
72+ if smt_use == SMTUse .STRIP_PRAGMAS :
73+ return file_content .replace ('pragma experimental SMTChecker;' , '' , 1 )
74+
6375 return file_content
6476
6577
78+ def clean_string (value : Optional [str ]) -> Optional [str ]:
79+ value = value .strip () if value is not None else None
80+ return value if value != '' else None
81+
82+
6683def parse_standard_json_output (source_file_name : Path , standard_json_output : str ) -> FileReport :
6784 decoded_json_output = json .loads (standard_json_output .strip ())
6885
@@ -89,8 +106,8 @@ def parse_standard_json_output(source_file_name: Path, standard_json_output: str
89106 file_report .contract_reports .append (ContractReport (
90107 contract_name = contract_name ,
91108 file_name = Path (file_name ),
92- bytecode = contract_results .get ('evm' , {}).get ('bytecode' , {}).get ('object' ),
93- metadata = contract_results .get ('metadata' ),
109+ bytecode = clean_string ( contract_results .get ('evm' , {}).get ('bytecode' , {}).get ('object' ) ),
110+ metadata = clean_string ( contract_results .get ('metadata' ) ),
94111 ))
95112
96113 return file_report
@@ -113,55 +130,92 @@ def parse_cli_output(source_file_name: Path, cli_output: str) -> FileReport:
113130
114131 assert file_report .contract_reports is not None
115132 file_report .contract_reports .append (ContractReport (
116- contract_name = contract_name ,
117- file_name = Path (file_name ) ,
118- bytecode = bytecode_match ['bytecode' ] if bytecode_match is not None else None ,
119- metadata = metadata_match ['metadata' ] if metadata_match is not None else None ,
133+ contract_name = contract_name . strip () ,
134+ file_name = Path (file_name . strip ()) if file_name is not None else None ,
135+ bytecode = clean_string ( bytecode_match ['bytecode' ] if bytecode_match is not None else None ) ,
136+ metadata = clean_string ( metadata_match ['metadata' ] if metadata_match is not None else None ) ,
120137 ))
121138
122139 return file_report
123140
124141
125- def prepare_compiler_input (
142+ def prepare_compiler_input ( # pylint: disable=too-many-arguments
126143 compiler_path : Path ,
127144 source_file_name : Path ,
128145 optimize : bool ,
129- interface : CompilerInterface
146+ force_no_optimize_yul : bool ,
147+ interface : CompilerInterface ,
148+ smt_use : SMTUse ,
149+ metadata_option_supported : bool ,
130150) -> Tuple [List [str ], str ]:
131151
132152 if interface == CompilerInterface .STANDARD_JSON :
133153 json_input : dict = {
134154 'language' : 'Solidity' ,
135155 'sources' : {
136- str (source_file_name ): {'content' : load_source (source_file_name )}
156+ str (source_file_name ): {'content' : load_source (source_file_name , smt_use )}
137157 },
138158 'settings' : {
139159 'optimizer' : {'enabled' : optimize },
140160 'outputSelection' : {'*' : {'*' : ['evm.bytecode.object' , 'metadata' ]}},
141- 'modelChecker' : {'engine' : 'none' },
142161 }
143162 }
144163
164+ if smt_use == SMTUse .DISABLE :
165+ json_input ['settings' ]['modelChecker' ] = {'engine' : 'none' }
166+
145167 command_line = [str (compiler_path ), '--standard-json' ]
146168 compiler_input = json .dumps (json_input )
147169 else :
148170 assert interface == CompilerInterface .CLI
149171
150- compiler_options = [str (source_file_name ), '--bin' , '--metadata' , '--model-checker-engine' , 'none' ]
172+ compiler_options = [str (source_file_name ), '--bin' ]
173+ if metadata_option_supported :
174+ compiler_options .append ('--metadata' )
151175 if optimize :
152176 compiler_options .append ('--optimize' )
177+ elif force_no_optimize_yul :
178+ compiler_options .append ('--no-optimize-yul' )
179+ if smt_use == SMTUse .DISABLE :
180+ compiler_options += ['--model-checker-engine' , 'none' ]
153181
154182 command_line = [str (compiler_path )] + compiler_options
155- compiler_input = load_source (source_file_name )
183+ compiler_input = load_source (source_file_name , smt_use )
156184
157185 return (command_line , compiler_input )
158186
159187
160- def run_compiler (
188+ def detect_metadata_cli_option_support (compiler_path : Path ):
189+ process = subprocess .run (
190+ [str (compiler_path .absolute ()), '--metadata' , '-' ],
191+ input = "contract C {}" ,
192+ encoding = 'utf8' ,
193+ capture_output = True ,
194+ check = False ,
195+ )
196+
197+ negative_response = "unrecognised option '--metadata'" .strip ()
198+ if (process .returncode == 0 ) != (process .stderr .strip () != negative_response ):
199+ # If the error is other than expected or there's an error message but no error, don't try
200+ # to guess. Just fail.
201+ print (
202+ f"Compiler exit code: { process .returncode } \n "
203+ f"Compiler output:\n { process .stderr } \n " ,
204+ file = sys .stderr
205+ )
206+ raise Exception ("Failed to determine if the compiler supports the --metadata option." )
207+
208+ return process .returncode == 0
209+
210+
211+ def run_compiler ( # pylint: disable=too-many-arguments
161212 compiler_path : Path ,
162213 source_file_name : Path ,
163214 optimize : bool ,
215+ force_no_optimize_yul : bool ,
164216 interface : CompilerInterface ,
217+ smt_use : SMTUse ,
218+ metadata_option_supported : bool ,
165219 tmp_dir : Path ,
166220) -> FileReport :
167221
@@ -170,7 +224,10 @@ def run_compiler(
170224 compiler_path ,
171225 Path (source_file_name .name ),
172226 optimize ,
173- interface
227+ force_no_optimize_yul ,
228+ interface ,
229+ smt_use ,
230+ metadata_option_supported ,
174231 )
175232
176233 process = subprocess .run (
@@ -190,7 +247,10 @@ def run_compiler(
190247 compiler_path .absolute (),
191248 Path (source_file_name .name ),
192249 optimize ,
193- interface
250+ force_no_optimize_yul ,
251+ interface ,
252+ smt_use ,
253+ metadata_option_supported ,
194254 )
195255
196256 # Create a copy that we can use directly with the CLI interface
@@ -211,13 +271,30 @@ def run_compiler(
211271 return parse_cli_output (Path (source_file_name ), process .stdout )
212272
213273
214- def generate_report (source_file_names : List [str ], compiler_path : Path , interface : CompilerInterface ):
274+ def generate_report (
275+ source_file_names : List [str ],
276+ compiler_path : Path ,
277+ interface : CompilerInterface ,
278+ smt_use : SMTUse ,
279+ force_no_optimize_yul : bool
280+ ):
281+ metadata_option_supported = detect_metadata_cli_option_support (compiler_path )
282+
215283 with open ('report.txt' , mode = 'w' , encoding = 'utf8' , newline = '\n ' ) as report_file :
216284 for optimize in [False , True ]:
217285 with TemporaryDirectory (prefix = 'prepare_report-' ) as tmp_dir :
218286 for source_file_name in sorted (source_file_names ):
219287 try :
220- report = run_compiler (compiler_path , Path (source_file_name ), optimize , interface , Path (tmp_dir ))
288+ report = run_compiler (
289+ compiler_path ,
290+ Path (source_file_name ),
291+ optimize ,
292+ force_no_optimize_yul ,
293+ interface ,
294+ smt_use ,
295+ metadata_option_supported ,
296+ Path (tmp_dir ),
297+ )
221298 report_file .write (report .format_report ())
222299 except subprocess .CalledProcessError as exception :
223300 print (
@@ -250,7 +327,21 @@ def commandline_parser() -> ArgumentParser:
250327 dest = 'interface' ,
251328 default = CompilerInterface .STANDARD_JSON .value ,
252329 choices = [c .value for c in CompilerInterface ],
253- help = "Compiler interface to use."
330+ help = "Compiler interface to use." ,
331+ )
332+ parser .add_argument (
333+ '--smt-use' ,
334+ dest = 'smt_use' ,
335+ default = SMTUse .DISABLE .value ,
336+ choices = [s .value for s in SMTUse ],
337+ help = "What to do about contracts that use the experimental SMT checker."
338+ )
339+ parser .add_argument (
340+ '--force-no-optimize-yul' ,
341+ dest = 'force_no_optimize_yul' ,
342+ default = False ,
343+ action = 'store_true' ,
344+ help = "Explicitly disable Yul optimizer in CLI runs without optimization to work around a bug in solc 0.6.0 and 0.6.1."
254345 )
255346 return parser ;
256347
@@ -261,4 +352,6 @@ def commandline_parser() -> ArgumentParser:
261352 glob ("*.sol" ),
262353 Path (options .compiler_path ),
263354 CompilerInterface (options .interface ),
355+ SMTUse (options .smt_use ),
356+ options .force_no_optimize_yul ,
264357 )
0 commit comments