diff --git a/toolchain/parse/handle_lambda.cpp b/toolchain/parse/handle_lambda.cpp index d761e6762112..225b56577407 100644 --- a/toolchain/parse/handle_lambda.cpp +++ b/toolchain/parse/handle_lambda.cpp @@ -62,8 +62,17 @@ auto HandleLambdaAfterParams(Context& context) -> void { CARBON_DIAGNOSTIC(ExpectedLambdaBody, Error, "expected `->`, `=>`, or `{{`"); context.emitter().Emit(*context.position(), ExpectedLambdaBody); + + // Add a dummy node for the missing body without consuming the current + // token, then bundle everything into a complete lambda node. This keeps the + // lambda a valid expression for error recovery -- otherwise the orphaned + // `LambdaIntroducer` would be left where an expression is required, for + // example in `(fn)`. + context.AddLeafNode(NodeKind::InvalidParse, *context.position(), + /*has_error=*/true); + state.has_error = true; - context.ReturnErrorOnState(); + context.PushState(state, StateKind::LambdaBodyFinish); } } diff --git a/toolchain/parse/testdata/lambda/lambda.carbon b/toolchain/parse/testdata/lambda/lambda.carbon index 1032df9bae64..40ac309b3f46 100644 --- a/toolchain/parse/testdata/lambda/lambda.carbon +++ b/toolchain/parse/testdata/lambda/lambda.carbon @@ -155,9 +155,11 @@ fn G() { // CHECK:STDOUT: {kind: 'VarBindingPattern', text: ':', subtree_size: 3}, // CHECK:STDOUT: {kind: 'VariablePattern', text: 'var', subtree_size: 4}, // CHECK:STDOUT: {kind: 'VariableInitializer', text: '='}, -// CHECK:STDOUT: {kind: 'LambdaIntroducer', text: 'fn'}, -// CHECK:STDOUT: {kind: 'VariableDecl', text: ';', has_error: yes, subtree_size: 8}, -// CHECK:STDOUT: {kind: 'FunctionDefinition', text: '}', subtree_size: 14}, +// CHECK:STDOUT: {kind: 'LambdaIntroducer', text: 'fn'}, +// CHECK:STDOUT: {kind: 'InvalidParse', text: ';', has_error: yes}, +// CHECK:STDOUT: {kind: 'Lambda', text: 'fn', has_error: yes, subtree_size: 3}, +// CHECK:STDOUT: {kind: 'VariableDecl', text: ';', subtree_size: 10}, +// CHECK:STDOUT: {kind: 'FunctionDefinition', text: '}', subtree_size: 16}, // CHECK:STDOUT: {kind: 'FileEnd', text: ''}, // CHECK:STDOUT: ] // CHECK:STDOUT: - filename: fail_expected_body_after_return_type.carbon diff --git a/toolchain/parse/tree_test.cpp b/toolchain/parse/tree_test.cpp index b44aaa5ef665..b354ee69677f 100644 --- a/toolchain/parse/tree_test.cpp +++ b/toolchain/parse/tree_test.cpp @@ -178,5 +178,21 @@ TEST_F(TreeTest, HighRecursion) { EXPECT_FALSE(tree.has_errors()); } +TEST_F(TreeTest, IncompleteLambdaRecovers) { + // An incomplete lambda -- a `fn` introducer with no body, here `(fn)` -- must + // still parse to a structurally valid tree (a `Lambda` node with a + // placeholder body) rather than leaving an orphaned introducer where an + // expression is required, which would fail the parser's own tree + // verification. + Lex::TokenizedBuffer& tokens = + compile_helper_.GetTokenizedBuffer("var x: auto = (fn);"); + ASSERT_FALSE(tokens.has_errors()); + ::testing::NiceMock consumer; + Parse::ParseOptions options; + options.consumer = &consumer; + Tree tree = Parse(tokens, options); + EXPECT_TRUE(tree.has_errors()); +} + } // namespace } // namespace Carbon::Parse