From 456c7420d5b04bbffe2495ac6610584c76ecf137 Mon Sep 17 00:00:00 2001 From: Anton Lydike Date: Sun, 14 Jul 2024 14:49:10 +0100 Subject: [PATCH 1/2] add support for --check-prefixes --- filecheck/matcher.py | 15 +++++++---- filecheck/ops.py | 15 +++++++---- filecheck/options.py | 31 ++++++++++++++++++----- filecheck/parser.py | 30 ++++++++++++---------- filecheck/preprocess.py | 4 +-- tests/filecheck/flags/check-prefixes.test | 12 +++++++++ 6 files changed, 75 insertions(+), 32 deletions(-) create mode 100644 tests/filecheck/flags/check-prefixes.test diff --git a/filecheck/matcher.py b/filecheck/matcher.py index 233cef1..68f7a94 100644 --- a/filecheck/matcher.py +++ b/filecheck/matcher.py @@ -73,8 +73,13 @@ def run(self) -> int: try: ops = tuple(self.operations) if not ops: + # fix plural case for multiple prefixes + if len(self.opts.check_prefixes) == 1: + pref = f"prefix {self.opts.check_prefixes[0]}" + else: + pref = f"prefixes {', '.join(self.opts.check_prefixes)}" print( - f"{ERR}filecheck error:{FMT.RESET} No check strings found with prefix {self.opts.check_prefix}:", + f"{ERR}filecheck error:{FMT.RESET} No check strings found with {pref}:", file=sys.stderr, ) return 2 @@ -112,7 +117,7 @@ def run(self) -> int: # run the post-check one last time to make sure all NOT checks are taken # care of. self.file.range.start = len(self.file.content) - 1 - self._post_check(CheckOp("NOP", "", -1, [])) + self._post_check(CheckOp("SYNTH", "NOP", "", -1, [])) except CheckError as ex: print( f"{self.opts.match_filename}:{ex.op.source_line}: {ERR}error:{FMT.RESET} {ex.message}", @@ -188,7 +193,7 @@ def check_dag(self, op: CheckOp) -> None: match = self.file.match_and_add_hole(pattern) if match is None: raise CheckError( - f"{self.opts.check_prefix}-DAG: Can't find match ('{op.arg}')", + f"{op.check_name}: Can't find match ('{op.arg}')", op, ) self.capture_results(match, capture, op) @@ -206,7 +211,7 @@ def check_not(self, op: CheckOp, search_range: InputRange): pattern, _ = compile_uops(op, self.ctx.live_variables, self.opts) if self.file.find_between(pattern, search_range): raise CheckError( - f"{self.opts.check_prefix}-NOT: excluded string found in input ('{op.arg}')", + f"{op.check_name}: excluded string found in input ('{op.arg}')", op, ) @@ -240,7 +245,7 @@ def check_empty(self, op: CheckOp): self.file.skip_to_end_of_line() if not self.file.starts_with("\n\n"): raise CheckError( - f"{self.opts.check_prefix}-EMPTY: is not on the line after the previous match", + f"{op.check_name}: is not on the line after the previous match", op, ) # consume single newline diff --git a/filecheck/ops.py b/filecheck/ops.py index fa3e89d..2273301 100644 --- a/filecheck/ops.py +++ b/filecheck/ops.py @@ -16,26 +16,31 @@ class CheckOp: Represents a concrete check instruction (e.g. CHECK-NEXT) """ + prefix: str name: str arg: str source_line: int uops: list[UOp] is_literal: bool = field(default=False, kw_only=True) - def check_line_repr(self, prefix: str = "CHECK"): - return f"{prefix}{self._suffix()}: {self.arg}" + def check_line_repr(self): + return f"{self.check_name}: {self.arg}" def source_repr(self, opts: Options) -> str: return ( f"Check rule at {opts.match_filename}:{self.source_line}\n" - f"{self.check_line_repr(opts.check_prefix)}" + f"{self.check_line_repr()}" ) def _suffix(self): suffix = "{LITERAL}" if self.is_literal else "" if self.name == "CHECK": return suffix - return "-" + self.name + suffix + return f"-{self.name}{suffix}" + + @property + def check_name(self): + return f"{self.prefix}{self._suffix()}" @dataclass(slots=True) @@ -48,7 +53,7 @@ class CountOp(CheckOp): def _suffix(self): suffix = "{LITERAL}" if self.is_literal else "" - return f"-COUNT{self.count}" + suffix + return f"-COUNT{self.count}{suffix}" @dataclass(frozen=True, slots=True) diff --git a/filecheck/options.py b/filecheck/options.py index 3e80834..7ca9680 100644 --- a/filecheck/options.py +++ b/filecheck/options.py @@ -12,14 +12,14 @@ class Extension(Enum): class Options: match_filename: str input_file: str = "-" - check_prefix: str = "CHECK" + check_prefixes: list[str] = "CHECK" # type: ignore[reportAssignmentType] + comment_prefixes: list[str] = "COM,RUN" # type: ignore[reportAssignmentType] strict_whitespace: bool = False enable_var_scope: bool = False match_full_lines: bool = False allow_empty: bool = False - comment_prefixes: list[str] = "COM,RUN" # type: ignore[reportAssignmentType] - variables: dict[str, str | int] = field(default_factory=dict) reject_empty_vars: bool = False + variables: dict[str, str | int] = field(default_factory=dict) extensions: set[Extension] = field(default_factory=set) @@ -27,6 +27,8 @@ def __post_init__(self): # make sure we split the comment prefixes if isinstance(self.comment_prefixes, str): self.comment_prefixes = self.comment_prefixes.split(",") + if isinstance(self.check_prefixes, str): + self.check_prefixes = self.check_prefixes.split(",") extensions: set[Extension] = set() for ext in self.extensions: if isinstance(ext, str): @@ -52,6 +54,11 @@ def parse_argv_options(argv: list[str]) -> Options: # pop the name off of argv _ = argv.pop(0) + # create a set of valid fields + valid_fields = set(Options.__dataclass_fields__) + # add check-prefix as another valid field (merged with check-prefixes) + valid_fields.add("check_prefix") + # final options to return opts: dict[str, str | bool] = {} variables: dict[str, str | int] = {} @@ -80,18 +87,28 @@ def parse_argv_options(argv: list[str]) -> Options: arg = arg[1:].replace("-", "_") else: continue - if arg not in Options.__dataclass_fields__: + if arg not in valid_fields: continue remove.add(i) - if Options.__dataclass_fields__[arg].type == bool: + if (f := Options.__dataclass_fields__.get(arg, None)) and f.type == bool: opts[arg] = True else: if i == len(argv) - 1: raise RuntimeError("Out of range arguments") - opts[arg] = argv[i + 1] + # make sure to append check and comment prefixes if flag is used multiple times + if arg in ("check_prefix", "comment_prefixes") and arg in opts: + opts[arg] += "," + argv[i + 1] + else: + opts[arg] = argv[i + 1] remove.add(i + 1) + if "check_prefix" in opts: + prefixes = opts.pop("check_prefix") + if "check_prefixes" in opts: + prefixes += "," + opts.pop("check_prefixes") + opts["check_prefixes"] = prefixes + for idx in sorted(remove, reverse=True): argv.pop(idx) @@ -99,6 +116,8 @@ def parse_argv_options(argv: list[str]) -> Options: raise RuntimeError( f"Unconsumed arguments: {argv}, expected one remaining arg, the match-filename." ) + if len(argv) != 1: + raise RuntimeError(f"Missing argument: check-file") opts["match_filename"] = argv[0] diff --git a/filecheck/parser.py b/filecheck/parser.py index 324ee0c..574a35c 100644 --- a/filecheck/parser.py +++ b/filecheck/parser.py @@ -17,15 +17,11 @@ def pattern_for_opts(opts: Options) -> tuple[re.Pattern[str], re.Pattern[str]]: + prefixes = f"({'|'.join(map(re.escape, opts.check_prefixes))})" return re.compile( - re.escape(opts.check_prefix) + prefixes + r"(-(DAG|COUNT-\d+|NOT|EMPTY|NEXT|SAME|LABEL))?(\{LITERAL})?:\s?([^\n]*)\n?" - ), re.compile( - "(" - + "|".join(map(re.escape, opts.comment_prefixes)) - + ").*" - + re.escape(opts.check_prefix) - ) + ), re.compile(f"({'|'.join(map(re.escape, opts.comment_prefixes))}).*{prefixes}") # see https://llvm.org/docs/CommandGuide/FileCheck.html#filecheck-string-substitution-blocks @@ -90,9 +86,10 @@ def __next__(self) -> CheckOp: # no check line = skip if match is None: continue - kind = match.group(2) - literal = match.group(3) - arg = match.group(4) + prefix = match.group(1) + kind = match.group(3) + literal = match.group(4) + arg = match.group(5) if kind is None: kind = "CHECK" if arg is None: @@ -103,7 +100,7 @@ def __next__(self) -> CheckOp: raise ParseError( f"found empty check string with prefix '{kind}:'", self.line_no, - match.start(3), + match.start(4), line, ) if not self.opts.strict_whitespace: @@ -121,12 +118,14 @@ def __next__(self) -> CheckOp: count = int(kind[6:]) if count == 0: raise ParseError( - f"invalid count in -COUNT specification on prefix '{opts.check_prefix}' (count can't be 0)", + f"invalid count in -COUNT specification on prefix '{prefix}' " + f"(count can't be 0)", self.line_no, match.end(2), line, ) return CountOp( + prefix, "COUNT", arg, self.line_no, @@ -135,7 +134,12 @@ def __next__(self) -> CheckOp: count=count, ) return CheckOp( - kind, arg, self.line_no, uops, is_literal=literal is not None + prefix, + kind, + arg, + self.line_no, + uops, + is_literal=literal is not None, ) def parse_args(self, arg: str, line: str) -> list[UOp]: diff --git a/filecheck/preprocess.py b/filecheck/preprocess.py index f53ed3b..13056e2 100644 --- a/filecheck/preprocess.py +++ b/filecheck/preprocess.py @@ -29,9 +29,7 @@ def preprocess_label(self, op: CheckOp): pattern, _ = compile_uops(op, dict(), self.opts) match = self.input.find_between(pattern, self.range) if not match: - raise CheckError( - f"{self.opts.check_prefix}-LABEL: Could not find label in input", op - ) + raise CheckError(f"{op.check_name}: Could not find label in input", op) self.range = self.range.split_at(match) self.input.ranges.append(self.range) diff --git a/tests/filecheck/flags/check-prefixes.test b/tests/filecheck/flags/check-prefixes.test new file mode 100644 index 0000000..b25a768 --- /dev/null +++ b/tests/filecheck/flags/check-prefixes.test @@ -0,0 +1,12 @@ +// RUN: strip-comments.sh %s | filecheck --check-prefixes=CHECK,CHECK2 %s +// RUN: strip-comments.sh %s | filecheck --check-prefix CHECK --check-prefix CHECK2 %s +// RUN: strip-comments.sh %s | filecheck --check-prefixes=CHECK --check-prefix CHECK2 %s + +test 123 +// CHECK: test +// CHECK2-SAME: 123 +test 456 +// CHECK: test +// CHECK2-SAME: 456 +// CHECK-NOT: bad +// CHECK2-NOT: bad2 From 3666956cbdb90207c65378a13fe967aa508d851c Mon Sep 17 00:00:00 2001 From: Anton Lydike Date: Sun, 14 Jul 2024 14:55:43 +0100 Subject: [PATCH 2/2] little cleanup --- README.md | 6 +++--- filecheck/options.py | 9 +++++++-- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 9866a9f..d0dff25 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ # filecheck - A Python-native clone of LLVMs FileCheck tool -This tries to be as close a clone of LLVMs FileCheck as possible, without going crazy. It currently passes 1555 out of -1645 (94.5%) of LLVMs MLIR filecheck tests. +This tries to be as close a clone of LLVMs FileCheck as possible, without going crazy. It currently passes 1576 out of +1645 (95.8%) of LLVMs MLIR filecheck tests. There are some features that are left out for now (e.g.a [pseudo-numeric variables](https://llvm.org/docs/CommandGuide/FileCheck.html#filecheck-pseudo-numeric-variables) and @@ -25,7 +25,7 @@ Here's an overview of all FileCheck features and their implementation status. - [X] `CHECK-COUNT` - **Flags:** - [X] `--check-prefix` - - [ ] `--check-prefixes` (Tracking: [#13](https://github.com/AntonLydike/filecheck/issues/13)) + - [X] `--check-prefixes` - [X] `--comment-prefixes` - [ ] `--allow-unused-prefixes` - [X] `--input-file` diff --git a/filecheck/options.py b/filecheck/options.py index 7ca9680..db58baf 100644 --- a/filecheck/options.py +++ b/filecheck/options.py @@ -98,15 +98,20 @@ def parse_argv_options(argv: list[str]) -> Options: raise RuntimeError("Out of range arguments") # make sure to append check and comment prefixes if flag is used multiple times if arg in ("check_prefix", "comment_prefixes") and arg in opts: - opts[arg] += "," + argv[i + 1] + existing_opts = opts[arg] + assert isinstance(existing_opts, str) + existing_opts += "," + argv[i + 1] else: opts[arg] = argv[i + 1] remove.add(i + 1) if "check_prefix" in opts: prefixes = opts.pop("check_prefix") + assert isinstance(prefixes, str) if "check_prefixes" in opts: - prefixes += "," + opts.pop("check_prefixes") + more_prefixes = opts.pop("check_prefixes") + assert isinstance(more_prefixes, str) + prefixes += "," + more_prefixes opts["check_prefixes"] = prefixes for idx in sorted(remove, reverse=True):