Quellcode durchsuchen

Check support for form literals and `:?` bindings (#6747)

Co-authored-by: Jon Ross-Perkins <jperkins@google.com>
Co-authored-by: Carbon Infra Bot <carbon-external-infra@google.com>
Geoff Romer vor 2 Monaten
Ursprung
Commit
bf9219d30e
33 geänderte Dateien mit 1111 neuen und 309 gelöschten Zeilen
  1. 9 0
      toolchain/check/context.h
  2. 41 15
      toolchain/check/convert.cpp
  3. 17 3
      toolchain/check/convert.h
  4. 5 7
      toolchain/check/cpp/import.cpp
  5. 12 0
      toolchain/check/eval_inst.cpp
  6. 1 1
      toolchain/check/function.cpp
  7. 77 24
      toolchain/check/handle_binding_pattern.cpp
  8. 36 11
      toolchain/check/handle_form_literal.cpp
  9. 1 1
      toolchain/check/handle_function.cpp
  10. 2 0
      toolchain/check/merge.cpp
  11. 21 8
      toolchain/check/pattern.cpp
  12. 1 0
      toolchain/check/pattern.h
  13. 177 152
      toolchain/check/pattern_match.cpp
  14. 4 4
      toolchain/check/testdata/basics/raw_sem_ir/cpp_interop.carbon
  15. 1 1
      toolchain/check/testdata/basics/raw_sem_ir/multifile.carbon
  16. 1 1
      toolchain/check/testdata/basics/raw_sem_ir/multifile_with_textual_ir.carbon
  17. 64 64
      toolchain/check/testdata/basics/raw_sem_ir/one_file.carbon
  18. 1 1
      toolchain/check/testdata/basics/raw_sem_ir/one_file_with_textual_ir.carbon
  19. 419 0
      toolchain/check/testdata/function/call/form.carbon
  20. 36 0
      toolchain/check/testdata/impl/fail_todo_form_thunk.carbon
  21. 11 5
      toolchain/check/thunk.cpp
  22. 5 0
      toolchain/check/type.cpp
  23. 4 0
      toolchain/check/type.h
  24. 1 0
      toolchain/diagnostics/kind.def
  25. 5 0
      toolchain/lower/handle.cpp
  26. 22 0
      toolchain/parse/testdata/function/declaration.carbon
  27. 8 1
      toolchain/sem_ir/entity_name.h
  28. 19 0
      toolchain/sem_ir/expr_info.cpp
  29. 24 8
      toolchain/sem_ir/inst_categories.h
  30. 5 0
      toolchain/sem_ir/inst_kind.def
  31. 5 0
      toolchain/sem_ir/stringify.cpp
  32. 1 0
      toolchain/sem_ir/type_iterator.cpp
  33. 75 2
      toolchain/sem_ir/typed_insts.h

+ 9 - 0
toolchain/check/context.h

@@ -256,7 +256,11 @@ class Context {
   }
 
   // Data about a form expression.
+  //
+  // TODO: consider moving this out of Context.
   struct FormExpr {
+    static const FormExpr Error;
+
     // The inst ID of the form expression itself. This is always a form inst,
     // such as InitForm or RefForm.
     // TODO: Consider creating an AnyForm inst category to refer to those insts.
@@ -532,6 +536,11 @@ class Context {
   CoreIdentifierCache core_identifiers_;
 };
 
+inline constexpr Context::FormExpr Context::FormExpr::Error = {
+    .form_inst_id = SemIR::ErrorInst::InstId,
+    .type_component_inst_id = SemIR::ErrorInst::TypeInstId,
+    .type_component_id = SemIR::ErrorInst::TypeId};
+
 }  // namespace Carbon::Check
 
 #endif  // CARBON_TOOLCHAIN_CHECK_CONTEXT_H_

+ 41 - 15
toolchain/check/convert.cpp

@@ -819,6 +819,7 @@ static auto IsValidExprCategoryForConversionTarget(
       return category == SemIR::ExprCategory::DurableRef;
     case ConversionTarget::CppThunkRef:
       return category == SemIR::ExprCategory::EphemeralRef;
+    case ConversionTarget::NoOp:
     case ConversionTarget::ExplicitAs:
     case ConversionTarget::ExplicitUnsafeAs:
       return true;
@@ -1721,6 +1722,11 @@ auto Convert(Context& context, SemIR::LocId loc_id, SemIR::InstId expr_id,
     return SemIR::ErrorInst::InstId;
   }
 
+  if (target.kind == ConversionTarget::NoOp) {
+    CARBON_CHECK(target.type_id == sem_ir.insts().Get(expr_id).type_id());
+    return expr_id;
+  }
+
   // Diagnose unnecessary `ref` tags early, so that they're not obscured by
   // conversions.
   if (starting_category == SemIR::ExprCategory::RefTagged &&
@@ -2006,15 +2012,15 @@ static auto DiagnoseTypeExprEvaluationFailure(Context& context,
 
 auto ExprAsType(Context& context, SemIR::LocId loc_id, SemIR::InstId value_id,
                 bool diagnose) -> TypeExpr {
-  auto type_inst_id =
+  auto type_as_inst_id =
       ConvertToValueOfType(context, loc_id, value_id, SemIR::TypeType::TypeId);
-  if (type_inst_id == SemIR::ErrorInst::TypeInstId) {
+  if (type_as_inst_id == SemIR::ErrorInst::InstId) {
     return {.inst_id = SemIR::ErrorInst::TypeInstId,
             .type_id = SemIR::ErrorInst::TypeId};
   }
 
-  auto type_const_id = context.constant_values().Get(type_inst_id);
-  if (!type_const_id.is_constant()) {
+  auto type_as_const_id = context.constant_values().Get(type_as_inst_id);
+  if (!type_as_const_id.is_constant()) {
     if (diagnose) {
       DiagnoseTypeExprEvaluationFailure(context, loc_id);
     }
@@ -2022,28 +2028,48 @@ auto ExprAsType(Context& context, SemIR::LocId loc_id, SemIR::InstId value_id,
             .type_id = SemIR::ErrorInst::TypeId};
   }
 
-  return {.inst_id = context.types().GetAsTypeInstId(type_inst_id),
-          .type_id = context.types().GetTypeIdForTypeConstantId(type_const_id)};
+  return {
+      .inst_id = context.types().GetAsTypeInstId(type_as_inst_id),
+      .type_id = context.types().GetTypeIdForTypeConstantId(type_as_const_id)};
+}
+
+auto FormExprAsForm(Context& context, SemIR::LocId loc_id,
+                    SemIR::InstId value_id) -> Context::FormExpr {
+  auto form_inst_id =
+      ConvertToValueOfType(context, loc_id, value_id, SemIR::FormType::TypeId);
+  if (form_inst_id == SemIR::ErrorInst::InstId) {
+    return Context::FormExpr::Error;
+  }
+
+  auto form_const_id = context.constant_values().Get(form_inst_id);
+  if (!form_const_id.is_constant()) {
+    CARBON_DIAGNOSTIC(FormExprEvaluationFailure, Error,
+                      "cannot evaluate form expression");
+    context.emitter().Emit(loc_id, FormExprEvaluationFailure);
+    return Context::FormExpr::Error;
+  }
+
+  auto type_id = GetTypeComponent(context, form_inst_id);
+  auto type_inst_id = context.types().GetTypeInstId(type_id);
+  return {.form_inst_id = form_inst_id,
+          .type_component_inst_id = type_inst_id,
+          .type_component_id = type_id};
 }
 
-auto ExprAsReturnForm(Context& context, SemIR::LocId loc_id,
+auto ReturnExprAsForm(Context& context, SemIR::LocId loc_id,
                       SemIR::InstId value_id) -> Context::FormExpr {
-  constexpr Context::FormExpr ErrorFormExpr = {
-      .form_inst_id = SemIR::ErrorInst::InstId,
-      .type_component_inst_id = SemIR::ErrorInst::TypeInstId,
-      .type_component_id = SemIR::ErrorInst::TypeId};
   auto form_inst_id = SemIR::InstId::None;
   auto type_inst_id = SemIR::InstId::None;
   if (auto ref_tag = context.insts().TryGetAs<SemIR::RefTagExpr>(value_id)) {
     type_inst_id = ConvertToValueOfType(context, loc_id, ref_tag->expr_id,
                                         SemIR::TypeType::TypeId);
     if (type_inst_id == SemIR::ErrorInst::InstId) {
-      return ErrorFormExpr;
+      return Context::FormExpr::Error;
     }
     if (!context.constant_values().Get(type_inst_id).is_constant()) {
       DiagnoseTypeExprEvaluationFailure(context,
                                         SemIR::LocId(ref_tag->expr_id));
-      return ErrorFormExpr;
+      return Context::FormExpr::Error;
     }
     form_inst_id = AddInst(
         context,
@@ -2056,11 +2082,11 @@ auto ExprAsReturnForm(Context& context, SemIR::LocId loc_id,
     type_inst_id = ConvertToValueOfType(context, loc_id, value_id,
                                         SemIR::TypeType::TypeId);
     if (type_inst_id == SemIR::ErrorInst::InstId) {
-      return ErrorFormExpr;
+      return Context::FormExpr::Error;
     }
     if (!context.constant_values().Get(type_inst_id).is_constant()) {
       DiagnoseTypeExprEvaluationFailure(context, loc_id);
-      return ErrorFormExpr;
+      return Context::FormExpr::Error;
     }
     form_inst_id = AddInst(
         context,

+ 17 - 3
toolchain/check/convert.h

@@ -15,6 +15,9 @@ namespace Carbon::Check {
 // Description of the target of a conversion.
 struct ConversionTarget {
   enum Kind : int8_t {
+    // Perform no conversion. The source expression must already have type
+    // `type_id`.
+    NoOp,
     // Convert to a value of type `type_id`.
     Value,
     // Convert to either a value or a reference of type `type_id`.
@@ -187,9 +190,20 @@ inline constexpr TypeExpr TypeExpr::None = {.inst_id = SemIR::TypeInstId::None,
 auto ExprAsType(Context& context, SemIR::LocId loc_id, SemIR::InstId value_id,
                 bool diagnose = true) -> TypeExpr;
 
-// Converts an expression for use as a form. If the expression is a type
-// expression, it is interpreted as an initializing form.
-auto ExprAsReturnForm(Context& context, SemIR::LocId loc_id,
+// Converts an expression in a form position for use as a form.
+//
+// Note that the right-hand side of a `->` return type declaration is not
+// a form position for this purpose, because it uses a special syntax to specify
+// forms. `ReturnExprAsForm` should be used instead in that case.
+//
+// `diagnose` has the same effect as in `ExprAsType`.
+auto FormExprAsForm(Context& context, SemIR::LocId loc_id,
+                    SemIR::InstId value_id) -> Context::FormExpr;
+
+// Evaluates an expression in the return-type position (following `->`, not
+// `->?`) for use as a form, following the special-case language rules for
+// evaluating an expression in that position.
+auto ReturnExprAsForm(Context& context, SemIR::LocId loc_id,
                       SemIR::InstId value_id) -> Context::FormExpr;
 
 // Handles an expression whose result value is unused.

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

@@ -1252,9 +1252,7 @@ static auto GetReturnTypeExpr(Context& context, SemIR::LocId loc_id,
     if (!orig_type_inst_id.has_value()) {
       context.TODO(loc_id, llvm::formatv("Unsupported: return type: {0}",
                                          orig_ret_type.getAsString()));
-      return {.form_inst_id = SemIR::ErrorInst::InstId,
-              .type_component_inst_id = SemIR::ErrorInst::TypeInstId,
-              .type_component_id = SemIR::ErrorInst::TypeId};
+      return Context::FormExpr::Error;
     }
     Context::FormExpr result = {
         .form_inst_id = is_reference ? make_ref_form(orig_type_inst_id)
@@ -1708,10 +1706,10 @@ static auto ImportVarDecl(Context& context, SemIR::LocId loc_id,
   SemIR::NameId var_name_id = AddIdentifierName(context, var_decl->getName());
 
   // Create an entity name to identify this variable.
-  SemIR::EntityNameId entity_name_id =
-      context.entity_names().AddSymbolicBindingName(
-          var_name_id, GetParentNameScopeId(context, var_decl),
-          SemIR::CompileTimeBindIndex::None, false, /*is_unused=*/false);
+  SemIR::EntityNameId entity_name_id = context.entity_names().Add(
+      {.name_id = var_name_id,
+       .parent_scope_id = GetParentNameScopeId(context, var_decl),
+       .is_unused = false});
 
   // Create `RefBindingPattern` and `VarPattern`. Mirror the behavior of
   // import_ref and don't create a `NameBindingDecl` here; we'd never use it for

+ 12 - 0
toolchain/check/eval_inst.cpp

@@ -730,6 +730,18 @@ auto EvalConstantInst(Context& /*context*/, SemIR::TupleLiteral inst)
       .type_id = inst.type_id, .elements_id = inst.elements_id});
 }
 
+auto EvalConstantInst(Context& context, SemIR::TypeComponentOf inst)
+    -> ConstantEvalResult {
+  auto form_constant_inst_id =
+      context.constant_values().GetConstantInstId(inst.form_inst_id);
+  if (auto primitive_form = context.insts().TryGetAs<SemIR::AnyPrimitiveForm>(
+          form_constant_inst_id)) {
+    return ConstantEvalResult::Existing(
+        context.constant_values().Get(primitive_form->type_component_id));
+  }
+  return ConstantEvalResult::NewSamePhase(inst);
+}
+
 auto EvalConstantInst(Context& context, SemIR::TypeLiteral inst)
     -> ConstantEvalResult {
   return ConstantEvalResult::Existing(

+ 1 - 1
toolchain/check/function.cpp

@@ -142,7 +142,7 @@ auto MakeBuiltinFunction(Context& context, SemIR::LocId loc_id,
       .type_component_inst_id = SemIR::TypeInstId::None,
       .type_component_id = SemIR::TypeId::None};
   if (signature.return_type_id.has_value()) {
-    return_form = ExprAsReturnForm(
+    return_form = ReturnExprAsForm(
         context, loc_id,
         context.types().GetTypeInstId(signature.return_type_id));
     return_patterns_id = AddReturnPatterns(context, loc_id, return_form);

+ 77 - 24
toolchain/check/handle_binding_pattern.cpp

@@ -43,6 +43,8 @@ static auto GetPatternInstKind(Parse::NodeKind node_kind, bool is_ref)
                     : SemIR::InstKind::ValueBindingPattern;
     case Parse::NodeKind::VarBindingPattern:
       return SemIR::InstKind::RefBindingPattern;
+    case Parse::NodeKind::FormBindingPattern:
+      return SemIR::InstKind::FormBindingPattern;
     default:
       CARBON_FATAL("Unexpected node kind: {0}", node_kind);
   }
@@ -104,17 +106,56 @@ static auto IsValidParamForIntroducer(Context& context, Parse::NodeId node_id,
   }
 }
 
+namespace {
+// Information about the expression in the type position of a binding pattern,
+// i.e. the position following the `:`/`:?`/`:!` separator. Note that this
+// expression may be interpreted as a type or a form, depending on the binding
+// kind.
+struct BindingPatternTypeInfo {
+  // The parse node representing the expression.
+  Parse::AnyExprId node_id;
+  // The inst representing the converted value of that expression. For a `:?`
+  // binding the expression is converted to type `Core.Form`; otherwise it is
+  // converted to type `type`.
+  SemIR::InstId inst_id;
+  // For a `:?` binding this is the type component of the form denoted by
+  // `inst_id`. Otherwise this is the type denoted by `inst_id`.
+  SemIR::TypeId type_component_id;
+};
+}  // namespace
+
+// Handle the type position of a binding pattern.
+static auto HandleAnyBindingPatternType(Context& context,
+                                        Parse::NodeKind node_kind)
+    -> BindingPatternTypeInfo {
+  auto [node_id, original_inst_id] = context.node_stack().PopExprWithNodeId();
+
+  if (node_kind == Parse::FormBindingPattern::Kind) {
+    auto as_form = FormExprAsForm(context, node_id, original_inst_id);
+    return {.node_id = node_id,
+            .inst_id = as_form.form_inst_id,
+            .type_component_id = as_form.type_component_id};
+  } else {
+    auto as_type = ExprAsType(context, node_id, original_inst_id);
+    return {.node_id = node_id,
+            .inst_id = as_type.inst_id,
+            .type_component_id = as_type.type_id};
+  }
+}
+
 // TODO: make this function shorter by factoring pieces out.
 static auto HandleAnyBindingPattern(Context& context, Parse::NodeId node_id,
                                     Parse::NodeKind node_kind,
                                     bool is_unused = false) -> bool {
-  // TODO: split this into smaller, more focused functions.
-  auto [type_node, parsed_type_id] = context.node_stack().PopExprWithNodeId();
-  auto [cast_type_inst_id, cast_type_id] =
-      ExprAsType(context, type_node, parsed_type_id);
+  auto type_expr = HandleAnyBindingPatternType(context, node_kind);
+  if (context.types()
+          .GetAsInst(type_expr.type_component_id)
+          .Is<SemIR::TypeComponentOf>()) {
+    return context.TODO(node_id, "Support symbolic form bindings");
+  }
 
   SemIR::ExprRegionId type_expr_region_id =
-      EndSubpatternAsExpr(context, cast_type_inst_id);
+      EndSubpatternAsExpr(context, type_expr.inst_id);
 
   // The name in a generic binding may be wrapped in `template`.
   bool is_generic = node_kind == Parse::NodeKind::CompileTimeBindingPattern;
@@ -139,9 +180,10 @@ static auto HandleAnyBindingPattern(Context& context, Parse::NodeId node_id,
   auto make_binding_pattern = [&]() -> SemIR::InstId {
     // TODO: Eventually the name will need to support associations with other
     // scopes, but right now we don't support qualified names here.
-    auto binding = AddBindingPattern(context, name_node, name_id, cast_type_id,
-                                     type_expr_region_id, pattern_inst_kind,
-                                     is_template, is_unused);
+    auto binding = AddBindingPattern(
+        context, name_node, name_id, type_expr.type_component_id,
+        context.constant_values().Get(type_expr.inst_id), type_expr_region_id,
+        pattern_inst_kind, is_template, is_unused);
 
     // TODO: If `is_generic`, then `binding.bind_id is a SymbolicBinding. Subst
     // the `.Self` of type `type` in the `cast_type_id` type (a `FacetType`)
@@ -168,7 +210,8 @@ static auto HandleAnyBindingPattern(Context& context, Parse::NodeId node_id,
                       "binding pattern has abstract type {0} in `var` "
                       "pattern",
                       SemIR::TypeId);
-    builder.Context(type_node, AbstractTypeInVarPattern, cast_type_id);
+    builder.Context(type_expr.node_id, AbstractTypeInVarPattern,
+                    type_expr.type_component_id);
   };
 
   // A `self` binding can only appear in an implicit parameter list.
@@ -209,7 +252,8 @@ static auto HandleAnyBindingPattern(Context& context, Parse::NodeId node_id,
       // to fail since `Self` is an incomplete type.
       if (node_kind == Parse::NodeKind::VarBindingPattern) {
         auto [unqualified_type_id, qualifiers] =
-            context.types().GetUnqualifiedTypeAndQualifiers(cast_type_id);
+            context.types().GetUnqualifiedTypeAndQualifiers(
+                type_expr.type_component_id);
         if ((qualifiers & SemIR::TypeQualifiers::Partial) !=
                 SemIR::TypeQualifiers::Partial &&
             context.types().Is<SemIR::ClassType>(unqualified_type_id)) {
@@ -222,7 +266,7 @@ static auto HandleAnyBindingPattern(Context& context, Parse::NodeId node_id,
                                             abstract_diagnostic_context);
             DiagnoseAbstractClass(context, class_type.class_id,
                                   /*direct_use=*/true);
-            cast_type_id = SemIR::ErrorInst::TypeId;
+            type_expr.type_component_id = SemIR::ErrorInst::TypeId;
           }
         }
       }
@@ -233,7 +277,8 @@ static auto HandleAnyBindingPattern(Context& context, Parse::NodeId node_id,
       // unless it's nested inside a `var` pattern (because then the
       // enclosing `var` pattern is), or it's a compile-time binding pattern
       // (because then it's not passed to the `Call` inst).
-      if (node_kind == Parse::NodeKind::LetBindingPattern) {
+      if (node_kind == Parse::NodeKind::LetBindingPattern ||
+          node_kind == Parse::NodeKind::FormBindingPattern) {
         auto type_id = context.insts().GetAttachedType(result_inst_id);
         if (is_ref) {
           result_inst_id = AddPatternInst<SemIR::RefParamPattern>(
@@ -241,6 +286,12 @@ static auto HandleAnyBindingPattern(Context& context, Parse::NodeId node_id,
               {.type_id = type_id,
                .subpattern_id = result_inst_id,
                .index = context.full_pattern_stack().NextCallParamIndex()});
+        } else if (node_kind == Parse::NodeKind::FormBindingPattern) {
+          result_inst_id = AddPatternInst<SemIR::FormParamPattern>(
+              context, node_id,
+              {.type_id = type_id,
+               .subpattern_id = result_inst_id,
+               .index = context.full_pattern_stack().NextCallParamIndex()});
         } else {
           result_inst_id = AddPatternInst<SemIR::ValueParamPattern>(
               context, node_id,
@@ -259,19 +310,20 @@ static auto HandleAnyBindingPattern(Context& context, Parse::NodeId node_id,
                           "binding pattern has incomplete type {0} in name "
                           "binding declaration",
                           InstIdAsType);
-        builder.Context(type_node, IncompleteTypeInBindingDecl,
-                        cast_type_inst_id);
+        builder.Context(type_expr.node_id, IncompleteTypeInBindingDecl,
+                        type_expr.inst_id);
       };
       if (node_kind == Parse::NodeKind::VarBindingPattern) {
-        if (!RequireConcreteType(context, cast_type_id, type_node,
-                                 incomplete_diagnostic_context,
-                                 abstract_diagnostic_context)) {
-          cast_type_id = SemIR::ErrorInst::TypeId;
+        if (!RequireConcreteType(
+                context, type_expr.type_component_id, type_expr.node_id,
+                incomplete_diagnostic_context, abstract_diagnostic_context)) {
+          type_expr.type_component_id = SemIR::ErrorInst::TypeId;
         }
       } else {
-        if (!RequireCompleteType(context, cast_type_id, type_node,
+        if (!RequireCompleteType(context, type_expr.type_component_id,
+                                 type_expr.node_id,
                                  incomplete_diagnostic_context)) {
-          cast_type_id = SemIR::ErrorInst::TypeId;
+          type_expr.type_component_id = SemIR::ErrorInst::TypeId;
         }
       }
 
@@ -286,9 +338,9 @@ static auto HandleAnyBindingPattern(Context& context, Parse::NodeId node_id,
                              .Lookup(binding_pattern_id)
                              .value()
                              .bind_name_id;
-          RegisterReturnedVar(context,
-                              introducer.modifier_node_id(ModifierOrder::Decl),
-                              type_node, cast_type_id, bind_id, name_id);
+          RegisterReturnedVar(
+              context, introducer.modifier_node_id(ModifierOrder::Decl),
+              type_expr.node_id, type_expr.type_component_id, bind_id, name_id);
         }
       }
       context.node_stack().Push(node_id, binding_pattern_id);
@@ -312,7 +364,8 @@ auto HandleParseNode(Context& context, Parse::VarBindingPatternId node_id)
 
 auto HandleParseNode(Context& context, Parse::FormBindingPatternId node_id)
     -> bool {
-  return context.TODO(node_id, "Implement :? support");
+  return HandleAnyBindingPattern(context, node_id,
+                                 Parse::NodeKind::FormBindingPattern);
 }
 
 auto HandleParseNode(Context& context,

+ 36 - 11
toolchain/check/handle_form_literal.cpp

@@ -3,38 +3,63 @@
 // SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
 
 #include "toolchain/check/context.h"
+#include "toolchain/check/convert.h"
 #include "toolchain/check/handle.h"
+#include "toolchain/check/inst.h"
 #include "toolchain/parse/node_category.h"
 
 namespace Carbon::Check {
 
 auto HandleParseNode(Context& context, Parse::RefPrimitiveFormId node_id)
     -> bool {
-  return context.TODO(node_id, "Implement form literals");
+  auto [type_node_id, type_inst_id] = context.node_stack().PopExprWithNodeId();
+  auto type_expr = ExprAsType(context, type_node_id, type_inst_id);
+  auto inst_id =
+      AddInst<SemIR::RefForm>(context, node_id,
+                              {.type_id = SemIR::FormType::TypeId,
+                               .type_component_inst_id = type_expr.inst_id});
+  context.node_stack().Push(node_id, inst_id);
+  return true;
 }
 
 auto HandleParseNode(Context& context, Parse::ValPrimitiveFormId node_id)
     -> bool {
-  return context.TODO(node_id, "Implement form literals");
+  auto [type_node_id, type_inst_id] = context.node_stack().PopExprWithNodeId();
+  auto type_expr = ExprAsType(context, type_node_id, type_inst_id);
+  auto inst_id =
+      AddInst<SemIR::ValueForm>(context, node_id,
+                                {.type_id = SemIR::FormType::TypeId,
+                                 .type_component_inst_id = type_expr.inst_id});
+  context.node_stack().Push(node_id, inst_id);
+  return true;
 }
 
 auto HandleParseNode(Context& context, Parse::VarPrimitiveFormId node_id)
     -> bool {
-  return context.TODO(node_id, "Implement form literals");
+  auto [type_node_id, type_inst_id] = context.node_stack().PopExprWithNodeId();
+  auto type_expr = ExprAsType(context, type_node_id, type_inst_id);
+  auto inst_id =
+      AddInst<SemIR::InitForm>(context, node_id,
+                               {.type_id = SemIR::FormType::TypeId,
+                                .type_component_inst_id = type_expr.inst_id,
+                                .index = SemIR::CallParamIndex::None});
+  context.node_stack().Push(node_id, inst_id);
+  return true;
 }
 
-auto HandleParseNode(Context& context, Parse::FormLiteralKeywordId node_id)
-    -> bool {
-  return context.TODO(node_id, "Implement form literals");
+auto HandleParseNode(Context& /*context*/,
+                     Parse::FormLiteralKeywordId /*node_id*/) -> bool {
+  return true;
 }
 
-auto HandleParseNode(Context& context, Parse::FormLiteralOpenParenId node_id)
-    -> bool {
-  return context.TODO(node_id, "Implement form literals");
+auto HandleParseNode(Context& /*context*/,
+                     Parse::FormLiteralOpenParenId /*node_id*/) -> bool {
+  return true;
 }
 
-auto HandleParseNode(Context& context, Parse::FormLiteralId node_id) -> bool {
-  return context.TODO(node_id, "Implement form literals");
+auto HandleParseNode(Context& /*context*/, Parse::FormLiteralId /*node_id*/)
+    -> bool {
+  return true;
 }
 
 }  // namespace Carbon::Check

+ 1 - 1
toolchain/check/handle_function.cpp

@@ -53,7 +53,7 @@ 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 = ExprAsReturnForm(context, type_node_id, type_inst_id);
+  auto form_expr = ReturnExprAsForm(context, type_node_id, type_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);

+ 2 - 0
toolchain/check/merge.cpp

@@ -249,6 +249,7 @@ static auto CheckRedeclParam(Context& context, bool is_implicit_param,
     }
 
     switch (new_param_pattern.kind()) {
+      case SemIR::FormParamPattern::Kind:
       case SemIR::OutParamPattern::Kind:
       case SemIR::RefParamPattern::Kind:
       case SemIR::ValueParamPattern::Kind:
@@ -267,6 +268,7 @@ static auto CheckRedeclParam(Context& context, bool is_implicit_param,
              .new_id =
                  new_param_pattern.As<SemIR::VarPattern>().subpattern_id});
         break;
+      case SemIR::FormBindingPattern::Kind:
       case SemIR::RefBindingPattern::Kind:
       case SemIR::SymbolicBindingPattern::Kind:
       case SemIR::ValueBindingPattern::Kind: {

+ 21 - 8
toolchain/check/pattern.cpp

@@ -48,11 +48,15 @@ auto EndSubpatternAsNonExpr(Context& context) -> void {
 
 auto AddBindingPattern(Context& context, SemIR::LocId name_loc,
                        SemIR::NameId name_id, SemIR::TypeId type_id,
+                       SemIR::ConstantId form_id,
                        SemIR::ExprRegionId type_region_id,
                        SemIR::InstKind pattern_kind, bool is_template,
                        bool is_unused) -> BindingPatternInfo {
   SemIR::InstKind bind_name_kind;
   switch (pattern_kind) {
+    case SemIR::InstKind::FormBindingPattern:
+      bind_name_kind = SemIR::InstKind::FormBinding;
+      break;
     case SemIR::InstKind::RefBindingPattern:
       bind_name_kind = SemIR::InstKind::RefBinding;
       break;
@@ -63,15 +67,23 @@ auto AddBindingPattern(Context& context, SemIR::LocId name_loc,
       bind_name_kind = SemIR::InstKind::ValueBinding;
       break;
     default:
-      CARBON_FATAL("pattern_kind is not a binding pattern kind");
+      CARBON_FATAL("pattern_kind {0} is not a binding pattern kind",
+                   pattern_kind);
   }
   bool is_generic = pattern_kind == SemIR::SymbolicBindingPattern::Kind;
 
-  auto entity_name_id = context.entity_names().AddSymbolicBindingName(
-      name_id, context.scope_stack().PeekNameScopeId(),
-      is_generic ? context.scope_stack().AddCompileTimeBinding()
-                 : SemIR::CompileTimeBindIndex::None,
-      is_template, is_unused || name_id == SemIR::NameId::Underscore);
+  SemIR::EntityName entity_name = {
+      .name_id = name_id,
+      .parent_scope_id = context.scope_stack().PeekNameScopeId(),
+      .is_unused = is_unused || name_id == SemIR::NameId::Underscore};
+  if (is_generic) {
+    entity_name.bind_index_value =
+        context.scope_stack().AddCompileTimeBinding().index;
+    entity_name.is_template = is_template;
+  } else if (pattern_kind == SemIR::InstKind::FormBindingPattern) {
+    entity_name.form_id = form_id;
+  }
+  auto entity_name_id = context.entity_names().Add(entity_name);
 
   auto bind_id = AddInstInNoBlock(
       context,
@@ -145,8 +157,9 @@ auto AddParamPattern(Context& context, SemIR::LocId loc_id,
   const auto& binding_pattern_kind = is_ref ? SemIR::RefBindingPattern::Kind
                                             : SemIR::ValueBindingPattern::Kind;
   SemIR::InstId pattern_id =
-      AddBindingPattern(context, loc_id, name_id, type_id, type_expr_region_id,
-                        binding_pattern_kind,
+      AddBindingPattern(context, loc_id, name_id, type_id,
+                        /*form_id=*/SemIR::ConstantId::None,
+                        type_expr_region_id, binding_pattern_kind,
                         /*is_template=*/false, /*is_unused=*/false)
           .pattern_id;
 

+ 1 - 0
toolchain/check/pattern.h

@@ -44,6 +44,7 @@ struct BindingPatternInfo {
 // bindings.
 auto AddBindingPattern(Context& context, SemIR::LocId name_loc,
                        SemIR::NameId name_id, SemIR::TypeId type_id,
+                       SemIR::ConstantId form_id,
                        SemIR::ExprRegionId type_region_id,
                        SemIR::InstKind pattern_kind, bool is_template,
                        bool is_unused) -> BindingPatternInfo;

+ 177 - 152
toolchain/check/pattern_match.cpp

@@ -97,21 +97,11 @@ class MatchContext {
   auto EmitPatternMatch(Context& context, MatchContext::WorkItem entry) -> void;
 
   // Implementations of `EmitPatternMatch` for particular pattern inst kinds.
-  // The pattern argument is always equal to
-  // `context.insts().Get(entry.pattern_id)`.
   auto DoEmitPatternMatch(Context& context,
                           SemIR::AnyBindingPattern binding_pattern,
                           WorkItem entry) -> void;
   auto DoEmitPatternMatch(Context& context,
-                          SemIR::ValueParamPattern param_pattern,
-                          WorkItem entry) -> void;
-  template <typename RefParamPatternT>
-    requires std::is_same_v<RefParamPatternT, SemIR::RefParamPattern> ||
-             std::is_same_v<RefParamPatternT, SemIR::VarParamPattern>
-  auto DoEmitPatternMatch(Context& context, RefParamPatternT param_pattern,
-                          WorkItem entry) -> void;
-  auto DoEmitPatternMatch(Context& context,
-                          SemIR::OutParamPattern param_pattern, WorkItem entry)
+                          SemIR::AnyParamPattern param_pattern, WorkItem entry)
       -> void;
   auto DoEmitPatternMatch(Context& context,
                           SemIR::ReturnSlotPattern return_slot_pattern,
@@ -121,6 +111,15 @@ class MatchContext {
   auto DoEmitPatternMatch(Context& context, SemIR::TuplePattern tuple_pattern,
                           WorkItem entry) -> void;
 
+  // Performs the core logic of matching a variable pattern whose type is
+  // `pattern_type_id`, but returns the scrutinee that its subpattern should be
+  // matched with, rather than pushing it onto the worklist. This is factored
+  // out so it can be reused when handling a `FormBindingPattern` or
+  // `FormParamPattern` with an initializing form.
+  auto DoEmitVarPatternMatchImpl(Context& context,
+                                 SemIR::TypeId pattern_type_id,
+                                 WorkItem entry) const -> SemIR::InstId;
+
   // The stack of work to be processed.
   llvm::SmallVector<WorkItem> stack_;
 
@@ -241,6 +240,75 @@ static auto InsertHere(Context& context, SemIR::ExprRegionId region_id)
   return region.result_id;
 }
 
+// Returns the kind of conversion to perform on the scrutinee when matching the
+// given pattern. `form_kind` is the form of the pattern, if known; it only
+// affects the behavior of `FormBindingPattern` and `FormParamPattern`,
+// and it must be set in the `FormParamPattern` case.
+static auto ConversionKindFor(
+    Context& context, SemIR::Inst pattern, MatchContext::WorkItem entry,
+    std::optional<SemIR::InstKind> form_kind = std::nullopt)
+    -> ConversionTarget::Kind {
+  CARBON_KIND_SWITCH(pattern) {
+    case SemIR::OutParamPattern::Kind:
+    case SemIR::VarParamPattern::Kind:
+      return ConversionTarget::NoOp;
+    case SemIR::RefBindingPattern::Kind:
+      return ConversionTarget::DurableRef;
+    case SemIR::RefParamPattern::Kind:
+      return entry.allow_unmarked_ref ? ConversionTarget::UnmarkedRefParam
+                                      : ConversionTarget::RefParam;
+    case SemIR::SymbolicBindingPattern::Kind:
+    case SemIR::ValueBindingPattern::Kind:
+    case SemIR::ValueParamPattern::Kind:
+      return ConversionTarget::Value;
+    case CARBON_KIND(SemIR::FormBindingPattern form_binding_pattern): {
+      if (!form_kind) {
+        auto form_id = context.entity_names()
+                           .Get(form_binding_pattern.entity_name_id)
+                           .form_id;
+        auto form_inst_id = context.constant_values().GetInstId(form_id);
+        form_kind = context.insts().Get(form_inst_id).kind();
+      }
+
+      switch (*form_kind) {
+        case SemIR::InitForm::Kind:
+          context.TODO(entry.pattern_id, "Support local initializing forms");
+          [[fallthrough]];
+        case SemIR::RefForm::Kind:
+          return ConversionTarget::DurableRef;
+        case SemIR::SymbolicBinding::Kind:
+          context.TODO(entry.pattern_id, "Support symbolic form bindings");
+          [[fallthrough]];
+        case SemIR::ValueForm::Kind:
+          return ConversionTarget::Value;
+        default:
+          CARBON_FATAL("Unexpected form kind {0}", form_kind);
+      }
+    }
+    case SemIR::FormParamPattern::Kind: {
+      CARBON_CHECK(form_kind);
+      switch (*form_kind) {
+        case SemIR::InitForm::Kind:
+          return ConversionTarget::NoOp;
+        case SemIR::RefForm::Kind:
+          // TODO: Figure out rules for when the argument must have a `ref` tag.
+          return entry.allow_unmarked_ref ? ConversionTarget::UnmarkedRefParam
+                                          : ConversionTarget::RefParam;
+        case SemIR::SymbolicBinding::Kind:
+          context.TODO(entry.pattern_id, "Support symbolic form params");
+          [[fallthrough]];
+        case SemIR::ErrorInst::Kind:
+        case SemIR::ValueForm::Kind:
+          return ConversionTarget::Value;
+        default:
+          CARBON_FATAL("Unexpected form kind {0}", form_kind);
+      }
+    }
+    default:
+      CARBON_FATAL("Unexpected pattern kind in {0}", pattern);
+  }
+}
+
 auto MatchContext::DoEmitPatternMatch(Context& context,
                                       SemIR::AnyBindingPattern binding_pattern,
                                       MatchContext::WorkItem entry) -> void {
@@ -262,17 +330,7 @@ auto MatchContext::DoEmitPatternMatch(Context& context,
   InsertHere(context, type_expr_region_id);
   auto value_id = SemIR::InstId::None;
   if (kind_ == MatchKind::Local) {
-    auto conversion_kind = [&binding_pattern]() -> ConversionTarget::Kind {
-      switch (binding_pattern.kind) {
-        case SemIR::SymbolicBindingPattern::Kind:
-        case SemIR::ValueBindingPattern::Kind:
-          return ConversionTarget::Value;
-        case SemIR::RefBindingPattern::Kind:
-          return ConversionTarget::DurableRef;
-        default:
-          CARBON_FATAL("Unexpected inst kind {0}", binding_pattern.kind);
-      }
-    }();
+    auto conversion_kind = ConversionKindFor(context, binding_pattern, entry);
 
     if (!bind_name_id.has_value()) {
       // TODO: Is this appropriate, or should we perform a conversion based on
@@ -298,104 +356,67 @@ auto MatchContext::DoEmitPatternMatch(Context& context,
   }
 }
 
-auto MatchContext::DoEmitPatternMatch(Context& context,
-                                      SemIR::ValueParamPattern param_pattern,
-                                      WorkItem entry) -> void {
-  switch (kind_) {
-    case MatchKind::Caller: {
-      CARBON_CHECK(
-          static_cast<size_t>(param_pattern.index.index) == call_args_.size(),
-          "Parameters out of order; expecting {0} but got {1}",
-          call_args_.size(), param_pattern.index.index);
-      CARBON_CHECK(entry.scrutinee_id.has_value());
-      if (entry.scrutinee_id == SemIR::ErrorInst::InstId) {
-        call_args_.push_back(SemIR::ErrorInst::InstId);
-      } else {
-        call_args_.push_back(ConvertToValueOfType(
-            context, SemIR::LocId(entry.scrutinee_id), entry.scrutinee_id,
-            ExtractScrutineeType(
-                context.sem_ir(),
-                SemIR::GetTypeOfInstInSpecific(
-                    context.sem_ir(), callee_specific_id_, entry.pattern_id))));
+// Returns the inst kind to use for the parameter corresponding to the given
+// parameter pattern. If the pattern is a `FormParamPattern`, `form_kind`
+// must be the pattern's form; otherwise it is ignored.
+static auto ParamKindFor(
+    Context& context, SemIR::Inst param_pattern, MatchContext::WorkItem entry,
+    std::optional<SemIR::InstKind> form_kind = std::nullopt)
+    -> SemIR::InstKind {
+  switch (param_pattern.kind()) {
+    case SemIR::OutParamPattern::Kind:
+      return SemIR::OutParam::Kind;
+    case SemIR::RefParamPattern::Kind:
+    case SemIR::VarParamPattern::Kind:
+      return SemIR::RefParam::Kind;
+    case SemIR::ValueParamPattern::Kind:
+      return SemIR::ValueParam::Kind;
+    case SemIR::FormParamPattern::Kind:
+      CARBON_CHECK(form_kind);
+      switch (*form_kind) {
+        case SemIR::InitForm::Kind:
+        case SemIR::RefForm::Kind:
+          return SemIR::RefParam::Kind;
+        case SemIR::SymbolicBinding::Kind:
+          context.TODO(entry.pattern_id, "Support symbolic form params");
+          [[fallthrough]];
+        case SemIR::ErrorInst::Kind:
+        case SemIR::ValueForm::Kind:
+          return SemIR::ValueParam::Kind;
+        default:
+          CARBON_FATAL("Unexpected form kind {0}", form_kind);
       }
-      // Do not traverse farther, because the caller side of the pattern
-      // ends here.
-      break;
-    }
-    case MatchKind::Callee: {
-      auto param_id = AddInst<SemIR::ValueParam>(
-          context, SemIR::LocId(entry.pattern_id),
-          {.type_id =
-               ExtractScrutineeType(context.sem_ir(), param_pattern.type_id),
-           .index = param_pattern.index,
-           .pretty_name_id = SemIR::GetPrettyNameFromPatternId(
-               context.sem_ir(), entry.pattern_id)});
-      AddWork({.pattern_id = param_pattern.subpattern_id,
-               .scrutinee_id = param_id});
-      call_params_.push_back(param_id);
-      call_param_patterns_.push_back(entry.pattern_id);
-      break;
-    }
-    case MatchKind::Local: {
-      CARBON_FATAL("Found ValueParamPattern during local pattern match");
-    }
+    default:
+      CARBON_FATAL("Unexpected param pattern kind: {0}", param_pattern);
   }
 }
 
-template <typename RefParamPatternT>
-  requires std::is_same_v<RefParamPatternT, SemIR::RefParamPattern> ||
-           std::is_same_v<RefParamPatternT, SemIR::VarParamPattern>
 auto MatchContext::DoEmitPatternMatch(Context& context,
-                                      RefParamPatternT param_pattern,
+                                      SemIR::AnyParamPattern param_pattern,
                                       WorkItem entry) -> void {
-  switch (kind_) {
-    case MatchKind::Caller: {
-      CARBON_CHECK(
-          static_cast<size_t>(param_pattern.index.index) == call_args_.size(),
-          "Parameters out of order; expecting {0} but got {1}",
-          call_args_.size(), param_pattern.index.index);
-      CARBON_CHECK(entry.scrutinee_id.has_value());
-
-      if (std::is_same_v<RefParamPatternT, SemIR::VarParamPattern>) {
-        call_args_.push_back(entry.scrutinee_id);
-        break;
+  // If this is a FormParamPattern, determine its form.
+  std::optional<SemIR::InstKind> form_kind;
+  if (param_pattern.kind == SemIR::FormParamPattern::Kind) {
+    if (param_pattern.subpattern_id == SemIR::ErrorInst::InstId) {
+      form_kind = SemIR::ErrorInst::Kind;
+    } else {
+      auto binding_pattern = context.insts().GetAs<SemIR::FormBindingPattern>(
+          param_pattern.subpattern_id);
+      auto form_id =
+          context.entity_names().Get(binding_pattern.entity_name_id).form_id;
+      auto form_inst_id = context.constant_values().GetInstId(form_id);
+      form_kind = context.insts().Get(form_inst_id).kind();
+
+      // If the form is initializing, match this as a `VarPattern` before
+      // matching it as a parameter pattern.
+      if (form_kind == SemIR::InitForm::Kind) {
+        auto new_scrutinee_id =
+            DoEmitVarPatternMatchImpl(context, param_pattern.type_id, entry);
+        entry.scrutinee_id = new_scrutinee_id;
       }
-      auto scrutinee_type_id = ExtractScrutineeType(
-          context.sem_ir(),
-          SemIR::GetTypeOfInstInSpecific(context.sem_ir(), callee_specific_id_,
-                                         entry.pattern_id));
-      call_args_.push_back(Convert(
-          context, SemIR::LocId(entry.scrutinee_id), entry.scrutinee_id,
-          {.kind = entry.allow_unmarked_ref ? ConversionTarget::UnmarkedRefParam
-                                            : ConversionTarget::RefParam,
-           .type_id = scrutinee_type_id}));
-      // Do not traverse farther, because the caller side of the pattern
-      // ends here.
-      break;
-    }
-    case MatchKind::Callee: {
-      auto param_id = AddInst<SemIR::RefParam>(
-          context, SemIR::LocId(entry.pattern_id),
-          {.type_id =
-               ExtractScrutineeType(context.sem_ir(), param_pattern.type_id),
-           .index = param_pattern.index,
-           .pretty_name_id = SemIR::GetPrettyNameFromPatternId(
-               context.sem_ir(), entry.pattern_id)});
-      AddWork({.pattern_id = param_pattern.subpattern_id,
-               .scrutinee_id = param_id});
-      call_params_.push_back(param_id);
-      call_param_patterns_.push_back(entry.pattern_id);
-      break;
-    }
-    case MatchKind::Local: {
-      CARBON_FATAL("Found RefParamPattern during local pattern match");
     }
   }
-}
 
-auto MatchContext::DoEmitPatternMatch(Context& context,
-                                      SemIR::OutParamPattern param_pattern,
-                                      WorkItem entry) -> void {
   switch (kind_) {
     case MatchKind::Caller: {
       CARBON_CHECK(
@@ -403,35 +424,42 @@ auto MatchContext::DoEmitPatternMatch(Context& context,
           "Parameters out of order; expecting {0} but got {1}",
           call_args_.size(), param_pattern.index.index);
       CARBON_CHECK(entry.scrutinee_id.has_value());
-      CARBON_CHECK(
-          context.insts().Get(entry.scrutinee_id).type_id() ==
-          ExtractScrutineeType(
-              context.sem_ir(),
-              SemIR::GetTypeOfInstInSpecific(
-                  context.sem_ir(), callee_specific_id_, entry.pattern_id)));
-      call_args_.push_back(entry.scrutinee_id);
+      if (entry.scrutinee_id == SemIR::ErrorInst::InstId) {
+        call_args_.push_back(SemIR::ErrorInst::InstId);
+      } else {
+        auto scrutinee_type_id = ExtractScrutineeType(
+            context.sem_ir(),
+            SemIR::GetTypeOfInstInSpecific(
+                context.sem_ir(), callee_specific_id_, entry.pattern_id));
+        call_args_.push_back(Convert(
+            context, SemIR::LocId(entry.scrutinee_id), entry.scrutinee_id,
+            {.kind =
+                 ConversionKindFor(context, param_pattern, entry, form_kind),
+             .type_id = scrutinee_type_id}));
+      }
       // Do not traverse farther, because the caller side of the pattern
       // ends here.
       break;
     }
     case MatchKind::Callee: {
-      // TODO: Consider ways to address near-duplication with the
-      // other ParamPattern cases.
-      auto param_id = AddInst<SemIR::OutParam>(
-          context, SemIR::LocId(entry.pattern_id),
-          {.type_id =
-               ExtractScrutineeType(context.sem_ir(), param_pattern.type_id),
-           .index = param_pattern.index,
-           .pretty_name_id = SemIR::GetPrettyNameFromPatternId(
-               context.sem_ir(), entry.pattern_id)});
+      SemIR::AnyParam param = {
+          .kind = ParamKindFor(context, param_pattern, entry, form_kind),
+          .type_id =
+              ExtractScrutineeType(context.sem_ir(), param_pattern.type_id),
+          .index = param_pattern.index,
+          .pretty_name_id = SemIR::GetPrettyNameFromPatternId(
+              context.sem_ir(), entry.pattern_id)};
+      auto param_id =
+          AddInst(context, SemIR::LocIdAndInst::UncheckedLoc(
+                               SemIR::LocId(entry.pattern_id), param));
       AddWork({.pattern_id = param_pattern.subpattern_id,
                .scrutinee_id = param_id});
-      call_param_patterns_.push_back(entry.pattern_id);
       call_params_.push_back(param_id);
+      call_param_patterns_.push_back(entry.pattern_id);
       break;
     }
     case MatchKind::Local: {
-      CARBON_FATAL("Found OutParamPattern during local pattern match");
+      CARBON_FATAL("Found ValueParamPattern during local pattern match");
     }
   }
 }
@@ -457,15 +485,23 @@ auto MatchContext::DoEmitPatternMatch(
 auto MatchContext::DoEmitPatternMatch(Context& context,
                                       SemIR::VarPattern var_pattern,
                                       WorkItem entry) -> void {
+  auto new_scrutinee_id =
+      DoEmitVarPatternMatchImpl(context, var_pattern.type_id, entry);
+  AddWork({.pattern_id = var_pattern.subpattern_id,
+           .scrutinee_id = new_scrutinee_id});
+}
+
+auto MatchContext::DoEmitVarPatternMatchImpl(Context& context,
+                                             SemIR::TypeId pattern_type_id,
+                                             WorkItem entry) const
+    -> SemIR::InstId {
   auto storage_id = SemIR::InstId::None;
   switch (kind_) {
     case MatchKind::Callee: {
       // We're emitting pattern-match IR for the callee, but we're still on
       // the caller side of the pattern, so we traverse without emitting any
       // insts.
-      AddWork({.pattern_id = var_pattern.subpattern_id,
-               .scrutinee_id = SemIR::InstId::None});
-      return;
+      return SemIR::InstId::None;
     }
     case MatchKind::Local: {
       // In a `var`/`let` declaration, the `VarStorage` inst is created before
@@ -478,8 +514,7 @@ auto MatchContext::DoEmitPatternMatch(Context& context,
     case MatchKind::Caller: {
       storage_id = AddInst<SemIR::TemporaryStorage>(
           context, SemIR::LocId(entry.pattern_id),
-          {.type_id =
-               ExtractScrutineeType(context.sem_ir(), var_pattern.type_id)});
+          {.type_id = ExtractScrutineeType(context.sem_ir(), pattern_type_id)});
       CARBON_CHECK(entry.scrutinee_id.has_value());
       break;
     }
@@ -511,11 +546,10 @@ auto MatchContext::DoEmitPatternMatch(Context& context,
                              {.lhs_id = storage_id, .rhs_id = init_id});
     }
   }
-  AddWork(
-      {.pattern_id = var_pattern.subpattern_id, .scrutinee_id = storage_id});
   if (context.scope_stack().PeekIndex() == ScopeIndex::Package) {
     context.global_init().Suspend();
   }
+  return storage_id;
 }
 
 auto MatchContext::DoEmitPatternMatch(Context& context,
@@ -606,27 +640,18 @@ auto MatchContext::EmitPatternMatch(Context& context,
   CARBON_KIND_SWITCH(pattern) {
     case SemIR::RefBindingPattern::Kind:
     case SemIR::SymbolicBindingPattern::Kind:
-    case SemIR::ValueBindingPattern::Kind: {
+    case SemIR::ValueBindingPattern::Kind:
+    case SemIR::FormBindingPattern::Kind:
       DoEmitPatternMatch(context, pattern.As<SemIR::AnyBindingPattern>(),
                          entry);
       break;
-    }
-    case CARBON_KIND(SemIR::ValueParamPattern param_pattern): {
-      DoEmitPatternMatch(context, param_pattern, entry);
-      break;
-    }
-    case CARBON_KIND(SemIR::RefParamPattern param_pattern): {
-      DoEmitPatternMatch(context, param_pattern, entry);
-      break;
-    }
-    case CARBON_KIND(SemIR::VarParamPattern param_pattern): {
-      DoEmitPatternMatch(context, param_pattern, entry);
+    case SemIR::FormParamPattern::Kind:
+    case SemIR::RefParamPattern::Kind:
+    case SemIR::ValueParamPattern::Kind:
+    case SemIR::VarParamPattern::Kind:
+    case SemIR::OutParamPattern::Kind:
+      DoEmitPatternMatch(context, pattern.As<SemIR::AnyParamPattern>(), entry);
       break;
-    }
-    case CARBON_KIND(SemIR::OutParamPattern param_pattern): {
-      DoEmitPatternMatch(context, param_pattern, entry);
-      break;
-    }
     case CARBON_KIND(SemIR::ReturnSlotPattern return_slot_pattern): {
       DoEmitPatternMatch(context, return_slot_pattern, entry);
       break;

+ 4 - 4
toolchain/check/testdata/basics/raw_sem_ir/cpp_interop.carbon

@@ -66,10 +66,10 @@ fn G(x: Cpp.X) {
 // CHECK:STDOUT:     name_scope60000001: {inst: inst60000011, parent_scope: name_scope0, has_error: false, extended_scopes: [], names: {name2: inst60000014, name3: inst6000002A, name4: inst6000004E}}
 // CHECK:STDOUT:     name_scope60000002: {inst: inst60000014, parent_scope: name_scope60000001, has_error: false, extended_scopes: [], names: {}}
 // CHECK:STDOUT:   entity_names:
-// CHECK:STDOUT:     entity_name60000000: {name: name1, parent_scope: name_scope<none>, index: -1, is_template: 0, is_unused: 0}
-// CHECK:STDOUT:     entity_name60000001: {name: name1, parent_scope: name_scope<none>, index: -1, is_template: 0, is_unused: 0}
-// CHECK:STDOUT:     entity_name60000002: {name: name1, parent_scope: name_scope<none>, index: -1, is_template: 0, is_unused: 0}
-// CHECK:STDOUT:     entity_name60000003: {name: name4, parent_scope: name_scope60000001, index: -1, is_template: 0, is_unused: 0}
+// CHECK:STDOUT:     entity_name60000000: {name: name1, parent_scope: name_scope<none>, index: -1, is_template: 0, is_unused: 0, form: constant<none>}
+// CHECK:STDOUT:     entity_name60000001: {name: name1, parent_scope: name_scope<none>, index: -1, is_template: 0, is_unused: 0, form: constant<none>}
+// CHECK:STDOUT:     entity_name60000002: {name: name1, parent_scope: name_scope<none>, index: -1, is_template: 0, is_unused: 0, form: constant<none>}
+// CHECK:STDOUT:     entity_name60000003: {name: name4, parent_scope: name_scope60000001, index: -1, is_template: 0, is_unused: 0, form: constant<none>}
 // CHECK:STDOUT:   cpp_global_vars:
 // CHECK:STDOUT:     cpp_global_var60000000: {key: {entity_name_id: entity_name60000003}, clang_decl_id: clang_decl_id60000007}
 // CHECK:STDOUT:   functions:

+ 1 - 1
toolchain/check/testdata/basics/raw_sem_ir/multifile.carbon

@@ -121,7 +121,7 @@ fn B() {
 // CHECK:STDOUT:     name_scope0:     {inst: instF, parent_scope: name_scope<none>, has_error: false, extended_scopes: [], names: {name1: inst50000011, name0: inst50000012}}
 // CHECK:STDOUT:     name_scope50000001: {inst: inst50000011, parent_scope: name_scope0, has_error: false, extended_scopes: [], names: {name1: inst50000017}}
 // CHECK:STDOUT:   entity_names:
-// CHECK:STDOUT:     entity_name50000000: {name: name1, parent_scope: name_scope50000001, index: -1, is_template: 0, is_unused: 0}
+// CHECK:STDOUT:     entity_name50000000: {name: name1, parent_scope: name_scope50000001, index: -1, is_template: 0, is_unused: 0, form: constant<none>}
 // CHECK:STDOUT:   cpp_global_vars: {}
 // CHECK:STDOUT:   functions:
 // CHECK:STDOUT:     function50000000: {name: name0, parent_scope: name_scope0, call_param_patterns_id: inst_block_empty, call_params_id: inst_block_empty, body: [inst_block50000006]}

+ 1 - 1
toolchain/check/testdata/basics/raw_sem_ir/multifile_with_textual_ir.carbon

@@ -140,7 +140,7 @@ fn B() {
 // CHECK:STDOUT:     name_scope0:     {inst: instF, parent_scope: name_scope<none>, has_error: false, extended_scopes: [], names: {name1: inst50000011, name0: inst50000012}}
 // CHECK:STDOUT:     name_scope50000001: {inst: inst50000011, parent_scope: name_scope0, has_error: false, extended_scopes: [], names: {name1: inst50000017}}
 // CHECK:STDOUT:   entity_names:
-// CHECK:STDOUT:     entity_name50000000: {name: name1, parent_scope: name_scope50000001, index: -1, is_template: 0, is_unused: 0}
+// CHECK:STDOUT:     entity_name50000000: {name: name1, parent_scope: name_scope50000001, index: -1, is_template: 0, is_unused: 0, form: constant<none>}
 // CHECK:STDOUT:   cpp_global_vars: {}
 // CHECK:STDOUT:   functions:
 // CHECK:STDOUT:     function50000000: {name: name0, parent_scope: name_scope0, call_param_patterns_id: inst_block_empty, call_params_id: inst_block_empty, body: [inst_block50000006]}

+ 64 - 64
toolchain/check/testdata/basics/raw_sem_ir/one_file.carbon

@@ -270,70 +270,70 @@ fn Foo[T:! type](p: T*) -> (T*, ()) {
 // CHECK:STDOUT:     name_scope6000000C: {inst: inst600000DB, parent_scope: name_scope60000001, has_error: false, extended_scopes: [], names: {}}
 // CHECK:STDOUT:     name_scope6000000D: {inst: inst6000011F, parent_scope: name_scope60000001, has_error: false, extended_scopes: [], names: {}}
 // CHECK:STDOUT:   entity_names:
-// CHECK:STDOUT:     entity_name60000000: {name: name(PeriodSelf), parent_scope: name_scope<none>, index: -1, is_template: 0, is_unused: 0}
-// CHECK:STDOUT:     entity_name60000001: {name: name1, parent_scope: name_scope<none>, index: 0, is_template: 0, is_unused: 0}
-// CHECK:STDOUT:     entity_name60000002: {name: name2, parent_scope: name_scope<none>, index: -1, is_template: 0, is_unused: 0}
-// CHECK:STDOUT:     entity_name60000003: {name: name3, parent_scope: name_scope60000001, index: -1, is_template: 0, is_unused: 0}
-// CHECK:STDOUT:     entity_name60000004: {name: name(SelfType), parent_scope: name_scope<none>, index: 0, is_template: 0, is_unused: 0}
-// CHECK:STDOUT:     entity_name60000005: {name: name4, parent_scope: name_scope60000003, index: -1, is_template: 0, is_unused: 0}
-// CHECK:STDOUT:     entity_name60000006: {name: name(SelfType), parent_scope: name_scope<none>, index: 0, is_template: 0, is_unused: 0}
-// CHECK:STDOUT:     entity_name60000007: {name: name(SelfType), parent_scope: name_scope<none>, index: 0, is_template: 0, is_unused: 0}
-// CHECK:STDOUT:     entity_name60000008: {name: name(SelfValue), parent_scope: name_scope<none>, index: -1, is_template: 0, is_unused: 0}
-// CHECK:STDOUT:     entity_name60000009: {name: name(SelfType), parent_scope: name_scope<none>, index: 0, is_template: 0, is_unused: 0}
-// CHECK:STDOUT:     entity_name6000000A: {name: name(SelfType), parent_scope: name_scope<none>, index: 0, is_template: 0, is_unused: 0}
-// CHECK:STDOUT:     entity_name6000000B: {name: name(SelfType), parent_scope: name_scope<none>, index: 0, is_template: 0, is_unused: 0}
-// CHECK:STDOUT:     entity_name6000000C: {name: name(SelfType), parent_scope: name_scope<none>, index: 0, is_template: 0, is_unused: 0}
-// CHECK:STDOUT:     entity_name6000000D: {name: name1, parent_scope: name_scope<none>, index: 0, is_template: 0, is_unused: 0}
-// CHECK:STDOUT:     entity_name6000000E: {name: name1, parent_scope: name_scope<none>, index: 0, is_template: 0, is_unused: 0}
-// CHECK:STDOUT:     entity_name6000000F: {name: name(SelfValue), parent_scope: name_scope<none>, index: -1, is_template: 0, is_unused: 0}
-// CHECK:STDOUT:     entity_name60000010: {name: name1, parent_scope: name_scope<none>, index: 0, is_template: 0, is_unused: 0}
-// CHECK:STDOUT:     entity_name60000011: {name: name1, parent_scope: name_scope<none>, index: 0, is_template: 0, is_unused: 0}
-// CHECK:STDOUT:     entity_name60000012: {name: name1, parent_scope: name_scope<none>, index: 0, is_template: 0, is_unused: 0}
-// CHECK:STDOUT:     entity_name60000013: {name: name1, parent_scope: name_scope<none>, index: 0, is_template: 0, is_unused: 0}
-// CHECK:STDOUT:     entity_name60000014: {name: name1, parent_scope: name_scope<none>, index: 0, is_template: 0, is_unused: 0}
-// CHECK:STDOUT:     entity_name60000015: {name: name1, parent_scope: name_scope<none>, index: 0, is_template: 0, is_unused: 0}
-// CHECK:STDOUT:     entity_name60000016: {name: name1, parent_scope: name_scope<none>, index: 0, is_template: 0, is_unused: 0}
-// CHECK:STDOUT:     entity_name60000017: {name: name1, parent_scope: name_scope<none>, index: 0, is_template: 0, is_unused: 0}
-// CHECK:STDOUT:     entity_name60000018: {name: name1, parent_scope: name_scope<none>, index: 0, is_template: 0, is_unused: 0}
-// CHECK:STDOUT:     entity_name60000019: {name: name1, parent_scope: name_scope<none>, index: 0, is_template: 0, is_unused: 0}
-// CHECK:STDOUT:     entity_name6000001A: {name: name(SelfValue), parent_scope: name_scope<none>, index: -1, is_template: 0, is_unused: 0}
-// CHECK:STDOUT:     entity_name6000001B: {name: name1, parent_scope: name_scope<none>, index: 0, is_template: 0, is_unused: 0}
-// CHECK:STDOUT:     entity_name6000001C: {name: name5, parent_scope: name_scope<none>, index: 1, is_template: 0, is_unused: 0}
-// CHECK:STDOUT:     entity_name6000001D: {name: name5, parent_scope: name_scope<none>, index: 1, is_template: 0, is_unused: 0}
-// CHECK:STDOUT:     entity_name6000001E: {name: name(SelfValue), parent_scope: name_scope<none>, index: -1, is_template: 0, is_unused: 0}
-// CHECK:STDOUT:     entity_name6000001F: {name: name5, parent_scope: name_scope<none>, index: 1, is_template: 0, is_unused: 0}
-// CHECK:STDOUT:     entity_name60000020: {name: name5, parent_scope: name_scope<none>, index: 1, is_template: 0, is_unused: 0}
-// CHECK:STDOUT:     entity_name60000021: {name: name1, parent_scope: name_scope<none>, index: 0, is_template: 0, is_unused: 0}
-// CHECK:STDOUT:     entity_name60000022: {name: name1, parent_scope: name_scope<none>, index: 0, is_template: 0, is_unused: 0}
-// CHECK:STDOUT:     entity_name60000023: {name: name5, parent_scope: name_scope<none>, index: 1, is_template: 0, is_unused: 0}
-// CHECK:STDOUT:     entity_name60000024: {name: name1, parent_scope: name_scope<none>, index: 0, is_template: 0, is_unused: 0}
-// CHECK:STDOUT:     entity_name60000025: {name: name5, parent_scope: name_scope<none>, index: 1, is_template: 0, is_unused: 0}
-// CHECK:STDOUT:     entity_name60000026: {name: name1, parent_scope: name_scope<none>, index: 0, is_template: 0, is_unused: 0}
-// CHECK:STDOUT:     entity_name60000027: {name: name5, parent_scope: name_scope<none>, index: 1, is_template: 0, is_unused: 0}
-// CHECK:STDOUT:     entity_name60000028: {name: name1, parent_scope: name_scope<none>, index: 0, is_template: 0, is_unused: 0}
-// CHECK:STDOUT:     entity_name60000029: {name: name5, parent_scope: name_scope<none>, index: 1, is_template: 0, is_unused: 0}
-// CHECK:STDOUT:     entity_name6000002A: {name: name1, parent_scope: name_scope<none>, index: 0, is_template: 0, is_unused: 0}
-// CHECK:STDOUT:     entity_name6000002B: {name: name6, parent_scope: name_scope<none>, index: 2, is_template: 0, is_unused: 0}
-// CHECK:STDOUT:     entity_name6000002C: {name: name6, parent_scope: name_scope<none>, index: 2, is_template: 0, is_unused: 0}
-// CHECK:STDOUT:     entity_name6000002D: {name: name(SelfValue), parent_scope: name_scope<none>, index: -1, is_template: 0, is_unused: 0}
-// CHECK:STDOUT:     entity_name6000002E: {name: name6, parent_scope: name_scope<none>, index: 2, is_template: 0, is_unused: 0}
-// CHECK:STDOUT:     entity_name6000002F: {name: name6, parent_scope: name_scope<none>, index: 2, is_template: 0, is_unused: 0}
-// CHECK:STDOUT:     entity_name60000030: {name: name5, parent_scope: name_scope<none>, index: 1, is_template: 0, is_unused: 0}
-// CHECK:STDOUT:     entity_name60000031: {name: name5, parent_scope: name_scope<none>, index: 1, is_template: 0, is_unused: 0}
-// CHECK:STDOUT:     entity_name60000032: {name: name1, parent_scope: name_scope<none>, index: 0, is_template: 0, is_unused: 0}
-// CHECK:STDOUT:     entity_name60000033: {name: name1, parent_scope: name_scope<none>, index: 0, is_template: 0, is_unused: 0}
-// CHECK:STDOUT:     entity_name60000034: {name: name6, parent_scope: name_scope<none>, index: 2, is_template: 0, is_unused: 0}
-// CHECK:STDOUT:     entity_name60000035: {name: name5, parent_scope: name_scope<none>, index: 1, is_template: 0, is_unused: 0}
-// CHECK:STDOUT:     entity_name60000036: {name: name1, parent_scope: name_scope<none>, index: 0, is_template: 0, is_unused: 0}
-// CHECK:STDOUT:     entity_name60000037: {name: name6, parent_scope: name_scope<none>, index: 2, is_template: 0, is_unused: 0}
-// CHECK:STDOUT:     entity_name60000038: {name: name5, parent_scope: name_scope<none>, index: 1, is_template: 0, is_unused: 0}
-// CHECK:STDOUT:     entity_name60000039: {name: name1, parent_scope: name_scope<none>, index: 0, is_template: 0, is_unused: 0}
-// CHECK:STDOUT:     entity_name6000003A: {name: name6, parent_scope: name_scope<none>, index: 2, is_template: 0, is_unused: 0}
-// CHECK:STDOUT:     entity_name6000003B: {name: name5, parent_scope: name_scope<none>, index: 1, is_template: 0, is_unused: 0}
-// CHECK:STDOUT:     entity_name6000003C: {name: name1, parent_scope: name_scope<none>, index: 0, is_template: 0, is_unused: 0}
-// CHECK:STDOUT:     entity_name6000003D: {name: name6, parent_scope: name_scope<none>, index: 2, is_template: 0, is_unused: 0}
-// CHECK:STDOUT:     entity_name6000003E: {name: name5, parent_scope: name_scope<none>, index: 1, is_template: 0, is_unused: 0}
-// CHECK:STDOUT:     entity_name6000003F: {name: name1, parent_scope: name_scope<none>, index: 0, is_template: 0, is_unused: 0}
+// CHECK:STDOUT:     entity_name60000000: {name: name(PeriodSelf), parent_scope: name_scope<none>, index: -1, is_template: 0, is_unused: 0, form: constant<none>}
+// CHECK:STDOUT:     entity_name60000001: {name: name1, parent_scope: name_scope<none>, index: 0, is_template: 0, is_unused: 0, form: constant<none>}
+// CHECK:STDOUT:     entity_name60000002: {name: name2, parent_scope: name_scope<none>, index: -1, is_template: 0, is_unused: 0, form: constant<none>}
+// CHECK:STDOUT:     entity_name60000003: {name: name3, parent_scope: name_scope60000001, index: -1, is_template: 0, is_unused: 0, form: constant<none>}
+// CHECK:STDOUT:     entity_name60000004: {name: name(SelfType), parent_scope: name_scope<none>, index: 0, is_template: 0, is_unused: 0, form: constant<none>}
+// CHECK:STDOUT:     entity_name60000005: {name: name4, parent_scope: name_scope60000003, index: -1, is_template: 0, is_unused: 0, form: constant<none>}
+// CHECK:STDOUT:     entity_name60000006: {name: name(SelfType), parent_scope: name_scope<none>, index: 0, is_template: 0, is_unused: 0, form: constant<none>}
+// CHECK:STDOUT:     entity_name60000007: {name: name(SelfType), parent_scope: name_scope<none>, index: 0, is_template: 0, is_unused: 0, form: constant<none>}
+// CHECK:STDOUT:     entity_name60000008: {name: name(SelfValue), parent_scope: name_scope<none>, index: -1, is_template: 0, is_unused: 0, form: constant<none>}
+// CHECK:STDOUT:     entity_name60000009: {name: name(SelfType), parent_scope: name_scope<none>, index: 0, is_template: 0, is_unused: 0, form: constant<none>}
+// CHECK:STDOUT:     entity_name6000000A: {name: name(SelfType), parent_scope: name_scope<none>, index: 0, is_template: 0, is_unused: 0, form: constant<none>}
+// CHECK:STDOUT:     entity_name6000000B: {name: name(SelfType), parent_scope: name_scope<none>, index: 0, is_template: 0, is_unused: 0, form: constant<none>}
+// CHECK:STDOUT:     entity_name6000000C: {name: name(SelfType), parent_scope: name_scope<none>, index: 0, is_template: 0, is_unused: 0, form: constant<none>}
+// CHECK:STDOUT:     entity_name6000000D: {name: name1, parent_scope: name_scope<none>, index: 0, is_template: 0, is_unused: 0, form: constant<none>}
+// CHECK:STDOUT:     entity_name6000000E: {name: name1, parent_scope: name_scope<none>, index: 0, is_template: 0, is_unused: 0, form: constant<none>}
+// CHECK:STDOUT:     entity_name6000000F: {name: name(SelfValue), parent_scope: name_scope<none>, index: -1, is_template: 0, is_unused: 0, form: constant<none>}
+// CHECK:STDOUT:     entity_name60000010: {name: name1, parent_scope: name_scope<none>, index: 0, is_template: 0, is_unused: 0, form: constant<none>}
+// CHECK:STDOUT:     entity_name60000011: {name: name1, parent_scope: name_scope<none>, index: 0, is_template: 0, is_unused: 0, form: constant<none>}
+// CHECK:STDOUT:     entity_name60000012: {name: name1, parent_scope: name_scope<none>, index: 0, is_template: 0, is_unused: 0, form: constant<none>}
+// CHECK:STDOUT:     entity_name60000013: {name: name1, parent_scope: name_scope<none>, index: 0, is_template: 0, is_unused: 0, form: constant<none>}
+// CHECK:STDOUT:     entity_name60000014: {name: name1, parent_scope: name_scope<none>, index: 0, is_template: 0, is_unused: 0, form: constant<none>}
+// CHECK:STDOUT:     entity_name60000015: {name: name1, parent_scope: name_scope<none>, index: 0, is_template: 0, is_unused: 0, form: constant<none>}
+// CHECK:STDOUT:     entity_name60000016: {name: name1, parent_scope: name_scope<none>, index: 0, is_template: 0, is_unused: 0, form: constant<none>}
+// CHECK:STDOUT:     entity_name60000017: {name: name1, parent_scope: name_scope<none>, index: 0, is_template: 0, is_unused: 0, form: constant<none>}
+// CHECK:STDOUT:     entity_name60000018: {name: name1, parent_scope: name_scope<none>, index: 0, is_template: 0, is_unused: 0, form: constant<none>}
+// CHECK:STDOUT:     entity_name60000019: {name: name1, parent_scope: name_scope<none>, index: 0, is_template: 0, is_unused: 0, form: constant<none>}
+// CHECK:STDOUT:     entity_name6000001A: {name: name(SelfValue), parent_scope: name_scope<none>, index: -1, is_template: 0, is_unused: 0, form: constant<none>}
+// CHECK:STDOUT:     entity_name6000001B: {name: name1, parent_scope: name_scope<none>, index: 0, is_template: 0, is_unused: 0, form: constant<none>}
+// CHECK:STDOUT:     entity_name6000001C: {name: name5, parent_scope: name_scope<none>, index: 1, is_template: 0, is_unused: 0, form: constant<none>}
+// CHECK:STDOUT:     entity_name6000001D: {name: name5, parent_scope: name_scope<none>, index: 1, is_template: 0, is_unused: 0, form: constant<none>}
+// CHECK:STDOUT:     entity_name6000001E: {name: name(SelfValue), parent_scope: name_scope<none>, index: -1, is_template: 0, is_unused: 0, form: constant<none>}
+// CHECK:STDOUT:     entity_name6000001F: {name: name5, parent_scope: name_scope<none>, index: 1, is_template: 0, is_unused: 0, form: constant<none>}
+// CHECK:STDOUT:     entity_name60000020: {name: name5, parent_scope: name_scope<none>, index: 1, is_template: 0, is_unused: 0, form: constant<none>}
+// CHECK:STDOUT:     entity_name60000021: {name: name1, parent_scope: name_scope<none>, index: 0, is_template: 0, is_unused: 0, form: constant<none>}
+// CHECK:STDOUT:     entity_name60000022: {name: name1, parent_scope: name_scope<none>, index: 0, is_template: 0, is_unused: 0, form: constant<none>}
+// CHECK:STDOUT:     entity_name60000023: {name: name5, parent_scope: name_scope<none>, index: 1, is_template: 0, is_unused: 0, form: constant<none>}
+// CHECK:STDOUT:     entity_name60000024: {name: name1, parent_scope: name_scope<none>, index: 0, is_template: 0, is_unused: 0, form: constant<none>}
+// CHECK:STDOUT:     entity_name60000025: {name: name5, parent_scope: name_scope<none>, index: 1, is_template: 0, is_unused: 0, form: constant<none>}
+// CHECK:STDOUT:     entity_name60000026: {name: name1, parent_scope: name_scope<none>, index: 0, is_template: 0, is_unused: 0, form: constant<none>}
+// CHECK:STDOUT:     entity_name60000027: {name: name5, parent_scope: name_scope<none>, index: 1, is_template: 0, is_unused: 0, form: constant<none>}
+// CHECK:STDOUT:     entity_name60000028: {name: name1, parent_scope: name_scope<none>, index: 0, is_template: 0, is_unused: 0, form: constant<none>}
+// CHECK:STDOUT:     entity_name60000029: {name: name5, parent_scope: name_scope<none>, index: 1, is_template: 0, is_unused: 0, form: constant<none>}
+// CHECK:STDOUT:     entity_name6000002A: {name: name1, parent_scope: name_scope<none>, index: 0, is_template: 0, is_unused: 0, form: constant<none>}
+// CHECK:STDOUT:     entity_name6000002B: {name: name6, parent_scope: name_scope<none>, index: 2, is_template: 0, is_unused: 0, form: constant<none>}
+// CHECK:STDOUT:     entity_name6000002C: {name: name6, parent_scope: name_scope<none>, index: 2, is_template: 0, is_unused: 0, form: constant<none>}
+// CHECK:STDOUT:     entity_name6000002D: {name: name(SelfValue), parent_scope: name_scope<none>, index: -1, is_template: 0, is_unused: 0, form: constant<none>}
+// CHECK:STDOUT:     entity_name6000002E: {name: name6, parent_scope: name_scope<none>, index: 2, is_template: 0, is_unused: 0, form: constant<none>}
+// CHECK:STDOUT:     entity_name6000002F: {name: name6, parent_scope: name_scope<none>, index: 2, is_template: 0, is_unused: 0, form: constant<none>}
+// CHECK:STDOUT:     entity_name60000030: {name: name5, parent_scope: name_scope<none>, index: 1, is_template: 0, is_unused: 0, form: constant<none>}
+// CHECK:STDOUT:     entity_name60000031: {name: name5, parent_scope: name_scope<none>, index: 1, is_template: 0, is_unused: 0, form: constant<none>}
+// CHECK:STDOUT:     entity_name60000032: {name: name1, parent_scope: name_scope<none>, index: 0, is_template: 0, is_unused: 0, form: constant<none>}
+// CHECK:STDOUT:     entity_name60000033: {name: name1, parent_scope: name_scope<none>, index: 0, is_template: 0, is_unused: 0, form: constant<none>}
+// CHECK:STDOUT:     entity_name60000034: {name: name6, parent_scope: name_scope<none>, index: 2, is_template: 0, is_unused: 0, form: constant<none>}
+// CHECK:STDOUT:     entity_name60000035: {name: name5, parent_scope: name_scope<none>, index: 1, is_template: 0, is_unused: 0, form: constant<none>}
+// CHECK:STDOUT:     entity_name60000036: {name: name1, parent_scope: name_scope<none>, index: 0, is_template: 0, is_unused: 0, form: constant<none>}
+// CHECK:STDOUT:     entity_name60000037: {name: name6, parent_scope: name_scope<none>, index: 2, is_template: 0, is_unused: 0, form: constant<none>}
+// CHECK:STDOUT:     entity_name60000038: {name: name5, parent_scope: name_scope<none>, index: 1, is_template: 0, is_unused: 0, form: constant<none>}
+// CHECK:STDOUT:     entity_name60000039: {name: name1, parent_scope: name_scope<none>, index: 0, is_template: 0, is_unused: 0, form: constant<none>}
+// CHECK:STDOUT:     entity_name6000003A: {name: name6, parent_scope: name_scope<none>, index: 2, is_template: 0, is_unused: 0, form: constant<none>}
+// CHECK:STDOUT:     entity_name6000003B: {name: name5, parent_scope: name_scope<none>, index: 1, is_template: 0, is_unused: 0, form: constant<none>}
+// CHECK:STDOUT:     entity_name6000003C: {name: name1, parent_scope: name_scope<none>, index: 0, is_template: 0, is_unused: 0, form: constant<none>}
+// CHECK:STDOUT:     entity_name6000003D: {name: name6, parent_scope: name_scope<none>, index: 2, is_template: 0, is_unused: 0, form: constant<none>}
+// CHECK:STDOUT:     entity_name6000003E: {name: name5, parent_scope: name_scope<none>, index: 1, is_template: 0, is_unused: 0, form: constant<none>}
+// CHECK:STDOUT:     entity_name6000003F: {name: name1, parent_scope: name_scope<none>, index: 0, is_template: 0, is_unused: 0, form: constant<none>}
 // CHECK:STDOUT:   cpp_global_vars: {}
 // CHECK:STDOUT:   functions:
 // CHECK:STDOUT:     function60000000: {name: name0, parent_scope: name_scope0, call_param_patterns_id: inst_block60000011, call_params_id: inst_block60000012, return_type_inst_id: inst60000030, return_form_inst_id: inst60000032, return_patterns_id: inst_block60000010, body: [inst_block60000019]}

+ 1 - 1
toolchain/check/testdata/basics/raw_sem_ir/one_file_with_textual_ir.carbon

@@ -31,7 +31,7 @@ fn Foo(n: ()) -> ((), ()) {
 // CHECK:STDOUT:   name_scopes:
 // CHECK:STDOUT:     name_scope0:     {inst: instF, parent_scope: name_scope<none>, has_error: false, extended_scopes: [], names: {name0: inst6000002A}}
 // CHECK:STDOUT:   entity_names:
-// CHECK:STDOUT:     entity_name60000000: {name: name1, parent_scope: name_scope<none>, index: -1, is_template: 0, is_unused: 0}
+// CHECK:STDOUT:     entity_name60000000: {name: name1, parent_scope: name_scope<none>, index: -1, is_template: 0, is_unused: 0, form: constant<none>}
 // CHECK:STDOUT:   cpp_global_vars: {}
 // CHECK:STDOUT:   functions:
 // CHECK:STDOUT:     function60000000: {name: name0, parent_scope: name_scope0, call_param_patterns_id: inst_block6000000C, call_params_id: inst_block6000000D, return_type_inst_id: inst60000020, return_form_inst_id: inst60000021, return_patterns_id: inst_block6000000B, body: [inst_block60000010]}

+ 419 - 0
toolchain/check/testdata/function/call/form.carbon

@@ -0,0 +1,419 @@
+// 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/check/testdata/function/call/form.carbon
+// TIP: To dump output, run:
+// TIP:   bazel run //toolchain/testing:file_test -- --dump_output --file_tests=toolchain/check/testdata/function/call/form.carbon
+
+// --- ref_form_param.carbon
+library "[[@TEST_NAME]]";
+
+//@dump-sem-ir-begin
+fn F(x:? form(ref i32));
+//@dump-sem-ir-end
+
+fn G() {
+  var y: i32 = 0;
+  //@dump-sem-ir-begin
+  F(ref y);
+  //@dump-sem-ir-end
+}
+
+// --- var_form_param.carbon
+library "[[@TEST_NAME]]";
+
+//@dump-sem-ir-begin
+fn F(x:? form(var i32));
+//@dump-sem-ir-end
+
+fn G() {
+  //@dump-sem-ir-begin
+  F(0);
+  //@dump-sem-ir-end
+}
+
+// --- val_form_param.carbon
+library "[[@TEST_NAME]]";
+
+//@dump-sem-ir-begin
+fn F(x:? form(val i32));
+//@dump-sem-ir-end
+
+fn G() {
+  //@dump-sem-ir-begin
+  F(0);
+  //@dump-sem-ir-end
+}
+
+// --- type_component_needs_conversion.carbon
+library "[[@TEST_NAME]]";
+
+//@dump-sem-ir-begin
+fn F(x:? form(ref ()));
+//@dump-sem-ir-end
+
+// --- fail_param_form_not_constant.carbon
+library "[[@TEST_NAME]]";
+
+fn F() -> Core.Form();
+// CHECK:STDERR: fail_param_form_not_constant.carbon:[[@LINE+4]]:10: error: cannot evaluate form expression [FormExprEvaluationFailure]
+// CHECK:STDERR: fn G(x:? F());
+// CHECK:STDERR:          ^~~
+// CHECK:STDERR:
+fn G(x:? F());
+
+// --- fail_form_param_not_form.carbon
+library "[[@TEST_NAME]]";
+
+// CHECK:STDERR: fail_form_param_not_form.carbon:[[@LINE+7]]:10: error: cannot implicitly convert expression of type `type` to `Core.Form` [ConversionFailure]
+// CHECK:STDERR: fn F(x:? i32);
+// CHECK:STDERR:          ^~~
+// CHECK:STDERR: fail_form_param_not_form.carbon:[[@LINE+4]]:10: note: type `type` does not implement interface `Core.ImplicitAs(Core.Form)` [MissingImplInMemberAccessInContext]
+// CHECK:STDERR: fn F(x:? i32);
+// CHECK:STDERR:          ^~~
+// CHECK:STDERR:
+fn F(x:? i32);
+// CHECK:STDERR: fail_form_param_not_form.carbon:[[@LINE+8]]:6: error: semantics TODO: `handle invalid parse trees in `check`` [SemanticsTodo]
+// CHECK:STDERR: fn G(ref x:? i32);
+// CHECK:STDERR:      ^~~~~
+// CHECK:STDERR:
+// CHECK:STDERR: fail_form_param_not_form.carbon:[[@LINE+4]]:6: error: expected `:` binding after `ref` [ExpectedRuntimeBindingPatternAfterRef]
+// CHECK:STDERR: fn G(ref x:? i32);
+// CHECK:STDERR:      ^~~
+// CHECK:STDERR:
+fn G(ref x:? i32);
+
+// --- fail_missing_ref_tag.carbon
+library "[[@TEST_NAME]]";
+
+fn F(x:? form(ref i32));
+
+fn G() {
+  var y: i32 = 0;
+  // CHECK:STDERR: fail_missing_ref_tag.carbon:[[@LINE+7]]:5: error: argument to `ref` parameter not marked with `ref` [RefParamNoRefTag]
+  // CHECK:STDERR:   F(y);
+  // CHECK:STDERR:     ^
+  // CHECK:STDERR: fail_missing_ref_tag.carbon:[[@LINE-7]]:6: note: initializing function parameter [InCallToFunctionParam]
+  // CHECK:STDERR: fn F(x:? form(ref i32));
+  // CHECK:STDERR:      ^~~~~~~~~~~~~~~~~
+  // CHECK:STDERR:
+  F(y);
+}
+
+// --- fail_todo_form_generic_param.carbon
+library "[[@TEST_NAME]]";
+
+// CHECK:STDERR: fail_todo_form_generic_param.carbon:[[@LINE+4]]:26: error: semantics TODO: `Support symbolic form bindings` [SemanticsTodo]
+// CHECK:STDERR: fn F(Form:! Core.Form(), x:? Form);
+// CHECK:STDERR:                          ^~~~~~~~
+// CHECK:STDERR:
+fn F(Form:! Core.Form(), x:? Form);
+
+fn G() {
+  var x: i32;
+  F(form(var i32), x);
+}
+
+// --- fail_todo_local_init_form_binding.carbon
+library "[[@TEST_NAME]]";
+
+fn F() {
+  // CHECK:STDERR: fail_todo_local_init_form_binding.carbon:[[@LINE+12]]:7: error: semantics TODO: `Support local initializing forms` [SemanticsTodo]
+  // CHECK:STDERR:   let x:? form(var i32) = 0;
+  // CHECK:STDERR:       ^
+  // CHECK:STDERR:
+  // CHECK:STDERR: fail_todo_local_init_form_binding.carbon:[[@LINE+8]]:27: error: cannot bind durable reference to non-reference value of type `i32` [ConversionFailureNonRefToRef]
+  // CHECK:STDERR:   let x:? form(var i32) = 0;
+  // CHECK:STDERR:                           ^
+  // CHECK:STDERR:
+  // CHECK:STDERR: fail_todo_local_init_form_binding.carbon:[[@LINE+4]]:7: warning: binding `x` unused [UnusedBinding]
+  // CHECK:STDERR:   let x:? form(var i32) = 0;
+  // CHECK:STDERR:       ^
+  // CHECK:STDERR:
+  let x:? form(var i32) = 0;
+}
+
+// --- fail_todo_local_symbolic_form_binding.carbon
+library "[[@TEST_NAME]]";
+
+// TODO: this code should fail for a different reason, but right now we don't
+// have a way to write correct code that reaches the TODO we're exercising here.
+fn F(Form:! Core.Form()) {
+  // CHECK:STDERR: fail_todo_local_symbolic_form_binding.carbon:[[@LINE+4]]:7: error: semantics TODO: `Support symbolic form bindings` [SemanticsTodo]
+  // CHECK:STDERR:   let y:? Form = 0;
+  // CHECK:STDERR:       ^~~~~~~~
+  // CHECK:STDERR:
+  let y:? Form = 0;
+}
+
+// --- fail_todo_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();
+  //@dump-sem-ir-end
+}
+
+// --- fail_todo_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();
+  //@dump-sem-ir-end
+}
+
+// --- fail_todo_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();
+  //@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: fn F() ->? i32;
+// CHECK:STDERR:        ^~~~~~~
+// CHECK:STDERR:
+fn F() ->? i32;
+fn F() ->? ref i32;
+
+// CHECK:STDOUT: --- ref_form_param.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:   %.1da: Core.Form = ref_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: }
+// CHECK:STDOUT:
+// CHECK:STDOUT: file {
+// CHECK:STDOUT:   %F.decl: %F.type = fn_decl @F [concrete = constants.%F] {
+// CHECK:STDOUT:     %.loc4_6.1: %pattern_type.7ce = form_binding_pattern x [concrete]
+// CHECK:STDOUT:     %.loc4_7: %pattern_type.7ce = form_param_pattern %.loc4_6.1, call_param0 [concrete]
+// CHECK:STDOUT:   } {
+// CHECK:STDOUT:     %x.param: ref %i32 = ref_param call_param0
+// CHECK:STDOUT:     %.loc4_15.1: Core.Form = splice_block %.loc4_15.2 [concrete = constants.%.1da] {
+// CHECK:STDOUT:       %i32: type = type_literal constants.%i32 [concrete = constants.%i32]
+// CHECK:STDOUT:       %.loc4_15.2: Core.Form = ref_form %i32 [concrete = constants.%.1da]
+// CHECK:STDOUT:     }
+// CHECK:STDOUT:     %.loc4_6.2: ref %i32 = form_binding x, %x.param
+// CHECK:STDOUT:   }
+// CHECK:STDOUT: }
+// CHECK:STDOUT:
+// CHECK:STDOUT: fn @F(%x.param: ref %i32);
+// CHECK:STDOUT:
+// CHECK:STDOUT: fn @G() {
+// CHECK:STDOUT: !entry:
+// CHECK:STDOUT:   <elided>
+// CHECK:STDOUT:   %F.ref: %F.type = name_ref F, file.%F.decl [concrete = constants.%F]
+// CHECK:STDOUT:   %y.ref: ref %i32 = name_ref y, %y
+// CHECK:STDOUT:   %.loc10: %i32 = ref_tag %y.ref
+// CHECK:STDOUT:   %F.call: init %empty_tuple.type = call %F.ref(%.loc10)
+// CHECK:STDOUT:   <elided>
+// CHECK:STDOUT: }
+// CHECK:STDOUT:
+// CHECK:STDOUT: --- var_form_param.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:   %.324: Core.Form = init_form %i32, call_param<none> [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:   %int_0.5c6: Core.IntLiteral = int_value 0 [concrete]
+// CHECK:STDOUT:   %ImplicitAs.type.e8c: type = facet_type <@ImplicitAs, @ImplicitAs(%i32)> [concrete]
+// CHECK:STDOUT:   %To: Core.IntLiteral = symbolic_binding To, 0 [symbolic]
+// CHECK:STDOUT:   %Core.IntLiteral.as.ImplicitAs.impl.Convert.type.4e6: type = fn_type @Core.IntLiteral.as.ImplicitAs.impl.Convert, @Core.IntLiteral.as.ImplicitAs.impl(%To) [symbolic]
+// CHECK:STDOUT:   %Core.IntLiteral.as.ImplicitAs.impl.Convert.3c2: %Core.IntLiteral.as.ImplicitAs.impl.Convert.type.4e6 = struct_value () [symbolic]
+// CHECK:STDOUT:   %ImplicitAs.impl_witness.6bc: <witness> = impl_witness imports.%ImplicitAs.impl_witness_table.74f, @Core.IntLiteral.as.ImplicitAs.impl(%int_32) [concrete]
+// CHECK:STDOUT:   %Core.IntLiteral.as.ImplicitAs.impl.Convert.type.e0d: type = fn_type @Core.IntLiteral.as.ImplicitAs.impl.Convert, @Core.IntLiteral.as.ImplicitAs.impl(%int_32) [concrete]
+// CHECK:STDOUT:   %Core.IntLiteral.as.ImplicitAs.impl.Convert.0b5: %Core.IntLiteral.as.ImplicitAs.impl.Convert.type.e0d = struct_value () [concrete]
+// CHECK:STDOUT:   %ImplicitAs.facet: %ImplicitAs.type.e8c = facet_value Core.IntLiteral, (%ImplicitAs.impl_witness.6bc) [concrete]
+// CHECK:STDOUT:   %ImplicitAs.WithSelf.Convert.type.b37: type = fn_type @ImplicitAs.WithSelf.Convert, @ImplicitAs(%i32, %ImplicitAs.facet) [concrete]
+// CHECK:STDOUT:   %.545: type = fn_type_with_self_type %ImplicitAs.WithSelf.Convert.type.b37, %ImplicitAs.facet [concrete]
+// CHECK:STDOUT:   %Core.IntLiteral.as.ImplicitAs.impl.Convert.bound: <bound method> = bound_method %int_0.5c6, %Core.IntLiteral.as.ImplicitAs.impl.Convert.0b5 [concrete]
+// CHECK:STDOUT:   %Core.IntLiteral.as.ImplicitAs.impl.Convert.specific_fn: <specific function> = specific_function %Core.IntLiteral.as.ImplicitAs.impl.Convert.0b5, @Core.IntLiteral.as.ImplicitAs.impl.Convert(%int_32) [concrete]
+// CHECK:STDOUT:   %bound_method: <bound method> = bound_method %int_0.5c6, %Core.IntLiteral.as.ImplicitAs.impl.Convert.specific_fn [concrete]
+// CHECK:STDOUT:   %int_0.6a9: %i32 = int_value 0 [concrete]
+// CHECK:STDOUT:   %DestroyOp.type: type = fn_type @DestroyOp [concrete]
+// CHECK:STDOUT:   %DestroyOp: %DestroyOp.type = struct_value () [concrete]
+// CHECK:STDOUT: }
+// CHECK:STDOUT:
+// CHECK:STDOUT: imports {
+// CHECK:STDOUT:   %Core.import_ref.42d: @Core.IntLiteral.as.ImplicitAs.impl.%Core.IntLiteral.as.ImplicitAs.impl.Convert.type (%Core.IntLiteral.as.ImplicitAs.impl.Convert.type.4e6) = import_ref Core//prelude/parts/int, loc{{\d+_\d+}}, loaded [symbolic = @Core.IntLiteral.as.ImplicitAs.impl.%Core.IntLiteral.as.ImplicitAs.impl.Convert (constants.%Core.IntLiteral.as.ImplicitAs.impl.Convert.3c2)]
+// CHECK:STDOUT:   %ImplicitAs.impl_witness_table.74f = impl_witness_table (%Core.import_ref.42d), @Core.IntLiteral.as.ImplicitAs.impl [concrete]
+// CHECK:STDOUT: }
+// CHECK:STDOUT:
+// CHECK:STDOUT: file {
+// CHECK:STDOUT:   %F.decl: %F.type = fn_decl @F [concrete = constants.%F] {
+// CHECK:STDOUT:     %.loc4_6.1: %pattern_type.7ce = form_binding_pattern x [concrete]
+// CHECK:STDOUT:     %.loc4_7: %pattern_type.7ce = form_param_pattern %.loc4_6.1, call_param0 [concrete]
+// CHECK:STDOUT:   } {
+// CHECK:STDOUT:     %x.param: ref %i32 = ref_param call_param0
+// CHECK:STDOUT:     %.loc4_15.1: Core.Form = splice_block %.loc4_15.2 [concrete = constants.%.324] {
+// CHECK:STDOUT:       %i32: type = type_literal constants.%i32 [concrete = constants.%i32]
+// CHECK:STDOUT:       %.loc4_15.2: Core.Form = init_form %i32, call_param<none> [concrete = constants.%.324]
+// CHECK:STDOUT:     }
+// CHECK:STDOUT:     %.loc4_6.2: ref %i32 = form_binding x, %x.param
+// CHECK:STDOUT:   }
+// CHECK:STDOUT: }
+// CHECK:STDOUT:
+// CHECK:STDOUT: fn @F(%x.param: ref %i32);
+// CHECK:STDOUT:
+// CHECK:STDOUT: fn @G() {
+// CHECK:STDOUT: !entry:
+// CHECK:STDOUT:   %F.ref: %F.type = name_ref F, file.%F.decl [concrete = constants.%F]
+// CHECK:STDOUT:   %int_0: Core.IntLiteral = int_value 0 [concrete = constants.%int_0.5c6]
+// CHECK:STDOUT:   %.loc4_7.1: ref %i32 = temporary_storage
+// CHECK:STDOUT:   %impl.elem0: %.545 = impl_witness_access constants.%ImplicitAs.impl_witness.6bc, element0 [concrete = constants.%Core.IntLiteral.as.ImplicitAs.impl.Convert.0b5]
+// CHECK:STDOUT:   %bound_method.loc4_7.1: <bound method> = bound_method %int_0, %impl.elem0 [concrete = constants.%Core.IntLiteral.as.ImplicitAs.impl.Convert.bound]
+// CHECK:STDOUT:   %specific_fn: <specific function> = specific_function %impl.elem0, @Core.IntLiteral.as.ImplicitAs.impl.Convert(constants.%int_32) [concrete = constants.%Core.IntLiteral.as.ImplicitAs.impl.Convert.specific_fn]
+// CHECK:STDOUT:   %bound_method.loc4_7.2: <bound method> = bound_method %int_0, %specific_fn [concrete = constants.%bound_method]
+// CHECK:STDOUT:   %Core.IntLiteral.as.ImplicitAs.impl.Convert.call: init %i32 = call %bound_method.loc4_7.2(%int_0) [concrete = constants.%int_0.6a9]
+// CHECK:STDOUT:   %.loc4_7.2: init %i32 = converted %int_0, %Core.IntLiteral.as.ImplicitAs.impl.Convert.call [concrete = constants.%int_0.6a9]
+// CHECK:STDOUT:   %.loc4_7.3: ref %i32 = temporary %.loc4_7.1, %.loc4_7.2
+// CHECK:STDOUT:   %F.call: init %empty_tuple.type = call %F.ref(%.loc4_7.3)
+// CHECK:STDOUT:   %DestroyOp.bound: <bound method> = bound_method %.loc4_7.3, constants.%DestroyOp
+// CHECK:STDOUT:   %DestroyOp.call: init %empty_tuple.type = call %DestroyOp.bound(%.loc4_7.3)
+// CHECK:STDOUT:   <elided>
+// CHECK:STDOUT: }
+// CHECK:STDOUT:
+// CHECK:STDOUT: fn @DestroyOp(%self.param: ref %i32) = "no_op";
+// CHECK:STDOUT:
+// CHECK:STDOUT: --- val_form_param.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:   %.754: Core.Form = value_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:   %int_0.5c6: Core.IntLiteral = int_value 0 [concrete]
+// CHECK:STDOUT:   %ImplicitAs.type.e8c: type = facet_type <@ImplicitAs, @ImplicitAs(%i32)> [concrete]
+// CHECK:STDOUT:   %To: Core.IntLiteral = symbolic_binding To, 0 [symbolic]
+// CHECK:STDOUT:   %Core.IntLiteral.as.ImplicitAs.impl.Convert.type.4e6: type = fn_type @Core.IntLiteral.as.ImplicitAs.impl.Convert, @Core.IntLiteral.as.ImplicitAs.impl(%To) [symbolic]
+// CHECK:STDOUT:   %Core.IntLiteral.as.ImplicitAs.impl.Convert.3c2: %Core.IntLiteral.as.ImplicitAs.impl.Convert.type.4e6 = struct_value () [symbolic]
+// CHECK:STDOUT:   %ImplicitAs.impl_witness.6bc: <witness> = impl_witness imports.%ImplicitAs.impl_witness_table.74f, @Core.IntLiteral.as.ImplicitAs.impl(%int_32) [concrete]
+// CHECK:STDOUT:   %Core.IntLiteral.as.ImplicitAs.impl.Convert.type.e0d: type = fn_type @Core.IntLiteral.as.ImplicitAs.impl.Convert, @Core.IntLiteral.as.ImplicitAs.impl(%int_32) [concrete]
+// CHECK:STDOUT:   %Core.IntLiteral.as.ImplicitAs.impl.Convert.0b5: %Core.IntLiteral.as.ImplicitAs.impl.Convert.type.e0d = struct_value () [concrete]
+// CHECK:STDOUT:   %ImplicitAs.facet: %ImplicitAs.type.e8c = facet_value Core.IntLiteral, (%ImplicitAs.impl_witness.6bc) [concrete]
+// CHECK:STDOUT:   %ImplicitAs.WithSelf.Convert.type.b37: type = fn_type @ImplicitAs.WithSelf.Convert, @ImplicitAs(%i32, %ImplicitAs.facet) [concrete]
+// CHECK:STDOUT:   %.545: type = fn_type_with_self_type %ImplicitAs.WithSelf.Convert.type.b37, %ImplicitAs.facet [concrete]
+// CHECK:STDOUT:   %Core.IntLiteral.as.ImplicitAs.impl.Convert.bound: <bound method> = bound_method %int_0.5c6, %Core.IntLiteral.as.ImplicitAs.impl.Convert.0b5 [concrete]
+// CHECK:STDOUT:   %Core.IntLiteral.as.ImplicitAs.impl.Convert.specific_fn: <specific function> = specific_function %Core.IntLiteral.as.ImplicitAs.impl.Convert.0b5, @Core.IntLiteral.as.ImplicitAs.impl.Convert(%int_32) [concrete]
+// CHECK:STDOUT:   %bound_method: <bound method> = bound_method %int_0.5c6, %Core.IntLiteral.as.ImplicitAs.impl.Convert.specific_fn [concrete]
+// CHECK:STDOUT:   %int_0.6a9: %i32 = int_value 0 [concrete]
+// CHECK:STDOUT: }
+// CHECK:STDOUT:
+// CHECK:STDOUT: imports {
+// CHECK:STDOUT:   %Core.import_ref.42d: @Core.IntLiteral.as.ImplicitAs.impl.%Core.IntLiteral.as.ImplicitAs.impl.Convert.type (%Core.IntLiteral.as.ImplicitAs.impl.Convert.type.4e6) = import_ref Core//prelude/parts/int, loc{{\d+_\d+}}, loaded [symbolic = @Core.IntLiteral.as.ImplicitAs.impl.%Core.IntLiteral.as.ImplicitAs.impl.Convert (constants.%Core.IntLiteral.as.ImplicitAs.impl.Convert.3c2)]
+// CHECK:STDOUT:   %ImplicitAs.impl_witness_table.74f = impl_witness_table (%Core.import_ref.42d), @Core.IntLiteral.as.ImplicitAs.impl [concrete]
+// CHECK:STDOUT: }
+// CHECK:STDOUT:
+// CHECK:STDOUT: file {
+// CHECK:STDOUT:   %F.decl: %F.type = fn_decl @F [concrete = constants.%F] {
+// CHECK:STDOUT:     %.loc4_6.1: %pattern_type.7ce = form_binding_pattern x [concrete]
+// CHECK:STDOUT:     %.loc4_7: %pattern_type.7ce = form_param_pattern %.loc4_6.1, call_param0 [concrete]
+// CHECK:STDOUT:   } {
+// CHECK:STDOUT:     %x.param: %i32 = value_param call_param0
+// CHECK:STDOUT:     %.loc4_15.1: Core.Form = splice_block %.loc4_15.2 [concrete = constants.%.754] {
+// CHECK:STDOUT:       %i32: type = type_literal constants.%i32 [concrete = constants.%i32]
+// CHECK:STDOUT:       %.loc4_15.2: Core.Form = value_form %i32 [concrete = constants.%.754]
+// CHECK:STDOUT:     }
+// CHECK:STDOUT:     %.loc4_6.2: %i32 = form_binding x, %x.param
+// CHECK:STDOUT:   }
+// CHECK:STDOUT: }
+// CHECK:STDOUT:
+// CHECK:STDOUT: fn @F(%x.param: %i32);
+// CHECK:STDOUT:
+// CHECK:STDOUT: fn @G() {
+// CHECK:STDOUT: !entry:
+// CHECK:STDOUT:   %F.ref: %F.type = name_ref F, file.%F.decl [concrete = constants.%F]
+// CHECK:STDOUT:   %int_0: Core.IntLiteral = int_value 0 [concrete = constants.%int_0.5c6]
+// CHECK:STDOUT:   %impl.elem0: %.545 = impl_witness_access constants.%ImplicitAs.impl_witness.6bc, element0 [concrete = constants.%Core.IntLiteral.as.ImplicitAs.impl.Convert.0b5]
+// CHECK:STDOUT:   %bound_method.loc9_5.1: <bound method> = bound_method %int_0, %impl.elem0 [concrete = constants.%Core.IntLiteral.as.ImplicitAs.impl.Convert.bound]
+// CHECK:STDOUT:   %specific_fn: <specific function> = specific_function %impl.elem0, @Core.IntLiteral.as.ImplicitAs.impl.Convert(constants.%int_32) [concrete = constants.%Core.IntLiteral.as.ImplicitAs.impl.Convert.specific_fn]
+// CHECK:STDOUT:   %bound_method.loc9_5.2: <bound method> = bound_method %int_0, %specific_fn [concrete = constants.%bound_method]
+// CHECK:STDOUT:   %Core.IntLiteral.as.ImplicitAs.impl.Convert.call: init %i32 = call %bound_method.loc9_5.2(%int_0) [concrete = constants.%int_0.6a9]
+// CHECK:STDOUT:   %.loc9_5.1: %i32 = value_of_initializer %Core.IntLiteral.as.ImplicitAs.impl.Convert.call [concrete = constants.%int_0.6a9]
+// CHECK:STDOUT:   %.loc9_5.2: %i32 = converted %int_0, %.loc9_5.1 [concrete = constants.%int_0.6a9]
+// CHECK:STDOUT:   %F.call: init %empty_tuple.type = call %F.ref(%.loc9_5.2)
+// CHECK:STDOUT:   <elided>
+// CHECK:STDOUT: }
+// CHECK:STDOUT:
+// CHECK:STDOUT: --- type_component_needs_conversion.carbon
+// CHECK:STDOUT:
+// CHECK:STDOUT: constants {
+// CHECK:STDOUT:   %empty_tuple.type: type = tuple_type () [concrete]
+// CHECK:STDOUT:   %empty_tuple: %empty_tuple.type = tuple_value () [concrete]
+// CHECK:STDOUT:   %.9f9: Core.Form = ref_form %empty_tuple.type [concrete]
+// CHECK:STDOUT:   %pattern_type: type = pattern_type %empty_tuple.type [concrete]
+// CHECK:STDOUT:   %F.type: type = fn_type @F [concrete]
+// CHECK:STDOUT:   %F: %F.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:     %.loc4_6.1: %pattern_type = form_binding_pattern x [concrete]
+// CHECK:STDOUT:     %.loc4_7: %pattern_type = form_param_pattern %.loc4_6.1, call_param0 [concrete]
+// CHECK:STDOUT:   } {
+// CHECK:STDOUT:     %x.param: ref %empty_tuple.type = ref_param call_param0
+// CHECK:STDOUT:     %.loc4_15.1: Core.Form = splice_block %.loc4_15.2 [concrete = constants.%.9f9] {
+// CHECK:STDOUT:       %.loc4_20.1: %empty_tuple.type = tuple_literal () [concrete = constants.%empty_tuple]
+// CHECK:STDOUT:       %.loc4_20.2: type = converted %.loc4_20.1, constants.%empty_tuple.type [concrete = constants.%empty_tuple.type]
+// CHECK:STDOUT:       %.loc4_15.2: Core.Form = ref_form %.loc4_20.2 [concrete = constants.%.9f9]
+// CHECK:STDOUT:     }
+// CHECK:STDOUT:     %.loc4_6.2: ref %empty_tuple.type = form_binding x, %x.param
+// CHECK:STDOUT:   }
+// CHECK:STDOUT: }
+// CHECK:STDOUT:
+// CHECK:STDOUT: fn @F(%x.param: ref %empty_tuple.type);
+// CHECK:STDOUT:
+// CHECK:STDOUT: --- fail_todo_ref_return_form.carbon
+// CHECK:STDOUT:
+// CHECK:STDOUT: --- fail_todo_var_return_form.carbon
+// CHECK:STDOUT:
+// CHECK:STDOUT: --- fail_todo_val_return_form.carbon
+// CHECK:STDOUT:

+ 36 - 0
toolchain/check/testdata/impl/fail_todo_form_thunk.carbon

@@ -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
+//
+// INCLUDE-FILE: toolchain/testing/testdata/min_prelude/none.carbon
+//
+// AUTOUPDATE
+// TIP: To test this file alone, run:
+// TIP:   bazel test //toolchain/testing:file_test --test_arg=--file_tests=toolchain/check/testdata/impl/fail_todo_form_thunk.carbon
+// TIP: To dump output, run:
+// TIP:   bazel run //toolchain/testing:file_test -- --dump_output --file_tests=toolchain/check/testdata/impl/fail_todo_form_thunk.carbon
+
+interface I {
+  let T:! type;
+  // CHECK:STDERR: fail_todo_form_thunk.carbon:[[@LINE+7]]:8: error: semantics TODO: `Support for cloning form bindings` [SemanticsTodo]
+  // CHECK:STDERR:   fn F(x:? form(ref ()), y: T);
+  // CHECK:STDERR:        ^
+  // CHECK:STDERR:
+  // CHECK:STDERR: fail_todo_form_thunk.carbon:[[@LINE+3]]:8: error: value expression passed to reference parameter [ValueForRefParam]
+  // CHECK:STDERR:   fn F(x:? form(ref ()), y: T);
+  // CHECK:STDERR:        ^~~~~~~~~~~~~~~~
+  fn F(x:? form(ref ()), y: T);
+}
+
+class C {
+  impl as I where .T = () {
+    // CHECK:STDERR: fail_todo_form_thunk.carbon:[[@LINE+7]]:10: note: initializing function parameter [InCallToFunctionParam]
+    // CHECK:STDERR:     fn F(x:? form(ref ()), ());
+    // CHECK:STDERR:          ^~~~~~~~~~~~~~~~
+    // CHECK:STDERR: fail_todo_form_thunk.carbon:[[@LINE-8]]:3: note: while building thunk to match the signature of this function [ThunkSignature]
+    // CHECK:STDERR:   fn F(x:? form(ref ()), y: T);
+    // CHECK:STDERR:   ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+    // CHECK:STDERR:
+    fn F(x:? form(ref ()), ());
+  }
+}

+ 11 - 5
toolchain/check/thunk.cpp

@@ -62,7 +62,12 @@ static auto CloneBindingPattern(Context& context, SemIR::InstId pattern_id,
   auto entity_name = context.entity_names().Get(pattern.entity_name_id);
   CARBON_CHECK((pattern.kind == SemIR::SymbolicBindingPattern::Kind) ==
                entity_name.bind_index().has_value());
-
+  CARBON_CHECK((pattern.kind == SemIR::FormBindingPattern::Kind) ==
+               entity_name.form_id.has_value());
+  if (pattern.kind == SemIR::FormBindingPattern::Kind) {
+    context.TODO(pattern_id, "Support for cloning form bindings");
+    return SemIR::ErrorInst::InstId;
+  }
   // Get the transformed type of the binding.
   if (new_pattern_type_id == SemIR::ErrorInst::TypeId) {
     return SemIR::ErrorInst::InstId;
@@ -75,10 +80,11 @@ static auto CloneBindingPattern(Context& context, SemIR::InstId pattern_id,
       {.block_ids = {SemIR::InstBlockId::Empty}, .result_id = type_inst_id});
 
   // Rebuild the binding pattern.
-  return AddBindingPattern(context, SemIR::LocId(pattern_id),
-                           entity_name.name_id, type_id, type_expr_region_id,
-                           pattern.kind, entity_name.is_template,
-                           /*is_unused=*/false)
+  return AddBindingPattern(
+             context, SemIR::LocId(pattern_id), entity_name.name_id, type_id,
+             /*form_id=*/SemIR::ConstantId::None, type_expr_region_id,
+             pattern.kind, entity_name.is_template,
+             /*is_unused=*/false)
       .pattern_id;
 }
 

+ 5 - 0
toolchain/check/type.cpp

@@ -137,6 +137,11 @@ auto GetConstType(Context& context, SemIR::TypeInstId inner_type_id)
   return GetTypeImpl<SemIR::ConstType>(context, inner_type_id);
 }
 
+auto GetTypeComponent(Context& context, SemIR::InstId form_inst_id)
+    -> SemIR::TypeId {
+  return GetTypeImpl<SemIR::TypeComponentOf>(context, form_inst_id);
+}
+
 auto GetQualifiedType(Context& context, SemIR::TypeId type_id,
                       SemIR::TypeQualifiers quals) -> SemIR::TypeId {
   if (quals.HasAnyOf(SemIR::TypeQualifiers::Const)) {

+ 4 - 0
toolchain/check/type.h

@@ -125,6 +125,10 @@ auto GetTupleType(Context& context, llvm::ArrayRef<SemIR::InstId> type_inst_ids)
 auto GetPatternType(Context& context, SemIR::TypeId scrutinee_type_id)
     -> SemIR::TypeId;
 
+// Returns the type component of the given form value.
+auto GetTypeComponent(Context& context, SemIR::InstId form_inst_id)
+    -> SemIR::TypeId;
+
 // Returns an unbound element type.
 auto GetUnboundElementType(Context& context, SemIR::TypeInstId class_type_id,
                            SemIR::TypeInstId element_type_id) -> SemIR::TypeId;

+ 1 - 0
toolchain/diagnostics/kind.def

@@ -437,6 +437,7 @@ CARBON_DIAGNOSTIC_KIND(DerefOfType)
 CARBON_DIAGNOSTIC_KIND(CompileTimeBindingInVarDecl)
 CARBON_DIAGNOSTIC_KIND(CompoundMemberAccessDoesNotUseBase)
 CARBON_DIAGNOSTIC_KIND(EvalRequiresConstantValue)
+CARBON_DIAGNOSTIC_KIND(FormExprEvaluationFailure)
 CARBON_DIAGNOSTIC_KIND(NameDeclDuplicate)
 CARBON_DIAGNOSTIC_KIND(NameDeclPrevious)
 CARBON_DIAGNOSTIC_KIND(NameUseBeforeDecl)

+ 5 - 0
toolchain/lower/handle.cpp

@@ -119,6 +119,11 @@ auto HandleInst(FunctionContext& context, SemIR::InstId inst_id,
   context.SetLocal(inst_id, context.GetValue(inst.value_id));
 }
 
+auto HandleInst(FunctionContext& context, SemIR::InstId inst_id,
+                SemIR::FormBinding inst) -> void {
+  context.SetLocal(inst_id, context.GetValue(inst.value_id));
+}
+
 auto HandleInst(FunctionContext& context, SemIR::InstId inst_id,
                 SemIR::BlockArg inst) -> void {
   context.SetLocal(

+ 22 - 0
toolchain/parse/testdata/function/declaration.carbon

@@ -167,6 +167,14 @@ fn (a tokens c d e f g h i j k l m n o p q r s t u v w x y z);
 fn FormParam(x:? Form);
 fn ComplexFormParam(x:? X.Y(Z));
 
+// --- fail_ref_on_form_parameter.carbon
+
+// CHECK:STDERR: fail_ref_on_form_parameter.carbon:[[@LINE+4]]:6: error: expected `:` binding after `ref` [ExpectedRuntimeBindingPatternAfterRef]
+// CHECK:STDERR: fn F(ref x:? Form);
+// CHECK:STDERR:      ^~~
+// CHECK:STDERR:
+fn F(ref x:? Form);
+
 // --- return_form.carbon
 
 fn SimpleReturnForm() ->? Form;
@@ -457,6 +465,20 @@ fn ComplexReturnForm() ->? X.Y(Z);
 // CHECK:STDOUT:     {kind: 'FunctionDecl', text: ';', subtree_size: 13},
 // CHECK:STDOUT:     {kind: 'FileEnd', text: ''},
 // CHECK:STDOUT:   ]
+// CHECK:STDOUT: - filename: fail_ref_on_form_parameter.carbon
+// CHECK:STDOUT:   parse_tree: [
+// CHECK:STDOUT:     {kind: 'FileStart', text: ''},
+// CHECK:STDOUT:       {kind: 'FunctionIntroducer', text: 'fn'},
+// CHECK:STDOUT:       {kind: 'IdentifierNameMaybeBeforeSignature', text: 'F'},
+// CHECK:STDOUT:         {kind: 'ExplicitParamListStart', text: '('},
+// CHECK:STDOUT:             {kind: 'IdentifierNameNotBeforeSignature', text: 'x'},
+// CHECK:STDOUT:           {kind: 'RefBindingName', text: 'ref', has_error: yes, subtree_size: 2},
+// CHECK:STDOUT:           {kind: 'IdentifierNameExpr', text: 'Form'},
+// CHECK:STDOUT:         {kind: 'FormBindingPattern', text: ':?', has_error: yes, subtree_size: 4},
+// CHECK:STDOUT:       {kind: 'ExplicitParamList', text: ')', has_error: yes, subtree_size: 6},
+// CHECK:STDOUT:     {kind: 'FunctionDecl', text: ';', subtree_size: 9},
+// CHECK:STDOUT:     {kind: 'FileEnd', text: ''},
+// CHECK:STDOUT:   ]
 // CHECK:STDOUT: - filename: return_form.carbon
 // CHECK:STDOUT:   parse_tree: [
 // CHECK:STDOUT:     {kind: 'FileStart', text: ''},

+ 8 - 1
toolchain/sem_ir/entity_name.h

@@ -17,7 +17,7 @@ struct EntityName : public Printable<EntityName> {
   auto Print(llvm::raw_ostream& out) const -> void {
     out << "{name: " << name_id << ", parent_scope: " << parent_scope_id
         << ", index: " << bind_index_value << ", is_template: " << is_template
-        << ", is_unused: " << is_unused << "}";
+        << ", is_unused: " << is_unused << ", form: " << form_id << "}";
   }
 
   friend auto CarbonHashtableEq(const EntityName& lhs, const EntityName& rhs)
@@ -58,6 +58,13 @@ struct EntityName : public Printable<EntityName> {
   bool is_template : 1 = false;
   // Whether this binding is marked unused.
   bool is_unused : 1 = false;
+
+  // The declared form of the binding. This is guaranteed to be set for
+  // `:?` bindings, and may be set for other binding kinds as well.
+  //
+  // TODO: Unify this with the previous three fields, which also represent form
+  // information.
+  ConstantId form_id = ConstantId::None;
 };
 
 // Value store for EntityName. In addition to the regular ValueStore

+ 19 - 0
toolchain/sem_ir/expr_info.cpp

@@ -91,6 +91,25 @@ static auto GetExprCategoryImpl(const File* ir, InstId inst_id)
             return ExprCategory::ReprInitializing;
           }
         }
+      } else if constexpr (std::same_as<TypedInstT, FormBinding>) {
+        auto form_id = ir->entity_names().Get(inst.entity_name_id).form_id;
+        if (form_id.is_symbolic()) {
+          return ExprCategory::Dependent;
+        }
+        auto form_inst_id = ir->constant_values().GetInstId(form_id);
+        auto form_inst = ir->insts().Get(form_inst_id);
+        CARBON_KIND_SWITCH(form_inst) {
+          case InitForm::Kind:
+            // A `var` binding pattern produces a `ref` binding.
+          case RefForm::Kind:
+            return ExprCategory::DurableRef;
+          case ValueForm::Kind:
+            return ExprCategory::Value;
+          case ErrorInst::Kind:
+            return ExprCategory::Error;
+          default:
+            CARBON_FATAL("Unexpected kind for form inst {0}", form_inst);
+        }
       } else {
         static_assert(
             TypedInstT::Kind.expr_category().TryAsComputedCategory() !=

+ 24 - 8
toolchain/sem_ir/inst_categories.h

@@ -65,8 +65,8 @@ struct AnyAggregateValue {
 // Common representation for various `*binding_pattern` nodes.
 struct AnyBindingPattern {
   // TODO: Also handle TemplateBindingPattern once it exists.
-  using CategoryInfo = CategoryOf<RefBindingPattern, SymbolicBindingPattern,
-                                  ValueBindingPattern>;
+  using CategoryInfo = CategoryOf<FormBindingPattern, RefBindingPattern,
+                                  SymbolicBindingPattern, ValueBindingPattern>;
 
   InstKind kind;
 
@@ -83,8 +83,8 @@ struct AnyBindingPattern {
 // Common representation for various `bind*` nodes.
 struct AnyBinding {
   // TODO: Also handle BindTemplateName once it exists.
-  using CategoryInfo =
-      CategoryOf<AliasBinding, RefBinding, SymbolicBinding, ValueBinding>;
+  using CategoryInfo = CategoryOf<AliasBinding, FormBinding, RefBinding,
+                                  SymbolicBinding, ValueBinding>;
 
   InstKind kind;
   TypeId type_id;
@@ -98,8 +98,8 @@ struct AnyBinding {
 // Common representation for various `bind*` nodes, and `export name`.
 struct AnyBindingOrExportDecl {
   // TODO: Also handle BindTemplateName once it exists.
-  using CategoryInfo = CategoryOf<AliasBinding, RefBinding, SymbolicBinding,
-                                  ValueBinding, ExportDecl>;
+  using CategoryInfo = CategoryOf<AliasBinding, FormBinding, RefBinding,
+                                  SymbolicBinding, ValueBinding, ExportDecl>;
 
   InstKind kind;
   TypeId type_id;
@@ -159,8 +159,9 @@ struct AnyParam {
 // A pattern that represents a `Call` parameter. It delegates to subpattern_id
 // in pattern matching.
 struct AnyParamPattern {
-  using CategoryInfo = CategoryOf<OutParamPattern, RefParamPattern,
-                                  ValueParamPattern, VarParamPattern>;
+  using CategoryInfo =
+      CategoryOf<FormParamPattern, OutParamPattern, RefParamPattern,
+                 ValueParamPattern, VarParamPattern>;
 
   InstKind kind;
 
@@ -171,6 +172,21 @@ struct AnyParamPattern {
   CallParamIndex index;
 };
 
+// An inst that represents a primitive form.
+struct AnyPrimitiveForm {
+  using CategoryInfo = CategoryOf<InitForm, RefForm, ValueForm>;
+
+  InstKind kind;
+
+  // Always FormType.
+  TypeId type_id;
+
+  // The type component of the form.
+  TypeInstId type_component_id;
+
+  AnyRawId arg1;
+};
+
 // A type qualifier that wraps another type and has the same object
 // representation. Qualifiers are arranged so that adding a qualifier is
 // generally safe, and removing a qualifier is not necessarily safe or correct.

+ 5 - 0
toolchain/sem_ir/inst_kind.def

@@ -69,6 +69,9 @@ CARBON_SEM_IR_INST_KIND(FloatLiteralType)
 CARBON_SEM_IR_INST_KIND(FloatLiteralValue)
 CARBON_SEM_IR_INST_KIND(FloatType)
 CARBON_SEM_IR_INST_KIND(FloatValue)
+CARBON_SEM_IR_INST_KIND(FormBinding)
+CARBON_SEM_IR_INST_KIND(FormBindingPattern)
+CARBON_SEM_IR_INST_KIND(FormParamPattern)
 CARBON_SEM_IR_INST_KIND(FormType)
 CARBON_SEM_IR_INST_KIND(FunctionDecl)
 CARBON_SEM_IR_INST_KIND(FunctionType)
@@ -151,6 +154,7 @@ CARBON_SEM_IR_INST_KIND(TupleLiteral)
 CARBON_SEM_IR_INST_KIND(TuplePattern)
 CARBON_SEM_IR_INST_KIND(TupleType)
 CARBON_SEM_IR_INST_KIND(TupleValue)
+CARBON_SEM_IR_INST_KIND(TypeComponentOf)
 CARBON_SEM_IR_INST_KIND(TypeLiteral)
 CARBON_SEM_IR_INST_KIND(TypeOfInst)
 CARBON_SEM_IR_INST_KIND(TypeType)
@@ -160,6 +164,7 @@ CARBON_SEM_IR_INST_KIND(UninitializedValue)
 CARBON_SEM_IR_INST_KIND(ValueAsRef)
 CARBON_SEM_IR_INST_KIND(ValueBinding)
 CARBON_SEM_IR_INST_KIND(ValueBindingPattern)
+CARBON_SEM_IR_INST_KIND(ValueForm)
 CARBON_SEM_IR_INST_KIND(ValueOfInitializer)
 CARBON_SEM_IR_INST_KIND(ValueParam)
 CARBON_SEM_IR_INST_KIND(ValueParamPattern)

+ 5 - 0
toolchain/sem_ir/stringify.cpp

@@ -667,6 +667,11 @@ class Stringifier {
     }
   }
 
+  auto StringifyInst(InstId /*inst_id*/, TypeComponentOf inst) -> void {
+    *out_ << "<type component of form ";
+    step_stack_->Push(inst.form_inst_id, ">");
+  }
+
   auto StringifyInst(InstId inst_id, TypeOfInst /*inst*/) -> void {
     // Print the constant value if we've already computed the inst.
     auto const_inst_id = sem_ir_->constant_values().GetConstantInstId(inst_id);

+ 1 - 0
toolchain/sem_ir/type_iterator.cpp

@@ -98,6 +98,7 @@ auto TypeIterator::ProcessTypeId(TypeId type_id) -> std::optional<Step> {
     case FacetType::Kind:
     case FloatLiteralType::Kind:
     case FloatType::Kind:
+    case FormType::Kind:
     case FunctionType::Kind:
     case FunctionTypeWithSelfType::Kind:
     case GenericClassType::Kind:

+ 75 - 2
toolchain/sem_ir/typed_insts.h

@@ -746,6 +746,51 @@ struct FloatValue {
   FloatId float_id;
 };
 
+// A form binding, such as the `x` declared by `x:? F`. See `AnyBinding` for
+// member documentation.
+struct FormBinding {
+  static constexpr auto Kind =
+      InstKind::FormBinding.Define<Parse::FormBindingPatternId>(
+          {.ir_name = "form_binding",
+           .expr_category = ComputedExprCategory::DependsOnOperands,
+           .constant_kind = InstConstantKind::Never});
+
+  TypeId type_id;
+  EntityNameId entity_name_id;
+  InstId value_id;
+};
+
+// A form binding pattern, such as `x:? F`. See `AnyBindingPattern` for member
+// documentation.
+struct FormBindingPattern {
+  static constexpr auto Kind =
+      InstKind::FormBindingPattern.Define<Parse::FormBindingPatternId>(
+          {.ir_name = "form_binding_pattern",
+           .expr_category = ExprCategory::Pattern,
+           .constant_kind = InstConstantKind::AlwaysUnique,
+           .is_lowered = false});
+
+  TypeId type_id;
+  // Note that the EntityName's `form_id` represents the scrutinee form, so it
+  // doesn't directly correspond to `type_id` (which is a pattern type).
+  EntityNameId entity_name_id;
+};
+
+// A pattern that represents a form-parameterized parameter, such as `x:? F`.
+// See `AnyParamPattern` for member documentation.
+struct FormParamPattern {
+  // TODO: Replace `Parse::NodeId` with `Parse::FormBindingPattern`.
+  static constexpr auto Kind = InstKind::FormParamPattern.Define<Parse::NodeId>(
+      {.ir_name = "form_param_pattern",
+       .expr_category = ExprCategory::Pattern,
+       .constant_kind = InstConstantKind::AlwaysUnique,
+       .is_lowered = false});
+
+  TypeId type_id;
+  InstId subpattern_id;
+  CallParamIndex index;
+};
+
 // The type `Core.Form`.
 struct FormType : public SingletonTypeInst<InstKind::FormType, "Core.Form"> {
   // `FormType` is always set complete in file.cpp.
@@ -1395,7 +1440,8 @@ struct RefBindingPattern {
 
 struct RefForm {
   static constexpr auto Kind =
-      InstKind::RefForm.Define<Parse::PrefixOperatorRefId>(
+      InstKind::RefForm.Define<Parse::NodeIdOneOf<Parse::PrefixOperatorRefId,
+                                                  Parse::RefPrimitiveFormId>>(
           {.ir_name = "ref_form",
            .constant_kind = InstConstantKind::Always,
            .is_lowered = false});
@@ -1726,7 +1772,7 @@ struct SpliceInst {
   static constexpr auto Kind = InstKind::SpliceInst.Define<Parse::NodeId>(
       {.ir_name = "splice_inst",
        // TODO: The expression category is in general dependent on
-       // instantiation. Add ExprCategory::Dependent to model this.
+       // instantiation. Use ExprCategory::Dependent to model this.
        .expr_category = ExprCategory::Value});
 
   TypeId type_id;
@@ -1955,6 +2001,21 @@ struct TupleValue {
   InstBlockId elements_id;
 };
 
+// Extracts the type component of `form_inst_id`, which must have type
+// `Core.Form`.
+struct TypeComponentOf {
+  static constexpr auto Kind =
+      InstKind::TypeComponentOf.Define<Parse::NoneNodeId>(
+          {.ir_name = "type_component_of",
+           .is_type = InstIsType::Always,
+           .constant_kind = InstConstantKind::SymbolicOnly,
+           .is_lowered = false});
+
+  // Always TypeType.
+  TypeId type_id;
+  InstId form_inst_id;
+};
+
 // A type literal, such as `bool` or `type` or `i32`. The constant value of this
 // instruction will be the type value that the type literal evaluates to, which
 // is typically either a builtin type or a class defined in the prelude.
@@ -2072,6 +2133,18 @@ struct ValueBindingPattern {
   EntityNameId entity_name_id;
 };
 
+// A primitive value form.
+struct ValueForm {
+  static constexpr auto Kind =
+      InstKind::ValueForm.Define<Parse::ValPrimitiveFormId>(
+          {.ir_name = "value_form",
+           .constant_kind = InstConstantKind::Always,
+           .is_lowered = false});
+
+  TypeId type_id;
+  TypeInstId type_component_inst_id;
+};
+
 // Converts an initializing expression to a value expression, in the case
 // where the initializing representation is the same as the value
 // representation.