As a follow up from Future of Diagnostics UX a couple weeks back, I wanted to further explore the idea of "extended diagnostic messages" and see if they would be a good fit for Swift. What follows is a rough pitch for what that might look like. I'm not sure if this will/should result in an evolution proposal, but I think writing this up using the pitch format makes it easy to evaluate and discuss alternatives.
I'm interested in any and all feedback people have on whether they'd find this useful! I'm especially interested in hearing from anyone who has worked with Rust/Elm in the past and can comment on the benefits & drawbacks of their approaches to diagnostics.
Extended Diagnostic Messages
- Authors: Owen Voorhees
- Status: Pitch
Introduction
This document proposes adding support for a new form of extended diagnostic message to the compiler which can be exposed through the swift explain
command, or an IDE. These new messages will supplement (not replace) the existing diagnostics by providing additional context, examples, and references for beginners learning Swift for the first time and more experienced users when debugging unfamiliar issues. This approach is heavily inspired by that of the Rust and Elm programming languages.
Swift-evolution thread: Discussion thread topic for that proposal
Recommended Reading
- [1] Swift Diagnostics Style
- [2] Rust RFC #1567 - Rust Extended Error Messages Style
- [3] Compiler Errors for Humans - Elm
Motivation
The existing Swift diagnostics style[1], "aim[s] to fit a terse but clear description of an issue into a single line, ideally suitable for display both on the command line and in IDEs." Top level diagnostics (warnings and errors) are sometimes accompanied by additional notes and fix-its which provide additional information. This focus on precision and brevity has generally worked out well in practice from a user experience point of view. Most of the time, an experienced Swift programmer is able to quickly scan a diagnostic and identify the issue in their code.
However, there are a few cases where the existing diagnostics style may be insufficient. Most of these occur when a user, beginner or otherwise, encounters a new issue or language feature for the first time. For example, consider the following code which might be written by someone unfamiliar with protocol existentials:
protocol P {
var bar: Self { get }
}
func baz(p: P) {}
This code will result in the following compiler error:
protocol 'P' can only be used as a generic constraint because it has Self or associated type requirements
To be clear, this is a good error message and it follows the existing style guide well. It clearly points out the issue and briefly explains why the code was rejected. While an experienced Swift programmer may be able to fix this quite quickly, however, to a beginner this message can be frustratingly opaque. It assumes the programmer is familiar with both 'generic constraints' and 'associated type requirements', concepts a programmer might not have needed to learn before writing the above code snippet. Today, users frequently end up relying on Google, Stack Overflow, and other resources to understand unfamiliar error messages like this one (a search of Stack Overflow reveals over 200 similar questions about this error message). In the spirit of progressive disclosure, we should instead point users toward authoritative sources when they encounter errors.
Proposed Solution
To address the issues described above, I propose supplementing the existing diagnostics with 'extended error messages'(to borrow terminology from Rust). These are longer form explanations of errors and warnings which provide more-in depth information. The style of these messages and how they are accessed is described below.
Detailed Design
Accessing Extended Diagnostic Messages
Extended diagnostic messages may be accessed through a new command, swift explain
, followed by the name of an error or warning. For example, to access the extended error information for the error above, one could run swift explain unsupported_existential_type
.
Additionally, if the compiler is not in editor/IDE mode, when a compile finishes, it will emit a new remark which tells the user the name of any emitted diagnostics which have extended explanations:
remark: unsupported_existential_type has an additional explanation available; run `swift explain unsupported_existential_type` to view
This is very similar to Rust's existing model (rustc --explain EXXXX
).
TODO: How should these be exposed to IDEs/LSP?
Which Diagnostics Should Have Extended Messages?
Extended diagnostic messages are limited to errors and warnings only. Detailed descriptions of notes should be included in the extended message of their corresponding error or warning if desired.
As a general rule, all errors and warnings should provide an extended message if possible. However, exceptions may be made if a diagnostic's meaning is clear and obvious. Extended error messages should not be required when contributing new diagnostics to the compiler in order to preserve development velocity. However, if this is the case a bug should be filed to record the eventual need for an extended message.
Extended Diagnostic Message Style
-
Extended messages should be written in third-person, unabbreviated English. Like the regular diagnostic messages, they should focus on language rules which were violated as opposed to compiler failures.
-
Extended messages should be written in plain text. Code blocks should be denoted Markdown-style using triple backticks.
-
Code blocks included in messages should only use standard library and user-defined types if possible. Exceptions include, for example, using Foundation types in messages related to Objective-C bridging.
-
Code samples included in diagnostic messages should avoid assigning meaningless names like 'foo' and 'bar' to types and functions, as this can make them more difficult to understand. Instead, code samples should demonstrate realistic, if simplified, uses of language features. As a general rule of thumb, they should avoid exceeding 8-10 lines.
The following is a proposed outline for extended diagnostic messages, heavily inspired by Rust RFC #1567. Deviations from the established format should have some kind of compelling justification.
- The name of the diagnostic, enclosed in square brackets, followed by the standard diagnostic message restated in unabbreviated English on the same line. If the diagnostic is no longer emitted by the current compiler version, this should be noted, but the message should not be removed.
- A paragraph explaining the error/warning in greater detail. This longer explanation should describe the language rule which was violated and may suggest ways in which it could be resolved.
- An example of code which triggers the diagnostic, following the code sample guidelines above.
- The specific standard diagnostic message output that is emitted when compiling the provided code sample.
- A 1-2 sentence explanation of how the code sample might be fixed, followed by a fixed code sample. If there are multiple valid ways of fixing the example code which warrant individual explanations, they may be listed one after another.
- References to sections of The Swift Programming Language which explain concepts relevant to the diagnostic.
Using this style, the extended error message for the existential conformance issue above might be written as:
[unsupported_existential_type] A protocol with Self or associated type requirements can only be used as a generic constraint.
If a protocol 'P' has a requirement which references either Self or an associated type, then the type spelled 'P' does not conform to the protocol 'P' because it is unable to satisfy that requirement. As a result, 'P' cannot be used as the type of a variable, argument, return value, etc. It may be used to constrain the type of a generic parameter 'T' which represents a specific type.
Consider the following incorrect code:
protocol Copyable {
init(copying: Self)
}
func cacheCopy(of copyable: Copyable) { /* ... */ }
Which results in the following compiler error:
5:30:error: protocol 'Copyable' can only be used as a generic constraint because it has Self or associated type requirements
This function should probably be rewritten to instead accept a constrained generic argument. Consider the following working code:
protocol Copyable {
init(copying: Self)
}
func cacheCopy<T: Copyable>(of copyable: T) { /* ... */ }
To learn more about generic constraints and protocols with associated types, see the Generics section of The Swift Programming Language.
Alternatives considered
Do Nothing
Writing extended error messages for all of the existing diagnostics would be a large undertaking, and shouldn't be done lightly. It also creates more work each time a new diagnostic is added. We should consider whether time spent improving diagnostics would be better focused on other areas.
More Detailed 'Standard' Diagnostic Messages
One alternative to having both 'standard' and 'extended' messages is to instead adopt a new, more verbose style for existing messages. This would avoid needing to maintain two messages for each error and warning, and is roughly the model used by Elm[3]. However, this wouldn't necessarily be a good fit for Swift. It's unclear how they would affect the experience of IDE users, and potentially loses some of the benefits of the existing style as described in [1]. Additionally, the implementation complexity required to maintain correct messages in all edge cases is significantly higher due to the additional specificity compared to the proposed approach.
Future Directions
Swift Error Index
Rust has an online compiler error index which makes it easy to look up all of compiler's error codes. It also provides a link from each code sample to an online playground where user's can try out the erroneous and fixed code for themselves. If Swift adopts a similar extended message style, it would be nice to set up our own index as well.
Localized Extended Diagnostic Messages
If this feature is adopted, adding a --language
option or similar to the swift explain
command might be a good way to provide open-source reference material in languages other than English.