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