浏览代码

Support for `returned var` and `return var`. (#3374)

Implement toolchain support for `returned var` and `return var`.

- Modeled `returned` in the parse tree as a `ReturnedSpecifier`
appearing after the `VariableIntroducer`.
- Modeled `return var` in the parse tree as a `ReturnVarSpecifier`
appearing after the `ReturnStatementStart`.
- Factored out the implementation of `return` statement and `returned
var` handling in check into a new `return.{h,cpp}`. The parse nodes
themselves are still handled in `handle_*.cpp`. This allows easy code
reuse between `return` and `returned var`.
Richard Smith 2 年之前
父节点
当前提交
afd6d85610
共有 34 个文件被更改,包括 1077 次插入146 次删除
  1. 2 0
      toolchain/check/BUILD
  2. 24 1
      toolchain/check/context.cpp
  3. 24 5
      toolchain/check/context.h
  4. 29 0
      toolchain/check/handle_expression_statement.cpp
  5. 2 1
      toolchain/check/handle_function.cpp
  6. 14 3
      toolchain/check/handle_pattern_binding.cpp
  7. 60 0
      toolchain/check/handle_return_statement.cpp
  8. 0 91
      toolchain/check/handle_statement.cpp
  9. 29 19
      toolchain/check/handle_variable.cpp
  10. 13 0
      toolchain/check/node_stack.h
  11. 180 0
      toolchain/check/return.cpp
  12. 36 0
      toolchain/check/return.h
  13. 21 0
      toolchain/check/testdata/return/fail_return_var_no_returned_var.carbon
  14. 81 0
      toolchain/check/testdata/return/fail_return_with_returned_var.carbon
  15. 34 0
      toolchain/check/testdata/return/fail_returned_var_no_return_type.carbon
  16. 89 0
      toolchain/check/testdata/return/fail_returned_var_shadow.carbon
  17. 28 0
      toolchain/check/testdata/return/fail_returned_var_type.carbon
  18. 1 1
      toolchain/check/testdata/return/fail_value_disallowed.carbon
  19. 4 1
      toolchain/check/testdata/return/fail_value_missing.carbon
  20. 72 0
      toolchain/check/testdata/return/returned_var.carbon
  21. 79 0
      toolchain/check/testdata/return/returned_var_scope.carbon
  22. 9 1
      toolchain/diagnostics/diagnostic_kind.def
  23. 27 0
      toolchain/lower/testdata/return/return_var.carbon
  24. 20 0
      toolchain/lower/testdata/return/return_var_byval.carbon
  25. 1 1
      toolchain/parse/handle_declaration_scope_loop.cpp
  26. 13 2
      toolchain/parse/handle_statement.cpp
  27. 31 9
      toolchain/parse/handle_var.cpp
  28. 10 4
      toolchain/parse/node_kind.def
  29. 15 7
      toolchain/parse/state.def
  30. 38 0
      toolchain/parse/testdata/for/fail_returned_var.carbon
  31. 27 0
      toolchain/parse/testdata/return/fail_returned_no_var.carbon
  32. 27 0
      toolchain/parse/testdata/return/fail_var_no_semi.carbon
  33. 35 0
      toolchain/parse/testdata/return/returned_var.carbon
  34. 2 0
      toolchain/sem_ir/file.h

+ 2 - 0
toolchain/check/BUILD

@@ -36,6 +36,8 @@ cc_library(
         "convert.cpp",
         "declaration_name_stack.cpp",
         "inst_block_stack.cpp",
+        "return.cpp",
+        "return.h",
     ] +
     # Glob handler files to avoid missing any.
     glob([

+ 24 - 1
toolchain/check/context.cpp

@@ -263,6 +263,12 @@ auto Context::PopScope() -> void {
     CARBON_CHECK(non_lexical_scope_stack_.back().first == scope.index);
     non_lexical_scope_stack_.pop_back();
   }
+
+  if (scope.has_returned_var) {
+    CARBON_CHECK(!return_scope_stack_.empty());
+    CARBON_CHECK(return_scope_stack_.back().returned_var.is_valid());
+    return_scope_stack_.back().returned_var = SemIR::InstId::Invalid;
+  }
 }
 
 auto Context::PopToScope(ScopeIndex index) -> void {
@@ -274,6 +280,23 @@ auto Context::PopToScope(ScopeIndex index) -> void {
       << current_scope_index();
 }
 
+auto Context::SetReturnedVarOrGetExisting(SemIR::InstId inst_id)
+    -> SemIR::InstId {
+  CARBON_CHECK(!return_scope_stack_.empty()) << "`returned var` in no function";
+  auto& returned_var = return_scope_stack_.back().returned_var;
+  if (returned_var.is_valid()) {
+    return returned_var;
+  }
+
+  returned_var = inst_id;
+  CARBON_CHECK(!current_scope().has_returned_var)
+      << "Scope has returned var but none is set";
+  if (inst_id.is_valid()) {
+    current_scope().has_returned_var = true;
+  }
+  return SemIR::InstId::Invalid;
+}
+
 auto Context::FollowNameReferences(SemIR::InstId inst_id) -> SemIR::InstId {
   while (auto name_ref = insts().Get(inst_id).TryAs<SemIR::NameReference>()) {
     inst_id = name_ref->value_id;
@@ -396,7 +419,7 @@ auto Context::AddCurrentCodeBlockToFunction(Parse::Node parse_node) -> void {
 
   auto function_id =
       insts()
-          .GetAs<SemIR::FunctionDecl>(return_scope_stack().back())
+          .GetAs<SemIR::FunctionDecl>(return_scope_stack().back().decl_id)
           .function_id;
   functions()
       .Get(function_id)

+ 24 - 5
toolchain/check/context.h

@@ -30,6 +30,17 @@ class Context {
     SemIR::InstBlockId continue_target;
   };
 
+  // A scope in which `return` can be used.
+  struct ReturnScope {
+    // The declaration from which we can return. Inside a function, this will
+    // be a `FunctionDecl`.
+    SemIR::InstId decl_id;
+
+    // The value corresponding to the current `returned var`, if any. Will be
+    // set and unset as `returned var`s are declared and go out of scope.
+    SemIR::InstId returned_var = SemIR::InstId::Invalid;
+  };
+
   // Stores references for work.
   explicit Context(const Lex::TokenizedBuffer& tokens,
                    DiagnosticEmitter& emitter, const Parse::Tree& parse_tree,
@@ -115,6 +126,11 @@ class Context {
     return insts().Get(current_scope_inst_id).TryAs<InstT>();
   }
 
+  // If there is no `returned var` in scope, sets the given instruction to be
+  // the current `returned var` and returns an invalid instruction ID. If there
+  // is already a `returned var`, returns it instead.
+  auto SetReturnedVarOrGetExisting(SemIR::InstId bind_id) -> SemIR::InstId;
+
   // Follows NameReference instructions to find the value named by a given
   // instruction.
   auto FollowNameReferences(SemIR::InstId inst_id) -> SemIR::InstId;
@@ -259,7 +275,7 @@ class Context {
     return args_type_info_stack_;
   }
 
-  auto return_scope_stack() -> llvm::SmallVector<SemIR::InstId>& {
+  auto return_scope_stack() -> llvm::SmallVector<ReturnScope>& {
     return return_scope_stack_;
   }
 
@@ -332,9 +348,13 @@ class Context {
     SemIR::NameScopeId scope_id;
 
     // Names which are registered with name_lookup_, and will need to be
-    // deregistered when the scope ends.
+    // unregistered when the scope ends.
     llvm::DenseSet<SemIR::NameId> names;
 
+    // Whether a `returned var` was introduced in this scope, and needs to be
+    // unregistered when the scope ends.
+    bool has_returned_var = false;
+
     // TODO: This likely needs to track things which need to be destructed.
   };
 
@@ -405,9 +425,8 @@ class Context {
   // for a type separate from the literal arguments.
   InstBlockStack args_type_info_stack_;
 
-  // A stack of return scopes; i.e., targets for `return`. Inside a function,
-  // this will be a FunctionDecl.
-  llvm::SmallVector<SemIR::InstId> return_scope_stack_;
+  // A stack of scopes from which we can `return`.
+  llvm::SmallVector<ReturnScope> return_scope_stack_;
 
   // A stack of `break` and `continue` targets.
   llvm::SmallVector<BreakContinueScope> break_continue_stack_;

+ 29 - 0
toolchain/check/handle_expression_statement.cpp

@@ -0,0 +1,29 @@
+// 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/check/context.h"
+#include "toolchain/check/convert.h"
+#include "toolchain/sem_ir/inst.h"
+
+namespace Carbon::Check {
+
+// TODO: Find a better home for this. We'll likely need it for more than just
+// expression statements.
+static auto HandleDiscardedExpr(Context& context, SemIR::InstId expr_id)
+    -> void {
+  // If we discard an initializing expression, convert it to a value or
+  // reference so that it has something to initialize.
+  auto expr = context.insts().Get(expr_id);
+  Convert(context, expr.parse_node(), expr_id,
+          {.kind = ConversionTarget::Discarded, .type_id = expr.type_id()});
+
+  // TODO: This will eventually need to do some "do not discard" analysis.
+}
+
+auto HandleExprStatement(Context& context, Parse::Node /*parse_node*/) -> bool {
+  HandleDiscardedExpr(context, context.node_stack().PopExpr());
+  return true;
+}
+
+}  // namespace Carbon::Check

+ 2 - 1
toolchain/check/handle_function.cpp

@@ -100,6 +100,7 @@ static auto BuildFunctionDecl(Context& context, bool is_definition)
              name_context.state == DeclNameStack::NameContext::State::Unresolved
                  ? name_context.unresolved_name_id
                  : SemIR::NameId::Invalid,
+         .declaration_id = function_decl_id,
          .implicit_param_refs_id = implicit_param_refs_id,
          .param_refs_id = param_refs_id,
          .return_type_id = return_type_id,
@@ -182,7 +183,7 @@ auto HandleFunctionDefinitionStart(Context& context, Parse::Node parse_node)
   }
 
   // Create the function scope and the entry block.
-  context.return_scope_stack().push_back(decl_id);
+  context.return_scope_stack().push_back({.decl_id = decl_id});
   context.inst_block_stack().Push();
   context.PushScope(decl_id);
   context.AddCurrentCodeBlockToFunction();

+ 14 - 3
toolchain/check/handle_pattern_binding.cpp

@@ -4,6 +4,7 @@
 
 #include "toolchain/check/context.h"
 #include "toolchain/check/convert.h"
+#include "toolchain/check/return.h"
 #include "toolchain/sem_ir/inst.h"
 
 namespace Carbon::Check {
@@ -62,6 +63,7 @@ auto HandlePatternBinding(Context& context, Parse::Node parse_node) -> bool {
   // error locations.
   switch (auto context_parse_node_kind = context.parse_tree().node_kind(
               context.node_stack().PeekParseNode())) {
+    case Parse::NodeKind::ReturnedSpecifier:
     case Parse::NodeKind::VariableIntroducer: {
       // A `var` declaration at class scope introduces a field.
       auto enclosing_class_decl = context.GetCurrentScopeAs<SemIR::ClassDecl>();
@@ -78,7 +80,12 @@ auto HandlePatternBinding(Context& context, Parse::Node parse_node) -> bool {
       }
       SemIR::InstId value_id = SemIR::InstId::Invalid;
       SemIR::TypeId value_type_id = cast_type_id;
-      if (enclosing_class_decl) {
+      if (context_parse_node_kind == Parse::NodeKind::ReturnedSpecifier) {
+        CARBON_CHECK(!enclosing_class_decl) << "`returned var` at class scope";
+        value_id =
+            CheckReturnedVar(context, context.node_stack().PeekParseNode(),
+                             name_node, name_id, type_node, cast_type_id);
+      } else if (enclosing_class_decl) {
         auto& class_info =
             context.classes().Get(enclosing_class_decl->class_id);
         auto field_type_inst_id = context.AddInst(SemIR::UnboundFieldType{
@@ -98,9 +105,13 @@ auto HandlePatternBinding(Context& context, Parse::Node parse_node) -> bool {
         value_id = context.AddInst(
             SemIR::VarStorage{name_node, value_type_id, name_id});
       }
-      context.AddInstAndPush(
-          parse_node,
+      auto bind_id = context.AddInst(
           SemIR::BindName{name_node, value_type_id, name_id, value_id});
+      context.node_stack().Push(parse_node, bind_id);
+
+      if (context_parse_node_kind == Parse::NodeKind::ReturnedSpecifier) {
+        RegisterReturnedVar(context, bind_id);
+      }
       break;
     }
 

+ 60 - 0
toolchain/check/handle_return_statement.cpp

@@ -0,0 +1,60 @@
+// 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/check/context.h"
+#include "toolchain/check/return.h"
+
+namespace Carbon::Check {
+
+auto HandleReturnStatementStart(Context& context, Parse::Node parse_node)
+    -> bool {
+  // No action, just a bracketing node.
+  context.node_stack().Push(parse_node);
+  return true;
+}
+
+auto HandleReturnVarSpecifier(Context& context, Parse::Node parse_node)
+    -> bool {
+  // No action, just a bracketing node.
+  context.node_stack().Push(parse_node);
+  return true;
+}
+
+auto HandleReturnStatement(Context& context, Parse::Node parse_node) -> bool {
+  switch (
+      context.parse_tree().node_kind(context.node_stack().PeekParseNode())) {
+    case Parse::NodeKind::ReturnStatementStart:
+      // This is a `return;` statement.
+      context.node_stack()
+          .PopAndDiscardSoloParseNode<Parse::NodeKind::ReturnStatementStart>();
+      BuildReturnWithNoExpr(context, parse_node);
+      break;
+
+    case Parse::NodeKind::ReturnVarSpecifier:
+      // This is a `return var;` statement.
+      context.node_stack()
+          .PopAndDiscardSoloParseNode<Parse::NodeKind::ReturnVarSpecifier>();
+      context.node_stack()
+          .PopAndDiscardSoloParseNode<Parse::NodeKind::ReturnStatementStart>();
+      BuildReturnVar(context, parse_node);
+      break;
+
+    default:
+      // This is a `return <expression>;` statement.
+      auto expr_id = context.node_stack().PopExpr();
+      context.node_stack()
+          .PopAndDiscardSoloParseNode<Parse::NodeKind::ReturnStatementStart>();
+      BuildReturnWithExpr(context, parse_node, expr_id);
+      break;
+  }
+
+  // Switch to a new, unreachable, empty instruction block. This typically won't
+  // contain any semantics IR, but it can do if there are statements following
+  // the `return` statement.
+  context.inst_block_stack().Pop();
+  context.inst_block_stack().PushUnreachable();
+  return true;
+}
+
+}  // namespace Carbon::Check

+ 0 - 91
toolchain/check/handle_statement.cpp

@@ -1,91 +0,0 @@
-// 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/check/context.h"
-#include "toolchain/check/convert.h"
-#include "toolchain/sem_ir/inst.h"
-
-namespace Carbon::Check {
-
-// TODO: Find a better home for this. We'll likely need it for more than just
-// expression statements.
-static auto HandleDiscardedExpr(Context& context, SemIR::InstId expr_id)
-    -> void {
-  // If we discard an initializing expression, convert it to a value or
-  // reference so that it has something to initialize.
-  auto expr = context.insts().Get(expr_id);
-  Convert(context, expr.parse_node(), expr_id,
-          {.kind = ConversionTarget::Discarded, .type_id = expr.type_id()});
-
-  // TODO: This will eventually need to do some "do not discard" analysis.
-}
-
-auto HandleExprStatement(Context& context, Parse::Node /*parse_node*/) -> bool {
-  HandleDiscardedExpr(context, context.node_stack().PopExpr());
-  return true;
-}
-
-auto HandleReturnStatement(Context& context, Parse::Node parse_node) -> bool {
-  CARBON_CHECK(!context.return_scope_stack().empty());
-  auto fn_inst = context.insts().GetAs<SemIR::FunctionDecl>(
-      context.return_scope_stack().back());
-  const auto& callable = context.functions().Get(fn_inst.function_id);
-
-  if (context.parse_tree().node_kind(context.node_stack().PeekParseNode()) ==
-      Parse::NodeKind::ReturnStatementStart) {
-    context.node_stack()
-        .PopAndDiscardSoloParseNode<Parse::NodeKind::ReturnStatementStart>();
-
-    if (callable.return_type_id.is_valid()) {
-      // TODO: Add a note pointing at the return type's parse node.
-      CARBON_DIAGNOSTIC(ReturnStatementMissingExpr, Error, "Must return a {0}.",
-                        std::string);
-      context.emitter()
-          .Build(parse_node, ReturnStatementMissingExpr,
-                 context.sem_ir().StringifyType(callable.return_type_id))
-          .Emit();
-    }
-
-    context.AddInst(SemIR::Return{parse_node});
-  } else {
-    auto arg = context.node_stack().PopExpr();
-    context.node_stack()
-        .PopAndDiscardSoloParseNode<Parse::NodeKind::ReturnStatementStart>();
-
-    if (!callable.return_type_id.is_valid()) {
-      CARBON_DIAGNOSTIC(
-          ReturnStatementDisallowExpr, Error,
-          "No return expression should be provided in this context.");
-      CARBON_DIAGNOSTIC(ReturnStatementImplicitNote, Note,
-                        "There was no return type provided.");
-      context.emitter()
-          .Build(parse_node, ReturnStatementDisallowExpr)
-          .Note(fn_inst.parse_node, ReturnStatementImplicitNote)
-          .Emit();
-    } else if (callable.return_slot_id.is_valid()) {
-      arg = Initialize(context, parse_node, callable.return_slot_id, arg);
-    } else {
-      arg = ConvertToValueOfType(context, parse_node, arg,
-                                 callable.return_type_id);
-    }
-
-    context.AddInst(SemIR::ReturnExpr{parse_node, arg});
-  }
-
-  // Switch to a new, unreachable, empty instruction block. This typically won't
-  // contain any semantics IR, but it can do if there are statements following
-  // the `return` statement.
-  context.inst_block_stack().Pop();
-  context.inst_block_stack().PushUnreachable();
-  return true;
-}
-
-auto HandleReturnStatementStart(Context& context, Parse::Node parse_node)
-    -> bool {
-  // No action, just a bracketing node.
-  context.node_stack().Push(parse_node);
-  return true;
-}
-
-}  // namespace Carbon::Check

+ 29 - 19
toolchain/check/handle_variable.cpp

@@ -8,6 +8,26 @@
 
 namespace Carbon::Check {
 
+auto HandleVariableIntroducer(Context& context, Parse::Node parse_node)
+    -> bool {
+  // No action, just a bracketing node.
+  context.node_stack().Push(parse_node);
+  return true;
+}
+
+auto HandleReturnedSpecifier(Context& context, Parse::Node parse_node) -> bool {
+  // No action, just a bracketing node.
+  context.node_stack().Push(parse_node);
+  return true;
+}
+
+auto HandleVariableInitializer(Context& context, Parse::Node parse_node)
+    -> bool {
+  // No action, just a bracketing node.
+  context.node_stack().Push(parse_node);
+  return true;
+}
+
 auto HandleVariableDecl(Context& context, Parse::Node parse_node) -> bool {
   // Handle the optional initializer.
   auto init_id = SemIR::InstId::Invalid;
@@ -32,17 +52,21 @@ auto HandleVariableDecl(Context& context, Parse::Node parse_node) -> bool {
     value_id = bind_name->value_id;
   }
 
+  // Pop the `returned` specifier if present.
+  context.node_stack()
+      .PopAndDiscardSoloParseNodeIf<Parse::NodeKind::ReturnedSpecifier>();
+
   // If there was an initializer, assign it to the storage.
   if (has_init) {
-    if (context.insts().Get(value_id).Is<SemIR::VarStorage>()) {
+    if (context.GetCurrentScopeAs<SemIR::ClassDecl>()) {
+      // TODO: In a class scope, we should instead save the initializer
+      // somewhere so that we can use it as a default.
+      context.TODO(parse_node, "Field initializer");
+    } else {
       init_id = Initialize(context, parse_node, value_id, init_id);
       // TODO: Consider using different instruction kinds for assignment versus
       // initialization.
       context.AddInst(SemIR::Assign{parse_node, value_id, init_id});
-    } else {
-      // TODO: In a class scope, we should instead save the initializer
-      // somewhere so that we can use it as a default.
-      context.TODO(parse_node, "Field initializer");
     }
   }
 
@@ -52,18 +76,4 @@ auto HandleVariableDecl(Context& context, Parse::Node parse_node) -> bool {
   return true;
 }
 
-auto HandleVariableIntroducer(Context& context, Parse::Node parse_node)
-    -> bool {
-  // No action, just a bracketing node.
-  context.node_stack().Push(parse_node);
-  return true;
-}
-
-auto HandleVariableInitializer(Context& context, Parse::Node parse_node)
-    -> bool {
-  // No action, just a bracketing node.
-  context.node_stack().Push(parse_node);
-  return true;
-}
-
 }  // namespace Carbon::Check

+ 13 - 0
toolchain/check/node_stack.h

@@ -100,6 +100,17 @@ class NodeStack {
     PopForSoloParseNode<RequiredParseKind>();
   }
 
+  // Pops the top of the stack if it is the given kind. Returns `true` if a node
+  // was popped.
+  template <Parse::NodeKind::RawEnumType RequiredParseKind>
+  auto PopAndDiscardSoloParseNodeIf() -> bool {
+    if (!PeekIs<RequiredParseKind>()) {
+      return false;
+    }
+    PopForSoloParseNode<RequiredParseKind>();
+    return true;
+  }
+
   // Pops an expression from the top of the stack and returns the parse_node and
   // the ID.
   auto PopExprWithParseNode() -> std::pair<Parse::Node, SemIR::InstId> {
@@ -327,7 +338,9 @@ class NodeStack {
       case Parse::NodeKind::ParameterListStart:
       case Parse::NodeKind::ParenExprOrTupleLiteralStart:
       case Parse::NodeKind::QualifiedDecl:
+      case Parse::NodeKind::ReturnedSpecifier:
       case Parse::NodeKind::ReturnStatementStart:
+      case Parse::NodeKind::ReturnVarSpecifier:
       case Parse::NodeKind::SelfValueName:
       case Parse::NodeKind::StructLiteralOrStructTypeLiteralStart:
       case Parse::NodeKind::VariableInitializer:

+ 180 - 0
toolchain/check/return.cpp

@@ -0,0 +1,180 @@
+// 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/check/context.h"
+#include "toolchain/check/convert.h"
+#include "toolchain/parse/tree.h"
+#include "toolchain/sem_ir/inst.h"
+
+namespace Carbon::Check {
+
+// Gets the function that lexically encloses the current location.
+static auto GetCurrentFunction(Context& context) -> SemIR::Function& {
+  CARBON_CHECK(!context.return_scope_stack().empty())
+      << "Handling return but not in a function";
+  auto function_id = context.insts()
+                         .GetAs<SemIR::FunctionDecl>(
+                             context.return_scope_stack().back().decl_id)
+                         .function_id;
+  return context.functions().Get(function_id);
+}
+
+// Gets the currently in scope `returned var`, if any, that would be returned
+// by a `return var;`.
+static auto GetCurrentReturnedVar(Context& context) -> SemIR::InstId {
+  CARBON_CHECK(!context.return_scope_stack().empty())
+      << "Handling return but not in a function";
+  return context.return_scope_stack().back().returned_var;
+}
+
+// Produces a note that the given function has no explicit return type.
+static auto NoteNoReturnTypeProvided(Context& context,
+                                     Context::DiagnosticBuilder& diag,
+                                     const SemIR::Function& function) {
+  CARBON_DIAGNOSTIC(ReturnTypeOmittedNote, Note,
+                    "There was no return type provided.");
+  diag.Note(context.insts().Get(function.declaration_id).parse_node(),
+            ReturnTypeOmittedNote);
+}
+
+// Produces a note describing the return type of the given function.
+static auto NoteReturnType(Context& context, Context::DiagnosticBuilder& diag,
+                           const SemIR::Function& function) {
+  // TODO: This is the location of the `fn` keyword. Find the location of the
+  // return type.
+  auto type_parse_node =
+      context.insts().Get(function.declaration_id).parse_node();
+  CARBON_DIAGNOSTIC(ReturnTypeHereNote, Note,
+                    "Return type of function is `{0}`.", std::string);
+  diag.Note(type_parse_node, ReturnTypeHereNote,
+            context.sem_ir().StringifyType(function.return_type_id, true));
+}
+
+// Produces a note pointing at the currently in scope `returned var`.
+static auto NoteReturnedVar(Context& context, Context::DiagnosticBuilder& diag,
+                            SemIR::InstId returned_var_id) {
+  CARBON_DIAGNOSTIC(ReturnedVarHere, Note, "`returned var` was declared here.");
+  diag.Note(context.insts().Get(returned_var_id).parse_node(), ReturnedVarHere);
+}
+
+auto CheckReturnedVar(Context& context, Parse::Node returned_node,
+                      Parse::Node name_node, SemIR::NameId name_id,
+                      Parse::Node type_node, SemIR::TypeId type_id)
+    -> SemIR::InstId {
+  // A `returned var` requires an explicit return type.
+  auto& function = GetCurrentFunction(context);
+  if (!function.return_type_id.is_valid()) {
+    CARBON_DIAGNOSTIC(ReturnedVarWithNoReturnType, Error,
+                      "Cannot declare a `returned var` in this function.");
+    auto diag =
+        context.emitter().Build(returned_node, ReturnedVarWithNoReturnType);
+    NoteNoReturnTypeProvided(context, diag, function);
+    diag.Emit();
+    return SemIR::InstId::BuiltinError;
+  }
+
+  // The declared type of the var must match the return type of the function.
+  if (function.return_type_id != type_id) {
+    CARBON_DIAGNOSTIC(ReturnedVarWrongType, Error,
+                      "Type `{0}` of `returned var` does not match "
+                      "return type of enclosing function.",
+                      std::string);
+    auto diag =
+        context.emitter().Build(type_node, ReturnedVarWrongType,
+                                context.sem_ir().StringifyType(type_id, true));
+    NoteReturnType(context, diag, function);
+    diag.Emit();
+    return SemIR::InstId::BuiltinError;
+  }
+
+  // The variable aliases the return slot if there is one. If not, it has its
+  // own storage.
+  if (function.return_slot_id.is_valid()) {
+    return function.return_slot_id;
+  }
+  return context.AddInst(SemIR::VarStorage{name_node, type_id, name_id});
+}
+
+auto RegisterReturnedVar(Context& context, SemIR::InstId bind_id) -> void {
+  auto existing_id = context.SetReturnedVarOrGetExisting(bind_id);
+  if (existing_id.is_valid()) {
+    CARBON_DIAGNOSTIC(ReturnedVarShadowed, Error,
+                      "Cannot declare a `returned var` in the scope of "
+                      "another `returned var`.");
+    auto diag = context.emitter().Build(
+        context.insts().Get(bind_id).parse_node(), ReturnedVarShadowed);
+    NoteReturnedVar(context, diag, existing_id);
+    diag.Emit();
+  }
+}
+
+auto BuildReturnWithNoExpr(Context& context, Parse::Node parse_node) -> void {
+  const auto& function = GetCurrentFunction(context);
+
+  if (function.return_type_id.is_valid()) {
+    CARBON_DIAGNOSTIC(ReturnStatementMissingExpr, Error,
+                      "Missing return value.", std::string);
+    auto diag = context.emitter().Build(
+        parse_node, ReturnStatementMissingExpr,
+        context.sem_ir().StringifyType(function.return_type_id));
+    NoteReturnType(context, diag, function);
+    diag.Emit();
+  }
+
+  context.AddInst(SemIR::Return{parse_node});
+}
+
+auto BuildReturnWithExpr(Context& context, Parse::Node parse_node,
+                         SemIR::InstId expr_id) -> void {
+  const auto& function = GetCurrentFunction(context);
+  auto returned_var_id = GetCurrentReturnedVar(context);
+
+  if (!function.return_type_id.is_valid()) {
+    CARBON_DIAGNOSTIC(
+        ReturnStatementDisallowExpr, Error,
+        "No return expression should be provided in this context.");
+    auto diag =
+        context.emitter().Build(parse_node, ReturnStatementDisallowExpr);
+    NoteNoReturnTypeProvided(context, diag, function);
+    diag.Emit();
+    expr_id = SemIR::InstId::BuiltinError;
+  } else if (returned_var_id.is_valid()) {
+    CARBON_DIAGNOSTIC(
+        ReturnExprWithReturnedVar, Error,
+        "Can only `return var;` in the scope of a `returned var`.");
+    auto diag = context.emitter().Build(parse_node, ReturnExprWithReturnedVar);
+    NoteReturnedVar(context, diag, returned_var_id);
+    diag.Emit();
+    expr_id = SemIR::InstId::BuiltinError;
+  } else if (function.return_slot_id.is_valid()) {
+    expr_id = Initialize(context, parse_node, function.return_slot_id, expr_id);
+  } else {
+    expr_id = ConvertToValueOfType(context, parse_node, expr_id,
+                                   function.return_type_id);
+  }
+
+  context.AddInst(SemIR::ReturnExpr{parse_node, expr_id});
+}
+
+auto BuildReturnVar(Context& context, Parse::Node parse_node) -> void {
+  const auto& function = GetCurrentFunction(context);
+  auto returned_var_id = GetCurrentReturnedVar(context);
+
+  if (!returned_var_id.is_valid()) {
+    CARBON_DIAGNOSTIC(ReturnVarWithNoReturnedVar, Error,
+                      "`return var;` with no `returned var` in scope.");
+    context.emitter().Emit(parse_node, ReturnVarWithNoReturnedVar);
+    returned_var_id = SemIR::InstId::BuiltinError;
+  }
+
+  if (!function.return_slot_id.is_valid()) {
+    // If we don't have a return slot, we're returning by value. Convert to a
+    // value expression.
+    returned_var_id = ConvertToValueExpr(context, returned_var_id);
+  }
+
+  context.AddInst(SemIR::ReturnExpr{parse_node, returned_var_id});
+}
+
+}  // namespace Carbon::Check

+ 36 - 0
toolchain/check/return.h

@@ -0,0 +1,36 @@
+// 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
+
+#ifndef CARBON_TOOLCHAIN_CHECK_RETURN_H_
+#define CARBON_TOOLCHAIN_CHECK_RETURN_H_
+
+#include "toolchain/check/context.h"
+#include "toolchain/parse/tree.h"
+#include "toolchain/sem_ir/inst.h"
+
+namespace Carbon::Check {
+
+// Checks a `returned var` binding and returns the location of the return
+// value storage that the name should bind to.
+auto CheckReturnedVar(Context& context, Parse::Node returned_node,
+                      Parse::Node name_node, SemIR::NameId name_id,
+                      Parse::Node type_node, SemIR::TypeId type_id)
+    -> SemIR::InstId;
+
+// Registers the given binding as the current `returned var` in this scope.
+auto RegisterReturnedVar(Context& context, SemIR::InstId bind_id) -> void;
+
+// Checks and builds SemIR for a `return;` statement.
+auto BuildReturnWithNoExpr(Context& context, Parse::Node parse_node) -> void;
+
+// Checks and builds SemIR for a `return <expression>;` statement.
+auto BuildReturnWithExpr(Context& context, Parse::Node parse_node,
+                         SemIR::InstId expr_id) -> void;
+
+// Checks and builds SemIR for a `return var;` statement.
+auto BuildReturnVar(Context& context, Parse::Node parse_node) -> void;
+
+}  // namespace Carbon::Check
+
+#endif  // CARBON_TOOLCHAIN_CHECK_RETURN_H_

+ 21 - 0
toolchain/check/testdata/return/fail_return_var_no_returned_var.carbon

@@ -0,0 +1,21 @@
+// 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
+//
+// AUTOUPDATE
+
+fn Procedure() -> i32 {
+  // CHECK:STDERR: fail_return_var_no_returned_var.carbon:[[@LINE+3]]:13: ERROR: `return var;` with no `returned var` in scope.
+  // CHECK:STDERR:   return var;
+  // CHECK:STDERR:             ^
+  return var;
+}
+
+// CHECK:STDOUT: file "fail_return_var_no_returned_var.carbon" {
+// CHECK:STDOUT:   %Procedure: <function> = fn_decl @Procedure
+// CHECK:STDOUT: }
+// CHECK:STDOUT:
+// CHECK:STDOUT: fn @Procedure() -> i32 {
+// CHECK:STDOUT: !entry:
+// CHECK:STDOUT:   return <error>
+// CHECK:STDOUT: }

+ 81 - 0
toolchain/check/testdata/return/fail_return_with_returned_var.carbon

@@ -0,0 +1,81 @@
+// 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
+//
+// AUTOUPDATE
+
+fn F() -> i32 {
+  returned var v: i32 = 0;
+  // CHECK:STDERR: fail_return_with_returned_var.carbon:[[@LINE+6]]:11: ERROR: Can only `return var;` in the scope of a `returned var`.
+  // CHECK:STDERR:   return 1;
+  // CHECK:STDERR:           ^
+  // CHECK:STDERR: fail_return_with_returned_var.carbon:[[@LINE-4]]:16: `returned var` was declared here.
+  // CHECK:STDERR:   returned var v: i32 = 0;
+  // CHECK:STDERR:                ^
+  return 1;
+}
+
+class C { var a: i32; var b: i32; }
+fn G() -> C {
+  returned var c: C = {.a = 1, .b = 2};
+  // CHECK:STDERR: fail_return_with_returned_var.carbon:[[@LINE+6]]:11: ERROR: Can only `return var;` in the scope of a `returned var`.
+  // CHECK:STDERR:   return c;
+  // CHECK:STDERR:           ^
+  // CHECK:STDERR: fail_return_with_returned_var.carbon:[[@LINE-4]]:16: `returned var` was declared here.
+  // CHECK:STDERR:   returned var c: C = {.a = 1, .b = 2};
+  // CHECK:STDERR:                ^
+  return c;
+}
+
+// CHECK:STDOUT: constants {
+// CHECK:STDOUT:   %.loc18_35.1: type = struct_type {.a: i32, .b: i32}
+// CHECK:STDOUT:   %.loc18_35.2: type = ptr_type {.a: i32, .b: i32}
+// CHECK:STDOUT: }
+// CHECK:STDOUT:
+// CHECK:STDOUT: file "fail_return_with_returned_var.carbon" {
+// CHECK:STDOUT:   %F: <function> = fn_decl @F
+// CHECK:STDOUT:   class_decl @C, ()
+// CHECK:STDOUT:   %C: type = class_type @C
+// CHECK:STDOUT:   %G: <function> = fn_decl @G
+// CHECK:STDOUT: }
+// CHECK:STDOUT:
+// CHECK:STDOUT: class @C {
+// CHECK:STDOUT:   %.loc18_16.1: type = unbound_field_type C, i32
+// CHECK:STDOUT:   %.loc18_16.2: <unbound field of class C> = field a, member0
+// CHECK:STDOUT:   %a: <unbound field of class C> = bind_name a, %.loc18_16.2
+// CHECK:STDOUT:   %.loc18_28.1: type = unbound_field_type C, i32
+// CHECK:STDOUT:   %.loc18_28.2: <unbound field of class C> = field b, member1
+// CHECK:STDOUT:   %b: <unbound field of class C> = bind_name b, %.loc18_28.2
+// CHECK:STDOUT:
+// CHECK:STDOUT: !members:
+// CHECK:STDOUT:   .a = %a
+// CHECK:STDOUT:   .b = %b
+// CHECK:STDOUT: }
+// CHECK:STDOUT:
+// CHECK:STDOUT: fn @F() -> i32 {
+// CHECK:STDOUT: !entry:
+// CHECK:STDOUT:   %v.var: ref i32 = var v
+// CHECK:STDOUT:   %v: ref i32 = bind_name v, %v.var
+// CHECK:STDOUT:   %.loc8: i32 = int_literal 0
+// CHECK:STDOUT:   assign %v.var, %.loc8
+// CHECK:STDOUT:   %.loc15: i32 = int_literal 1
+// CHECK:STDOUT:   return <error>
+// CHECK:STDOUT: }
+// CHECK:STDOUT:
+// CHECK:STDOUT: fn @G() -> %return: C {
+// CHECK:STDOUT: !entry:
+// CHECK:STDOUT:   %C.ref: type = name_reference C, file.%C
+// CHECK:STDOUT:   %c: ref C = bind_name c, %return
+// CHECK:STDOUT:   %.loc20_29: i32 = int_literal 1
+// CHECK:STDOUT:   %.loc20_37: i32 = int_literal 2
+// CHECK:STDOUT:   %.loc20_38.1: {.a: i32, .b: i32} = struct_literal (%.loc20_29, %.loc20_37)
+// CHECK:STDOUT:   %.loc20_38.2: ref i32 = class_field_access %return, member0
+// CHECK:STDOUT:   %.loc20_38.3: init i32 = initialize_from %.loc20_29 to %.loc20_38.2
+// CHECK:STDOUT:   %.loc20_38.4: ref i32 = class_field_access %return, member1
+// CHECK:STDOUT:   %.loc20_38.5: init i32 = initialize_from %.loc20_37 to %.loc20_38.4
+// CHECK:STDOUT:   %.loc20_38.6: init C = class_init (%.loc20_38.3, %.loc20_38.5), %return
+// CHECK:STDOUT:   %.loc20_38.7: init C = converted %.loc20_38.1, %.loc20_38.6
+// CHECK:STDOUT:   assign %return, %.loc20_38.7
+// CHECK:STDOUT:   %c.ref: ref C = name_reference c, %c
+// CHECK:STDOUT:   return <error>
+// CHECK:STDOUT: }

+ 34 - 0
toolchain/check/testdata/return/fail_returned_var_no_return_type.carbon

@@ -0,0 +1,34 @@
+// 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
+//
+// AUTOUPDATE
+
+fn Procedure() {
+  // CHECK:STDERR: fail_returned_var_no_return_type.carbon:[[@LINE+6]]:3: ERROR: Cannot declare a `returned var` in this function.
+  // CHECK:STDERR:   returned var v: () = ();
+  // CHECK:STDERR:   ^
+  // CHECK:STDERR: fail_returned_var_no_return_type.carbon:[[@LINE-4]]:1: There was no return type provided.
+  // CHECK:STDERR: fn Procedure() {
+  // CHECK:STDERR: ^
+  returned var v: () = ();
+  return;
+}
+
+// CHECK:STDOUT: constants {
+// CHECK:STDOUT:   %.loc14: type = tuple_type ()
+// CHECK:STDOUT: }
+// CHECK:STDOUT:
+// CHECK:STDOUT: file "fail_returned_var_no_return_type.carbon" {
+// CHECK:STDOUT:   %Procedure: <function> = fn_decl @Procedure
+// CHECK:STDOUT: }
+// CHECK:STDOUT:
+// CHECK:STDOUT: fn @Procedure() {
+// CHECK:STDOUT: !entry:
+// CHECK:STDOUT:   %.loc14_20.1: () = tuple_literal ()
+// CHECK:STDOUT:   %.loc14_20.2: type = converted %.loc14_20.1, constants.%.loc14
+// CHECK:STDOUT:   %v: () = bind_name v, <error>
+// CHECK:STDOUT:   %.loc14_25: () = tuple_literal ()
+// CHECK:STDOUT:   assign <error>, <error>
+// CHECK:STDOUT:   return
+// CHECK:STDOUT: }

+ 89 - 0
toolchain/check/testdata/return/fail_returned_var_shadow.carbon

@@ -0,0 +1,89 @@
+// 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
+//
+// AUTOUPDATE
+
+fn SameScope() -> i32 {
+  if (true) {
+    returned var v: i32 = 0;
+    // CHECK:STDERR: fail_returned_var_shadow.carbon:[[@LINE+6]]:18: ERROR: Cannot declare a `returned var` in the scope of another `returned var`.
+    // CHECK:STDERR:     returned var w: i32 = 1;
+    // CHECK:STDERR:                  ^
+    // CHECK:STDERR: fail_returned_var_shadow.carbon:[[@LINE-4]]:18: `returned var` was declared here.
+    // CHECK:STDERR:     returned var v: i32 = 0;
+    // CHECK:STDERR:                  ^
+    returned var w: i32 = 1;
+  }
+  return 0;
+}
+
+fn DifferentScopes() -> i32 {
+  if (true) {
+    returned var v: i32 = 0;
+    if (true) {
+      // CHECK:STDERR: fail_returned_var_shadow.carbon:[[@LINE+6]]:20: ERROR: Cannot declare a `returned var` in the scope of another `returned var`.
+      // CHECK:STDERR:       returned var w: i32 = 1;
+      // CHECK:STDERR:                    ^
+      // CHECK:STDERR: fail_returned_var_shadow.carbon:[[@LINE-5]]:18: `returned var` was declared here.
+      // CHECK:STDERR:     returned var v: i32 = 0;
+      // CHECK:STDERR:                  ^
+      returned var w: i32 = 1;
+    }
+  }
+  return 0;
+}
+
+// CHECK:STDOUT: file "fail_returned_var_shadow.carbon" {
+// CHECK:STDOUT:   %SameScope: <function> = fn_decl @SameScope
+// CHECK:STDOUT:   %DifferentScopes: <function> = fn_decl @DifferentScopes
+// CHECK:STDOUT: }
+// CHECK:STDOUT:
+// CHECK:STDOUT: fn @SameScope() -> i32 {
+// CHECK:STDOUT: !entry:
+// CHECK:STDOUT:   %.loc8: bool = bool_literal true
+// CHECK:STDOUT:   if %.loc8 br !if.then else br !if.else
+// CHECK:STDOUT:
+// CHECK:STDOUT: !if.then:
+// CHECK:STDOUT:   %v.var: ref i32 = var v
+// CHECK:STDOUT:   %v: ref i32 = bind_name v, %v.var
+// CHECK:STDOUT:   %.loc9: i32 = int_literal 0
+// CHECK:STDOUT:   assign %v.var, %.loc9
+// CHECK:STDOUT:   %w.var: ref i32 = var w
+// CHECK:STDOUT:   %w: ref i32 = bind_name w, %w.var
+// CHECK:STDOUT:   %.loc16: i32 = int_literal 1
+// CHECK:STDOUT:   assign %w.var, %.loc16
+// CHECK:STDOUT:   br !if.else
+// CHECK:STDOUT:
+// CHECK:STDOUT: !if.else:
+// CHECK:STDOUT:   %.loc18: i32 = int_literal 0
+// CHECK:STDOUT:   return %.loc18
+// CHECK:STDOUT: }
+// CHECK:STDOUT:
+// CHECK:STDOUT: fn @DifferentScopes() -> i32 {
+// CHECK:STDOUT: !entry:
+// CHECK:STDOUT:   %.loc22: bool = bool_literal true
+// CHECK:STDOUT:   if %.loc22 br !if.then.loc22 else br !if.else.loc22
+// CHECK:STDOUT:
+// CHECK:STDOUT: !if.then.loc22:
+// CHECK:STDOUT:   %v.var: ref i32 = var v
+// CHECK:STDOUT:   %v: ref i32 = bind_name v, %v.var
+// CHECK:STDOUT:   %.loc23: i32 = int_literal 0
+// CHECK:STDOUT:   assign %v.var, %.loc23
+// CHECK:STDOUT:   %.loc24: bool = bool_literal true
+// CHECK:STDOUT:   if %.loc24 br !if.then.loc24 else br !if.else.loc24
+// CHECK:STDOUT:
+// CHECK:STDOUT: !if.then.loc24:
+// CHECK:STDOUT:   %w.var: ref i32 = var w
+// CHECK:STDOUT:   %w: ref i32 = bind_name w, %w.var
+// CHECK:STDOUT:   %.loc31: i32 = int_literal 1
+// CHECK:STDOUT:   assign %w.var, %.loc31
+// CHECK:STDOUT:   br !if.else.loc24
+// CHECK:STDOUT:
+// CHECK:STDOUT: !if.else.loc24:
+// CHECK:STDOUT:   br !if.else.loc22
+// CHECK:STDOUT:
+// CHECK:STDOUT: !if.else.loc22:
+// CHECK:STDOUT:   %.loc34: i32 = int_literal 0
+// CHECK:STDOUT:   return %.loc34
+// CHECK:STDOUT: }

+ 28 - 0
toolchain/check/testdata/return/fail_returned_var_type.carbon

@@ -0,0 +1,28 @@
+// 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
+//
+// AUTOUPDATE
+
+fn Mismatch() -> i32 {
+  // CHECK:STDERR: fail_returned_var_type.carbon:[[@LINE+6]]:19: ERROR: Type `f64` of `returned var` does not match return type of enclosing function.
+  // CHECK:STDERR:   returned var v: f64 = 0.0;
+  // CHECK:STDERR:                   ^
+  // CHECK:STDERR: fail_returned_var_type.carbon:[[@LINE-4]]:1: Return type of function is `i32`.
+  // CHECK:STDERR: fn Mismatch() -> i32 {
+  // CHECK:STDERR: ^
+  returned var v: f64 = 0.0;
+  return var;
+}
+
+// CHECK:STDOUT: file "fail_returned_var_type.carbon" {
+// CHECK:STDOUT:   %Mismatch: <function> = fn_decl @Mismatch
+// CHECK:STDOUT: }
+// CHECK:STDOUT:
+// CHECK:STDOUT: fn @Mismatch() -> i32 {
+// CHECK:STDOUT: !entry:
+// CHECK:STDOUT:   %v: f64 = bind_name v, <error>
+// CHECK:STDOUT:   %.loc14: f64 = real_literal 0e-1
+// CHECK:STDOUT:   assign <error>, <error>
+// CHECK:STDOUT:   return <error>
+// CHECK:STDOUT: }

+ 1 - 1
toolchain/check/testdata/return/fail_value_disallowed.carbon

@@ -21,5 +21,5 @@ fn Main() {
 // CHECK:STDOUT: fn @Main() {
 // CHECK:STDOUT: !entry:
 // CHECK:STDOUT:   %.loc14: i32 = int_literal 0
-// CHECK:STDOUT:   return %.loc14
+// CHECK:STDOUT:   return <error>
 // CHECK:STDOUT: }

+ 4 - 1
toolchain/check/testdata/return/fail_value_missing.carbon

@@ -5,9 +5,12 @@
 // AUTOUPDATE
 
 fn Main() -> i32 {
-  // CHECK:STDERR: fail_value_missing.carbon:[[@LINE+3]]:9: ERROR: Must return a i32.
+  // CHECK:STDERR: fail_value_missing.carbon:[[@LINE+6]]:9: ERROR: Missing return value.
   // CHECK:STDERR:   return;
   // CHECK:STDERR:         ^
+  // CHECK:STDERR: fail_value_missing.carbon:[[@LINE-4]]:1: Return type of function is `i32`.
+  // CHECK:STDERR: fn Main() -> i32 {
+  // CHECK:STDERR: ^
   return;
 }
 

+ 72 - 0
toolchain/check/testdata/return/returned_var.carbon

@@ -0,0 +1,72 @@
+// 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
+//
+// AUTOUPDATE
+
+class C {
+  var a: i32;
+  var b: i32;
+}
+
+fn F() -> C {
+  returned var result: C = {.a = 1, .b = 2};
+  return var;
+}
+
+fn G() -> i32 {
+  returned var result: i32 = 0;
+  return var;
+}
+
+// CHECK:STDOUT: constants {
+// CHECK:STDOUT:   %.loc10_1.1: type = struct_type {.a: i32, .b: i32}
+// CHECK:STDOUT:   %.loc10_1.2: type = ptr_type {.a: i32, .b: i32}
+// CHECK:STDOUT: }
+// CHECK:STDOUT:
+// CHECK:STDOUT: file "returned_var.carbon" {
+// CHECK:STDOUT:   class_decl @C, ()
+// CHECK:STDOUT:   %C: type = class_type @C
+// CHECK:STDOUT:   %F: <function> = fn_decl @F
+// CHECK:STDOUT:   %G: <function> = fn_decl @G
+// CHECK:STDOUT: }
+// CHECK:STDOUT:
+// CHECK:STDOUT: class @C {
+// CHECK:STDOUT:   %.loc8_8.1: type = unbound_field_type C, i32
+// CHECK:STDOUT:   %.loc8_8.2: <unbound field of class C> = field a, member0
+// CHECK:STDOUT:   %a: <unbound field of class C> = bind_name a, %.loc8_8.2
+// CHECK:STDOUT:   %.loc9_8.1: type = unbound_field_type C, i32
+// CHECK:STDOUT:   %.loc9_8.2: <unbound field of class C> = field b, member1
+// CHECK:STDOUT:   %b: <unbound field of class C> = bind_name b, %.loc9_8.2
+// CHECK:STDOUT:
+// CHECK:STDOUT: !members:
+// CHECK:STDOUT:   .a = %a
+// CHECK:STDOUT:   .b = %b
+// CHECK:STDOUT: }
+// CHECK:STDOUT:
+// CHECK:STDOUT: fn @F() -> %return: C {
+// CHECK:STDOUT: !entry:
+// CHECK:STDOUT:   %C.ref: type = name_reference C, file.%C
+// CHECK:STDOUT:   %result: ref C = bind_name result, %return
+// CHECK:STDOUT:   %.loc13_34: i32 = int_literal 1
+// CHECK:STDOUT:   %.loc13_42: i32 = int_literal 2
+// CHECK:STDOUT:   %.loc13_43.1: {.a: i32, .b: i32} = struct_literal (%.loc13_34, %.loc13_42)
+// CHECK:STDOUT:   %.loc13_43.2: ref i32 = class_field_access %return, member0
+// CHECK:STDOUT:   %.loc13_43.3: init i32 = initialize_from %.loc13_34 to %.loc13_43.2
+// CHECK:STDOUT:   %.loc13_43.4: ref i32 = class_field_access %return, member1
+// CHECK:STDOUT:   %.loc13_43.5: init i32 = initialize_from %.loc13_42 to %.loc13_43.4
+// CHECK:STDOUT:   %.loc13_43.6: init C = class_init (%.loc13_43.3, %.loc13_43.5), %return
+// CHECK:STDOUT:   %.loc13_43.7: init C = converted %.loc13_43.1, %.loc13_43.6
+// CHECK:STDOUT:   assign %return, %.loc13_43.7
+// CHECK:STDOUT:   return %result
+// CHECK:STDOUT: }
+// CHECK:STDOUT:
+// CHECK:STDOUT: fn @G() -> i32 {
+// CHECK:STDOUT: !entry:
+// CHECK:STDOUT:   %result.var: ref i32 = var result
+// CHECK:STDOUT:   %result: ref i32 = bind_name result, %result.var
+// CHECK:STDOUT:   %.loc18_30: i32 = int_literal 0
+// CHECK:STDOUT:   assign %result.var, %.loc18_30
+// CHECK:STDOUT:   %.loc18_16: i32 = bind_value %result
+// CHECK:STDOUT:   return %.loc18_16
+// CHECK:STDOUT: }

+ 79 - 0
toolchain/check/testdata/return/returned_var_scope.carbon

@@ -0,0 +1,79 @@
+// 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
+//
+// AUTOUPDATE
+
+fn UnrelatedScopes() -> i32 {
+  if (true) {
+    returned var v: i32 = 0;
+  }
+  if (true) {
+    returned var w: i32 = 1;
+  }
+  return 0;
+}
+
+fn EnclosingButAfter(b: bool) -> i32 {
+  if (b) {
+    returned var v: i32 = 0;
+    return var;
+  }
+  returned var w: i32 = 1;
+  return var;
+}
+
+// CHECK:STDOUT: file "returned_var_scope.carbon" {
+// CHECK:STDOUT:   %UnrelatedScopes: <function> = fn_decl @UnrelatedScopes
+// CHECK:STDOUT:   %EnclosingButAfter: <function> = fn_decl @EnclosingButAfter
+// CHECK:STDOUT: }
+// CHECK:STDOUT:
+// CHECK:STDOUT: fn @UnrelatedScopes() -> i32 {
+// CHECK:STDOUT: !entry:
+// CHECK:STDOUT:   %.loc8: bool = bool_literal true
+// CHECK:STDOUT:   if %.loc8 br !if.then.loc8 else br !if.else.loc8
+// CHECK:STDOUT:
+// CHECK:STDOUT: !if.then.loc8:
+// CHECK:STDOUT:   %v.var: ref i32 = var v
+// CHECK:STDOUT:   %v: ref i32 = bind_name v, %v.var
+// CHECK:STDOUT:   %.loc9: i32 = int_literal 0
+// CHECK:STDOUT:   assign %v.var, %.loc9
+// CHECK:STDOUT:   br !if.else.loc8
+// CHECK:STDOUT:
+// CHECK:STDOUT: !if.else.loc8:
+// CHECK:STDOUT:   %.loc11: bool = bool_literal true
+// CHECK:STDOUT:   if %.loc11 br !if.then.loc11 else br !if.else.loc11
+// CHECK:STDOUT:
+// CHECK:STDOUT: !if.then.loc11:
+// CHECK:STDOUT:   %w.var: ref i32 = var w
+// CHECK:STDOUT:   %w: ref i32 = bind_name w, %w.var
+// CHECK:STDOUT:   %.loc12: i32 = int_literal 1
+// CHECK:STDOUT:   assign %w.var, %.loc12
+// CHECK:STDOUT:   br !if.else.loc11
+// CHECK:STDOUT:
+// CHECK:STDOUT: !if.else.loc11:
+// CHECK:STDOUT:   %.loc14: i32 = int_literal 0
+// CHECK:STDOUT:   return %.loc14
+// CHECK:STDOUT: }
+// CHECK:STDOUT:
+// CHECK:STDOUT: fn @EnclosingButAfter(%b: bool) -> i32 {
+// CHECK:STDOUT: !entry:
+// CHECK:STDOUT:   %b.ref: bool = name_reference b, %b
+// CHECK:STDOUT:   if %b.ref br !if.then else br !if.else
+// CHECK:STDOUT:
+// CHECK:STDOUT: !if.then:
+// CHECK:STDOUT:   %v.var: ref i32 = var v
+// CHECK:STDOUT:   %v: ref i32 = bind_name v, %v.var
+// CHECK:STDOUT:   %.loc19_27: i32 = int_literal 0
+// CHECK:STDOUT:   assign %v.var, %.loc19_27
+// CHECK:STDOUT:   %.loc19_18: i32 = bind_value %v
+// CHECK:STDOUT:   return %.loc19_18
+// CHECK:STDOUT:
+// CHECK:STDOUT: !if.else:
+// CHECK:STDOUT:   %w.var: ref i32 = var w
+// CHECK:STDOUT:   %w: ref i32 = bind_name w, %w.var
+// CHECK:STDOUT:   %.loc22_25: i32 = int_literal 1
+// CHECK:STDOUT:   assign %w.var, %.loc22_25
+// CHECK:STDOUT:   %.loc22_16: i32 = bind_value %w
+// CHECK:STDOUT:   return %.loc22_16
+// CHECK:STDOUT: }

+ 9 - 1
toolchain/diagnostics/diagnostic_kind.def

@@ -70,6 +70,7 @@ CARBON_DIAGNOSTIC_KIND(ExpectedParenAfter)
 CARBON_DIAGNOSTIC_KIND(ExpectedExprSemi)
 CARBON_DIAGNOSTIC_KIND(ExpectedStatementSemi)
 CARBON_DIAGNOSTIC_KIND(ExpectedStructLiteralField)
+CARBON_DIAGNOSTIC_KIND(ExpectedVarAfterReturned)
 CARBON_DIAGNOSTIC_KIND(ExpectedVariableDecl)
 CARBON_DIAGNOSTIC_KIND(ExpectedVariableName)
 CARBON_DIAGNOSTIC_KIND(OperatorRequiresParentheses)
@@ -162,8 +163,15 @@ CARBON_DIAGNOSTIC_KIND(StructInitMissingFieldInLiteral)
 CARBON_DIAGNOSTIC_KIND(StructInitMissingFieldInConversion)
 CARBON_DIAGNOSTIC_KIND(TupleIndexIntegerLiteral)
 CARBON_DIAGNOSTIC_KIND(TupleInitElementCountMismatch)
+CARBON_DIAGNOSTIC_KIND(ReturnedVarHere)
+CARBON_DIAGNOSTIC_KIND(ReturnedVarShadowed)
+CARBON_DIAGNOSTIC_KIND(ReturnedVarWithNoReturnType)
+CARBON_DIAGNOSTIC_KIND(ReturnedVarWrongType)
+CARBON_DIAGNOSTIC_KIND(ReturnExprWithReturnedVar)
+CARBON_DIAGNOSTIC_KIND(ReturnVarWithNoReturnedVar)
 CARBON_DIAGNOSTIC_KIND(ReturnStatementDisallowExpr)
-CARBON_DIAGNOSTIC_KIND(ReturnStatementImplicitNote)
+CARBON_DIAGNOSTIC_KIND(ReturnTypeHereNote)
+CARBON_DIAGNOSTIC_KIND(ReturnTypeOmittedNote)
 CARBON_DIAGNOSTIC_KIND(ReturnStatementMissingExpr)
 CARBON_DIAGNOSTIC_KIND(ImplicitAsConversionFailure)
 CARBON_DIAGNOSTIC_KIND(ExplicitAsConversionFailure)

+ 27 - 0
toolchain/lower/testdata/return/return_var.carbon

@@ -0,0 +1,27 @@
+// 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
+//
+// AUTOUPDATE
+
+class C {
+  var data: i32;
+  var next: C*;
+}
+
+fn Make() -> C {
+  returned var c: C;
+  c = {.data = 42, .next = &c};
+  return var;
+}
+
+// CHECK:STDOUT: ; ModuleID = 'return_var.carbon'
+// CHECK:STDOUT: source_filename = "return_var.carbon"
+// CHECK:STDOUT:
+// CHECK:STDOUT: define void @Make(ptr sret({ i32, ptr }) %return) {
+// CHECK:STDOUT:   %data = getelementptr inbounds { i32, ptr }, ptr %return, i32 0, i32 0
+// CHECK:STDOUT:   store i32 42, ptr %data, align 4
+// CHECK:STDOUT:   %next = getelementptr inbounds { i32, ptr }, ptr %return, i32 0, i32 1
+// CHECK:STDOUT:   store ptr %return, ptr %next, align 8
+// CHECK:STDOUT:   ret void
+// CHECK:STDOUT: }

+ 20 - 0
toolchain/lower/testdata/return/return_var_byval.carbon

@@ -0,0 +1,20 @@
+// 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
+//
+// AUTOUPDATE
+
+fn Main() -> i32 {
+  returned var x: i32 = 0;
+  return var;
+}
+
+// CHECK:STDOUT: ; ModuleID = 'return_var_byval.carbon'
+// CHECK:STDOUT: source_filename = "return_var_byval.carbon"
+// CHECK:STDOUT:
+// CHECK:STDOUT: define i32 @Main() {
+// CHECK:STDOUT:   %x = alloca i32, align 4
+// CHECK:STDOUT:   store i32 0, ptr %x, align 4
+// CHECK:STDOUT:   %1 = load i32, ptr %x, align 4
+// CHECK:STDOUT:   ret i32 %1
+// CHECK:STDOUT: }

+ 1 - 1
toolchain/parse/handle_declaration_scope_loop.cpp

@@ -78,7 +78,7 @@ auto HandleDeclScopeLoop(Context& context) -> void {
           break;
         }
         case Lex::TokenKind::Var: {
-          context.PushState(State::VarAsSemicolon);
+          context.PushState(State::VarAsDecl);
           break;
         }
         case Lex::TokenKind::Let: {

+ 13 - 2
toolchain/parse/handle_statement.cpp

@@ -38,8 +38,12 @@ auto HandleStatement(Context& context) -> void {
       context.PushState(State::StatementReturn);
       break;
     }
+    case Lex::TokenKind::Returned: {
+      context.PushState(State::VarAsReturned);
+      break;
+    }
     case Lex::TokenKind::Var: {
-      context.PushState(State::VarAsSemicolon);
+      context.PushState(State::VarAsDecl);
       break;
     }
     case Lex::TokenKind::While: {
@@ -179,8 +183,15 @@ auto HandleStatementReturn(Context& context) -> void {
   context.PushState(state);
 
   context.AddLeafNode(NodeKind::ReturnStatementStart, context.Consume());
-  if (!context.PositionIs(Lex::TokenKind::Semi)) {
+
+  if (auto var_token = context.ConsumeIf(Lex::TokenKind::Var)) {
+    // `return var;`
+    context.AddLeafNode(NodeKind::ReturnVarSpecifier, *var_token);
+  } else if (!context.PositionIs(Lex::TokenKind::Semi)) {
+    // `return <expression>;`
     context.PushState(State::Expr);
+  } else {
+    // `return;`
   }
 }
 

+ 31 - 9
toolchain/parse/handle_var.cpp

@@ -6,22 +6,44 @@
 
 namespace Carbon::Parse {
 
-// Handles VarAs(Semicolon|For).
-static auto HandleVar(Context& context, State finish_state) -> void {
-  context.PopAndDiscardState();
+// Handles VarAs(Decl|For).
+static auto HandleVar(Context& context, State finish_state,
+                      Lex::Token returned_token = Lex::Token::Invalid) -> void {
+  auto state = context.PopState();
+
+  // The finished variable declaration will start at the `var` or `returned`.
+  state.state = finish_state;
+  context.PushState(state);
 
-  // These will start at the `var`.
-  context.PushState(finish_state);
   context.PushState(State::VarAfterPattern);
 
   context.AddLeafNode(NodeKind::VariableIntroducer, context.Consume());
+  if (returned_token.is_valid()) {
+    context.AddLeafNode(NodeKind::ReturnedSpecifier, returned_token);
+  }
 
-  // This will start at the pattern.
   context.PushState(State::PatternAsVariable);
 }
 
-auto HandleVarAsSemicolon(Context& context) -> void {
-  HandleVar(context, State::VarFinishAsSemicolon);
+auto HandleVarAsDecl(Context& context) -> void {
+  HandleVar(context, State::VarFinishAsDecl);
+}
+
+auto HandleVarAsReturned(Context& context) -> void {
+  auto returned_token = context.Consume();
+
+  if (!context.PositionIs(Lex::TokenKind::Var)) {
+    CARBON_DIAGNOSTIC(ExpectedVarAfterReturned, Error,
+                      "Expected `var` after `returned`.");
+    context.emitter().Emit(*context.position(), ExpectedVarAfterReturned);
+    auto semi = context.SkipPastLikelyEnd(returned_token);
+    context.AddLeafNode(NodeKind::EmptyDecl, semi ? *semi : returned_token,
+                        /*has_error=*/true);
+    context.PopAndDiscardState();
+    return;
+  }
+
+  HandleVar(context, State::VarFinishAsDecl, returned_token);
 }
 
 auto HandleVarAsFor(Context& context) -> void {
@@ -44,7 +66,7 @@ auto HandleVarAfterPattern(Context& context) -> void {
   }
 }
 
-auto HandleVarFinishAsSemicolon(Context& context) -> void {
+auto HandleVarFinishAsDecl(Context& context) -> void {
   auto state = context.PopState();
 
   auto end_token = state.token;

+ 10 - 4
toolchain/parse/node_kind.def

@@ -223,8 +223,9 @@ CARBON_PARSE_NODE_KIND_BRACKET(LetDecl, LetIntroducer,
                                CARBON_TOKEN(Semi)
                                    CARBON_IF_ERROR(CARBON_TOKEN(Let)))
 
-// `var`:
+// `var` and `returned var`:
 //   VariableIntroducer
+//   _optional_ ReturnedSpecifier
 //   _external_: PatternBinding
 //   _optional_ VariableInitializer
 //   _optional_ _external_: expression
@@ -232,11 +233,15 @@ CARBON_PARSE_NODE_KIND_BRACKET(LetDecl, LetIntroducer,
 //
 // The VariableInitializer and following expression are paired: either both will
 // be present, or neither will.
-CARBON_PARSE_NODE_KIND_CHILD_COUNT(VariableIntroducer, 0, CARBON_TOKEN(Var))
+CARBON_PARSE_NODE_KIND_CHILD_COUNT(VariableIntroducer, 0,
+                                   CARBON_TOKEN(Var)
+                                       CARBON_IF_ERROR(CARBON_TOKEN(Returned)))
+CARBON_PARSE_NODE_KIND_CHILD_COUNT(ReturnedSpecifier, 0, CARBON_TOKEN(Returned))
 CARBON_PARSE_NODE_KIND_CHILD_COUNT(VariableInitializer, 0, CARBON_TOKEN(Equal))
 CARBON_PARSE_NODE_KIND_BRACKET(VariableDecl, VariableIntroducer,
                                CARBON_TOKEN(Semi)
-                                   CARBON_IF_ERROR(CARBON_TOKEN(Var)))
+                                   CARBON_IF_ERROR(CARBON_TOKEN(Var)
+                                                       CARBON_TOKEN(Returned)))
 
 // An expression statement:
 //   _external_: expression
@@ -262,10 +267,11 @@ CARBON_PARSE_NODE_KIND_CHILD_COUNT(ContinueStatement, 1,
 
 // `return`:
 //   ReturnStatementStart
-//   _optional_ _external_: expression
+//   _optional_ ReturnVarSpecifier or _external_: expression
 // ReturnStatement
 CARBON_PARSE_NODE_KIND_CHILD_COUNT(ReturnStatementStart, 0,
                                    CARBON_TOKEN(Return))
+CARBON_PARSE_NODE_KIND_CHILD_COUNT(ReturnVarSpecifier, 0, CARBON_TOKEN(Var))
 CARBON_PARSE_NODE_KIND_BRACKET(ReturnStatement, ReturnStatementStart,
                                CARBON_TOKEN(Semi)
                                    CARBON_IF_ERROR(CARBON_TOKEN(Return)))

+ 15 - 7
toolchain/parse/state.def

@@ -229,7 +229,7 @@ CARBON_PARSE_STATE(DeclNameAndParamsAfterImplicit)
 // If `Semi`:
 //   1. DeclScopeLoop
 // If `Var`:
-//   1. VarAsSemicolon
+//   1. VarAsDecl
 //   2. DeclScopeLoop
 // If `Let`:
 //   1. Let
@@ -568,7 +568,9 @@ CARBON_PARSE_STATE_VARIANTS2(PatternFinish, Generic, Regular)
 // If `Return`:
 //   1. StatementReturn
 // If `Var`:
-//   1. VarAsSemicolon
+//   1. VarAsDecl
+// If `Returned`:
+//   1. VarAsReturned
 // If `While`:
 //   1. StatementWhile
 // Else:
@@ -660,6 +662,8 @@ CARBON_PARSE_STATE(StatementIfElseBlockFinish)
 //
 // If `Semi`:
 //   1. StatementReturnFinish
+// If `Var`:
+//   1. StatementReturnFinish
 // Else:
 //   1. Expr
 //   2. StatementReturnFinish
@@ -725,13 +729,17 @@ CARBON_PARSE_STATE_VARIANTS3(TypeIntroducer, Class, Interface, NamedConstraint)
 //   (state done)
 CARBON_PARSE_STATE_VARIANTS3(TypeAfterParams, Class, Interface, NamedConstraint)
 
-// Handles the start of a `var`.
+// Handles the start of a `var` or `returned var`.
 //
-// Always:
+// On error:
+//   (state done)
+// Else:
 //   1. PatternAsVariable
 //   2. VarAfterPattern
-//   3. VarFinishAs(Semicolon|For)
-CARBON_PARSE_STATE_VARIANTS2(Var, Semicolon, For)
+//   3. VarFinishAs(Decl|For)
+//
+// VarAsReturned uses VarFinishAsDecl.
+CARBON_PARSE_STATE_VARIANTS3(Var, Decl, Returned, For)
 
 // Handles `var` after the pattern, either followed by an initializer or the
 // semicolon.
@@ -746,7 +754,7 @@ CARBON_PARSE_STATE(VarAfterPattern)
 //
 // Always:
 //   (state done)
-CARBON_PARSE_STATE_VARIANTS2(VarFinish, Semicolon, For)
+CARBON_PARSE_STATE_VARIANTS2(VarFinish, Decl, For)
 
 // Handles the start of a `let`.
 //

+ 38 - 0
toolchain/parse/testdata/for/fail_returned_var.carbon

@@ -0,0 +1,38 @@
+// 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
+//
+// AUTOUPDATE
+
+fn foo() -> i32 {
+  // TODO: Should we allow this?
+  // CHECK:STDERR: fail_returned_var.carbon:[[@LINE+3]]:8: ERROR: Expected `var` declaration.
+  // CHECK:STDERR:   for (returned var x: i32 in y) {
+  // CHECK:STDERR:        ^
+  for (returned var x: i32 in y) {
+    return var;
+  }
+}
+
+// CHECK:STDOUT: - filename: fail_returned_var.carbon
+// CHECK:STDOUT:   parse_tree: [
+// CHECK:STDOUT:     {kind: 'FileStart', text: ''},
+// CHECK:STDOUT:         {kind: 'FunctionIntroducer', text: 'fn'},
+// CHECK:STDOUT:         {kind: 'Name', text: 'foo'},
+// CHECK:STDOUT:           {kind: 'ParameterListStart', text: '('},
+// CHECK:STDOUT:         {kind: 'ParameterList', text: ')', subtree_size: 2},
+// CHECK:STDOUT:           {kind: 'Literal', text: 'i32'},
+// CHECK:STDOUT:         {kind: 'ReturnType', text: '->', subtree_size: 2},
+// CHECK:STDOUT:       {kind: 'FunctionDefinitionStart', text: '{', subtree_size: 7},
+// CHECK:STDOUT:           {kind: 'ForHeaderStart', text: '('},
+// CHECK:STDOUT:           {kind: 'NameExpr', text: 'y'},
+// CHECK:STDOUT:         {kind: 'ForHeader', text: ')', has_error: yes, subtree_size: 3},
+// CHECK:STDOUT:           {kind: 'CodeBlockStart', text: '{'},
+// CHECK:STDOUT:             {kind: 'ReturnStatementStart', text: 'return'},
+// CHECK:STDOUT:             {kind: 'ReturnVarSpecifier', text: 'var'},
+// CHECK:STDOUT:           {kind: 'ReturnStatement', text: ';', subtree_size: 3},
+// CHECK:STDOUT:         {kind: 'CodeBlock', text: '}', subtree_size: 5},
+// CHECK:STDOUT:       {kind: 'ForStatement', text: 'for', subtree_size: 9},
+// CHECK:STDOUT:     {kind: 'FunctionDefinition', text: '}', subtree_size: 17},
+// CHECK:STDOUT:     {kind: 'FileEnd', text: ''},
+// CHECK:STDOUT:   ]

+ 27 - 0
toolchain/parse/testdata/return/fail_returned_no_var.carbon

@@ -0,0 +1,27 @@
+// 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
+//
+// AUTOUPDATE
+
+fn F() -> String {
+  // CHECK:STDERR: fail_returned_no_var.carbon:[[@LINE+3]]:12: ERROR: Expected `var` after `returned`.
+  // CHECK:STDERR:   returned fn G() -> i32;
+  // CHECK:STDERR:            ^
+  returned fn G() -> i32;
+}
+
+// CHECK:STDOUT: - filename: fail_returned_no_var.carbon
+// CHECK:STDOUT:   parse_tree: [
+// CHECK:STDOUT:     {kind: 'FileStart', text: ''},
+// CHECK:STDOUT:         {kind: 'FunctionIntroducer', text: 'fn'},
+// CHECK:STDOUT:         {kind: 'Name', text: 'F'},
+// CHECK:STDOUT:           {kind: 'ParameterListStart', text: '('},
+// CHECK:STDOUT:         {kind: 'ParameterList', text: ')', subtree_size: 2},
+// CHECK:STDOUT:           {kind: 'Literal', text: 'String'},
+// CHECK:STDOUT:         {kind: 'ReturnType', text: '->', subtree_size: 2},
+// CHECK:STDOUT:       {kind: 'FunctionDefinitionStart', text: '{', subtree_size: 7},
+// CHECK:STDOUT:       {kind: 'EmptyDecl', text: ';', has_error: yes},
+// CHECK:STDOUT:     {kind: 'FunctionDefinition', text: '}', subtree_size: 9},
+// CHECK:STDOUT:     {kind: 'FileEnd', text: ''},
+// CHECK:STDOUT:   ]

+ 27 - 0
toolchain/parse/testdata/return/fail_var_no_semi.carbon

@@ -0,0 +1,27 @@
+// 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
+//
+// AUTOUPDATE
+
+fn F() {
+  return var
+// CHECK:STDERR: fail_var_no_semi.carbon:[[@LINE+3]]:1: ERROR: `return` statements must end with a `;`.
+// CHECK:STDERR: }
+// CHECK:STDERR: ^
+}
+
+// CHECK:STDOUT: - filename: fail_var_no_semi.carbon
+// CHECK:STDOUT:   parse_tree: [
+// CHECK:STDOUT:     {kind: 'FileStart', text: ''},
+// CHECK:STDOUT:         {kind: 'FunctionIntroducer', text: 'fn'},
+// CHECK:STDOUT:         {kind: 'Name', text: 'F'},
+// CHECK:STDOUT:           {kind: 'ParameterListStart', text: '('},
+// CHECK:STDOUT:         {kind: 'ParameterList', text: ')', subtree_size: 2},
+// CHECK:STDOUT:       {kind: 'FunctionDefinitionStart', text: '{', subtree_size: 5},
+// CHECK:STDOUT:         {kind: 'ReturnStatementStart', text: 'return'},
+// CHECK:STDOUT:         {kind: 'ReturnVarSpecifier', text: 'var'},
+// CHECK:STDOUT:       {kind: 'ReturnStatement', text: 'return', has_error: yes, subtree_size: 3},
+// CHECK:STDOUT:     {kind: 'FunctionDefinition', text: '}', subtree_size: 9},
+// CHECK:STDOUT:     {kind: 'FileEnd', text: ''},
+// CHECK:STDOUT:   ]

+ 35 - 0
toolchain/parse/testdata/return/returned_var.carbon

@@ -0,0 +1,35 @@
+// 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
+//
+// AUTOUPDATE
+
+fn F() -> String {
+  returned var s: String = "hello";
+  return var;
+}
+
+// CHECK:STDOUT: - filename: returned_var.carbon
+// CHECK:STDOUT:   parse_tree: [
+// CHECK:STDOUT:     {kind: 'FileStart', text: ''},
+// CHECK:STDOUT:         {kind: 'FunctionIntroducer', text: 'fn'},
+// CHECK:STDOUT:         {kind: 'Name', text: 'F'},
+// CHECK:STDOUT:           {kind: 'ParameterListStart', text: '('},
+// CHECK:STDOUT:         {kind: 'ParameterList', text: ')', subtree_size: 2},
+// CHECK:STDOUT:           {kind: 'Literal', text: 'String'},
+// CHECK:STDOUT:         {kind: 'ReturnType', text: '->', subtree_size: 2},
+// CHECK:STDOUT:       {kind: 'FunctionDefinitionStart', text: '{', subtree_size: 7},
+// CHECK:STDOUT:         {kind: 'VariableIntroducer', text: 'var'},
+// CHECK:STDOUT:         {kind: 'ReturnedSpecifier', text: 'returned'},
+// CHECK:STDOUT:           {kind: 'Name', text: 's'},
+// CHECK:STDOUT:           {kind: 'Literal', text: 'String'},
+// CHECK:STDOUT:         {kind: 'PatternBinding', text: ':', subtree_size: 3},
+// CHECK:STDOUT:         {kind: 'VariableInitializer', text: '='},
+// CHECK:STDOUT:         {kind: 'Literal', text: '"hello"'},
+// CHECK:STDOUT:       {kind: 'VariableDecl', text: ';', subtree_size: 8},
+// CHECK:STDOUT:         {kind: 'ReturnStatementStart', text: 'return'},
+// CHECK:STDOUT:         {kind: 'ReturnVarSpecifier', text: 'var'},
+// CHECK:STDOUT:       {kind: 'ReturnStatement', text: ';', subtree_size: 3},
+// CHECK:STDOUT:     {kind: 'FunctionDefinition', text: '}', subtree_size: 19},
+// CHECK:STDOUT:     {kind: 'FileEnd', text: ''},
+// CHECK:STDOUT:   ]

+ 2 - 0
toolchain/sem_ir/file.h

@@ -37,6 +37,8 @@ struct Function : public Printable<Function> {
 
   // The function name.
   NameId name_id;
+  // The first declaration of the function. This is a FunctionDeclaration.
+  InstId declaration_id = InstId::Invalid;
   // The definition, if the function has been defined or is currently being
   // defined. This is a FunctionDecl.
   InstId definition_id = InstId::Invalid;