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