xref: /llvm-project/mlir/docs/Diagnostics.md (revision 12dba4d48461c571cb5bdb7c3ac6078b9a357a02)
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