9.8 KiB
Introduction and Usage Guide to MLIR's Diagnostics Infrastructure
[TOC]
This document presents an introduction to using and interfacing with MLIR's diagnostics infrastucture.
See MLIR specification 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.
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. It can be interfaced with via an MLIRContext
instance.
DiagnosticEngine engine = ctx->getDiagEngine();
engine.setHandler([](Diagnostic diag) {
// Handle the reported diagnostic.
});
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 that can be
modified further.
InFlightDiagnostic emit(Location loc, DiagnosticSeverity severity);
Using the DiagnosticEngine
, though, is generally not the preferred way to emit
diagnostics in MLIR. MLIRContext
, function
, and
operation
all provide utility methods for emitting
diagnostics:
InFlightDiagnostic MLIRContext::emitError/Remark/Warning(Location);
// These methods use the location attached to the function/operation.
InFlightDiagnostic Function::emitError/Remark/Warning();
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
- 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:
// 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.
// 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 have been explained, we introduce the
InFlightDiagnostic
. is 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.
{
InFlightDiagnostic diag = op->emitError() << "...";
} // The diagnostic is automatically reported here.
Common Diagnostic Handlers
To interface with the diagnostics infrastructure, users will need to register a
diagnostic handler with the DiagnosticEngine
.
Recognizing the many users will want the same handler functionality, MLIR
provides several common diagnostic handlers for immediate use.
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.
$ 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:
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:
// 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
}
The handler will report an error if any unexpected diagnostics were seen, or if any expected diagnostics weren't.
$ 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, this handler can be added to any tool via the following:
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.
Note: This handler automatically saves and restores the current handler registered with the context.
A simple example is shown below:
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];
...
});