Эх сурвалжийг харах

Support `ref` tags on arguments to `ref` params (#6312)

The issue of whether/how to include `ref` tags in the textual and
in-memory SemIR (see discussion
[here](https://discord.com/channels/655572317891461132/655578254970716160/1431316355742961805))
is left as future work.

---------

Co-authored-by: Richard Smith <richard@metafoo.co.uk>
Geoff Romer 5 сар өмнө
parent
commit
43ffd721a4
39 өөрчлөгдсөн 798 нэмэгдсэн , 337 устгасан
  1. 2 2
      toolchain/check/call.cpp
  2. 14 0
      toolchain/check/context.cpp
  3. 14 0
      toolchain/check/context.h
  4. 47 1
      toolchain/check/convert.cpp
  5. 5 0
      toolchain/check/convert.h
  6. 16 6
      toolchain/check/handle_call_expr.cpp
  7. 1 1
      toolchain/check/handle_let_and_var.cpp
  8. 3 0
      toolchain/check/import_ref.cpp
  9. 2 1
      toolchain/check/merge.cpp
  10. 0 1
      toolchain/check/node_stack.h
  11. 7 0
      toolchain/check/operator.cpp
  12. 23 28
      toolchain/check/pattern_match.cpp
  13. 1 2
      toolchain/check/testdata/class/fail_ref_self.carbon
  14. 253 0
      toolchain/check/testdata/function/call/ref.carbon
  15. 3 3
      toolchain/check/testdata/impl/lookup/specialization_with_symbolic_rewrite.carbon
  16. 0 206
      toolchain/check/testdata/let/ref.carbon
  17. 4 4
      toolchain/check/testdata/var/var_pattern.carbon
  18. 1 2
      toolchain/check/thunk.cpp
  19. 4 0
      toolchain/diagnostics/diagnostic_kind.def
  20. 2 1
      toolchain/lower/file_context.cpp
  21. 1 1
      toolchain/lower/testdata/function/call/ref_param.carbon
  22. 5 0
      toolchain/parse/handle_brace_expr.cpp
  23. 0 42
      toolchain/parse/handle_call_expr.cpp
  24. 54 5
      toolchain/parse/handle_paren_expr.cpp
  25. 2 1
      toolchain/parse/node_kind.def
  26. 51 16
      toolchain/parse/state.def
  27. 1 1
      toolchain/parse/testdata/basics/function_call.carbon
  28. 201 0
      toolchain/parse/testdata/function/call.carbon
  29. 1 1
      toolchain/parse/testdata/function/definition.carbon
  30. 1 1
      toolchain/parse/testdata/operators/fail_postfix_space_before_comma.carbon
  31. 1 1
      toolchain/parse/testdata/operators/fixity_in_call.carbon
  32. 10 3
      toolchain/parse/typed_nodes.h
  33. 1 0
      toolchain/sem_ir/expr_info.cpp
  34. 37 0
      toolchain/sem_ir/file.cpp
  35. 6 0
      toolchain/sem_ir/file.h
  36. 3 4
      toolchain/sem_ir/inst_categories.h
  37. 1 0
      toolchain/sem_ir/inst_kind.def
  38. 2 1
      toolchain/sem_ir/inst_namer.cpp
  39. 18 2
      toolchain/sem_ir/typed_insts.h

+ 2 - 2
toolchain/check/call.cpp

@@ -300,8 +300,6 @@ auto PerformCallToFunction(Context& context, SemIR::LocId loc_id,
     }
 
     case SemIR::Function::SpecialFunctionKind::HasCppThunk: {
-      // This recurses back into `PerformCall`. However, we never form a C++
-      // thunk to a C++ thunk, so we only recurse once.
       return PerformCppThunkCall(context, loc_id, callee_function.function_id,
                                  context.inst_blocks().Get(converted_args_id),
                                  callee.cpp_thunk_decl_id());
@@ -359,6 +357,7 @@ auto PerformCall(Context& context, SemIR::LocId loc_id, SemIR::InstId callee_id,
       return SemIR::ErrorInst::InstId;
     }
     case CARBON_KIND(SemIR::CalleeFunction fn): {
+      context.ref_tags().Insert(fn.self_id, Context::RefTag::NotRequired);
       return PerformCallToFunction(context, loc_id, callee_id, fn, arg_ids);
     }
     case CARBON_KIND(SemIR::CalleeNonFunction _): {
@@ -366,6 +365,7 @@ auto PerformCall(Context& context, SemIR::LocId loc_id, SemIR::InstId callee_id,
     }
 
     case CARBON_KIND(SemIR::CalleeCppOverloadSet overload): {
+      context.ref_tags().Insert(overload.self_id, Context::RefTag::NotRequired);
       return PerformCallToCppFunction(context, loc_id,
                                       overload.cpp_overload_set_id,
                                       overload.self_id, arg_ids);

+ 14 - 0
toolchain/check/context.cpp

@@ -8,6 +8,7 @@
 #include <utility>
 
 #include "common/check.h"
+#include "toolchain/base/kind_switch.h"
 #include "toolchain/check/deferred_definition_worklist.h"
 #include "toolchain/sem_ir/ids.h"
 
@@ -77,6 +78,19 @@ auto Context::VerifyOnFinish() const -> void {
     CARBON_FATAL("{0}Built invalid semantics IR: {1}\n", sem_ir_,
                  verify.error());
   }
+
+  if (!sem_ir_->has_errors()) {
+    auto ref_tags_needed = sem_ir_->CollectRefTagsNeeded();
+
+    ref_tags_.ForEach([&ref_tags_needed](SemIR::InstId inst_id, RefTag kind) {
+      CARBON_CHECK(
+          ref_tags_needed.Erase(inst_id) || kind == RefTag::NotRequired,
+          "Inst has unnecessary `ref` tag: {0}", inst_id);
+    });
+    ref_tags_needed.ForEach([this](SemIR::InstId inst_id) {
+      CARBON_FATAL("Inst missing `ref` tag: {0}", insts().Get(inst_id));
+    });
+  }
 #endif
 }
 

+ 14 - 0
toolchain/check/context.h

@@ -193,6 +193,13 @@ class Context {
     return var_storage_map_;
   }
 
+  enum class RefTag { Present, NotRequired };
+
+  auto ref_tags() -> Map<SemIR::InstId, RefTag>& { return ref_tags_; }
+  auto ref_tags() const -> const Map<SemIR::InstId, RefTag>& {
+    return ref_tags_;
+  }
+
   // During Choice typechecking, each alternative turns into a name binding on
   // the Choice type, but this can't be done until the full Choice type is
   // known. This represents each binding to be done at the end of checking the
@@ -445,6 +452,13 @@ class Context {
   // processing the enclosing full-pattern.
   Map<SemIR::InstId, SemIR::InstId> var_storage_map_;
 
+  // Insts in this map are syntactically permitted to be bound to a reference
+  // parameter, either because they've been explicitly tagged with `ref` in the
+  // source code, or because they appear in a position where that tag is not
+  // required, such as an operator operand (the RefTag value indicates which
+  // of those is the case).
+  Map<SemIR::InstId, RefTag> ref_tags_;
+
   // Each alternative in a Choice gets an entry here, they are stored in
   // declaration order. The vector is consumed and emptied at the end of the
   // Choice definition.

+ 47 - 1
toolchain/check/convert.cpp

@@ -738,6 +738,10 @@ static auto IsValidExprCategoryForConversionTarget(
              category == SemIR::ExprCategory::DurableRef ||
              category == SemIR::ExprCategory::EphemeralRef ||
              category == SemIR::ExprCategory::Initializing;
+    case ConversionTarget::RefParam:
+      return category == SemIR::ExprCategory::DurableRef ||
+             category == SemIR::ExprCategory::EphemeralRef ||
+             category == SemIR::ExprCategory::Initializing;
     case ConversionTarget::DurableRef:
       return category == SemIR::ExprCategory::DurableRef;
     case ConversionTarget::CppThunkRef:
@@ -1381,6 +1385,28 @@ auto PerformAction(Context& context, SemIR::LocId loc_id,
                       action.target_type_inst_id)});
 }
 
+// Diagnoses a missing or unnecessary `ref` tag when converting `expr_id` to
+// `target`, and returns whether a `ref` tag is present.
+static auto CheckRefTag(Context& context, SemIR::InstId expr_id,
+                        ConversionTarget target) -> bool {
+  if (auto lookup_result = context.ref_tags().Lookup(expr_id)) {
+    if (lookup_result.value() == Context::RefTag::Present &&
+        target.kind != ConversionTarget::RefParam) {
+      CARBON_DIAGNOSTIC(RefTagNoRefParam, Error,
+                        "`ref` tag is not an argument to a `ref` parameter");
+      context.emitter().Emit(expr_id, RefTagNoRefParam);
+    }
+    return true;
+  } else {
+    if (target.kind == ConversionTarget::RefParam) {
+      CARBON_DIAGNOSTIC(RefParamNoRefTag, Error,
+                        "argument to `ref` parameter not marked with `ref`");
+      context.emitter().Emit(expr_id, RefParamNoRefTag);
+    }
+    return false;
+  }
+}
+
 auto Convert(Context& context, SemIR::LocId loc_id, SemIR::InstId expr_id,
              ConversionTarget target, SemIR::ClassType* vtable_class_type)
     -> SemIR::InstId {
@@ -1406,6 +1432,8 @@ auto Convert(Context& context, SemIR::LocId loc_id, SemIR::InstId expr_id,
     return SemIR::ErrorInst::InstId;
   }
 
+  bool has_ref_tag = CheckRefTag(context, expr_id, target);
+
   // We can only perform initialization for complete, non-abstract types. Note
   // that `RequireConcreteType` returns true for facet types, since their
   // representation is fixed. This allows us to support using the `Self` of an
@@ -1538,6 +1566,9 @@ auto Convert(Context& context, SemIR::LocId loc_id, SemIR::InstId expr_id,
                                         {.type_id = target.type_id,
                                          .original_id = orig_expr_id,
                                          .result_id = expr_id});
+    if (has_ref_tag) {
+      context.ref_tags().Insert(expr_id, Context::RefTag::NotRequired);
+    }
   }
 
   // For `as`, don't perform any value category conversions. In particular, an
@@ -1592,7 +1623,8 @@ auto Convert(Context& context, SemIR::LocId loc_id, SemIR::InstId expr_id,
       // If a reference expression is an acceptable result, we're done.
       if (target.kind == ConversionTarget::ValueOrRef ||
           target.kind == ConversionTarget::Discarded ||
-          target.kind == ConversionTarget::CppThunkRef) {
+          target.kind == ConversionTarget::CppThunkRef ||
+          target.kind == ConversionTarget::RefParam) {
         break;
       }
 
@@ -1617,6 +1649,20 @@ auto Convert(Context& context, SemIR::LocId loc_id, SemIR::InstId expr_id,
         }
         return SemIR::ErrorInst::InstId;
       }
+      if (target.kind == ConversionTarget::RefParam) {
+        // Don't diagnose a non-reference scrutinee if it has a user-written
+        // `ref` tag, because that's diagnosed in `CheckRefTag`.
+        if (target.diagnose) {
+          if (auto lookup_result = context.ref_tags().Lookup(expr_id);
+              !lookup_result ||
+              lookup_result.value() != Context::RefTag::Present) {
+            CARBON_DIAGNOSTIC(ValueForRefParam, Error,
+                              "value expression passed to reference parameter");
+            context.emitter().Emit(loc_id, ValueForRefParam);
+          }
+        }
+        return SemIR::ErrorInst::InstId;
+      }
 
       // When initializing from a value, perform a copy.
       if (target.is_initializer()) {

+ 5 - 0
toolchain/check/convert.h

@@ -21,6 +21,11 @@ struct ConversionTarget {
     ValueOrRef,
     // Convert to a durable reference of type `type_id`.
     DurableRef,
+    // Convert to a reference, suitable for binding to a reference parameter.
+    // This allows both durable and ephemeral references. The restriction that
+    // only a `ref self` parameter can bind to an ephemeral reference is
+    // enforced separately when handling `ref` tags on call arguments.
+    RefParam,
     // Convert to a reference of type `type_id`, for use as the argument to a
     // C++ thunk.
     CppThunkRef,

+ 16 - 6
toolchain/check/handle_call_expr.cpp

@@ -5,6 +5,8 @@
 #include "toolchain/check/call.h"
 #include "toolchain/check/context.h"
 #include "toolchain/check/handle.h"
+#include "toolchain/check/inst.h"
+#include "toolchain/sem_ir/expr_info.h"
 #include "toolchain/sem_ir/inst.h"
 
 namespace Carbon::Check {
@@ -16,12 +18,6 @@ auto HandleParseNode(Context& context, Parse::CallExprStartId node_id) -> bool {
   return true;
 }
 
-auto HandleParseNode(Context& context, Parse::CallExprCommaId /*node_id*/)
-    -> bool {
-  context.param_and_arg_refs_stack().ApplyComma();
-  return true;
-}
-
 auto HandleParseNode(Context& context, Parse::CallExprId node_id) -> bool {
   // Process the final explicit call argument now, but leave the arguments
   // block on the stack until the end of this function.
@@ -37,4 +33,18 @@ auto HandleParseNode(Context& context, Parse::CallExprId node_id) -> bool {
   return true;
 }
 
+auto HandleParseNode(Context& context, Parse::RefTagId node_id) -> bool {
+  auto expr_id = context.node_stack().Peek<Parse::NodeCategory::Expr>();
+
+  if (SemIR::GetExprCategory(context.sem_ir(), expr_id) !=
+      SemIR::ExprCategory::DurableRef) {
+    CARBON_DIAGNOSTIC(
+        RefTagNotDurableRef, Error,
+        "expression tagged with `ref` is not a durable reference");
+    context.emitter().Emit(node_id, RefTagNotDurableRef);
+  }
+  context.ref_tags().Insert(expr_id, Context::RefTag::Present);
+  return true;
+}
+
 }  // namespace Carbon::Check

+ 1 - 1
toolchain/check/handle_let_and_var.cpp

@@ -127,7 +127,7 @@ auto HandleParseNode(Context& context, Parse::VariablePatternId node_id)
   switch (context.full_pattern_stack().CurrentKind()) {
     case FullPatternStack::Kind::ExplicitParamList:
     case FullPatternStack::Kind::ImplicitParamList:
-      subpattern_id = AddPatternInst<SemIR::RefParamPattern>(
+      subpattern_id = AddPatternInst<SemIR::VarParamPattern>(
           context, node_id,
           {.type_id = type_id,
            .subpattern_id = subpattern_id,

+ 3 - 0
toolchain/check/import_ref.cpp

@@ -3570,6 +3570,9 @@ static auto TryResolveInstCanonical(ImportRefResolver& resolver,
     case CARBON_KIND(SemIR::ValueParamPattern inst): {
       return TryResolveTypedInst(resolver, inst, constant_inst_id);
     }
+    case CARBON_KIND(SemIR::VarParamPattern inst): {
+      return TryResolveTypedInst(resolver, inst, constant_inst_id);
+    }
     case CARBON_KIND(SemIR::VarPattern inst): {
       return TryResolveTypedInst(resolver, inst, constant_inst_id);
     }

+ 2 - 1
toolchain/check/merge.cpp

@@ -257,7 +257,8 @@ static auto CheckRedeclParam(Context& context, bool is_implicit_param,
       }
       case SemIR::OutParamPattern::Kind:
       case SemIR::RefParamPattern::Kind:
-      case SemIR::ValueParamPattern::Kind: {
+      case SemIR::ValueParamPattern::Kind:
+      case SemIR::VarParamPattern::Kind: {
         pattern_stack.push_back(
             {.prev_id =
                  prev_param_pattern.As<SemIR::AnyParamPattern>().subpattern_id,

+ 0 - 1
toolchain/check/node_stack.h

@@ -485,7 +485,6 @@ class NodeStack {
       case Parse::NodeKind::BaseColon:
       case Parse::NodeKind::BaseIntroducer:
       case Parse::NodeKind::BreakStatementStart:
-      case Parse::NodeKind::CallExprComma:
       case Parse::NodeKind::ChoiceAlternativeListComma:
       case Parse::NodeKind::CodeBlock:
       case Parse::NodeKind::CompileTimeBindingPatternStart:

+ 7 - 0
toolchain/check/operator.cpp

@@ -64,6 +64,9 @@ auto BuildUnaryOperator(Context& context, SemIR::LocId loc_id, Operator op,
     return SemIR::ErrorInst::InstId;
   }
 
+  // Operator operands don't require `ref` tags.
+  context.ref_tags().Insert(operand_id, Context::RefTag::NotRequired);
+
   // For unary operators with a C++ class as the operand, try to import and call
   // the C++ operator.
   // TODO: Change impl lookup instead. See
@@ -102,6 +105,10 @@ auto BuildBinaryOperator(Context& context, SemIR::LocId loc_id, Operator op,
     return SemIR::ErrorInst::InstId;
   }
 
+  // Operator operands don't require `ref` tags.
+  context.ref_tags().Insert(lhs_id, Context::RefTag::NotRequired);
+  context.ref_tags().Insert(rhs_id, Context::RefTag::NotRequired);
+
   // For binary operators with a C++ class as at least one of the operands, try
   // to import and call the C++ operator.
   // TODO: Instead of hooking this here, change impl lookup, so that a generic

+ 23 - 28
toolchain/check/pattern_match.cpp

@@ -103,9 +103,11 @@ class MatchContext {
   auto DoEmitPatternMatch(Context& context,
                           SemIR::ValueParamPattern param_pattern,
                           WorkItem entry) -> void;
-  auto DoEmitPatternMatch(Context& context,
-                          SemIR::RefParamPattern 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)
       -> void;
@@ -337,8 +339,11 @@ auto MatchContext::DoEmitPatternMatch(Context& context,
   }
 }
 
+template <typename RefParamPatternT>
+  requires std::is_same_v<RefParamPatternT, SemIR::RefParamPattern> ||
+           std::is_same_v<RefParamPatternT, SemIR::VarParamPattern>
 auto MatchContext::DoEmitPatternMatch(Context& context,
-                                      SemIR::RefParamPattern param_pattern,
+                                      RefParamPatternT param_pattern,
                                       WorkItem entry) -> void {
   switch (kind_) {
     case MatchKind::Caller: {
@@ -348,31 +353,17 @@ auto MatchContext::DoEmitPatternMatch(Context& context,
           param_pattern.index.index);
       CARBON_CHECK(entry.scrutinee_id.has_value());
 
-      // TODO: If this is a `ref` pattern and !entry.is_self, require the
-      // scrutinee to have a `ref` tag.
-
-      auto scrutinee_ref_id = ConvertToValueOrRefOfType(
-          context, SemIR::LocId(entry.scrutinee_id), entry.scrutinee_id,
-          ExtractScrutineeType(
-              context.sem_ir(),
-              SemIR::GetTypeOfInstInSpecific(
-                  context.sem_ir(), callee_specific_id_, entry.pattern_id)));
-
-      switch (SemIR::GetExprCategory(context.sem_ir(), scrutinee_ref_id)) {
-        case SemIR::ExprCategory::Error:
-        case SemIR::ExprCategory::DurableRef:
-        case SemIR::ExprCategory::EphemeralRef:
-          break;
-        default:
-          CARBON_DIAGNOSTIC(ValueForRefParam, Error,
-                            "value expression passed to reference parameter");
-          context.emitter().Emit(entry.scrutinee_id, ValueForRefParam);
-          // Add fake reference expression to preserve invariants.
-          auto scrutinee = context.insts().GetWithLocId(entry.scrutinee_id);
-          scrutinee_ref_id = AddInst<SemIR::TemporaryStorage>(
-              context, scrutinee.loc_id, {.type_id = scrutinee.inst.type_id()});
+      if (std::is_same_v<RefParamPatternT, SemIR::VarParamPattern>) {
+        results_.push_back(entry.scrutinee_id);
+        break;
       }
-      results_.push_back(scrutinee_ref_id);
+      auto scrutinee_type_id = ExtractScrutineeType(
+          context.sem_ir(),
+          SemIR::GetTypeOfInstInSpecific(context.sem_ir(), callee_specific_id_,
+                                         entry.pattern_id));
+      results_.push_back(Convert(
+          context, SemIR::LocId(entry.scrutinee_id), entry.scrutinee_id,
+          {.kind = ConversionTarget::RefParam, .type_id = scrutinee_type_id}));
       // Do not traverse farther, because the caller side of the pattern
       // ends here.
       break;
@@ -627,6 +618,10 @@ auto MatchContext::EmitPatternMatch(Context& context,
       DoEmitPatternMatch(context, param_pattern, entry);
       break;
     }
+    case CARBON_KIND(SemIR::VarParamPattern param_pattern): {
+      DoEmitPatternMatch(context, param_pattern, entry);
+      break;
+    }
     case CARBON_KIND(SemIR::OutParamPattern param_pattern): {
       DoEmitPatternMatch(context, param_pattern, entry);
       break;

+ 1 - 2
toolchain/check/testdata/class/fail_ref_self.carbon

@@ -129,8 +129,7 @@ fn F(c: Class, p: Class*) {
 // CHECK:STDOUT:   %c.ref: %Class = name_ref c, %c
 // CHECK:STDOUT:   %F.ref.loc29: %Class.F.type = name_ref F, @Class.%Class.F.decl [concrete = constants.%Class.F]
 // CHECK:STDOUT:   %Class.F.bound.loc29: <bound method> = bound_method %c.ref, %F.ref.loc29
-// CHECK:STDOUT:   %.loc29: ref %Class = temporary_storage
-// CHECK:STDOUT:   %Class.F.call.loc29: init %empty_tuple.type = call %Class.F.bound.loc29(%.loc29)
+// CHECK:STDOUT:   %Class.F.call.loc29: init %empty_tuple.type = call %Class.F.bound.loc29(<error>)
 // CHECK:STDOUT:   %p.ref: %ptr.e71 = name_ref p, %p
 // CHECK:STDOUT:   %.loc32: ref %Class = deref %p.ref
 // CHECK:STDOUT:   %F.ref.loc32: %Class.F.type = name_ref F, @Class.%Class.F.decl [concrete = constants.%Class.F]

+ 253 - 0
toolchain/check/testdata/function/call/ref.carbon

@@ -0,0 +1,253 @@
+// 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
+// EXTRA-ARGS: --dump-sem-ir-ranges=only
+//
+// AUTOUPDATE
+// TIP: To test this file alone, run:
+// TIP:   bazel test //toolchain/testing:file_test --test_arg=--file_tests=toolchain/check/testdata/function/call/ref.carbon
+// TIP: To dump output, run:
+// TIP:   bazel run //toolchain/testing:file_test -- --dump_output --file_tests=toolchain/check/testdata/function/call/ref.carbon
+
+// --- basics.carbon
+
+library "[[@TEST_NAME]]";
+
+//@dump-sem-ir-begin
+fn F(ref x: i32);
+
+fn G() {
+  var y: i32 = 0;
+
+  F(ref y);
+}
+//@dump-sem-ir-end
+
+// --- class.carbon
+
+library "[[@TEST_NAME]]";
+
+//@dump-sem-ir-begin
+class C {
+  fn F[ref self: Self]();
+}
+
+fn G() {
+  var c: C;
+
+  c.F();
+}
+//@dump-sem-ir-end
+
+// --- fail_ref_not_durable_ref.carbon
+
+library "[[@TEST_NAME]]";
+
+fn F(ref x: i32);
+
+fn G() {
+  let y: i32 = 0;
+  // CHECK:STDERR: fail_ref_not_durable_ref.carbon:[[@LINE+4]]:5: error: expression tagged with `ref` is not a durable reference [RefTagNotDurableRef]
+  // CHECK:STDERR:   F(ref y);
+  // CHECK:STDERR:     ^~~~~
+  // CHECK:STDERR:
+  F(ref y);
+}
+
+// --- fail_missing_ref.carbon
+
+library "[[@TEST_NAME]]";
+
+fn F(ref x: i32);
+
+fn G() {
+  var y: i32 = 0;
+  // CHECK:STDERR: fail_missing_ref.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.carbon:[[@LINE-7]]:6: note: initializing function parameter [InCallToFunctionParam]
+  // CHECK:STDERR: fn F(ref x: i32);
+  // CHECK:STDERR:      ^~~~~~~~~~
+  // CHECK:STDERR:
+  F(y);
+}
+
+// --- fail_unnecessary_ref.carbon
+
+library "[[@TEST_NAME]]";
+
+fn F(x: i32);
+
+fn G() {
+  var y: i32 = 0;
+
+  // CHECK:STDERR: fail_unnecessary_ref.carbon:[[@LINE+4]]:24: error: `ref` tag is not an argument to a `ref` parameter [RefTagNoRefParam]
+  // CHECK:STDERR:   var z: (i32,) = (ref y,);
+  // CHECK:STDERR:                        ^
+  // CHECK:STDERR:
+  var z: (i32,) = (ref y,);
+  // CHECK:STDERR: fail_unnecessary_ref.carbon:[[@LINE+7]]:9: error: `ref` tag is not an argument to a `ref` parameter [RefTagNoRefParam]
+  // CHECK:STDERR:   F(ref y);
+  // CHECK:STDERR:         ^
+  // CHECK:STDERR: fail_unnecessary_ref.carbon:[[@LINE-13]]:6: note: initializing function parameter [InCallToFunctionParam]
+  // CHECK:STDERR: fn F(x: i32);
+  // CHECK:STDERR:      ^~~~~~
+  // CHECK:STDERR:
+  F(ref y);
+}
+
+// CHECK:STDOUT: --- basics.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:   %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:   %G.type: type = fn_type @G [concrete]
+// CHECK:STDOUT:   %G: %G.type = struct_value () [concrete]
+// CHECK:STDOUT:   %int_0.5c6: Core.IntLiteral = int_value 0 [concrete]
+// CHECK:STDOUT:   %ImplicitAs.type.d14: type = facet_type <@ImplicitAs, @ImplicitAs(%i32)> [concrete]
+// CHECK:STDOUT:   %ImplicitAs.Convert.type.1b6: type = fn_type @ImplicitAs.Convert, @ImplicitAs(%i32) [concrete]
+// CHECK:STDOUT:   %To: Core.IntLiteral = symbolic_binding To, 0 [symbolic]
+// CHECK:STDOUT:   %Core.IntLiteral.as.ImplicitAs.impl.Convert.type.f51: type = fn_type @Core.IntLiteral.as.ImplicitAs.impl.Convert, @Core.IntLiteral.as.ImplicitAs.impl(%To) [symbolic]
+// CHECK:STDOUT:   %Core.IntLiteral.as.ImplicitAs.impl.Convert.2a1: %Core.IntLiteral.as.ImplicitAs.impl.Convert.type.f51 = struct_value () [symbolic]
+// CHECK:STDOUT:   %ImplicitAs.impl_witness.bc9: <witness> = impl_witness imports.%ImplicitAs.impl_witness_table.132, @Core.IntLiteral.as.ImplicitAs.impl(%int_32) [concrete]
+// CHECK:STDOUT:   %Core.IntLiteral.as.ImplicitAs.impl.Convert.type.51e: 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.e9b: %Core.IntLiteral.as.ImplicitAs.impl.Convert.type.51e = struct_value () [concrete]
+// CHECK:STDOUT:   %ImplicitAs.facet: %ImplicitAs.type.d14 = facet_value Core.IntLiteral, (%ImplicitAs.impl_witness.bc9) [concrete]
+// CHECK:STDOUT:   %.322: type = fn_type_with_self_type %ImplicitAs.Convert.type.1b6, %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.e9b [concrete]
+// CHECK:STDOUT:   %Core.IntLiteral.as.ImplicitAs.impl.Convert.specific_fn: <specific function> = specific_function %Core.IntLiteral.as.ImplicitAs.impl.Convert.e9b, @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:   %type_where: type = facet_type <type where .Self impls <CanDestroy>> [concrete]
+// CHECK:STDOUT:   %facet_value: %type_where = facet_value %i32, () [concrete]
+// CHECK:STDOUT:   %DestroyT.binding.as_type.as.Destroy.impl.Op.type.d4e: type = fn_type @DestroyT.binding.as_type.as.Destroy.impl.Op, @DestroyT.binding.as_type.as.Destroy.impl(%facet_value) [concrete]
+// CHECK:STDOUT:   %DestroyT.binding.as_type.as.Destroy.impl.Op.424: %DestroyT.binding.as_type.as.Destroy.impl.Op.type.d4e = struct_value () [concrete]
+// CHECK:STDOUT:   %ptr.235: type = ptr_type %i32 [concrete]
+// CHECK:STDOUT: }
+// CHECK:STDOUT:
+// CHECK:STDOUT: imports {
+// CHECK:STDOUT:   %Core.import_ref.e24: @Core.IntLiteral.as.ImplicitAs.impl.%Core.IntLiteral.as.ImplicitAs.impl.Convert.type (%Core.IntLiteral.as.ImplicitAs.impl.Convert.type.f51) = 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.2a1)]
+// CHECK:STDOUT:   %ImplicitAs.impl_witness_table.132 = impl_witness_table (%Core.import_ref.e24), @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:     %x.patt: %pattern_type.7ce = ref_binding_pattern x [concrete]
+// CHECK:STDOUT:     %x.param_patt: %pattern_type.7ce = ref_param_pattern %x.patt, call_param0 [concrete]
+// CHECK:STDOUT:   } {
+// CHECK:STDOUT:     %x.param: ref %i32 = ref_param call_param0
+// CHECK:STDOUT:     %.loc5: type = splice_block %i32 [concrete = constants.%i32] {
+// CHECK:STDOUT:       %int_32: Core.IntLiteral = int_value 32 [concrete = constants.%int_32]
+// CHECK:STDOUT:       %i32: type = class_type @Int, @Int(constants.%int_32) [concrete = constants.%i32]
+// CHECK:STDOUT:     }
+// CHECK:STDOUT:     %x: ref %i32 = ref_binding x, %x.param
+// CHECK:STDOUT:   }
+// CHECK:STDOUT:   %G.decl: %G.type = fn_decl @G [concrete = constants.%G] {} {}
+// CHECK:STDOUT: }
+// CHECK:STDOUT:
+// CHECK:STDOUT: fn @F(%x.param: %i32);
+// CHECK:STDOUT:
+// CHECK:STDOUT: fn @G() {
+// CHECK:STDOUT: !entry:
+// CHECK:STDOUT:   name_binding_decl {
+// CHECK:STDOUT:     %y.patt: %pattern_type.7ce = ref_binding_pattern y [concrete]
+// CHECK:STDOUT:     %y.var_patt: %pattern_type.7ce = var_pattern %y.patt [concrete]
+// CHECK:STDOUT:   }
+// CHECK:STDOUT:   %y.var: ref %i32 = var %y.var_patt
+// CHECK:STDOUT:   %int_0: Core.IntLiteral = int_value 0 [concrete = constants.%int_0.5c6]
+// CHECK:STDOUT:   %impl.elem0: %.322 = impl_witness_access constants.%ImplicitAs.impl_witness.bc9, element0 [concrete = constants.%Core.IntLiteral.as.ImplicitAs.impl.Convert.e9b]
+// CHECK:STDOUT:   %bound_method.loc8_3.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.loc8_3.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.loc8_3.2(%int_0) [concrete = constants.%int_0.6a9]
+// CHECK:STDOUT:   %.loc8_3: init %i32 = converted %int_0, %Core.IntLiteral.as.ImplicitAs.impl.Convert.call [concrete = constants.%int_0.6a9]
+// CHECK:STDOUT:   assign %y.var, %.loc8_3
+// CHECK:STDOUT:   %.loc8_10: type = splice_block %i32 [concrete = constants.%i32] {
+// CHECK:STDOUT:     %int_32: Core.IntLiteral = int_value 32 [concrete = constants.%int_32]
+// CHECK:STDOUT:     %i32: type = class_type @Int, @Int(constants.%int_32) [concrete = constants.%i32]
+// CHECK:STDOUT:   }
+// CHECK:STDOUT:   %y: ref %i32 = ref_binding y, %y.var
+// 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:   %F.call: init %empty_tuple.type = call %F.ref(%y.ref)
+// CHECK:STDOUT:   %DestroyT.binding.as_type.as.Destroy.impl.Op.bound: <bound method> = bound_method %y.var, constants.%DestroyT.binding.as_type.as.Destroy.impl.Op.424
+// CHECK:STDOUT:   <elided>
+// CHECK:STDOUT:   %bound_method.loc8_3.3: <bound method> = bound_method %y.var, %DestroyT.binding.as_type.as.Destroy.impl.Op.specific_fn
+// CHECK:STDOUT:   %addr: %ptr.235 = addr_of %y.var
+// CHECK:STDOUT:   %DestroyT.binding.as_type.as.Destroy.impl.Op.call: init %empty_tuple.type = call %bound_method.loc8_3.3(%addr)
+// CHECK:STDOUT:   return
+// CHECK:STDOUT: }
+// CHECK:STDOUT:
+// CHECK:STDOUT: --- class.carbon
+// CHECK:STDOUT:
+// CHECK:STDOUT: constants {
+// CHECK:STDOUT:   %C: type = class_type @C [concrete]
+// CHECK:STDOUT:   %pattern_type.c48: type = pattern_type %C [concrete]
+// CHECK:STDOUT:   %C.F.type: type = fn_type @C.F [concrete]
+// CHECK:STDOUT:   %empty_tuple.type: type = tuple_type () [concrete]
+// CHECK:STDOUT:   %C.F: %C.F.type = struct_value () [concrete]
+// CHECK:STDOUT:   %empty_struct_type: type = struct_type {} [concrete]
+// CHECK:STDOUT:   %complete_type.357: <witness> = complete_type_witness %empty_struct_type [concrete]
+// CHECK:STDOUT:   %G.type: type = fn_type @G [concrete]
+// CHECK:STDOUT:   %G: %G.type = struct_value () [concrete]
+// CHECK:STDOUT:   %type_where: type = facet_type <type where .Self impls <CanDestroy>> [concrete]
+// CHECK:STDOUT:   %facet_value: %type_where = facet_value %C, () [concrete]
+// CHECK:STDOUT:   %DestroyT.binding.as_type.as.Destroy.impl.Op.type.eeb: type = fn_type @DestroyT.binding.as_type.as.Destroy.impl.Op, @DestroyT.binding.as_type.as.Destroy.impl(%facet_value) [concrete]
+// CHECK:STDOUT:   %DestroyT.binding.as_type.as.Destroy.impl.Op.88a: %DestroyT.binding.as_type.as.Destroy.impl.Op.type.eeb = struct_value () [concrete]
+// CHECK:STDOUT:   %ptr.019: type = ptr_type %C [concrete]
+// CHECK:STDOUT: }
+// CHECK:STDOUT:
+// CHECK:STDOUT: imports {
+// CHECK:STDOUT: }
+// CHECK:STDOUT:
+// CHECK:STDOUT: file {
+// CHECK:STDOUT:   %C.decl: type = class_decl @C [concrete = constants.%C] {} {}
+// CHECK:STDOUT:   %G.decl: %G.type = fn_decl @G [concrete = constants.%G] {} {}
+// CHECK:STDOUT: }
+// CHECK:STDOUT:
+// CHECK:STDOUT: class @C {
+// CHECK:STDOUT:   %C.F.decl: %C.F.type = fn_decl @C.F [concrete = constants.%C.F] {
+// CHECK:STDOUT:     %self.patt: %pattern_type.c48 = ref_binding_pattern self [concrete]
+// CHECK:STDOUT:     %self.param_patt: %pattern_type.c48 = ref_param_pattern %self.patt, call_param0 [concrete]
+// CHECK:STDOUT:   } {
+// CHECK:STDOUT:     %self.param: ref %C = ref_param call_param0
+// CHECK:STDOUT:     %Self.ref: type = name_ref Self, constants.%C [concrete = constants.%C]
+// CHECK:STDOUT:     %self: ref %C = ref_binding self, %self.param
+// CHECK:STDOUT:   }
+// CHECK:STDOUT:   %complete_type: <witness> = complete_type_witness constants.%empty_struct_type [concrete = constants.%complete_type.357]
+// CHECK:STDOUT:   complete_type_witness = %complete_type
+// CHECK:STDOUT:
+// CHECK:STDOUT: !members:
+// CHECK:STDOUT:   .Self = constants.%C
+// CHECK:STDOUT:   .F = %C.F.decl
+// CHECK:STDOUT: }
+// CHECK:STDOUT:
+// CHECK:STDOUT: fn @C.F(%self.param: %C);
+// CHECK:STDOUT:
+// CHECK:STDOUT: fn @G() {
+// CHECK:STDOUT: !entry:
+// CHECK:STDOUT:   name_binding_decl {
+// CHECK:STDOUT:     %c.patt: %pattern_type.c48 = ref_binding_pattern c [concrete]
+// CHECK:STDOUT:     %c.var_patt: %pattern_type.c48 = var_pattern %c.patt [concrete]
+// CHECK:STDOUT:   }
+// CHECK:STDOUT:   %c.var: ref %C = var %c.var_patt
+// CHECK:STDOUT:   %C.ref: type = name_ref C, file.%C.decl [concrete = constants.%C]
+// CHECK:STDOUT:   %c: ref %C = ref_binding c, %c.var
+// CHECK:STDOUT:   %c.ref: ref %C = name_ref c, %c
+// CHECK:STDOUT:   %F.ref: %C.F.type = name_ref F, @C.%C.F.decl [concrete = constants.%C.F]
+// CHECK:STDOUT:   %C.F.bound: <bound method> = bound_method %c.ref, %F.ref
+// CHECK:STDOUT:   %C.F.call: init %empty_tuple.type = call %C.F.bound(%c.ref)
+// CHECK:STDOUT:   %DestroyT.binding.as_type.as.Destroy.impl.Op.bound: <bound method> = bound_method %c.var, constants.%DestroyT.binding.as_type.as.Destroy.impl.Op.88a
+// CHECK:STDOUT:   <elided>
+// CHECK:STDOUT:   %bound_method: <bound method> = bound_method %c.var, %DestroyT.binding.as_type.as.Destroy.impl.Op.specific_fn
+// CHECK:STDOUT:   %addr: %ptr.019 = addr_of %c.var
+// CHECK:STDOUT:   %DestroyT.binding.as_type.as.Destroy.impl.Op.call: init %empty_tuple.type = call %bound_method(%addr)
+// CHECK:STDOUT:   return
+// CHECK:STDOUT: }
+// CHECK:STDOUT:

+ 3 - 3
toolchain/check/testdata/impl/lookup/specialization_with_symbolic_rewrite.carbon

@@ -854,7 +854,7 @@ fn F[T:! Ptr](var t: T) -> T.(Ptr.Type) {
 // CHECK:STDOUT:   %F.decl: %F.type = fn_decl @F [concrete = constants.%F] {
 // CHECK:STDOUT:     %T.patt: %pattern_type.98f = symbolic_binding_pattern T, 0 [concrete]
 // CHECK:STDOUT:     %t.patt: @F.%pattern_type.loc9_20 (%pattern_type.e68) = ref_binding_pattern t [concrete]
-// CHECK:STDOUT:     %t.param_patt: @F.%pattern_type.loc9_20 (%pattern_type.e68) = ref_param_pattern %t.patt, call_param0 [concrete]
+// CHECK:STDOUT:     %t.param_patt: @F.%pattern_type.loc9_20 (%pattern_type.e68) = var_param_pattern %t.patt, call_param0 [concrete]
 // CHECK:STDOUT:     %t.var_patt: @F.%pattern_type.loc9_20 (%pattern_type.e68) = var_pattern %t.param_patt [concrete]
 // CHECK:STDOUT:     %return.patt: @F.%pattern_type.loc9_26 (%pattern_type.a60) = return_slot_pattern [concrete]
 // CHECK:STDOUT:     %return.param_patt: @F.%pattern_type.loc9_26 (%pattern_type.a60) = out_param_pattern %return.patt, call_param1 [concrete]
@@ -1055,7 +1055,7 @@ fn F[T:! Ptr](var t: T) -> T.(Ptr.Type) {
 // CHECK:STDOUT:   %F.decl: %F.type = fn_decl @F [concrete = constants.%F] {
 // CHECK:STDOUT:     %T.patt: %pattern_type.3d8 = symbolic_binding_pattern T, 0 [concrete]
 // CHECK:STDOUT:     %t.patt: @F.%pattern_type.loc9_19 (%pattern_type.6f0) = ref_binding_pattern t [concrete]
-// CHECK:STDOUT:     %t.param_patt: @F.%pattern_type.loc9_19 (%pattern_type.6f0) = ref_param_pattern %t.patt, call_param0 [concrete]
+// CHECK:STDOUT:     %t.param_patt: @F.%pattern_type.loc9_19 (%pattern_type.6f0) = var_param_pattern %t.patt, call_param0 [concrete]
 // CHECK:STDOUT:     %t.var_patt: @F.%pattern_type.loc9_19 (%pattern_type.6f0) = var_pattern %t.param_patt [concrete]
 // CHECK:STDOUT:     %return.patt: @F.%pattern_type.loc9_25 (%pattern_type.425) = return_slot_pattern [concrete]
 // CHECK:STDOUT:     %return.param_patt: @F.%pattern_type.loc9_25 (%pattern_type.425) = out_param_pattern %return.patt, call_param1 [concrete]
@@ -1262,7 +1262,7 @@ fn F[T:! Ptr](var t: T) -> T.(Ptr.Type) {
 // CHECK:STDOUT:   %F.decl: %F.type = fn_decl @F [concrete = constants.%F] {
 // CHECK:STDOUT:     %T.patt: %pattern_type.3d8 = symbolic_binding_pattern T, 0 [concrete]
 // CHECK:STDOUT:     %t.patt: @F.%pattern_type.loc9_19 (%pattern_type.6f0) = ref_binding_pattern t [concrete]
-// CHECK:STDOUT:     %t.param_patt: @F.%pattern_type.loc9_19 (%pattern_type.6f0) = ref_param_pattern %t.patt, call_param0 [concrete]
+// CHECK:STDOUT:     %t.param_patt: @F.%pattern_type.loc9_19 (%pattern_type.6f0) = var_param_pattern %t.patt, call_param0 [concrete]
 // CHECK:STDOUT:     %t.var_patt: @F.%pattern_type.loc9_19 (%pattern_type.6f0) = var_pattern %t.param_patt [concrete]
 // CHECK:STDOUT:     %return.patt: @F.%pattern_type.loc9_25 (%pattern_type.425) = return_slot_pattern [concrete]
 // CHECK:STDOUT:     %return.param_patt: @F.%pattern_type.loc9_25 (%pattern_type.425) = out_param_pattern %return.patt, call_param1 [concrete]

+ 0 - 206
toolchain/check/testdata/let/ref.carbon

@@ -22,35 +22,6 @@ fn F(a: i32) {
   //@dump-sem-ir-end
 }
 
-// --- param.carbon
-
-library "[[@TEST_NAME]]";
-
-//@dump-sem-ir-begin
-fn F(ref a: i32) {
-  a = 1;
-}
-
-fn G() {
-  var x: i32 = 0;
-  F(x);
-}
-//@dump-sem-ir-end
-
-// --- self.carbon
-
-library "[[@TEST_NAME]]";
-
-//@dump-sem-ir-begin
-class C {
-  var a: i32;
-
-  fn F[ref self: Self]() {
-    self.a = 1;
-  }
-}
-//@dump-sem-ir-end
-
 // --- fail_ref_to_value.carbon
 
 library "[[@TEST_NAME]]";
@@ -123,180 +94,3 @@ fn F() {
 // CHECK:STDOUT:   <elided>
 // CHECK:STDOUT: }
 // CHECK:STDOUT:
-// CHECK:STDOUT: --- 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:   %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_1.5b8: Core.IntLiteral = int_value 1 [concrete]
-// CHECK:STDOUT:   %ImplicitAs.type.d14: type = facet_type <@ImplicitAs, @ImplicitAs(%i32)> [concrete]
-// CHECK:STDOUT:   %ImplicitAs.Convert.type.1b6: type = fn_type @ImplicitAs.Convert, @ImplicitAs(%i32) [concrete]
-// CHECK:STDOUT:   %To: Core.IntLiteral = symbolic_binding To, 0 [symbolic]
-// CHECK:STDOUT:   %Core.IntLiteral.as.ImplicitAs.impl.Convert.type.f51: type = fn_type @Core.IntLiteral.as.ImplicitAs.impl.Convert, @Core.IntLiteral.as.ImplicitAs.impl(%To) [symbolic]
-// CHECK:STDOUT:   %Core.IntLiteral.as.ImplicitAs.impl.Convert.2a1: %Core.IntLiteral.as.ImplicitAs.impl.Convert.type.f51 = struct_value () [symbolic]
-// CHECK:STDOUT:   %ImplicitAs.impl_witness.bc9: <witness> = impl_witness imports.%ImplicitAs.impl_witness_table.132, @Core.IntLiteral.as.ImplicitAs.impl(%int_32) [concrete]
-// CHECK:STDOUT:   %Core.IntLiteral.as.ImplicitAs.impl.Convert.type.51e: 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.e9b: %Core.IntLiteral.as.ImplicitAs.impl.Convert.type.51e = struct_value () [concrete]
-// CHECK:STDOUT:   %ImplicitAs.facet: %ImplicitAs.type.d14 = facet_value Core.IntLiteral, (%ImplicitAs.impl_witness.bc9) [concrete]
-// CHECK:STDOUT:   %.322: type = fn_type_with_self_type %ImplicitAs.Convert.type.1b6, %ImplicitAs.facet [concrete]
-// CHECK:STDOUT:   %Core.IntLiteral.as.ImplicitAs.impl.Convert.bound.265: <bound method> = bound_method %int_1.5b8, %Core.IntLiteral.as.ImplicitAs.impl.Convert.e9b [concrete]
-// CHECK:STDOUT:   %Core.IntLiteral.as.ImplicitAs.impl.Convert.specific_fn: <specific function> = specific_function %Core.IntLiteral.as.ImplicitAs.impl.Convert.e9b, @Core.IntLiteral.as.ImplicitAs.impl.Convert(%int_32) [concrete]
-// CHECK:STDOUT:   %bound_method.dfc: <bound method> = bound_method %int_1.5b8, %Core.IntLiteral.as.ImplicitAs.impl.Convert.specific_fn [concrete]
-// CHECK:STDOUT:   %int_1.5d2: %i32 = int_value 1 [concrete]
-// CHECK:STDOUT:   %G.type: type = fn_type @G [concrete]
-// CHECK:STDOUT:   %G: %G.type = struct_value () [concrete]
-// CHECK:STDOUT:   %int_0.5c6: Core.IntLiteral = int_value 0 [concrete]
-// CHECK:STDOUT:   %Core.IntLiteral.as.ImplicitAs.impl.Convert.bound.0b3: <bound method> = bound_method %int_0.5c6, %Core.IntLiteral.as.ImplicitAs.impl.Convert.e9b [concrete]
-// CHECK:STDOUT:   %bound_method.9be: <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:   %type_where: type = facet_type <type where .Self impls <CanDestroy>> [concrete]
-// CHECK:STDOUT:   %facet_value: %type_where = facet_value %i32, () [concrete]
-// CHECK:STDOUT:   %DestroyT.binding.as_type.as.Destroy.impl.Op.type.d4e: type = fn_type @DestroyT.binding.as_type.as.Destroy.impl.Op, @DestroyT.binding.as_type.as.Destroy.impl(%facet_value) [concrete]
-// CHECK:STDOUT:   %DestroyT.binding.as_type.as.Destroy.impl.Op.424: %DestroyT.binding.as_type.as.Destroy.impl.Op.type.d4e = struct_value () [concrete]
-// CHECK:STDOUT:   %ptr.235: type = ptr_type %i32 [concrete]
-// CHECK:STDOUT: }
-// CHECK:STDOUT:
-// CHECK:STDOUT: imports {
-// CHECK:STDOUT:   %Core.import_ref.e24: @Core.IntLiteral.as.ImplicitAs.impl.%Core.IntLiteral.as.ImplicitAs.impl.Convert.type (%Core.IntLiteral.as.ImplicitAs.impl.Convert.type.f51) = 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.2a1)]
-// CHECK:STDOUT:   %ImplicitAs.impl_witness_table.132 = impl_witness_table (%Core.import_ref.e24), @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:     %a.patt: %pattern_type.7ce = ref_binding_pattern a [concrete]
-// CHECK:STDOUT:     %a.param_patt: %pattern_type.7ce = ref_param_pattern %a.patt, call_param0 [concrete]
-// CHECK:STDOUT:   } {
-// CHECK:STDOUT:     %a.param: ref %i32 = ref_param call_param0
-// CHECK:STDOUT:     %.loc5: type = splice_block %i32 [concrete = constants.%i32] {
-// CHECK:STDOUT:       %int_32: Core.IntLiteral = int_value 32 [concrete = constants.%int_32]
-// CHECK:STDOUT:       %i32: type = class_type @Int, @Int(constants.%int_32) [concrete = constants.%i32]
-// CHECK:STDOUT:     }
-// CHECK:STDOUT:     %a: ref %i32 = ref_binding a, %a.param
-// CHECK:STDOUT:   }
-// CHECK:STDOUT:   %G.decl: %G.type = fn_decl @G [concrete = constants.%G] {} {}
-// CHECK:STDOUT: }
-// CHECK:STDOUT:
-// CHECK:STDOUT: fn @F(%a.param: %i32) {
-// CHECK:STDOUT: !entry:
-// CHECK:STDOUT:   %a.ref: ref %i32 = name_ref a, %a
-// CHECK:STDOUT:   %int_1: Core.IntLiteral = int_value 1 [concrete = constants.%int_1.5b8]
-// CHECK:STDOUT:   %impl.elem0: %.322 = impl_witness_access constants.%ImplicitAs.impl_witness.bc9, element0 [concrete = constants.%Core.IntLiteral.as.ImplicitAs.impl.Convert.e9b]
-// CHECK:STDOUT:   %bound_method.loc6_5.1: <bound method> = bound_method %int_1, %impl.elem0 [concrete = constants.%Core.IntLiteral.as.ImplicitAs.impl.Convert.bound.265]
-// 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.loc6_5.2: <bound method> = bound_method %int_1, %specific_fn [concrete = constants.%bound_method.dfc]
-// CHECK:STDOUT:   %Core.IntLiteral.as.ImplicitAs.impl.Convert.call: init %i32 = call %bound_method.loc6_5.2(%int_1) [concrete = constants.%int_1.5d2]
-// CHECK:STDOUT:   %.loc6: init %i32 = converted %int_1, %Core.IntLiteral.as.ImplicitAs.impl.Convert.call [concrete = constants.%int_1.5d2]
-// CHECK:STDOUT:   assign %a.ref, %.loc6
-// CHECK:STDOUT:   return
-// CHECK:STDOUT: }
-// CHECK:STDOUT:
-// CHECK:STDOUT: fn @G() {
-// CHECK:STDOUT: !entry:
-// CHECK:STDOUT:   name_binding_decl {
-// CHECK:STDOUT:     %x.patt: %pattern_type.7ce = ref_binding_pattern x [concrete]
-// CHECK:STDOUT:     %x.var_patt: %pattern_type.7ce = var_pattern %x.patt [concrete]
-// CHECK:STDOUT:   }
-// CHECK:STDOUT:   %x.var: ref %i32 = var %x.var_patt
-// CHECK:STDOUT:   %int_0: Core.IntLiteral = int_value 0 [concrete = constants.%int_0.5c6]
-// CHECK:STDOUT:   %impl.elem0: %.322 = impl_witness_access constants.%ImplicitAs.impl_witness.bc9, element0 [concrete = constants.%Core.IntLiteral.as.ImplicitAs.impl.Convert.e9b]
-// CHECK:STDOUT:   %bound_method.loc10_3.1: <bound method> = bound_method %int_0, %impl.elem0 [concrete = constants.%Core.IntLiteral.as.ImplicitAs.impl.Convert.bound.0b3]
-// 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.loc10_3.2: <bound method> = bound_method %int_0, %specific_fn [concrete = constants.%bound_method.9be]
-// CHECK:STDOUT:   %Core.IntLiteral.as.ImplicitAs.impl.Convert.call: init %i32 = call %bound_method.loc10_3.2(%int_0) [concrete = constants.%int_0.6a9]
-// CHECK:STDOUT:   %.loc10_3: init %i32 = converted %int_0, %Core.IntLiteral.as.ImplicitAs.impl.Convert.call [concrete = constants.%int_0.6a9]
-// CHECK:STDOUT:   assign %x.var, %.loc10_3
-// CHECK:STDOUT:   %.loc10_10: type = splice_block %i32 [concrete = constants.%i32] {
-// CHECK:STDOUT:     %int_32: Core.IntLiteral = int_value 32 [concrete = constants.%int_32]
-// CHECK:STDOUT:     %i32: type = class_type @Int, @Int(constants.%int_32) [concrete = constants.%i32]
-// CHECK:STDOUT:   }
-// CHECK:STDOUT:   %x: ref %i32 = ref_binding x, %x.var
-// CHECK:STDOUT:   %F.ref: %F.type = name_ref F, file.%F.decl [concrete = constants.%F]
-// CHECK:STDOUT:   %x.ref: ref %i32 = name_ref x, %x
-// CHECK:STDOUT:   %F.call: init %empty_tuple.type = call %F.ref(%x.ref)
-// CHECK:STDOUT:   %DestroyT.binding.as_type.as.Destroy.impl.Op.bound: <bound method> = bound_method %x.var, constants.%DestroyT.binding.as_type.as.Destroy.impl.Op.424
-// CHECK:STDOUT:   <elided>
-// CHECK:STDOUT:   %bound_method.loc10_3.3: <bound method> = bound_method %x.var, %DestroyT.binding.as_type.as.Destroy.impl.Op.specific_fn
-// CHECK:STDOUT:   %addr: %ptr.235 = addr_of %x.var
-// CHECK:STDOUT:   %DestroyT.binding.as_type.as.Destroy.impl.Op.call: init %empty_tuple.type = call %bound_method.loc10_3.3(%addr)
-// CHECK:STDOUT:   return
-// CHECK:STDOUT: }
-// CHECK:STDOUT:
-// CHECK:STDOUT: --- self.carbon
-// CHECK:STDOUT:
-// CHECK:STDOUT: constants {
-// CHECK:STDOUT:   %C: type = class_type @C [concrete]
-// CHECK:STDOUT:   %int_32: Core.IntLiteral = int_value 32 [concrete]
-// CHECK:STDOUT:   %i32: type = class_type @Int, @Int(%int_32) [concrete]
-// CHECK:STDOUT:   %C.elem: type = unbound_element_type %C, %i32 [concrete]
-// CHECK:STDOUT:   %pattern_type.c48: type = pattern_type %C [concrete]
-// CHECK:STDOUT:   %C.F.type: type = fn_type @C.F [concrete]
-// CHECK:STDOUT:   %C.F: %C.F.type = struct_value () [concrete]
-// CHECK:STDOUT:   %struct_type.a: type = struct_type {.a: %i32} [concrete]
-// CHECK:STDOUT:   %complete_type.fd7: <witness> = complete_type_witness %struct_type.a [concrete]
-// CHECK:STDOUT:   %int_1.5b8: Core.IntLiteral = int_value 1 [concrete]
-// CHECK:STDOUT:   %ImplicitAs.type.d14: type = facet_type <@ImplicitAs, @ImplicitAs(%i32)> [concrete]
-// CHECK:STDOUT:   %ImplicitAs.Convert.type.1b6: type = fn_type @ImplicitAs.Convert, @ImplicitAs(%i32) [concrete]
-// CHECK:STDOUT:   %To: Core.IntLiteral = symbolic_binding To, 0 [symbolic]
-// CHECK:STDOUT:   %Core.IntLiteral.as.ImplicitAs.impl.Convert.type.f51: type = fn_type @Core.IntLiteral.as.ImplicitAs.impl.Convert, @Core.IntLiteral.as.ImplicitAs.impl(%To) [symbolic]
-// CHECK:STDOUT:   %Core.IntLiteral.as.ImplicitAs.impl.Convert.2a1: %Core.IntLiteral.as.ImplicitAs.impl.Convert.type.f51 = struct_value () [symbolic]
-// CHECK:STDOUT:   %ImplicitAs.impl_witness.bc9: <witness> = impl_witness imports.%ImplicitAs.impl_witness_table.132, @Core.IntLiteral.as.ImplicitAs.impl(%int_32) [concrete]
-// CHECK:STDOUT:   %Core.IntLiteral.as.ImplicitAs.impl.Convert.type.51e: 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.e9b: %Core.IntLiteral.as.ImplicitAs.impl.Convert.type.51e = struct_value () [concrete]
-// CHECK:STDOUT:   %ImplicitAs.facet: %ImplicitAs.type.d14 = facet_value Core.IntLiteral, (%ImplicitAs.impl_witness.bc9) [concrete]
-// CHECK:STDOUT:   %.322: type = fn_type_with_self_type %ImplicitAs.Convert.type.1b6, %ImplicitAs.facet [concrete]
-// CHECK:STDOUT:   %Core.IntLiteral.as.ImplicitAs.impl.Convert.bound: <bound method> = bound_method %int_1.5b8, %Core.IntLiteral.as.ImplicitAs.impl.Convert.e9b [concrete]
-// CHECK:STDOUT:   %Core.IntLiteral.as.ImplicitAs.impl.Convert.specific_fn: <specific function> = specific_function %Core.IntLiteral.as.ImplicitAs.impl.Convert.e9b, @Core.IntLiteral.as.ImplicitAs.impl.Convert(%int_32) [concrete]
-// CHECK:STDOUT:   %bound_method: <bound method> = bound_method %int_1.5b8, %Core.IntLiteral.as.ImplicitAs.impl.Convert.specific_fn [concrete]
-// CHECK:STDOUT:   %int_1.5d2: %i32 = int_value 1 [concrete]
-// CHECK:STDOUT: }
-// CHECK:STDOUT:
-// CHECK:STDOUT: imports {
-// CHECK:STDOUT:   %Core.import_ref.e24: @Core.IntLiteral.as.ImplicitAs.impl.%Core.IntLiteral.as.ImplicitAs.impl.Convert.type (%Core.IntLiteral.as.ImplicitAs.impl.Convert.type.f51) = 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.2a1)]
-// CHECK:STDOUT:   %ImplicitAs.impl_witness_table.132 = impl_witness_table (%Core.import_ref.e24), @Core.IntLiteral.as.ImplicitAs.impl [concrete]
-// CHECK:STDOUT: }
-// CHECK:STDOUT:
-// CHECK:STDOUT: file {
-// CHECK:STDOUT:   %C.decl: type = class_decl @C [concrete = constants.%C] {} {}
-// CHECK:STDOUT: }
-// CHECK:STDOUT:
-// CHECK:STDOUT: class @C {
-// CHECK:STDOUT:   %int_32: Core.IntLiteral = int_value 32 [concrete = constants.%int_32]
-// CHECK:STDOUT:   %i32: type = class_type @Int, @Int(constants.%int_32) [concrete = constants.%i32]
-// CHECK:STDOUT:   %.loc6: %C.elem = field_decl a, element0 [concrete]
-// CHECK:STDOUT:   %C.F.decl: %C.F.type = fn_decl @C.F [concrete = constants.%C.F] {
-// CHECK:STDOUT:     %self.patt: %pattern_type.c48 = ref_binding_pattern self [concrete]
-// CHECK:STDOUT:     %self.param_patt: %pattern_type.c48 = ref_param_pattern %self.patt, call_param0 [concrete]
-// CHECK:STDOUT:   } {
-// CHECK:STDOUT:     %self.param: ref %C = ref_param call_param0
-// CHECK:STDOUT:     %Self.ref: type = name_ref Self, constants.%C [concrete = constants.%C]
-// CHECK:STDOUT:     %self: ref %C = ref_binding self, %self.param
-// CHECK:STDOUT:   }
-// CHECK:STDOUT:   %complete_type: <witness> = complete_type_witness constants.%struct_type.a [concrete = constants.%complete_type.fd7]
-// CHECK:STDOUT:   complete_type_witness = %complete_type
-// CHECK:STDOUT:
-// CHECK:STDOUT: !members:
-// CHECK:STDOUT:   .Self = constants.%C
-// CHECK:STDOUT:   .a = %.loc6
-// CHECK:STDOUT:   .F = %C.F.decl
-// CHECK:STDOUT: }
-// CHECK:STDOUT:
-// CHECK:STDOUT: fn @C.F(%self.param: %C) {
-// CHECK:STDOUT: !entry:
-// CHECK:STDOUT:   %self.ref: ref %C = name_ref self, %self
-// CHECK:STDOUT:   %a.ref: %C.elem = name_ref a, @C.%.loc6 [concrete = @C.%.loc6]
-// CHECK:STDOUT:   %.loc9_9: ref %i32 = class_element_access %self.ref, element0
-// CHECK:STDOUT:   %int_1: Core.IntLiteral = int_value 1 [concrete = constants.%int_1.5b8]
-// CHECK:STDOUT:   %impl.elem0: %.322 = impl_witness_access constants.%ImplicitAs.impl_witness.bc9, element0 [concrete = constants.%Core.IntLiteral.as.ImplicitAs.impl.Convert.e9b]
-// CHECK:STDOUT:   %bound_method.loc9_12.1: <bound method> = bound_method %int_1, %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_12.2: <bound method> = bound_method %int_1, %specific_fn [concrete = constants.%bound_method]
-// CHECK:STDOUT:   %Core.IntLiteral.as.ImplicitAs.impl.Convert.call: init %i32 = call %bound_method.loc9_12.2(%int_1) [concrete = constants.%int_1.5d2]
-// CHECK:STDOUT:   %.loc9_12: init %i32 = converted %int_1, %Core.IntLiteral.as.ImplicitAs.impl.Convert.call [concrete = constants.%int_1.5d2]
-// CHECK:STDOUT:   assign %.loc9_9, %.loc9_12
-// CHECK:STDOUT:   return
-// CHECK:STDOUT: }
-// CHECK:STDOUT:

+ 4 - 4
toolchain/check/testdata/var/var_pattern.carbon

@@ -473,7 +473,7 @@ fn G() {
 // CHECK:STDOUT:     %x.patt: %pattern_type.cb1 = value_binding_pattern x [concrete]
 // CHECK:STDOUT:     %x.param_patt: %pattern_type.cb1 = value_param_pattern %x.patt, call_param0 [concrete]
 // CHECK:STDOUT:     %y.patt: %pattern_type.cb1 = ref_binding_pattern y [concrete]
-// CHECK:STDOUT:     %y.param_patt: %pattern_type.cb1 = ref_param_pattern %y.patt, call_param1 [concrete]
+// CHECK:STDOUT:     %y.param_patt: %pattern_type.cb1 = var_param_pattern %y.patt, call_param1 [concrete]
 // CHECK:STDOUT:     %y.var_patt: %pattern_type.cb1 = var_pattern %y.param_patt [concrete]
 // CHECK:STDOUT:   } {
 // CHECK:STDOUT:     %x.param: %empty_tuple.type = value_param call_param0
@@ -559,7 +559,7 @@ fn G() {
 // CHECK:STDOUT:     %x.patt: %pattern_type = value_binding_pattern x [concrete]
 // CHECK:STDOUT:     %x.param_patt: %pattern_type = value_param_pattern %x.patt, call_param0 [concrete]
 // CHECK:STDOUT:     %y.patt: %pattern_type = ref_binding_pattern y [concrete]
-// CHECK:STDOUT:     %y.param_patt: %pattern_type = ref_param_pattern %y.patt, call_param1 [concrete]
+// CHECK:STDOUT:     %y.param_patt: %pattern_type = var_param_pattern %y.patt, call_param1 [concrete]
 // CHECK:STDOUT:     %y.var_patt: %pattern_type = var_pattern %y.param_patt [concrete]
 // CHECK:STDOUT:   } {
 // CHECK:STDOUT:     %x.param: %empty_tuple.type = value_param call_param0
@@ -607,7 +607,7 @@ fn G() {
 // CHECK:STDOUT:     %x.patt: %pattern_type = value_binding_pattern x [concrete]
 // CHECK:STDOUT:     %x.param_patt: %pattern_type = value_param_pattern %x.patt, call_param0 [concrete]
 // CHECK:STDOUT:     %y.patt: %pattern_type = ref_binding_pattern y [concrete]
-// CHECK:STDOUT:     %y.param_patt: %pattern_type = ref_param_pattern %y.patt, call_param1 [concrete]
+// CHECK:STDOUT:     %y.param_patt: %pattern_type = var_param_pattern %y.patt, call_param1 [concrete]
 // CHECK:STDOUT:     %y.var_patt: %pattern_type = var_pattern %y.param_patt [concrete]
 // CHECK:STDOUT:   } {
 // CHECK:STDOUT:     %x.param: %empty_tuple.type = value_param call_param0
@@ -863,7 +863,7 @@ fn G() {
 // CHECK:STDOUT: class @C {
 // CHECK:STDOUT:   %C.F.decl: %C.F.type = fn_decl @C.F [concrete = constants.%C.F] {
 // CHECK:STDOUT:     %self.patt: %pattern_type = ref_binding_pattern self [concrete]
-// CHECK:STDOUT:     %self.param_patt: %pattern_type = ref_param_pattern %self.patt, call_param0 [concrete]
+// CHECK:STDOUT:     %self.param_patt: %pattern_type = var_param_pattern %self.patt, call_param0 [concrete]
 // CHECK:STDOUT:     %self.var_patt: %pattern_type = var_pattern %self.param_patt [concrete]
 // CHECK:STDOUT:   } {
 // CHECK:STDOUT:     %self.param: ref %C = ref_param call_param0

+ 1 - 2
toolchain/check/thunk.cpp

@@ -298,8 +298,7 @@ static auto BuildPatternRef(Context& context,
   auto pattern_ref_id = SemIR::InstId::None;
   if (auto value_param = pattern.TryAs<SemIR::AnyParamPattern>();
       value_param.has_value() &&
-      (value_param->kind == SemIR::ValueParamPattern::Kind ||
-       value_param->kind == SemIR::RefParamPattern::Kind)) {
+      value_param->kind != SemIR::OutParamPattern::Kind) {
     pattern_ref_id = arg_ids[value_param->index.index];
   } else {
     if (pattern_id != SemIR::ErrorInst::InstId) {

+ 4 - 0
toolchain/diagnostics/diagnostic_kind.def

@@ -118,6 +118,7 @@ CARBON_DIAGNOSTIC_KIND(UnaryOperatorRequiresWhitespace)
 CARBON_DIAGNOSTIC_KIND(UnexpectedTokenAfterListElement)
 CARBON_DIAGNOSTIC_KIND(UnrecognizedDecl)
 CARBON_DIAGNOSTIC_KIND(UnexpectedTokenInCompoundMemberAccess)
+CARBON_DIAGNOSTIC_KIND(UnexpectedRef)
 
 // Match diagnostics.
 CARBON_DIAGNOSTIC_KIND(ExpectedMatchCaseArrow)
@@ -255,6 +256,9 @@ CARBON_DIAGNOSTIC_KIND(InCallToEntity)
 CARBON_DIAGNOSTIC_KIND(InCallToFunction)
 CARBON_DIAGNOSTIC_KIND(InCallToFunctionParam)
 CARBON_DIAGNOSTIC_KIND(MissingObjectInMethodCall)
+CARBON_DIAGNOSTIC_KIND(RefParamNoRefTag)
+CARBON_DIAGNOSTIC_KIND(RefTagNoRefParam)
+CARBON_DIAGNOSTIC_KIND(RefTagNotDurableRef)
 CARBON_DIAGNOSTIC_KIND(SelfParameterNotAllowed)
 CARBON_DIAGNOSTIC_KIND(ValueForRefParam)
 

+ 2 - 1
toolchain/lower/file_context.cpp

@@ -357,7 +357,8 @@ auto FileContext::BuildFunctionTypeInfo(const SemIR::Function& function,
     }
     // TODO: Use a more general mechanism to determine if the binding is a
     // reference binding.
-    if (param_pattern_info->inst.kind == SemIR::RefParamPattern::Kind) {
+    if (param_pattern_info->inst.kind == SemIR::RefParamPattern::Kind ||
+        param_pattern_info->inst.kind == SemIR::VarParamPattern::Kind) {
       param_types.push_back(
           llvm::PointerType::get(llvm_context(), /*AddressSpace=*/0));
       param_inst_ids.push_back(param_pattern_id);

+ 1 - 1
toolchain/lower/testdata/function/call/ref_param.carbon

@@ -14,7 +14,7 @@ fn DoNothing(ref a: i32) {}
 
 fn Main() {
   var a: i32 = 0;
-  DoNothing(a);
+  DoNothing(ref a);
 }
 
 // CHECK:STDOUT: ; ModuleID = 'ref_param.carbon'

+ 5 - 0
toolchain/parse/handle_brace_expr.cpp

@@ -133,6 +133,11 @@ static auto HandleBraceExprParamAfterDesignator(Context& context,
   // that one has a `:` separator and the other has an `=` separator.
   state.token = context.Consume();
   context.PushState(state, param_finish_kind);
+  if (param_finish_kind == StateKind::BraceExprParamFinishAsValue &&
+      context.PositionIs(Lex::TokenKind::Ref)) {
+    context.PushState(StateKind::RefTagFinishAsRegular);
+    context.ConsumeChecked(Lex::TokenKind::Ref);
+  }
   context.PushState(StateKind::Expr);
 }
 

+ 0 - 42
toolchain/parse/handle_call_expr.cpp

@@ -1,42 +0,0 @@
-// Part of the Carbon Language project, under the Apache License v2.0 with LLVM
-// Exceptions. See /LICENSE for license information.
-// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
-
-#include "toolchain/parse/context.h"
-#include "toolchain/parse/handle.h"
-
-namespace Carbon::Parse {
-
-auto HandleCallExpr(Context& context) -> void {
-  auto state = context.PopState();
-  context.PushState(state, StateKind::CallExprFinish);
-
-  context.AddNode(NodeKind::CallExprStart, context.Consume(), state.has_error);
-  if (!context.PositionIs(Lex::TokenKind::CloseParen)) {
-    context.PushState(StateKind::CallExprParamFinish);
-    context.PushState(StateKind::Expr);
-  }
-}
-
-auto HandleCallExprParamFinish(Context& context) -> void {
-  auto state = context.PopState();
-
-  if (state.has_error) {
-    context.ReturnErrorOnState();
-  }
-
-  if (context.ConsumeListToken(NodeKind::CallExprComma,
-                               Lex::TokenKind::CloseParen, state.has_error) ==
-      Context::ListTokenKind::Comma) {
-    context.PushState(StateKind::CallExprParamFinish);
-    context.PushState(StateKind::Expr);
-  }
-}
-
-auto HandleCallExprFinish(Context& context) -> void {
-  auto state = context.PopState();
-
-  context.AddNode(NodeKind::CallExpr, context.Consume(), state.has_error);
-}
-
-}  // namespace Carbon::Parse

+ 54 - 5
toolchain/parse/handle_paren_expr.cpp

@@ -43,6 +43,34 @@ auto HandleOnlyParenExprFinish(Context& context) -> void {
   FinishParenExpr(context, state);
 }
 
+static auto HandleRefTagFinish(Context& context, StateKind state_kind) -> void {
+  auto state = context.PopState();
+  if (state_kind == StateKind::RefTagFinishAsAfterOpenParen &&
+      !context.PositionIs(Lex::TokenKind::Comma)) {
+    CARBON_DIAGNOSTIC(UnexpectedRef, Error,
+                      "found `ref` in unexpected position");
+    context.emitter().Emit(state.token, UnexpectedRef);
+  }
+  context.AddNode(NodeKind::RefTag, state.token, state.has_error);
+}
+
+auto HandleRefTagFinishAsRegular(Context& context) -> void {
+  HandleRefTagFinish(context, StateKind::RefTagFinishAsRegular);
+}
+
+auto HandleRefTagFinishAsAfterOpenParen(Context& context) -> void {
+  HandleRefTagFinish(context, StateKind::RefTagFinishAsAfterOpenParen);
+}
+
+static auto StartTupleLiteralElement(Context& context) -> void {
+  context.PushState(StateKind::TupleLiteralElementFinish);
+  if (context.PositionIs(Lex::TokenKind::Ref)) {
+    context.PushState(StateKind::RefTagFinishAsRegular);
+    context.ConsumeChecked(Lex::TokenKind::Ref);
+  }
+  context.PushState(StateKind::Expr);
+}
+
 auto HandleParenExpr(Context& context) -> void {
   auto state = context.PopState();
 
@@ -56,6 +84,10 @@ auto HandleParenExpr(Context& context) -> void {
   } else {
     context.PushState(state, StateKind::ParenExprFinish);
     context.PushState(StateKind::ExprAfterOpenParenFinish);
+    if (context.PositionIs(Lex::TokenKind::Ref)) {
+      context.PushState(StateKind::RefTagFinishAsAfterOpenParen);
+      context.ConsumeChecked(Lex::TokenKind::Ref);
+    }
     context.PushState(StateKind::Expr);
   }
 }
@@ -78,25 +110,26 @@ auto HandleExprAfterOpenParenFinish(Context& context) -> void {
   // If the comma is not immediately followed by a close paren, push handlers
   // for the next tuple element.
   if (list_token_kind != Context::ListTokenKind::CommaClose) {
-    context.PushState(state, StateKind::TupleLiteralElementFinish);
-    context.PushState(StateKind::Expr);
+    StartTupleLiteralElement(context);
   }
 }
 
 auto HandleTupleLiteralElementFinish(Context& context) -> void {
   auto state = context.PopState();
 
+  if (state.has_error) {
+    context.ReturnErrorOnState();
+  }
+
   if (context.ConsumeListToken(NodeKind::TupleLiteralComma,
                                Lex::TokenKind::CloseParen, state.has_error) ==
       Context::ListTokenKind::Comma) {
-    context.PushState(state);
-    context.PushState(StateKind::Expr);
+    StartTupleLiteralElement(context);
   }
 }
 
 auto HandleParenExprFinish(Context& context) -> void {
   auto state = context.PopState();
-
   context.ReplacePlaceholderNode(state.subtree_start, NodeKind::ParenExprStart,
                                  state.token);
   FinishParenExpr(context, state);
@@ -110,4 +143,20 @@ auto HandleTupleLiteralFinish(Context& context) -> void {
   context.AddNode(NodeKind::TupleLiteral, context.Consume(), state.has_error);
 }
 
+auto HandleCallExpr(Context& context) -> void {
+  auto state = context.PopState();
+  context.PushState(state, StateKind::CallExprFinish);
+
+  context.AddNode(NodeKind::CallExprStart, context.Consume(), state.has_error);
+  if (!context.PositionIs(Lex::TokenKind::CloseParen)) {
+    StartTupleLiteralElement(context);
+  }
+}
+
+auto HandleCallExprFinish(Context& context) -> void {
+  auto state = context.PopState();
+
+  context.AddNode(NodeKind::CallExpr, context.Consume(), state.has_error);
+}
+
 }  // namespace Carbon::Parse

+ 2 - 1
toolchain/parse/node_kind.def

@@ -234,7 +234,6 @@ CARBON_PARSE_NODE_KIND(TupleLiteralComma)
 CARBON_PARSE_NODE_KIND(TupleLiteral)
 
 CARBON_PARSE_NODE_KIND(CallExprStart)
-CARBON_PARSE_NODE_KIND(CallExprComma)
 CARBON_PARSE_NODE_KIND(CallExpr)
 
 CARBON_PARSE_NODE_KIND(MemberAccessExpr)
@@ -243,6 +242,8 @@ CARBON_PARSE_NODE_KIND(PointerMemberAccessExpr)
 
 CARBON_PARSE_NODE_KIND(IntLiteral)
 
+CARBON_PARSE_NODE_KIND(RefTag)
+
 CARBON_PARSE_NODE_KIND_TOKEN_LITERAL(BoolLiteralFalse, False)
 CARBON_PARSE_NODE_KIND_TOKEN_LITERAL(BoolLiteralTrue, True)
 CARBON_PARSE_NODE_KIND_TOKEN_LITERAL(CharLiteral, CharLiteral)

+ 51 - 16
toolchain/parse/state.def

@@ -125,6 +125,12 @@ CARBON_PARSE_STATE_VARIANTS3(BraceExprParam, Type, Value, Unknown)
 // Handles a brace expression parameter after the initial designator. This
 // should be at a `:` or `=`, depending on whether it's a type or value literal.
 //
+// { .foo = ref bar ... }
+//        ^~~~~
+//   1. Expr
+//   2. RefTagFinishAsRegular
+//   3. BraceExprParamFinishAsValue
+//
 // { .foo = bar ... }
 //        ^
 //   1. Expr
@@ -167,31 +173,35 @@ CARBON_PARSE_STATE_VARIANTS3(BraceExprParamFinish, Type, Value, Unknown)
 //   (state done)
 CARBON_PARSE_STATE_VARIANTS3(BraceExprFinish, Type, Value, Unknown)
 
-// Handles a call expression `(...)`.
+// Handles the end of an argument expression tagged with `ref`. The
+// `AfterOpenParen` variant diagnoses if the expression is not followed by `,`.
+//
+// ref ...
+//        ^
+//   (state done)
+CARBON_PARSE_STATE_VARIANTS2(RefTagFinish, Regular, AfterOpenParen)
+
+// Handles a call expression `(...)`, including the initial `ref`, if any.
 //
 // F()
 //  ^
 //   1. CallExprFinish
 //
+// F(ref ...
+//  ^~~~
+//
+// 1. Expr
+// 2. RefTagFinishAsRegular
+// 2. TupleLiteralElementFinish
+// 3. CallExprFinish
+//
 // F( ...
 //  ^
 //   1. Expr
-//   2. CallExprParamFinish
+//   2. TupleLiteralElementFinish
 //   3. CallExprFinish
 CARBON_PARSE_STATE(CallExpr)
 
-// Handles the `,` or `)` after a call parameter.
-//
-// F(a, ...)
-//    ^
-//   1. Expr
-//   2. CallExprParamFinish
-//
-// F(a )
-//    ^
-//   (state done)
-CARBON_PARSE_STATE(CallExprParamFinish)
-
 // Handles finishing the call expression.
 //
 // F(a, b)
@@ -924,17 +934,29 @@ CARBON_PARSE_STATE(OnlyParenExpr)
 //       ^
 CARBON_PARSE_STATE(OnlyParenExprFinish)
 
-// Handles the `(` of a parenthesized single expression
+// Handles the `(` of an expression that's presumed to be a parenthesized
+// single expression, but may later be reclassified as a tuple literal.
+// This also handles a `ref` following the `(`, if present. That's only
+// valid in tuple literals, but doesn't cause the expression to be a tuple
+// literal.
 //
 // ( )
 // ^
 //   1. TupleLiteralFinish
 //
+// (ref ... )
+// ^~~~
+//
+//   1. Expr
+//   2. RefTagFinishAsAfterOpenParen
+//   3. ExprAfterOpenParenFinish
+//   4. ParenExprFinish             (SPECIAL: may be replaced)
+//
 // ( ... )
 // ^
 //   1. Expr
 //   2. ExprAfterOpenParenFinish
-//   3. ParenExprFinish    (SPECIAL: may be replaced)
+//   3. ParenExprFinish             (SPECIAL: may be replaced)
 CARBON_PARSE_STATE(ParenExpr)
 
 // Handles the `)` of a tuple literal.
@@ -951,6 +973,13 @@ CARBON_PARSE_STATE(TupleLiteralFinish)
 //   (state done)
 //   SPECIAL: parent becomes TupleLiteralFinish
 //
+// ( ... , ref ... )
+//       ^~~~~
+//   1. Expr
+//   2. RefTagFinishAsRegular
+//   3. TupleLiteralElementFinish
+//   SPECIAL: parent becomes TupleLiteralFinish
+//
 // ( ... , ... )
 //       ^
 //   1. Expr
@@ -969,6 +998,12 @@ CARBON_PARSE_STATE(ExprAfterOpenParenFinish)
 //       ^
 //   (state done)
 //
+// ( ... , ref ... )
+//       ^~~~~
+//   1. Expr
+//   2. RefTagFinishAsRegular
+//   3. TupleLiteralElementFinish
+//
 // ( ... , ... )
 //       ^
 //   1. Expr

+ 1 - 1
toolchain/parse/testdata/basics/function_call.carbon

@@ -30,7 +30,7 @@ fn F() {
 // CHECK:STDOUT:                   {kind: 'IdentifierNameExpr', text: 'c'},
 // CHECK:STDOUT:                   {kind: 'IdentifierNameNotBeforeParams', text: 'd'},
 // CHECK:STDOUT:                 {kind: 'MemberAccessExpr', text: '.', subtree_size: 3},
-// CHECK:STDOUT:                 {kind: 'CallExprComma', text: ','},
+// CHECK:STDOUT:                 {kind: 'TupleLiteralComma', text: ','},
 // CHECK:STDOUT:                   {kind: 'ParenExprStart', text: '('},
 // CHECK:STDOUT:                   {kind: 'IdentifierNameExpr', text: 'e'},
 // CHECK:STDOUT:                 {kind: 'ParenExpr', text: ')', subtree_size: 3},

+ 201 - 0
toolchain/parse/testdata/function/call.carbon

@@ -0,0 +1,201 @@
+// Part of the Carbon Language project, under the Apache License v2.0 with LLVM
+// Exceptions. See /LICENSE for license information.
+// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+//
+// AUTOUPDATE
+// TIP: To test this file alone, run:
+// TIP:   bazel test //toolchain/testing:file_test --test_arg=--file_tests=toolchain/parse/testdata/function/call.carbon
+// TIP: To dump output, run:
+// TIP:   bazel run //toolchain/testing:file_test -- --dump_output --file_tests=toolchain/parse/testdata/function/call.carbon
+
+// --- basic.carbon
+
+fn F() {
+  a.b.f(c.d, (e)).g();
+}
+
+// --- ref_args.carbon
+
+fn F() {
+  G(ref x);
+  G(ref x, y);
+  G(x, ref y);
+}
+
+// --- nested_ref.carbon
+
+fn F() {
+  G(x, (ref x,));
+  G(x, (ref y, z));
+  G(x, (y, ref z));
+
+  G(x, {.y = ref y, .z = z});
+}
+
+// --- fail_ref_out_of_place.carbon
+
+fn F() {
+  (ref x,);
+  G(ref x);
+  // CHECK:STDERR: fail_ref_out_of_place.carbon:[[@LINE+4]]:4: error: found `ref` in unexpected position [UnexpectedRef]
+  // CHECK:STDERR:   (ref x);
+  // CHECK:STDERR:    ^~~
+  // CHECK:STDERR:
+  (ref x);
+}
+
+// CHECK:STDOUT: - filename: basic.carbon
+// CHECK:STDOUT:   parse_tree: [
+// CHECK:STDOUT:     {kind: 'FileStart', text: ''},
+// CHECK:STDOUT:         {kind: 'FunctionIntroducer', text: 'fn'},
+// CHECK:STDOUT:         {kind: 'IdentifierNameBeforeParams', text: 'F'},
+// CHECK:STDOUT:           {kind: 'ExplicitParamListStart', text: '('},
+// CHECK:STDOUT:         {kind: 'ExplicitParamList', text: ')', subtree_size: 2},
+// CHECK:STDOUT:       {kind: 'FunctionDefinitionStart', text: '{', subtree_size: 5},
+// CHECK:STDOUT:                       {kind: 'IdentifierNameExpr', text: 'a'},
+// CHECK:STDOUT:                       {kind: 'IdentifierNameNotBeforeParams', text: 'b'},
+// CHECK:STDOUT:                     {kind: 'MemberAccessExpr', text: '.', subtree_size: 3},
+// CHECK:STDOUT:                     {kind: 'IdentifierNameNotBeforeParams', text: 'f'},
+// CHECK:STDOUT:                   {kind: 'MemberAccessExpr', text: '.', subtree_size: 5},
+// CHECK:STDOUT:                 {kind: 'CallExprStart', text: '(', subtree_size: 6},
+// CHECK:STDOUT:                   {kind: 'IdentifierNameExpr', text: 'c'},
+// CHECK:STDOUT:                   {kind: 'IdentifierNameNotBeforeParams', text: 'd'},
+// CHECK:STDOUT:                 {kind: 'MemberAccessExpr', text: '.', subtree_size: 3},
+// CHECK:STDOUT:                 {kind: 'TupleLiteralComma', text: ','},
+// CHECK:STDOUT:                   {kind: 'ParenExprStart', text: '('},
+// CHECK:STDOUT:                   {kind: 'IdentifierNameExpr', text: 'e'},
+// CHECK:STDOUT:                 {kind: 'ParenExpr', text: ')', subtree_size: 3},
+// CHECK:STDOUT:               {kind: 'CallExpr', text: ')', subtree_size: 14},
+// CHECK:STDOUT:               {kind: 'IdentifierNameNotBeforeParams', text: 'g'},
+// CHECK:STDOUT:             {kind: 'MemberAccessExpr', text: '.', subtree_size: 16},
+// CHECK:STDOUT:           {kind: 'CallExprStart', text: '(', subtree_size: 17},
+// CHECK:STDOUT:         {kind: 'CallExpr', text: ')', subtree_size: 18},
+// CHECK:STDOUT:       {kind: 'ExprStatement', text: ';', subtree_size: 19},
+// CHECK:STDOUT:     {kind: 'FunctionDefinition', text: '}', subtree_size: 25},
+// CHECK:STDOUT:     {kind: 'FileEnd', text: ''},
+// CHECK:STDOUT:   ]
+// CHECK:STDOUT: - filename: ref_args.carbon
+// CHECK:STDOUT:   parse_tree: [
+// CHECK:STDOUT:     {kind: 'FileStart', text: ''},
+// CHECK:STDOUT:         {kind: 'FunctionIntroducer', text: 'fn'},
+// CHECK:STDOUT:         {kind: 'IdentifierNameBeforeParams', text: 'F'},
+// CHECK:STDOUT:           {kind: 'ExplicitParamListStart', text: '('},
+// CHECK:STDOUT:         {kind: 'ExplicitParamList', text: ')', subtree_size: 2},
+// CHECK:STDOUT:       {kind: 'FunctionDefinitionStart', text: '{', subtree_size: 5},
+// CHECK:STDOUT:             {kind: 'IdentifierNameExpr', text: 'G'},
+// CHECK:STDOUT:           {kind: 'CallExprStart', text: '(', subtree_size: 2},
+// CHECK:STDOUT:             {kind: 'IdentifierNameExpr', text: 'x'},
+// CHECK:STDOUT:           {kind: 'RefTag', text: 'ref', subtree_size: 2},
+// CHECK:STDOUT:         {kind: 'CallExpr', text: ')', subtree_size: 5},
+// CHECK:STDOUT:       {kind: 'ExprStatement', text: ';', subtree_size: 6},
+// CHECK:STDOUT:             {kind: 'IdentifierNameExpr', text: 'G'},
+// CHECK:STDOUT:           {kind: 'CallExprStart', text: '(', subtree_size: 2},
+// CHECK:STDOUT:             {kind: 'IdentifierNameExpr', text: 'x'},
+// CHECK:STDOUT:           {kind: 'RefTag', text: 'ref', subtree_size: 2},
+// CHECK:STDOUT:           {kind: 'TupleLiteralComma', text: ','},
+// CHECK:STDOUT:           {kind: 'IdentifierNameExpr', text: 'y'},
+// CHECK:STDOUT:         {kind: 'CallExpr', text: ')', subtree_size: 7},
+// CHECK:STDOUT:       {kind: 'ExprStatement', text: ';', subtree_size: 8},
+// CHECK:STDOUT:             {kind: 'IdentifierNameExpr', text: 'G'},
+// CHECK:STDOUT:           {kind: 'CallExprStart', text: '(', subtree_size: 2},
+// CHECK:STDOUT:           {kind: 'IdentifierNameExpr', text: 'x'},
+// CHECK:STDOUT:           {kind: 'TupleLiteralComma', text: ','},
+// CHECK:STDOUT:             {kind: 'IdentifierNameExpr', text: 'y'},
+// CHECK:STDOUT:           {kind: 'RefTag', text: 'ref', subtree_size: 2},
+// CHECK:STDOUT:         {kind: 'CallExpr', text: ')', subtree_size: 7},
+// CHECK:STDOUT:       {kind: 'ExprStatement', text: ';', subtree_size: 8},
+// CHECK:STDOUT:     {kind: 'FunctionDefinition', text: '}', subtree_size: 28},
+// CHECK:STDOUT:     {kind: 'FileEnd', text: ''},
+// CHECK:STDOUT:   ]
+// CHECK:STDOUT: - filename: nested_ref.carbon
+// CHECK:STDOUT:   parse_tree: [
+// CHECK:STDOUT:     {kind: 'FileStart', text: ''},
+// CHECK:STDOUT:         {kind: 'FunctionIntroducer', text: 'fn'},
+// CHECK:STDOUT:         {kind: 'IdentifierNameBeforeParams', text: 'F'},
+// CHECK:STDOUT:           {kind: 'ExplicitParamListStart', text: '('},
+// CHECK:STDOUT:         {kind: 'ExplicitParamList', text: ')', subtree_size: 2},
+// CHECK:STDOUT:       {kind: 'FunctionDefinitionStart', text: '{', subtree_size: 5},
+// CHECK:STDOUT:             {kind: 'IdentifierNameExpr', text: 'G'},
+// CHECK:STDOUT:           {kind: 'CallExprStart', text: '(', subtree_size: 2},
+// CHECK:STDOUT:           {kind: 'IdentifierNameExpr', text: 'x'},
+// CHECK:STDOUT:           {kind: 'TupleLiteralComma', text: ','},
+// CHECK:STDOUT:             {kind: 'TupleLiteralStart', text: '('},
+// CHECK:STDOUT:               {kind: 'IdentifierNameExpr', text: 'x'},
+// CHECK:STDOUT:             {kind: 'RefTag', text: 'ref', subtree_size: 2},
+// CHECK:STDOUT:             {kind: 'TupleLiteralComma', text: ','},
+// CHECK:STDOUT:           {kind: 'TupleLiteral', text: ')', subtree_size: 5},
+// CHECK:STDOUT:         {kind: 'CallExpr', text: ')', subtree_size: 10},
+// CHECK:STDOUT:       {kind: 'ExprStatement', text: ';', subtree_size: 11},
+// CHECK:STDOUT:             {kind: 'IdentifierNameExpr', text: 'G'},
+// CHECK:STDOUT:           {kind: 'CallExprStart', text: '(', subtree_size: 2},
+// CHECK:STDOUT:           {kind: 'IdentifierNameExpr', text: 'x'},
+// CHECK:STDOUT:           {kind: 'TupleLiteralComma', text: ','},
+// CHECK:STDOUT:             {kind: 'TupleLiteralStart', text: '('},
+// CHECK:STDOUT:               {kind: 'IdentifierNameExpr', text: 'y'},
+// CHECK:STDOUT:             {kind: 'RefTag', text: 'ref', subtree_size: 2},
+// CHECK:STDOUT:             {kind: 'TupleLiteralComma', text: ','},
+// CHECK:STDOUT:             {kind: 'IdentifierNameExpr', text: 'z'},
+// CHECK:STDOUT:           {kind: 'TupleLiteral', text: ')', subtree_size: 6},
+// CHECK:STDOUT:         {kind: 'CallExpr', text: ')', subtree_size: 11},
+// CHECK:STDOUT:       {kind: 'ExprStatement', text: ';', subtree_size: 12},
+// CHECK:STDOUT:             {kind: 'IdentifierNameExpr', text: 'G'},
+// CHECK:STDOUT:           {kind: 'CallExprStart', text: '(', subtree_size: 2},
+// CHECK:STDOUT:           {kind: 'IdentifierNameExpr', text: 'x'},
+// CHECK:STDOUT:           {kind: 'TupleLiteralComma', text: ','},
+// CHECK:STDOUT:             {kind: 'TupleLiteralStart', text: '('},
+// CHECK:STDOUT:             {kind: 'IdentifierNameExpr', text: 'y'},
+// CHECK:STDOUT:             {kind: 'TupleLiteralComma', text: ','},
+// CHECK:STDOUT:               {kind: 'IdentifierNameExpr', text: 'z'},
+// CHECK:STDOUT:             {kind: 'RefTag', text: 'ref', subtree_size: 2},
+// CHECK:STDOUT:           {kind: 'TupleLiteral', text: ')', subtree_size: 6},
+// CHECK:STDOUT:         {kind: 'CallExpr', text: ')', subtree_size: 11},
+// CHECK:STDOUT:       {kind: 'ExprStatement', text: ';', subtree_size: 12},
+// CHECK:STDOUT:             {kind: 'IdentifierNameExpr', text: 'G'},
+// CHECK:STDOUT:           {kind: 'CallExprStart', text: '(', subtree_size: 2},
+// CHECK:STDOUT:           {kind: 'IdentifierNameExpr', text: 'x'},
+// CHECK:STDOUT:           {kind: 'TupleLiteralComma', text: ','},
+// CHECK:STDOUT:             {kind: 'StructLiteralStart', text: '{'},
+// CHECK:STDOUT:                 {kind: 'IdentifierNameNotBeforeParams', text: 'y'},
+// CHECK:STDOUT:               {kind: 'StructFieldDesignator', text: '.', subtree_size: 2},
+// CHECK:STDOUT:                 {kind: 'IdentifierNameExpr', text: 'y'},
+// CHECK:STDOUT:               {kind: 'RefTag', text: 'ref', subtree_size: 2},
+// CHECK:STDOUT:             {kind: 'StructLiteralField', text: '=', subtree_size: 5},
+// CHECK:STDOUT:             {kind: 'StructLiteralComma', text: ','},
+// CHECK:STDOUT:                 {kind: 'IdentifierNameNotBeforeParams', text: 'z'},
+// CHECK:STDOUT:               {kind: 'StructFieldDesignator', text: '.', subtree_size: 2},
+// CHECK:STDOUT:               {kind: 'IdentifierNameExpr', text: 'z'},
+// CHECK:STDOUT:             {kind: 'StructLiteralField', text: '=', subtree_size: 4},
+// CHECK:STDOUT:           {kind: 'StructLiteral', text: '}', subtree_size: 12},
+// CHECK:STDOUT:         {kind: 'CallExpr', text: ')', subtree_size: 17},
+// CHECK:STDOUT:       {kind: 'ExprStatement', text: ';', subtree_size: 18},
+// CHECK:STDOUT:     {kind: 'FunctionDefinition', text: '}', subtree_size: 59},
+// CHECK:STDOUT:     {kind: 'FileEnd', text: ''},
+// CHECK:STDOUT:   ]
+// CHECK:STDOUT: - filename: fail_ref_out_of_place.carbon
+// CHECK:STDOUT:   parse_tree: [
+// CHECK:STDOUT:     {kind: 'FileStart', text: ''},
+// CHECK:STDOUT:         {kind: 'FunctionIntroducer', text: 'fn'},
+// CHECK:STDOUT:         {kind: 'IdentifierNameBeforeParams', text: 'F'},
+// CHECK:STDOUT:           {kind: 'ExplicitParamListStart', text: '('},
+// CHECK:STDOUT:         {kind: 'ExplicitParamList', text: ')', subtree_size: 2},
+// CHECK:STDOUT:       {kind: 'FunctionDefinitionStart', text: '{', subtree_size: 5},
+// CHECK:STDOUT:           {kind: 'TupleLiteralStart', text: '('},
+// CHECK:STDOUT:             {kind: 'IdentifierNameExpr', text: 'x'},
+// CHECK:STDOUT:           {kind: 'RefTag', text: 'ref', subtree_size: 2},
+// CHECK:STDOUT:           {kind: 'TupleLiteralComma', text: ','},
+// CHECK:STDOUT:         {kind: 'TupleLiteral', text: ')', subtree_size: 5},
+// CHECK:STDOUT:       {kind: 'ExprStatement', text: ';', subtree_size: 6},
+// CHECK:STDOUT:             {kind: 'IdentifierNameExpr', text: 'G'},
+// CHECK:STDOUT:           {kind: 'CallExprStart', text: '(', subtree_size: 2},
+// CHECK:STDOUT:             {kind: 'IdentifierNameExpr', text: 'x'},
+// CHECK:STDOUT:           {kind: 'RefTag', text: 'ref', subtree_size: 2},
+// CHECK:STDOUT:         {kind: 'CallExpr', text: ')', subtree_size: 5},
+// CHECK:STDOUT:       {kind: 'ExprStatement', text: ';', subtree_size: 6},
+// CHECK:STDOUT:           {kind: 'ParenExprStart', text: '('},
+// CHECK:STDOUT:             {kind: 'IdentifierNameExpr', text: 'x'},
+// CHECK:STDOUT:           {kind: 'RefTag', text: 'ref', subtree_size: 2},
+// CHECK:STDOUT:         {kind: 'ParenExpr', text: ')', subtree_size: 4},
+// CHECK:STDOUT:       {kind: 'ExprStatement', text: ';', subtree_size: 5},
+// CHECK:STDOUT:     {kind: 'FunctionDefinition', text: '}', subtree_size: 23},
+// CHECK:STDOUT:     {kind: 'FileEnd', text: ''},
+// CHECK:STDOUT:   ]

+ 1 - 1
toolchain/parse/testdata/function/definition.carbon

@@ -107,7 +107,7 @@ fn TestRecoveryFromSpuriousEquals();
 // CHECK:STDOUT:             {kind: 'IdentifierNameExpr', text: 'foo'},
 // CHECK:STDOUT:           {kind: 'CallExprStart', text: '(', subtree_size: 2},
 // CHECK:STDOUT:           {kind: 'IdentifierNameExpr', text: 'baz'},
-// CHECK:STDOUT:           {kind: 'CallExprComma', text: ','},
+// CHECK:STDOUT:           {kind: 'TupleLiteralComma', text: ','},
 // CHECK:STDOUT:             {kind: 'IdentifierNameExpr', text: 'bar'},
 // CHECK:STDOUT:             {kind: 'IdentifierNameExpr', text: 'baz'},
 // CHECK:STDOUT:           {kind: 'InfixOperatorPlus', text: '+', subtree_size: 3},

+ 1 - 1
toolchain/parse/testdata/operators/fail_postfix_space_before_comma.carbon

@@ -27,7 +27,7 @@ var n: i8 = F(i8 *, 0);
 // CHECK:STDOUT:         {kind: 'CallExprStart', text: '(', subtree_size: 2},
 // CHECK:STDOUT:           {kind: 'IntTypeLiteral', text: 'i8'},
 // CHECK:STDOUT:         {kind: 'PostfixOperatorStar', text: '*', subtree_size: 2},
-// CHECK:STDOUT:         {kind: 'CallExprComma', text: ','},
+// CHECK:STDOUT:         {kind: 'TupleLiteralComma', text: ','},
 // CHECK:STDOUT:         {kind: 'IntLiteral', text: '0'},
 // CHECK:STDOUT:       {kind: 'CallExpr', text: ')', subtree_size: 7},
 // CHECK:STDOUT:     {kind: 'VariableDecl', text: ';', subtree_size: 14},

+ 1 - 1
toolchain/parse/testdata/operators/fixity_in_call.carbon

@@ -24,7 +24,7 @@ fn F() {
 // CHECK:STDOUT:           {kind: 'CallExprStart', text: '(', subtree_size: 2},
 // CHECK:STDOUT:             {kind: 'IntTypeLiteral', text: 'i32'},
 // CHECK:STDOUT:           {kind: 'PostfixOperatorStar', text: '*', subtree_size: 2},
-// CHECK:STDOUT:           {kind: 'CallExprComma', text: ','},
+// CHECK:STDOUT:           {kind: 'TupleLiteralComma', text: ','},
 // CHECK:STDOUT:             {kind: 'IdentifierNameExpr', text: 'n'},
 // CHECK:STDOUT:             {kind: 'IdentifierNameExpr', text: 'n'},
 // CHECK:STDOUT:           {kind: 'InfixOperatorStar', text: '*', subtree_size: 3},

+ 10 - 3
toolchain/parse/typed_nodes.h

@@ -996,18 +996,25 @@ struct CallExprStart {
   Lex::OpenParenTokenIndex token;
 };
 
-using CallExprComma = LeafNode<NodeKind::CallExprComma, Lex::CommaTokenIndex>;
-
 // A call expression: `F(a, b, c)`.
 struct CallExpr {
   static constexpr auto Kind = NodeKind::CallExpr.Define(
       {.category = NodeCategory::Expr, .bracketed_by = CallExprStart::Kind});
 
   CallExprStartId start;
-  CommaSeparatedList<AnyExprId, CallExprCommaId> arguments;
+  CommaSeparatedList<AnyExprId, TupleLiteralCommaId> arguments;
   Lex::CloseParenTokenIndex token;
 };
 
+// A callsite `ref` tag: `F(ref x)` or `F({.x = ref x})
+struct RefTag {
+  static constexpr auto Kind = NodeKind::RefTag.Define(
+      {.category = NodeCategory::Expr, .child_count = 1});
+
+  Lex::RefTokenIndex token;
+  AnyExprId tagged_expr;
+};
+
 // A member access expression: `a.b` or `a.(b)`.
 struct MemberAccessExpr {
   static constexpr auto Kind = NodeKind::MemberAccessExpr.Define(

+ 1 - 0
toolchain/sem_ir/expr_info.cpp

@@ -47,6 +47,7 @@ auto GetExprCategory(const File& file, InstId inst_id) -> ExprCategory {
       case TuplePattern::Kind:
       case ValueBindingPattern::Kind:
       case ValueParamPattern::Kind:
+      case VarParamPattern::Kind:
       case VarPattern::Kind:
         return ExprCategory::NotExpr;
 

+ 37 - 0
toolchain/sem_ir/file.cpp

@@ -157,6 +157,43 @@ auto File::OutputYaml(bool include_singletons) const -> Yaml::OutputMapping {
   });
 }
 
+auto File::CollectRefTagsNeeded() const -> Set<SemIR::InstId> {
+  CARBON_CHECK(!has_errors_);
+  Set<SemIR::InstId> ref_tags_needed;
+  for (auto [id, inst] : insts_.enumerate()) {
+    if (inst.kind() != SemIR::InstKind::Call) {
+      continue;
+    }
+    auto call_inst = inst.As<SemIR::Call>();
+    auto callee = SemIR::GetCallee(*this, call_inst.callee_id);
+    CARBON_KIND_SWITCH(callee) {
+      case CARBON_KIND(SemIR::CalleeError _):
+        break;
+      case CARBON_KIND(SemIR::CalleeNonFunction _):
+        break;
+      case CARBON_KIND(SemIR::CalleeCppOverloadSet _): {
+        // TODO: Perform validation here once we model C++ ref parameters as
+        // Carbon ref parameters.
+        break;
+      }
+      case CARBON_KIND(SemIR::CalleeFunction fn): {
+        auto function = functions_.Get(fn.function_id);
+        auto args = inst_blocks_.GetOrEmpty(call_inst.args_id);
+        for (auto param_id : llvm::concat<const InstId>(
+                 inst_blocks_.GetOrEmpty(function.implicit_param_patterns_id),
+                 inst_blocks_.GetOrEmpty(function.param_patterns_id))) {
+          if (auto ref_param_pattern =
+                  insts_.TryGetAs<SemIR::RefParamPattern>(param_id)) {
+            ref_tags_needed.Insert(args[ref_param_pattern->index.index]);
+          }
+        }
+        break;
+      }
+    }
+  }
+  return ref_tags_needed;
+}
+
 auto File::CollectMemUsage(MemUsage& mem_usage, llvm::StringRef label) const
     -> void {
   mem_usage.Collect(MemUsage::ConcatLabel(label, "allocator_"), allocator_);

+ 6 - 0
toolchain/sem_ir/file.h

@@ -94,6 +94,12 @@ class File : public Printable<File> {
   }
   auto OutputYaml(bool include_singletons) const -> Yaml::OutputMapping;
 
+  // Returns the set of all insts corresponding to expressions that are used
+  // in positions where a `ref` tag is needed. Should only be called if
+  // has_errors is false. This is intended for validation purposes, and should
+  // only be called if !NDEBUG, because it walks the entire IR.
+  auto CollectRefTagsNeeded() const -> Set<SemIR::InstId>;
+
   // Collects memory usage of members.
   auto CollectMemUsage(MemUsage& mem_usage, llvm::StringRef label) const
       -> void;

+ 3 - 4
toolchain/sem_ir/inst_categories.h

@@ -157,11 +157,10 @@ struct AnyParam {
 };
 
 // A pattern that represents a `Call` parameter. It delegates to subpattern_id
-// in pattern matching. The sub-kinds differ only in the expression category
-// of the corresponding parameter inst.
+// in pattern matching.
 struct AnyParamPattern {
-  using CategoryInfo =
-      CategoryOf<OutParamPattern, RefParamPattern, ValueParamPattern>;
+  using CategoryInfo = CategoryOf<OutParamPattern, RefParamPattern,
+                                  ValueParamPattern, VarParamPattern>;
 
   InstKind kind;
 

+ 1 - 0
toolchain/sem_ir/inst_kind.def

@@ -153,6 +153,7 @@ CARBON_SEM_IR_INST_KIND(ValueBindingPattern)
 CARBON_SEM_IR_INST_KIND(ValueOfInitializer)
 CARBON_SEM_IR_INST_KIND(ValueParam)
 CARBON_SEM_IR_INST_KIND(ValueParamPattern)
+CARBON_SEM_IR_INST_KIND(VarParamPattern)
 CARBON_SEM_IR_INST_KIND(VarPattern)
 CARBON_SEM_IR_INST_KIND(VarStorage)
 CARBON_SEM_IR_INST_KIND(VtableDecl)

+ 2 - 1
toolchain/sem_ir/inst_namer.cpp

@@ -1153,7 +1153,8 @@ auto InstNamer::NamingContext::NameInst() -> void {
     }
     case OutParamPattern::Kind:
     case RefParamPattern::Kind:
-    case ValueParamPattern::Kind: {
+    case ValueParamPattern::Kind:
+    case VarParamPattern::Kind: {
       AddInstNameId(GetPrettyNameFromPatternId(sem_ir(), inst_id_),
                     ".param_patt");
       return;

+ 18 - 2
toolchain/sem_ir/typed_insts.h

@@ -1296,7 +1296,8 @@ struct RefBindingPattern {
   EntityNameId entity_name_id;
 };
 
-// A by-reference `Call` parameter. See AnyParam for member documentation.
+// A by-reference `Call` parameter. See AnyParam for member documentation. Note
+// that this may correspond to either a RefParamPattern or a VarParamPattern.
 struct RefParam {
   // TODO: Make Parse::NodeId more specific.
   static constexpr auto Kind = InstKind::RefParam.Define<Parse::NodeId>(
@@ -1307,7 +1308,7 @@ struct RefParam {
   NameId pretty_name_id;
 };
 
-// A pattern that represents a by-reference `Call` parameter. See
+// A pattern that represents a `ref`-qualified `Call` parameter. See
 // `AnyParamPattern` for member documentation.
 struct RefParamPattern {
   // TODO: Make Parse::NodeId more specific.
@@ -1912,6 +1913,21 @@ struct ValueParamPattern {
   CallParamIndex index;
 };
 
+// A pattern that represents a `Call` parameter corresponding to a `var`
+// pattern. See `AnyParamPattern` for member documentation. Note that there is
+// no `VarParam` -- a `VarParamPattern` corresponds to a `RefParam`.
+struct VarParamPattern {
+  static constexpr auto Kind =
+      InstKind::VarParamPattern.Define<Parse::VariablePatternId>(
+          {.ir_name = "var_param_pattern",
+           .constant_kind = InstConstantKind::AlwaysUnique,
+           .is_lowered = false});
+
+  TypeId type_id;
+  InstId subpattern_id;
+  CallParamIndex index;
+};
+
 // A `var` pattern.
 struct VarPattern {
   static constexpr auto Kind =