From f3b8e231ca239d122926fe4769c67dcb810ed10c Mon Sep 17 00:00:00 2001 From: mstr-six <169169413+mstr-six@users.noreply.github.com> Date: Thu, 11 Jun 2026 18:33:29 -0400 Subject: [PATCH] Recover base declarations missing a colon (#7341) Malformed `base` declarations with an omitted colon need two different recovery paths. For `extend base`, the consumed `extend` modifier requires the parse tree to retain its `BaseColon` and base expression children, so this synthesizes an errored `BaseColon` and continues parsing the expression. Other malformed forms, such as `base calss X {}`, now use the standard declaration-error recovery: emit `ExpectedAfterBase`, skip past the likely declaration end, and form an errored `BaseDecl` without inventing a colon or cascading diagnostics. The regression covers `extend base Foo;`, bare `base;`, and the reviewer counterexample `base calss X {}`. Tests: - `prek run --files toolchain/parse/handle_base.cpp toolchain/parse/testdata/class/fail_base.carbon` - `./scripts/run_bazelisk.py test -c dbg //toolchain/parse/...` - `./scripts/run_bazelisk.py test -c dbg //toolchain/testing:file_test` AI assistance: OpenAI Codex helped inspect the parser recovery path, implement the change, and run verification. The operator reviewed and authorized the contribution. Assisted-by: OpenAI Codex --- toolchain/parse/handle_base.cpp | 18 ++++++---- .../parse/testdata/class/fail_base.carbon | 34 ++++++++++++++++++- 2 files changed, 45 insertions(+), 7 deletions(-) diff --git a/toolchain/parse/handle_base.cpp b/toolchain/parse/handle_base.cpp index d05a09aeb071..3167cf58143e 100644 --- a/toolchain/parse/handle_base.cpp +++ b/toolchain/parse/handle_base.cpp @@ -13,15 +13,21 @@ auto HandleBaseAfterIntroducer(Context& context) -> void { if (!context.ConsumeAndAddLeafNodeIf(Lex::TokenKind::Colon, NodeKind::BaseColon)) { - // TODO: If the next token isn't a colon or `class`, try to recover - // based on whether we're in a class, whether we have an `extend` - // modifier, and the following tokens. CARBON_DIAGNOSTIC(ExpectedAfterBase, Error, "`class` or `:` expected after `base`"); context.emitter().Emit(*context.position(), ExpectedAfterBase); - context.RecoverFromDeclError(state, NodeKind::BaseDecl, - /*skip_past_likely_end=*/true); - return; + auto base_token = *(context.position() - 1); + auto previous_token = Lex::TokenIndex(base_token.index - 1); + if (context.tokens().GetKind(previous_token) != Lex::TokenKind::Extend) { + context.RecoverFromDeclError(state, NodeKind::BaseDecl, + /*skip_past_likely_end=*/true); + return; + } + + // Preserve the `extend base` tree shape using an errored placeholder. + context.AddLeafNode(NodeKind::BaseColon, *context.position(), + /*has_error=*/true); + state.has_error = true; } state.kind = StateKind::BaseDecl; diff --git a/toolchain/parse/testdata/class/fail_base.carbon b/toolchain/parse/testdata/class/fail_base.carbon index d976bf4e9f5b..d607f47aedf8 100644 --- a/toolchain/parse/testdata/class/fail_base.carbon +++ b/toolchain/parse/testdata/class/fail_base.carbon @@ -15,6 +15,12 @@ class A { // CHECK:STDERR: base; + // CHECK:STDERR: fail_base.carbon:[[@LINE+4]]:8: error: `class` or `:` expected after `base` [ExpectedAfterBase] + // CHECK:STDERR: base calss X {} + // CHECK:STDERR: ^~~~~ + // CHECK:STDERR: + base calss X {} + // CHECK:STDERR: fail_base.carbon:[[@LINE+4]]:9: error: expected expression [ExpectedExpr] // CHECK:STDERR: base: ; // CHECK:STDERR: ^ @@ -34,6 +40,16 @@ class A { var n: i32; } +base class Foo {} + +class B { + // CHECK:STDERR: fail_base.carbon:[[@LINE+4]]:15: error: `class` or `:` expected after `base` [ExpectedAfterBase] + // CHECK:STDERR: extend base Foo; + // CHECK:STDERR: ^~~ + // CHECK:STDERR: + extend base Foo; +} + // CHECK:STDOUT: - filename: fail_base.carbon // CHECK:STDOUT: parse_tree: [ // CHECK:STDOUT: {kind: 'FileStart', text: ''}, @@ -43,6 +59,8 @@ class A { // CHECK:STDOUT: {kind: 'BaseIntroducer', text: 'base'}, // CHECK:STDOUT: {kind: 'BaseDecl', text: ';', has_error: yes, subtree_size: 2}, // CHECK:STDOUT: {kind: 'BaseIntroducer', text: 'base'}, +// CHECK:STDOUT: {kind: 'BaseDecl', text: '}', has_error: yes, subtree_size: 2}, +// CHECK:STDOUT: {kind: 'BaseIntroducer', text: 'base'}, // CHECK:STDOUT: {kind: 'BaseColon', text: ':'}, // CHECK:STDOUT: {kind: 'InvalidParse', text: ';', has_error: yes}, // CHECK:STDOUT: {kind: 'BaseDecl', text: ';', has_error: yes, subtree_size: 4}, @@ -53,6 +71,20 @@ class A { // CHECK:STDOUT: {kind: 'BaseDecl', text: 'var', has_error: yes, subtree_size: 5}, // CHECK:STDOUT: {kind: 'InvalidParseStart', text: 'n', has_error: yes}, // CHECK:STDOUT: {kind: 'InvalidParseSubtree', text: ';', has_error: yes, subtree_size: 2}, -// CHECK:STDOUT: {kind: 'ClassDefinition', text: '}', subtree_size: 17}, +// CHECK:STDOUT: {kind: 'ClassDefinition', text: '}', subtree_size: 19}, +// CHECK:STDOUT: {kind: 'ClassIntroducer', text: 'class'}, +// CHECK:STDOUT: {kind: 'BaseModifier', text: 'base'}, +// CHECK:STDOUT: {kind: 'IdentifierNameNotBeforeSignature', text: 'Foo'}, +// CHECK:STDOUT: {kind: 'ClassDefinitionStart', text: '{', subtree_size: 4}, +// CHECK:STDOUT: {kind: 'ClassDefinition', text: '}', subtree_size: 5}, +// CHECK:STDOUT: {kind: 'ClassIntroducer', text: 'class'}, +// CHECK:STDOUT: {kind: 'IdentifierNameNotBeforeSignature', text: 'B'}, +// CHECK:STDOUT: {kind: 'ClassDefinitionStart', text: '{', subtree_size: 3}, +// CHECK:STDOUT: {kind: 'BaseIntroducer', text: 'base'}, +// CHECK:STDOUT: {kind: 'ExtendModifier', text: 'extend'}, +// CHECK:STDOUT: {kind: 'BaseColon', text: 'Foo', has_error: yes}, +// CHECK:STDOUT: {kind: 'IdentifierNameExpr', text: 'Foo'}, +// CHECK:STDOUT: {kind: 'BaseDecl', text: ';', has_error: yes, subtree_size: 5}, +// CHECK:STDOUT: {kind: 'ClassDefinition', text: '}', subtree_size: 9}, // CHECK:STDOUT: {kind: 'FileEnd', text: ''}, // CHECK:STDOUT: ]