| # Diagnostic Infrastructure | |
| [TOC] | |
| This document presents an introduction to using and interfacing with MLIR's | |
| diagnostics infrastructure. | |
| See [MLIR specification](LangRef.md) for more information about MLIR, the | |
| structure of the IR, operations, etc. | |
| ## Source Locations | |
| Source location information is extremely important for any compiler, because it | |
| provides a baseline for debuggability and error-reporting. The | |
| [builtin dialect](Dialects/Builtin.md) provides several different location | |
| attributes types depending on the situational need. | |
| ## Diagnostic Engine | |
| The `DiagnosticEngine` acts as the main interface for diagnostics in MLIR. It | |
| manages the registration of diagnostic handlers, as well as the core API for | |
| diagnostic emission. Handlers generally take the form of | |
| `LogicalResult(Diagnostic &)`. If the result is `success`, it signals that the | |
| diagnostic has been fully processed and consumed. If `failure`, it signals that | |
| the diagnostic should be propagated to any previously registered handlers. It | |
| can be interfaced with via an `MLIRContext` instance. | |
| ```c++ | |
| DiagnosticEngine& engine = ctx->getDiagEngine(); | |
| /// Handle the reported diagnostic. | |
| // Return success to signal that the diagnostic has either been fully processed, | |
| // or failure if the diagnostic should be propagated to the previous handlers. | |
| DiagnosticEngine::HandlerID id = engine.registerHandler( | |
| [](Diagnostic &diag) -> LogicalResult { | |
| bool should_propagate_diagnostic = ...; | |
| return failure(should_propagate_diagnostic); | |
| }); | |
| // We can also elide the return value completely, in which the engine assumes | |
| // that all diagnostics are consumed(i.e. a success() result). | |
| DiagnosticEngine::HandlerID id = engine.registerHandler([](Diagnostic &diag) { | |
| return; | |
| }); | |
| // Unregister this handler when we are done. | |
| engine.eraseHandler(id); | |
| ``` | |
| ### Constructing a Diagnostic | |
| As stated above, the `DiagnosticEngine` holds the core API for diagnostic | |
| emission. A new diagnostic can be emitted with the engine via `emit`. This | |
| method returns an [InFlightDiagnostic](#inflight-diagnostic) that can be | |
| modified further. | |
| ```c++ | |
| InFlightDiagnostic emit(Location loc, DiagnosticSeverity severity); | |
| ``` | |
| Using the `DiagnosticEngine`, though, is generally not the preferred way to emit | |
| diagnostics in MLIR. [`operation`](LangRef.md/#operations) provides utility | |
| methods for emitting diagnostics: | |
| ```c++ | |
| // `emit` methods available in the mlir namespace. | |
| InFlightDiagnostic emitError/Remark/Warning(Location); | |
| // These methods use the location attached to the operation. | |
| InFlightDiagnostic Operation::emitError/Remark/Warning(); | |
| // This method creates a diagnostic prefixed with "'op-name' op ". | |
| InFlightDiagnostic Operation::emitOpError(); | |
| ``` | |
| ## Diagnostic | |
| A `Diagnostic` in MLIR contains all of the necessary information for reporting a | |
| message to the user. A `Diagnostic` essentially boils down to four main | |
| components: | |
| * [Source Location](#source-locations) | |
| * Severity Level | |
| - Error, Note, Remark, Warning | |
| * Diagnostic Arguments | |
| - The diagnostic arguments are used when constructing the output message. | |
| * Metadata | |
| - Some additional information attached that can be used to identify | |
| this diagnostic other than source location and severity level | |
| (e.g. for diagnostic handlers to do some filtering). | |
| Metadata is not part of the output message. | |
| ### Appending arguments | |
| One a diagnostic has been constructed, the user can start composing it. The | |
| output message of a diagnostic is composed of a set of diagnostic arguments that | |
| have been attached to it. New arguments can be attached to a diagnostic in a few | |
| different ways: | |
| ```c++ | |
| // A few interesting things to use when composing a diagnostic. | |
| Attribute fooAttr; | |
| Type fooType; | |
| SmallVector<int> fooInts; | |
| // Diagnostics can be composed via the streaming operators. | |
| op->emitError() << "Compose an interesting error: " << fooAttr << ", " << fooType | |
| << ", (" << fooInts << ')'; | |
| // This could generate something like (FuncAttr:@foo, IntegerType:i32, {0,1,2}): | |
| "Compose an interesting error: @foo, i32, (0, 1, 2)" | |
| ``` | |
| Operations attached to a diagnostic will be printed in generic form if the | |
| severity level is `Error`, otherwise custom operation printers will be used. | |
| ```c++ | |
| // `anotherOp` will be printed in generic form, | |
| // e.g. %3 = "arith.addf"(%arg4, %2) : (f32, f32) -> f32 | |
| op->emitError() << anotherOp; | |
| // `anotherOp` will be printed using the custom printer, | |
| // e.g. %3 = arith.addf %arg4, %2 : f32 | |
| op->emitRemark() << anotherOp; | |
| ``` | |
| To make a custom type compatible with Diagnostics, one must implement the | |
| following friend function. | |
| ```c++ | |
| friend mlir::Diagnostic &operator<<( | |
| mlir::Diagnostic &diagnostic, const MyType &foo); | |
| ``` | |
| ### Attaching notes | |
| Unlike many other compiler frameworks, notes in MLIR cannot be emitted directly. | |
| They must be explicitly attached to another diagnostic non-note diagnostic. When | |
| emitting a diagnostic, notes can be directly attached via `attachNote`. When | |
| attaching a note, if the user does not provide an explicit source location the | |
| note will inherit the location of the parent diagnostic. | |
| ```c++ | |
| // Emit a note with an explicit source location. | |
| op->emitError("...").attachNote(noteLoc) << "..."; | |
| // Emit a note that inherits the parent location. | |
| op->emitError("...").attachNote() << "..."; | |
| ``` | |
| ### Managing Metadata | |
| Metadata is a mutable vector of DiagnosticArguments. | |
| It can be accessed and modified as a vector. | |
| ## InFlight Diagnostic | |
| Now that [Diagnostics](#diagnostic) have been explained, we introduce the | |
| `InFlightDiagnostic`, an RAII wrapper around a diagnostic that is set to be | |
| reported. This allows for modifying a diagnostic while it is still in flight. If | |
| it is not reported directly by the user it will automatically report when | |
| destroyed. | |
| ```c++ | |
| { | |
| InFlightDiagnostic diag = op->emitError() << "..."; | |
| } // The diagnostic is automatically reported here. | |
| ``` | |
| ## Diagnostic Configuration Options | |
| Several options are provided to help control and enhance the behavior of | |
| diagnostics. These options can be configured via the MLIRContext, and registered | |
| to the command line with the `registerMLIRContextCLOptions` method. These | |
| options are listed below: | |
| ### Print Operation On Diagnostic | |
| Command Line Flag: `-mlir-print-op-on-diagnostic` | |
| When a diagnostic is emitted on an operation, via `Operation::emitError/...`, | |
| the textual form of that operation is printed and attached as a note to the | |
| diagnostic. This option is useful for understanding the current form of an | |
| operation that may be invalid, especially when debugging verifier failures. An | |
| example output is shown below: | |
| ```shell | |
| test.mlir:3:3: error: 'module_terminator' op expects parent op 'builtin.module' | |
| "module_terminator"() : () -> () | |
| ^ | |
| test.mlir:3:3: note: see current operation: "module_terminator"() : () -> () | |
| "module_terminator"() : () -> () | |
| ^ | |
| ``` | |
| ### Print StackTrace On Diagnostic | |
| Command Line Flag: `-mlir-print-stacktrace-on-diagnostic` | |
| When a diagnostic is emitted, attach the current stack trace as a note to the | |
| diagnostic. This option is useful for understanding which part of the compiler | |
| generated certain diagnostics. An example output is shown below: | |
| ```shell | |
| test.mlir:3:3: error: 'module_terminator' op expects parent op 'builtin.module' | |
| "module_terminator"() : () -> () | |
| ^ | |
| test.mlir:3:3: note: diagnostic emitted with trace: | |
| #0 0x000055dd40543805 llvm::sys::PrintStackTrace(llvm::raw_ostream&) llvm/lib/Support/Unix/Signals.inc:553:11 | |
| #1 0x000055dd3f8ac162 emitDiag(mlir::Location, mlir::DiagnosticSeverity, llvm::Twine const&) /lib/IR/Diagnostics.cpp:292:7 | |
| #2 0x000055dd3f8abe8e mlir::emitError(mlir::Location, llvm::Twine const&) /lib/IR/Diagnostics.cpp:304:10 | |
| #3 0x000055dd3f998e87 mlir::Operation::emitError(llvm::Twine const&) /lib/IR/Operation.cpp:324:29 | |
| #4 0x000055dd3f99d21c mlir::Operation::emitOpError(llvm::Twine const&) /lib/IR/Operation.cpp:652:10 | |
| #5 0x000055dd3f96b01c mlir::OpTrait::HasParent<mlir::ModuleOp>::Impl<mlir::ModuleTerminatorOp>::verifyTrait(mlir::Operation*) /mlir/IR/OpDefinition.h:897:18 | |
| #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 | |
| # ... | |
| "module_terminator"() : () -> () | |
| ^ | |
| ``` | |
| ## Common Diagnostic Handlers | |
| To interface with the diagnostics infrastructure, users will need to register a | |
| diagnostic handler with the [`DiagnosticEngine`](#diagnostic-engine). | |
| Recognizing the many users will want the same handler functionality, MLIR | |
| provides several common diagnostic handlers for immediate use. | |
| ### Scoped Diagnostic Handler | |
| This diagnostic handler is a simple RAII class that registers and unregisters a | |
| given diagnostic handler. This class can be either be used directly, or in | |
| conjunction with a derived diagnostic handler. | |
| ```c++ | |
| // Construct the handler directly. | |
| MLIRContext context; | |
| ScopedDiagnosticHandler scopedHandler(&context, [](Diagnostic &diag) { | |
| ... | |
| }); | |
| // Use this handler in conjunction with another. | |
| class MyDerivedHandler : public ScopedDiagnosticHandler { | |
| MyDerivedHandler(MLIRContext *ctx) : ScopedDiagnosticHandler(ctx) { | |
| // Set the handler that should be RAII managed. | |
| setHandler([&](Diagnostic diag) { | |
| ... | |
| }); | |
| } | |
| }; | |
| ``` | |
| ### SourceMgr Diagnostic Handler | |
| This diagnostic handler is a wrapper around an llvm::SourceMgr instance. It | |
| provides support for displaying diagnostic messages inline with a line of a | |
| respective source file. This handler will also automatically load newly seen | |
| source files into the SourceMgr when attempting to display the source line of a | |
| diagnostic. Example usage of this handler can be seen in the `mlir-opt` tool. | |
| ```shell | |
| $ mlir-opt foo.mlir | |
| /tmp/test.mlir:6:24: error: expected non-function type | |
| func.func @foo() -> (index, ind) { | |
| ^ | |
| ``` | |
| To use this handler in your tool, add the following: | |
| ```c++ | |
| SourceMgr sourceMgr; | |
| MLIRContext context; | |
| SourceMgrDiagnosticHandler sourceMgrHandler(sourceMgr, &context); | |
| ``` | |
| #### Filtering Locations | |
| In some situations, a diagnostic may be emitted with a callsite location in a | |
| very deep call stack in which many frames are unrelated to the user source code. | |
| These situations often arise when the user source code is intertwined with that | |
| of a large framework or library. The context of the diagnostic in these cases is | |
| often obfuscated by the unrelated framework source locations. To help alleviate | |
| this obfuscation, the `SourceMgrDiagnosticHandler` provides support for | |
| filtering which locations are shown to the user. To enable filtering, a user | |
| must simply provide a filter function to the `SourceMgrDiagnosticHandler` on | |
| construction that indicates which locations should be shown. A quick example is | |
| shown below: | |
| ```c++ | |
| // Here we define the functor that controls which locations are shown to the | |
| // user. This functor should return true when a location should be shown, and | |
| // false otherwise. When filtering a container location, such as a NameLoc, this | |
| // function should not recurse into the child location. Recursion into nested | |
| // location is performed as necessary by the caller. | |
| auto shouldShowFn = [](Location loc) -> bool { | |
| FileLineColLoc fileLoc = dyn_cast<FileLineColLoc>(loc); | |
| // We don't perform any filtering on non-file locations. | |
| // Reminder: The caller will recurse into any necessary child locations. | |
| if (!fileLoc) | |
| return true; | |
| // Don't show file locations that contain our framework code. | |
| return !fileLoc.getFilename().strref().contains("my/framework/source/"); | |
| }; | |
| SourceMgr sourceMgr; | |
| MLIRContext context; | |
| SourceMgrDiagnosticHandler sourceMgrHandler(sourceMgr, &context, shouldShowFn); | |
| ``` | |
| Note: In the case where all locations are filtered out, the first location in | |
| the stack will still be shown. | |
| ### SourceMgr Diagnostic Verifier Handler | |
| This handler is a wrapper around a llvm::SourceMgr that is used to verify that | |
| 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)(-re)? {{ message }}` | |
| 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. | |
| func.func @bad_branch() { | |
| cf.br ^missing // expected-error {{reference to an undefined block}} | |
| } | |
| // Expect an error on an adjacent line. | |
| func.func @foo(%a : f32) { | |
| // expected-error@+1 {{unknown comparison predicate "foo"}} | |
| %result = arith.cmpf "foo", %a, %a : f32 | |
| return | |
| } | |
| // Expect an error on the next line that does not contain a designator. | |
| // expected-remark@below {{remark on function below}} | |
| // expected-remark@below {{another remark on function below}} | |
| func.func @bar(%a : f32) | |
| // Expect an error on the previous line that does not contain a designator. | |
| func.func @baz(%a : f32) | |
| // 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 | |
| any expected diagnostics weren't. | |
| ```shell | |
| $ mlir-opt foo.mlir | |
| /tmp/test.mlir:6:24: error: unexpected error: expected non-function type | |
| func.func @foo() -> (index, ind) { | |
| ^ | |
| /tmp/test.mlir:15:4: error: expected remark "expected some remark" was not produced | |
| // expected-remark {{expected some remark}} | |
| ^~~~~~~~~~~~~~~~~~~~~~~~~~ | |
| ``` | |
| Similarly to the [SourceMgr Diagnostic Handler](#sourcemgr-diagnostic-handler), | |
| this handler can be added to any tool via the following: | |
| ```c++ | |
| SourceMgr sourceMgr; | |
| MLIRContext context; | |
| SourceMgrDiagnosticVerifierHandler sourceMgrHandler(sourceMgr, &context); | |
| ``` | |
| ### Parallel Diagnostic Handler | |
| MLIR is designed from the ground up to be multi-threaded. One important to thing | |
| to keep in mind when multi-threading is determinism. This means that the | |
| behavior seen when operating on multiple threads is the same as when operating | |
| on a single thread. For diagnostics, this means that the ordering of the | |
| diagnostics is the same regardless of the amount of threads being operated on. | |
| The ParallelDiagnosticHandler is introduced to solve this problem. | |
| After creating a handler of this type, the only remaining step is to ensure that | |
| each thread that will be emitting diagnostics to the handler sets a respective | |
| 'orderID'. The orderID corresponds to the order in which diagnostics would be | |
| emitted when executing synchronously. For example, if we were processing a list | |
| of operations [a, b, c] on a single-thread. Diagnostics emitted while processing | |
| operation 'a' would be emitted before those for 'b' or 'c'. This corresponds 1-1 | |
| with the 'orderID'. The thread that is processing 'a' should set the orderID to | |
| '0'; the thread processing 'b' should set it to '1'; and so on and so forth. | |
| This provides a way for the handler to deterministically order the diagnostics | |
| that it receives given the thread that it is receiving on. | |
| A simple example is shown below: | |
| ```c++ | |
| MLIRContext *context = ...; | |
| ParallelDiagnosticHandler handler(context); | |
| // Process a list of operations in parallel. | |
| std::vector<Operation *> opsToProcess = ...; | |
| llvm::parallelFor(0, opsToProcess.size(), [&](size_t i) { | |
| // Notify the handler that we are processing the i'th operation. | |
| handler.setOrderIDForThread(i); | |
| auto *op = opsToProcess[i]; | |
| ... | |
| // Notify the handler that we are finished processing diagnostics on this | |
| // thread. | |
| handler.eraseOrderIDForThread(); | |
| }); | |
| ``` |