405 lines · python
1#!/usr/bin/env python32 3"""A script to generate FileCheck statements for 'opt' regression tests.4 5This script is a utility to update LLVM opt test cases with new6FileCheck patterns. It can either update all of the tests in the file or7a single test function.8 9Example usage:10 11# Default to using `opt` as found in your PATH.12$ update_test_checks.py test/foo.ll13 14# Override the path lookup.15$ update_test_checks.py --tool-binary=../bin/opt test/foo.ll16 17# Use a custom tool instead of `opt`.18$ update_test_checks.py --tool=yourtool test/foo.ll19 20Workflow:211. Make a compiler patch that requires updating some number of FileCheck lines22 in regression test files.232. Save the patch and revert it from your local work area.243. Update the RUN-lines in the affected regression tests to look canonical.25 Example: "; RUN: opt < %s -instcombine -S | FileCheck %s"264. Refresh the FileCheck lines for either the entire file or select functions by27 running this script.285. Commit the fresh baseline of checks.296. Apply your patch from step 1 and rebuild your local binaries.307. Re-run this script on affected regression tests.318. Check the diffs to ensure the script has done something reasonable.329. Submit a patch including the regression test diffs for review.33"""34 35from __future__ import print_function36 37from sys import stderr38from traceback import print_exc39import argparse40import os # Used to advertise this file's name ("autogenerated_note").41import re42import sys43 44from UpdateTestChecks import common45 46 47def update_test(ti: common.TestInfo):48 # If requested we scrub trailing attribute annotations, e.g., '#0', together with whitespaces49 if ti.args.scrub_attributes:50 common.SCRUB_TRAILING_WHITESPACE_TEST_RE = (51 common.SCRUB_TRAILING_WHITESPACE_AND_ATTRIBUTES_RE52 )53 else:54 common.SCRUB_TRAILING_WHITESPACE_TEST_RE = common.SCRUB_TRAILING_WHITESPACE_RE55 56 tool_basename = ti.args.tool57 58 prefix_list = []59 for l in ti.run_lines:60 if "|" not in l:61 common.warn("Skipping unparsable RUN line: " + l)62 continue63 64 cropped_content = l65 if "%if" in l:66 match = re.search(r"%{\s*(.*?)\s*%}", l)67 if match:68 cropped_content = match.group(1)69 70 commands = [cmd.strip() for cmd in cropped_content.split("|")]71 assert len(commands) >= 272 preprocess_cmd = None73 if len(commands) > 2:74 preprocess_cmd = " | ".join(commands[:-2])75 tool_cmd = commands[-2]76 filecheck_cmd = commands[-1]77 common.verify_filecheck_prefixes(filecheck_cmd)78 if not tool_cmd.startswith(tool_basename + " "):79 common.warn("Skipping non-%s RUN line: %s" % (tool_basename, l))80 continue81 82 if not filecheck_cmd.startswith("FileCheck "):83 common.warn("Skipping non-FileChecked RUN line: " + l)84 continue85 86 tool_cmd_args = tool_cmd[len(tool_basename) :].strip()87 tool_cmd_args = tool_cmd_args.replace("< %s", "").replace("%s", "").strip()88 check_prefixes = common.get_check_prefixes(filecheck_cmd)89 90 # FIXME: We should use multiple check prefixes to common check lines. For91 # now, we just ignore all but the last.92 prefix_list.append((check_prefixes, tool_cmd_args, preprocess_cmd))93 94 ginfo = common.make_ir_generalizer(ti.args.version, ti.args.check_globals == "none")95 global_vars_seen_dict = {}96 global_tbaa_records_for_prefixes = {}97 builder = common.FunctionTestBuilder(98 run_list=prefix_list,99 flags=ti.args,100 scrubber_args=[],101 path=ti.path,102 ginfo=ginfo,103 )104 105 tool_binary = ti.args.tool_binary106 if not tool_binary:107 tool_binary = tool_basename108 109 for prefixes, tool_args, preprocess_cmd in prefix_list:110 common.debug("Extracted tool cmd: " + tool_basename + " " + tool_args)111 common.debug("Extracted FileCheck prefixes: " + str(prefixes))112 113 raw_tool_output = common.invoke_tool(114 tool_binary,115 tool_args,116 ti.path,117 preprocess_cmd=preprocess_cmd,118 verbose=ti.args.verbose,119 )120 builder.process_run_line(121 common.OPT_FUNCTION_RE,122 common.scrub_body,123 raw_tool_output,124 prefixes,125 )126 builder.processed_prefixes(prefixes)127 128 # Extract TBAA metadata for later usage in check lines.129 tbaa_map = common.get_tbaa_records(ti.args.version, raw_tool_output)130 global_tbaa_records_for_prefixes[tuple(prefixes)] = tbaa_map131 132 prefix_set = set([prefix for prefixes, _, _ in prefix_list for prefix in prefixes])133 134 if not ti.args.reset_variable_names:135 original_check_lines = common.collect_original_check_lines(ti, prefix_set)136 else:137 original_check_lines = {}138 139 func_dict = builder.finish_and_get_func_dict()140 is_in_function = False141 is_in_function_start = False142 has_checked_pre_function_globals = False143 common.debug("Rewriting FileCheck prefixes:", str(prefix_set))144 output_lines = []145 146 include_generated_funcs = common.find_arg_in_test(147 ti,148 lambda args: ti.args.include_generated_funcs,149 "--include-generated-funcs",150 True,151 )152 generated_prefixes = []153 if include_generated_funcs:154 # Generate the appropriate checks for each function. We need to emit155 # these in the order according to the generated output so that CHECK-LABEL156 # works properly. func_order provides that.157 158 # We can't predict where various passes might insert functions so we can't159 # be sure the input function order is maintained. Therefore, first spit160 # out all the source lines.161 common.dump_input_lines(output_lines, ti, prefix_set, ";")162 163 args = ti.args164 if args.check_globals != "none":165 generated_prefixes.extend(166 common.add_global_checks(167 builder.global_var_dict(),168 ";",169 prefix_list,170 output_lines,171 ginfo,172 global_vars_seen_dict,173 global_tbaa_records_for_prefixes,174 args.preserve_names,175 True,176 args.check_globals,177 )178 )179 180 # Now generate all the checks.181 generated_prefixes.extend(182 common.add_checks_at_end(183 output_lines,184 prefix_list,185 builder.func_order(),186 ";",187 lambda my_output_lines, prefixes, func: common.add_ir_checks(188 my_output_lines,189 ";",190 prefixes,191 func_dict,192 func,193 False,194 args.function_signature,195 ginfo,196 global_vars_seen_dict,197 global_tbaa_records_for_prefixes,198 is_filtered=builder.is_filtered(),199 original_check_lines=original_check_lines.get(func, {}),200 check_inst_comments=args.check_inst_comments,201 ),202 )203 )204 else:205 # "Normal" mode.206 dropped_previous_line = False207 for input_line_info in ti.iterlines(output_lines):208 input_line = input_line_info.line209 args = input_line_info.args210 if is_in_function_start:211 if input_line == "":212 continue213 if input_line.lstrip().startswith(";"):214 m = common.CHECK_RE.match(input_line)215 if not m or m.group(1) not in prefix_set:216 output_lines.append(input_line)217 continue218 219 # Print out the various check lines here.220 generated_prefixes.extend(221 common.add_ir_checks(222 output_lines,223 ";",224 prefix_list,225 func_dict,226 func_name,227 args.preserve_names,228 args.function_signature,229 ginfo,230 global_vars_seen_dict,231 global_tbaa_records_for_prefixes,232 is_filtered=builder.is_filtered(),233 original_check_lines=original_check_lines.get(func_name, {}),234 check_inst_comments=args.check_inst_comments,235 )236 )237 is_in_function_start = False238 239 m = common.IR_FUNCTION_RE.match(input_line)240 if m and not has_checked_pre_function_globals:241 if args.check_globals:242 generated_prefixes.extend(243 common.add_global_checks(244 builder.global_var_dict(),245 ";",246 prefix_list,247 output_lines,248 ginfo,249 global_vars_seen_dict,250 global_tbaa_records_for_prefixes,251 args.preserve_names,252 True,253 args.check_globals,254 )255 )256 has_checked_pre_function_globals = True257 258 if common.should_add_line_to_output(259 input_line,260 prefix_set,261 skip_global_checks=not is_in_function,262 skip_same_checks=dropped_previous_line,263 ):264 # This input line of the function body will go as-is into the output.265 # Except make leading whitespace uniform: 2 spaces. 4 for debug records/switch cases.266 indent = (267 " " * 4268 if (269 common.IS_DEBUG_RECORD_RE.match(input_line)270 or (271 ti.args.version > 6272 and common.IS_SWITCH_CASE_RE.match(input_line)273 )274 )275 else " " * 2276 )277 input_line = common.SCRUB_LEADING_WHITESPACE_RE.sub(indent, input_line)278 output_lines.append(input_line)279 dropped_previous_line = False280 if input_line.strip() == "}":281 is_in_function = False282 continue283 else:284 # If we are removing a check line, and the next line is CHECK-SAME, it MUST also be removed285 dropped_previous_line = True286 287 if is_in_function:288 continue289 290 m = common.IR_FUNCTION_RE.match(input_line)291 if not m:292 continue293 func_name = m.group(1)294 if args.function is not None and func_name != args.function:295 # When filtering on a specific function, skip all others.296 continue297 is_in_function = is_in_function_start = True298 299 if args.check_globals != "none":300 generated_prefixes.extend(301 common.add_global_checks(302 builder.global_var_dict(),303 ";",304 prefix_list,305 output_lines,306 ginfo,307 global_vars_seen_dict,308 global_tbaa_records_for_prefixes,309 args.preserve_names,310 False,311 args.check_globals,312 )313 )314 if ti.args.gen_unused_prefix_body:315 output_lines.extend(316 ti.get_checks_for_unused_prefixes(prefix_list, generated_prefixes)317 )318 common.debug("Writing %d lines to %s..." % (len(output_lines), ti.path))319 320 with open(ti.path, "wb") as f:321 f.writelines(["{}\n".format(l).encode("utf-8") for l in output_lines])322 323 324def main():325 from argparse import RawTextHelpFormatter326 327 parser = argparse.ArgumentParser(328 description=__doc__, formatter_class=RawTextHelpFormatter329 )330 parser.add_argument(331 "--tool",332 default="opt",333 help='The name of the tool used to generate the test case (defaults to "opt")',334 )335 parser.add_argument(336 "--tool-binary",337 "--opt-binary",338 help="The tool binary used to generate the test case",339 )340 parser.add_argument("--function", help="The function in the test file to update")341 parser.add_argument(342 "-p", "--preserve-names", action="store_true", help="Do not scrub IR names"343 )344 parser.add_argument(345 "--function-signature",346 action="store_true",347 help="Keep function signature information around for the check line",348 )349 parser.add_argument(350 "--scrub-attributes",351 action="store_true",352 help="Remove attribute annotations (#0) from the end of check line",353 )354 parser.add_argument(355 "--check-attributes",356 action="store_true",357 help='Check "Function Attributes" for functions',358 )359 parser.add_argument(360 "--check-globals",361 nargs="?",362 const="all",363 default="default",364 choices=["none", "smart", "all"],365 help="Check global entries (global variables, metadata, attribute sets, ...) for functions",366 )367 parser.add_argument(368 "--check-inst-comments",369 action="store_true",370 default=False,371 help="Check the generated comments describing instructions (e.g., -print-predicate-info/print<memssa>)",372 )373 parser.add_argument(374 "--reset-variable-names",375 action="store_true",376 help="Reset all variable names to correspond closely to the variable names in IR. "377 "This tends to result in larger diffs.",378 )379 parser.add_argument("tests", nargs="+")380 initial_args = common.parse_commandline_args(parser)381 382 script_name = os.path.basename(__file__)383 384 if initial_args.tool_binary:385 tool_basename = os.path.basename(initial_args.tool_binary)386 if not re.match(r"^%s(-\d+)?(\.exe)?$" % (initial_args.tool), tool_basename):387 common.error("Unexpected tool name: " + tool_basename)388 sys.exit(1)389 390 returncode = 0391 for ti in common.itertests(392 initial_args.tests, parser, script_name="utils/" + script_name393 ):394 try:395 update_test(ti)396 except Exception as e:397 stderr.write(f"Error: Failed to update test {ti.path}\n")398 print_exc()399 returncode = 1400 return returncode401 402 403if __name__ == "__main__":404 sys.exit(main())405