More structured errors


#1

We would like to support emitting errors in different ways, for example as JSON or HTML. The former for easier integration with IDEs, the latter for better user understanding. The main impediment to these more structured forms is that our error messages are not particularly structured. In particular, when we issue notes, help messages, or suggestions with an error message, these are treated as independent messages with no relation. I would like to have the notes associated with the errors.

I see two ways to do this: either we support error transactions, so a multi-part error message looks like:

handler.start_transaction();
handler.span_err(...);
handler.span_note(...);
handler.span_note(...);
handler.end_transaction();

or we support structured exceptions:

let mut err = span_err(...);
err.note(...);
err.note(...);
handler.report(err);

Some of the constraints are that we should be able to handle notes which are not in the same function as the error and we should strive to make orphan notes a static error, rather than a compile-time one (if possible).

Does anyone have any better designs? Or an opinion on these two? I think I prefer the second, although the first would be less work to implement (since existing code would still compile (if we admit single errors without transactions), which also means it is potentially buggier).


#2

I’ve been wanting to experiment with this sort of error emission for a while (even for just terminal users, this would allow us to emit errors in a much more focused way). That said, I’d thought about doing it something like:

let mut err: DiagnosticBuilder = sess.span_err(...);
err.note(...);
err.emit();

(i.e. have the handler stored by the builder internally.) Or even just having a Drop implementation that outputs the error (although, this is possibly unnecessarily magic).

Is this adaquately handled by passing the DiagnosticBuilder into the function emitting a note?

I think this is handled by (eventually) only providing the note function on DiagnosticBuilder, so the only way to actually emit a note is in the process of emitting a more important diagnostic.

It seems like the second can be implemented in a way that lets current code still compile, which can be migrated onto the newer strategy progressively. The old functions could even have a separate feature gate (i.e. not rustc_internals) so that people won’t accidentally add more uses of them to crates that have been migrated (and hence had the #[feature] removed).


#3

too early for bikeshedding?

DiagnosticBuilder could be made must_use and span_{err, lint, warn} would return it like in the second example. The only difference would be that note and help actually take the DiagnosticBuilder by value and return it again, and finally emit consumes it properly.

sess.span_err(...)
    .note(...)
    .emit();

#4

I’ve been thinking about this a lot. I want to group errors into transactions, yes, but I also want to go further, and support a kind of markup. Basically I want you to be able to “tag” parts of the error string with spans, to support a multipart error. For example, I plan to convert borrow check errors so that they look something like what is shown in this gist (compare “borrowck-error-new.txt” against “borrowck-error-old.txt”).

Still, making things transactional is an obvious first step in this direction. In this respect, I prefer the second of your alternatives:

let mut err = span_err(...);
err.note(...);
err.note(...);
handler.report(err);

#5

well, I prefer the builder approaches others already suggested even more :smile:


#6

The debug builders on Formatter worked out decently well I think: https://doc.rust-lang.org/stable/std/fmt/struct.Formatter.html#method.debug_struct


#7

Thanks all, the builder approach looks like a nice approach. I’ll start implementing soon.

I think I would like to keep convenience functions for errors without notes, since this is the majority of them at the moment. Emit in the destructor does feel a bit too magic to me. I worry a little bit about ordering too.

@nikomatsakis I also want to structure the errors more internally as well as externally. And indeed, this is meant to be a first step towards that.