-
Notifications
You must be signed in to change notification settings - Fork 1.6k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
The #[diagnostic]
attribute namespace
#3368
Changes from 7 commits
b9bf6ad
74958a3
30f575c
a9fc409
6e9038b
cecccb4
c6b8f65
cb8f2fc
4b112aa
0d519ab
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,232 @@ | ||
- Feature Name: The `#[diagnostic]` attribute namespace | ||
- Start Date: 2023-01-06 | ||
- RFC PR: [rust-lang/rfcs#0000](https://github.com/rust-lang/rfcs/pull/0000) | ||
- Rust Issue: [rust-lang/rust#0000](https://github.com/rust-lang/rust/issues/0000) | ||
|
||
|
||
# Summary | ||
[summary]: #summary | ||
|
||
This RFC proposed to add a stable `#[diagnostic]` attribute namespace, which contains attributes to influence error messages emitted by the compiler. In addition it proposed to add a `#[diagnostic::on_unimplemented]` attribute to influence error messages emitted by unsatisfied traits bounds. | ||
|
||
# Motivation | ||
[motivation]: #motivation | ||
|
||
Rust has the reputation to generate helpful error messages when something goes wrong. Nevertheless there are always cases of error messages that can be improved. One common example of such error messages in the rust ecosystem are those that are generated by crates using the type system to verify certain invariants at compile time. While these crates provide additional guarantees about these invariants, they sometimes generate large incomprehensible error messages when something goes wrong. These error messages do not always indicate clearly what went wrong. Well known examples of crates with such issues include bevy, axum or diesel. In some situations such a specific error message always indicates a certain problem. By providing authors of such crates tools to control the error messages emitted by the compiler they can improve the situation on their own. | ||
|
||
# Guide-level explanation | ||
[guide-level-explanation]: #guide-level-explanation | ||
|
||
This feature has two possible groups of users: | ||
|
||
* Users that develop code and consume error messages from the compiler | ||
* Users that write crates involving complex type hierarchies | ||
|
||
The first user group will interact with the proposed feature through the error messages emitted by the compiler. As of this I do not expect any major documentation requirements for this group of users. Although we might want to indicate that a certain error message was provided via the described feature set, rather than by the compiler itself to prevent users for filling issues about bad error messages in the compilers issue tracker. | ||
|
||
The second user group interacts with the described feature through attributes. These attributes allow them to hint the compiler to emit specific error messages in certain cases. The `#[diagnostic]` attribute namespace provides a general framework for what can and can't be done by such an attribute. As of this users won't interact directly with the attribute namespace itself. | ||
|
||
The `#[diagnostic::on_unimplemented]` attribute allows to hint the compiler to emit a specific error message if a certain trait is not implemented. This attribute should provide the following interface: | ||
|
||
```rust | ||
#[diagnostic::on_unimplemented( | ||
message="message", | ||
label="label", | ||
note="note" | ||
)] | ||
trait MyIterator<A> { | ||
fn next(&mut self) -> A; | ||
} | ||
|
||
|
||
fn iterate_chars<I: MyIterator<char>>(i: I) { | ||
// ... | ||
} | ||
fn main() { | ||
iterate_chars(&[1, 2, 3][..]); | ||
} | ||
``` | ||
|
||
which might result in the compiler emitting the following error message: | ||
|
||
``` | ||
error[E0277]: message | ||
--> <anon>:14:5 | ||
| | ||
14 | iterate_chars(&[1, 2, 3][..]); | ||
| ^^^^^^^^^^^^^ label | ||
| | ||
= note: note | ||
= help: the trait `MyIterator<char>` is not implemented for `&[{integer}]` | ||
= note: required by `iterate_chars` | ||
``` | ||
|
||
I expect the new attributes to be documented on the existing [Diagnostics](https://doc.rust-lang.org/reference/attributes/diagnostics.html) attributes page on the rust reference similar to existing attributes like for example `#[deprecated]` | ||
|
||
|
||
# Reference-level explanation | ||
[reference-level-explanation]: #reference-level-explanation | ||
|
||
## The `#[diagnostic]` attribute namespace | ||
|
||
This RFC proposes to introduce a new `#[diagnostic]` attribute namespace. This namespace is supposed to contain different attributes, which allow users to hint the compiler to emit specific diagnostic messages in certain cases like type mismatches, unsatisfied trait bounds or similar situations. By collecting such attributes in a common namespace it is easier for users to find useful attributes and it is easier for the language team to establish a set of common rules for these attributes. | ||
|
||
Attributes in this namespace are generally expected to be formed like: | ||
```rust | ||
#[diagnostic::attribute(option)] | ||
``` | ||
where several `option` entries can appear in the same attribute. `option` is expected to be a valid attribute argument in this position. | ||
|
||
Any attribute in this namespace may: | ||
|
||
* Hint the compiler to emit a specific diagnostic message in a specific situation | ||
* Only affect the messages emitted by a compiler | ||
|
||
Any attribute in this namespace is not allowed to: | ||
|
||
* Change the result of the compilation, which means applying such an attribute should never cause a compilation error as long as they are syntactically valid | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should this say “applying or removing such an attribute should never cause a compilation error”? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I would argue that this is already somewhat implied by the first part of the sentence (cannot change the result of the compilation). That written, it might be meaningful to add it explicitly to the text as well. |
||
* Pass-through information from the source of the diagnostic in a way that users can rely on. E.g. Such an attribute should not allow users to keep the compilation successful and dump information about `extern` blocks to generate C header files | ||
|
||
The compiler is allowed to: | ||
|
||
* Ignore the hints provided by: | ||
+ A specific attribute | ||
+ A specific option | ||
* Change the support for a specific attribute or option at any time | ||
|
||
The compiler must not: | ||
|
||
* Change the semantic of an attribute or option | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What does "Change the semantic of an attribute or option" mean in this context (namely, the context of "The compiler must not:") I.e., from the rest of this text, it seems like these attribute are limited in scope such that they 1. are always just hints and 2. cannot do anything except replace (or add) some diagnostic output. So what does it mean to say that the compiler cannot change those above semantics? Does it mean that the compiler cannot decide on its own to change what trait the diagnostic applies to? Or is it just a restatement that the compiler cannot decide to make the attribute start having effects that go beyond the limits specified by 1. and 2. above? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Its for supposed to prevent that compiler starts at to emit what's passed as |
||
* Emit an hard error on malformed attribute | ||
|
||
The compiler should: | ||
|
||
* Emit implement a warn-by-default lint for unrecognised attributes or options | ||
|
||
Adding a new attribute or option to the `#[diagnostic]` namespace is for now a decision of the language team. The language team can delegate these decisions partially or completely to a different team without requiring a new RFC. | ||
|
||
## The `#[diagnostic::on_unimplemented]` attribute | ||
|
||
This section describes the syntax of the `on_unimplemented` attribute and additionally how it is supposed to work. The specification of this attribute is partially provided as example and motivation for the `#[diagnostic]` attribute namespace. In addition it is provided to give this RFC a concrete use, such that we not only define an empty attribute namespace. | ||
|
||
```rust | ||
#[diagnostic::on_unimplemented( | ||
message="message", | ||
label="label", | ||
note="note", | ||
)] | ||
trait MyIterator<A> { | ||
fn next(&mut self) -> A; | ||
} | ||
``` | ||
|
||
|
||
Each of the options `message`, `label` and `note` are optional. They are separated by comma. The trailing comma is optional. Specifying any of these options hints the compiler to replace the normally emitted part of the error message with the provided string. At least one of these options needs to exist. Each option can appear at most once. These options can include type information for the `Self` type or any generic type by using `{Self}` or `{A}` (where `A` refers to the generic type name in the definition). These placeholders are replaced by the compiler with the actual type name. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I understand the idea of replacing the message and the label, in the sense that I think there is always a message, and there is one label (potentially at most one label? Can the label be omitted? Is this inserted in its stead in that case?). So replacing each of those is the sensible option here. However, a given diagnostic can have zero, one, or more than one notes supplied, right?. What is replaced for the "more than one" case, here? And would it be better to just accumulate this note onto the set, or are we indeed better off sticking with replacement here? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What the best approach is depends entirely on the situation, sadly (which tells me it should be configurable somehow). Currently I can see for example errors that would be very verbose where the underlying problem is very simple, so as a crate author I'd like to be able to almost entirely clear the diagnostic and clearly state "this is wrong and this is how you fix it", avoiding that messaging being lost in the noise. That being said there would have to be a consideration of whether this kind of "obscuring" of the emitted error could constitute a problem if misused. All this being said, I believe these are specific questions to be had around the stabilization of |
||
|
||
In addition the `on_unimplemented` attribute provides mechanisms to specify for which exact types a certain message should be emitted via an `if()` option. It accepts a set of filter options. A filter option consists of the generic parameter name from the trait definition and a type path against which the parameter should be checked. This type path could either be a fully qualified path or refer to any type in the current scope. As a special generic parameter name `Self` is added to refer to the `Self` type of the trait implementation. A filter option evaluates to `true` if the corresponding generic parameter in the trait definition matches the specified type. The provided `message`/`note`/`label` options are only emitted if the filter operation evaluates to `true`. | ||
|
||
The `any` and `all` options allow to combine multiple filter options. The `any` option matches if one of the supplied filter options evaluates to `true`, the `all` option requires that all supplied filter options evaluate to true. `not` allows to negate a given filter option. It evaluates to `true` if the inner filter option evaluates to `false`. These options can be nested to construct complex filters. | ||
|
||
The `on_unimplemented` attribute can be applied multiple times to the same trait definition. Multiple attributes are evaluated in order. The first matching instance for each of the `message`/`label`/`note` options is emitted. The compiler should provide a warn-by-default lint for ignored variants as this is the case for `match` arms. | ||
```rust | ||
#[diagnostic::on_unimplemented( | ||
if(Self = std::string::String), | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If we ever had where clause equality syntax, do we think it would look like this? Does anyone in @rust-lang/lang know? I mostly want there to be sharing of existing work on possible syntax ideas, for a higher probability of convergence in the long term. I especially don't want to block the RFC on finalizing syntax for a nonexistent feature ;) |
||
note = "That's only emitted if Self == std::string::String" | ||
)] | ||
#[diagnostic::on_unimplemented( | ||
if(A = String), // Refers to whatever `String` is in the current scope | ||
note = "That's only emitted if A == String", | ||
)] | ||
#[diagnostic::on_unimplemented( | ||
if(any(A = i32, Self = i32)), | ||
note = "That's emitted if A or Self is a i32", | ||
)] | ||
Comment on lines
+132
to
+143
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should this check whether a type has the name String or whether it is String? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I would anticipate that There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I propose to use the regular visibility rules here instead of just matching the name as string as that's currently done. I feel that aligns the proposed attribute more with other parts of rust. |
||
// this attribute will not have any affect as | ||
// the attribute above will always match before | ||
#[diagnostic::on_unimplemented( | ||
if(all(A = i32, Self = i32)), | ||
note = "That's emitted if A and Self is a i32" | ||
)] | ||
#[diagnostic::on_unimplemented( | ||
if(not(A = String)), | ||
// and implicitly `A` is not a `i32` as that case is | ||
// matched earlier | ||
note = "That's emitted if A is not a `String`" | ||
)] | ||
#[diagnostic::on_unimplemented( | ||
message="message", | ||
label="label", | ||
note="That's emitted if neither of the condition above are meet", | ||
)] | ||
trait MyIterator<A> { | ||
fn next(&mut self) -> A; | ||
} | ||
``` | ||
|
||
# Drawbacks | ||
[drawbacks]: #drawbacks | ||
|
||
A possible drawback is that this feature adds additional complexity to the compiler implementation. The compiler needs to handle an additional attribute namespace with at least one additional attribute. | ||
|
||
Another drawback is that crates might hint lower quality error messages than the compiler itself. Technically the compiler would be free to ignore such hints, practically I would assume that it is impossible to judge the quality of such error messages in an automated way. | ||
|
||
# Rationale and alternatives | ||
[rationale-and-alternatives]: #rationale-and-alternatives | ||
|
||
This proposal tries to improve error messages generated by rustc. It would give crate authors a tool to influence what error message is emitted in a certain situation, as they might sometimes want to provide specific details on certain error conditions. Not implementing this proposal would result in the current status quo. Currently the compiler always shows a "general" error message, even if it would be helpful to show additional details. | ||
|
||
There are alternatives for the naming of the `#[diagnostic]` namespace: | ||
|
||
* Use a `#[rustc]` namespace for these attributes. This would signifies that these are rustc specific extensions. We likely want to encourage other rust implementations to utilise these information as well, therefore a more general attribute namespace seems to be a better solution. | ||
|
||
There are alternative designs for the proposed `on_unimplemented` attribute: | ||
|
||
* The `on()` based filtering might be replaceable by placing the attribute on negative trait impls. This would turn a filter like | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Good catch. That should be address in cb8f2fc |
||
```rust | ||
#[diagnostic::on_unimplemented( | ||
on(Self = `String`, message = "Strings do not implement `IntoIterator` directly") | ||
)] | ||
trait IntoIterator {} | ||
``` | ||
|
||
into the following negative trait impl: | ||
```rust | ||
#[diagnostic::on_unimplemented(message = "Strings do not implement `IntoIterator` directly")] | ||
impl !IntoIterator for String {} | ||
``` | ||
|
||
This would simplify the syntax of the proposed attribute, but in turn block the implementation of type based filtering on the stabilization of `negative_impls`. On the other hand it would likely simplify writing more complex filters, that match only a certain generic set of types and it would prevent "duplicating" the filter-logic as this reuses the exiting trait system. To express complex filtering logic this would likely need some sort of `specialization` for at least negative trait implementations. A second disadvantage of this approach is that it couples error messages to the crates public API. Removing a negative trait impl is a breaking change, removing a `#[on_unimplemented]` attribute is only a change in the emitted compiler error. | ||
|
||
|
||
# Prior art | ||
[prior-art]: #prior-art | ||
|
||
* [rustc_on_unimplemented](https://rustc-dev-guide.rust-lang.org/diagnostics.html#rustc_on_unimplemented) already provides the described functionality as rustc internal attribute. It is used for improving error messages for various standard library API's. [This repo](https://github.com/weiznich/rust-foundation-community-grant/) contains several examples on how this attribute can be used in external crates to improve their error messages. | ||
* [GHC](https://ghc.gitlab.haskell.org/ghc/doc/users_guide/exts/type_errors.html) provides a Haskell mechanism for specifying custom compile time errors | ||
|
||
Notably all of the listed similar features are unofficial language extensions. | ||
|
||
# Unresolved questions | ||
[unresolved-questions]: #unresolved-questions | ||
|
||
Clarify the procedure of various potential changes prior stabilisation of the attribute namespace: | ||
|
||
* Exact syntax of the `on_unimplemented` attribute | ||
+ Can `Self` be accepted in that position or do we need another name? | ||
+ Is `if()` a valid identifier in the proposed position? | ||
|
||
# Future possibilities | ||
[future-possibilities]: #future-possibilities | ||
|
||
* Add a versioning scheme | ||
+ For specific attributes | ||
+ For the namespace | ||
+ Based on editions | ||
+ Custom versioning scheme | ||
* More attributes like `#[diagnostics::on_type_error]` | ||
* Extend the `#[diagnostics::on_unimplemented]` attribute to incorporate the semantics of `#[do_not_recommend]` or | ||
provide a distinct `#[diagnostics::do_not_recommend]` attribute | ||
* Un-RFC `#[do_not_recommend]`? | ||
* Apply `#[diagnostics::on_unimplemented]` to types as well | ||
* Extend the `if()` filter syntax to allow more complex filter expressions | ||
* Allow `#[diagnostic::on_unimplemented]` to be placed on types instead of traits. This would allow third party crates to customize the error messages emitted for unsatisfied trait bounds with out of crate traits. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good catch, that's something I've missed after updating the RFC to incorporate a version mechanism. Should I push an update now or should I wait for the proposed design meeting to happen?