Skip to content
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
3 changes: 2 additions & 1 deletion SConstruct
Original file line number Diff line number Diff line change
Expand Up @@ -1527,11 +1527,12 @@ def CommandTestFileCheck(env, name, cmd, check_file):
of stdout will then be piped to the file_check.py tool which
will search for the regexes specified in |check_file|. """

env = env.Clone()
env['ENV']['PYTHONPATH'] = Dir('#/src/third_party/python-filecheck').abspath
return env.CommandTest(
name,
['${PYTHON}',
env.File('${SCONSTRUCT_DIR}/tools/llvm_file_check_wrapper.py'),
'${FILECHECK}',
check_file] + cmd,
direct_emulation=False)

Expand Down
13 changes: 13 additions & 0 deletions src/third_party/python-filecheck/LICENSE.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
Copyright 2024 Anton Lydike

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
127 changes: 127 additions & 0 deletions src/third_party/python-filecheck/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
# filecheck - A Python-native clone of LLVMs FileCheck tool

**Note:** This project is the successor of [mull-project/FileCheck.py](https://github.com/mull-project/FileCheck.py).

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. We are tracking all 69 remaining test failures in GitHub issues.

There are some features that are left out for now (e.g. parts of
[numeric substitution](https://llvm.org/docs/CommandGuide/FileCheck.html#filecheck-numeric-substitution-blocks)
).

The codebase is fully type checked by `pyright`, and automatically formatted using `black`. We aim to have tests
covering everything from normal matching down to error messages.

Install by running `pip install filecheck`.

## Features:
Here's an overview of all FileCheck features and their implementation status.

- **Checks:**
- [X] `CHECK`
- [X] `CHECK-NEXT`
- [X] `CHECK-NOT` (Bug: [#10](https://github.com/AntonLydike/filecheck/issues/10))
- [X] `CHECK-LABEL` (Bug: [#8](https://github.com/AntonLydike/filecheck/issues/8))
- [X] `CHECK-EMPTY`
- [X] `CHECK-SAME`
- [X] `CHECK-DAG`
- [X] `CHECK-COUNT`
- **Flags:**
- [X] `--check-prefix`
- [X] `--check-prefixes`
- [X] `--comment-prefixes`
- [ ] `--allow-unused-prefixes`
- [X] `--input-file`
- [X] `--match-full-lines`
- [X] `--strict-whitespace` (Bug: [#6](https://github.com/AntonLydike/filecheck/issues/6))
- [ ] `--ignore-case`
- [ ] `--implicit-check-not` (Tracked: [#20](https://github.com/AntonLydike/filecheck/issues/20))
- [X] `--dump-input` (only `fail` and `never` supported)
- [ ] `--dump-input-context`
- [ ] `--dump-input-filter`
- [X] `--enable-var-scope`
- [X] `-D<VAR=VALUE>`
- [ ] `-D#<FMT>,<NUMVAR>=<NUMERIC EXPRESSION>`
- [X] `-version`
- [ ] `-v`
- [ ] `-vv`
- [ ] `--allow-deprecated-dag-overlap`
- [X] `--allow-empty`
- [ ] `--color` Colored output is supported and automatically detected. No support for the flag.
- **Base Features:**
- [X] Regex patterns
- [X] Captures and Capture Matches (Bug: [#11](https://github.com/AntonLydike/filecheck/issues/11))
- [X] Numeric Captures
- [ ] Numeric Substitutions (jesus christ, wtf man) (Tracked: [#45](https://github.com/AntonLydike/filecheck/issues/45))
- [X] Literal matching (`CHECK{LITERAL}`)
- [X] Weird regex features (`[:xdigits:]` and friends)
- [X] Correct(?) handling of matching check lines (Bug: [#22](https://github.com/AntonLydike/filecheck/issues/22))
- **Testing:**
- [X] Base cases
- [X] Negative tests
- [ ] Error messages (started)
- [ ] Lots of edge cases
- [ ] MLIR/xDSL integration tests
- **UX:**
- Good error messages: Error messages are on an okay level, not great, but not terrible either.
- [X] Parse errors
- [X] Matching errors
- [X] Print possible intended matches (could be better still)
- [X] Malformed regexes
- [ ] Wrong/unkown command line arguments
- [ ] Print variables and their origin in error messages
- **Infrastructure:**
- [X] Formatting: black
- [X] Pyright
- [X] `pre-commit`
- [X] CI for everything

We are open to PRs for bugfixes or any features listed here.

## Differences to LLVMs FileCheck:
We want to be as close as possible to the original FileCheck, and document our differences very clearly.

If you encounter a difference that is not documented here, feel free to file a bug report.

### Better Regexes:
We use pythons regex library, which is a flavour of a Perl compatible regular expression (PCRE), instead of FileChecks
POSIX regex flavour.

**Example:**
```
// LLVM filecheck:
// CHECK: %{{[[:alnum:]]+}}, %{{[[:digit:]]+}}

// our fileheck:
// CHECK: %{{[a-zA-Z0-9]+}}, %{{\d+}}
```

Some effort is made to translate character classes from POSIX to PCRE, although it might be wrong in edge cases.

### Relaxed Matching:

We relax some of the matching rules, like:

- Allow a file to start with `CHECK-NEXT`


### No Numerical Substitution:

This is used in 2 out of 1645 tests in our benchmark (upstream MLIR tests).

While our filecheck supports [numeric capture](https://llvm.org/docs/CommandGuide/FileCheck.html#filecheck-numeric-substitution-blocks)
(`[[#%.3x,VAR:]]` will capture a three-digit hex number), we don't support arithmetic expressions on these captured
values at the moment.

### Special Feature Flags:

This version of filecheck implements some non-standard extensions to LLVMs filecheck. These are disabled by default but
can be enabled through the environment variable `FILECHECK_FEATURE_ENABLE=...`. Avialable extensions are documented here:

- `MLIR_REGEX_CLS`: Add additional special regex matchers to match MLIR/LLVM constructs:
- `\V` will match any SSA value name

### Reject Empty Captures:

We introduce a new flag called `reject-empty-vars` that throws an error when a capture expression captures an empty
string.
Empty file.
4 changes: 4 additions & 0 deletions src/third_party/python-filecheck/filecheck/__main__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
from filecheck.main import main

if __name__ == "__main__":
main()
46 changes: 46 additions & 0 deletions src/third_party/python-filecheck/filecheck/colors.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import sys
from enum import Flag, auto

COLOR_SUPPORT = hasattr(sys.stdout, "isatty") and sys.stdout.isatty()


class FMT(Flag):
RED = auto()
BLUE = auto()
YELLOW = auto()
GREEN = auto()
ORANGE = auto()
BOLD = auto()
GRAY = auto()
UNDERLINE = auto()
RESET = auto()

def __str__(self) -> str:
if not COLOR_SUPPORT:
return ""
fmt_str: list[str] = []

if FMT.RED in self:
fmt_str.append("\033[31m")
if FMT.ORANGE in self:
fmt_str.append("\033[33m")
if FMT.GRAY in self:
fmt_str.append("\033[37m")
if FMT.GREEN in self:
fmt_str.append("\033[32m")
if FMT.BLUE in self:
fmt_str.append("\033[34m")
if FMT.YELLOW in self:
fmt_str.append("\033[93m")
if FMT.BOLD in self:
fmt_str.append("\033[1m")
if FMT.RESET in self:
fmt_str.append("\033[0m")
if FMT.UNDERLINE in self:
fmt_str.append("\033[4m")

return "".join(fmt_str)


WARN = FMT.ORANGE | FMT.UNDERLINE
ERR = FMT.RED | FMT.BOLD
100 changes: 100 additions & 0 deletions src/third_party/python-filecheck/filecheck/compiler.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
import re

from filecheck.error import CheckError
from filecheck.ops import (
Literal,
RE,
Capture,
NumSubst,
PseudoVar,
Subst,
CheckOp,
VALUE_MAPPER_T,
)
from filecheck.options import Options

UNESCAPED_BRACKETS = re.compile(r"^\(|[^\\]\(")

CHECK_EMPTY_EXPR = re.compile(r"[^\n]*\n\n")


def compile_uops(
check: CheckOp, variables: dict[str, str | int], opts: Options
) -> tuple[re.Pattern[str], dict[str, tuple[int, VALUE_MAPPER_T]]]:
"""
Compile a series of uops, given a set of variables, to a regex pattern and an
extraction dictionary.

The extraction dictionary tells you which Capture can be found in which regex
group.
"""
groups = 0
expr: list[str] = []
if check.name == "NEXT":
expr.append(r"\n?[^\n]*")
elif check.name == "EMPTY":
return CHECK_EMPTY_EXPR, dict()

captures: dict[str, tuple[int, VALUE_MAPPER_T]] = dict()

for uop in check.uops:
if isinstance(uop, Literal):
# literals are matched as is
if opts.strict_whitespace:
expr.append(re.escape(uop.content))
else:
# TODO: fix this mess
# basically, I need to replace all whitespaces in the original literal by "\s+"
# but I still want to regex escape them
# and re.sub doesn't let me insert \s into the new string for some reason......
expr.append(
re.sub(r"(\\ )+", " ", re.escape(uop.content)).replace(
" ", r"[ \t\v]+"
),
)

elif isinstance(uop, RE):
# For regexes, we must make sure that we count the number of capture groups
# present, so that we know which ones contain the Captures, and which ones
# contain "user supplied" groups.

# count number of capture groups by counting unescaped brackets
groups += len(UNESCAPED_BRACKETS.findall(uop.content))
# add brackets sorrounding patterns that contain ORs (fixes #3)
if "|" in uop.content:
expr.append(f"({uop.content})")
groups += 1
else:
expr.append(uop.content)
elif isinstance(uop, Capture):
# record the group we capture in the dictionary
captures[uop.name] = (groups + 1, uop.value_mapper)
# this is used to match same-line use of variables
# like this:
# // CHECK: alloc [[REG:[a-z]+]], [[REG]]
# here we can't insert the value now, as it will only be determined when we
# match the pattern. Luckily, python regex has backwards references.
expr.append(f"({uop.pattern})")
groups += len(UNESCAPED_BRACKETS.findall(uop.pattern)) + 1
elif isinstance(uop, Subst):
# if we have substitutions, check if the variable is defined in this line
if uop.variable in captures:
expr.append(f"\\{captures[uop.variable][0]}")
else:
# otherwise match immediate
if uop.variable not in variables:
raise CheckError(
f"Variable {uop.variable} referenced before assignment",
check,
)
expr.append(re.escape(str(variables[uop.variable])))
elif isinstance(uop, PseudoVar):
expr.append(f"{check.source_line + uop.offset}")
elif isinstance(uop, NumSubst):
# we don't do numerical substitutions yet
raise NotImplementedError("Numerical substitutions not supported!")
try:
# compile with MULTILINE flag, so that `^` and `$` can match start/end of line correctly
return re.compile("".join(expr), flags=re.MULTILINE), captures
except re.error:
raise CheckError(f"Malformed regex expression: '{''.join(expr)}'", check)
38 changes: 38 additions & 0 deletions src/third_party/python-filecheck/filecheck/error.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import re
from dataclasses import dataclass
from typing import Any

from filecheck.ops import CheckOp


class CheckError(Exception):
message: str
op: CheckOp

def __init__(self, msg: str, op: CheckOp, *args: Any):
super().__init__(*args)
self.message = msg
self.op = op


@dataclass
class ErrorOnMatch(Exception):
"""
Signal error on the provided match
"""

message: str
op: CheckOp
match: re.Match[str]


@dataclass
class ParseError(Exception):
"""
Signal an error during parsing
"""

message: str
line_no: int
offset: int
offending_line: str
Loading