[Mlir-commits] [llvm] [mlir] [Support] Extract SourceMgrDiagnosticVerifier from MLIR's verifier handler. NFC (PR #227565)
llvmlistbot at llvm.org
llvmlistbot at llvm.org
Sun Oct 18 15:14:24 PDT 2026
llvmorg-github-actions[bot] wrote:
<!--LLVM PR SUMMARY COMMENT-->
@llvm/pr-subscribers-mlir
Author: Alexander Richardson (arichardson)
<details>
<summary>Changes</summary>
Move the SourceMgr-based `expected-(error|warning|remark|note)` diagnostic
parsing and matching logic out of `mlir/lib/IR/Diagnostics.cpp` into a
reusable `llvm::SourceMgrDiagnosticVerifier` class in `LLVMSupport` so that
non-MLIR tools (such as `llvm-mc -verify`) can share the same
implementation.
`mlir::SourceMgrDiagnosticVerifierHandler` is now a thin MLIR-specific
adapter around `llvm::SourceMgrDiagnosticVerifier`.
Assisted-by: AI tooling
---
Patch is 30.22 KiB, truncated to 20.00 KiB below, full version: https://github.com/llvm/llvm-project/pull/227565.diff
5 Files Affected:
- (added) llvm/include/llvm/Support/SourceMgrDiagnosticVerifier.h (+144)
- (modified) llvm/lib/Support/CMakeLists.txt (+1)
- (added) llvm/lib/Support/SourceMgrDiagnosticVerifier.cpp (+255)
- (modified) llvm/utils/gn/secondary/llvm/lib/Support/BUILD.gn (+1)
- (modified) mlir/lib/IR/Diagnostics.cpp (+18-271)
``````````diff
diff --git a/llvm/include/llvm/Support/SourceMgrDiagnosticVerifier.h b/llvm/include/llvm/Support/SourceMgrDiagnosticVerifier.h
new file mode 100644
index 00000000000000..8a5a1e3e1f5dc1
--- /dev/null
+++ b/llvm/include/llvm/Support/SourceMgrDiagnosticVerifier.h
@@ -0,0 +1,144 @@
+//===- SourceMgrDiagnosticVerifier.h ---------------------------*- C++ -*-===//
+//
+// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
+// See https://llvm.org/LICENSE.txt for license information.
+// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+//
+//===----------------------------------------------------------------------===//
+//
+// This file defines a utility for verifying that diagnostics reported through
+// a SourceMgr match 'expected-<kind>' comments in the source, for
+// implementing '-verify'-style diagnostic tests on top of a plain
+// llvm::SourceMgr.
+//
+//===----------------------------------------------------------------------===//
+
+#ifndef LLVM_SUPPORT_SOURCEMGRDIAGNOSTICVERIFIER_H
+#define LLVM_SUPPORT_SOURCEMGRDIAGNOSTICVERIFIER_H
+
+#include "llvm/ADT/ArrayRef.h"
+#include "llvm/ADT/SmallVector.h"
+#include "llvm/ADT/StringMap.h"
+#include "llvm/Support/Compiler.h"
+#include "llvm/Support/Regex.h"
+#include "llvm/Support/SourceMgr.h"
+#include <optional>
+
+namespace llvm {
+
+class MemoryBuffer;
+class raw_ostream;
+
+/// Scans SourceMgr source buffers for 'expected-<kind>' comments (e.g.
+/// 'expected-error {{message}}') and verifies that the diagnostics reported
+/// to it via \c process match them, and that every expected diagnostic is
+/// matched at least once.
+class LLVM_ABI SourceMgrDiagnosticVerifier {
+public:
+ /// A single diagnostic expected via an 'expected-<kind>' comment.
+ struct ExpectedDiag {
+ ExpectedDiag(SourceMgr::DiagKind kind, unsigned lineNo, SMLoc fileLoc,
+ StringRef substring)
+ : kind(kind), lineNo(lineNo), fileLoc(fileLoc), substring(substring) {}
+
+ /// Returns true if this diagnostic matches the given message.
+ LLVM_ABI bool match(StringRef str) const;
+
+ /// Computes the regex matcher for a '-re' diagnostic's substring.
+ /// Returns false and prints a message through \p mgr on error.
+ LLVM_ABI bool computeRegex(raw_ostream &os, SourceMgr &mgr);
+
+ /// Prints \p msg at this diagnostic's location and returns false, for
+ /// use as `return emitError(...);` in functions that report failure via
+ /// a bool return.
+ LLVM_ABI bool emitError(raw_ostream &os, SourceMgr &mgr,
+ const Twine &msg) const;
+
+ /// The severity of the diagnostic expected.
+ SourceMgr::DiagKind kind;
+ /// The line number the expected diagnostic should be on.
+ unsigned lineNo;
+ /// The location of the expected diagnostic within the input file.
+ SMLoc fileLoc;
+ /// A flag indicating if the expected diagnostic has been matched yet.
+ bool matched = false;
+ /// The substring that is expected to be within the diagnostic.
+ StringRef substring;
+ /// An optional regex matcher, if the expected diagnostic substring was a
+ /// regex string.
+ std::optional<Regex> substringRegex;
+ };
+
+ /// The result of matching a single actual diagnostic against the expected
+ /// diagnostics recorded for its location.
+ enum class MatchResult {
+ /// The diagnostic matched an expected diagnostic (kind and text).
+ Matched,
+ /// The diagnostic's text matched an expected diagnostic, but its kind
+ /// didn't; a message about this has already been printed.
+ NearMiss,
+ /// The diagnostic did not match any expected diagnostic. The caller is
+ /// responsible for reporting it, if desired.
+ Unexpected,
+ /// The diagnostic did not match, but \p reportUnexpected was false, so
+ /// nothing was printed and nothing needs to be done.
+ Ignored,
+ };
+
+ SourceMgrDiagnosticVerifier() = default;
+
+ /// Computes and caches the list of expected diagnostics for \p buf, if not
+ /// already cached. Returns the (mutable) cached list.
+ MutableArrayRef<ExpectedDiag> computeExpectedDiags(raw_ostream &os,
+ SourceMgr &mgr,
+ const MemoryBuffer *buf);
+
+ /// Returns the cached expected diagnostics for the buffer named \p bufName,
+ /// or std::nullopt if \p computeExpectedDiags hasn't been called for it.
+ std::optional<MutableArrayRef<ExpectedDiag>>
+ getExpectedDiags(StringRef bufName);
+
+ /// Returns the expected diagnostics with an '@unknown' location.
+ MutableArrayRef<ExpectedDiag> getExpectedUnknownLocDiags() {
+ return expectedUnknownLocDiags;
+ }
+
+ /// Matches a single actual diagnostic against the expected diagnostics
+ /// recorded for \p buf / \p lineNo, computing them first via \p
+ /// computeExpectedDiags if they haven't been already. If \p hasLoc is
+ /// false, the diagnostic has no location and is matched against the
+ /// '@unknown' list instead (\p buf / \p lineNo are ignored). If \p hasLoc
+ /// is true but \p buf is null (e.g. the diagnostic's file isn't a known
+ /// buffer), the diagnostic is matched against an empty list, i.e. it can
+ /// never match and is always unexpected. On a near miss, prints a message
+ /// through \p mgr. \p reportUnexpected controls whether near misses /
+ /// unexpected diagnostics are reported at all.
+ MatchResult process(raw_ostream &os, SourceMgr &mgr, SourceMgr::DiagKind kind,
+ bool hasLoc, const MemoryBuffer *buf, unsigned lineNo,
+ StringRef message, bool reportUnexpected = true);
+
+ /// Reports (through \p mgr) any expected diagnostic that was never matched
+ /// by a call to \p process. Returns whether verification succeeded overall,
+ /// i.e. no diagnostic mismatches were recorded either here or by \p
+ /// process.
+ bool verify(raw_ostream &os, SourceMgr &mgr);
+
+private:
+ /// Regex used to recognize 'expected-<kind>' comments.
+ Regex expected{"expected-(error|note|remark|warning)(-re)? "
+ "*(@([+-][0-9]+|above|below|unknown))? *{{(.*)}}$"};
+
+ /// The expected diagnostics for each buffer that has been scanned so far,
+ /// keyed by buffer identifier (i.e. file name).
+ StringMap<SmallVector<ExpectedDiag, 2>> expectedDiagsPerFile;
+
+ /// The expected diagnostics with an '@unknown' location.
+ SmallVector<ExpectedDiag, 2> expectedUnknownLocDiags;
+
+ /// Whether any diagnostic mismatch has been recorded so far.
+ bool ok = true;
+};
+
+} // namespace llvm
+
+#endif // LLVM_SUPPORT_SOURCEMGRDIAGNOSTICVERIFIER_H
diff --git a/llvm/lib/Support/CMakeLists.txt b/llvm/lib/Support/CMakeLists.txt
index 40c5c392addd58..3d3e27d89ef352 100644
--- a/llvm/lib/Support/CMakeLists.txt
+++ b/llvm/lib/Support/CMakeLists.txt
@@ -271,6 +271,7 @@ add_llvm_component_library(LLVMSupport
SmallPtrSet.cpp
SmallVector.cpp
SourceMgr.cpp
+ SourceMgrDiagnosticVerifier.cpp
SpecialCaseList.cpp
Statistic.cpp
StringExtras.cpp
diff --git a/llvm/lib/Support/SourceMgrDiagnosticVerifier.cpp b/llvm/lib/Support/SourceMgrDiagnosticVerifier.cpp
new file mode 100644
index 00000000000000..1e33637f05bc4a
--- /dev/null
+++ b/llvm/lib/Support/SourceMgrDiagnosticVerifier.cpp
@@ -0,0 +1,255 @@
+//===- SourceMgrDiagnosticVerifier.cpp -----------------------------------===//
+//
+// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
+// See https://llvm.org/LICENSE.txt for license information.
+// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+//
+//===----------------------------------------------------------------------===//
+
+#include "llvm/Support/SourceMgrDiagnosticVerifier.h"
+#include "llvm/Support/MemoryBuffer.h"
+#include "llvm/Support/raw_ostream.h"
+
+using namespace llvm;
+
+/// Given a diagnostic kind, return a human readable string for it.
+static StringRef getDiagKindStr(SourceMgr::DiagKind kind) {
+ switch (kind) {
+ case SourceMgr::DK_Note:
+ return "note";
+ case SourceMgr::DK_Warning:
+ return "warning";
+ case SourceMgr::DK_Error:
+ return "error";
+ case SourceMgr::DK_Remark:
+ return "remark";
+ }
+ llvm_unreachable("Unknown SourceMgr::DiagKind");
+}
+
+bool SourceMgrDiagnosticVerifier::ExpectedDiag::emitError(
+ raw_ostream &os, SourceMgr &mgr, const Twine &msg) const {
+ if (fileLoc.isValid()) {
+ SMRange range(fileLoc, SMLoc::getFromPointer(fileLoc.getPointer() +
+ substring.size()));
+ mgr.PrintMessage(os, fileLoc, SourceMgr::DK_Error, msg, range);
+ } else {
+ mgr.PrintMessage(os, fileLoc, SourceMgr::DK_Error, msg);
+ }
+ return false;
+}
+
+bool SourceMgrDiagnosticVerifier::ExpectedDiag::match(StringRef str) const {
+ // If this isn't a regex diagnostic, we simply check if the string was
+ // contained.
+ if (substringRegex)
+ return substringRegex->match(str);
+ return str.contains(substring);
+}
+
+bool SourceMgrDiagnosticVerifier::ExpectedDiag::computeRegex(raw_ostream &os,
+ SourceMgr &mgr) {
+ std::string regexStr;
+ raw_string_ostream regexOS(regexStr);
+ StringRef strToProcess = substring;
+ while (!strToProcess.empty()) {
+ // Find the next regex block.
+ size_t regexIt = strToProcess.find("{{");
+ if (regexIt == StringRef::npos) {
+ regexOS << Regex::escape(strToProcess);
+ break;
+ }
+ regexOS << Regex::escape(strToProcess.take_front(regexIt));
+ strToProcess = strToProcess.drop_front(regexIt + 2);
+
+ // Find the end of the regex block.
+ size_t regexEndIt = strToProcess.find("}}");
+ if (regexEndIt == StringRef::npos)
+ return emitError(os, mgr, "found start of regex with no end '}}'");
+ StringRef regexBlock = strToProcess.take_front(regexEndIt);
+
+ // Validate that the regex is actually valid.
+ std::string regexError;
+ if (!Regex(regexBlock).isValid(regexError))
+ return emitError(os, mgr, "invalid regex: " + regexError);
+
+ regexOS << '(' << regexBlock << ')';
+ strToProcess = strToProcess.drop_front(regexEndIt + 2);
+ }
+ substringRegex = Regex(regexStr);
+ return true;
+}
+
+MutableArrayRef<SourceMgrDiagnosticVerifier::ExpectedDiag>
+SourceMgrDiagnosticVerifier::computeExpectedDiags(raw_ostream &os,
+ SourceMgr &mgr,
+ const MemoryBuffer *buf) {
+ // If the buffer is invalid, return an empty list.
+ if (!buf)
+ return {};
+ auto &expectedDiags = expectedDiagsPerFile[buf->getBufferIdentifier()];
+
+ // The number of the last line that did not correlate to a designator.
+ unsigned lastNonDesignatorLine = 0;
+
+ // The indices of designators that apply to the next non designator line.
+ SmallVector<unsigned, 1> designatorsForNextLine;
+
+ // Scan the file for expected-* designators.
+ SmallVector<StringRef, 100> lines;
+ buf->getBuffer().split(lines, '\n');
+ for (unsigned lineNo = 0, e = lines.size(); lineNo < e; ++lineNo) {
+ SmallVector<StringRef, 4> matches;
+ if (!expected.match(lines[lineNo].rtrim(), &matches)) {
+ // Check for designators that apply to this line.
+ if (!designatorsForNextLine.empty()) {
+ for (unsigned diagIndex : designatorsForNextLine)
+ expectedDiags[diagIndex].lineNo = lineNo + 1;
+ designatorsForNextLine.clear();
+ }
+ lastNonDesignatorLine = lineNo;
+ continue;
+ }
+
+ // Point to the start of expected-*.
+ SMLoc expectedStart = SMLoc::getFromPointer(matches[0].data());
+
+ SourceMgr::DiagKind kind;
+ if (matches[1] == "error")
+ kind = SourceMgr::DK_Error;
+ else if (matches[1] == "warning")
+ kind = SourceMgr::DK_Warning;
+ else if (matches[1] == "remark")
+ kind = SourceMgr::DK_Remark;
+ else {
+ assert(matches[1] == "note");
+ kind = SourceMgr::DK_Note;
+ }
+ ExpectedDiag record(kind, lineNo + 1, expectedStart, matches[5]);
+
+ // Check to see if this is a regex match, i.e. it includes the `-re`.
+ if (!matches[2].empty() && !record.computeRegex(os, mgr)) {
+ ok = false;
+ continue;
+ }
+
+ StringRef offsetMatch = matches[3];
+ if (!offsetMatch.empty()) {
+ offsetMatch = offsetMatch.drop_front(1);
+
+ // Get the integer value without the @ and +/- prefix.
+ if (offsetMatch[0] == '+' || offsetMatch[0] == '-') {
+ int offset;
+ offsetMatch.drop_front().getAsInteger(0, offset);
+
+ if (offsetMatch.front() == '+')
+ record.lineNo += offset;
+ else
+ record.lineNo -= offset;
+ } else if (offsetMatch.consume_front("unknown")) {
+ // This is matching unknown locations.
+ record.fileLoc = SMLoc();
+ expectedUnknownLocDiags.emplace_back(std::move(record));
+ continue;
+ } else if (offsetMatch.consume_front("above")) {
+ // If the designator applies 'above' we add it to the last non
+ // designator line.
+ record.lineNo = lastNonDesignatorLine + 1;
+ } else {
+ // Otherwise, this is a 'below' designator and applies to the next
+ // non-designator line.
+ assert(offsetMatch.consume_front("below"));
+ designatorsForNextLine.push_back(expectedDiags.size());
+
+ // Set the line number to the last in the case that this designator
+ // ends up dangling.
+ record.lineNo = e;
+ }
+ }
+ expectedDiags.emplace_back(std::move(record));
+ }
+ return expectedDiags;
+}
+
+std::optional<MutableArrayRef<SourceMgrDiagnosticVerifier::ExpectedDiag>>
+SourceMgrDiagnosticVerifier::getExpectedDiags(StringRef bufName) {
+ auto expectedDiags = expectedDiagsPerFile.find(bufName);
+ if (expectedDiags != expectedDiagsPerFile.end())
+ return MutableArrayRef<ExpectedDiag>(expectedDiags->second);
+ return std::nullopt;
+}
+
+SourceMgrDiagnosticVerifier::MatchResult
+SourceMgrDiagnosticVerifier::process(raw_ostream &os, SourceMgr &mgr,
+ SourceMgr::DiagKind kind, bool hasLoc,
+ const MemoryBuffer *buf, unsigned lineNo,
+ StringRef message, bool reportUnexpected) {
+ MutableArrayRef<ExpectedDiag> diags;
+ if (hasLoc) {
+ // If the buffer couldn't be resolved, `diags` stays empty: a diagnostic
+ // with a location in an unknown file can never match anything.
+ if (buf) {
+ if (auto maybeDiags = getExpectedDiags(buf->getBufferIdentifier()))
+ diags = *maybeDiags;
+ else
+ diags = computeExpectedDiags(os, mgr, buf);
+ }
+ } else {
+ diags = expectedUnknownLocDiags;
+ }
+
+ // Search for a matching expected diagnostic.
+ // If we find something that is close then emit a more specific error.
+ ExpectedDiag *nearMiss = nullptr;
+
+ // If this was an expected error, remember that we saw it and return.
+ for (auto &e : diags) {
+ // File line must match (unless it's an unknown location).
+ if (hasLoc && e.lineNo != lineNo)
+ continue;
+ if (e.match(message)) {
+ if (e.kind == kind) {
+ e.matched = true;
+ return MatchResult::Matched;
+ }
+
+ // If this only differs based on the diagnostic kind, then consider it
+ // to be a near miss.
+ nearMiss = &e;
+ }
+ }
+
+ if (!reportUnexpected)
+ return MatchResult::Ignored;
+
+ ok = false;
+
+ // Otherwise, emit an error for the near miss.
+ if (nearMiss) {
+ mgr.PrintMessage(os, nearMiss->fileLoc, SourceMgr::DK_Error,
+ "'" + getDiagKindStr(kind) +
+ "' diagnostic emitted when expecting a '" +
+ getDiagKindStr(nearMiss->kind) + "'");
+ return MatchResult::NearMiss;
+ }
+ return MatchResult::Unexpected;
+}
+
+bool SourceMgrDiagnosticVerifier::verify(raw_ostream &os, SourceMgr &mgr) {
+ // Verify that all expected errors were seen.
+ auto checkExpectedDiags = [&](ExpectedDiag &diag) {
+ if (!diag.matched) {
+ diag.emitError(os, mgr,
+ "expected " + getDiagKindStr(diag.kind) + " \"" +
+ diag.substring + "\" was not produced");
+ ok = false;
+ }
+ };
+ for (auto &expectedDiagsPair : expectedDiagsPerFile)
+ for (auto &diag : expectedDiagsPair.second)
+ checkExpectedDiags(diag);
+ for (auto &diag : expectedUnknownLocDiags)
+ checkExpectedDiags(diag);
+ expectedDiagsPerFile.clear();
+ return ok;
+}
diff --git a/llvm/utils/gn/secondary/llvm/lib/Support/BUILD.gn b/llvm/utils/gn/secondary/llvm/lib/Support/BUILD.gn
index fb7b057acbf7cb..35f364e073a69c 100644
--- a/llvm/utils/gn/secondary/llvm/lib/Support/BUILD.gn
+++ b/llvm/utils/gn/secondary/llvm/lib/Support/BUILD.gn
@@ -153,6 +153,7 @@ static_library("Support") {
"SmallPtrSet.cpp",
"SmallVector.cpp",
"SourceMgr.cpp",
+ "SourceMgrDiagnosticVerifier.cpp",
"SpecialCaseList.cpp",
"Statistic.cpp",
"StringExtras.cpp",
diff --git a/mlir/lib/IR/Diagnostics.cpp b/mlir/lib/IR/Diagnostics.cpp
index a3ddb76a66313c..ae678e1c44dfe0 100644
--- a/mlir/lib/IR/Diagnostics.cpp
+++ b/mlir/lib/IR/Diagnostics.cpp
@@ -21,6 +21,7 @@
#include "llvm/Support/Regex.h"
#include "llvm/Support/Signals.h"
#include "llvm/Support/SourceMgr.h"
+#include "llvm/Support/SourceMgrDiagnosticVerifier.h"
#include "llvm/Support/raw_ostream.h"
#include <optional>
@@ -637,122 +638,17 @@ SMLoc SourceMgrDiagnosticHandler::convertLocToSMLoc(FileLineColLoc loc) {
namespace mlir {
namespace detail {
-/// This class represents an expected output diagnostic.
-struct ExpectedDiag {
- ExpectedDiag(DiagnosticSeverity kind, unsigned lineNo, SMLoc fileLoc,
- StringRef substring)
- : kind(kind), lineNo(lineNo), fileLoc(fileLoc), substring(substring) {}
-
- /// Emit an error at the location referenced by this diagnostic.
- LogicalResult emitError(raw_ostream &os, llvm::SourceMgr &mgr,
- const Twine &msg) {
- // fileLoc may be invalid when the expected diagnostic used an unknown
- // location specifier (e.g. `// expected-error @unknown {{...}}`). In that
- // case, skip the source range to avoid a null-pointer dereference and an
- // assertion in SMRange that both endpoints must have the same validity.
- if (fileLoc.isValid()) {
- SMRange range(fileLoc, SMLoc::getFromPointer(fileLoc.getPointer() +
- substring.size()));
- mgr.PrintMessage(os, fileLoc, llvm::SourceMgr::DK_Error, msg, range);
- } else {
- mgr.PrintMessage(os, fileLoc, llvm::SourceMgr::DK_Error, msg);
- }
- return failure();
- }
-
- /// Returns true if this diagnostic matches the given string.
- bool match(StringRef str) const {
- // If this isn't a regex diagnostic, we simply check if the string was
- // contained.
- if (substringRegex)
- return substringRegex->match(str);
- return str.contains(substring);
- }
-
- /// Compute the regex matcher for this diagnostic, using the provided stream
- /// and manager to emit diagnostics as necessary.
- LogicalResult computeRegex(raw_ostream &os, llvm::SourceMgr &mgr) {
- std::string regexStr;
- llvm::raw_string_ostream regexOS(regexStr);
- StringRef strToProcess = substring;
- while (!strToProcess.empty()) {
- // Find the next regex block.
- size_t regexIt = strToProcess.find("{{");
- if (regexIt == StringRef::npos) {
- regexOS << llvm::Regex::escape(strToProcess);
- break;
- }
- regexOS << llvm::Regex::escape(strToProcess.take_front(regexIt));
- strToProcess = strToProcess.drop_front(regexIt + 2);
-
- // Find the end of the regex block.
- size_t regexEndIt = strToProcess.find("}}");
- if (regexEndIt == StringRef::npos)
- return emitError(os, mgr, "found start of regex with no end '}}'");
- StringRef regexStr = strToProcess.take_front(regexEndIt);
-
- // Validate that the regex is actually valid.
- std::string regexError;
- if (!llvm::Regex(regexStr).isValid(regexError))
- return emitError(os, mgr, "invalid regex: " + regexError);
-
- regexOS << '(' << regexStr << ')';
- strToProcess = strToProcess.drop_front(regexEndIt + 2);
- }
- substr...
[truncated]
``````````
</details>
https://github.com/llvm/llvm-project/pull/227565
More information about the Mlir-commits
mailing list