diff --git a/mlir/docs/Diagnostics.md b/mlir/docs/Diagnostics.md --- a/mlir/docs/Diagnostics.md +++ b/mlir/docs/Diagnostics.md @@ -300,9 +300,13 @@ certain diagnostics have been emitted to the context. To use this handler, annotate your source file with expected diagnostics in the form of: -* `expected-(error|note|remark|warning) {{ message }}` +* `expected-(error|note|remark|warning)(-re)? {{ message }}` -A few examples are shown below: +The provided `message` is a string expected to be contained within the generated +diagnostic. The `-re` suffix may be used to enable regex matching within the +`message`. When present, the `message` may define regex match sequences within +`{{` `}}` blocks. The regular expression matcher supports Extended POSIX regular +expressions (ERE). A few examples are shown below: ```mlir // Expect an error on the same line. @@ -327,6 +331,12 @@ // expected-remark@above {{remark on function above}} // expected-remark@above {{another remark on function above}} +// Expect an error mentioning the parent function, but use regex to avoid +// hardcoding the name. +func.func @foo() -> i32 { + // expected-error-re@+1 {{'func.return' op has 0 operands, but enclosing function (@{{.*}}) returns 1}} + return +} ``` The handler will report an error if any unexpected diagnostics were seen, or if diff --git a/mlir/lib/IR/Diagnostics.cpp b/mlir/lib/IR/Diagnostics.cpp --- a/mlir/lib/IR/Diagnostics.cpp +++ b/mlir/lib/IR/Diagnostics.cpp @@ -590,14 +590,77 @@ namespace mlir { namespace detail { -// Record the expected diagnostic's position, substring and whether it was -// seen. +/// 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) { + SMRange range(fileLoc, SMLoc::getFromPointer(fileLoc.getPointer() + + substring.size())); + mgr.PrintMessage(os, fileLoc, llvm::SourceMgr::DK_Error, msg, range); + 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); + } + substringRegex = llvm::Regex(regexOS.str()); + return success(); + } + + /// The severity of the diagnosic expected. DiagnosticSeverity kind; + /// The line number the expected diagnostic should be on. unsigned lineNo; - StringRef substring; + /// The location of the expected diagnostic within the input file. SMLoc fileLoc; - bool matched; + /// 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 sub-string was a + /// regex string. + Optional substringRegex; }; struct SourceMgrDiagnosticVerifierHandlerImpl { @@ -608,7 +671,8 @@ /// Computes the expected diagnostics for the given source buffer. MutableArrayRef - computeExpectedDiags(const llvm::MemoryBuffer *buf); + computeExpectedDiags(raw_ostream &os, llvm::SourceMgr &mgr, + const llvm::MemoryBuffer *buf); /// The current status of the verifier. LogicalResult status; @@ -617,8 +681,9 @@ llvm::StringMap> expectedDiagsPerFile; /// Regex to match the expected diagnostics format. - llvm::Regex expected = llvm::Regex("expected-(error|note|remark|warning) " - "*(@([+-][0-9]+|above|below))? *{{(.*)}}"); + llvm::Regex expected = + llvm::Regex("expected-(error|note|remark|warning)(-re)? " + "*(@([+-][0-9]+|above|below))? *{{(.*)}}$"); }; } // namespace detail } // namespace mlir @@ -638,7 +703,6 @@ llvm_unreachable("Unknown DiagnosticSeverity"); } -/// Returns the expected diagnostics for the given source file. Optional> SourceMgrDiagnosticVerifierHandlerImpl::getExpectedDiags(StringRef bufName) { auto expectedDiags = expectedDiagsPerFile.find(bufName); @@ -647,10 +711,9 @@ return llvm::None; } -/// Computes the expected diagnostics for the given source buffer. MutableArrayRef SourceMgrDiagnosticVerifierHandlerImpl::computeExpectedDiags( - const llvm::MemoryBuffer *buf) { + raw_ostream &os, llvm::SourceMgr &mgr, const llvm::MemoryBuffer *buf) { // If the buffer is invalid, return an empty list. if (!buf) return llvm::None; @@ -667,7 +730,7 @@ buf->getBuffer().split(lines, '\n'); for (unsigned lineNo = 0, e = lines.size(); lineNo < e; ++lineNo) { SmallVector matches; - if (!expected.match(lines[lineNo], &matches)) { + if (!expected.match(lines[lineNo].rtrim(), &matches)) { // Check for designators that apply to this line. if (!designatorsForNextLine.empty()) { for (unsigned diagIndex : designatorsForNextLine) @@ -679,7 +742,7 @@ } // Point to the start of expected-*. - auto expectedStart = SMLoc::getFromPointer(matches[0].data()); + SMLoc expectedStart = SMLoc::getFromPointer(matches[0].data()); DiagnosticSeverity kind; if (matches[1] == "error") @@ -692,9 +755,15 @@ assert(matches[1] == "note"); kind = DiagnosticSeverity::Note; } + ExpectedDiag record(kind, lineNo + 1, expectedStart, matches[5]); - ExpectedDiag record{kind, lineNo + 1, matches[4], expectedStart, false}; - auto offsetMatch = matches[2]; + // Check to see if this is a regex match, i.e. it includes the `-re`. + if (!matches[2].empty() && failed(record.computeRegex(os, mgr))) { + status = failure(); + continue; + } + + StringRef offsetMatch = matches[3]; if (!offsetMatch.empty()) { offsetMatch = offsetMatch.drop_front(1); @@ -722,7 +791,7 @@ record.lineNo = e; } } - expectedDiags.push_back(record); + expectedDiags.emplace_back(std::move(record)); } return expectedDiags; } @@ -734,7 +803,7 @@ // Compute the expected diagnostics for each of the current files in the // source manager. for (unsigned i = 0, e = mgr.getNumBuffers(); i != e; ++i) - (void)impl->computeExpectedDiags(mgr.getMemoryBuffer(i + 1)); + (void)impl->computeExpectedDiags(out, mgr, mgr.getMemoryBuffer(i + 1)); // Register a handler to verify the diagnostics. setHandler([&](Diagnostic &diag) { @@ -765,14 +834,10 @@ for (auto &err : expectedDiagsPair.second) { if (err.matched) continue; - SMRange range(err.fileLoc, - SMLoc::getFromPointer(err.fileLoc.getPointer() + - err.substring.size())); - mgr.PrintMessage(os, err.fileLoc, llvm::SourceMgr::DK_Error, - "expected " + getDiagKindStr(err.kind) + " \"" + - err.substring + "\" was not produced", - range); - impl->status = failure(); + impl->status = + err.emitError(os, mgr, + "expected " + getDiagKindStr(err.kind) + " \"" + + err.substring + "\" was not produced"); } } impl->expectedDiagsPerFile.clear(); @@ -799,8 +864,10 @@ DiagnosticSeverity kind) { // Get the expected diagnostics for this file. auto diags = impl->getExpectedDiags(loc.getFilename()); - if (!diags) - diags = impl->computeExpectedDiags(getBufferForFile(loc.getFilename())); + if (!diags) { + diags = impl->computeExpectedDiags(os, mgr, + getBufferForFile(loc.getFilename())); + } // Search for a matching expected diagnostic. // If we find something that is close then emit a more specific error. @@ -809,7 +876,7 @@ // If this was an expected error, remember that we saw it and return. unsigned line = loc.getLine(); for (auto &e : *diags) { - if (line == e.lineNo && msg.contains(e.substring)) { + if (line == e.lineNo && e.match(msg)) { if (e.kind == kind) { e.matched = true; return; diff --git a/mlir/test/Dialect/SPIRV/IR/memory-ops.mlir b/mlir/test/Dialect/SPIRV/IR/memory-ops.mlir --- a/mlir/test/Dialect/SPIRV/IR/memory-ops.mlir +++ b/mlir/test/Dialect/SPIRV/IR/memory-ops.mlir @@ -405,7 +405,7 @@ func.func @simple_store_missing_operand(%arg0 : f32) -> () { %0 = spv.Variable : !spv.ptr - // expected-error @+1 {{custom op 'spv.Store' expected 2 operands}} : f32 + // expected-error @+1 {{custom op 'spv.Store' expected 2 operands}} spv.Store "Function" %0 : f32 return } diff --git a/mlir/test/IR/diagnostic-handler-verify-regex.mlir b/mlir/test/IR/diagnostic-handler-verify-regex.mlir new file mode 100644 --- /dev/null +++ b/mlir/test/IR/diagnostic-handler-verify-regex.mlir @@ -0,0 +1,16 @@ +// RUN: not mlir-opt -allow-unregistered-dialect %s -split-input-file -verify-diagnostics 2>&1 | FileCheck %s + +// CHECK: found start of regex with no end '}}' +// expected-error-re {{{{}} + +// ----- + +// CHECK: invalid regex: parentheses not balanced +// expected-error-re {{ {{(}} }} + +// ----- + +func.func @foo() -> i32 { + // expected-error-re@+1 {{'func.return' op has 0 operands, but enclosing function (@{{.*}}) returns 1}} + return +}