blob: 1bcf834cd08fcd9da0cc60442a1a3e9ecfd020b1 [file] [edit]
#!/usr/bin/env python3
"""Flag direct LLVM API calls that have an Enzyme wrapper in Utils.h.
Enzyme builds against every LLVM release from 15 to main, so a handful of LLVM
APIs cannot be called directly: either their signature changed across that
range, or their contract did. Utils.h wraps each of those. Calling the LLVM API
directly compiles fine against whichever LLVM the author happened to have, and
breaks -- often only at runtime, under an assertion -- against the others.
Two such APIs are checked here.
`BasicBlock::getTerminator()` used as a null test
-------------------------------------------------
if (BB->getTerminator()) // wrong
B.SetInsertPoint(BB->getTerminator());
Through LLVM 22 `getTerminator()` returned null for a block that has none, so
this reads as "does this block have a terminator". As of LLVM 23 it asserts
instead:
Assertion `hasTerminator() && "cannot get terminator of non-well-formed
block"' failed.
so the test crashes on exactly the input it was written to handle. Use the
`hasTerminator()` helper from Utils.h, which is the version-correct spelling:
if (hasTerminator(BB)) // right
B.SetInsertPoint(BB->getTerminator());
Only *null tests* are flagged. `dyn_cast<BranchInst>(BB->getTerminator())` and
friends are fine -- those assume a terminator exists and test its type, which
is the normal assumption for a well-formed block.
`PointerType::get` / `PointerType::getUnqual`
---------------------------------------------
PointerType::get(T, AS) // typed pointers, gone in LLVM 17
The element-type overload was removed when typed pointers were. `Utils.h`
provides `getPointerType(T, AS)`, which picks the right spelling per version,
plus `getUnqual(T)` and `getInt8PtrTy(Ctx, AS)`. Call those instead of the LLVM
API.
Rebuilding a pointer type only to move it to another address space has its own
helper, `changePointerAddrSpace(PT, AS)`. Prefer it: reaching for
`getInt8PtrTy(Ctx, AS)` there happens to produce the right type, but it reads
as "make an i8*", so a genuine loss of the pointee type is indistinguishable
from a deliberate opaque pointer.
MLIR's unrelated `LLVM::LLVMPointerType::get` is not affected and not flagged.
`BasicBlock::getFirstNonPHI` and friends
----------------------------------------
BB->getFirstNonPHI() // removed on LLVM main
LLVM removed the instruction-returning `getFirstNonPHI` (llvm-project
62c5ede9fd14), leaving the iterator-flavoured `getFirstNonPHIIt` -- which in
turn does not exist before LLVM 18. `getFirstNonPHIOrDbg` changed its return
type to an iterator in LLVM 20 the same way. Utils.h wraps both; call the
free functions `getFirstNonPHI(BB)` / `getFirstNonPHIOrDbg(BB)` instead of
any member spelling.
Suppress a false positive with `// NOLINT(<check>)` on the flagged line, or
`// NOLINTNEXTLINE(<check>)` on the line before it, where `<check>` is
`terminator-null-test`, `enzyme-pointer-type` or `enzyme-first-non-phi`.
"""
import argparse
import os
import re
import sys
SOURCE_SUFFIXES = (".cpp", ".cc", ".h", ".hpp")
SKIP_DIRS = {".git", "build", "third_party", "external"}
# `X->getTerminator()` / `X.getTerminator()` with the result used as a boolean:
# if (BB->getTerminator()) if (!BB->getTerminator())
# if (auto T = BB->getTerminator()) BB->getTerminator() ? a : b
# BB->getTerminator() == nullptr nullptr != BB->getTerminator()
# A receiver is a chain of segments like `BB->`, `blocks[0]->`, `foo()->`,
# `A::B.` -- crucially it may not contain a bare `(`, or the pattern would
# reach across an enclosing call's argument list and flag argument uses such
# as `isAnyBranch(foo->getTerminator())` as null tests.
RECEIVER = r"(?:[*&]?(?:\w|::)+(?:\(\))?(?:\[[^\]()]*\])?(?:->|\.))*"
TERMINATOR_NULL_TEST = re.compile(
r"if\s*\(\s*!?\s*" + RECEIVER + r"getTerminator\s*\(\s*\)\s*\)"
r"|if\s*\(\s*(?:auto|const\s+auto|[\w:]+\s*\*)\s*\*?\s*\w+\s*=\s*"
+ RECEIVER
+ r"getTerminator\s*\(\s*\)\s*\)"
r"|getTerminator\s*\(\s*\)\s*(?:\?|[=!]=\s*(?:nullptr|NULL))"
r"|(?:nullptr|NULL)\s*[=!]=\s*" + RECEIVER + r"getTerminator\s*\(\s*\)"
)
# PointerType::get(...) / PointerType::getUnqual(...), but not MLIR's
# LLVM::LLVMPointerType::get.
POINTER_TYPE = re.compile(
r"(?<![\w:])(?:llvm::)?PointerType::(?:get|getUnqual)\s*\("
)
# Member-call spellings of the getFirstNonPHI family. The Utils.h wrappers are
# free functions, so `getFirstNonPHI(BB)` does not match: only `X->` / `X.`
# calls do. `getFirstNonPHIOrDbgOrLifetime` is not matched (the `\s*\(` must
# follow one of the listed suffixes immediately).
FIRST_NON_PHI = re.compile(r"(?:->|\.)\s*getFirstNonPHI(?:It|OrDbg)?\s*\(")
CHECKS = [
(
"terminator-null-test",
TERMINATOR_NULL_TEST,
"getTerminator() used as a null test; this returns null only through "
"LLVM 22 and asserts from LLVM 23 on. Use hasTerminator(BB) from "
"Utils.h.",
),
(
"enzyme-first-non-phi",
FIRST_NON_PHI,
"getFirstNonPHI/getFirstNonPHIIt/getFirstNonPHIOrDbg called as a "
"member; no one spelling exists on every supported LLVM. Use the "
"free functions getFirstNonPHI(BB) / getFirstNonPHIOrDbg(BB) from "
"Utils.h.",
),
(
"enzyme-pointer-type",
POINTER_TYPE,
"PointerType::get/getUnqual called directly; the element-type overload "
"does not exist on every supported LLVM. Use getPointerType/getUnqual/"
"getInt8PtrTy from Utils.h, or changePointerAddrSpace() if you are "
"only moving a pointer to another address space.",
),
]
def strip_noise(src):
"""Blank out comments, strings and char literals, preserving offsets."""
out = list(src)
i, n = 0, len(src)
while i < n:
c = src[i]
if c == "/" and i + 1 < n and src[i + 1] == "/":
while i < n and src[i] != "\n":
out[i] = " "
i += 1
elif c == "/" and i + 1 < n and src[i + 1] == "*":
while i < n and not (src[i] == "*" and i + 1 < n and src[i + 1] == "/"):
if src[i] != "\n":
out[i] = " "
i += 1
for j in range(i, min(i + 2, n)):
out[j] = " "
i += 2
elif c in "\"'":
quote = c
i += 1
while i < n and src[i] != quote:
if src[i] == "\\":
out[i] = " "
i += 1
if i < n:
if src[i] != "\n":
out[i] = " "
i += 1
if i < n:
out[i] = " "
i += 1
else:
i += 1
return "".join(out)
def iter_sources(roots):
for root in roots:
if os.path.isfile(root):
yield root
continue
for dirpath, dirnames, filenames in os.walk(root):
dirnames[:] = [
d for d in dirnames if d not in SKIP_DIRS and not d.startswith("bazel-")
]
for name in sorted(filenames):
if name.endswith(SOURCE_SUFFIXES):
yield os.path.join(dirpath, name)
def suppressed(raw_lines, idx, check):
"""True if line `idx` (0-based) carries a NOLINT for `check`."""
same = re.compile(r"NOLINT\(\s*" + re.escape(check) + r"\s*\)")
prev = re.compile(r"NOLINTNEXTLINE\(\s*" + re.escape(check) + r"\s*\)")
if same.search(raw_lines[idx]):
return True
return idx > 0 and prev.search(raw_lines[idx - 1])
def main():
parser = argparse.ArgumentParser(
description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter
)
parser.add_argument("roots", nargs="+", help="directories or files to check")
parser.add_argument(
"--github",
action="store_true",
help="also emit GitHub Actions error annotations",
)
args = parser.parse_args()
files = list(iter_sources(args.roots))
errors = 0
for path in files:
raw = open(path, encoding="utf-8", errors="replace").read()
raw_lines = raw.splitlines()
# Search the comment- and string-stripped text so that prose and
# NOLINT markers cannot themselves trip a check, but report the
# original line so the message shows the real code.
lines = strip_noise(raw).splitlines()
for check, pattern, msg in CHECKS:
for idx, line in enumerate(lines):
if not pattern.search(line):
continue
if suppressed(raw_lines, idx, check):
continue
errors += 1
lineno = idx + 1
print(f"{path}:{lineno}: error: {msg}\n {raw_lines[idx].strip()}")
if args.github:
print(
f"::error file={path},line={lineno},"
f"title=Direct LLVM API use ({check})::{msg}"
)
if errors:
print(
f"\n{errors} direct use(s) of an LLVM API that Utils.h wraps. Use "
f"the helper, or add `// NOLINT(<check>)` if the direct call is "
f"provably correct on every supported LLVM.",
file=sys.stderr,
)
return 1
print(f"checked {len(files)} files: no direct uses of wrapped LLVM APIs")
return 0
if __name__ == "__main__":
sys.exit(main())