From bf106c3b4b77e164615311a98345639ecda7089d Mon Sep 17 00:00:00 2001 From: Dana Jansens Date: Fri, 10 Jul 2026 20:03:31 -0400 Subject: [PATCH] Handle missing curlies after `match_first` without crashing in parse (#7480) --- toolchain/diagnostics/kind.def | 1 + toolchain/parse/context.cpp | 16 ++++++++++ toolchain/parse/context.h | 8 +++++ toolchain/parse/handle_match_first.cpp | 14 ++++++-- .../testdata/generics/impl/match_first.carbon | 32 ++++++++++++++++++- 5 files changed, 67 insertions(+), 4 deletions(-) diff --git a/toolchain/diagnostics/kind.def b/toolchain/diagnostics/kind.def index 8f070774ac93..35a25e0bf30b 100644 --- a/toolchain/diagnostics/kind.def +++ b/toolchain/diagnostics/kind.def @@ -114,6 +114,7 @@ CARBON_DIAGNOSTIC_KIND(ExpectedIdentifierAfterPeriodOrArrow) CARBON_DIAGNOSTIC_KIND(ExpectedIdentifierOrSelfAfterPeriod) CARBON_DIAGNOSTIC_KIND(ExpectedBindingName) CARBON_DIAGNOSTIC_KIND(ExpectedBindingPattern) +CARBON_DIAGNOSTIC_KIND(ExpectedCurlyBraceAfter) CARBON_DIAGNOSTIC_KIND(ExpectedGenericBindingPatternAfterTemplate) CARBON_DIAGNOSTIC_KIND(ExpectedParenAfter) CARBON_DIAGNOSTIC_KIND(ExpectedRuntimeBindingPatternAfterRef) diff --git a/toolchain/parse/context.cpp b/toolchain/parse/context.cpp index c71316bdca07..c19b5566a8e4 100644 --- a/toolchain/parse/context.cpp +++ b/toolchain/parse/context.cpp @@ -52,6 +52,22 @@ auto Context::ReplacePlaceholderNode(int32_t position, NodeKind kind, *node_impl = Tree::NodeImpl(kind, has_error, token); } +auto Context::ConsumeAndAddOpenCurlyBrace(Lex::TokenIndex default_token, + NodeKind start_kind) + -> std::optional { + if (auto open_curly = ConsumeIf(Lex::TokenKind::OpenCurlyBrace)) { + AddLeafNode(start_kind, *open_curly, /*has_error=*/false); + return open_curly; + } else { + CARBON_DIAGNOSTIC(ExpectedCurlyBraceAfter, Error, + "expected `{` after `{0}`", Lex::TokenKind); + emitter_.Emit(*position_, ExpectedCurlyBraceAfter, + tokens().GetKind(default_token)); + AddLeafNode(start_kind, default_token, /*has_error=*/true); + return std::nullopt; + } +} + auto Context::ConsumeAndAddOpenParen(Lex::TokenIndex default_token, NodeKind start_kind) -> std::optional { diff --git a/toolchain/parse/context.h b/toolchain/parse/context.h index 8b980025fc19..0d5ec6e7fbf4 100644 --- a/toolchain/parse/context.h +++ b/toolchain/parse/context.h @@ -189,6 +189,14 @@ class Context { // Consumes the current token. Does not return it. auto ConsumeAndDiscard() -> void { ++position_; } + // Parses an open curly brace token, possibly diagnosing if necessary. Creates + // a leaf parse node of the specified start kind. The default_token is used + // when there's no open curly brace. Returns the open curly brace token if it + // was found. + auto ConsumeAndAddOpenCurlyBrace(Lex::TokenIndex default_token, + NodeKind start_kind) + -> std::optional; + // Parses an open paren token, possibly diagnosing if necessary. Creates a // leaf parse node of the specified start kind. The default_token is used when // there's no open paren. Returns the open paren token if it was found. diff --git a/toolchain/parse/handle_match_first.cpp b/toolchain/parse/handle_match_first.cpp index 7929485c0982..7a9cfd470706 100644 --- a/toolchain/parse/handle_match_first.cpp +++ b/toolchain/parse/handle_match_first.cpp @@ -2,6 +2,7 @@ // Exceptions. See /LICENSE for license information. // SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception +#include "toolchain/lex/token_kind.h" #include "toolchain/parse/context.h" #include "toolchain/parse/handle.h" @@ -10,10 +11,17 @@ namespace Carbon::Parse { auto HandleMatchFirst(Context& context) -> void { auto state = context.PopState(); // MatchFirstIntroducer node is automatically added for the MatchFirst state. - context.AddNode(NodeKind::MatchFirstDefinitionStart, context.Consume(), - state.has_error); + if (auto open_curly = context.ConsumeAndAddOpenCurlyBrace( + state.token, NodeKind::MatchFirstDefinitionStart)) { + state.token = *open_curly; + } else { + state.has_error = true; + } + context.PushState(state, StateKind::MatchFirstFinish); - context.PushState(StateKind::DeclScopeLoopAsRegular); + if (!state.has_error) { + context.PushState(StateKind::DeclScopeLoopAsRegular); + } } auto HandleMatchFirstFinish(Context& context) -> void { diff --git a/toolchain/parse/testdata/generics/impl/match_first.carbon b/toolchain/parse/testdata/generics/impl/match_first.carbon index 19835deee0e9..62d767da82a3 100644 --- a/toolchain/parse/testdata/generics/impl/match_first.carbon +++ b/toolchain/parse/testdata/generics/impl/match_first.carbon @@ -8,6 +8,20 @@ // TIP: To dump output, run: // TIP: bazel run //toolchain/testing:file_test -- --dump_output --file_tests=toolchain/parse/testdata/generics/impl/match_first.carbon +// --- empty.carbon + +match_first {} + +// --- fail_missing_braces.carbon + +// CHECK:STDERR: fail_missing_braces.carbon:[[@LINE+4]]:12: error: expected `{` after `match_first` [ExpectedCurlyBraceAfter] +// CHECK:STDERR: match_first; +// CHECK:STDERR: ^ +// CHECK:STDERR: +match_first; + +// --- with_impls.carbon + match_first { impl forall [T:! I] T as Interface { @@ -20,7 +34,23 @@ impl forall [T:! J] T as Interface { } -// CHECK:STDOUT: - filename: match_first.carbon +// CHECK:STDOUT: - filename: empty.carbon +// CHECK:STDOUT: parse_tree: [ +// CHECK:STDOUT: {kind: 'FileStart', text: ''}, +// CHECK:STDOUT: {kind: 'MatchFirstIntroducer', text: 'match_first'}, +// CHECK:STDOUT: {kind: 'MatchFirstDefinitionStart', text: '{', subtree_size: 2}, +// CHECK:STDOUT: {kind: 'MatchFirst', text: '}', subtree_size: 3}, +// CHECK:STDOUT: {kind: 'FileEnd', text: ''}, +// CHECK:STDOUT: ] +// CHECK:STDOUT: - filename: fail_missing_braces.carbon +// CHECK:STDOUT: parse_tree: [ +// CHECK:STDOUT: {kind: 'FileStart', text: ''}, +// CHECK:STDOUT: {kind: 'MatchFirstIntroducer', text: 'match_first'}, +// CHECK:STDOUT: {kind: 'MatchFirstDefinitionStart', text: 'match_first', has_error: yes, subtree_size: 2}, +// CHECK:STDOUT: {kind: 'MatchFirst', text: ';', has_error: yes, subtree_size: 3}, +// CHECK:STDOUT: {kind: 'FileEnd', text: ''}, +// CHECK:STDOUT: ] +// CHECK:STDOUT: - filename: with_impls.carbon // CHECK:STDOUT: parse_tree: [ // CHECK:STDOUT: {kind: 'FileStart', text: ''}, // CHECK:STDOUT: {kind: 'MatchFirstIntroducer', text: 'match_first'},