Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for --check-prefixes (fixes #13) #18

Merged
merged 2 commits into from
Jul 14, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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`
Expand Down
15 changes: 10 additions & 5 deletions filecheck/matcher.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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}",
Expand Down Expand Up @@ -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)
Expand All @@ -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,
)

Expand Down Expand Up @@ -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
Expand Down
15 changes: 10 additions & 5 deletions filecheck/ops.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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)
Expand Down
36 changes: 30 additions & 6 deletions filecheck/options.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,21 +12,23 @@ 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)

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):
Expand All @@ -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] = {}
Expand Down Expand Up @@ -80,25 +87,42 @@ 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:
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:
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):
argv.pop(idx)

if len(argv) > 1:
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]

Expand Down
30 changes: 17 additions & 13 deletions filecheck/parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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:
Expand All @@ -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:
Expand All @@ -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,
Expand All @@ -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]:
Expand Down
4 changes: 1 addition & 3 deletions filecheck/preprocess.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
12 changes: 12 additions & 0 deletions tests/filecheck/flags/check-prefixes.test
Original file line number Diff line number Diff line change
@@ -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
Loading