mirror of
https://github.com/carbon-language/carbon-lang.git
synced 2026-11-15 16:48:10 +00:00
Lexer diagnostics used a single `const char*` as their source location, which only allowed pointing at a single character rather than a run of characters. This limited the underlining of unrecognized tokens, and also made escape sequence diagnostics particularly strange by stopping at whatever point the lexer discovered the error and pointing only at that location. For example: `"\w"` was reported at the `w`, and `"\x"` at the closing `"`. This change tries to always keep the primary location of a diagnostic the point at which typing (inserting or deleting) might fix the error. So for `"\x"`, `\x` is underlined, but the position is at the `"` as that's where you would type a hex code. The lexer's emitter now takes a special lexing source location that represents either a single position _between_ two characters, a single character, or a range of the source buffer. The lexer diagnostics are updated to construct these for the character range as appropriate. Parser diagnostics for a missing token were located at the _next_ token, which is particularly confusing for terminators like missing `;`s. Now they are located at a position after the previous token, with or without a space depending on the token, but typically on the same line. Diagnostics still sort by how far the parser has read. The parser diagnostics for unclosed groups also now attaches a label to the opening token when it is on a separate line. And the diagnostics for mismatched indent in a multi-line string attaches a label to the closing `'''` that establishes that indent. There are likely more location improvements we should do here, but this seemed like a reasonable incremental improvement and is what Claude found when doing an initial audit. One example where I think we should make further improvements is around suggesting raw identifiers when we find an unexpected `Cpp` or `Core` or `base`. We do this consistently now, but raw identifiers should be rare. A TODO is added for these cases. Assisted-by: Claude Code --------- Co-authored-by: Richard Smith <richard@metafoo.co.uk>
305 lines
12 KiB
C++
305 lines
12 KiB
C++
// Part of the Carbon Language project, under the Apache License v2.0 with LLVM
|
|
// Exceptions. See /LICENSE for license information.
|
|
// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
|
|
|
|
#include "toolchain/base/shared_value_stores.h"
|
|
#include "toolchain/diagnostics/format_providers.h"
|
|
#include "toolchain/lex/token_kind.h"
|
|
#include "toolchain/lex/tokenized_buffer.h"
|
|
#include "toolchain/parse/context.h"
|
|
#include "toolchain/parse/handle.h"
|
|
#include "toolchain/parse/node_ids.h"
|
|
#include "toolchain/parse/node_kind.h"
|
|
|
|
namespace Carbon::Parse {
|
|
|
|
// Provides common error exiting logic that skips to the semi, if present.
|
|
static auto OnParseError(Context& context, Context::State state,
|
|
NodeKind declaration) -> void {
|
|
context.AddNode(declaration, context.SkipPastLikelyEnd(state.token),
|
|
/*has_error=*/true);
|
|
}
|
|
|
|
// Finds the specified modifier within the introducer of the given declaration,
|
|
// returning `None` when it isn't there.
|
|
// TODO: Restructure how we handle packaging declarations to avoid the need to
|
|
// do this.
|
|
static auto FindModifier(Context& context, Context::State state,
|
|
Lex::TokenKind modifier) -> Lex::TokenIndex {
|
|
for (Lex::TokenIterator it(state.token); it != context.position(); ++it) {
|
|
if (context.tokens().GetKind(*it) == modifier) {
|
|
return *it;
|
|
}
|
|
}
|
|
return Lex::TokenIndex::None;
|
|
}
|
|
|
|
// Handles everything after the declaration's introducer.
|
|
template <const Parse::NodeKind& DeclKind>
|
|
static auto HandleDeclContent(Context& context, Context::State state,
|
|
Lex::TokenIndex export_token, bool is_impl,
|
|
llvm::function_ref<auto()->void> on_parse_error)
|
|
-> void {
|
|
Tree::PackagingNames names = {.is_export = export_token.has_value()};
|
|
|
|
// Parse the package name.
|
|
if (DeclKind == NodeKind::LibraryDecl ||
|
|
(DeclKind == NodeKind::ImportDecl &&
|
|
context.PositionIs(Lex::TokenKind::Library))) {
|
|
// This is either `library ...` or `import library ...`, so no package name
|
|
// is expected.
|
|
} else {
|
|
// We require a package name. This is either an identifier or package name
|
|
// keyword.
|
|
if (auto ident = context.ConsumeIf(Lex::TokenKind::Identifier)) {
|
|
names.package_id =
|
|
PackageNameId::ForIdentifier(context.tokens().GetIdentifier(*ident));
|
|
context.AddLeafNode(NodeKind::IdentifierPackageName, *ident);
|
|
} else if (auto core = context.ConsumeIf(Lex::TokenKind::Core)) {
|
|
names.package_id = PackageNameId::Core;
|
|
context.AddLeafNode(NodeKind::CorePackageName, *core);
|
|
} else if (auto cpp = context.ConsumeIf(Lex::TokenKind::Cpp)) {
|
|
names.package_id = PackageNameId::Cpp;
|
|
context.AddLeafNode(NodeKind::CppPackageName, *cpp);
|
|
} else {
|
|
// A word that can't be a name, followed by a token that can follow one,
|
|
// was most likely meant as the name, and is what the message is about.
|
|
// Otherwise the name is missing, and goes after the token before it.
|
|
bool recover_as_raw =
|
|
context.PositionKind().is_word() &&
|
|
context.PositionKind(Lookahead::NextToken)
|
|
.IsOneOf({Lex::TokenKind::Semi, Lex::TokenKind::Library});
|
|
CARBON_DIAGNOSTIC(
|
|
ExpectedIdentifierAfterPackage, Error,
|
|
"expected identifier after `package`"
|
|
"{0:; prefix reserved word with `r#` to form a valid identifier|}",
|
|
Diagnostics::BoolAsSelect);
|
|
CARBON_DIAGNOSTIC(
|
|
ExpectedIdentifierAfterImport, Error,
|
|
"expected identifier or `library` after `import`"
|
|
"{0:; prefix reserved word with `r#` to form a valid identifier|}",
|
|
Diagnostics::BoolAsSelect);
|
|
context.emitter().Emit(recover_as_raw ? DiagnosticLoc(*context.position())
|
|
: context.PointAfterPreviousToken(),
|
|
DeclKind == NodeKind::PackageDecl
|
|
? ExpectedIdentifierAfterPackage
|
|
: ExpectedIdentifierAfterImport,
|
|
recover_as_raw);
|
|
if (!recover_as_raw) {
|
|
on_parse_error();
|
|
return;
|
|
}
|
|
auto ident = context.tokens().AddPostLexingRecoveryTokenAsIdentifier(
|
|
context.Consume());
|
|
names.package_id =
|
|
PackageNameId::ForIdentifier(context.tokens().GetIdentifier(ident));
|
|
context.AddLeafNode(NodeKind::IdentifierPackageName, ident);
|
|
}
|
|
|
|
if (names.is_export) {
|
|
names.is_export = false;
|
|
state.has_error = true;
|
|
|
|
CARBON_DIAGNOSTIC(ExportImportPackage, Error,
|
|
"`export` cannot be used when importing a package");
|
|
context.emitter().Emit(export_token, ExportImportPackage);
|
|
}
|
|
}
|
|
|
|
// Parse the optional library keyword.
|
|
bool accept_default = !names.package_id.has_value();
|
|
if constexpr (DeclKind == NodeKind::LibraryDecl) {
|
|
auto library_id = context.ParseLibraryName(accept_default);
|
|
if (!library_id) {
|
|
on_parse_error();
|
|
return;
|
|
}
|
|
names.library_id = *library_id;
|
|
} else {
|
|
auto next_kind = context.PositionKind();
|
|
if (next_kind == Lex::TokenKind::Library) {
|
|
if (auto library_id = context.ParseLibrarySpecifier(accept_default)) {
|
|
names.library_id = *library_id;
|
|
} else {
|
|
on_parse_error();
|
|
return;
|
|
}
|
|
} else if (DeclKind == NodeKind::ImportDecl &&
|
|
next_kind == Lex::TokenKind::Inline) {
|
|
auto inline_token = context.ConsumeChecked(Lex::TokenKind::Inline);
|
|
auto body_position = *context.position();
|
|
auto inline_body_token = context.ConsumeIf(Lex::TokenKind::StringLiteral);
|
|
names.inline_body_id = context.AddNode<NodeKind::InlineImportBody>(
|
|
body_position, /*has_error=*/!inline_body_token);
|
|
context.AddNode(NodeKind::InlineImportSpecifier, inline_token,
|
|
/*has_error=*/false);
|
|
if (!inline_body_token) {
|
|
CARBON_DIAGNOSTIC(ExpectedStringAfterInline, Error,
|
|
"expected string literal after `inline`");
|
|
context.emitter().Emit(context.PointAfterPreviousToken(),
|
|
ExpectedStringAfterInline);
|
|
on_parse_error();
|
|
return;
|
|
}
|
|
} else if (next_kind == Lex::TokenKind::StringLiteral ||
|
|
(accept_default && next_kind == Lex::TokenKind::Default)) {
|
|
// If we come across a string literal and we didn't parse `library
|
|
// "..."` yet, then most probably the user forgot to add `library`
|
|
// before the library name.
|
|
CARBON_DIAGNOSTIC(MissingLibraryKeyword, Error,
|
|
"missing `library` keyword");
|
|
context.emitter().Emit(context.PointAfterPreviousToken(),
|
|
MissingLibraryKeyword);
|
|
on_parse_error();
|
|
return;
|
|
}
|
|
}
|
|
|
|
if (auto semi = context.ConsumeIf(Lex::TokenKind::Semi)) {
|
|
names.node_id = context.AddNode<DeclKind>(*semi, state.has_error);
|
|
|
|
if constexpr (DeclKind == NodeKind::ImportDecl) {
|
|
context.AddImport(names);
|
|
} else {
|
|
context.set_packaging_decl(names, is_impl);
|
|
}
|
|
} else {
|
|
context.DiagnoseExpectedDeclSemi(context.tokens().GetKind(state.token));
|
|
on_parse_error();
|
|
}
|
|
}
|
|
|
|
// Returns true if currently in a valid state for imports, false otherwise. May
|
|
// update the packaging state respectively.
|
|
static auto VerifyInImports(Context& context, Lex::TokenIndex intro_token)
|
|
-> bool {
|
|
switch (context.packaging_state()) {
|
|
case Context::PackagingState::FileStart:
|
|
// `package` is no longer allowed, but `import` may repeat.
|
|
context.set_packaging_state(Context::PackagingState::InImports);
|
|
return true;
|
|
|
|
case Context::PackagingState::InImports:
|
|
return true;
|
|
|
|
case Context::PackagingState::AfterNonPackagingDecl: {
|
|
context.set_packaging_state(
|
|
Context::PackagingState::InImportsAfterNonPackagingDecl);
|
|
CARBON_DIAGNOSTIC(ImportTooLate, Error,
|
|
"`import` declarations must come after the `package` "
|
|
"declaration (if present) and before any other "
|
|
"entities in the file");
|
|
CARBON_DIAGNOSTIC_LABEL(FirstDecl, Info, "first declaration is here");
|
|
context.emitter()
|
|
.Build(intro_token, ImportTooLate)
|
|
.Attach(context.first_non_packaging_token(), FirstDecl)
|
|
.Emit();
|
|
return false;
|
|
}
|
|
|
|
case Context::PackagingState::InImportsAfterNonPackagingDecl:
|
|
// There is a sequential block of misplaced `import` statements, which can
|
|
// occur if a declaration is added above `import`s. Avoid duplicate
|
|
// warnings.
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// Diagnoses if `export` is used in an `impl` file.
|
|
static auto RestrictExportToApi(Context& context, Context::State& state)
|
|
-> void {
|
|
// Error for both Main//default and every implementation file.
|
|
auto packaging = context.tree().packaging_decl();
|
|
if (!packaging || packaging->is_impl) {
|
|
CARBON_DIAGNOSTIC(ExportFromImpl, Error,
|
|
"`export` is only allowed in API files");
|
|
context.emitter().Emit(state.token, ExportFromImpl);
|
|
state.has_error = true;
|
|
}
|
|
}
|
|
|
|
auto HandleImport(Context& context) -> void {
|
|
auto state = context.PopState();
|
|
|
|
auto on_parse_error = [&] {
|
|
OnParseError(context, state, NodeKind::ImportDecl);
|
|
};
|
|
|
|
if (VerifyInImports(context, state.token)) {
|
|
// Scan the modifiers to see if this import declaration is exported.
|
|
auto export_token = FindModifier(context, state, Lex::TokenKind::Export);
|
|
if (export_token.has_value()) {
|
|
RestrictExportToApi(context, state);
|
|
}
|
|
|
|
HandleDeclContent<NodeKind::ImportDecl>(context, state, export_token,
|
|
/*is_impl=*/false, on_parse_error);
|
|
} else {
|
|
on_parse_error();
|
|
}
|
|
}
|
|
|
|
auto HandleExportName(Context& context) -> void {
|
|
auto state = context.PopState();
|
|
|
|
RestrictExportToApi(context, state);
|
|
|
|
context.PushState(state, StateKind::ExportNameFinish);
|
|
context.PushState(StateKind::DeclNameAndParams, state.token,
|
|
BindingContext::CompileTimeEntityParam);
|
|
}
|
|
|
|
auto HandleExportNameFinish(Context& context) -> void {
|
|
auto state = context.PopState();
|
|
|
|
context.AddNodeExpectingDeclSemi(state, NodeKind::ExportDecl,
|
|
Lex::TokenKind::Export,
|
|
/*is_def_allowed=*/false);
|
|
}
|
|
|
|
// Handles common logic for `package` and `library`.
|
|
template <const Parse::NodeKind& DeclKind>
|
|
static auto HandlePackageAndLibraryDecls(Context& context,
|
|
Lex::TokenKind intro_token_kind)
|
|
-> void {
|
|
auto state = context.PopState();
|
|
|
|
bool is_impl = FindModifier(context, state, Lex::TokenKind::Impl).has_value();
|
|
|
|
auto on_parse_error = [&] { OnParseError(context, state, DeclKind); };
|
|
|
|
if (state.token != Lex::TokenIndex::FirstNonCommentToken) {
|
|
CARBON_DIAGNOSTIC(
|
|
PackageTooLate, Error,
|
|
"the `{0}` declaration must be the first non-comment line",
|
|
Lex::TokenKind);
|
|
CARBON_DIAGNOSTIC_LABEL(FirstNonCommentLine, Info,
|
|
"first non-comment line is here");
|
|
context.emitter()
|
|
.Build(state.token, PackageTooLate, intro_token_kind)
|
|
.Attach(Lex::TokenIndex::FirstNonCommentToken, FirstNonCommentLine)
|
|
.Emit();
|
|
on_parse_error();
|
|
return;
|
|
}
|
|
|
|
// `package`/`library` is no longer allowed, but `import` may repeat.
|
|
context.set_packaging_state(Context::PackagingState::InImports);
|
|
|
|
HandleDeclContent<DeclKind>(context, state,
|
|
/*export_token=*/Lex::TokenIndex::None, is_impl,
|
|
on_parse_error);
|
|
}
|
|
|
|
auto HandlePackage(Context& context) -> void {
|
|
HandlePackageAndLibraryDecls<NodeKind::PackageDecl>(context,
|
|
Lex::TokenKind::Package);
|
|
}
|
|
|
|
auto HandleLibrary(Context& context) -> void {
|
|
HandlePackageAndLibraryDecls<NodeKind::LibraryDecl>(context,
|
|
Lex::TokenKind::Library);
|
|
}
|
|
|
|
} // namespace Carbon::Parse
|