Skip to content

Commit caa6316

Browse files
authored
Merge pull request #28052 from owenv/educational-notes
[Diagnostics] Introduce "Educational Notes" for diagnostics
2 parents e33ed77 + da7e1ca commit caa6316

15 files changed

+138
-1
lines changed

CMakeLists.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1129,6 +1129,8 @@ endif()
11291129

11301130
add_subdirectory(utils)
11311131

1132+
add_subdirectory(userdocs)
1133+
11321134
if ("${CMAKE_SYSTEM_NAME}" STREQUAL "Darwin")
11331135
if(SWIFT_BUILD_PERF_TESTSUITE)
11341136
add_subdirectory(benchmark)

docs/Diagnostics.md

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,25 @@ The Swift compiler has a setting (under LangOptions) called `DiagnosticsEditorMo
9090

9191
Most diagnostics have no reason to change behavior under editor mode. An example of an exception is the "protocol requirements not satisfied diagnostic"; on the command line, it may be better to show all unsatisfied requirements, while in an IDE a single multi-line fix-it would be preferred.
9292

93+
### Educational Notes ###
94+
95+
**Note**: This feature is currently experimental. It can be enabled by passing the `-Xfrontend -enable-descriptive-diagnostics` flag.
96+
97+
Educational notes are small snippets of documentation attached to a diagnostic which explain relevant language concepts. They are intended to further Swift's goal of progressive disclosure by providing a learning resource at the point of use for users encountering a new error message for the first time. In very limited circumstances, they also allow the main diagnostic message to use more precise and correct terminology (e.g. nominal types) which would otherwise be too unfriendly for beginners.
98+
99+
When outputting diagnostics on the command line, educational notes will be printed after the main diagnostic body if descriptive diagnostics are enabled. When presented in an IDE, it's expected they will be collapsed under a disclosure arrow, info button, or similar to avoid cluttering output.
100+
101+
Generally speaking, a diagnostic should try to provide educational notes for any concepts/terminology which is difficult to understand from context or is especially subtle. Educational notes should:
102+
- Explain a single language concept. This makes them easy to reuse across diagnostics and helps keep them clear, concise, and easy to understand.
103+
- Be written in unabbreviated English. These are longer form messages compared to the main diagnostic, so there is no need to omit needless words and punctuation.
104+
- Not generally exceed 3-4 paragraphs. Educational notes should be clear and easily digestible. Messages which are too long also have the potential to create diagnostics UX issues in some contexts.
105+
- Be accessible. Educational notes should be beginner friendly and avoid assuming unnecesary prior knowledge. The goal is not only to help users understand what a diagnostic is telling them, but also to turn errors and warnings into "teachable moments".
106+
- Include references to relevant chapters of _The Swift Programming Language_ if applicable.
107+
- Be written in Markdown, but avoid excessive markup to avoid impacting the terminal UX.
108+
109+
To add a new educational note:
110+
1. Add a new Markdown file in the `userdocs/diagnostics/` directory containing the contents of the note.
111+
2. Associate the note with one or more diagnostics in EducationalNotes.def.
93112

94113
### Format Specifiers ###
95114

include/swift/AST/DiagnosticConsumer.h

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,9 @@ struct DiagnosticInfo {
5757
/// DiagnosticInfo of notes which are children of this diagnostic, if any
5858
ArrayRef<DiagnosticInfo *> ChildDiagnosticInfo;
5959

60+
/// Paths to "educational note" diagnostic documentation in the toolchain.
61+
ArrayRef<std::string> EducationalNotePaths;
62+
6063
/// Represents a fix-it, a replacement of one range of text with another.
6164
class FixIt {
6265
CharSourceRange Range;

include/swift/AST/DiagnosticEngine.h

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -673,6 +673,9 @@ namespace swift {
673673
/// Use descriptive diagnostic style when available.
674674
bool useDescriptiveDiagnostics = false;
675675

676+
/// Path to diagnostic documentation directory.
677+
std::string diagnosticDocumentationPath = "";
678+
676679
friend class InFlightDiagnostic;
677680
friend class DiagnosticTransaction;
678681
friend class CompoundDiagnosticTransaction;
@@ -723,6 +726,13 @@ namespace swift {
723726
return useDescriptiveDiagnostics;
724727
}
725728

729+
void setDiagnosticDocumentationPath(std::string path) {
730+
diagnosticDocumentationPath = path;
731+
}
732+
StringRef getDiagnosticDocumentationPath() {
733+
return diagnosticDocumentationPath;
734+
}
735+
726736
void ignoreDiagnostic(DiagID id) {
727737
state.setDiagnosticBehavior(id, DiagnosticState::Behavior::Ignore);
728738
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
//===-- EducationalNotes.def - Diagnostic Documentation Content -*- C++ -*-===//
2+
//
3+
// This source file is part of the Swift.org open source project
4+
//
5+
// Copyright (c) 2014 - 2019 Apple Inc. and the Swift project authors
6+
// Licensed under Apache License v2.0 with Runtime Library Exception
7+
//
8+
// See https://swift.org/LICENSE.txt for license information
9+
// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
10+
//
11+
//===----------------------------------------------------------------------===//
12+
//
13+
// This file associates diagnostics with relevant educational notes which
14+
// explain important concepts.
15+
//
16+
//===----------------------------------------------------------------------===//
17+
18+
#ifndef EDUCATIONAL_NOTES
19+
# error Must define EDUCATIONAL_NOTES
20+
#endif
21+
22+
// EDUCATIONAL_NOTES(DIAG_ID, EDUCATIONAL_NOTE_FILENAMES...)
23+
24+
EDUCATIONAL_NOTES(non_nominal_no_initializers, "nominal-types.md")
25+
EDUCATIONAL_NOTES(non_nominal_extension, "nominal-types.md")
26+
EDUCATIONAL_NOTES(associated_type_witness_conform_impossible,
27+
"nominal-types.md")
28+
29+
#undef EDUCATIONAL_NOTES

include/swift/Basic/DiagnosticOptions.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,8 @@ class DiagnosticOptions {
5959
/// Descriptive diagnostic output is not intended to be machine-readable.
6060
bool EnableDescriptiveDiagnostics = false;
6161

62+
std::string DiagnosticDocumentationPath = "";
63+
6264
/// Return a hash code of any components from these options that should
6365
/// contribute to a Swift Bridging PCH hash.
6466
llvm::hash_code getPCHHashComponents() const {

include/swift/Option/FrontendOptions.td

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,10 @@ def show_diagnostics_after_fatal : Flag<["-"], "show-diagnostics-after-fatal">,
115115

116116
def enable_descriptive_diagnostics : Flag<["-"], "enable-descriptive-diagnostics">,
117117
HelpText<"Show descriptive diagnostic information, if available.">;
118+
119+
def diagnostic_documentation_path
120+
: Separate<["-"], "diagnostic-documentation-path">, MetaVarName<"<path>">,
121+
HelpText<"Path to diagnostic documentation resources">;
118122

119123
def enable_swiftcall : Flag<["-"], "enable-swiftcall">,
120124
HelpText<"Enable the use of LLVM swiftcall support">;

lib/AST/DiagnosticEngine.cpp

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,18 @@ static constexpr const char *const fixItStrings[] = {
116116
"<not a fix-it>",
117117
};
118118

119+
#define EDUCATIONAL_NOTES(DIAG, ...) \
120+
static constexpr const char *const DIAG##_educationalNotes[] = {__VA_ARGS__, \
121+
nullptr};
122+
#include "swift/AST/EducationalNotes.def"
123+
124+
static constexpr const char *const *educationalNotes[LocalDiagID::NumDiags]{
125+
[LocalDiagID::invalid_diagnostic] = {},
126+
#define EDUCATIONAL_NOTES(DIAG, ...) \
127+
[LocalDiagID::DIAG] = DIAG##_educationalNotes,
128+
#include "swift/AST/EducationalNotes.def"
129+
};
130+
119131
DiagnosticState::DiagnosticState() {
120132
// Initialize our per-diagnostic state to default
121133
perDiagnosticBehavior.resize(LocalDiagID::NumDiags, Behavior::Unspecified);
@@ -951,6 +963,19 @@ void DiagnosticEngine::emitDiagnostic(const Diagnostic &diagnostic) {
951963
}
952964
}
953965
info->ChildDiagnosticInfo = childInfoPtrs;
966+
967+
SmallVector<std::string, 1> educationalNotePaths;
968+
if (useDescriptiveDiagnostics) {
969+
auto associatedNotes = educationalNotes[(uint32_t)diagnostic.getID()];
970+
while (associatedNotes && *associatedNotes) {
971+
SmallString<128> notePath(getDiagnosticDocumentationPath());
972+
llvm::sys::path::append(notePath, *associatedNotes);
973+
educationalNotePaths.push_back(notePath.str());
974+
associatedNotes++;
975+
}
976+
info->EducationalNotePaths = educationalNotePaths;
977+
}
978+
954979
for (auto &consumer : Consumers) {
955980
consumer->handleDiagnostic(SourceMgr, *info);
956981
}

lib/Frontend/CompilerInvocation.cpp

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,13 @@ void CompilerInvocation::setMainExecutablePath(StringRef Path) {
4444
llvm::sys::path::remove_filename(LibPath); // Remove /bin
4545
llvm::sys::path::append(LibPath, "lib", "swift");
4646
setRuntimeResourcePath(LibPath.str());
47+
48+
llvm::SmallString<128> DiagnosticDocsPath(Path);
49+
llvm::sys::path::remove_filename(DiagnosticDocsPath); // Remove /swift
50+
llvm::sys::path::remove_filename(DiagnosticDocsPath); // Remove /bin
51+
llvm::sys::path::append(DiagnosticDocsPath, "share", "doc", "swift",
52+
"diagnostics");
53+
DiagnosticOpts.DiagnosticDocumentationPath = DiagnosticDocsPath.str();
4754
}
4855

4956
/// If we haven't explicitly passed -prebuilt-module-cache-path, set it to
@@ -684,7 +691,9 @@ static bool ParseDiagnosticArgs(DiagnosticOptions &Opts, ArgList &Args,
684691
Opts.PrintDiagnosticNames |= Args.hasArg(OPT_debug_diagnostic_names);
685692
Opts.EnableDescriptiveDiagnostics |=
686693
Args.hasArg(OPT_enable_descriptive_diagnostics);
687-
694+
if (Arg *A = Args.getLastArg(OPT_diagnostic_documentation_path)) {
695+
Opts.DiagnosticDocumentationPath = A->getValue();
696+
}
688697
assert(!(Opts.WarningsAsErrors && Opts.SuppressWarnings) &&
689698
"conflicting arguments; should have been caught by driver");
690699

lib/Frontend/Frontend.cpp

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -319,6 +319,8 @@ void CompilerInstance::setUpDiagnosticOptions() {
319319
if (Invocation.getDiagnosticOptions().EnableDescriptiveDiagnostics) {
320320
Diagnostics.setUseDescriptiveDiagnostics(true);
321321
}
322+
Diagnostics.setDiagnosticDocumentationPath(
323+
Invocation.getDiagnosticOptions().DiagnosticDocumentationPath);
322324
}
323325

324326
// The ordering of ModuleLoaders is important!

lib/Frontend/PrintingDiagnosticConsumer.cpp

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,10 @@ void PrintingDiagnosticConsumer::handleDiagnostic(SourceManager &SM,
6969
return;
7070

7171
printDiagnostic(SM, Info);
72+
for (auto path : Info.EducationalNotePaths) {
73+
if (auto buffer = SM.getFileSystem()->getBufferForFile(path))
74+
Stream << buffer->get()->getBuffer() << "\n";
75+
}
7276

7377
for (auto ChildInfo : Info.ChildDiagnosticInfo) {
7478
printDiagnostic(SM, *ChildInfo);
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
// RUN: not %target-swift-frontend -enable-descriptive-diagnostics -diagnostic-documentation-path %S/test-docs/ -typecheck %s 2>&1 | %FileCheck %s
2+
3+
// A diagnostic with no educational notes
4+
let x = 1 +
5+
// CHECK: error: expected expression after operator
6+
// CHECK-NOT: {{-+$}}
7+
8+
// A diagnostic with an educational note
9+
extension (Int, Int) {}
10+
// CHECK: error: non-nominal type '(Int, Int)' cannot be extended
11+
// CHECK-NEXT: extension (Int, Int) {}
12+
// CHECK-NEXT: ^ ~~~~~~~~~~
13+
// CHECK-NEXT: Nominal Types
14+
// CHECK-NEXT: -------------
15+
// CHECK-NEXT: Nominal types documentation content
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
Nominal Types
2+
-------------
3+
Nominal types documentation content

userdocs/CMakeLists.txt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
swift_install_in_component(DIRECTORY diagnostics
2+
DESTINATION "share/doc/swift"
3+
COMPONENT compiler)

userdocs/diagnostics/nominal-types.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
Nominal types
2+
-------------
3+
In Swift, a type is considered a nominal type if it is named. In other words, it has been defined by declaring the type somewhere in code. Examples of nominal types include classes, structs and enums, all of which must be declared before using them. Nominal types are an important concept in Swift because they can be extended, explicitly initialized using the 'MyType()' syntax, and may conform to protocols.
4+
5+
In contrast, non-nominal types have none of these capabilities. A non-nominal type is any type which is not nominal. They are sometimes called ”structural types” because they are usually obtained by composing other types. Examples include function types like '(Int) -> (String)', tuple types like '(Int, String)', metatypes like 'Int.Type', and special types like 'Any' and 'AnyObject'.
6+
7+
Whether the name of a protocol refers to a nominal or non-nominal type depends on where it appears in code. When used in a declaration or extension like 'extension MyProtocol { … }', 'MyProtocol' refers to a protocol type, which is nominal. This means that it may be extended and conform to protocols. However, when written as the type of a constant or variable, MyProtocol instead refers to a non-nominal, existential type. As a result, code like 'let value: MyProtocol = MyProtocol()' is not allowed because 'MyProtocol' refers to a non-nominal type in this context and cannot be explicitly initialized.

0 commit comments

Comments
 (0)