Просмотр исходного кода

Add support for `->?` return forms (#6849)

This includes checking and lowering for concrete form literals. Support
for symbolic forms is future work.

---------

Co-authored-by: Jon Ross-Perkins <jperkins@google.com>
Geoff Romer 1 месяц назад
Родитель
Сommit
18cfeb7476

+ 3 - 5
toolchain/check/cpp/import.cpp

@@ -1364,27 +1364,25 @@ static auto CreateFunctionSignatureInsts(
     Context& context, SemIR::LocId loc_id, clang::FunctionDecl* clang_decl,
     SemIR::ClangDeclKey::Signature signature)
     -> std::optional<FunctionSignatureInsts> {
-  context.full_pattern_stack().PushFullPattern(
-      FullPatternStack::Kind::ImplicitParamList);
-  std::optional pop = llvm::scope_exit(
-      [&context] { context.full_pattern_stack().PopFullPattern(); });
+  context.full_pattern_stack().StartImplicitParamList();
   auto implicit_param_patterns_id =
       MakeImplicitParamPatternsBlockId(context, loc_id, *clang_decl);
   if (!implicit_param_patterns_id.has_value()) {
     return std::nullopt;
   }
   context.full_pattern_stack().EndImplicitParamList();
+  context.full_pattern_stack().StartExplicitParamList();
   auto param_patterns_id =
       MakeParamPatternsBlockId(context, loc_id, *clang_decl, signature);
   if (!param_patterns_id.has_value()) {
     return std::nullopt;
   }
+  context.full_pattern_stack().EndExplicitParamList();
   auto [return_type_inst_id, return_form_inst_id, return_patterns_id] =
       GetReturnInfo(context, loc_id, clang_decl);
   if (return_type_inst_id == SemIR::ErrorInst::TypeInstId) {
     return std::nullopt;
   }
-  pop.reset();
 
   auto match_results =
       CalleePatternMatch(context, implicit_param_patterns_id, param_patterns_id,

+ 62 - 10
toolchain/check/full_pattern_stack.h

@@ -29,33 +29,85 @@ class FullPatternStack {
  public:
   explicit FullPatternStack(LexicalLookup* lookup) : lookup_(lookup) {}
 
-  // The kind of a full-pattern. Note that an implicit parameter list and
-  // adjacent explicit parameter list together form a single full-pattern,
-  // but we separate them here in order to represent the state transition
-  // between them.
+  // The kind of a full-pattern. There are two primary kinds: name binding
+  // declarations and parameterized entity declarations. However, for efficiency
+  // we also use this enum to track state transitions within a parameterized
+  // entity declaration. A parameterized entity declaration always starts and
+  // finishes in the `NotInEitherParamList` state, and can transition to either
+  // the `ImplicitParamList` or `ExplicitParamList` state, and then back to the
+  // `NotInEitherParamList` state.
   enum class Kind {
+    // A name-binding declaration, such as a `let` or `var` statement.
     NameBindingDecl,
+
+    // The implicit parameter list of a function or impl declaration.
     ImplicitParamList,
+
+    // The explicit parameter list of a function declaration.
     ExplicitParamList,
+
+    // This kind indicates that we're within the declaration of a parameterized
+    // entity (such as a function or impl), but not within an explicit or
+    // implicit parameter list. This is primarily useful for the return part of
+    // a function declaration, which doesn't contain pattern syntax, but can
+    // implicitly introduce output parameter patterns. However, the parse tree
+    // doesn't let us reliably distinguish the return part from the part before
+    // the parameter lists (or between them), particularly in the case where
+    // there is no explicit parameter list.
+    NotInEitherParamList
   };
 
+  auto empty() const -> bool { return kind_stack_.empty(); }
+
   // The kind of the current full-pattern.
   auto CurrentKind() const -> Kind { return kind_stack_.back(); }
 
-  // Marks the start of a new full-pattern of the specified kind.
-  auto PushFullPattern(Kind kind) -> void {
-    kind_stack_.push_back(kind);
+  // Marks the start of a new full-pattern for a parameterized entity
+  // declaration, such as a function or impl. The kind is initially
+  // NotInEitherParamList.
+  auto PushParameterizedDecl() -> void {
+    kind_stack_.push_back(Kind::NotInEitherParamList);
     bind_name_stack_.PushArray();
   }
 
-  // Marks the end of an implicit parameter list, and the presumptive start
-  // of the corresponding explicit parameter list.
+  // Marks the start of a new full-pattern for a name binding declaration.
+  auto PushNameBindingDecl() -> void {
+    kind_stack_.push_back(Kind::NameBindingDecl);
+    bind_name_stack_.PushArray();
+  }
+
+  // Marks the start of the current parameterized entity's implicit parameter
+  // list.
+  auto StartImplicitParamList() -> void {
+    CARBON_CHECK(kind_stack_.back() == Kind::NotInEitherParamList, "{0}",
+                 kind_stack_.back());
+    kind_stack_.back() = Kind::ImplicitParamList;
+  }
+
+  // Marks the end of the current parameterized entity's implicit parameter
+  // list.
   auto EndImplicitParamList() -> void {
     CARBON_CHECK(kind_stack_.back() == Kind::ImplicitParamList, "{0}",
                  kind_stack_.back());
+    kind_stack_.back() = Kind::NotInEitherParamList;
+  }
+
+  // Marks the start of the current parameterized entity's explicit parameter
+  // list.
+  auto StartExplicitParamList() -> void {
+    CARBON_CHECK(kind_stack_.back() == Kind::NotInEitherParamList, "{0}",
+                 kind_stack_.back());
     kind_stack_.back() = Kind::ExplicitParamList;
   }
 
+  // Marks the end of the current parameterized entity's explicit parameter
+  // list.
+  auto EndExplicitParamList() -> void {
+    CARBON_CHECK(kind_stack_.back() == Kind::ExplicitParamList, "{0}",
+                 kind_stack_.back());
+    kind_stack_.back() = Kind::NotInEitherParamList;
+  }
+
   // Marks the start of the initializer for the current name binding decl.
   auto StartPatternInitializer() -> void {
     CARBON_CHECK(kind_stack_.back() == Kind::NameBindingDecl);
@@ -98,7 +150,7 @@ class FullPatternStack {
   // Runs verification that the processing cleanly finished.
   auto VerifyOnFinish() const -> void {
     CARBON_CHECK(kind_stack_.empty(),
-                 "full_pattern_stack still has {1} entries",
+                 "full_pattern_stack still has {0} entries",
                  kind_stack_.size());
   }
 

+ 14 - 8
toolchain/check/function.cpp

@@ -35,9 +35,11 @@ auto FindSelfPattern(Context& context,
 auto AddReturnPatterns(Context& context, SemIR::LocId loc_id,
                        Context::FormExpr form_expr) -> SemIR::InstBlockId {
   llvm::SmallVector<SemIR::InstId, 1> return_patterns;
-  auto form_inst = context.insts().Get(form_expr.form_inst_id);
+  auto form_inst = context.insts().Get(
+      context.constant_values().GetConstantInstId(form_expr.form_inst_id));
   CARBON_KIND_SWITCH(form_inst) {
-    case SemIR::RefForm::Kind: {
+    case SemIR::RefForm::Kind:
+    case SemIR::ValueForm::Kind: {
       break;
     }
     case CARBON_KIND(SemIR::InitForm _): {
@@ -56,6 +58,11 @@ auto AddReturnPatterns(Context& context, SemIR::LocId loc_id,
     case SemIR::ErrorInst::Kind: {
       break;
     }
+    case SemIR::SymbolicBinding::Kind:
+      CARBON_CHECK(
+          context.constant_values().Get(form_expr.form_inst_id).is_symbolic());
+      context.TODO(loc_id, "Support symbolic return forms");
+      break;
     default:
       CARBON_FATAL("unexpected inst kind: {0}", form_inst);
   }
@@ -115,8 +122,7 @@ static auto MakeFunctionSignature(Context& context, SemIR::LocId loc_id,
 
   // Build and add a `[ref self: Self]` parameter if needed.
   if (args.self_type_id.has_value()) {
-    context.full_pattern_stack().PushFullPattern(
-        FullPatternStack::Kind::ImplicitParamList);
+    context.full_pattern_stack().StartImplicitParamList();
 
     BeginSubpattern(context);
     auto self_type_region_id = EndSubpatternAsExpr(
@@ -129,13 +135,11 @@ static auto MakeFunctionSignature(Context& context, SemIR::LocId loc_id,
         context.inst_blocks().Add({insts.self_param_id});
 
     context.full_pattern_stack().EndImplicitParamList();
-  } else {
-    context.full_pattern_stack().PushFullPattern(
-        FullPatternStack::Kind::ExplicitParamList);
   }
 
   // Build and add any explicit parameters. We always use value parameters for
   // now.
+  context.full_pattern_stack().StartExplicitParamList();
   if (args.param_type_ids.empty()) {
     insts.param_patterns_id = SemIR::InstBlockId::Empty;
   } else {
@@ -151,6 +155,7 @@ static auto MakeFunctionSignature(Context& context, SemIR::LocId loc_id,
     }
     insts.param_patterns_id = context.inst_block_stack().Pop();
   }
+  context.full_pattern_stack().EndExplicitParamList();
 
   // Build and add the return type. We always use an initializing form for now.
   if (args.return_type_id.has_value()) {
@@ -168,7 +173,6 @@ static auto MakeFunctionSignature(Context& context, SemIR::LocId loc_id,
   insts.call_params_id = match_results.call_params_id;
   insts.call_param_ranges = match_results.param_ranges;
 
-  context.full_pattern_stack().PopFullPattern();
   auto [pattern_block_id, decl_block_id] =
       FinishFunctionSignature(context, /*check_unused=*/false);
   insts.pattern_block_id = pattern_block_id;
@@ -428,10 +432,12 @@ auto StartFunctionSignature(Context& context) -> void {
   context.scope_stack().PushForDeclName();
   context.inst_block_stack().Push();
   context.pattern_block_stack().Push();
+  context.full_pattern_stack().PushParameterizedDecl();
 }
 
 auto FinishFunctionSignature(Context& context, bool check_unused)
     -> FinishFunctionSignatureResult {
+  context.full_pattern_stack().PopFullPattern();
   auto pattern_block_id = context.pattern_block_stack().Pop();
   auto decl_block_id = context.inst_block_stack().Pop();
   context.scope_stack().Pop(check_unused);

+ 3 - 0
toolchain/check/handle_binding_pattern.cpp

@@ -340,6 +340,9 @@ static auto HandleAnyBindingPattern(Context& context, Parse::NodeId node_id,
       context.node_stack().Push(node_id, binding_pattern_id);
       break;
     }
+
+    case FullPatternStack::Kind::NotInEitherParamList:
+      CARBON_FATAL("Unreachable");
   }
   return true;
 }

+ 18 - 7
toolchain/check/handle_function.cpp

@@ -49,19 +49,29 @@ auto HandleParseNode(Context& context, Parse::FunctionIntroducerId node_id)
   return true;
 }
 
-auto HandleParseNode(Context& context, Parse::ReturnTypeId node_id) -> bool {
-  auto [type_node_id, type_inst_id] = context.node_stack().PopExprWithNodeId();
-
-  // Propagate the type expression.
-  auto form_expr = ReturnExprAsForm(context, type_node_id, type_inst_id);
+// Handles a `->` or `->?` return declaration.
+static auto HandleReturnDecl(Context& context, Parse::AnyReturnDeclId node_id)
+    -> bool {
+  auto [expr_node_id, expr_inst_id] = context.node_stack().PopExprWithNodeId();
+  Context::FormExpr form_expr = [&]() {
+    if (context.parse_tree().node_kind(node_id) == Parse::ReturnTypeId::Kind) {
+      return ReturnExprAsForm(context, expr_node_id, expr_inst_id);
+    } else {
+      return FormExprAsForm(context, expr_node_id, expr_inst_id);
+    }
+  }();
   context.PushReturnForm(form_expr);
   auto return_patterns_id = AddReturnPatterns(context, node_id, form_expr);
   context.node_stack().Push(node_id, return_patterns_id);
   return true;
 }
 
+auto HandleParseNode(Context& context, Parse::ReturnTypeId node_id) -> bool {
+  return HandleReturnDecl(context, node_id);
+}
+
 auto HandleParseNode(Context& context, Parse::ReturnFormId node_id) -> bool {
-  return context.TODO(node_id, "Support ->?");
+  return HandleReturnDecl(context, node_id);
 }
 
 // Diagnoses issues with the modifiers, removing modifiers that shouldn't be
@@ -486,7 +496,8 @@ static auto BuildFunctionDecl(Context& context,
   auto return_type_inst_id = SemIR::TypeInstId::None;
   auto return_form_inst_id = SemIR::InstId::None;
   if (auto [return_node, maybe_return_patterns_id] =
-          context.node_stack().PopWithNodeIdIf<Parse::NodeKind::ReturnType>();
+          context.node_stack()
+              .PopWithNodeIdIf<Parse::NodeCategory::ReturnDecl>();
       maybe_return_patterns_id) {
     return_patterns_id = *maybe_return_patterns_id;
     auto return_form = context.PopReturnForm();

+ 1 - 2
toolchain/check/handle_impl.cpp

@@ -63,8 +63,7 @@ auto HandleParseNode(Context& context, Parse::ImplIntroducerId node_id)
 auto HandleParseNode(Context& context, Parse::ForallId /*node_id*/) -> bool {
   // Push a pattern block for the signature of the `forall`.
   context.pattern_block_stack().Push();
-  context.full_pattern_stack().PushFullPattern(
-      FullPatternStack::Kind::ImplicitParamList);
+  context.full_pattern_stack().PushParameterizedDecl();
   return true;
 }
 

+ 3 - 2
toolchain/check/handle_let_and_var.cpp

@@ -62,8 +62,7 @@ static auto HandleIntroducer(Context& context, Parse::NodeId node_id) -> bool {
   // Push a bracketing node and pattern block to establish the pattern context.
   context.node_stack().Push(node_id);
   context.pattern_block_stack().Push();
-  context.full_pattern_stack().PushFullPattern(
-      FullPatternStack::Kind::NameBindingDecl);
+  context.full_pattern_stack().PushNameBindingDecl();
   BeginSubpattern(context);
   return true;
 }
@@ -113,6 +112,8 @@ auto HandleParseNode(Context& context, Parse::VariablePatternId node_id)
       break;
     case FullPatternStack::Kind::NameBindingDecl:
       break;
+    case FullPatternStack::Kind::NotInEitherParamList:
+      CARBON_FATAL("Unreachable");
   }
 
   auto pattern_id = AddPatternInst<SemIR::VarPattern>(

+ 1 - 2
toolchain/check/handle_loop_statement.cpp

@@ -115,8 +115,7 @@ auto HandleParseNode(Context& context, Parse::ForHeaderStartId node_id)
   // Begin an implicit let declaration context for the pattern.
   context.decl_introducer_state_stack().Push<Lex::TokenKind::Let>();
   context.pattern_block_stack().Push();
-  context.full_pattern_stack().PushFullPattern(
-      FullPatternStack::Kind::NameBindingDecl);
+  context.full_pattern_stack().PushNameBindingDecl();
   BeginSubpattern(context);
 
   context.node_stack().Push(node_id);

+ 1 - 2
toolchain/check/handle_name.cpp

@@ -128,8 +128,7 @@ auto HandleParseNode(Context& context,
     -> bool {
   // Push a pattern block stack entry to handle the parameter pattern.
   context.pattern_block_stack().Push();
-  context.full_pattern_stack().PushFullPattern(
-      FullPatternStack::Kind::ImplicitParamList);
+  context.full_pattern_stack().PushParameterizedDecl();
   // The parent is responsible for binding the name.
   context.node_stack().Push(node_id, GetIdentifierAsNameId(context, node_id));
   return true;

+ 4 - 1
toolchain/check/handle_pattern_list.cpp

@@ -21,6 +21,7 @@ static auto HandlePatternListStart(Context& context, Parse::NodeId node_id)
 
 auto HandleParseNode(Context& context, Parse::ImplicitParamListStartId node_id)
     -> bool {
+  context.full_pattern_stack().StartImplicitParamList();
   return HandlePatternListStart(context, node_id);
 }
 
@@ -31,7 +32,7 @@ auto HandleParseNode(Context& context, Parse::TuplePatternStartId node_id)
 
 auto HandleParseNode(Context& context, Parse::ExplicitParamListStartId node_id)
     -> bool {
-  context.full_pattern_stack().EndImplicitParamList();
+  context.full_pattern_stack().StartExplicitParamList();
   return HandlePatternListStart(context, node_id);
 }
 
@@ -53,12 +54,14 @@ static auto HandleParamListEnd(Context& context, Parse::NodeId node_id,
 
 auto HandleParseNode(Context& context, Parse::ImplicitParamListId node_id)
     -> bool {
+  context.full_pattern_stack().EndImplicitParamList();
   return HandleParamListEnd(context, node_id,
                             Parse::NodeKind::ImplicitParamListStart);
 }
 
 auto HandleParseNode(Context& context, Parse::ExplicitParamListId node_id)
     -> bool {
+  context.full_pattern_stack().EndExplicitParamList();
   return HandleParamListEnd(context, node_id,
                             Parse::NodeKind::ExplicitParamListStart);
 }

+ 2 - 2
toolchain/check/node_stack.h

@@ -403,6 +403,8 @@ class NodeStack {
                               Parse::NodeCategory::Statement |
                               Parse::NodeCategory::Modifier,
                           Id::Kind::None);
+    set_id_if_category_is(Parse::NodeCategory::ReturnDecl,
+                          Id::KindFor<SemIR::InstBlockId>());
     return result;
   }
 
@@ -426,8 +428,6 @@ class NodeStack {
       case Parse::NodeKind::IfExprIf:
       case Parse::NodeKind::ImplicitParamList:
       case Parse::NodeKind::WhileConditionStart:
-      case Parse::NodeKind::ReturnType:
-      case Parse::NodeKind::ReturnForm:
         return Id::KindFor<SemIR::InstBlockId>();
       case Parse::NodeKind::FunctionDefinitionStart:
       case Parse::NodeKind::BuiltinFunctionDefinitionStart:

+ 145 - 23
toolchain/check/testdata/function/call/form.carbon

@@ -151,67 +151,84 @@ fn F(Form:! Core.Form()) {
   let y:? Form = 0;
 }
 
-// --- fail_todo_ref_return_form.carbon
+// --- ref_return_form.carbon
 library "[[@TEST_NAME]]";
 
 //@dump-sem-ir-begin
-// CHECK:STDERR: fail_todo_ref_return_form.carbon:[[@LINE+4]]:8: error: semantics TODO: `Support ->?` [SemanticsTodo]
-// CHECK:STDERR: fn F() ->? form(ref i32);
-// CHECK:STDERR:        ^~~~~~~~~~~~~~~~~
-// CHECK:STDERR:
 fn F() ->? form(ref i32);
 //@dump-sem-ir-end
 
 fn G() {
   //@dump-sem-ir-begin
-  let ref x: i32 = F();
+  let ref _: i32 = F();
   //@dump-sem-ir-end
 }
 
-// --- fail_todo_var_return_form.carbon
+// --- var_return_form.carbon
 library "[[@TEST_NAME]]";
 
 //@dump-sem-ir-begin
-// CHECK:STDERR: fail_todo_var_return_form.carbon:[[@LINE+4]]:8: error: semantics TODO: `Support ->?` [SemanticsTodo]
-// CHECK:STDERR: fn F() ->? form(var i32);
-// CHECK:STDERR:        ^~~~~~~~~~~~~~~~~
-// CHECK:STDERR:
 fn F() ->? form(var i32);
 //@dump-sem-ir-end
 
 fn G() {
   //@dump-sem-ir-begin
-  let var x: i32 = F();
+  let var _: i32 = F();
   //@dump-sem-ir-end
 }
 
-// --- fail_todo_val_return_form.carbon
+// --- val_return_form.carbon
 library "[[@TEST_NAME]]";
 
 //@dump-sem-ir-begin
-// CHECK:STDERR: fail_todo_val_return_form.carbon:[[@LINE+4]]:8: error: semantics TODO: `Support ->?` [SemanticsTodo]
-// CHECK:STDERR: fn F() ->? form(val i32);
-// CHECK:STDERR:        ^~~~~~~~~~~~~~~~~
-// CHECK:STDERR:
 fn F() ->? form(val i32);
 //@dump-sem-ir-end
 
 fn G() {
   //@dump-sem-ir-begin
-  let x: i32 = F();
+  let _: i32 = F();
   //@dump-sem-ir-end
 }
 
 // --- fail_return_form_not_form.carbon
 library "[[@TEST_NAME]]";
 
-// CHECK:STDERR: fail_return_form_not_form.carbon:[[@LINE+4]]:8: error: semantics TODO: `Support ->?` [SemanticsTodo]
+// CHECK:STDERR: fail_return_form_not_form.carbon:[[@LINE+7]]:12: error: cannot implicitly convert expression of type `type` to `Core.Form` [ConversionFailure]
 // CHECK:STDERR: fn F() ->? i32;
-// CHECK:STDERR:        ^~~~~~~
+// CHECK:STDERR:            ^~~
+// CHECK:STDERR: fail_return_form_not_form.carbon:[[@LINE+4]]:12: note: type `type` does not implement interface `Core.ImplicitAs(Core.Form)` [MissingImplInMemberAccessInContext]
+// CHECK:STDERR: fn F() ->? i32;
+// CHECK:STDERR:            ^~~
 // CHECK:STDERR:
 fn F() ->? i32;
+// CHECK:STDERR: fail_return_form_not_form.carbon:[[@LINE+11]]:12: error: `ref` tag is not an argument to a `ref` parameter [RefTagNoRefParam]
+// CHECK:STDERR: fn F() ->? ref i32;
+// CHECK:STDERR:            ^~~~~~~
+// CHECK:STDERR:
+// CHECK:STDERR: fail_return_form_not_form.carbon:[[@LINE+7]]:12: error: cannot implicitly convert expression of type `type` to `Core.Form` [ConversionFailure]
+// CHECK:STDERR: fn F() ->? ref i32;
+// CHECK:STDERR:            ^~~~~~~
+// CHECK:STDERR: fail_return_form_not_form.carbon:[[@LINE+4]]:12: note: type `type` does not implement interface `Core.ImplicitAs(Core.Form)` [MissingImplInMemberAccessInContext]
+// CHECK:STDERR: fn F() ->? ref i32;
+// CHECK:STDERR:            ^~~~~~~
+// CHECK:STDERR:
 fn F() ->? ref i32;
 
+// -- fail_todo_form_generic_return.carbon
+
+// CHECK:STDERR: fail_return_form_not_form.carbon:[[@LINE+11]]:26: error: semantics TODO: `Support symbolic return forms` [SemanticsTodo]
+// CHECK:STDERR: fn F(Form:! Core.Form()) ->? Form;
+// CHECK:STDERR:                          ^~~~~~~~
+// CHECK:STDERR:
+// CHECK:STDERR: fail_return_form_not_form.carbon:[[@LINE+7]]:1: error: redeclaration differs because of parameter count of 1 [RedeclParamCountDiffers]
+// CHECK:STDERR: fn F(Form:! Core.Form()) ->? Form;
+// CHECK:STDERR: ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+// CHECK:STDERR: fail_return_form_not_form.carbon:[[@LINE-23]]:1: note: previously declared with parameter count of 0 [RedeclParamCountPrevious]
+// CHECK:STDERR: fn F() ->? i32;
+// CHECK:STDERR: ^~~~~~~~~~~~~~~
+// CHECK:STDERR:
+fn F(Form:! Core.Form()) ->? Form;
+
 // CHECK:STDOUT: --- ref_form_param.carbon
 // CHECK:STDOUT:
 // CHECK:STDOUT: constants {
@@ -411,9 +428,114 @@ fn F() ->? ref i32;
 // CHECK:STDOUT:
 // CHECK:STDOUT: fn @F(%x.param: ref %empty_tuple.type);
 // CHECK:STDOUT:
-// CHECK:STDOUT: --- fail_todo_ref_return_form.carbon
+// CHECK:STDOUT: --- ref_return_form.carbon
+// CHECK:STDOUT:
+// CHECK:STDOUT: constants {
+// CHECK:STDOUT:   %int_32: Core.IntLiteral = int_value 32 [concrete]
+// CHECK:STDOUT:   %i32: type = class_type @Int, @Int(%int_32) [concrete]
+// CHECK:STDOUT:   %.1da: Core.Form = ref_form %i32 [concrete]
+// CHECK:STDOUT:   %F.type: type = fn_type @F [concrete]
+// CHECK:STDOUT:   %F: %F.type = struct_value () [concrete]
+// CHECK:STDOUT:   %pattern_type.7ce: type = pattern_type %i32 [concrete]
+// CHECK:STDOUT: }
+// CHECK:STDOUT:
+// CHECK:STDOUT: file {
+// CHECK:STDOUT:   %F.decl: %F.type = fn_decl @F [concrete = constants.%F] {} {
+// CHECK:STDOUT:     %i32: type = type_literal constants.%i32 [concrete = constants.%i32]
+// CHECK:STDOUT:     %.loc4: Core.Form = ref_form %i32 [concrete = constants.%.1da]
+// CHECK:STDOUT:   }
+// CHECK:STDOUT: }
+// CHECK:STDOUT:
+// CHECK:STDOUT: fn @F() -> ref %i32;
+// CHECK:STDOUT:
+// CHECK:STDOUT: fn @G() {
+// CHECK:STDOUT: !entry:
+// CHECK:STDOUT:   name_binding_decl {
+// CHECK:STDOUT:     %_.patt: %pattern_type.7ce = ref_binding_pattern _ [concrete]
+// CHECK:STDOUT:   }
+// CHECK:STDOUT:   %F.ref: %F.type = name_ref F, file.%F.decl [concrete = constants.%F]
+// CHECK:STDOUT:   %F.call: ref %i32 = call %F.ref()
+// CHECK:STDOUT:   %i32: type = type_literal constants.%i32 [concrete = constants.%i32]
+// CHECK:STDOUT:   %_: ref %i32 = ref_binding _, %F.call
+// CHECK:STDOUT:   <elided>
+// CHECK:STDOUT: }
+// CHECK:STDOUT:
+// CHECK:STDOUT: --- var_return_form.carbon
+// CHECK:STDOUT:
+// CHECK:STDOUT: constants {
+// CHECK:STDOUT:   %int_32: Core.IntLiteral = int_value 32 [concrete]
+// CHECK:STDOUT:   %empty_tuple.type: type = tuple_type () [concrete]
+// CHECK:STDOUT:   %i32: type = class_type @Int, @Int(%int_32) [concrete]
+// CHECK:STDOUT:   %.ff5: Core.Form = init_form %i32 [concrete]
+// CHECK:STDOUT:   %pattern_type.7ce: type = pattern_type %i32 [concrete]
+// CHECK:STDOUT:   %F.type: type = fn_type @F [concrete]
+// CHECK:STDOUT:   %F: %F.type = struct_value () [concrete]
+// CHECK:STDOUT:   %Destroy.Op.type: type = fn_type @Destroy.Op [concrete]
+// CHECK:STDOUT:   %Destroy.Op: %Destroy.Op.type = struct_value () [concrete]
+// CHECK:STDOUT: }
+// CHECK:STDOUT:
+// CHECK:STDOUT: file {
+// CHECK:STDOUT:   %F.decl: %F.type = fn_decl @F [concrete = constants.%F] {
+// CHECK:STDOUT:     %return.patt: %pattern_type.7ce = return_slot_pattern [concrete]
+// CHECK:STDOUT:     %return.param_patt: %pattern_type.7ce = out_param_pattern %return.patt [concrete]
+// CHECK:STDOUT:   } {
+// CHECK:STDOUT:     %i32: type = type_literal constants.%i32 [concrete = constants.%i32]
+// CHECK:STDOUT:     %.loc4: Core.Form = init_form %i32 [concrete = constants.%.ff5]
+// CHECK:STDOUT:     %return.param: ref %i32 = out_param call_param0
+// CHECK:STDOUT:     %return: ref %i32 = return_slot %return.param
+// CHECK:STDOUT:   }
+// CHECK:STDOUT: }
+// CHECK:STDOUT:
+// CHECK:STDOUT: fn @F() -> out %return.param: %i32;
+// CHECK:STDOUT:
+// CHECK:STDOUT: fn @G() {
+// CHECK:STDOUT: !entry:
+// CHECK:STDOUT:   name_binding_decl {
+// CHECK:STDOUT:     %_.patt: %pattern_type.7ce = ref_binding_pattern _ [concrete]
+// CHECK:STDOUT:     %_.var_patt: %pattern_type.7ce = var_pattern %_.patt [concrete]
+// CHECK:STDOUT:   }
+// CHECK:STDOUT:   %_.var: ref %i32 = var %_.var_patt
+// CHECK:STDOUT:   %F.ref: %F.type = name_ref F, file.%F.decl [concrete = constants.%F]
+// CHECK:STDOUT:   %F.call: init %i32 = call %F.ref()
+// CHECK:STDOUT:   assign %_.var, %F.call
+// CHECK:STDOUT:   %i32: type = type_literal constants.%i32 [concrete = constants.%i32]
+// CHECK:STDOUT:   %_: ref %i32 = ref_binding _, %_.var
+// CHECK:STDOUT:   %Destroy.Op.bound: <bound method> = bound_method %_.var, constants.%Destroy.Op
+// CHECK:STDOUT:   %Destroy.Op.call: init %empty_tuple.type = call %Destroy.Op.bound(%_.var)
+// CHECK:STDOUT:   <elided>
+// CHECK:STDOUT: }
+// CHECK:STDOUT:
+// CHECK:STDOUT: fn @Destroy.Op(%self.param: ref %i32) = "no_op";
 // CHECK:STDOUT:
-// CHECK:STDOUT: --- fail_todo_var_return_form.carbon
+// CHECK:STDOUT: --- val_return_form.carbon
 // CHECK:STDOUT:
-// CHECK:STDOUT: --- fail_todo_val_return_form.carbon
+// CHECK:STDOUT: constants {
+// CHECK:STDOUT:   %int_32: Core.IntLiteral = int_value 32 [concrete]
+// CHECK:STDOUT:   %i32: type = class_type @Int, @Int(%int_32) [concrete]
+// CHECK:STDOUT:   %.754: Core.Form = value_form %i32 [concrete]
+// CHECK:STDOUT:   %F.type: type = fn_type @F [concrete]
+// CHECK:STDOUT:   %F: %F.type = struct_value () [concrete]
+// CHECK:STDOUT:   %pattern_type.7ce: type = pattern_type %i32 [concrete]
+// CHECK:STDOUT: }
+// CHECK:STDOUT:
+// CHECK:STDOUT: file {
+// CHECK:STDOUT:   %F.decl: %F.type = fn_decl @F [concrete = constants.%F] {} {
+// CHECK:STDOUT:     %i32: type = type_literal constants.%i32 [concrete = constants.%i32]
+// CHECK:STDOUT:     %.loc4: Core.Form = value_form %i32 [concrete = constants.%.754]
+// CHECK:STDOUT:   }
+// CHECK:STDOUT: }
+// CHECK:STDOUT:
+// CHECK:STDOUT: fn @F() -> val %i32;
+// CHECK:STDOUT:
+// CHECK:STDOUT: fn @G() {
+// CHECK:STDOUT: !entry:
+// CHECK:STDOUT:   name_binding_decl {
+// CHECK:STDOUT:     %_.patt: %pattern_type.7ce = value_binding_pattern _ [concrete]
+// CHECK:STDOUT:   }
+// CHECK:STDOUT:   %F.ref: %F.type = name_ref F, file.%F.decl [concrete = constants.%F]
+// CHECK:STDOUT:   %F.call: %i32 = call %F.ref()
+// CHECK:STDOUT:   %i32: type = type_literal constants.%i32 [concrete = constants.%i32]
+// CHECK:STDOUT:   %_: %i32 = value_binding _, %F.call
+// CHECK:STDOUT:   <elided>
+// CHECK:STDOUT: }
 // CHECK:STDOUT:

+ 56 - 8
toolchain/lower/file_context.cpp

@@ -519,6 +519,26 @@ auto FileContext::FunctionTypeInfoBuilder::HandleReturnForm(
                   ref_form.type_component_inst_id));
       return SetReturnByReference(return_type_id);
     }
+    case CARBON_KIND(SemIR::ValueForm val_form): {
+      auto return_type_id =
+          context_.sem_ir().types().GetTypeIdForTypeConstantId(
+              SemIR::GetConstantValueInSpecific(
+                  context_.sem_ir(), specific_id_,
+                  val_form.type_component_inst_id));
+      switch (
+          SemIR::ValueRepr::ForType(context_.sem_ir(), return_type_id).kind) {
+        case SemIR::ValueRepr::Unknown:
+        case SemIR::ValueRepr::Dependent:
+          return Abort();
+        case SemIR::ValueRepr::None:
+          return SetReturnByCopy(SemIR::TypeId::None);
+        case SemIR::ValueRepr::Copy:
+          return SetReturnByCopy(return_type_id);
+        case SemIR::ValueRepr::Pointer:
+        case SemIR::ValueRepr::Custom:
+          return SetReturnByReference(return_type_id);
+      }
+    }
     default:
       CARBON_FATAL("Unexpected inst kind: {0}", return_form_inst);
   }
@@ -526,12 +546,12 @@ auto FileContext::FunctionTypeInfoBuilder::HandleReturnForm(
 
 auto FileContext::FunctionTypeInfoBuilder::HandleParameter(
     SemIR::CallParamIndex index) -> bool {
+  const auto& sem_ir = context_.sem_ir();
   auto param_pattern_id = call_param_pattern_ids_[index.index];
-  auto param_pattern = context_.sem_ir().insts().Get(param_pattern_id);
+  auto param_pattern = sem_ir.insts().Get(param_pattern_id);
   auto param_type_id = ExtractScrutineeType(
-      context_.sem_ir(),
-      SemIR::GetTypeOfInstInSpecific(context_.sem_ir(), specific_id_,
-                                     param_pattern_id));
+      sem_ir,
+      SemIR::GetTypeOfInstInSpecific(sem_ir, specific_id_, param_pattern_id));
 
   // Returns the appropriate LoweredTypes for reference-like parameters.
   auto ref_lowered_types = [&]() -> LoweredTypes {
@@ -545,13 +565,42 @@ auto FileContext::FunctionTypeInfoBuilder::HandleParameter(
       !param_type_id.AsConstantId().is_symbolic(),
       "Found symbolic type id after resolution when lowering type {0}.",
       param_pattern.type_id());
-  CARBON_KIND_SWITCH(param_pattern) {
+
+  auto param_kind = param_pattern.kind();
+
+  // Treat a form parameter pattern like the kind of param pattern that
+  // corresponds to its form.
+  if (auto form_param_pattern =
+          param_pattern.TryAs<SemIR::FormParamPattern>()) {
+    auto form_binding_pattern = sem_ir.insts().GetAs<SemIR::FormBindingPattern>(
+        form_param_pattern->subpattern_id);
+    auto form_id =
+        sem_ir.entity_names().Get(form_binding_pattern.entity_name_id).form_id;
+    CARBON_CHECK(!form_id.is_symbolic(), "TODO");
+    auto form_inst_id = sem_ir.constant_values().GetInstId(form_id);
+    auto form_kind = sem_ir.insts().Get(form_inst_id).kind();
+    switch (form_kind) {
+      case SemIR::InitForm::Kind:
+        param_kind = SemIR::VarParamPattern::Kind;
+        break;
+      case SemIR::RefForm::Kind:
+        param_kind = SemIR::RefParamPattern::Kind;
+        break;
+      case SemIR::ValueForm::Kind:
+        param_kind = SemIR::ValueParamPattern::Kind;
+        break;
+      default:
+        CARBON_FATAL("Unexpected kind {0} for form inst", form_kind);
+    }
+  }
+
+  switch (param_kind) {
     case SemIR::RefParamPattern::Kind:
     case SemIR::VarParamPattern::Kind: {
       return AddLoweredParam(index, param_pattern_id, ref_lowered_types());
     }
     case SemIR::OutParamPattern::Kind: {
-      switch (SemIR::InitRepr::ForType(context_.sem_ir(), param_type_id).kind) {
+      switch (SemIR::InitRepr::ForType(sem_ir, param_type_id).kind) {
         case SemIR::InitRepr::InPlace:
           return AddLoweredParam(index, param_pattern_id, ref_lowered_types());
         case SemIR::InitRepr::ByCopy:
@@ -564,8 +613,7 @@ auto FileContext::FunctionTypeInfoBuilder::HandleParameter(
       }
     }
     case SemIR::ValueParamPattern::Kind: {
-      switch (auto value_rep =
-                  SemIR::ValueRepr::ForType(context_.sem_ir(), param_type_id);
+      switch (auto value_rep = SemIR::ValueRepr::ForType(sem_ir, param_type_id);
               value_rep.kind) {
         case SemIR::ValueRepr::Unknown:
           return Abort();

+ 247 - 0
toolchain/lower/testdata/function/call/form.carbon

@@ -0,0 +1,247 @@
+// 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-FILE: toolchain/testing/testdata/min_prelude/int.carbon
+//
+// AUTOUPDATE
+// TIP: To test this file alone, run:
+// TIP:   bazel test //toolchain/testing:file_test --test_arg=--file_tests=toolchain/lower/testdata/function/call/form.carbon
+// TIP: To dump output, run:
+// TIP:   bazel run //toolchain/testing:file_test -- --dump_output --file_tests=toolchain/lower/testdata/function/call/form.carbon
+
+// --- ref_form_param.carbon
+library "[[@TEST_NAME]]";
+
+fn F(x:? form(ref i32));
+
+fn G() {
+  var y: i32 = 0;
+  F(ref y);
+}
+
+// --- var_form_param.carbon
+library "[[@TEST_NAME]]";
+
+fn F(x:? form(var i32));
+
+fn G() {
+  F(0);
+}
+
+// --- val_form_param.carbon
+library "[[@TEST_NAME]]";
+
+fn F(x:? form(val i32));
+
+fn G() {
+  F(0);
+}
+
+// --- ref_return_form.carbon
+library "[[@TEST_NAME]]";
+
+fn F() ->? form(ref i32);
+
+fn G() {
+  let unused ref x: i32 = F();
+}
+
+// --- var_return_form.carbon
+library "[[@TEST_NAME]]";
+
+fn F() ->? form(var i32);
+
+fn G() {
+  let unused var x: i32 = F();
+}
+
+// --- val_return_form.carbon
+library "[[@TEST_NAME]]";
+
+fn F() ->? form(val i32);
+
+fn G() {
+  let unused x: i32 = F();
+}
+
+// CHECK:STDOUT: ; ModuleID = 'ref_form_param.carbon'
+// CHECK:STDOUT: source_filename = "ref_form_param.carbon"
+// CHECK:STDOUT:
+// CHECK:STDOUT: declare void @_CF.Main(ptr)
+// CHECK:STDOUT:
+// CHECK:STDOUT: ; Function Attrs: nounwind
+// CHECK:STDOUT: define void @_CG.Main() #0 !dbg !4 {
+// CHECK:STDOUT: entry:
+// CHECK:STDOUT:   %y.var = alloca i32, align 4, !dbg !7
+// CHECK:STDOUT:   call void @llvm.lifetime.start.p0(ptr %y.var), !dbg !7
+// CHECK:STDOUT:   store i32 0, ptr %y.var, align 4, !dbg !7
+// CHECK:STDOUT:   call void @_CF.Main(ptr %y.var), !dbg !8
+// CHECK:STDOUT:   ret void, !dbg !9
+// CHECK:STDOUT: }
+// CHECK:STDOUT:
+// CHECK:STDOUT: ; Function Attrs: nocallback nofree nosync nounwind willreturn memory(argmem: readwrite)
+// CHECK:STDOUT: declare void @llvm.lifetime.start.p0(ptr captures(none)) #1
+// CHECK:STDOUT:
+// CHECK:STDOUT: attributes #0 = { nounwind }
+// CHECK:STDOUT: attributes #1 = { nocallback nofree nosync nounwind willreturn memory(argmem: readwrite) }
+// CHECK:STDOUT:
+// CHECK:STDOUT: !llvm.module.flags = !{!0, !1}
+// CHECK:STDOUT: !llvm.dbg.cu = !{!2}
+// CHECK:STDOUT:
+// CHECK:STDOUT: !0 = !{i32 7, !"Dwarf Version", i32 5}
+// CHECK:STDOUT: !1 = !{i32 2, !"Debug Info Version", i32 3}
+// CHECK:STDOUT: !2 = distinct !DICompileUnit(language: DW_LANG_C_plus_plus, file: !3, producer: "carbon", isOptimized: false, runtimeVersion: 0, emissionKind: FullDebug)
+// CHECK:STDOUT: !3 = !DIFile(filename: "ref_form_param.carbon", directory: "")
+// CHECK:STDOUT: !4 = distinct !DISubprogram(name: "G", linkageName: "_CG.Main", scope: null, file: !3, line: 5, type: !5, spFlags: DISPFlagDefinition, unit: !2)
+// CHECK:STDOUT: !5 = !DISubroutineType(types: !6)
+// CHECK:STDOUT: !6 = !{null}
+// CHECK:STDOUT: !7 = !DILocation(line: 6, column: 3, scope: !4)
+// CHECK:STDOUT: !8 = !DILocation(line: 7, column: 3, scope: !4)
+// CHECK:STDOUT: !9 = !DILocation(line: 5, column: 1, scope: !4)
+// CHECK:STDOUT: ; ModuleID = 'var_form_param.carbon'
+// CHECK:STDOUT: source_filename = "var_form_param.carbon"
+// CHECK:STDOUT:
+// CHECK:STDOUT: declare void @_CF.Main(ptr)
+// CHECK:STDOUT:
+// CHECK:STDOUT: ; Function Attrs: nounwind
+// CHECK:STDOUT: define void @_CG.Main() #0 !dbg !4 {
+// CHECK:STDOUT: entry:
+// CHECK:STDOUT:   %.loc3_7.1.temp = alloca i32, align 4, !dbg !7
+// CHECK:STDOUT:   call void @llvm.lifetime.start.p0(ptr %.loc3_7.1.temp), !dbg !7
+// CHECK:STDOUT:   store i32 0, ptr %.loc3_7.1.temp, align 4, !dbg !7
+// CHECK:STDOUT:   call void @_CF.Main(ptr %.loc3_7.1.temp), !dbg !8
+// CHECK:STDOUT:   ret void, !dbg !9
+// CHECK:STDOUT: }
+// CHECK:STDOUT:
+// CHECK:STDOUT: ; Function Attrs: nocallback nofree nosync nounwind willreturn memory(argmem: readwrite)
+// CHECK:STDOUT: declare void @llvm.lifetime.start.p0(ptr captures(none)) #1
+// CHECK:STDOUT:
+// CHECK:STDOUT: attributes #0 = { nounwind }
+// CHECK:STDOUT: attributes #1 = { nocallback nofree nosync nounwind willreturn memory(argmem: readwrite) }
+// CHECK:STDOUT:
+// CHECK:STDOUT: !llvm.module.flags = !{!0, !1}
+// CHECK:STDOUT: !llvm.dbg.cu = !{!2}
+// CHECK:STDOUT:
+// CHECK:STDOUT: !0 = !{i32 7, !"Dwarf Version", i32 5}
+// CHECK:STDOUT: !1 = !{i32 2, !"Debug Info Version", i32 3}
+// CHECK:STDOUT: !2 = distinct !DICompileUnit(language: DW_LANG_C_plus_plus, file: !3, producer: "carbon", isOptimized: false, runtimeVersion: 0, emissionKind: FullDebug)
+// CHECK:STDOUT: !3 = !DIFile(filename: "var_form_param.carbon", directory: "")
+// CHECK:STDOUT: !4 = distinct !DISubprogram(name: "G", linkageName: "_CG.Main", scope: null, file: !3, line: 5, type: !5, spFlags: DISPFlagDefinition, unit: !2)
+// CHECK:STDOUT: !5 = !DISubroutineType(types: !6)
+// CHECK:STDOUT: !6 = !{null}
+// CHECK:STDOUT: !7 = !DILocation(line: 3, column: 6, scope: !4)
+// CHECK:STDOUT: !8 = !DILocation(line: 6, column: 3, scope: !4)
+// CHECK:STDOUT: !9 = !DILocation(line: 5, column: 1, scope: !4)
+// CHECK:STDOUT: ; ModuleID = 'val_form_param.carbon'
+// CHECK:STDOUT: source_filename = "val_form_param.carbon"
+// CHECK:STDOUT:
+// CHECK:STDOUT: declare void @_CF.Main(i32)
+// CHECK:STDOUT:
+// CHECK:STDOUT: ; Function Attrs: nounwind
+// CHECK:STDOUT: define void @_CG.Main() #0 !dbg !4 {
+// CHECK:STDOUT: entry:
+// CHECK:STDOUT:   call void @_CF.Main(i32 0), !dbg !7
+// CHECK:STDOUT:   ret void, !dbg !8
+// CHECK:STDOUT: }
+// CHECK:STDOUT:
+// CHECK:STDOUT: attributes #0 = { nounwind }
+// CHECK:STDOUT:
+// CHECK:STDOUT: !llvm.module.flags = !{!0, !1}
+// CHECK:STDOUT: !llvm.dbg.cu = !{!2}
+// CHECK:STDOUT:
+// CHECK:STDOUT: !0 = !{i32 7, !"Dwarf Version", i32 5}
+// CHECK:STDOUT: !1 = !{i32 2, !"Debug Info Version", i32 3}
+// CHECK:STDOUT: !2 = distinct !DICompileUnit(language: DW_LANG_C_plus_plus, file: !3, producer: "carbon", isOptimized: false, runtimeVersion: 0, emissionKind: FullDebug)
+// CHECK:STDOUT: !3 = !DIFile(filename: "val_form_param.carbon", directory: "")
+// CHECK:STDOUT: !4 = distinct !DISubprogram(name: "G", linkageName: "_CG.Main", scope: null, file: !3, line: 5, type: !5, spFlags: DISPFlagDefinition, unit: !2)
+// CHECK:STDOUT: !5 = !DISubroutineType(types: !6)
+// CHECK:STDOUT: !6 = !{null}
+// CHECK:STDOUT: !7 = !DILocation(line: 6, column: 3, scope: !4)
+// CHECK:STDOUT: !8 = !DILocation(line: 5, column: 1, scope: !4)
+// CHECK:STDOUT: ; ModuleID = 'ref_return_form.carbon'
+// CHECK:STDOUT: source_filename = "ref_return_form.carbon"
+// CHECK:STDOUT:
+// CHECK:STDOUT: declare ptr @_CF.Main()
+// CHECK:STDOUT:
+// CHECK:STDOUT: ; Function Attrs: nounwind
+// CHECK:STDOUT: define void @_CG.Main() #0 !dbg !4 {
+// CHECK:STDOUT: entry:
+// CHECK:STDOUT:   %F.call = call ptr @_CF.Main(), !dbg !7
+// CHECK:STDOUT:   ret void, !dbg !8
+// CHECK:STDOUT: }
+// CHECK:STDOUT:
+// CHECK:STDOUT: attributes #0 = { nounwind }
+// CHECK:STDOUT:
+// CHECK:STDOUT: !llvm.module.flags = !{!0, !1}
+// CHECK:STDOUT: !llvm.dbg.cu = !{!2}
+// CHECK:STDOUT:
+// CHECK:STDOUT: !0 = !{i32 7, !"Dwarf Version", i32 5}
+// CHECK:STDOUT: !1 = !{i32 2, !"Debug Info Version", i32 3}
+// CHECK:STDOUT: !2 = distinct !DICompileUnit(language: DW_LANG_C_plus_plus, file: !3, producer: "carbon", isOptimized: false, runtimeVersion: 0, emissionKind: FullDebug)
+// CHECK:STDOUT: !3 = !DIFile(filename: "ref_return_form.carbon", directory: "")
+// CHECK:STDOUT: !4 = distinct !DISubprogram(name: "G", linkageName: "_CG.Main", scope: null, file: !3, line: 5, type: !5, spFlags: DISPFlagDefinition, unit: !2)
+// CHECK:STDOUT: !5 = !DISubroutineType(types: !6)
+// CHECK:STDOUT: !6 = !{null}
+// CHECK:STDOUT: !7 = !DILocation(line: 6, column: 27, scope: !4)
+// CHECK:STDOUT: !8 = !DILocation(line: 5, column: 1, scope: !4)
+// CHECK:STDOUT: ; ModuleID = 'var_return_form.carbon'
+// CHECK:STDOUT: source_filename = "var_return_form.carbon"
+// CHECK:STDOUT:
+// CHECK:STDOUT: declare i32 @_CF.Main()
+// CHECK:STDOUT:
+// CHECK:STDOUT: ; Function Attrs: nounwind
+// CHECK:STDOUT: define void @_CG.Main() #0 !dbg !4 {
+// CHECK:STDOUT: entry:
+// CHECK:STDOUT:   %x.var = alloca i32, align 4, !dbg !7
+// CHECK:STDOUT:   call void @llvm.lifetime.start.p0(ptr %x.var), !dbg !7
+// CHECK:STDOUT:   %F.call = call i32 @_CF.Main(), !dbg !8
+// CHECK:STDOUT:   store i32 %F.call, ptr %x.var, align 4, !dbg !7
+// CHECK:STDOUT:   ret void, !dbg !9
+// CHECK:STDOUT: }
+// CHECK:STDOUT:
+// CHECK:STDOUT: ; Function Attrs: nocallback nofree nosync nounwind willreturn memory(argmem: readwrite)
+// CHECK:STDOUT: declare void @llvm.lifetime.start.p0(ptr captures(none)) #1
+// CHECK:STDOUT:
+// CHECK:STDOUT: attributes #0 = { nounwind }
+// CHECK:STDOUT: attributes #1 = { nocallback nofree nosync nounwind willreturn memory(argmem: readwrite) }
+// CHECK:STDOUT:
+// CHECK:STDOUT: !llvm.module.flags = !{!0, !1}
+// CHECK:STDOUT: !llvm.dbg.cu = !{!2}
+// CHECK:STDOUT:
+// CHECK:STDOUT: !0 = !{i32 7, !"Dwarf Version", i32 5}
+// CHECK:STDOUT: !1 = !{i32 2, !"Debug Info Version", i32 3}
+// CHECK:STDOUT: !2 = distinct !DICompileUnit(language: DW_LANG_C_plus_plus, file: !3, producer: "carbon", isOptimized: false, runtimeVersion: 0, emissionKind: FullDebug)
+// CHECK:STDOUT: !3 = !DIFile(filename: "var_return_form.carbon", directory: "")
+// CHECK:STDOUT: !4 = distinct !DISubprogram(name: "G", linkageName: "_CG.Main", scope: null, file: !3, line: 5, type: !5, spFlags: DISPFlagDefinition, unit: !2)
+// CHECK:STDOUT: !5 = !DISubroutineType(types: !6)
+// CHECK:STDOUT: !6 = !{null}
+// CHECK:STDOUT: !7 = !DILocation(line: 6, column: 14, scope: !4)
+// CHECK:STDOUT: !8 = !DILocation(line: 6, column: 27, scope: !4)
+// CHECK:STDOUT: !9 = !DILocation(line: 5, column: 1, scope: !4)
+// CHECK:STDOUT: ; ModuleID = 'val_return_form.carbon'
+// CHECK:STDOUT: source_filename = "val_return_form.carbon"
+// CHECK:STDOUT:
+// CHECK:STDOUT: declare i32 @_CF.Main()
+// CHECK:STDOUT:
+// CHECK:STDOUT: ; Function Attrs: nounwind
+// CHECK:STDOUT: define void @_CG.Main() #0 !dbg !4 {
+// CHECK:STDOUT: entry:
+// CHECK:STDOUT:   %F.call = call i32 @_CF.Main(), !dbg !7
+// CHECK:STDOUT:   ret void, !dbg !8
+// CHECK:STDOUT: }
+// CHECK:STDOUT:
+// CHECK:STDOUT: attributes #0 = { nounwind }
+// CHECK:STDOUT:
+// CHECK:STDOUT: !llvm.module.flags = !{!0, !1}
+// CHECK:STDOUT: !llvm.dbg.cu = !{!2}
+// CHECK:STDOUT:
+// CHECK:STDOUT: !0 = !{i32 7, !"Dwarf Version", i32 5}
+// CHECK:STDOUT: !1 = !{i32 2, !"Debug Info Version", i32 3}
+// CHECK:STDOUT: !2 = distinct !DICompileUnit(language: DW_LANG_C_plus_plus, file: !3, producer: "carbon", isOptimized: false, runtimeVersion: 0, emissionKind: FullDebug)
+// CHECK:STDOUT: !3 = !DIFile(filename: "val_return_form.carbon", directory: "")
+// CHECK:STDOUT: !4 = distinct !DISubprogram(name: "G", linkageName: "_CG.Main", scope: null, file: !3, line: 5, type: !5, spFlags: DISPFlagDefinition, unit: !2)
+// CHECK:STDOUT: !5 = !DISubroutineType(types: !6)
+// CHECK:STDOUT: !6 = !{null}
+// CHECK:STDOUT: !7 = !DILocation(line: 6, column: 23, scope: !4)
+// CHECK:STDOUT: !8 = !DILocation(line: 5, column: 1, scope: !4)

+ 1 - 0
toolchain/parse/node_category.h

@@ -32,6 +32,7 @@ namespace Carbon::Parse {
   /* `require <type> impls` or just `require impls` */       \
   X(RequireImpls)                                            \
   X(Requirement)                                             \
+  X(ReturnDecl)                                              \
   X(Statement)
 
 // We expect this to grow, so are using a bigger size than needed.

+ 1 - 0
toolchain/parse/node_ids.h

@@ -116,6 +116,7 @@ using AnyObserveOperandId =
     NodeIdInCategory<NodeCategory::Expr | NodeCategory::ObserveOperator>;
 using AnyNonExprNameId = NodeIdInCategory<NodeCategory::NonExprName>;
 using AnyPackageNameId = NodeIdInCategory<NodeCategory::PackageName>;
+using AnyReturnDeclId = NodeIdInCategory<NodeCategory::ReturnDecl>;
 
 namespace Internal {
 template <typename T>

+ 5 - 3
toolchain/parse/typed_nodes.h

@@ -459,7 +459,8 @@ using FunctionIntroducer =
 
 // A return type: `-> i32`.
 struct ReturnType {
-  static constexpr auto Kind = NodeKind::ReturnType.Define({.child_count = 1});
+  static constexpr auto Kind = NodeKind::ReturnType.Define(
+      {.category = NodeCategory::ReturnDecl, .child_count = 1});
 
   Lex::MinusGreaterTokenIndex token;
   AnyExprId type;
@@ -467,7 +468,8 @@ struct ReturnType {
 
 // A return form: `->? form(var i32)`
 struct ReturnForm {
-  static constexpr auto Kind = NodeKind::ReturnForm.Define({.child_count = 1});
+  static constexpr auto Kind = NodeKind::ReturnForm.Define(
+      {.category = NodeCategory::ReturnDecl, .child_count = 1});
 
   Lex::MinusGreaterQuestionTokenIndex token;
   AnyExprId type;
@@ -483,7 +485,7 @@ struct FunctionSignature {
   FunctionIntroducerId introducer;
   llvm::SmallVector<AnyModifierId> modifiers;
   DeclName name;
-  std::optional<NodeIdOneOf<ReturnTypeId, ReturnFormId>> return_type;
+  std::optional<AnyReturnDeclId> return_type;
   TokenKind token;
 };
 

+ 2 - 0
toolchain/sem_ir/expr_info.cpp

@@ -77,6 +77,8 @@ static auto GetExprCategoryImpl(const File* ir, InstId inst_id)
                 return ExprCategory::ReprInitializing;
               case CARBON_KIND(RefForm _):
                 return ExprCategory::DurableRef;
+              case CARBON_KIND(ValueForm _):
+                return ExprCategory::Value;
               case CARBON_KIND(ErrorInst _):
                 return ExprCategory::Error;
               default:

+ 5 - 0
toolchain/sem_ir/formatter.cpp

@@ -764,6 +764,11 @@ auto Formatter::FormatFunctionSignature(InstBlockId params_id,
         FormatInstAsType(ref_form.type_component_inst_id);
         break;
       }
+      case CARBON_KIND(ValueForm val_form): {
+        out() << "val ";
+        FormatInstAsType(val_form.type_component_inst_id);
+        break;
+      }
       case CARBON_KIND(ErrorInst _): {
         FormatInstAsType(return_form_id);
         break;