1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
|
# Introduction and Usage Guide to MLIR's Diagnostics 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. MLIR provides several
different location types depending on the situational need.
### CallSite Location
```
callsite-location ::= 'callsite' '(' location 'at' location ')'
```
An instance of this location allows for representing a directed stack of
location usages. This connects a location of a `callee` with the location of a
`caller`.
### FileLineCol Location
```
filelinecol-location ::= string-literal ':' integer-literal ':' integer-literal
```
An instance of this location represents a tuple of file, line number, and column
number. This is similar to the type of location that you get from most source
languages.
### Fused Location
```
fused-location ::= `fused` fusion-metadata? '[' location (location ',')* ']'
fusion-metadata ::= '<' attribute-value '>'
```
An instance of a `fused` location represents a grouping of several other source
locations, with optional metadata that describes the context of the fusion.
There are many places within a compiler in which several constructs may be fused
together, e.g. pattern rewriting, that normally result partial or even total
loss of location information. With `fused` locations, this is a non-issue.
### Name Location
```
name-location ::= string-literal ('(' location ')')?
```
An instance of this location allows for attaching a name to a child location.
This can be useful for representing the locations of variable, or node,
definitions.
### Opaque Location
An instance of this location essentially contains a pointer to some data
structure that is external to MLIR and an optional location that can be used if
the first one is not suitable. Since it contains an external structure, only the
optional location is used during serialization.
### Unknown Location
```
unknown-location ::= `unknown`
```
Source location information is an extremely integral part of the MLIR
infrastructure. As such, location information is always present in the IR, and
must explicitly be set to unknown. Thus an instance of the `unknown` location,
represents an unspecified source location.
## 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_propage_diagnostic = ...;
return failure(should_propage_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 three main
components:
* [Source Location](#source-locations)
* Severity Level
- Error, Note, Remark, Warning
* Diagnostic Arguments
- The diagnostic arguments are used when constructing 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)"
```
### 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() << "...";
```
## 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 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 '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 '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::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
# ...
"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 @foo() -> (index, ind) {
^
```
To use this handler in your tool, add the following:
```c++
SourceMgr sourceMgr;
MLIRContext context;
SourceMgrDiagnosticHandler sourceMgrHandler(sourceMgr, &context);
```
### 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) {{ message }}`
A few examples are shown below:
```mlir
// Expect an error on the same line.
func @bad_branch() {
br ^missing // expected-error {{reference to an undefined block}}
}
// Expect an error on an adjacent line.
func @foo(%a : f32) {
// expected-error@+1 {{unknown comparison predicate "foo"}}
%result = 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 @bar(%a : f32)
// Expect an error on the previous line that does not contain a designator.
func @baz(%a : f32)
// expected-remark@above {{remark on function above}}
// expected-remark@above {{another remark on function above}}
```
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 @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::for_each_n(llvm::parallel::par, 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();
});
```
|