1# Diagnostic Infrastructure 2 3[TOC] 4 5This document presents an introduction to using and interfacing with MLIR's 6diagnostics infrastructure. 7 8See [MLIR specification](LangRef.md) for more information about MLIR, the 9structure of the IR, operations, etc. 10 11## Source Locations 12 13Source location information is extremely important for any compiler, because it 14provides a baseline for debuggability and error-reporting. The 15[builtin dialect](Dialects/Builtin.md) provides several different location 16attributes types depending on the situational need. 17 18## Diagnostic Engine 19 20The `DiagnosticEngine` acts as the main interface for diagnostics in MLIR. It 21manages the registration of diagnostic handlers, as well as the core API for 22diagnostic emission. Handlers generally take the form of 23`LogicalResult(Diagnostic &)`. If the result is `success`, it signals that the 24diagnostic has been fully processed and consumed. If `failure`, it signals that 25the diagnostic should be propagated to any previously registered handlers. It 26can be interfaced with via an `MLIRContext` instance. 27 28```c++ 29DiagnosticEngine& engine = ctx->getDiagEngine(); 30 31/// Handle the reported diagnostic. 32// Return success to signal that the diagnostic has either been fully processed, 33// or failure if the diagnostic should be propagated to the previous handlers. 34DiagnosticEngine::HandlerID id = engine.registerHandler( 35 [](Diagnostic &diag) -> LogicalResult { 36 bool should_propagate_diagnostic = ...; 37 return failure(should_propagate_diagnostic); 38}); 39 40 41// We can also elide the return value completely, in which the engine assumes 42// that all diagnostics are consumed(i.e. a success() result). 43DiagnosticEngine::HandlerID id = engine.registerHandler([](Diagnostic &diag) { 44 return; 45}); 46 47// Unregister this handler when we are done. 48engine.eraseHandler(id); 49``` 50 51### Constructing a Diagnostic 52 53As stated above, the `DiagnosticEngine` holds the core API for diagnostic 54emission. A new diagnostic can be emitted with the engine via `emit`. This 55method returns an [InFlightDiagnostic](#inflight-diagnostic) that can be 56modified further. 57 58```c++ 59InFlightDiagnostic emit(Location loc, DiagnosticSeverity severity); 60``` 61 62Using the `DiagnosticEngine`, though, is generally not the preferred way to emit 63diagnostics in MLIR. [`operation`](LangRef.md/#operations) provides utility 64methods for emitting diagnostics: 65 66```c++ 67// `emit` methods available in the mlir namespace. 68InFlightDiagnostic emitError/Remark/Warning(Location); 69 70// These methods use the location attached to the operation. 71InFlightDiagnostic Operation::emitError/Remark/Warning(); 72 73// This method creates a diagnostic prefixed with "'op-name' op ". 74InFlightDiagnostic Operation::emitOpError(); 75``` 76 77## Diagnostic 78 79A `Diagnostic` in MLIR contains all of the necessary information for reporting a 80message to the user. A `Diagnostic` essentially boils down to four main 81components: 82 83* [Source Location](#source-locations) 84* Severity Level 85 - Error, Note, Remark, Warning 86* Diagnostic Arguments 87 - The diagnostic arguments are used when constructing the output message. 88* Metadata 89 - Some additional information attached that can be used to identify 90 this diagnostic other than source location and severity level 91 (e.g. for diagnostic handlers to do some filtering). 92 Metadata is not part of the output message. 93 94### Appending arguments 95 96One a diagnostic has been constructed, the user can start composing it. The 97output message of a diagnostic is composed of a set of diagnostic arguments that 98have been attached to it. New arguments can be attached to a diagnostic in a few 99different ways: 100 101```c++ 102// A few interesting things to use when composing a diagnostic. 103Attribute fooAttr; 104Type fooType; 105SmallVector<int> fooInts; 106 107// Diagnostics can be composed via the streaming operators. 108op->emitError() << "Compose an interesting error: " << fooAttr << ", " << fooType 109 << ", (" << fooInts << ')'; 110 111// This could generate something like (FuncAttr:@foo, IntegerType:i32, {0,1,2}): 112"Compose an interesting error: @foo, i32, (0, 1, 2)" 113``` 114 115Operations attached to a diagnostic will be printed in generic form if the 116severity level is `Error`, otherwise custom operation printers will be used. 117```c++ 118// `anotherOp` will be printed in generic form, 119// e.g. %3 = "arith.addf"(%arg4, %2) : (f32, f32) -> f32 120op->emitError() << anotherOp; 121 122// `anotherOp` will be printed using the custom printer, 123// e.g. %3 = arith.addf %arg4, %2 : f32 124op->emitRemark() << anotherOp; 125``` 126 127To make a custom type compatible with Diagnostics, one must implement the 128following friend function. 129 130```c++ 131friend mlir::Diagnostic &operator<<( 132 mlir::Diagnostic &diagnostic, const MyType &foo); 133``` 134 135### Attaching notes 136 137Unlike many other compiler frameworks, notes in MLIR cannot be emitted directly. 138They must be explicitly attached to another diagnostic non-note diagnostic. When 139emitting a diagnostic, notes can be directly attached via `attachNote`. When 140attaching a note, if the user does not provide an explicit source location the 141note will inherit the location of the parent diagnostic. 142 143```c++ 144// Emit a note with an explicit source location. 145op->emitError("...").attachNote(noteLoc) << "..."; 146 147// Emit a note that inherits the parent location. 148op->emitError("...").attachNote() << "..."; 149``` 150 151### Managing Metadata 152Metadata is a mutable vector of DiagnosticArguments. 153It can be accessed and modified as a vector. 154 155 156## InFlight Diagnostic 157 158Now that [Diagnostics](#diagnostic) have been explained, we introduce the 159`InFlightDiagnostic`, an RAII wrapper around a diagnostic that is set to be 160reported. This allows for modifying a diagnostic while it is still in flight. If 161it is not reported directly by the user it will automatically report when 162destroyed. 163 164```c++ 165{ 166 InFlightDiagnostic diag = op->emitError() << "..."; 167} // The diagnostic is automatically reported here. 168``` 169 170## Diagnostic Configuration Options 171 172Several options are provided to help control and enhance the behavior of 173diagnostics. These options can be configured via the MLIRContext, and registered 174to the command line with the `registerMLIRContextCLOptions` method. These 175options are listed below: 176 177### Print Operation On Diagnostic 178 179Command Line Flag: `-mlir-print-op-on-diagnostic` 180 181When a diagnostic is emitted on an operation, via `Operation::emitError/...`, 182the textual form of that operation is printed and attached as a note to the 183diagnostic. This option is useful for understanding the current form of an 184operation that may be invalid, especially when debugging verifier failures. An 185example output is shown below: 186 187```shell 188test.mlir:3:3: error: 'module_terminator' op expects parent op 'builtin.module' 189 "module_terminator"() : () -> () 190 ^ 191test.mlir:3:3: note: see current operation: "module_terminator"() : () -> () 192 "module_terminator"() : () -> () 193 ^ 194``` 195 196### Print StackTrace On Diagnostic 197 198Command Line Flag: `-mlir-print-stacktrace-on-diagnostic` 199 200When a diagnostic is emitted, attach the current stack trace as a note to the 201diagnostic. This option is useful for understanding which part of the compiler 202generated certain diagnostics. An example output is shown below: 203 204```shell 205test.mlir:3:3: error: 'module_terminator' op expects parent op 'builtin.module' 206 "module_terminator"() : () -> () 207 ^ 208test.mlir:3:3: note: diagnostic emitted with trace: 209 #0 0x000055dd40543805 llvm::sys::PrintStackTrace(llvm::raw_ostream&) llvm/lib/Support/Unix/Signals.inc:553:11 210 #1 0x000055dd3f8ac162 emitDiag(mlir::Location, mlir::DiagnosticSeverity, llvm::Twine const&) /lib/IR/Diagnostics.cpp:292:7 211 #2 0x000055dd3f8abe8e mlir::emitError(mlir::Location, llvm::Twine const&) /lib/IR/Diagnostics.cpp:304:10 212 #3 0x000055dd3f998e87 mlir::Operation::emitError(llvm::Twine const&) /lib/IR/Operation.cpp:324:29 213 #4 0x000055dd3f99d21c mlir::Operation::emitOpError(llvm::Twine const&) /lib/IR/Operation.cpp:652:10 214 #5 0x000055dd3f96b01c mlir::OpTrait::HasParent<mlir::ModuleOp>::Impl<mlir::ModuleTerminatorOp>::verifyTrait(mlir::Operation*) /mlir/IR/OpDefinition.h:897:18 215 #6 0x000055dd3f96ab38 mlir::Op<mlir::ModuleTerminatorOp, mlir::OpTrait::ZeroOperands, mlir::OpTrait::ZeroResults, mlir::OpTrait::HasParent<mlir::ModuleOp>::Impl, mlir::OpTrait::IsTerminator>::BaseVerifier<mlir::OpTrait::HasParent<mlir::ModuleOp>::Impl<mlir::ModuleTerminatorOp>, mlir::OpTrait::IsTerminator<mlir::ModuleTerminatorOp> >::verifyTrait(mlir::Operation*) /mlir/IR/OpDefinition.h:1052:29 216 # ... 217 "module_terminator"() : () -> () 218 ^ 219``` 220 221## Common Diagnostic Handlers 222 223To interface with the diagnostics infrastructure, users will need to register a 224diagnostic handler with the [`DiagnosticEngine`](#diagnostic-engine). 225Recognizing the many users will want the same handler functionality, MLIR 226provides several common diagnostic handlers for immediate use. 227 228### Scoped Diagnostic Handler 229 230This diagnostic handler is a simple RAII class that registers and unregisters a 231given diagnostic handler. This class can be either be used directly, or in 232conjunction with a derived diagnostic handler. 233 234```c++ 235// Construct the handler directly. 236MLIRContext context; 237ScopedDiagnosticHandler scopedHandler(&context, [](Diagnostic &diag) { 238 ... 239}); 240 241// Use this handler in conjunction with another. 242class MyDerivedHandler : public ScopedDiagnosticHandler { 243 MyDerivedHandler(MLIRContext *ctx) : ScopedDiagnosticHandler(ctx) { 244 // Set the handler that should be RAII managed. 245 setHandler([&](Diagnostic diag) { 246 ... 247 }); 248 } 249}; 250``` 251 252### SourceMgr Diagnostic Handler 253 254This diagnostic handler is a wrapper around an llvm::SourceMgr instance. It 255provides support for displaying diagnostic messages inline with a line of a 256respective source file. This handler will also automatically load newly seen 257source files into the SourceMgr when attempting to display the source line of a 258diagnostic. Example usage of this handler can be seen in the `mlir-opt` tool. 259 260```shell 261$ mlir-opt foo.mlir 262 263/tmp/test.mlir:6:24: error: expected non-function type 264func.func @foo() -> (index, ind) { 265 ^ 266``` 267 268To use this handler in your tool, add the following: 269 270```c++ 271SourceMgr sourceMgr; 272MLIRContext context; 273SourceMgrDiagnosticHandler sourceMgrHandler(sourceMgr, &context); 274``` 275 276#### Filtering Locations 277 278In some situations, a diagnostic may be emitted with a callsite location in a 279very deep call stack in which many frames are unrelated to the user source code. 280These situations often arise when the user source code is intertwined with that 281of a large framework or library. The context of the diagnostic in these cases is 282often obfuscated by the unrelated framework source locations. To help alleviate 283this obfuscation, the `SourceMgrDiagnosticHandler` provides support for 284filtering which locations are shown to the user. To enable filtering, a user 285must simply provide a filter function to the `SourceMgrDiagnosticHandler` on 286construction that indicates which locations should be shown. A quick example is 287shown below: 288 289```c++ 290// Here we define the functor that controls which locations are shown to the 291// user. This functor should return true when a location should be shown, and 292// false otherwise. When filtering a container location, such as a NameLoc, this 293// function should not recurse into the child location. Recursion into nested 294// location is performed as necessary by the caller. 295auto shouldShowFn = [](Location loc) -> bool { 296 FileLineColLoc fileLoc = loc.dyn_cast<FileLineColLoc>(); 297 298 // We don't perform any filtering on non-file locations. 299 // Reminder: The caller will recurse into any necessary child locations. 300 if (!fileLoc) 301 return true; 302 303 // Don't show file locations that contain our framework code. 304 return !fileLoc.getFilename().strref().contains("my/framework/source/"); 305}; 306 307SourceMgr sourceMgr; 308MLIRContext context; 309SourceMgrDiagnosticHandler sourceMgrHandler(sourceMgr, &context, shouldShowFn); 310``` 311 312Note: In the case where all locations are filtered out, the first location in 313the stack will still be shown. 314 315### SourceMgr Diagnostic Verifier Handler 316 317This handler is a wrapper around a llvm::SourceMgr that is used to verify that 318certain diagnostics have been emitted to the context. To use this handler, 319annotate your source file with expected diagnostics in the form of: 320 321* `expected-(error|note|remark|warning)(-re)? {{ message }}` 322 323The provided `message` is a string expected to be contained within the generated 324diagnostic. The `-re` suffix may be used to enable regex matching within the 325`message`. When present, the `message` may define regex match sequences within 326`{{` `}}` blocks. The regular expression matcher supports Extended POSIX regular 327expressions (ERE). A few examples are shown below: 328 329```mlir 330// Expect an error on the same line. 331func.func @bad_branch() { 332 cf.br ^missing // expected-error {{reference to an undefined block}} 333} 334 335// Expect an error on an adjacent line. 336func.func @foo(%a : f32) { 337 // expected-error@+1 {{unknown comparison predicate "foo"}} 338 %result = arith.cmpf "foo", %a, %a : f32 339 return 340} 341 342// Expect an error on the next line that does not contain a designator. 343// expected-remark@below {{remark on function below}} 344// expected-remark@below {{another remark on function below}} 345func.func @bar(%a : f32) 346 347// Expect an error on the previous line that does not contain a designator. 348func.func @baz(%a : f32) 349// expected-remark@above {{remark on function above}} 350// expected-remark@above {{another remark on function above}} 351 352// Expect an error mentioning the parent function, but use regex to avoid 353// hardcoding the name. 354func.func @foo() -> i32 { 355 // expected-error-re@+1 {{'func.return' op has 0 operands, but enclosing function (@{{.*}}) returns 1}} 356 return 357} 358``` 359 360The handler will report an error if any unexpected diagnostics were seen, or if 361any expected diagnostics weren't. 362 363```shell 364$ mlir-opt foo.mlir 365 366/tmp/test.mlir:6:24: error: unexpected error: expected non-function type 367func.func @foo() -> (index, ind) { 368 ^ 369 370/tmp/test.mlir:15:4: error: expected remark "expected some remark" was not produced 371// expected-remark {{expected some remark}} 372 ^~~~~~~~~~~~~~~~~~~~~~~~~~ 373``` 374 375Similarly to the [SourceMgr Diagnostic Handler](#sourcemgr-diagnostic-handler), 376this handler can be added to any tool via the following: 377 378```c++ 379SourceMgr sourceMgr; 380MLIRContext context; 381SourceMgrDiagnosticVerifierHandler sourceMgrHandler(sourceMgr, &context); 382``` 383 384### Parallel Diagnostic Handler 385 386MLIR is designed from the ground up to be multi-threaded. One important to thing 387to keep in mind when multi-threading is determinism. This means that the 388behavior seen when operating on multiple threads is the same as when operating 389on a single thread. For diagnostics, this means that the ordering of the 390diagnostics is the same regardless of the amount of threads being operated on. 391The ParallelDiagnosticHandler is introduced to solve this problem. 392 393After creating a handler of this type, the only remaining step is to ensure that 394each thread that will be emitting diagnostics to the handler sets a respective 395'orderID'. The orderID corresponds to the order in which diagnostics would be 396emitted when executing synchronously. For example, if we were processing a list 397of operations [a, b, c] on a single-thread. Diagnostics emitted while processing 398operation 'a' would be emitted before those for 'b' or 'c'. This corresponds 1-1 399with the 'orderID'. The thread that is processing 'a' should set the orderID to 400'0'; the thread processing 'b' should set it to '1'; and so on and so forth. 401This provides a way for the handler to deterministically order the diagnostics 402that it receives given the thread that it is receiving on. 403 404A simple example is shown below: 405 406```c++ 407MLIRContext *context = ...; 408ParallelDiagnosticHandler handler(context); 409 410// Process a list of operations in parallel. 411std::vector<Operation *> opsToProcess = ...; 412llvm::parallelFor(0, opsToProcess.size(), [&](size_t i) { 413 // Notify the handler that we are processing the i'th operation. 414 handler.setOrderIDForThread(i); 415 auto *op = opsToProcess[i]; 416 ... 417 418 // Notify the handler that we are finished processing diagnostics on this 419 // thread. 420 handler.eraseOrderIDForThread(); 421}); 422``` 423