| import argparse | |
| import bisect | |
| import collections | |
| import copy | |
| import glob | |
| import os | |
| import re | |
| import subprocess | |
| import sys | |
| import shlex | |
| from typing import List, Mapping, Set | |
| ##### Common utilities for update_*test_checks.py | |
| _verbose = False | |
| _prefix_filecheck_ir_name = "" | |
| """ | |
| Version changelog: | |
| 1: Initial version, used by tests that don't specify --version explicitly. | |
| 2: --function-signature is now enabled by default and also checks return | |
| type/attributes. | |
| 3: Opening parenthesis of function args is kept on the first LABEL line | |
| in case arguments are split to a separate SAME line. | |
| 4: --check-globals now has a third option ('smart'). The others are now called | |
| 'none' and 'all'. 'smart' is the default. | |
| 5: Basic block labels are matched by FileCheck expressions | |
| 6: The semantics of TBAA checks has been incorporated in the check lines. | |
| 7: Indent switch-cases correctly; CHECK-EMPTY instead of skipping blank lines. | |
| """ | |
| DEFAULT_VERSION = 6 | |
| SUPPORTED_ANALYSES = { | |
| "Branch Probability Analysis", | |
| "Cost Model Analysis", | |
| "Dependence Analysis", | |
| "Delinearization", | |
| "Loop Access Analysis", | |
| "Loop Cache Analysis", | |
| "Scalar Evolution Analysis", | |
| "Scalar Evolution Division", | |
| } | |
| class Regex(object): | |
| """Wrap a compiled regular expression object to allow deep copy of a regexp. | |
| This is required for the deep copy done in do_scrub. | |
| """ | |
| def __init__(self, regex): | |
| self.regex = regex | |
| def __deepcopy__(self, memo): | |
| result = copy.copy(self) | |
| result.regex = self.regex | |
| return result | |
| def search(self, line): | |
| return self.regex.search(line) | |
| def sub(self, repl, line): | |
| return self.regex.sub(repl, line) | |
| def pattern(self): | |
| return self.regex.pattern | |
| def flags(self): | |
| return self.regex.flags | |
| class Filter(Regex): | |
| """Augment a Regex object with a flag indicating whether a match should be | |
| added (!is_filter_out) or removed (is_filter_out) from the generated checks. | |
| """ | |
| def __init__(self, regex, is_filter_out, is_filter_out_after): | |
| super(Filter, self).__init__(regex) | |
| if is_filter_out and is_filter_out_after: | |
| raise ValueError("cannot use both --filter-out and --filter-out-after") | |
| self.is_filter_out = is_filter_out | |
| self.is_filter_out_after = is_filter_out_after | |
| def __deepcopy__(self, memo): | |
| result = copy.deepcopy(super(Filter, self), memo) | |
| result.is_filter_out = copy.deepcopy(self.is_filter_out, memo) | |
| result.is_filter_out_after = copy.deepcopy(self.is_filter_out_after, memo) | |
| return result | |
| def parse_commandline_args(parser): | |
| class RegexAction(argparse.Action): | |
| """Add a regular expression option value to a list of regular expressions. | |
| This compiles the expression, wraps it in a Regex and adds it to the option | |
| value list.""" | |
| def __init__(self, option_strings, dest, nargs=None, **kwargs): | |
| if nargs is not None: | |
| raise ValueError("nargs not allowed") | |
| super(RegexAction, self).__init__(option_strings, dest, **kwargs) | |
| def do_call(self, namespace, values, flags): | |
| value_list = getattr(namespace, self.dest) | |
| if value_list is None: | |
| value_list = [] | |
| try: | |
| value_list.append(Regex(re.compile(values, flags))) | |
| except re.error as error: | |
| raise ValueError( | |
| "{}: Invalid regular expression '{}' ({})".format( | |
| option_string, error.pattern, error.msg | |
| ) | |
| ) | |
| setattr(namespace, self.dest, value_list) | |
| def __call__(self, parser, namespace, values, option_string=None): | |
| self.do_call(namespace, values, 0) | |
| class FilterAction(RegexAction): | |
| """Add a filter to a list of filter option values.""" | |
| def __init__(self, option_strings, dest, nargs=None, **kwargs): | |
| super(FilterAction, self).__init__(option_strings, dest, nargs, **kwargs) | |
| def __call__(self, parser, namespace, values, option_string=None): | |
| super(FilterAction, self).__call__(parser, namespace, values, option_string) | |
| value_list = getattr(namespace, self.dest) | |
| is_filter_out = option_string == "--filter-out" | |
| is_filter_out_after = option_string == "--filter-out-after" | |
| value_list[-1] = Filter( | |
| value_list[-1].regex, is_filter_out, is_filter_out_after | |
| ) | |
| setattr(namespace, self.dest, value_list) | |
| filter_group = parser.add_argument_group( | |
| "filtering", | |
| """Filters are applied to each output line according to the order given. The | |
| first matching filter terminates filter processing for that current line.""", | |
| ) | |
| filter_group.add_argument( | |
| "--filter", | |
| action=FilterAction, | |
| dest="filters", | |
| metavar="REGEX", | |
| help="Only include lines matching REGEX (may be specified multiple times)", | |
| ) | |
| filter_group.add_argument( | |
| "--filter-out", | |
| action=FilterAction, | |
| dest="filters", | |
| metavar="REGEX", | |
| help="Exclude lines matching REGEX", | |
| ) | |
| filter_group.add_argument( | |
| "--filter-out-after", | |
| action=FilterAction, | |
| dest="filters", | |
| metavar="REGEX", | |
| help="Exclude all lines within a given function after line matching REGEX", | |
| ) | |
| parser.add_argument( | |
| "--include-generated-funcs", | |
| action="store_true", | |
| help="Output checks for functions not in source", | |
| ) | |
| parser.add_argument( | |
| "-v", "--verbose", action="store_true", help="Show verbose output" | |
| ) | |
| parser.add_argument( | |
| "-u", | |
| "--update-only", | |
| action="store_true", | |
| help="Only update test if it was already autogened", | |
| ) | |
| parser.add_argument( | |
| "--force-update", | |
| action="store_true", | |
| help="Update test even if it was autogened by a different script", | |
| ) | |
| parser.add_argument( | |
| "--enable", | |
| action="store_true", | |
| dest="enabled", | |
| default=True, | |
| help="Activate CHECK line generation from this point forward", | |
| ) | |
| parser.add_argument( | |
| "--disable", | |
| action="store_false", | |
| dest="enabled", | |
| help="Deactivate CHECK line generation from this point forward", | |
| ) | |
| parser.add_argument( | |
| "--replace-value-regex", | |
| nargs="+", | |
| default=[], | |
| help="List of regular expressions to replace matching value names", | |
| ) | |
| parser.add_argument( | |
| "--prefix-filecheck-ir-name", | |
| default="", | |
| help="Add a prefix to FileCheck IR value names to avoid conflicts with scripted names", | |
| ) | |
| parser.add_argument( | |
| "--global-value-regex", | |
| nargs="+", | |
| default=[], | |
| help="List of regular expressions that a global value declaration must match to generate a check (has no effect if checking globals is not enabled)", | |
| ) | |
| parser.add_argument( | |
| "--global-hex-value-regex", | |
| nargs="+", | |
| default=[], | |
| help="List of regular expressions such that, for matching global value declarations, literal integer values should be encoded in hex in the associated FileCheck directives", | |
| ) | |
| # FIXME: in 3.9, we can use argparse.BooleanOptionalAction. At that point, | |
| # we need to rename the flag to just -generate-body-for-unused-prefixes. | |
| parser.add_argument( | |
| "--no-generate-body-for-unused-prefixes", | |
| action="store_false", | |
| dest="gen_unused_prefix_body", | |
| default=True, | |
| help="Generate a function body that always matches for unused prefixes. This is useful when unused prefixes are desired, and it avoids needing to annotate each FileCheck as allowing them.", | |
| ) | |
| # This is the default when regenerating existing tests. The default when | |
| # generating new tests is determined by DEFAULT_VERSION. | |
| parser.add_argument( | |
| "--version", type=int, default=1, help="The version of output format" | |
| ) | |
| args = parser.parse_args() | |
| # TODO: This should not be handled differently from the other options | |
| global _verbose, _global_value_regex, _global_hex_value_regex | |
| _verbose = args.verbose | |
| _global_value_regex = args.global_value_regex | |
| _global_hex_value_regex = args.global_hex_value_regex | |
| return args | |
| def parse_args(parser, argv): | |
| args = parser.parse_args(argv) | |
| if args.version >= 2: | |
| args.function_signature = True | |
| # TODO: This should not be handled differently from the other options | |
| global _verbose, _global_value_regex, _global_hex_value_regex | |
| _verbose = args.verbose | |
| _global_value_regex = args.global_value_regex | |
| _global_hex_value_regex = args.global_hex_value_regex | |
| if "check_globals" in args and args.check_globals == "default": | |
| args.check_globals = "none" if args.version < 4 else "smart" | |
| return args | |
| class InputLineInfo(object): | |
| def __init__(self, line, line_number, args, argv): | |
| self.line = line | |
| self.line_number = line_number | |
| self.args = args | |
| self.argv = argv | |
| class TestInfo(object): | |
| def __init__( | |
| self, | |
| test, | |
| parser, | |
| script_name, | |
| input_lines, | |
| args, | |
| argv, | |
| comment_prefix, | |
| argparse_callback, | |
| ): | |
| self.parser = parser | |
| self.argparse_callback = argparse_callback | |
| self.path = test | |
| self.args = args | |
| if args.prefix_filecheck_ir_name: | |
| global _prefix_filecheck_ir_name | |
| _prefix_filecheck_ir_name = args.prefix_filecheck_ir_name | |
| self.argv = argv | |
| self.input_lines = input_lines | |
| self.run_lines = find_run_lines(test, self.input_lines) | |
| self.comment_prefix = comment_prefix | |
| if self.comment_prefix is None: | |
| if self.path.endswith(".mir") or self.path.endswith(".txt"): | |
| self.comment_prefix = "#" | |
| elif self.path.endswith(".s"): | |
| self.comment_prefix = "//" | |
| else: | |
| self.comment_prefix = ";" | |
| self.autogenerated_note_prefix = self.comment_prefix + " " + UTC_ADVERT | |
| self.test_autogenerated_note = self.autogenerated_note_prefix + script_name | |
| self.test_autogenerated_note += get_autogennote_suffix(parser, self.args) | |
| self.test_unused_note = ( | |
| self.comment_prefix + self.comment_prefix + " " + UNUSED_NOTE | |
| ) | |
| def ro_iterlines(self): | |
| for line_num, input_line in enumerate(self.input_lines): | |
| args, argv = check_for_command( | |
| input_line, self.parser, self.args, self.argv, self.argparse_callback | |
| ) | |
| yield InputLineInfo(input_line, line_num, args, argv) | |
| def iterlines(self, output_lines): | |
| output_lines.append(self.test_autogenerated_note) | |
| for line_info in self.ro_iterlines(): | |
| input_line = line_info.line | |
| # Discard any previous script advertising. | |
| if input_line.startswith(self.autogenerated_note_prefix): | |
| continue | |
| self.args = line_info.args | |
| self.argv = line_info.argv | |
| if not self.args.enabled: | |
| output_lines.append(input_line) | |
| continue | |
| yield line_info | |
| def get_checks_for_unused_prefixes( | |
| self, run_list, used_prefixes: List[str] | |
| ) -> List[str]: | |
| run_list = [element for element in run_list if element[0] is not None] | |
| unused_prefixes = set( | |
| [prefix for sublist in run_list for prefix in sublist[0]] | |
| ).difference(set(used_prefixes)) | |
| ret = [] | |
| if not unused_prefixes: | |
| return ret | |
| ret.append(self.test_unused_note) | |
| for unused in sorted(unused_prefixes): | |
| ret.append( | |
| "{comment} {prefix}: {match_everything}".format( | |
| comment=self.comment_prefix, | |
| prefix=unused, | |
| match_everything=r"""{{.*}}""", | |
| ) | |
| ) | |
| return ret | |
| def itertests( | |
| test_patterns, parser, script_name, comment_prefix=None, argparse_callback=None | |
| ): | |
| for pattern in test_patterns: | |
| # On Windows we must expand the patterns ourselves. | |
| tests_list = glob.glob(pattern) | |
| if not tests_list: | |
| warn("Test file pattern '%s' was not found. Ignoring it." % (pattern,)) | |
| continue | |
| for test in tests_list: | |
| with open(test) as f: | |
| input_lines = [l.rstrip() for l in f] | |
| first_line = input_lines[0] if input_lines else "" | |
| if UTC_AVOID in first_line: | |
| warn("Skipping test that must not be autogenerated: " + test) | |
| continue | |
| is_regenerate = UTC_ADVERT in first_line | |
| # If we're generating a new test, set the default version to the latest. | |
| argv = sys.argv[:] | |
| if not is_regenerate: | |
| argv.insert(1, "--version=" + str(DEFAULT_VERSION)) | |
| args = parse_args(parser, argv[1:]) | |
| if argparse_callback is not None: | |
| argparse_callback(args) | |
| if is_regenerate: | |
| if script_name not in first_line and not args.force_update: | |
| warn( | |
| "Skipping test which wasn't autogenerated by " + script_name, | |
| test, | |
| ) | |
| continue | |
| args, argv = check_for_command( | |
| first_line, parser, args, argv, argparse_callback | |
| ) | |
| elif args.update_only: | |
| assert UTC_ADVERT not in first_line | |
| warn("Skipping test which isn't autogenerated: " + test) | |
| continue | |
| final_input_lines = [] | |
| for l in input_lines: | |
| if UNUSED_NOTE in l: | |
| break | |
| final_input_lines.append(l) | |
| yield TestInfo( | |
| test, | |
| parser, | |
| script_name, | |
| final_input_lines, | |
| args, | |
| argv, | |
| comment_prefix, | |
| argparse_callback, | |
| ) | |
| def should_add_line_to_output( | |
| input_line, | |
| prefix_set, | |
| *, | |
| skip_global_checks=False, | |
| skip_same_checks=False, | |
| comment_marker=";", | |
| ): | |
| # Skip any blank comment lines in the IR. | |
| if not skip_global_checks and input_line.strip() == comment_marker: | |
| return False | |
| # Skip a special double comment line we use as a separator. | |
| if input_line.strip() == comment_marker + SEPARATOR: | |
| return False | |
| # Skip any blank lines in the IR. | |
| # if input_line.strip() == '': | |
| # return False | |
| # And skip any CHECK lines. We're building our own. | |
| m = CHECK_RE.match(input_line) | |
| if m and m.group(1) in prefix_set: | |
| if skip_same_checks and CHECK_SAME_RE.match(input_line): | |
| # The previous CHECK line was removed, so don't leave this dangling | |
| return False | |
| if skip_global_checks: | |
| # Skip checks only if they are of global value definitions | |
| global_ir_value_re = re.compile(r"(\[\[|@)", flags=(re.M)) | |
| is_global = global_ir_value_re.search(input_line) | |
| return not is_global | |
| return False | |
| return True | |
| def collect_original_check_lines(ti: TestInfo, prefix_set: set): | |
| """ | |
| Collect pre-existing check lines into a dictionary `result` which is | |
| returned. | |
| result[func_name][prefix] is filled with a list of right-hand-sides of check | |
| lines. | |
| """ | |
| result = collections.defaultdict(lambda: {}) | |
| current_prefix = None | |
| current_function = None | |
| for input_line_info in ti.ro_iterlines(): | |
| input_line = input_line_info.line | |
| if input_line.lstrip().startswith(";"): | |
| m = CHECK_RE.match(input_line) | |
| if m is not None: | |
| prefix = m.group(1) | |
| check_kind = m.group(2) | |
| line = input_line[m.end() :].strip() | |
| if prefix != current_prefix: | |
| current_function = None | |
| current_prefix = None | |
| if check_kind not in ["LABEL", "SAME"]: | |
| if current_function is not None: | |
| current_function.append(line) | |
| continue | |
| if check_kind == "SAME": | |
| continue | |
| if check_kind == "LABEL": | |
| m = IR_FUNCTION_LABEL_RE.match(line) | |
| if m is not None: | |
| func_name = m.group(1) | |
| if ( | |
| ti.args.function is not None | |
| and func_name != ti.args.function | |
| ): | |
| # When filtering on a specific function, skip all others. | |
| continue | |
| current_prefix = prefix | |
| current_function = result[func_name][prefix] = [] | |
| continue | |
| current_function = None | |
| return result | |
| # Perform lit-like substitutions | |
| def getSubstitutions(sourcepath): | |
| sourcedir = os.path.dirname(sourcepath) | |
| return [ | |
| ("%s", sourcepath), | |
| ("%S", sourcedir), | |
| ("%p", sourcedir), | |
| ("%{pathsep}", os.pathsep), | |
| ] | |
| def split_run_line(run_line): | |
| """Split a FileCheck RUN line into its tool, FileCheck, and pre-processing commands.""" | |
| if "%if" in run_line: | |
| match = re.search(r"%{\s*(.*?)\s*%}", run_line) | |
| if match: | |
| run_line = match.group(1) | |
| commands = [cmd.strip() for cmd in run_line.split("|")] | |
| assert len(commands) >= 2 | |
| preprocess_cmd = " | ".join(commands[:-2]) or None | |
| return commands[-2], commands[-1], preprocess_cmd | |
| def applySubstitutions(s, substitutions): | |
| for a, b in substitutions: | |
| s = s.replace(a, b) | |
| return s | |
| # Invoke the tool that is being tested. | |
| def invoke_tool(exe, cmd_args, ir, preprocess_cmd=None, verbose=False): | |
| with open(ir) as ir_file: | |
| substitutions = getSubstitutions(ir) | |
| # TODO Remove the str form which is used by update_test_checks.py and | |
| # update_llc_test_checks.py | |
| # The safer list form is used by update_cc_test_checks.py | |
| if preprocess_cmd: | |
| # Allow pre-processing the IR file (e.g. using sed): | |
| assert isinstance( | |
| preprocess_cmd, str | |
| ) # TODO: use a list instead of using shell | |
| preprocess_cmd = applySubstitutions(preprocess_cmd, substitutions).strip() | |
| if verbose: | |
| print( | |
| "Pre-processing input file: ", | |
| ir, | |
| " with command '", | |
| preprocess_cmd, | |
| "'", | |
| sep="", | |
| file=sys.stderr, | |
| ) | |
| pp = subprocess.Popen( | |
| preprocess_cmd, | |
| shell=True, | |
| stdin=subprocess.DEVNULL, | |
| stdout=subprocess.PIPE, | |
| ) | |
| ir_file = pp.stdout | |
| if isinstance(cmd_args, list): | |
| args = [applySubstitutions(a, substitutions) for a in cmd_args] | |
| stdout = subprocess.check_output([exe] + args, stdin=ir_file) | |
| else: | |
| stdout = subprocess.check_output( | |
| exe + " " + applySubstitutions(cmd_args, substitutions), | |
| shell=True, | |
| stdin=ir_file, | |
| ) | |
| if sys.version_info[0] > 2: | |
| # FYI, if you crashed here with a decode error, your run line probably | |
| # results in bitcode or other binary format being written to the pipe. | |
| # For an opt test, you probably want to add -S or -disable-output. | |
| stdout = stdout.decode() | |
| # Fix line endings to unix CR style. | |
| return stdout.replace("\r\n", "\n") | |
| ##### LLVM IR parser | |
| RUN_LINE_RE = re.compile(r"^\s*(?://|[;#])\s*RUN:\s*(.*)$") | |
| CHECK_PREFIX_RE = re.compile(r"--?check-prefix(?:es)?[= ](\S+)") | |
| PREFIX_RE = re.compile("^[a-zA-Z0-9_-]+$") | |
| CHECK_RE = re.compile( | |
| r"^\s*(?://|[;#])\s*([^:]+?)(?:-(NEXT|NOT|DAG|LABEL|SAME|EMPTY))?:" | |
| ) | |
| CHECK_SAME_RE = re.compile(r"^\s*(?://|[;#])\s*([^:]+?)(?:-SAME)?:") | |
| UTC_ARGS_KEY = "UTC_ARGS:" | |
| UTC_ARGS_CMD = re.compile(r".*" + UTC_ARGS_KEY + r"\s*(?P<cmd>.*)\s*$") | |
| UTC_ADVERT = "NOTE: Assertions have been autogenerated by " | |
| UTC_AVOID = "NOTE: Do not autogenerate" | |
| UNUSED_NOTE = "NOTE: These prefixes are unused and the list is autogenerated. Do not add tests below this line:" | |
| DATA_LAYOUT_RE = re.compile( | |
| r"target\s+datalayout\s+=\s+\"(?P<layout>.+)\"$", flags=(re.M | re.S) | |
| ) | |
| OPT_FUNCTION_RE = re.compile( | |
| r"^(\s*;\s*Function\sAttrs:\s(?P<attrs>[\w\s():,]+?))?\s*define\s+(?P<funcdef_attrs_and_ret>[^@]*)@(?P<func>[\w.$-]+?)\s*" | |
| r"(?P<args_and_sig>\((\)|(.*?[\w.-]+?)\))[^{]*\{)\n(?P<body>.*?)^\}$", | |
| flags=(re.M | re.S), | |
| ) | |
| ANALYZE_FUNCTION_RE = re.compile( | |
| r"^\s*\'(?P<analysis>[\w\s-]+?)\'\s+for\s+function\s+\'(?P<func>[\w.$-]+?)\':" | |
| r"\s*\n(?P<body>.*)$", | |
| flags=(re.X | re.S), | |
| ) | |
| LOOP_PASS_DEBUG_RE = re.compile( | |
| r"^\s*\'(?P<func>[\w.$-]+?)\'[^\n]*" r"\s*\n(?P<body>.*)$", flags=(re.X | re.S) | |
| ) | |
| VPLAN_RE = re.compile( | |
| r"\'(?P<func>[\w.$-]+?)\'[^\n]*\n(?P<body>.*)\n$", flags=(re.X | re.S) | |
| ) | |
| IR_FUNCTION_RE = re.compile(r'^\s*define\s+(?:internal\s+)?[^@]*@"?([\w.$-]+)"?\s*\(') | |
| IR_FUNCTION_LABEL_RE = re.compile( | |
| r'^\s*(?:define\s+(?:internal\s+)?[^@]*)?@"?([\w.$-]+)"?\s*\(' | |
| ) | |
| TRIPLE_IR_RE = re.compile(r'^\s*target\s+triple\s*=\s*"([^"]+)"$') | |
| TRIPLE_ARG_RE = re.compile(r"-m?triple[= ]([^ ]+)") | |
| MARCH_ARG_RE = re.compile(r"-march[= ]([^ ]+)") | |
| DEBUG_ONLY_ARG_RE = re.compile(r"-debug-only[= ]([^ ]+)") | |
| STOP_PASS_RE = re.compile(r"-stop-(before|after)=(\w+)") | |
| IS_DEBUG_RECORD_RE = re.compile(r"^(\s+)#dbg_") | |
| IS_SWITCH_CASE_RE = re.compile(r"^\s+i\d+ \d+, label %\S+") | |
| SCRUB_LEADING_WHITESPACE_RE = re.compile(r"^(\s+)") | |
| SCRUB_WHITESPACE_RE = re.compile(r"(?!^(| \w))[ \t]+", flags=re.M) | |
| SCRUB_PRESERVE_LEADING_WHITESPACE_RE = re.compile(r"((?!^)[ \t]*(\S))[ \t]+") | |
| SCRUB_TRAILING_WHITESPACE_RE = re.compile(r"[ \t]+$", flags=re.M) | |
| SCRUB_TRAILING_WHITESPACE_TEST_RE = SCRUB_TRAILING_WHITESPACE_RE | |
| SCRUB_TRAILING_WHITESPACE_AND_ATTRIBUTES_RE = re.compile( | |
| r"([ \t]|(#[0-9]+))+$", flags=re.M | |
| ) | |
| SCRUB_KILL_COMMENT_RE = re.compile(r"^ *#+ +kill:.*\n") | |
| SCRUB_LOOP_COMMENT_RE = re.compile( | |
| r"# =>This Inner Loop Header:.*|# in Loop:.*", flags=re.M | |
| ) | |
| SCRUB_TAILING_COMMENT_TOKEN_RE = re.compile(r"(?<=\S)+[ \t]*#$", flags=re.M) | |
| SEPARATOR = "." | |
| METADATA_NODES_RE = re.compile(r"^\s*!(\d+)\s*=\s*!\{(.*)\}", re.M) | |
| TBAA_TAGS_RE = re.compile(r"!tbaa\s*!([0-9]+)") | |
| MULTIPLE_BRACES_RE = re.compile(r"{{+|\[\[+") | |
| def error(msg, test_file=None): | |
| if test_file: | |
| msg = "{}: {}".format(msg, test_file) | |
| print("ERROR: {}".format(msg), file=sys.stderr) | |
| def warn(msg, test_file=None): | |
| if test_file: | |
| msg = "{}: {}".format(msg, test_file) | |
| print("WARNING: {}".format(msg), file=sys.stderr) | |
| def debug(*args, **kwargs): | |
| # Python2 does not allow def debug(*args, file=sys.stderr, **kwargs): | |
| if "file" not in kwargs: | |
| kwargs["file"] = sys.stderr | |
| if _verbose: | |
| print(*args, **kwargs) | |
| def find_run_lines(test, lines): | |
| debug("Scanning for RUN lines in test file:", test) | |
| raw_lines = [m.group(1) for m in [RUN_LINE_RE.match(l) for l in lines] if m] | |
| run_lines = [raw_lines[0]] if len(raw_lines) > 0 else [] | |
| for l in raw_lines[1:]: | |
| if run_lines[-1].endswith("\\"): | |
| run_lines[-1] = run_lines[-1].rstrip("\\") + " " + l | |
| else: | |
| run_lines.append(l) | |
| debug("Found {} RUN lines in {}:".format(len(run_lines), test)) | |
| for l in run_lines: | |
| debug(" RUN: {}".format(l)) | |
| return run_lines | |
| def get_triple_from_march(march): | |
| triples = { | |
| "amdgcn": "amdgcn", | |
| "r600": "r600", | |
| "mips": "mips", | |
| "nvptx64": "nvptx64", | |
| "sparc": "sparc", | |
| "hexagon": "hexagon", | |
| "ve": "ve", | |
| } | |
| for prefix, triple in triples.items(): | |
| if march.startswith(prefix): | |
| return triple | |
| print("Cannot find a triple. Assume 'x86'", file=sys.stderr) | |
| return "x86" | |
| def get_globals_name_prefix(raw_tool_output): | |
| m = DATA_LAYOUT_RE.search(raw_tool_output) | |
| if not m: | |
| return None | |
| data_layout = m.group("layout") | |
| idx = data_layout.find("m:") | |
| if idx < 0: | |
| return None | |
| ch = data_layout[idx + 2] | |
| return "_" if ch == "o" or ch == "x" else None | |
| def get_tbaa_records(version, raw_output_tools): | |
| if version < 6: | |
| return {} | |
| # Retrieve all unique tbaa tags for the given IR. | |
| unique_tbaa_tags = {f"!{n}" for n in TBAA_TAGS_RE.findall(raw_output_tools)} | |
| if not unique_tbaa_tags: | |
| return {} | |
| # Small dict of metadata ID and its node content as value. | |
| md_nodes = { | |
| f"!{m.group(1)}": m.group(2) | |
| for m in METADATA_NODES_RE.finditer(raw_output_tools) | |
| } | |
| assert md_nodes, "Shouldn't have TBAA tags without their type descriptors." | |
| result = {} | |
| for tag in unique_tbaa_tags: | |
| type_desc = md_nodes.get(tag) | |
| assert type_desc, f"Expected type descriptor for node {tag}." | |
| # We deal with a tag of kind `(BaseTy, AccessTy, Offset)`. | |
| access_ty = type_desc.split(",")[1].strip() | |
| parent_ty = md_nodes.get(access_ty) | |
| assert parent_ty, f"Couldn't find metadata for access type {access_ty}." | |
| # First operand should be a MDString. If not, likely dealing with | |
| # `new-struct-path-tbaa`. | |
| # TODO: Support `new-struct-path-tbaa` TBAA format. | |
| ty_name_field = parent_ty.split(",")[0] | |
| if not (ty_name_field.startswith('!"') and ty_name_field.endswith('"')): | |
| return {} | |
| ty_name = ty_name_field[2:-1] | |
| if ty_name.startswith("p"): | |
| # Dealing with a pointer here. | |
| pointee_ty_name = ty_name.split(maxsplit=1)[1] | |
| if pointee_ty_name.startswith("omnipotent"): | |
| pointee_ty_name = "char" | |
| # TODO: If pointee_ty_name is a C++ name, should it be demangled? | |
| pointee_ty_name = pointee_ty_name.replace(" ", "_") | |
| tbaa_prefix = f"{pointee_ty_name}ptr" | |
| elif ty_name.startswith("any"): | |
| tbaa_prefix = "anyptr" | |
| elif ty_name.startswith("omnipotent"): | |
| tbaa_prefix = "char" | |
| else: | |
| tbaa_prefix = ty_name.replace(" ", "_") | |
| # Record tag node and its semantics (e.g., INT_TBAA, INTPTR_TBAA). | |
| tbaa_sema = f"{tbaa_prefix.upper()}_TBAA" | |
| result[tag] = tbaa_sema | |
| return result | |
| def apply_filters(line, filters): | |
| has_filter = False | |
| for f in filters: | |
| if f.is_filter_out_after: | |
| continue | |
| if not f.is_filter_out: | |
| has_filter = True | |
| if f.search(line): | |
| return False if f.is_filter_out else True | |
| # If we only used filter-out, keep the line, otherwise discard it since no | |
| # filter matched. | |
| return False if has_filter else True | |
| def has_filter_out_after(filters): | |
| for f in filters: | |
| if f.is_filter_out_after: | |
| return True | |
| return False | |
| def filter_out_after(body, filters): | |
| lines = [] | |
| for line in body.splitlines(): | |
| lines.append(line) | |
| for f in filters: | |
| if f.is_filter_out_after: | |
| if f.search(line): | |
| return lines | |
| return lines | |
| def do_filter(body, filters): | |
| if not filters: | |
| return body | |
| filter_out_after_flag = has_filter_out_after(filters) | |
| lines = [] | |
| if filter_out_after_flag: | |
| lines = filter_out_after(body, filters) | |
| else: | |
| lines = body.splitlines() | |
| return "\n".join(filter(lambda line: apply_filters(line, filters), lines)) | |
| def scrub_body(body): | |
| # Scrub runs of whitespace out of the assembly, but leave the leading | |
| # whitespace in place. | |
| body = SCRUB_PRESERVE_LEADING_WHITESPACE_RE.sub(lambda m: m.group(2) + " ", body) | |
| # Expand the tabs used for indentation. | |
| body = str.expandtabs(body, 2) | |
| # Strip trailing whitespace. | |
| body = SCRUB_TRAILING_WHITESPACE_TEST_RE.sub(r"", body) | |
| return body | |
| def do_scrub(body, scrubber, scrubber_args, extra): | |
| if scrubber_args: | |
| local_args = copy.deepcopy(scrubber_args) | |
| local_args[0].extra_scrub = extra | |
| return scrubber(body, *local_args) | |
| return scrubber(body, *scrubber_args) | |
| # Build up a dictionary of all the function bodies. | |
| class function_body(object): | |
| def __init__( | |
| self, | |
| string, | |
| extra, | |
| funcdef_attrs_and_ret, | |
| args_and_sig, | |
| attrs, | |
| func_name_separator, | |
| ginfo, | |
| ): | |
| self.scrub = string | |
| self.extrascrub = extra | |
| self.funcdef_attrs_and_ret = funcdef_attrs_and_ret | |
| self.args_and_sig = args_and_sig | |
| self.attrs = attrs | |
| self.func_name_separator = func_name_separator | |
| self._ginfo = ginfo | |
| def is_same_except_arg_names( | |
| self, extrascrub, funcdef_attrs_and_ret, args_and_sig, attrs | |
| ): | |
| arg_names = set() | |
| def drop_arg_names(match): | |
| nameless_value = self._ginfo.get_nameless_value_from_match(match) | |
| if nameless_value.check_key == "%": | |
| arg_names.add(self._ginfo.get_name_from_match(match)) | |
| substitute = "" | |
| else: | |
| substitute = match.group(2) | |
| return match.group(1) + substitute + match.group(match.lastindex) | |
| def repl_arg_names(match): | |
| nameless_value = self._ginfo.get_nameless_value_from_match(match) | |
| if ( | |
| nameless_value.check_key == "%" | |
| and self._ginfo.get_name_from_match(match) in arg_names | |
| ): | |
| return match.group(1) + match.group(match.lastindex) | |
| return match.group(1) + match.group(2) + match.group(match.lastindex) | |
| if self.funcdef_attrs_and_ret != funcdef_attrs_and_ret: | |
| return False | |
| if self.attrs != attrs: | |
| return False | |
| regexp = self._ginfo.get_regexp() | |
| ans0 = regexp.sub(drop_arg_names, self.args_and_sig) | |
| ans1 = regexp.sub(drop_arg_names, args_and_sig) | |
| if ans0 != ans1: | |
| return False | |
| if self._ginfo.is_asm(): | |
| # Check without replacements, the replacements are not applied to the | |
| # body for backend checks. | |
| return self.extrascrub == extrascrub | |
| es0 = regexp.sub(repl_arg_names, self.extrascrub) | |
| es1 = regexp.sub(repl_arg_names, extrascrub) | |
| es0 = SCRUB_IR_COMMENT_RE.sub(r"", es0) | |
| es1 = SCRUB_IR_COMMENT_RE.sub(r"", es1) | |
| return es0 == es1 | |
| def __str__(self): | |
| return self.scrub | |
| class FunctionTestBuilder: | |
| def __init__(self, run_list, flags, scrubber_args, path, ginfo): | |
| self._run_list = run_list | |
| self._verbose = flags.verbose | |
| self._record_args = flags.function_signature | |
| self._check_attributes = flags.check_attributes | |
| # Strip double-quotes if input was read by UTC_ARGS | |
| self._filters = ( | |
| list( | |
| map( | |
| lambda f: Filter( | |
| re.compile(f.pattern().strip('"'), f.flags()), | |
| f.is_filter_out, | |
| f.is_filter_out_after, | |
| ), | |
| flags.filters, | |
| ) | |
| ) | |
| if flags.filters | |
| else [] | |
| ) | |
| self._scrubber_args = scrubber_args | |
| self._path = path | |
| self._ginfo = ginfo | |
| # Strip double-quotes if input was read by UTC_ARGS | |
| self._replace_value_regex = list( | |
| map(lambda x: x.strip('"'), flags.replace_value_regex) | |
| ) | |
| self._func_dict = {} | |
| self._func_order = {} | |
| self._global_var_dict = {} | |
| self._processed_prefixes = set() | |
| for tuple in run_list: | |
| for prefix in tuple[0]: | |
| self._func_dict.update({prefix: dict()}) | |
| self._func_order.update({prefix: []}) | |
| self._global_var_dict.update({prefix: dict()}) | |
| # Return true if there is conflicting output for different runs for the | |
| # given prefix and function name. | |
| def has_conflicting_output(self, prefix, func): | |
| # There was conflicting output if the func_dict is None for this | |
| # prefix and function. | |
| return self._func_dict[prefix].get(func) is None | |
| def finish_and_get_func_dict(self): | |
| all_funcs = set() | |
| for prefix in self._func_dict: | |
| all_funcs.update(self._func_dict[prefix].keys()) | |
| warnings_to_print = collections.defaultdict(list) | |
| for func in sorted(list(all_funcs)): | |
| for i, run_info in enumerate(self._run_list): | |
| prefixes = run_info[0] | |
| if not prefixes: | |
| continue | |
| # Check if this RUN line produces this function at all. If | |
| # not, we can skip analysing this function for this RUN. | |
| run_contains_func = all( | |
| func in self._func_dict.get(p, {}) for p in prefixes | |
| ) | |
| if not run_contains_func: | |
| continue | |
| # Check if this RUN line can print any checks for this | |
| # function. It can't if all of its prefixes have conflicting | |
| # (None) output. | |
| cannot_print_for_this_run = all( | |
| self.has_conflicting_output(p, func) for p in prefixes | |
| ) | |
| if cannot_print_for_this_run: | |
| warnings_to_print[func].append((i, prefixes)) | |
| for func, warning_info in warnings_to_print.items(): | |
| conflict_strs = [] | |
| for run_index, prefixes in warning_info: | |
| conflict_strs.append( | |
| f"RUN #{run_index + 1} (prefixes: {', '.join(prefixes)})" | |
| ) | |
| warn( | |
| f"For function '{func}', the following RUN lines will not generate checks due to conflicting output: {', '.join(conflict_strs)}", | |
| test_file=self._path, | |
| ) | |
| return self._func_dict | |
| def func_order(self): | |
| return self._func_order | |
| def global_var_dict(self): | |
| return self._global_var_dict | |
| def is_filtered(self): | |
| for f in self._filters: | |
| if not f.is_filter_out_after: | |
| return True | |
| return False | |
| def process_run_line(self, function_re, scrubber, raw_tool_output, prefixes): | |
| """ | |
| Returns the number of functions processed from the output by the regex. | |
| """ | |
| def escape_braces(match_obj): | |
| return "{{" + re.escape(match_obj.group(0)) + "}}" | |
| # Escape multiple {{ or [[ as {{}} and [[]] have special meaning in | |
| # FileCheck. | |
| raw_tool_output = MULTIPLE_BRACES_RE.sub(escape_braces, raw_tool_output) | |
| build_global_values_dictionary( | |
| self._global_var_dict, raw_tool_output, prefixes, self._ginfo | |
| ) | |
| processed_func_count = 0 | |
| for m in function_re.finditer(raw_tool_output): | |
| if not m: | |
| continue | |
| processed_func_count += 1 | |
| func = m.group("func") | |
| body = m.group("body") | |
| # func_name_separator is the string that is placed right after function name at the | |
| # beginning of assembly function definition. In most assemblies, that is just a | |
| # colon: `foo:`. But, for example, in nvptx it is a brace: `foo(`. If is_backend is | |
| # False, just assume that separator is an empty string. | |
| if self._ginfo.is_asm(): | |
| # Use ':' as default separator. | |
| func_name_separator = ( | |
| m.group("func_name_separator") | |
| if "func_name_separator" in m.groupdict() | |
| else ":" | |
| ) | |
| else: | |
| func_name_separator = "" | |
| attrs = m.group("attrs") if self._check_attributes else "" | |
| funcdef_attrs_and_ret = ( | |
| m.group("funcdef_attrs_and_ret") if self._record_args else "" | |
| ) | |
| # Determine if we print arguments, the opening brace, or nothing after the | |
| # function name | |
| if self._record_args and "args_and_sig" in m.groupdict(): | |
| args_and_sig = scrub_body(m.group("args_and_sig").strip()) | |
| elif "args_and_sig" in m.groupdict(): | |
| args_and_sig = "(" | |
| else: | |
| args_and_sig = "" | |
| filtered_body = do_filter(body, self._filters) | |
| scrubbed_body = do_scrub( | |
| filtered_body, scrubber, self._scrubber_args, extra=False | |
| ) | |
| scrubbed_extra = do_scrub( | |
| filtered_body, scrubber, self._scrubber_args, extra=True | |
| ) | |
| if "analysis" in m.groupdict(): | |
| analysis = m.group("analysis") | |
| if analysis not in SUPPORTED_ANALYSES: | |
| warn("Unsupported analysis mode: %r!" % (analysis,)) | |
| if func.startswith("stress"): | |
| # We only use the last line of the function body for stress tests. | |
| scrubbed_body = "\n".join(scrubbed_body.splitlines()[-1:]) | |
| if self._verbose: | |
| print("Processing function: " + func, file=sys.stderr) | |
| for l in scrubbed_body.splitlines(): | |
| print(" " + l, file=sys.stderr) | |
| for prefix in prefixes: | |
| # Replace function names matching the regex. | |
| for regex in self._replace_value_regex: | |
| # Pattern that matches capture groups in the regex in leftmost order. | |
| group_regex = re.compile(r"\(.*?\)") | |
| # Replace function name with regex. | |
| match = re.match(regex, func) | |
| if match: | |
| func_repl = regex | |
| # Replace any capture groups with their matched strings. | |
| for g in match.groups(): | |
| func_repl = group_regex.sub( | |
| re.escape(g), func_repl, count=1 | |
| ) | |
| func = re.sub(func_repl, "{{" + func_repl + "}}", func) | |
| # Replace all calls to regex matching functions. | |
| matches = re.finditer(regex, scrubbed_body) | |
| for match in matches: | |
| func_repl = regex | |
| # Replace any capture groups with their matched strings. | |
| for g in match.groups(): | |
| func_repl = group_regex.sub( | |
| re.escape(g), func_repl, count=1 | |
| ) | |
| # Substitute function call names that match the regex with the same | |
| # capture groups set. | |
| scrubbed_body = re.sub( | |
| func_repl, "{{" + func_repl + "}}", scrubbed_body | |
| ) | |
| if func in self._func_dict[prefix]: | |
| if self._func_dict[prefix][func] is not None and ( | |
| str(self._func_dict[prefix][func]) != scrubbed_body | |
| or self._func_dict[prefix][func].args_and_sig != args_and_sig | |
| or self._func_dict[prefix][func].attrs != attrs | |
| or self._func_dict[prefix][func].funcdef_attrs_and_ret | |
| != funcdef_attrs_and_ret | |
| ): | |
| if self._func_dict[prefix][func].is_same_except_arg_names( | |
| scrubbed_extra, | |
| funcdef_attrs_and_ret, | |
| args_and_sig, | |
| attrs, | |
| ): | |
| self._func_dict[prefix][func].scrub = scrubbed_extra | |
| self._func_dict[prefix][func].args_and_sig = args_and_sig | |
| else: | |
| # This means a previous RUN line produced a body for this function | |
| # that is different from the one produced by this current RUN line, | |
| # so the body can't be common across RUN lines. We use None to | |
| # indicate that. | |
| self._func_dict[prefix][func] = None | |
| else: | |
| if prefix not in self._processed_prefixes: | |
| self._func_dict[prefix][func] = function_body( | |
| scrubbed_body, | |
| scrubbed_extra, | |
| funcdef_attrs_and_ret, | |
| args_and_sig, | |
| attrs, | |
| func_name_separator, | |
| self._ginfo, | |
| ) | |
| self._func_order[prefix].append(func) | |
| else: | |
| # An earlier RUN line used this check prefixes but didn't produce | |
| # a body for this function. This happens in Clang tests that use | |
| # preprocesser directives to exclude individual functions from some | |
| # RUN lines. | |
| self._func_dict[prefix][func] = None | |
| return processed_func_count | |
| def processed_prefixes(self, prefixes): | |
| """ | |
| Mark a set of prefixes as having had at least one applicable RUN line fully | |
| processed. This is used to filter out function bodies that don't have | |
| outputs for all RUN lines. | |
| """ | |
| self._processed_prefixes.update(prefixes) | |
| ##### Generator of LLVM IR CHECK lines | |
| SCRUB_IR_COMMENT_RE = re.compile(r"\s*;.*") | |
| # Comments to indicate the predecessors of a block in the IR. | |
| SCRUB_PRED_COMMENT_RE = re.compile(r"\s*; preds = .*") | |
| SCRUB_IR_FUNC_META_RE = re.compile(r"((?:\!(?!dbg\b)[a-zA-Z_]\w*(?:\s+![0-9]+)?)\s*)+") | |
| # TODO: We should also derive check lines for global, debug, loop declarations, etc.. | |
| class NamelessValue: | |
| """ | |
| A NamelessValue object represents a type of value in the IR whose "name" we | |
| generalize in the generated check lines; where the "name" could be an actual | |
| name (as in e.g. `@some_global` or `%x`) or just a number (as in e.g. `%12` | |
| or `!4`). | |
| """ | |
| def __init__( | |
| self, | |
| check_prefix, | |
| check_key, | |
| ir_prefix, | |
| ir_regexp, | |
| global_ir_rhs_regexp, | |
| *, | |
| is_before_functions=False, | |
| is_number=False, | |
| replace_number_with_counter=False, | |
| match_literally=False, | |
| interlaced_with_previous=False, | |
| ir_suffix=r"", | |
| ): | |
| self.check_prefix = check_prefix | |
| self.check_key = check_key | |
| self.ir_prefix = ir_prefix | |
| self.ir_regexp = ir_regexp | |
| self.ir_suffix = ir_suffix | |
| self.global_ir_rhs_regexp = global_ir_rhs_regexp | |
| self.is_before_functions = is_before_functions | |
| self.is_number = is_number | |
| # Some variable numbers (e.g. MCINST1234) will change based on unrelated | |
| # modifications to LLVM, replace those with an incrementing counter. | |
| self.replace_number_with_counter = replace_number_with_counter | |
| self.match_literally = match_literally | |
| self.interlaced_with_previous = interlaced_with_previous | |
| self.variable_mapping = {} | |
| # Return true if this kind of IR value is defined "locally" to functions, | |
| # which we assume is only the case precisely for LLVM IR local values. | |
| def is_local_def_ir_value(self): | |
| return self.check_key == "%" | |
| # Return the IR regexp we use for this kind or IR value, e.g., [\w.-]+? for locals | |
| def get_ir_regex(self): | |
| # for backwards compatibility we check locals with '.*' | |
| if self.is_local_def_ir_value(): | |
| return ".*" | |
| return self.ir_regexp | |
| # Create a FileCheck variable name based on an IR name. | |
| def get_value_name(self, var: str, check_prefix: str): | |
| var = var.replace("!", "") | |
| var = var.replace("%", "") | |
| if var.startswith("."): | |
| var = var.replace(".", "dot", 1) | |
| if self.replace_number_with_counter: | |
| assert var | |
| replacement = self.variable_mapping.get(var, None) | |
| if replacement is None: | |
| # Replace variable with an incrementing counter | |
| replacement = str(len(self.variable_mapping) + 1) | |
| self.variable_mapping[var] = replacement | |
| var = replacement | |
| # This is a nameless value, prepend check_prefix. | |
| if var.isdigit(): | |
| var = check_prefix + var | |
| else: | |
| # This is a named value that clashes with the check_prefix, prepend with | |
| # _prefix_filecheck_ir_name, if it has been defined. | |
| if ( | |
| may_clash_with_default_check_prefix_name(check_prefix, var) | |
| and _prefix_filecheck_ir_name | |
| ): | |
| var = _prefix_filecheck_ir_name + var | |
| var = var.replace(".", "_") | |
| var = var.replace("-", "_") | |
| return var.upper() | |
| def get_affixes_from_match(self, match): | |
| prefix = re.match(self.ir_prefix, match.group(2)).group(0) | |
| suffix = re.search(self.ir_suffix + "$", match.group(2)).group(0) | |
| return prefix, suffix | |
| class GeneralizerInfo: | |
| """ | |
| A GeneralizerInfo object holds information about how check lines should be generalized | |
| (e.g., variable names replaced by FileCheck meta variables) as well as per-test-file | |
| state (e.g. information about IR global variables). | |
| """ | |
| MODE_IR = 0 | |
| MODE_ASM = 1 | |
| MODE_ANALYZE = 2 | |
| def __init__( | |
| self, | |
| version, | |
| mode, | |
| nameless_values: List[NamelessValue], | |
| regexp_prefix, | |
| regexp_suffix, | |
| no_meta_details=False, | |
| ): | |
| self._version = version | |
| self._mode = mode | |
| self._nameless_values = nameless_values | |
| self._regexp_prefix = regexp_prefix | |
| self._regexp_suffix = regexp_suffix | |
| self._no_meta_details = no_meta_details | |
| self._regexp, _ = self._build_regexp(False, False) | |
| ( | |
| self._unstable_globals_regexp, | |
| self._unstable_globals_values, | |
| ) = self._build_regexp(True, True) | |
| def _build_regexp(self, globals_only, unstable_only): | |
| matches = [] | |
| values = [] | |
| for nameless_value in self._nameless_values: | |
| is_global = nameless_value.global_ir_rhs_regexp is not None | |
| if globals_only and not is_global: | |
| continue | |
| if unstable_only and nameless_value.match_literally: | |
| continue | |
| match = f"(?:{nameless_value.ir_prefix}({nameless_value.ir_regexp}){nameless_value.ir_suffix})" | |
| if self.is_ir() and not globals_only and is_global: | |
| match = "^" + match | |
| matches.append(match) | |
| values.append(nameless_value) | |
| regexp_string = r"|".join(matches) | |
| return ( | |
| re.compile( | |
| self._regexp_prefix + r"(" + regexp_string + r")" + self._regexp_suffix | |
| ), | |
| values, | |
| ) | |
| def get_version(self): | |
| return self._version | |
| def is_ir(self): | |
| return self._mode == GeneralizerInfo.MODE_IR | |
| def is_asm(self): | |
| return self._mode == GeneralizerInfo.MODE_ASM | |
| def is_analyze(self): | |
| return self._mode == GeneralizerInfo.MODE_ANALYZE | |
| def get_nameless_values(self): | |
| return self._nameless_values | |
| def get_regexp(self): | |
| return self._regexp | |
| def get_unstable_globals_regexp(self): | |
| return self._unstable_globals_regexp | |
| def no_meta_details(self): | |
| return self._no_meta_details | |
| # The entire match is group 0, the prefix has one group (=1), the entire | |
| # IR_VALUE_REGEXP_STRING is one group (=2), and then the nameless values start. | |
| FIRST_NAMELESS_GROUP_IN_MATCH = 3 | |
| def get_match_info(self, match): | |
| """ | |
| Returns (name, nameless_value) for the given match object | |
| """ | |
| if match.re == self._regexp: | |
| values = self._nameless_values | |
| else: | |
| match.re == self._unstable_globals_regexp | |
| values = self._unstable_globals_values | |
| for i in range(len(values)): | |
| g = match.group(i + GeneralizerInfo.FIRST_NAMELESS_GROUP_IN_MATCH) | |
| if g is not None: | |
| return g, values[i] | |
| error("Unable to identify the kind of IR value from the match!") | |
| return None, None | |
| # See get_idx_from_match | |
| def get_name_from_match(self, match): | |
| return self.get_match_info(match)[0] | |
| def get_nameless_value_from_match(self, match) -> NamelessValue: | |
| return self.get_match_info(match)[1] | |
| def make_ir_generalizer(version, no_meta_details): | |
| values = [] | |
| if version >= 5: | |
| values += [ | |
| NamelessValue(r"BB", "%", r"label %", r"[\w$.-]+?", None), | |
| NamelessValue(r"BB", "%", r"^", r"[\w$.-]+?", None, ir_suffix=r":"), | |
| ] | |
| values += [ | |
| # check_prefix check_key ir_prefix ir_regexp global_ir_rhs_regexp | |
| NamelessValue(r"TMP", "%", r"%", r"[\w$.-]+?", None), | |
| NamelessValue(r"ATTR", "#", r"#", r"[0-9]+", None), | |
| NamelessValue(r"ATTR", "#", r"attributes #", r"[0-9]+", r"{[^}]*}"), | |
| NamelessValue(r"GLOB", "@", r"@", r"[0-9]+", None), | |
| NamelessValue(r"GLOB", "@", r"@", r"[0-9]+", r".+", is_before_functions=True), | |
| NamelessValue( | |
| r"GLOBNAMED", | |
| "@", | |
| r"@", | |
| r"[a-zA-Z0-9_$\"\\.-]*[a-zA-Z_$\"\\.-][a-zA-Z0-9_$\"\\.-]*", | |
| r".+", | |
| is_before_functions=True, | |
| match_literally=True, | |
| interlaced_with_previous=True, | |
| ), | |
| NamelessValue(r"DBG", "!", r"!dbg ", r"![0-9]+", None), | |
| NamelessValue(r"DIASSIGNID", "!", r"!DIAssignID ", r"![0-9]+", None), | |
| NamelessValue(r"PROF", "!", r"!prof ", r"![0-9]+", None), | |
| NamelessValue(r"TBAA", "!", r"!tbaa ", r"![0-9]+", None), | |
| NamelessValue(r"TBAA_STRUCT", "!", r"!tbaa.struct ", r"![0-9]+", None), | |
| NamelessValue(r"RNG", "!", r"!range ", r"![0-9]+", None), | |
| NamelessValue(r"LOOP", "!", r"!llvm.loop ", r"![0-9]+", None), | |
| NamelessValue(r"META", "!", r"", r"![0-9]+", r"(?:distinct |)!.*"), | |
| NamelessValue(r"ACC_GRP", "!", r"!llvm.access.group ", r"![0-9]+", None), | |
| NamelessValue(r"META", "!", r"![a-z.]+ ", r"![0-9]+", None), | |
| NamelessValue(r"META", "!", r"[, (]", r"![0-9]+", None), | |
| ] | |
| prefix = r"(\s*)" | |
| suffix = r"([,\s\(\)\}\]]|\Z)" | |
| # values = [ | |
| # nameless_value | |
| # for nameless_value in IR_NAMELESS_VALUES | |
| # if not (globals_only and nameless_value.global_ir_rhs_regexp is None) and | |
| # not (unstable_ids_only and nameless_value.match_literally) | |
| # ] | |
| return GeneralizerInfo( | |
| version, GeneralizerInfo.MODE_IR, values, prefix, suffix, no_meta_details | |
| ) | |
| def make_asm_generalizer(version): | |
| values = [ | |
| NamelessValue( | |
| r"MCINST", | |
| "Inst#", | |
| "<MCInst #", | |
| r"\d+", | |
| r".+", | |
| is_number=True, | |
| replace_number_with_counter=True, | |
| ), | |
| NamelessValue( | |
| r"MCREG", | |
| "Reg:", | |
| "<MCOperand Reg:", | |
| r"\d+", | |
| r".+", | |
| is_number=True, | |
| replace_number_with_counter=True, | |
| ), | |
| ] | |
| prefix = r"((?:#|//)\s*)" | |
| suffix = r"([>\s]|\Z)" | |
| return GeneralizerInfo(version, GeneralizerInfo.MODE_ASM, values, prefix, suffix) | |
| # TODO: This is no longer required. Generalize UTC over an empty GeneralizerInfo. | |
| def make_analyze_generalizer(version): | |
| values = [ | |
| NamelessValue( | |
| r"GRP", | |
| "#", | |
| r"group", | |
| r"0x[0-9a-f]+", | |
| None, | |
| replace_number_with_counter=True, | |
| ), | |
| NamelessValue( | |
| r"VP", | |
| r"vp", | |
| r"vp<", | |
| r"%[0-9]+", | |
| None, | |
| ir_suffix=r">", | |
| ), | |
| ] | |
| prefix = r"(\s*)" | |
| suffix = r"([,\s\(\)\}\]:]|\Z)" | |
| return GeneralizerInfo( | |
| version, GeneralizerInfo.MODE_ANALYZE, values, prefix, suffix | |
| ) | |
| # Return true if var clashes with the scripted FileCheck check_prefix. | |
| def may_clash_with_default_check_prefix_name(check_prefix, var): | |
| return check_prefix and re.match( | |
| r"^" + check_prefix + r"[0-9]+?$", var, re.IGNORECASE | |
| ) | |
| def find_diff_matching(lhs: List[str], rhs: List[str]) -> List[tuple]: | |
| """ | |
| Find a large ordered matching between strings in lhs and rhs. | |
| Think of this as finding the *unchanged* lines in a diff, where the entries | |
| of lhs and rhs are lines of the files being diffed. | |
| Returns a list of matched (lhs_idx, rhs_idx) pairs. | |
| """ | |
| if not lhs or not rhs: | |
| return [] | |
| # Collect matches in reverse order. | |
| matches = [] | |
| # First, collect a set of candidate matching edges. We limit this to a | |
| # constant multiple of the input size to avoid quadratic runtime. | |
| patterns = collections.defaultdict(lambda: ([], [])) | |
| for idx in range(len(lhs)): | |
| patterns[lhs[idx]][0].append(idx) | |
| for idx in range(len(rhs)): | |
| patterns[rhs[idx]][1].append(idx) | |
| multiple_patterns = [] | |
| candidates = [] | |
| for pattern in patterns.values(): | |
| if not pattern[0] or not pattern[1]: | |
| continue | |
| if len(pattern[0]) == len(pattern[1]) == 1: | |
| candidates.append((pattern[0][0], pattern[1][0])) | |
| else: | |
| multiple_patterns.append(pattern) | |
| multiple_patterns.sort(key=lambda pattern: len(pattern[0]) * len(pattern[1])) | |
| for pattern in multiple_patterns: | |
| if len(candidates) + len(pattern[0]) * len(pattern[1]) > 2 * ( | |
| len(lhs) + len(rhs) | |
| ): | |
| break | |
| for lhs_idx in pattern[0]: | |
| for rhs_idx in pattern[1]: | |
| candidates.append((lhs_idx, rhs_idx)) | |
| if not candidates: | |
| # The LHS and RHS either share nothing in common, or lines are just too | |
| # identical. In that case, let's give up and not match anything. | |
| return [] | |
| # Compute a maximal crossing-free matching via an algorithm that is | |
| # inspired by a mixture of dynamic programming and line-sweeping in | |
| # discrete geometry. | |
| # | |
| # I would be surprised if this algorithm didn't exist somewhere in the | |
| # literature, but I found it without consciously recalling any | |
| # references, so you'll have to make do with the explanation below. | |
| # Sorry. | |
| # | |
| # The underlying graph is bipartite: | |
| # - nodes on the LHS represent lines in the original check | |
| # - nodes on the RHS represent lines in the new (updated) check | |
| # | |
| # Nodes are implicitly sorted by the corresponding line number. | |
| # Edges (unique_matches) are sorted by the line number on the LHS. | |
| # | |
| # Here's the geometric intuition for the algorithm. | |
| # | |
| # * Plot the edges as points in the plane, with the original line | |
| # number on the X axis and the updated line number on the Y axis. | |
| # * The goal is to find a longest "chain" of points where each point | |
| # is strictly above and to the right of the previous point. | |
| # * The algorithm proceeds by sweeping a vertical line from left to | |
| # right. | |
| # * The algorithm maintains a table where `table[N]` answers the | |
| # question "What is currently the 'best' way to build a chain of N+1 | |
| # points to the left of the vertical line". Here, 'best' means | |
| # that the last point of the chain is a as low as possible (minimal | |
| # Y coordinate). | |
| # * `table[N]` is `(y, point_idx)` where `point_idx` is the index of | |
| # the last point in the chain and `y` is its Y coordinate | |
| # * A key invariant is that the Y values in the table are | |
| # monotonically increasing | |
| # * Thanks to these properties, the table can be used to answer the | |
| # question "What is the longest chain that can be built to the left | |
| # of the vertical line using only points below a certain Y value", | |
| # using a binary search over the table. | |
| # * The algorithm also builds a backlink structure in which every point | |
| # links back to the previous point on a best (longest) chain ending | |
| # at that point | |
| # | |
| # The core loop of the algorithm sweeps the line and updates the table | |
| # and backlink structure for every point that we cross during the sweep. | |
| # Therefore, the algorithm is trivially O(M log M) in the number of | |
| # points. | |
| candidates.sort(key=lambda candidate: (candidate[0], -candidate[1])) | |
| backlinks = [] | |
| table_rhs_idx = [] | |
| table_candidate_idx = [] | |
| for _, rhs_idx in candidates: | |
| candidate_idx = len(backlinks) | |
| ti = bisect.bisect_left(table_rhs_idx, rhs_idx) | |
| # Update the table to record a best chain ending in the current point. | |
| # There always is one, and if any of the previously visited points had | |
| # a higher Y coordinate, then there is always a previously recorded best | |
| # chain that can be improved upon by using the current point. | |
| # | |
| # There is only one case where there is some ambiguity. If the | |
| # pre-existing entry table[ti] has the same Y coordinate / rhs_idx as | |
| # the current point (this can only happen if the same line appeared | |
| # multiple times on the LHS), then we could choose to keep the | |
| # previously recorded best chain instead. That would bias the algorithm | |
| # differently but should have no systematic impact on the quality of the | |
| # result. | |
| if ti < len(table_rhs_idx): | |
| table_rhs_idx[ti] = rhs_idx | |
| table_candidate_idx[ti] = candidate_idx | |
| else: | |
| table_rhs_idx.append(rhs_idx) | |
| table_candidate_idx.append(candidate_idx) | |
| if ti > 0: | |
| backlinks.append(table_candidate_idx[ti - 1]) | |
| else: | |
| backlinks.append(None) | |
| # Commit to names in the matching by walking the backlinks. Recursively | |
| # attempt to fill in more matches in-between. | |
| match_idx = table_candidate_idx[-1] | |
| while match_idx is not None: | |
| current = candidates[match_idx] | |
| matches.append(current) | |
| match_idx = backlinks[match_idx] | |
| matches.reverse() | |
| return matches | |
| VARIABLE_TAG = "[[@@]]" | |
| METAVAR_PATTERN = r"\[\[([A-Z0-9_]+)(?::[^]]+)?\]\]" | |
| LABEL_METAVAR_PATTERN1 = r"label %" + METAVAR_PATTERN | |
| LABEL_METAVAR_PATTERN2 = r"^" + METAVAR_PATTERN + r":" | |
| METAVAR_RE = re.compile( | |
| rf"(?:{LABEL_METAVAR_PATTERN1})|(?:{LABEL_METAVAR_PATTERN2})|(?:{METAVAR_PATTERN})" | |
| ) | |
| NUMERIC_SUFFIX_RE = re.compile(r"[0-9]*$") | |
| class TestVar: | |
| def __init__(self, nameless_value: NamelessValue, prefix: str, suffix: str): | |
| self._nameless_value = nameless_value | |
| self._prefix = prefix | |
| self._suffix = suffix | |
| def seen(self, nameless_value: NamelessValue, prefix: str, suffix: str): | |
| if prefix != self._prefix: | |
| self._prefix = "" | |
| if suffix != self._suffix: | |
| self._suffix = "" | |
| def get_variable_name(self, text): | |
| return self._nameless_value.get_value_name( | |
| text, self._nameless_value.check_prefix | |
| ) | |
| def get_def(self, name, prefix, suffix): | |
| if self._nameless_value.is_number: | |
| return f"{prefix}[[#{name}:]]{suffix}" | |
| if self._prefix: | |
| assert self._prefix == prefix | |
| prefix = "" | |
| if self._suffix: | |
| assert self._suffix == suffix | |
| suffix = "" | |
| return f"{prefix}[[{name}:{self._prefix}{self._nameless_value.get_ir_regex()}{self._suffix}]]{suffix}" | |
| def get_use(self, name, prefix, suffix): | |
| if self._nameless_value.is_number: | |
| return f"{prefix}[[#{name}]]{suffix}" | |
| if self._prefix: | |
| assert self._prefix == prefix | |
| prefix = "" | |
| if self._suffix: | |
| assert self._suffix == suffix | |
| suffix = "" | |
| return f"{prefix}[[{name}]]{suffix}" | |
| class CheckValueInfo: | |
| def __init__( | |
| self, | |
| key, | |
| text, | |
| name: str, | |
| prefix: str, | |
| suffix: str, | |
| ): | |
| # Key for the value, e.g. '%' | |
| self.key = key | |
| # Text to be matched by the FileCheck variable (without any prefix or suffix) | |
| self.text = text | |
| # Name of the FileCheck variable | |
| self.name = name | |
| # Prefix and suffix that were captured by the NamelessValue regular expression | |
| self.prefix = prefix | |
| self.suffix = suffix | |
| # Represent a check line in a way that allows us to compare check lines while | |
| # ignoring some or all of the FileCheck variable names. | |
| class CheckLineInfo: | |
| def __init__(self, line, values): | |
| # Line with all FileCheck variable name occurrences replaced by VARIABLE_TAG | |
| self.line: str = line | |
| # Information on each FileCheck variable name occurrences in the line | |
| self.values: List[CheckValueInfo] = values | |
| def __repr__(self): | |
| return f"CheckLineInfo(line={self.line}, self.values={self.values})" | |
| def remap_metavar_names( | |
| old_line_infos: List[CheckLineInfo], | |
| new_line_infos: List[CheckLineInfo], | |
| committed_names: Set[str], | |
| ) -> Mapping[str, str]: | |
| """ | |
| Map all FileCheck variable names that appear in new_line_infos to new | |
| FileCheck variable names in an attempt to reduce the diff from old_line_infos | |
| to new_line_infos. | |
| This is done by: | |
| * Matching old check lines and new check lines using a diffing algorithm | |
| applied after replacing names with wildcards. | |
| * Committing to variable names such that the matched lines become equal | |
| (without wildcards) if possible | |
| * This is done recursively to handle cases where many lines are equal | |
| after wildcard replacement | |
| """ | |
| # Initialize uncommitted identity mappings | |
| new_mapping = {} | |
| for line in new_line_infos: | |
| for value in line.values: | |
| new_mapping[value.name] = value.name | |
| # Recursively commit to the identity mapping or find a better one | |
| def recurse(old_begin, old_end, new_begin, new_end): | |
| if old_begin == old_end or new_begin == new_end: | |
| return | |
| # Find a matching of lines where uncommitted names are replaced | |
| # with a placeholder. | |
| def diffify_line(line, mapper): | |
| values = [] | |
| for value in line.values: | |
| mapped = mapper(value.name) | |
| values.append(mapped if mapped in committed_names else "?") | |
| return line.line.strip() + " @@@ " + " @ ".join(values) | |
| lhs_lines = [ | |
| diffify_line(line, lambda x: x) | |
| for line in old_line_infos[old_begin:old_end] | |
| ] | |
| rhs_lines = [ | |
| diffify_line(line, lambda x: new_mapping[x]) | |
| for line in new_line_infos[new_begin:new_end] | |
| ] | |
| candidate_matches = find_diff_matching(lhs_lines, rhs_lines) | |
| candidate_matches = [ | |
| (old_begin + lhs_idx, new_begin + rhs_idx) | |
| for lhs_idx, rhs_idx in candidate_matches | |
| ] | |
| # Candidate matches may conflict if they require conflicting mappings of | |
| # names. We want to determine a large set of compatible candidates, | |
| # because that leads to a small diff. | |
| # | |
| # We think of the candidates as vertices in a conflict graph. The | |
| # conflict graph has edges between incompatible candidates. We want to | |
| # find a large independent set in this graph. | |
| # | |
| # Greedily selecting candidates and removing incompatible ones has the | |
| # disadvantage that making few bad decisions early on can have huge | |
| # consequences. | |
| # | |
| # Instead, we implicitly compute multiple independent sets by greedily | |
| # assigning a *coloring* to the conflict graph. Then, we select the | |
| # largest color class (which is the largest independent set we found), | |
| # commit to all candidates in it, and recurse. | |
| # | |
| # Note that we don't actually materialize the conflict graph. Instead, | |
| # each color class tracks the information needed to decide implicitly | |
| # whether a vertex conflicts (has an edge to) any of the vertices added | |
| # to the color class so far. | |
| class Color: | |
| def __init__(self): | |
| # (lhs_idx, rhs_idx) of matches in this color | |
| self.matches = [] | |
| # rhs_name -> lhs_name mappings required by this color | |
| self.mapping = {} | |
| # lhs_names committed for this color | |
| self.committed = set() | |
| colors = [] | |
| for lhs_idx, rhs_idx in candidate_matches: | |
| lhs_line = old_line_infos[lhs_idx] | |
| rhs_line = new_line_infos[rhs_idx] | |
| # We scan through the uncommitted names in the candidate line and | |
| # filter out the color classes to which the candidate could be | |
| # assigned. | |
| # | |
| # Simultaneously, we prepare a new color class in case the candidate | |
| # conflicts with all colors that have been established so far. | |
| compatible_colors = colors[:] | |
| new_color = Color() | |
| new_color.matches.append((lhs_idx, rhs_idx)) | |
| for lhs_value, rhs_value in zip(lhs_line.values, rhs_line.values): | |
| if new_mapping[rhs_value.name] in committed_names: | |
| # The new value has already been committed. If it was mapped | |
| # to the same name as the original value, we can consider | |
| # committing other values from this line. Otherwise, we | |
| # should ignore this line. | |
| if new_mapping[rhs_value.name] == lhs_value.name: | |
| continue | |
| else: | |
| break | |
| if rhs_value.name in new_color.mapping: | |
| # Same, but for a possible commit happening on the same line | |
| if new_color.mapping[rhs_value.name] == lhs_value.name: | |
| continue | |
| else: | |
| break | |
| if ( | |
| lhs_value.name in committed_names | |
| or lhs_value.name in new_color.committed | |
| ): | |
| # We can't map this value because the name we would map it | |
| # to has already been committed for something else. Give up | |
| # on this line. | |
| break | |
| new_color.mapping[rhs_value.name] = lhs_value.name | |
| new_color.committed.add(lhs_value.name) | |
| color_idx = 0 | |
| while color_idx < len(compatible_colors): | |
| color = compatible_colors[color_idx] | |
| compatible = True | |
| if rhs_value.name in color.mapping: | |
| compatible = color.mapping[rhs_value.name] == lhs_value.name | |
| else: | |
| compatible = lhs_value.name not in color.committed | |
| if compatible: | |
| color_idx += 1 | |
| else: | |
| del compatible_colors[color_idx] | |
| else: | |
| # We never broke out of the loop, which means that at a minimum, | |
| # this line is viable standalone | |
| if compatible_colors: | |
| color = max(compatible_colors, key=lambda color: len(color.matches)) | |
| color.mapping.update(new_color.mapping) | |
| color.committed.update(new_color.committed) | |
| color.matches.append((lhs_idx, rhs_idx)) | |
| else: | |
| colors.append(new_color) | |
| if colors: | |
| # Pick the largest color class. This gives us a large independent | |
| # (non-conflicting) set of candidate matches. Assign all names | |
| # required by the independent set and recurse. | |
| max_color = max(colors, key=lambda color: len(color.matches)) | |
| for rhs_var, lhs_var in max_color.mapping.items(): | |
| new_mapping[rhs_var] = lhs_var | |
| committed_names.add(lhs_var) | |
| if ( | |
| lhs_var != rhs_var | |
| and lhs_var in new_mapping | |
| and new_mapping[lhs_var] == lhs_var | |
| ): | |
| new_mapping[lhs_var] = "conflict_" + lhs_var | |
| matches = ( | |
| [(old_begin - 1, new_begin - 1)] | |
| + max_color.matches | |
| + [(old_end, new_end)] | |
| ) | |
| for (lhs_prev, rhs_prev), (lhs_next, rhs_next) in zip(matches, matches[1:]): | |
| recurse(lhs_prev + 1, lhs_next, rhs_prev + 1, rhs_next) | |
| recurse(0, len(old_line_infos), 0, len(new_line_infos)) | |
| # Commit to remaining names and resolve conflicts | |
| for new_name, mapped_name in new_mapping.items(): | |
| if mapped_name in committed_names: | |
| continue | |
| if not mapped_name.startswith("conflict_"): | |
| assert mapped_name == new_name | |
| committed_names.add(mapped_name) | |
| for new_name, mapped_name in new_mapping.items(): | |
| if mapped_name in committed_names: | |
| continue | |
| assert mapped_name.startswith("conflict_") | |
| m = NUMERIC_SUFFIX_RE.search(new_name) | |
| base_name = new_name[: m.start()] | |
| suffix = int(new_name[m.start() :]) if m.start() != m.end() else 1 | |
| while True: | |
| candidate = f"{base_name}{suffix}" | |
| if candidate not in committed_names: | |
| new_mapping[new_name] = candidate | |
| committed_names.add(candidate) | |
| break | |
| suffix += 1 | |
| return new_mapping | |
| def generalize_check_lines( | |
| lines, | |
| ginfo: GeneralizerInfo, | |
| vars_seen, | |
| global_vars_seen, | |
| global_tbaa_records={}, | |
| preserve_names=False, | |
| original_check_lines=None, | |
| *, | |
| unstable_globals_only=False, | |
| no_meta_details=False, | |
| ignore_all_comments=True, # If False, only ignore comments of predecessors | |
| ): | |
| if unstable_globals_only: | |
| regexp = ginfo.get_unstable_globals_regexp() | |
| else: | |
| regexp = ginfo.get_regexp() | |
| if ginfo.is_ir(): | |
| for i, line in enumerate(lines): | |
| for regex in _global_hex_value_regex: | |
| if re.match("^@" + regex + " = ", line): | |
| line = re.sub( | |
| r"\bi([0-9]+) ([0-9]+)", | |
| lambda m: "i" | |
| + m.group(1) | |
| + " [[#" | |
| + hex(int(m.group(2))) | |
| + "]]", | |
| line, | |
| ) | |
| break | |
| if ignore_all_comments: | |
| # Ignore any comments, since the check lines will too. | |
| scrubbed_line = SCRUB_IR_COMMENT_RE.sub(r"", line) | |
| else: | |
| # Ignore comments of predecessors only. | |
| scrubbed_line = SCRUB_PRED_COMMENT_RE.sub(r"", line) | |
| # Ignore the metadata details if check global is none | |
| if no_meta_details: | |
| scrubbed_line = SCRUB_IR_FUNC_META_RE.sub(r"{{.*}}", scrubbed_line) | |
| lines[i] = scrubbed_line | |
| if not preserve_names: | |
| committed_names = set( | |
| test_var.get_variable_name(name) | |
| for (name, _), test_var in vars_seen.items() | |
| ) | |
| defs = set() | |
| # Collect information about new check lines, and generalize global reference | |
| new_line_infos = [] | |
| for line in lines: | |
| filtered_line = "" | |
| values = [] | |
| while True: | |
| m = regexp.search(line) | |
| if m is None: | |
| filtered_line += line | |
| break | |
| name = ginfo.get_name_from_match(m) | |
| nameless_value = ginfo.get_nameless_value_from_match(m) | |
| prefix, suffix = nameless_value.get_affixes_from_match(m) | |
| if may_clash_with_default_check_prefix_name( | |
| nameless_value.check_prefix, name | |
| ): | |
| warn( | |
| "Change IR value name '%s' or use --prefix-filecheck-ir-name to prevent possible conflict" | |
| " with scripted FileCheck name." % (name,) | |
| ) | |
| # Record the variable as seen and (for locals) accumulate | |
| # prefixes/suffixes | |
| is_local_def = nameless_value.is_local_def_ir_value() | |
| if is_local_def: | |
| vars_dict = vars_seen | |
| else: | |
| vars_dict = global_vars_seen | |
| key = (name, nameless_value.check_key) | |
| if is_local_def: | |
| test_prefix = prefix | |
| test_suffix = suffix | |
| else: | |
| test_prefix = "" | |
| test_suffix = "" | |
| if key in vars_dict: | |
| vars_dict[key].seen(nameless_value, test_prefix, test_suffix) | |
| else: | |
| vars_dict[key] = TestVar(nameless_value, test_prefix, test_suffix) | |
| defs.add(key) | |
| var = vars_dict[key].get_variable_name(name) | |
| # Replace with a [[@@]] tag, but be sure to keep the spaces and commas. | |
| filtered_line += ( | |
| line[: m.start()] + m.group(1) + VARIABLE_TAG + m.group(m.lastindex) | |
| ) | |
| line = line[m.end() :] | |
| values.append( | |
| CheckValueInfo( | |
| key=nameless_value.check_key, | |
| text=name, | |
| name=var, | |
| prefix=prefix, | |
| suffix=suffix, | |
| ) | |
| ) | |
| new_line_infos.append(CheckLineInfo(filtered_line, values)) | |
| committed_names.update( | |
| test_var.get_variable_name(name) | |
| for (name, _), test_var in global_vars_seen.items() | |
| ) | |
| # Collect information about original check lines, if any. | |
| orig_line_infos = [] | |
| for line in original_check_lines or []: | |
| filtered_line = "" | |
| values = [] | |
| while True: | |
| m = METAVAR_RE.search(line) | |
| if m is None: | |
| filtered_line += line | |
| break | |
| # Replace with a [[@@]] tag, but be sure to keep the spaces and commas. | |
| filtered_line += line[: m.start()] + VARIABLE_TAG | |
| line = line[m.end() :] | |
| values.append( | |
| CheckValueInfo( | |
| key=None, | |
| text=None, | |
| # The sole capturing group is only designated for the name. | |
| name=m.group(m.lastindex), | |
| prefix="", | |
| suffix="", | |
| ) | |
| ) | |
| orig_line_infos.append(CheckLineInfo(filtered_line, values)) | |
| # Compute the variable name mapping | |
| mapping = remap_metavar_names(orig_line_infos, new_line_infos, committed_names) | |
| # Apply the variable name mapping | |
| for i, line_info in enumerate(new_line_infos): | |
| line_template = line_info.line | |
| line = "" | |
| for value in line_info.values: | |
| idx = line_template.find(VARIABLE_TAG) | |
| line += line_template[:idx] | |
| line_template = line_template[idx + len(VARIABLE_TAG) :] | |
| key = (value.text, value.key) | |
| if value.key == "%": | |
| vars_dict = vars_seen | |
| else: | |
| vars_dict = global_vars_seen | |
| mapped_name = mapping[value.name] | |
| # We have computed the name mapping. Now, if possible, | |
| # substitute the TBAA value name with its semantics. | |
| if ginfo.get_version() >= 6: | |
| if ( | |
| value.key == "!" | |
| and global_tbaa_records | |
| and mapped_name.startswith("TBAA") | |
| and mapped_name[4:].isdigit() | |
| ): | |
| tbaa_sema = global_tbaa_records.get(value.text) | |
| assert tbaa_sema, f"Shouldn't miss TBAA name for {value.text}?" | |
| mapped_name = f"{tbaa_sema}{mapped_name[4:]}" | |
| if key in defs: | |
| line += vars_dict[key].get_def( | |
| mapped_name, value.prefix, value.suffix | |
| ) | |
| defs.remove(key) | |
| else: | |
| line += vars_dict[key].get_use( | |
| mapped_name, value.prefix, value.suffix | |
| ) | |
| line += line_template | |
| lines[i] = line | |
| return lines | |
| def add_checks( | |
| output_lines, | |
| comment_marker, | |
| prefix_list, | |
| func_dict, | |
| func_name, | |
| check_label_format, | |
| ginfo, | |
| global_vars_seen_dict, | |
| is_filtered, | |
| global_tbaa_records_for_prefixes={}, | |
| preserve_names=False, | |
| original_check_lines: Mapping[str, List[str]] = {}, | |
| check_inst_comments=True, | |
| ): | |
| # prefix_exclusions are prefixes we cannot use to print the function because it doesn't exist in run lines that use these prefixes as well. | |
| prefix_exclusions = set() | |
| printed_prefixes = [] | |
| for p in prefix_list: | |
| checkprefixes = p[0] | |
| # If not all checkprefixes of this run line produced the function we cannot check for it as it does not | |
| # exist for this run line. A subset of the check prefixes might know about the function but only because | |
| # other run lines created it. | |
| if any( | |
| map( | |
| lambda checkprefix: func_name not in func_dict[checkprefix], | |
| checkprefixes, | |
| ) | |
| ): | |
| prefix_exclusions |= set(checkprefixes) | |
| continue | |
| # prefix_exclusions is constructed, we can now emit the output | |
| for p in prefix_list: | |
| global_vars_seen = {} | |
| checkprefixes = p[0] | |
| for checkprefix in checkprefixes: | |
| if checkprefix in global_vars_seen_dict: | |
| global_vars_seen.update(global_vars_seen_dict[checkprefix]) | |
| else: | |
| global_vars_seen_dict[checkprefix] = {} | |
| if checkprefix in printed_prefixes: | |
| break | |
| # Check if the prefix is excluded. | |
| if checkprefix in prefix_exclusions: | |
| continue | |
| # If we do not have output for this prefix we skip it. | |
| if not func_dict[checkprefix][func_name]: | |
| continue | |
| # Add some space between different check prefixes, but not after the last | |
| # check line (before the test code). | |
| if ginfo.is_asm(): | |
| if len(printed_prefixes) != 0: | |
| output_lines.append(comment_marker) | |
| if checkprefix not in global_vars_seen_dict: | |
| global_vars_seen_dict[checkprefix] = {} | |
| global_vars_seen_before = [key for key in global_vars_seen.keys()] | |
| global_tbaa_records = next( | |
| ( | |
| val | |
| for key, val in global_tbaa_records_for_prefixes.items() | |
| if checkprefix in key | |
| ), | |
| None, | |
| ) | |
| vars_seen = {} | |
| printed_prefixes.append(checkprefix) | |
| attrs = str(func_dict[checkprefix][func_name].attrs) | |
| attrs = "" if attrs == "None" else attrs | |
| if ginfo.get_version() > 1: | |
| funcdef_attrs_and_ret = func_dict[checkprefix][ | |
| func_name | |
| ].funcdef_attrs_and_ret | |
| else: | |
| funcdef_attrs_and_ret = "" | |
| if attrs: | |
| output_lines.append( | |
| "%s %s: Function Attrs: %s" % (comment_marker, checkprefix, attrs) | |
| ) | |
| args_and_sig = str(func_dict[checkprefix][func_name].args_and_sig) | |
| if args_and_sig: | |
| args_and_sig = generalize_check_lines( | |
| [args_and_sig], | |
| ginfo, | |
| vars_seen, | |
| global_vars_seen, | |
| global_tbaa_records, | |
| preserve_names, | |
| original_check_lines=[], | |
| no_meta_details=ginfo.no_meta_details(), | |
| )[0] | |
| func_name_separator = func_dict[checkprefix][func_name].func_name_separator | |
| if "[[" in args_and_sig: | |
| # Captures in label lines are not supported, thus split into a -LABEL | |
| # and a separate -SAME line that contains the arguments with captures. | |
| args_and_sig_prefix = "" | |
| if ginfo.get_version() >= 3 and args_and_sig.startswith("("): | |
| # Ensure the "(" separating function name and arguments is in the | |
| # label line. This is required in case of function names that are | |
| # prefixes of each other. Otherwise, the label line for "foo" might | |
| # incorrectly match on "foo.specialized". | |
| args_and_sig_prefix = args_and_sig[0] | |
| args_and_sig = args_and_sig[1:] | |
| # Removing args_and_sig from the label match line requires | |
| # func_name_separator to be empty. Otherwise, the match will not work. | |
| assert func_name_separator == "" | |
| output_lines.append( | |
| check_label_format | |
| % ( | |
| checkprefix, | |
| funcdef_attrs_and_ret, | |
| func_name, | |
| args_and_sig_prefix, | |
| func_name_separator, | |
| ) | |
| ) | |
| output_lines.append( | |
| "%s %s-SAME: %s" % (comment_marker, checkprefix, args_and_sig) | |
| ) | |
| else: | |
| output_lines.append( | |
| check_label_format | |
| % ( | |
| checkprefix, | |
| funcdef_attrs_and_ret, | |
| func_name, | |
| args_and_sig, | |
| func_name_separator, | |
| ) | |
| ) | |
| func_body = str(func_dict[checkprefix][func_name]).splitlines() | |
| if not func_body: | |
| # We have filtered everything. | |
| continue | |
| # For ASM output, just emit the check lines. | |
| if ginfo.is_asm(): | |
| body_start = 1 | |
| if is_filtered: | |
| # For filtered output we don't add "-NEXT" so don't add extra spaces | |
| # before the first line. | |
| body_start = 0 | |
| else: | |
| output_lines.append( | |
| "%s %s: %s" % (comment_marker, checkprefix, func_body[0]) | |
| ) | |
| func_lines = generalize_check_lines( | |
| func_body[body_start:], ginfo, vars_seen, global_vars_seen | |
| ) | |
| for func_line in func_lines: | |
| if func_line.strip() == "": | |
| output_lines.append( | |
| "%s %s-EMPTY:" % (comment_marker, checkprefix) | |
| ) | |
| else: | |
| check_suffix = "-NEXT" if not is_filtered else "" | |
| output_lines.append( | |
| "%s %s%s: %s" | |
| % (comment_marker, checkprefix, check_suffix, func_line) | |
| ) | |
| # Remember new global variables we have not seen before | |
| for key in global_vars_seen: | |
| if key not in global_vars_seen_before: | |
| global_vars_seen_dict[checkprefix][key] = global_vars_seen[key] | |
| break | |
| # For analyze output, generalize the output, and emit CHECK-EMPTY lines as well. | |
| elif ginfo.is_analyze(): | |
| func_body = generalize_check_lines( | |
| func_body, ginfo, vars_seen, global_vars_seen | |
| ) | |
| for func_line in func_body: | |
| if func_line.strip() == "": | |
| output_lines.append( | |
| "{} {}-EMPTY:".format(comment_marker, checkprefix) | |
| ) | |
| else: | |
| # TODO: Remove once only -vplan-print-after is supported. | |
| check_next = not is_filtered and "VPlan" not in func_line | |
| check_suffix = "-NEXT" if check_next else "" | |
| output_lines.append( | |
| "{} {}{}: {}".format( | |
| comment_marker, checkprefix, check_suffix, func_line | |
| ) | |
| ) | |
| # Add space between different check prefixes and also before the first | |
| # line of code in the test function. | |
| output_lines.append(comment_marker) | |
| # Remember new global variables we have not seen before | |
| for key in global_vars_seen: | |
| if key not in global_vars_seen_before: | |
| global_vars_seen_dict[checkprefix][key] = global_vars_seen[key] | |
| break | |
| # For IR output, change all defs to FileCheck variables, so we're immune | |
| # to variable naming fashions. | |
| else: | |
| if ginfo.get_version() >= 7: | |
| # Record the indices of blank lines in the function body preemptively. | |
| blank_line_indices = { | |
| i for i, line in enumerate(func_body) if line.strip() == "" | |
| } | |
| else: | |
| blank_line_indices = set() | |
| func_body = generalize_check_lines( | |
| func_body, | |
| ginfo, | |
| vars_seen, | |
| global_vars_seen, | |
| global_tbaa_records, | |
| preserve_names, | |
| original_check_lines=original_check_lines.get(checkprefix), | |
| # IR output might require comments checks, e.g., print-predicate-info, print<memssa> | |
| ignore_all_comments=not check_inst_comments, | |
| ) | |
| # This could be selectively enabled with an optional invocation argument. | |
| # Disabled for now: better to check everything. Be safe rather than sorry. | |
| # Handle the first line of the function body as a special case because | |
| # it's often just noise (a useless asm comment or entry label). | |
| # if func_body[0].startswith("#") or func_body[0].startswith("entry:"): | |
| # is_blank_line = True | |
| # else: | |
| # output_lines.append('%s %s: %s' % (comment_marker, checkprefix, func_body[0])) | |
| # is_blank_line = False | |
| is_blank_line = False | |
| for idx, func_line in enumerate(func_body): | |
| if func_line.strip() == "": | |
| # We should distinguish if the line is a 'fake' blank line generated by | |
| # generalize_check_lines removing comments. | |
| # Fortunately, generalize_check_lines does not change the index of each line, | |
| # we can record the indices of blank lines preemptively. | |
| if idx in blank_line_indices: | |
| output_lines.append( | |
| "{} {}-EMPTY:".format(comment_marker, checkprefix) | |
| ) | |
| else: | |
| is_blank_line = True | |
| continue | |
| if not check_inst_comments: | |
| # Do not waste time checking IR comments unless necessary. | |
| func_line = SCRUB_IR_COMMENT_RE.sub(r"", func_line) | |
| # Skip blank lines instead of checking them. | |
| if is_blank_line: | |
| output_lines.append( | |
| "{} {}: {}".format( | |
| comment_marker, checkprefix, func_line | |
| ) | |
| ) | |
| else: | |
| check_suffix = "-NEXT" if not is_filtered else "" | |
| output_lines.append( | |
| "{} {}{}: {}".format( | |
| comment_marker, checkprefix, check_suffix, func_line | |
| ) | |
| ) | |
| is_blank_line = False | |
| # Add space between different check prefixes and also before the first | |
| # line of code in the test function. | |
| output_lines.append(comment_marker) | |
| # Remember new global variables we have not seen before | |
| for key in global_vars_seen: | |
| if key not in global_vars_seen_before: | |
| global_vars_seen_dict[checkprefix][key] = global_vars_seen[key] | |
| break | |
| return printed_prefixes | |
| def add_ir_checks( | |
| output_lines, | |
| comment_marker, | |
| prefix_list, | |
| func_dict, | |
| func_name, | |
| preserve_names, | |
| function_sig, | |
| ginfo: GeneralizerInfo, | |
| global_vars_seen_dict, | |
| global_tbaa_records_for_prefixes, | |
| is_filtered, | |
| check_inst_comments=False, | |
| original_check_lines={}, | |
| ): | |
| assert ginfo.is_ir() | |
| # Label format is based on IR string. | |
| if function_sig and ginfo.get_version() > 1: | |
| function_def_regex = "define %s" | |
| elif function_sig: | |
| function_def_regex = "define {{[^@]+}}%s" | |
| else: | |
| function_def_regex = "%s" | |
| check_label_format = "{} %s-LABEL: {}@%s%s%s".format( | |
| comment_marker, function_def_regex | |
| ) | |
| return add_checks( | |
| output_lines, | |
| comment_marker, | |
| prefix_list, | |
| func_dict, | |
| func_name, | |
| check_label_format, | |
| ginfo, | |
| global_vars_seen_dict, | |
| is_filtered, | |
| global_tbaa_records_for_prefixes, | |
| preserve_names, | |
| original_check_lines=original_check_lines, | |
| check_inst_comments=check_inst_comments, | |
| ) | |
| def add_analyze_checks( | |
| output_lines, | |
| comment_marker, | |
| prefix_list, | |
| func_dict, | |
| func_name, | |
| ginfo: GeneralizerInfo, | |
| is_filtered, | |
| check_label_prefix="", | |
| ): | |
| assert ginfo.is_analyze() | |
| check_label_format = "{} %s-LABEL: {}'%s%s%s%s'".format( | |
| comment_marker, check_label_prefix | |
| ) | |
| global_vars_seen_dict = {} | |
| return add_checks( | |
| output_lines, | |
| comment_marker, | |
| prefix_list, | |
| func_dict, | |
| func_name, | |
| check_label_format, | |
| ginfo, | |
| global_vars_seen_dict, | |
| is_filtered, | |
| ) | |
| def build_global_values_dictionary(glob_val_dict, raw_tool_output, prefixes, ginfo): | |
| for nameless_value in ginfo.get_nameless_values(): | |
| if nameless_value.global_ir_rhs_regexp is None: | |
| continue | |
| lhs_re_str = nameless_value.ir_prefix + nameless_value.ir_regexp | |
| rhs_re_str = nameless_value.global_ir_rhs_regexp | |
| global_ir_value_re_str = r"^" + lhs_re_str + r"\s=\s" + rhs_re_str + r"$" | |
| global_ir_value_re = re.compile(global_ir_value_re_str, flags=(re.M)) | |
| lines = [] | |
| for m in global_ir_value_re.finditer(raw_tool_output): | |
| # Attach the substring's start index so that CHECK lines | |
| # can be sorted properly even if they are matched by different nameless values. | |
| # This is relevant for GLOB and GLOBNAMED since they may appear interlaced. | |
| lines.append((m.start(), m.group(0))) | |
| for prefix in prefixes: | |
| if glob_val_dict[prefix] is None: | |
| continue | |
| if nameless_value.check_prefix in glob_val_dict[prefix]: | |
| if lines == glob_val_dict[prefix][nameless_value.check_prefix]: | |
| continue | |
| if prefix == prefixes[-1]: | |
| warn("Found conflicting asm under the same prefix: %r!" % (prefix,)) | |
| else: | |
| glob_val_dict[prefix][nameless_value.check_prefix] = None | |
| continue | |
| glob_val_dict[prefix][nameless_value.check_prefix] = lines | |
| def filter_globals_according_to_preference( | |
| global_val_lines_w_index, global_vars_seen, nameless_value, global_check_setting | |
| ): | |
| if global_check_setting == "none": | |
| return [] | |
| if global_check_setting == "all": | |
| return global_val_lines_w_index | |
| assert global_check_setting == "smart" | |
| if nameless_value.check_key == "#": | |
| # attribute sets are usually better checked by --check-attributes | |
| return [] | |
| def extract(line, nv): | |
| p = ( | |
| "^" | |
| + nv.ir_prefix | |
| + "(" | |
| + nv.ir_regexp | |
| + ") = (" | |
| + nv.global_ir_rhs_regexp | |
| + ")" | |
| ) | |
| match = re.match(p, line) | |
| return (match.group(1), re.findall(nv.ir_regexp, match.group(2))) | |
| transitively_visible = set() | |
| contains_refs_to = {} | |
| def add(var): | |
| nonlocal transitively_visible | |
| nonlocal contains_refs_to | |
| if var in transitively_visible: | |
| return | |
| transitively_visible.add(var) | |
| if not var in contains_refs_to: | |
| return | |
| for x in contains_refs_to[var]: | |
| add(x) | |
| for i, line in global_val_lines_w_index: | |
| (var, refs) = extract(line, nameless_value) | |
| contains_refs_to[var] = refs | |
| for var, check_key in global_vars_seen: | |
| if check_key != nameless_value.check_key: | |
| continue | |
| add(var) | |
| return [ | |
| (i, line) | |
| for i, line in global_val_lines_w_index | |
| if extract(line, nameless_value)[0] in transitively_visible | |
| ] | |
| METADATA_FILTERS = [ | |
| ( | |
| r"(?<=\")(.+ )?(\w+ version )[\d.]+(?:[^\" ]*)(?: \([^)]+\))?", | |
| r"{{.*}}\2{{.*}}", | |
| ), # preface with glob also, to capture optional CLANG_VENDOR | |
| (r'(!DIFile\(filename: ")(.+/)?([^/]+", directory: )"[^"]*"', r"\1{{.*}}\3{{.*}}"), | |
| ] | |
| METADATA_FILTERS_RE = [(re.compile(f), r) for (f, r) in METADATA_FILTERS] | |
| def filter_unstable_metadata(line): | |
| for f, replacement in METADATA_FILTERS_RE: | |
| line = f.sub(replacement, line) | |
| return line | |
| def flush_current_checks(output_lines, new_lines_w_index, comment_marker): | |
| if not new_lines_w_index: | |
| return | |
| output_lines.append(comment_marker + SEPARATOR) | |
| new_lines_w_index.sort() | |
| for _, line in new_lines_w_index: | |
| output_lines.append(line) | |
| new_lines_w_index.clear() | |
| def add_global_checks( | |
| glob_val_dict, | |
| comment_marker, | |
| prefix_list, | |
| output_lines, | |
| ginfo: GeneralizerInfo, | |
| global_vars_seen_dict, | |
| global_tbaa_records_for_prefixes, | |
| preserve_names, | |
| is_before_functions, | |
| global_check_setting, | |
| ): | |
| printed_prefixes = set() | |
| output_lines_loc = {} # Allows GLOB and GLOBNAMED to be sorted correctly | |
| for nameless_value in ginfo.get_nameless_values(): | |
| if nameless_value.global_ir_rhs_regexp is None: | |
| continue | |
| if nameless_value.is_before_functions != is_before_functions: | |
| continue | |
| for p in prefix_list: | |
| global_vars_seen = {} | |
| checkprefixes = p[0] | |
| if checkprefixes is None: | |
| continue | |
| for checkprefix in checkprefixes: | |
| if checkprefix in global_vars_seen_dict: | |
| global_vars_seen.update(global_vars_seen_dict[checkprefix]) | |
| else: | |
| global_vars_seen_dict[checkprefix] = {} | |
| if (checkprefix, nameless_value.check_prefix) in printed_prefixes: | |
| break | |
| if not glob_val_dict[checkprefix]: | |
| continue | |
| if nameless_value.check_prefix not in glob_val_dict[checkprefix]: | |
| continue | |
| if not glob_val_dict[checkprefix][nameless_value.check_prefix]: | |
| continue | |
| check_lines = [] | |
| global_vars_seen_before = [key for key in global_vars_seen.keys()] | |
| global_tbaa_records = next( | |
| ( | |
| val | |
| for key, val in global_tbaa_records_for_prefixes.items() | |
| if checkprefix in key | |
| ), | |
| None, | |
| ) | |
| lines_w_index = glob_val_dict[checkprefix][nameless_value.check_prefix] | |
| lines_w_index = filter_globals_according_to_preference( | |
| lines_w_index, | |
| global_vars_seen_before, | |
| nameless_value, | |
| global_check_setting, | |
| ) | |
| for i, line in lines_w_index: | |
| if _global_value_regex: | |
| matched = False | |
| for regex in _global_value_regex: | |
| if re.match("^@" + regex + " = ", line) or re.match( | |
| "^!" + regex + " = ", line | |
| ): | |
| matched = True | |
| break | |
| if not matched: | |
| continue | |
| [new_line] = generalize_check_lines( | |
| [line], | |
| ginfo, | |
| {}, | |
| global_vars_seen, | |
| global_tbaa_records, | |
| preserve_names, | |
| unstable_globals_only=True, | |
| ) | |
| new_line = filter_unstable_metadata(new_line) | |
| check_line = "%s %s: %s" % (comment_marker, checkprefix, new_line) | |
| check_lines.append((i, check_line)) | |
| if not check_lines: | |
| continue | |
| if not checkprefix in output_lines_loc: | |
| output_lines_loc[checkprefix] = [] | |
| if not nameless_value.interlaced_with_previous: | |
| flush_current_checks( | |
| output_lines, output_lines_loc[checkprefix], comment_marker | |
| ) | |
| for check_line in check_lines: | |
| output_lines_loc[checkprefix].append(check_line) | |
| printed_prefixes.add((checkprefix, nameless_value.check_prefix)) | |
| # Remembe new global variables we have not seen before | |
| for key in global_vars_seen: | |
| if key not in global_vars_seen_before: | |
| global_vars_seen_dict[checkprefix][key] = global_vars_seen[key] | |
| break | |
| if printed_prefixes: | |
| for p in prefix_list: | |
| if p[0] is None: | |
| continue | |
| for checkprefix in p[0]: | |
| if checkprefix not in output_lines_loc: | |
| continue | |
| flush_current_checks( | |
| output_lines, output_lines_loc[checkprefix], comment_marker | |
| ) | |
| break | |
| output_lines.append(comment_marker + SEPARATOR) | |
| return printed_prefixes | |
| def check_prefix(prefix): | |
| if not PREFIX_RE.match(prefix): | |
| hint = "" | |
| if "," in prefix: | |
| hint = " Did you mean '--check-prefixes=" + prefix + "'?" | |
| warn( | |
| ( | |
| "Supplied prefix '%s' is invalid. Prefix must contain only alphanumeric characters, hyphens and underscores." | |
| + hint | |
| ) | |
| % (prefix) | |
| ) | |
| def get_check_prefixes(filecheck_cmd): | |
| check_prefixes = [ | |
| item | |
| for m in CHECK_PREFIX_RE.finditer(filecheck_cmd) | |
| for item in m.group(1).split(",") | |
| ] | |
| if not check_prefixes: | |
| check_prefixes = ["CHECK"] | |
| return check_prefixes | |
| def verify_filecheck_prefixes(fc_cmd): | |
| fc_cmd_parts = fc_cmd.split() | |
| for part in fc_cmd_parts: | |
| if "check-prefix=" in part: | |
| prefix = part.split("=", 1)[1] | |
| check_prefix(prefix) | |
| elif "check-prefixes=" in part: | |
| prefixes = part.split("=", 1)[1].split(",") | |
| for prefix in prefixes: | |
| check_prefix(prefix) | |
| if prefixes.count(prefix) > 1: | |
| warn( | |
| "Supplied prefix '%s' is not unique in the prefix list." | |
| % (prefix,) | |
| ) | |
| def get_autogennote_suffix(parser, args): | |
| autogenerated_note_args = "" | |
| for action in parser._actions: | |
| if not hasattr(args, action.dest): | |
| continue # Ignore options such as --help that aren't included in args | |
| # Ignore parameters such as paths to the binary or the list of tests | |
| if action.dest in ( | |
| "tests", | |
| "update_only", | |
| "tool_binary", | |
| "opt_binary", | |
| "llc_binary", | |
| "llubi_binary", | |
| "clang", | |
| "opt", | |
| "llvm_bin", | |
| "verbose", | |
| "force_update", | |
| "reset_variable_names", | |
| "llvm_mc_binary", | |
| ): | |
| continue | |
| value = getattr(args, action.dest) | |
| if action.dest == "check_globals": | |
| default_value = "none" if args.version < 4 else "smart" | |
| if value == default_value: | |
| continue | |
| autogenerated_note_args += action.option_strings[0] + " " | |
| if args.version < 4 and value == "all": | |
| continue | |
| autogenerated_note_args += "%s " % value | |
| continue | |
| if action.const is not None: # action stores a constant (usually True/False) | |
| # Skip actions with different constant values (this happens with boolean | |
| # --foo/--no-foo options) | |
| if value != action.const: | |
| continue | |
| if parser.get_default(action.dest) == value: | |
| continue # Don't add default values | |
| if action.dest == "function_signature" and args.version >= 2: | |
| continue # Enabled by default in version 2 | |
| if action.dest == "filters": | |
| # Create a separate option for each filter element. The value is a list | |
| # of Filter objects. | |
| for elem in value: | |
| if elem.is_filter_out: | |
| opt_name = "filter-out" | |
| elif elem.is_filter_out_after: | |
| opt_name = "filter-out-after" | |
| else: | |
| opt_name = "filter" | |
| opt_value = elem.pattern() | |
| new_arg = '--%s "%s" ' % (opt_name, opt_value.strip('"')) | |
| if new_arg not in autogenerated_note_args: | |
| autogenerated_note_args += new_arg | |
| else: | |
| autogenerated_note_args += action.option_strings[0] + " " | |
| if action.const is None: # action takes a parameter | |
| if action.nargs == "+": | |
| value = " ".join(map(lambda v: '"' + v.strip('"') + '"', value)) | |
| autogenerated_note_args += "%s " % value | |
| if autogenerated_note_args: | |
| autogenerated_note_args = " %s %s" % ( | |
| UTC_ARGS_KEY, | |
| autogenerated_note_args[:-1], | |
| ) | |
| return autogenerated_note_args | |
| def check_for_command(line, parser, args, argv, argparse_callback): | |
| cmd_m = UTC_ARGS_CMD.match(line) | |
| if cmd_m: | |
| for option in shlex.split(cmd_m.group("cmd").strip()): | |
| if option: | |
| argv.append(option) | |
| args = parse_args(parser, filter(lambda arg: arg not in args.tests, argv)) | |
| if argparse_callback is not None: | |
| argparse_callback(args) | |
| return args, argv | |
| def find_arg_in_test(test_info, get_arg_to_check, arg_string, is_global): | |
| result = get_arg_to_check(test_info.args) | |
| if not result and is_global: | |
| # See if this has been specified via UTC_ARGS. This is a "global" option | |
| # that affects the entire generation of test checks. If it exists anywhere | |
| # in the test, apply it to everything. | |
| saw_line = False | |
| for line_info in test_info.ro_iterlines(): | |
| line = line_info.line | |
| if not line.startswith(";") and line.strip() != "": | |
| saw_line = True | |
| result = get_arg_to_check(line_info.args) | |
| if result: | |
| if warn and saw_line: | |
| # We saw the option after already reading some test input lines. | |
| # Warn about it. | |
| print( | |
| "WARNING: Found {} in line following test start: ".format( | |
| arg_string | |
| ) | |
| + line, | |
| file=sys.stderr, | |
| ) | |
| print( | |
| "WARNING: Consider moving {} to top of file".format(arg_string), | |
| file=sys.stderr, | |
| ) | |
| break | |
| return result | |
| def dump_input_lines(output_lines, test_info, prefix_set, comment_string): | |
| for input_line_info in test_info.iterlines(output_lines): | |
| line = input_line_info.line | |
| args = input_line_info.args | |
| if line.strip() == comment_string: | |
| continue | |
| if line.strip() == comment_string + SEPARATOR: | |
| continue | |
| if line.lstrip().startswith(comment_string): | |
| m = CHECK_RE.match(line) | |
| if m and m.group(1) in prefix_set: | |
| continue | |
| output_lines.append(line.rstrip("\n")) | |
| def add_checks_at_end( | |
| output_lines, prefix_list, func_order, comment_string, check_generator | |
| ): | |
| added = set() | |
| generated_prefixes = set() | |
| for prefix in prefix_list: | |
| prefixes = prefix[0] | |
| tool_args = prefix[1] | |
| for prefix in prefixes: | |
| for func in func_order[prefix]: | |
| # The func order can contain the same functions multiple times. | |
| # If we see one again we are done. | |
| if (func, prefix) in added: | |
| continue | |
| if added: | |
| output_lines.append(comment_string) | |
| # The add_*_checks routines expect a run list whose items are | |
| # tuples that have a list of prefixes as their first element and | |
| # tool command args string as their second element. They output | |
| # checks for each prefix in the list of prefixes. By doing so, it | |
| # implicitly assumes that for each function every run line will | |
| # generate something for that function. That is not the case for | |
| # generated functions as some run lines might not generate them | |
| # (e.g. -fopenmp vs. no -fopenmp). | |
| # | |
| # Therefore, pass just the prefix we're interested in. This has | |
| # the effect of generating all of the checks for functions of a | |
| # single prefix before moving on to the next prefix. So checks | |
| # are ordered by prefix instead of by function as in "normal" | |
| # mode. | |
| for generated_prefix in check_generator( | |
| output_lines, [([prefix], tool_args)], func | |
| ): | |
| added.add((func, generated_prefix)) | |
| generated_prefixes.add(generated_prefix) | |
| return generated_prefixes |