• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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