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

Add declaration checking for `fn destroy` (#5127)

This adds support for the `destroy` name, and checking of `fn destroy`
structure. It does not add support for the actual destructor calls.
Jon Ross-Perkins 1 год назад
Родитель
Сommit
216c499cbf

+ 101 - 9
toolchain/check/handle_function.cpp

@@ -23,6 +23,7 @@
 #include "toolchain/check/name_lookup.h"
 #include "toolchain/check/type.h"
 #include "toolchain/check/type_completion.h"
+#include "toolchain/lex/token_kind.h"
 #include "toolchain/parse/node_ids.h"
 #include "toolchain/sem_ir/builtin_function_kind.h"
 #include "toolchain/sem_ir/entry_point.h"
@@ -125,7 +126,7 @@ static auto DiagnoseModifiers(Context& context,
   if (!self_param_id.has_value() &&
       introducer.modifier_set.HasAnyOf(KeywordModifierSet::Method)) {
     CARBON_DIAGNOSTIC(VirtualWithoutSelf, Error, "virtual class function");
-    context.emitter().Build(node_id, VirtualWithoutSelf).Emit();
+    context.emitter().Emit(node_id, VirtualWithoutSelf);
     introducer.modifier_set.Remove(KeywordModifierSet::Method);
   }
 }
@@ -329,7 +330,7 @@ static auto RequestVtableIfVirtual(
   if (virtual_modifier == SemIR::Function::VirtualModifier::Impl &&
       !class_info.base_id.has_value()) {
     CARBON_DIAGNOSTIC(ImplWithoutBase, Error, "impl without base class");
-    context.emitter().Build(node_id, ImplWithoutBase).Emit();
+    context.emitter().Emit(node_id, ImplWithoutBase);
   }
   // TODO: If this is an `impl` function, check there's a matching base
   // function that's impl or virtual.
@@ -337,6 +338,92 @@ static auto RequestVtableIfVirtual(
   context.vtable_stack().AddInstId(decl_id);
 }
 
+// Validates the `destroy` function's signature. May replace invalid values for
+// recovery.
+static auto ValidateIfDestroy(Context& context, bool is_redecl,
+                              std::optional<SemIR::Inst> parent_scope_inst,
+                              SemIR::Function& function_info) -> void {
+  if (function_info.name_id != SemIR::NameId::Destroy) {
+    return;
+  }
+
+  // For recovery, always force explicit parameters to be empty. We do this
+  // before any of the returns for simplicity.
+  auto orig_param_patterns_id = function_info.param_patterns_id;
+  function_info.param_patterns_id = SemIR::InstBlockId::Empty;
+
+  // Use differences on merge to diagnose remaining issues.
+  if (is_redecl) {
+    return;
+  }
+
+  if (!parent_scope_inst || !parent_scope_inst->Is<SemIR::ClassDecl>()) {
+    CARBON_DIAGNOSTIC(DestroyFunctionOutsideClass, Error,
+                      "declaring `fn destroy` in non-class scope");
+    context.emitter().Emit(function_info.latest_decl_id(),
+                           DestroyFunctionOutsideClass);
+    return;
+  }
+
+  if (!function_info.self_param_id.has_value()) {
+    CARBON_DIAGNOSTIC(DestroyFunctionMissingSelf, Error,
+                      "missing implicit `self` parameter");
+    context.emitter().Emit(function_info.latest_decl_id(),
+                           DestroyFunctionMissingSelf);
+    return;
+  }
+
+  // `self` must be the only implicit parameter.
+  if (auto block =
+          context.inst_blocks().Get(function_info.implicit_param_patterns_id);
+      block.size() > 1) {
+    // Point at the first non-`self` parameter.
+    auto param_id = block[function_info.self_param_id == block[0] ? 1 : 0];
+    CARBON_DIAGNOSTIC(DestroyFunctionUnexpectedImplicitParam, Error,
+                      "unexpected implicit parameter");
+    context.emitter().Emit(param_id, DestroyFunctionUnexpectedImplicitParam);
+    return;
+  }
+
+  CARBON_CHECK(
+      orig_param_patterns_id.has_value(),
+      "TODO: Positional parameters are currently rejected as part of requiring "
+      "implicit parameters, because it's an invalid parse tree; add a "
+      "diagnostic once this is testable");
+
+  if (orig_param_patterns_id != SemIR::InstBlockId::Empty) {
+    CARBON_DIAGNOSTIC(DestroyFunctionNonEmptyExplicitParams, Error,
+                      "unexpected parameter");
+    context.emitter().Emit(context.inst_blocks().Get(orig_param_patterns_id)[0],
+                           DestroyFunctionNonEmptyExplicitParams);
+    return;
+  }
+
+  if (auto return_type_id =
+          function_info.GetDeclaredReturnType(context.sem_ir());
+      return_type_id.has_value() &&
+      return_type_id != GetTupleType(context, {})) {
+    CARBON_DIAGNOSTIC(DestroyFunctionIncorrectReturnType, Error,
+                      "incorrect return type; must be unspecified or `()`");
+    context.emitter().Emit(function_info.return_slot_pattern_id,
+                           DestroyFunctionIncorrectReturnType);
+    return;
+  }
+}
+
+// Diagnoses when positional params aren't supported. Reassigns the pattern
+// block if needed.
+static auto DiagnosePositionalParams(Context& context,
+                                     SemIR::Function& function_info) -> void {
+  if (function_info.param_patterns_id.has_value()) {
+    return;
+  }
+
+  context.TODO(function_info.latest_decl_id(),
+               "function with positional parameters");
+  function_info.param_patterns_id = SemIR::InstBlockId::Empty;
+}
+
 // Build a FunctionDecl describing the signature of a function. This
 // handles the common logic shared by function declaration syntax and function
 // definition syntax.
@@ -352,17 +439,14 @@ static auto BuildFunctionDecl(Context& context,
   }
 
   auto name = PopNameComponent(context, return_slot_pattern_id);
-  if (!name.param_patterns_id.has_value()) {
-    context.TODO(node_id, "function with positional parameters");
-    name.param_patterns_id = SemIR::InstBlockId::Empty;
-  }
-  auto self_param_id =
-      FindSelfPattern(context, name.implicit_param_patterns_id);
-
   auto name_context = context.decl_name_stack().FinishName(name);
+
   context.node_stack()
       .PopAndDiscardSoloNodeId<Parse::NodeKind::FunctionIntroducer>();
 
+  auto self_param_id =
+      FindSelfPattern(context, name.implicit_param_patterns_id);
+
   // Process modifiers.
   auto [parent_scope_inst_id, parent_scope_inst] =
       context.name_scopes().GetInstIfValid(name_context.parent_scope_id);
@@ -394,6 +478,14 @@ static auto BuildFunctionDecl(Context& context,
     function_info.definition_id = decl_id;
   }
 
+  // Analyze standard function signatures before positional parameters, so that
+  // we can have more specific diagnostics and recovery.
+  bool is_redecl =
+      name_context.state == DeclNameStack::NameContext::State::Resolved;
+  ValidateIfDestroy(context, is_redecl, parent_scope_inst, function_info);
+
+  DiagnosePositionalParams(context, function_info);
+
   TryMergeRedecl(context, node_id, name_context, function_decl, function_info,
                  is_definition);
 

+ 53 - 22
toolchain/check/handle_name.cpp

@@ -91,8 +91,12 @@ auto HandleParseNode(Context& context, Parse::PointerMemberAccessExprId node_id)
   return true;
 }
 
-static auto GetIdentifierAsName(Context& context, Parse::NodeId node_id)
-    -> SemIR::NameId {
+// Returns the `NameId` for an identifier node.
+static auto GetIdentifierAsNameId(
+    Context& context, Parse::NodeIdOneOf<Parse::IdentifierNameNotBeforeParamsId,
+                                         Parse::IdentifierNameBeforeParamsId,
+                                         Parse::IdentifierNameExprId>
+                          node_id) -> SemIR::NameId {
   CARBON_CHECK(!context.parse_tree().node_has_error(node_id),
                "TODO: Support checking error parse nodes");
   auto token = context.parse_tree().node_token(node_id);
@@ -129,7 +133,7 @@ static auto HandleNameAsExpr(Context& context, Parse::NodeId node_id,
 auto HandleParseNode(Context& context,
                      Parse::IdentifierNameNotBeforeParamsId node_id) -> bool {
   // The parent is responsible for binding the name.
-  context.node_stack().Push(node_id, GetIdentifierAsName(context, node_id));
+  context.node_stack().Push(node_id, GetIdentifierAsNameId(context, node_id));
   return true;
 }
 
@@ -140,26 +144,48 @@ auto HandleParseNode(Context& context,
   context.full_pattern_stack().PushFullPattern(
       FullPatternStack::Kind::ImplicitParamList);
   // The parent is responsible for binding the name.
-  context.node_stack().Push(node_id, GetIdentifierAsName(context, node_id));
+  context.node_stack().Push(node_id, GetIdentifierAsNameId(context, node_id));
   return true;
 }
 
 auto HandleParseNode(Context& context, Parse::IdentifierNameExprId node_id)
     -> bool {
-  auto name_id = GetIdentifierAsName(context, node_id);
+  auto name_id = GetIdentifierAsNameId(context, node_id);
   context.node_stack().Push(node_id,
                             HandleNameAsExpr(context, node_id, name_id));
   return true;
 }
 
+// Returns the `NameId` for a keyword node.
+static auto GetKeywordAsNameId(
+    Context& context, Parse::NodeIdOneOf<Parse::KeywordNameNotBeforeParamsId,
+                                         Parse::KeywordNameBeforeParamsId>
+                          node_id) -> SemIR::NameId {
+  auto token = context.parse_tree().node_token(node_id);
+  switch (auto token_kind = context.tokens().GetKind(token)) {
+    case Lex::TokenKind::Destroy:
+      return SemIR::NameId::Destroy;
+    default:
+      CARBON_FATAL("Unexpected token kind: {0}", token_kind);
+  }
+}
+
 auto HandleParseNode(Context& context,
                      Parse::KeywordNameNotBeforeParamsId node_id) -> bool {
-  return context.TODO(node_id, "KeywordNameNotBeforeParamsId");
+  // The parent is responsible for binding the name.
+  context.node_stack().Push(node_id, GetKeywordAsNameId(context, node_id));
+  return true;
 }
 
 auto HandleParseNode(Context& context, Parse::KeywordNameBeforeParamsId node_id)
     -> bool {
-  return context.TODO(node_id, "KeywordNameBeforeParamsId");
+  // Push a pattern block stack entry to handle the parameter pattern.
+  context.pattern_block_stack().Push();
+  context.full_pattern_stack().PushFullPattern(
+      FullPatternStack::Kind::ImplicitParamList);
+  // The parent is responsible for binding the name.
+  context.node_stack().Push(node_id, GetKeywordAsNameId(context, node_id));
+  return true;
 }
 
 auto HandleParseNode(Context& context, Parse::BaseNameId node_id) -> bool {
@@ -191,18 +217,34 @@ auto HandleParseNode(Context& context, Parse::SelfValueNameExprId node_id)
   return true;
 }
 
+// Common logic for name qualifiers.
+static auto ApplyNameQualifier(Context& context) -> bool {
+  context.decl_name_stack().ApplyNameQualifier(PopNameComponent(context));
+  return true;
+}
+
 auto HandleParseNode(Context& context,
                      Parse::IdentifierNameQualifierWithParamsId /*node_id*/)
     -> bool {
-  context.decl_name_stack().ApplyNameQualifier(PopNameComponent(context));
-  return true;
+  return ApplyNameQualifier(context);
 }
 
 auto HandleParseNode(Context& context,
                      Parse::IdentifierNameQualifierWithoutParamsId /*node_id*/)
     -> bool {
-  context.decl_name_stack().ApplyNameQualifier(PopNameComponent(context));
-  return true;
+  return ApplyNameQualifier(context);
+}
+
+auto HandleParseNode(Context& context,
+                     Parse::KeywordNameQualifierWithParamsId /*node_id*/)
+    -> bool {
+  return ApplyNameQualifier(context);
+}
+
+auto HandleParseNode(Context& context,
+                     Parse::KeywordNameQualifierWithoutParamsId /*node_id*/)
+    -> bool {
+  return ApplyNameQualifier(context);
 }
 
 auto HandleParseNode(Context& context, Parse::DesignatorExprId node_id)
@@ -241,17 +283,6 @@ auto HandleParseNode(Context& context, Parse::DesignatorExprId node_id)
   return true;
 }
 
-auto HandleParseNode(Context& context,
-                     Parse::KeywordNameQualifierWithParamsId node_id) -> bool {
-  return context.TODO(node_id, "KeywordNameQualifierWithParamsId");
-}
-
-auto HandleParseNode(Context& context,
-                     Parse::KeywordNameQualifierWithoutParamsId node_id)
-    -> bool {
-  return context.TODO(node_id, "KeywordNameQualifierWithoutParamsId");
-}
-
 auto HandleParseNode(Context& context, Parse::PackageExprId node_id) -> bool {
   AddInstAndPush<SemIR::NameRef>(
       context, node_id,

+ 592 - 54
toolchain/check/testdata/class/no_prelude/destroy.carbon

@@ -8,161 +8,430 @@
 // TIP: To dump output, run:
 // TIP:   bazel run //toolchain/testing:file_test -- --dump_output --file_tests=toolchain/check/testdata/class/no_prelude/destroy.carbon
 
-// --- fail_todo_basic.carbon
+// --- self.carbon
 
 library "[[@TEST_NAME]]";
 
 class C {
-  // CHECK:STDERR: fail_todo_basic.carbon:[[@LINE+4]]:6: error: semantics TODO: `KeywordNameBeforeParamsId` [SemanticsTodo]
-  // CHECK:STDERR:   fn destroy[self: Self]();
-  // CHECK:STDERR:      ^~~~~~~
-  // CHECK:STDERR:
   fn destroy[self: Self]();
 }
 
-// --- fail_todo_addr.carbon
+// --- addr_self.carbon
 
 library "[[@TEST_NAME]]";
 
 class C {
-  // CHECK:STDERR: fail_todo_addr.carbon:[[@LINE+4]]:6: error: semantics TODO: `KeywordNameBeforeParamsId` [SemanticsTodo]
-  // CHECK:STDERR:   fn destroy[addr self: Self*]();
-  // CHECK:STDERR:      ^~~~~~~
-  // CHECK:STDERR:
   fn destroy[addr self: Self*]();
 }
 
-// --- fail_todo_class_function.carbon
+// --- explicit_return.carbon
 
 library "[[@TEST_NAME]]";
 
 class C {
-  // CHECK:STDERR: fail_todo_class_function.carbon:[[@LINE+4]]:6: error: semantics TODO: `KeywordNameBeforeParamsId` [SemanticsTodo]
+  fn destroy[self: Self]() -> ();
+}
+
+// --- fail_class_function.carbon
+
+library "[[@TEST_NAME]]";
+
+class C {
+  // CHECK:STDERR: fail_class_function.carbon:[[@LINE+4]]:3: error: missing implicit `self` parameter [DestroyFunctionMissingSelf]
   // CHECK:STDERR:   fn destroy();
-  // CHECK:STDERR:      ^~~~~~~
+  // CHECK:STDERR:   ^~~~~~~~~~~~~
   // CHECK:STDERR:
   fn destroy();
 }
 
-// --- fail_todo_destroy_in_namespace.carbon
+// --- fail_extra_implicit_params_second.carbon
 
 library "[[@TEST_NAME]]";
 
-namespace NS;
+class C {
+  // CHECK:STDERR: fail_extra_implicit_params_second.carbon:[[@LINE+4]]:26: error: unexpected implicit parameter [DestroyFunctionUnexpectedImplicitParam]
+  // CHECK:STDERR:   fn destroy[self: Self, T:! type]();
+  // CHECK:STDERR:                          ^
+  // CHECK:STDERR:
+  fn destroy[self: Self, T:! type]();
+}
 
-// CHECK:STDERR: fail_todo_destroy_in_namespace.carbon:[[@LINE+4]]:7: error: semantics TODO: `KeywordNameBeforeParamsId` [SemanticsTodo]
-// CHECK:STDERR: fn NS.destroy();
-// CHECK:STDERR:       ^~~~~~~
-// CHECK:STDERR:
-fn NS.destroy();
+// --- fail_extra_implicit_params_first.carbon
+
+library "[[@TEST_NAME]]";
+
+class C {
+  // CHECK:STDERR: fail_extra_implicit_params_first.carbon:[[@LINE+4]]:14: error: unexpected implicit parameter [DestroyFunctionUnexpectedImplicitParam]
+  // CHECK:STDERR:   fn destroy[T:! type, self: Self]();
+  // CHECK:STDERR:              ^
+  // CHECK:STDERR:
+  fn destroy[T:! type, self: Self]();
+}
+
+// --- fail_positional_params.carbon
+
+library "[[@TEST_NAME]]";
+
+class C {
+  // CHECK:STDERR: fail_positional_params.carbon:[[@LINE+8]]:25: error: a `(` for parameters is required after implicit parameters [ParamsRequiredAfterImplicit]
+  // CHECK:STDERR:   fn destroy[self: Self];
+  // CHECK:STDERR:                         ^
+  // CHECK:STDERR:
+  // CHECK:STDERR: fail_positional_params.carbon:[[@LINE+4]]:3: error: semantics TODO: `handle invalid parse trees in `check`` [SemanticsTodo]
+  // CHECK:STDERR:   fn destroy[self: Self];
+  // CHECK:STDERR:   ^~~~~~~~~~~~~~~~~~~~~~~
+  // CHECK:STDERR:
+  fn destroy[self: Self];
+}
 
-// --- fail_todo_missing_params.carbon
+// --- fail_explicit_params.carbon
 
 library "[[@TEST_NAME]]";
 
+class C {
+  // CHECK:STDERR: fail_explicit_params.carbon:[[@LINE+4]]:26: error: unexpected parameter [DestroyFunctionNonEmptyExplicitParams]
+  // CHECK:STDERR:   fn destroy[self: Self](x: ());
+  // CHECK:STDERR:                          ^~~~~
+  // CHECK:STDERR:
+  fn destroy[self: Self](x: ());
+}
+
+// --- fail_return_type.carbon
+
+library "[[@TEST_NAME]]";
 
 class C {
-  // CHECK:STDERR: fail_todo_missing_params.carbon:[[@LINE+4]]:6: error: semantics TODO: `KeywordNameNotBeforeParamsId` [SemanticsTodo]
-  // CHECK:STDERR:   fn destroy;
-  // CHECK:STDERR:      ^~~~~~~
+  // CHECK:STDERR: fail_return_type.carbon:[[@LINE+4]]:28: error: incorrect return type; must be unspecified or `()` [DestroyFunctionIncorrectReturnType]
+  // CHECK:STDERR:   fn destroy[self: Self]() -> {};
+  // CHECK:STDERR:                            ^~~~~
   // CHECK:STDERR:
-  fn destroy;
+  fn destroy[self: Self]() -> {};
+}
+
+// --- fail_out_of_line_missing_params.carbon
+
+library "[[@TEST_NAME]]";
+
+
+class C {
+  fn destroy[self: Self]();
 }
 
+// CHECK:STDERR: fail_out_of_line_missing_params.carbon:[[@LINE+7]]:1: error: redeclaration differs because of missing implicit parameter list [RedeclParamListDiffers]
+// CHECK:STDERR: fn C.destroy {}
+// CHECK:STDERR: ^~~~~~~~~~~~~~
+// CHECK:STDERR: fail_out_of_line_missing_params.carbon:[[@LINE-6]]:3: note: previously declared with implicit parameter list [RedeclParamListPrevious]
+// CHECK:STDERR:   fn destroy[self: Self]();
+// CHECK:STDERR:   ^~~~~~~~~~~~~~~~~~~~~~~~~
+// CHECK:STDERR:
 fn C.destroy {}
 
-// --- fail_todo_wrong_scope.carbon
+// --- fail_destroy_in_file_scope.carbon
+
+library "[[@TEST_NAME]]";
+
+// CHECK:STDERR: fail_destroy_in_file_scope.carbon:[[@LINE+4]]:1: error: declaring `fn destroy` in non-class scope [DestroyFunctionOutsideClass]
+// CHECK:STDERR: fn destroy[self: ()]();
+// CHECK:STDERR: ^~~~~~~~~~~~~~~~~~~~~~~
+// CHECK:STDERR:
+fn destroy[self: ()]();
+
+// --- fail_destroy_in_namespace_scope.carbon
+
+library "[[@TEST_NAME]]";
+
+namespace NS;
+
+// CHECK:STDERR: fail_destroy_in_namespace_scope.carbon:[[@LINE+4]]:1: error: declaring `fn destroy` in non-class scope [DestroyFunctionOutsideClass]
+// CHECK:STDERR: fn NS.destroy();
+// CHECK:STDERR: ^~~~~~~~~~~~~~~~
+// CHECK:STDERR:
+fn NS.destroy();
+
+// --- fail_invalid_qualifier_with_params.carbon
 
 library "[[@TEST_NAME]]";
 
+class C {
+  fn destroy[self: Self]();
+}
 
-// CHECK:STDERR: fail_todo_wrong_scope.carbon:[[@LINE+4]]:4: error: semantics TODO: `KeywordNameBeforeParamsId` [SemanticsTodo]
-// CHECK:STDERR: fn destroy();
-// CHECK:STDERR:    ^~~~~~~
+// CHECK:STDERR: fail_invalid_qualifier_with_params.carbon:[[@LINE+7]]:6: error: name qualifiers are only allowed for entities that provide a scope [QualifiedNameInNonScope]
+// CHECK:STDERR: fn C.destroy[self: Self]().Foo() {}
+// CHECK:STDERR:      ^~~~~~~
+// CHECK:STDERR: fail_invalid_qualifier_with_params.carbon:[[@LINE-6]]:3: note: referenced non-scope entity declared here [QualifiedNameNonScopeEntity]
+// CHECK:STDERR:   fn destroy[self: Self]();
+// CHECK:STDERR:   ^~~~~~~~~~~~~~~~~~~~~~~~~
 // CHECK:STDERR:
-fn destroy();
+fn C.destroy[self: Self]().Foo() {}
+
+// --- fail_invalid_qualifier_no_params.carbon
 
-// --- fail_invalid_qualifier.carbon
+library "[[@TEST_NAME]]";
 
 class C {
-  // CHECK:STDERR: fail_invalid_qualifier.carbon:[[@LINE+4]]:6: error: semantics TODO: `KeywordNameBeforeParamsId` [SemanticsTodo]
-  // CHECK:STDERR:   fn destroy();
-  // CHECK:STDERR:      ^~~~~~~
-  // CHECK:STDERR:
-  fn destroy();
+  fn destroy[self: Self]();
 }
 
-fn C.destroy.destroy() {}
+// CHECK:STDERR: fail_invalid_qualifier_no_params.carbon:[[@LINE+7]]:6: error: name qualifiers are only allowed for entities that provide a scope [QualifiedNameInNonScope]
+// CHECK:STDERR: fn C.destroy.Foo() {}
+// CHECK:STDERR:      ^~~~~~~
+// CHECK:STDERR: fail_invalid_qualifier_no_params.carbon:[[@LINE-6]]:3: note: referenced non-scope entity declared here [QualifiedNameNonScopeEntity]
+// CHECK:STDERR:   fn destroy[self: Self]();
+// CHECK:STDERR:   ^~~~~~~~~~~~~~~~~~~~~~~~~
+// CHECK:STDERR:
+fn C.destroy.Foo() {}
 
-// CHECK:STDOUT: --- fail_todo_basic.carbon
+// CHECK:STDOUT: --- self.carbon
 // CHECK:STDOUT:
 // CHECK:STDOUT: constants {
 // CHECK:STDOUT:   %C: type = class_type @C [concrete]
+// CHECK:STDOUT:   %destroy.type: type = fn_type @destroy [concrete]
+// CHECK:STDOUT:   %destroy: %destroy.type = struct_value () [concrete]
+// CHECK:STDOUT:   %empty_struct_type: type = struct_type {} [concrete]
+// CHECK:STDOUT:   %complete_type: <witness> = complete_type_witness %empty_struct_type [concrete]
 // CHECK:STDOUT: }
 // CHECK:STDOUT:
-// CHECK:STDOUT: file {}
+// CHECK:STDOUT: file {
+// CHECK:STDOUT:   package: <namespace> = namespace [concrete] {
+// CHECK:STDOUT:     .C = %C.decl
+// CHECK:STDOUT:   }
+// CHECK:STDOUT:   %C.decl: type = class_decl @C [concrete = constants.%C] {} {}
+// CHECK:STDOUT: }
 // CHECK:STDOUT:
 // CHECK:STDOUT: class @C {
-// CHECK:STDOUT:   complete_type_witness = invalid
+// CHECK:STDOUT:   %destroy.decl: %destroy.type = fn_decl @destroy [concrete = constants.%destroy] {
+// CHECK:STDOUT:     %self.patt: %C = binding_pattern self
+// CHECK:STDOUT:     %self.param_patt: %C = value_param_pattern %self.patt, call_param0
+// CHECK:STDOUT:   } {
+// CHECK:STDOUT:     %self.param: %C = value_param call_param0
+// CHECK:STDOUT:     %Self.ref: type = name_ref Self, constants.%C [concrete = constants.%C]
+// CHECK:STDOUT:     %self: %C = bind_name self, %self.param
+// CHECK:STDOUT:   }
+// CHECK:STDOUT:   %complete_type: <witness> = complete_type_witness %empty_struct_type [concrete = constants.%complete_type]
+// CHECK:STDOUT:   complete_type_witness = %complete_type
 // CHECK:STDOUT:
 // CHECK:STDOUT: !members:
 // CHECK:STDOUT:   .Self = constants.%C
+// CHECK:STDOUT:   .destroy = %destroy.decl
 // CHECK:STDOUT: }
 // CHECK:STDOUT:
-// CHECK:STDOUT: --- fail_todo_addr.carbon
+// CHECK:STDOUT: fn @destroy[%self.param_patt: %C]();
+// CHECK:STDOUT:
+// CHECK:STDOUT: --- addr_self.carbon
 // CHECK:STDOUT:
 // CHECK:STDOUT: constants {
 // CHECK:STDOUT:   %C: type = class_type @C [concrete]
+// CHECK:STDOUT:   %ptr: type = ptr_type %C [concrete]
+// CHECK:STDOUT:   %destroy.type: type = fn_type @destroy [concrete]
+// CHECK:STDOUT:   %destroy: %destroy.type = struct_value () [concrete]
+// CHECK:STDOUT:   %empty_struct_type: type = struct_type {} [concrete]
+// CHECK:STDOUT:   %complete_type: <witness> = complete_type_witness %empty_struct_type [concrete]
 // CHECK:STDOUT: }
 // CHECK:STDOUT:
-// CHECK:STDOUT: file {}
+// CHECK:STDOUT: file {
+// CHECK:STDOUT:   package: <namespace> = namespace [concrete] {
+// CHECK:STDOUT:     .C = %C.decl
+// CHECK:STDOUT:   }
+// CHECK:STDOUT:   %C.decl: type = class_decl @C [concrete = constants.%C] {} {}
+// CHECK:STDOUT: }
 // CHECK:STDOUT:
 // CHECK:STDOUT: class @C {
-// CHECK:STDOUT:   complete_type_witness = invalid
+// CHECK:STDOUT:   %destroy.decl: %destroy.type = fn_decl @destroy [concrete = constants.%destroy] {
+// CHECK:STDOUT:     %self.patt: %ptr = binding_pattern self
+// CHECK:STDOUT:     %self.param_patt: %ptr = value_param_pattern %self.patt, call_param0
+// CHECK:STDOUT:     %.loc5_14: auto = addr_pattern %self.param_patt
+// CHECK:STDOUT:   } {
+// CHECK:STDOUT:     %self.param: %ptr = value_param call_param0
+// CHECK:STDOUT:     %.loc5_29: type = splice_block %ptr [concrete = constants.%ptr] {
+// CHECK:STDOUT:       %Self.ref: type = name_ref Self, constants.%C [concrete = constants.%C]
+// CHECK:STDOUT:       %ptr: type = ptr_type %C [concrete = constants.%ptr]
+// CHECK:STDOUT:     }
+// CHECK:STDOUT:     %self: %ptr = bind_name self, %self.param
+// CHECK:STDOUT:   }
+// CHECK:STDOUT:   %complete_type: <witness> = complete_type_witness %empty_struct_type [concrete = constants.%complete_type]
+// CHECK:STDOUT:   complete_type_witness = %complete_type
 // CHECK:STDOUT:
 // CHECK:STDOUT: !members:
 // CHECK:STDOUT:   .Self = constants.%C
+// CHECK:STDOUT:   .destroy = %destroy.decl
 // CHECK:STDOUT: }
 // CHECK:STDOUT:
-// CHECK:STDOUT: --- fail_todo_class_function.carbon
+// CHECK:STDOUT: fn @destroy[addr %self.param_patt: %ptr]();
+// CHECK:STDOUT:
+// CHECK:STDOUT: --- explicit_return.carbon
 // CHECK:STDOUT:
 // CHECK:STDOUT: constants {
 // CHECK:STDOUT:   %C: type = class_type @C [concrete]
+// CHECK:STDOUT:   %empty_tuple.type: type = tuple_type () [concrete]
+// CHECK:STDOUT:   %destroy.type: type = fn_type @destroy [concrete]
+// CHECK:STDOUT:   %destroy: %destroy.type = struct_value () [concrete]
+// CHECK:STDOUT:   %empty_struct_type: type = struct_type {} [concrete]
+// CHECK:STDOUT:   %complete_type: <witness> = complete_type_witness %empty_struct_type [concrete]
 // CHECK:STDOUT: }
 // CHECK:STDOUT:
-// CHECK:STDOUT: file {}
+// CHECK:STDOUT: file {
+// CHECK:STDOUT:   package: <namespace> = namespace [concrete] {
+// CHECK:STDOUT:     .C = %C.decl
+// CHECK:STDOUT:   }
+// CHECK:STDOUT:   %C.decl: type = class_decl @C [concrete = constants.%C] {} {}
+// CHECK:STDOUT: }
 // CHECK:STDOUT:
 // CHECK:STDOUT: class @C {
-// CHECK:STDOUT:   complete_type_witness = invalid
+// CHECK:STDOUT:   %destroy.decl: %destroy.type = fn_decl @destroy [concrete = constants.%destroy] {
+// CHECK:STDOUT:     %self.patt: %C = binding_pattern self
+// CHECK:STDOUT:     %self.param_patt: %C = value_param_pattern %self.patt, call_param0
+// CHECK:STDOUT:     %return.patt: %empty_tuple.type = return_slot_pattern
+// CHECK:STDOUT:     %return.param_patt: %empty_tuple.type = out_param_pattern %return.patt, call_param1
+// CHECK:STDOUT:   } {
+// CHECK:STDOUT:     %.loc5_32.1: %empty_tuple.type = tuple_literal ()
+// CHECK:STDOUT:     %.loc5_32.2: type = converted %.loc5_32.1, constants.%empty_tuple.type [concrete = constants.%empty_tuple.type]
+// CHECK:STDOUT:     %self.param: %C = value_param call_param0
+// CHECK:STDOUT:     %Self.ref: type = name_ref Self, constants.%C [concrete = constants.%C]
+// CHECK:STDOUT:     %self: %C = bind_name self, %self.param
+// CHECK:STDOUT:     %return.param: ref %empty_tuple.type = out_param call_param1
+// CHECK:STDOUT:     %return: ref %empty_tuple.type = return_slot %return.param
+// CHECK:STDOUT:   }
+// CHECK:STDOUT:   %complete_type: <witness> = complete_type_witness %empty_struct_type [concrete = constants.%complete_type]
+// CHECK:STDOUT:   complete_type_witness = %complete_type
 // CHECK:STDOUT:
 // CHECK:STDOUT: !members:
 // CHECK:STDOUT:   .Self = constants.%C
+// CHECK:STDOUT:   .destroy = %destroy.decl
 // CHECK:STDOUT: }
 // CHECK:STDOUT:
-// CHECK:STDOUT: --- fail_todo_destroy_in_namespace.carbon
+// CHECK:STDOUT: fn @destroy[%self.param_patt: %C]() -> %empty_tuple.type;
 // CHECK:STDOUT:
-// CHECK:STDOUT: file {}
+// CHECK:STDOUT: --- fail_class_function.carbon
+// CHECK:STDOUT:
+// CHECK:STDOUT: constants {
+// CHECK:STDOUT:   %C: type = class_type @C [concrete]
+// CHECK:STDOUT:   %destroy.type: type = fn_type @destroy [concrete]
+// CHECK:STDOUT:   %destroy: %destroy.type = struct_value () [concrete]
+// CHECK:STDOUT:   %empty_struct_type: type = struct_type {} [concrete]
+// CHECK:STDOUT:   %complete_type: <witness> = complete_type_witness %empty_struct_type [concrete]
+// CHECK:STDOUT: }
+// CHECK:STDOUT:
+// CHECK:STDOUT: file {
+// CHECK:STDOUT:   package: <namespace> = namespace [concrete] {
+// CHECK:STDOUT:     .C = %C.decl
+// CHECK:STDOUT:   }
+// CHECK:STDOUT:   %C.decl: type = class_decl @C [concrete = constants.%C] {} {}
+// CHECK:STDOUT: }
+// CHECK:STDOUT:
+// CHECK:STDOUT: class @C {
+// CHECK:STDOUT:   %destroy.decl: %destroy.type = fn_decl @destroy [concrete = constants.%destroy] {} {}
+// CHECK:STDOUT:   %complete_type: <witness> = complete_type_witness %empty_struct_type [concrete = constants.%complete_type]
+// CHECK:STDOUT:   complete_type_witness = %complete_type
+// CHECK:STDOUT:
+// CHECK:STDOUT: !members:
+// CHECK:STDOUT:   .Self = constants.%C
+// CHECK:STDOUT:   .destroy = %destroy.decl
+// CHECK:STDOUT: }
+// CHECK:STDOUT:
+// CHECK:STDOUT: fn @destroy();
 // CHECK:STDOUT:
-// CHECK:STDOUT: --- fail_todo_missing_params.carbon
+// CHECK:STDOUT: --- fail_extra_implicit_params_second.carbon
 // CHECK:STDOUT:
 // CHECK:STDOUT: constants {
 // CHECK:STDOUT:   %C: type = class_type @C [concrete]
+// CHECK:STDOUT:   %T: type = bind_symbolic_name T, 0 [symbolic]
+// CHECK:STDOUT:   %T.patt: type = symbolic_binding_pattern T, 0 [symbolic]
+// CHECK:STDOUT:   %destroy.type: type = fn_type @destroy [concrete]
+// CHECK:STDOUT:   %destroy: %destroy.type = struct_value () [concrete]
+// CHECK:STDOUT:   %empty_struct_type: type = struct_type {} [concrete]
+// CHECK:STDOUT:   %complete_type: <witness> = complete_type_witness %empty_struct_type [concrete]
 // CHECK:STDOUT: }
 // CHECK:STDOUT:
-// CHECK:STDOUT: file {}
+// CHECK:STDOUT: file {
+// CHECK:STDOUT:   package: <namespace> = namespace [concrete] {
+// CHECK:STDOUT:     .C = %C.decl
+// CHECK:STDOUT:   }
+// CHECK:STDOUT:   %C.decl: type = class_decl @C [concrete = constants.%C] {} {}
+// CHECK:STDOUT: }
 // CHECK:STDOUT:
 // CHECK:STDOUT: class @C {
-// CHECK:STDOUT:   complete_type_witness = invalid
+// CHECK:STDOUT:   %destroy.decl: %destroy.type = fn_decl @destroy [concrete = constants.%destroy] {
+// CHECK:STDOUT:     %self.patt: %C = binding_pattern self
+// CHECK:STDOUT:     %self.param_patt: %C = value_param_pattern %self.patt, call_param0
+// CHECK:STDOUT:     %T.patt.loc9_26.1: type = symbolic_binding_pattern T, 0 [symbolic = %T.patt.loc9_26.2 (constants.%T.patt)]
+// CHECK:STDOUT:   } {
+// CHECK:STDOUT:     %self.param: %C = value_param call_param0
+// CHECK:STDOUT:     %Self.ref: type = name_ref Self, constants.%C [concrete = constants.%C]
+// CHECK:STDOUT:     %self: %C = bind_name self, %self.param
+// CHECK:STDOUT:     %T.loc9_26.2: type = bind_symbolic_name T, 0 [symbolic = %T.loc9_26.1 (constants.%T)]
+// CHECK:STDOUT:   }
+// CHECK:STDOUT:   %complete_type: <witness> = complete_type_witness %empty_struct_type [concrete = constants.%complete_type]
+// CHECK:STDOUT:   complete_type_witness = %complete_type
 // CHECK:STDOUT:
 // CHECK:STDOUT: !members:
 // CHECK:STDOUT:   .Self = constants.%C
+// CHECK:STDOUT:   .destroy = %destroy.decl
 // CHECK:STDOUT: }
 // CHECK:STDOUT:
-// CHECK:STDOUT: --- fail_todo_wrong_scope.carbon
+// CHECK:STDOUT: generic fn @destroy(%T.loc9_26.2: type) {
+// CHECK:STDOUT:   %T.loc9_26.1: type = bind_symbolic_name T, 0 [symbolic = %T.loc9_26.1 (constants.%T)]
+// CHECK:STDOUT:   %T.patt.loc9_26.2: type = symbolic_binding_pattern T, 0 [symbolic = %T.patt.loc9_26.2 (constants.%T.patt)]
 // CHECK:STDOUT:
-// CHECK:STDOUT: file {}
+// CHECK:STDOUT:   fn[%self.param_patt: %C, %T.patt.loc9_26.1: type]();
+// CHECK:STDOUT: }
+// CHECK:STDOUT:
+// CHECK:STDOUT: specific @destroy(constants.%T) {
+// CHECK:STDOUT:   %T.loc9_26.1 => constants.%T
+// CHECK:STDOUT:   %T.patt.loc9_26.2 => constants.%T
+// CHECK:STDOUT: }
+// CHECK:STDOUT:
+// CHECK:STDOUT: --- fail_extra_implicit_params_first.carbon
+// CHECK:STDOUT:
+// CHECK:STDOUT: constants {
+// CHECK:STDOUT:   %C: type = class_type @C [concrete]
+// CHECK:STDOUT:   %T: type = bind_symbolic_name T, 0 [symbolic]
+// CHECK:STDOUT:   %T.patt: type = symbolic_binding_pattern T, 0 [symbolic]
+// CHECK:STDOUT:   %destroy.type: type = fn_type @destroy [concrete]
+// CHECK:STDOUT:   %destroy: %destroy.type = struct_value () [concrete]
+// CHECK:STDOUT:   %empty_struct_type: type = struct_type {} [concrete]
+// CHECK:STDOUT:   %complete_type: <witness> = complete_type_witness %empty_struct_type [concrete]
+// CHECK:STDOUT: }
+// CHECK:STDOUT:
+// CHECK:STDOUT: file {
+// CHECK:STDOUT:   package: <namespace> = namespace [concrete] {
+// CHECK:STDOUT:     .C = %C.decl
+// CHECK:STDOUT:   }
+// CHECK:STDOUT:   %C.decl: type = class_decl @C [concrete = constants.%C] {} {}
+// CHECK:STDOUT: }
+// CHECK:STDOUT:
+// CHECK:STDOUT: class @C {
+// CHECK:STDOUT:   %destroy.decl: %destroy.type = fn_decl @destroy [concrete = constants.%destroy] {
+// CHECK:STDOUT:     %T.patt.loc9_14.1: type = symbolic_binding_pattern T, 0 [symbolic = %T.patt.loc9_14.2 (constants.%T.patt)]
+// CHECK:STDOUT:     %self.patt: %C = binding_pattern self
+// CHECK:STDOUT:     %self.param_patt: %C = value_param_pattern %self.patt, call_param0
+// CHECK:STDOUT:   } {
+// CHECK:STDOUT:     %T.loc9_14.2: type = bind_symbolic_name T, 0 [symbolic = %T.loc9_14.1 (constants.%T)]
+// CHECK:STDOUT:     %self.param: %C = value_param call_param0
+// CHECK:STDOUT:     %Self.ref: type = name_ref Self, constants.%C [concrete = constants.%C]
+// CHECK:STDOUT:     %self: %C = bind_name self, %self.param
+// CHECK:STDOUT:   }
+// CHECK:STDOUT:   %complete_type: <witness> = complete_type_witness %empty_struct_type [concrete = constants.%complete_type]
+// CHECK:STDOUT:   complete_type_witness = %complete_type
+// CHECK:STDOUT:
+// CHECK:STDOUT: !members:
+// CHECK:STDOUT:   .Self = constants.%C
+// CHECK:STDOUT:   .destroy = %destroy.decl
+// CHECK:STDOUT: }
+// CHECK:STDOUT:
+// CHECK:STDOUT: generic fn @destroy(%T.loc9_14.2: type) {
+// CHECK:STDOUT:   %T.loc9_14.1: type = bind_symbolic_name T, 0 [symbolic = %T.loc9_14.1 (constants.%T)]
+// CHECK:STDOUT:   %T.patt.loc9_14.2: type = symbolic_binding_pattern T, 0 [symbolic = %T.patt.loc9_14.2 (constants.%T.patt)]
+// CHECK:STDOUT:
+// CHECK:STDOUT:   fn[%T.patt.loc9_14.1: type, %self.param_patt: %C]();
+// CHECK:STDOUT: }
+// CHECK:STDOUT:
+// CHECK:STDOUT: specific @destroy(constants.%T) {
+// CHECK:STDOUT:   %T.loc9_14.1 => constants.%T
+// CHECK:STDOUT:   %T.patt.loc9_14.2 => constants.%T
+// CHECK:STDOUT: }
 // CHECK:STDOUT:
-// CHECK:STDOUT: --- fail_invalid_qualifier.carbon
+// CHECK:STDOUT: --- fail_positional_params.carbon
 // CHECK:STDOUT:
 // CHECK:STDOUT: constants {
 // CHECK:STDOUT:   %C: type = class_type @C [concrete]
@@ -177,3 +446,272 @@ fn C.destroy.destroy() {}
 // CHECK:STDOUT:   .Self = constants.%C
 // CHECK:STDOUT: }
 // CHECK:STDOUT:
+// CHECK:STDOUT: --- fail_explicit_params.carbon
+// CHECK:STDOUT:
+// CHECK:STDOUT: constants {
+// CHECK:STDOUT:   %C: type = class_type @C [concrete]
+// CHECK:STDOUT:   %empty_tuple.type: type = tuple_type () [concrete]
+// CHECK:STDOUT:   %destroy.type: type = fn_type @destroy [concrete]
+// CHECK:STDOUT:   %destroy: %destroy.type = struct_value () [concrete]
+// CHECK:STDOUT:   %empty_struct_type: type = struct_type {} [concrete]
+// CHECK:STDOUT:   %complete_type: <witness> = complete_type_witness %empty_struct_type [concrete]
+// CHECK:STDOUT: }
+// CHECK:STDOUT:
+// CHECK:STDOUT: file {
+// CHECK:STDOUT:   package: <namespace> = namespace [concrete] {
+// CHECK:STDOUT:     .C = %C.decl
+// CHECK:STDOUT:   }
+// CHECK:STDOUT:   %C.decl: type = class_decl @C [concrete = constants.%C] {} {}
+// CHECK:STDOUT: }
+// CHECK:STDOUT:
+// CHECK:STDOUT: class @C {
+// CHECK:STDOUT:   %destroy.decl: %destroy.type = fn_decl @destroy [concrete = constants.%destroy] {
+// CHECK:STDOUT:     %self.patt: %C = binding_pattern self
+// CHECK:STDOUT:     %self.param_patt: %C = value_param_pattern %self.patt, call_param0
+// CHECK:STDOUT:     %x.patt: %empty_tuple.type = binding_pattern x
+// CHECK:STDOUT:     %x.param_patt: %empty_tuple.type = value_param_pattern %x.patt, call_param1
+// CHECK:STDOUT:   } {
+// CHECK:STDOUT:     %self.param: %C = value_param call_param0
+// CHECK:STDOUT:     %Self.ref: type = name_ref Self, constants.%C [concrete = constants.%C]
+// CHECK:STDOUT:     %self: %C = bind_name self, %self.param
+// CHECK:STDOUT:     %x.param: %empty_tuple.type = value_param call_param1
+// CHECK:STDOUT:     %.loc9_30.1: type = splice_block %.loc9_30.3 [concrete = constants.%empty_tuple.type] {
+// CHECK:STDOUT:       %.loc9_30.2: %empty_tuple.type = tuple_literal ()
+// CHECK:STDOUT:       %.loc9_30.3: type = converted %.loc9_30.2, constants.%empty_tuple.type [concrete = constants.%empty_tuple.type]
+// CHECK:STDOUT:     }
+// CHECK:STDOUT:     %x: %empty_tuple.type = bind_name x, %x.param
+// CHECK:STDOUT:   }
+// CHECK:STDOUT:   %complete_type: <witness> = complete_type_witness %empty_struct_type [concrete = constants.%complete_type]
+// CHECK:STDOUT:   complete_type_witness = %complete_type
+// CHECK:STDOUT:
+// CHECK:STDOUT: !members:
+// CHECK:STDOUT:   .Self = constants.%C
+// CHECK:STDOUT:   .destroy = %destroy.decl
+// CHECK:STDOUT: }
+// CHECK:STDOUT:
+// CHECK:STDOUT: fn @destroy[%self.param_patt: %C]();
+// CHECK:STDOUT:
+// CHECK:STDOUT: --- fail_return_type.carbon
+// CHECK:STDOUT:
+// CHECK:STDOUT: constants {
+// CHECK:STDOUT:   %C: type = class_type @C [concrete]
+// CHECK:STDOUT:   %empty_struct_type: type = struct_type {} [concrete]
+// CHECK:STDOUT:   %destroy.type: type = fn_type @destroy [concrete]
+// CHECK:STDOUT:   %destroy: %destroy.type = struct_value () [concrete]
+// CHECK:STDOUT:   %complete_type: <witness> = complete_type_witness %empty_struct_type [concrete]
+// CHECK:STDOUT: }
+// CHECK:STDOUT:
+// CHECK:STDOUT: file {
+// CHECK:STDOUT:   package: <namespace> = namespace [concrete] {
+// CHECK:STDOUT:     .C = %C.decl
+// CHECK:STDOUT:   }
+// CHECK:STDOUT:   %C.decl: type = class_decl @C [concrete = constants.%C] {} {}
+// CHECK:STDOUT: }
+// CHECK:STDOUT:
+// CHECK:STDOUT: class @C {
+// CHECK:STDOUT:   %destroy.decl: %destroy.type = fn_decl @destroy [concrete = constants.%destroy] {
+// CHECK:STDOUT:     %self.patt: %C = binding_pattern self
+// CHECK:STDOUT:     %self.param_patt: %C = value_param_pattern %self.patt, call_param0
+// CHECK:STDOUT:     %return.patt: %empty_struct_type = return_slot_pattern
+// CHECK:STDOUT:     %return.param_patt: %empty_struct_type = out_param_pattern %return.patt, call_param1
+// CHECK:STDOUT:   } {
+// CHECK:STDOUT:     %.loc9_32.1: %empty_struct_type = struct_literal ()
+// CHECK:STDOUT:     %.loc9_32.2: type = converted %.loc9_32.1, constants.%empty_struct_type [concrete = constants.%empty_struct_type]
+// CHECK:STDOUT:     %self.param: %C = value_param call_param0
+// CHECK:STDOUT:     %Self.ref: type = name_ref Self, constants.%C [concrete = constants.%C]
+// CHECK:STDOUT:     %self: %C = bind_name self, %self.param
+// CHECK:STDOUT:     %return.param: ref %empty_struct_type = out_param call_param1
+// CHECK:STDOUT:     %return: ref %empty_struct_type = return_slot %return.param
+// CHECK:STDOUT:   }
+// CHECK:STDOUT:   %complete_type: <witness> = complete_type_witness %empty_struct_type [concrete = constants.%complete_type]
+// CHECK:STDOUT:   complete_type_witness = %complete_type
+// CHECK:STDOUT:
+// CHECK:STDOUT: !members:
+// CHECK:STDOUT:   .Self = constants.%C
+// CHECK:STDOUT:   .destroy = %destroy.decl
+// CHECK:STDOUT: }
+// CHECK:STDOUT:
+// CHECK:STDOUT: fn @destroy[%self.param_patt: %C]() -> %empty_struct_type;
+// CHECK:STDOUT:
+// CHECK:STDOUT: --- fail_out_of_line_missing_params.carbon
+// CHECK:STDOUT:
+// CHECK:STDOUT: constants {
+// CHECK:STDOUT:   %C: type = class_type @C [concrete]
+// CHECK:STDOUT:   %destroy.type.bd30b6.1: type = fn_type @destroy.1 [concrete]
+// CHECK:STDOUT:   %destroy.357925.1: %destroy.type.bd30b6.1 = struct_value () [concrete]
+// CHECK:STDOUT:   %empty_struct_type: type = struct_type {} [concrete]
+// CHECK:STDOUT:   %complete_type: <witness> = complete_type_witness %empty_struct_type [concrete]
+// CHECK:STDOUT:   %destroy.type.bd30b6.2: type = fn_type @destroy.2 [concrete]
+// CHECK:STDOUT:   %destroy.357925.2: %destroy.type.bd30b6.2 = struct_value () [concrete]
+// CHECK:STDOUT: }
+// CHECK:STDOUT:
+// CHECK:STDOUT: file {
+// CHECK:STDOUT:   package: <namespace> = namespace [concrete] {
+// CHECK:STDOUT:     .C = %C.decl
+// CHECK:STDOUT:   }
+// CHECK:STDOUT:   %C.decl: type = class_decl @C [concrete = constants.%C] {} {}
+// CHECK:STDOUT:   %destroy.decl: %destroy.type.bd30b6.2 = fn_decl @destroy.2 [concrete = constants.%destroy.357925.2] {} {}
+// CHECK:STDOUT: }
+// CHECK:STDOUT:
+// CHECK:STDOUT: class @C {
+// CHECK:STDOUT:   %destroy.decl: %destroy.type.bd30b6.1 = fn_decl @destroy.1 [concrete = constants.%destroy.357925.1] {
+// CHECK:STDOUT:     %self.patt: %C = binding_pattern self
+// CHECK:STDOUT:     %self.param_patt: %C = value_param_pattern %self.patt, call_param0
+// CHECK:STDOUT:   } {
+// CHECK:STDOUT:     %self.param: %C = value_param call_param0
+// CHECK:STDOUT:     %Self.ref: type = name_ref Self, constants.%C [concrete = constants.%C]
+// CHECK:STDOUT:     %self: %C = bind_name self, %self.param
+// CHECK:STDOUT:   }
+// CHECK:STDOUT:   %complete_type: <witness> = complete_type_witness %empty_struct_type [concrete = constants.%complete_type]
+// CHECK:STDOUT:   complete_type_witness = %complete_type
+// CHECK:STDOUT:
+// CHECK:STDOUT: !members:
+// CHECK:STDOUT:   .Self = constants.%C
+// CHECK:STDOUT:   .destroy = %destroy.decl
+// CHECK:STDOUT: }
+// CHECK:STDOUT:
+// CHECK:STDOUT: fn @destroy.1[%self.param_patt: %C]();
+// CHECK:STDOUT:
+// CHECK:STDOUT: fn @destroy.2() {
+// CHECK:STDOUT: !entry:
+// CHECK:STDOUT:   return
+// CHECK:STDOUT: }
+// CHECK:STDOUT:
+// CHECK:STDOUT: --- fail_destroy_in_file_scope.carbon
+// CHECK:STDOUT:
+// CHECK:STDOUT: constants {
+// CHECK:STDOUT:   %empty_tuple.type: type = tuple_type () [concrete]
+// CHECK:STDOUT:   %destroy.type: type = fn_type @destroy [concrete]
+// CHECK:STDOUT:   %destroy: %destroy.type = struct_value () [concrete]
+// CHECK:STDOUT: }
+// CHECK:STDOUT:
+// CHECK:STDOUT: file {
+// CHECK:STDOUT:   package: <namespace> = namespace [concrete] {
+// CHECK:STDOUT:     .destroy = %destroy.decl
+// CHECK:STDOUT:   }
+// CHECK:STDOUT:   %destroy.decl: %destroy.type = fn_decl @destroy [concrete = constants.%destroy] {
+// CHECK:STDOUT:     %self.patt: %empty_tuple.type = binding_pattern self
+// CHECK:STDOUT:     %self.param_patt: %empty_tuple.type = value_param_pattern %self.patt, call_param0
+// CHECK:STDOUT:   } {
+// CHECK:STDOUT:     %self.param: %empty_tuple.type = value_param call_param0
+// CHECK:STDOUT:     %.loc8_19.1: type = splice_block %.loc8_19.3 [concrete = constants.%empty_tuple.type] {
+// CHECK:STDOUT:       %.loc8_19.2: %empty_tuple.type = tuple_literal ()
+// CHECK:STDOUT:       %.loc8_19.3: type = converted %.loc8_19.2, constants.%empty_tuple.type [concrete = constants.%empty_tuple.type]
+// CHECK:STDOUT:     }
+// CHECK:STDOUT:     %self: %empty_tuple.type = bind_name self, %self.param
+// CHECK:STDOUT:   }
+// CHECK:STDOUT: }
+// CHECK:STDOUT:
+// CHECK:STDOUT: fn @destroy[%self.param_patt: %empty_tuple.type]();
+// CHECK:STDOUT:
+// CHECK:STDOUT: --- fail_destroy_in_namespace_scope.carbon
+// CHECK:STDOUT:
+// CHECK:STDOUT: constants {
+// CHECK:STDOUT:   %destroy.type: type = fn_type @destroy [concrete]
+// CHECK:STDOUT:   %destroy: %destroy.type = struct_value () [concrete]
+// CHECK:STDOUT: }
+// CHECK:STDOUT:
+// CHECK:STDOUT: file {
+// CHECK:STDOUT:   package: <namespace> = namespace [concrete] {
+// CHECK:STDOUT:     .NS = %NS
+// CHECK:STDOUT:   }
+// CHECK:STDOUT:   %NS: <namespace> = namespace [concrete] {
+// CHECK:STDOUT:     .destroy = %destroy.decl
+// CHECK:STDOUT:   }
+// CHECK:STDOUT:   %destroy.decl: %destroy.type = fn_decl @destroy [concrete = constants.%destroy] {} {}
+// CHECK:STDOUT: }
+// CHECK:STDOUT:
+// CHECK:STDOUT: fn @destroy();
+// CHECK:STDOUT:
+// CHECK:STDOUT: --- fail_invalid_qualifier_with_params.carbon
+// CHECK:STDOUT:
+// CHECK:STDOUT: constants {
+// CHECK:STDOUT:   %C: type = class_type @C [concrete]
+// CHECK:STDOUT:   %destroy.type: type = fn_type @destroy [concrete]
+// CHECK:STDOUT:   %destroy: %destroy.type = struct_value () [concrete]
+// CHECK:STDOUT:   %empty_struct_type: type = struct_type {} [concrete]
+// CHECK:STDOUT:   %complete_type: <witness> = complete_type_witness %empty_struct_type [concrete]
+// CHECK:STDOUT:   %Foo.type: type = fn_type @Foo [concrete]
+// CHECK:STDOUT:   %Foo: %Foo.type = struct_value () [concrete]
+// CHECK:STDOUT: }
+// CHECK:STDOUT:
+// CHECK:STDOUT: file {
+// CHECK:STDOUT:   package: <namespace> = namespace [concrete] {
+// CHECK:STDOUT:     .C = %C.decl
+// CHECK:STDOUT:   }
+// CHECK:STDOUT:   %C.decl: type = class_decl @C [concrete = constants.%C] {} {}
+// CHECK:STDOUT:   %Foo.decl: %Foo.type = fn_decl @Foo [concrete = constants.%Foo] {} {
+// CHECK:STDOUT:     %self.param: %C = value_param call_param0
+// CHECK:STDOUT:     %Self.ref: type = name_ref Self, constants.%C [concrete = constants.%C]
+// CHECK:STDOUT:     %self: %C = bind_name self, %self.param
+// CHECK:STDOUT:   }
+// CHECK:STDOUT: }
+// CHECK:STDOUT:
+// CHECK:STDOUT: class @C {
+// CHECK:STDOUT:   %destroy.decl: %destroy.type = fn_decl @destroy [concrete = constants.%destroy] {
+// CHECK:STDOUT:     %self.patt: %C = binding_pattern self
+// CHECK:STDOUT:     %self.param_patt: %C = value_param_pattern %self.patt, call_param0
+// CHECK:STDOUT:   } {
+// CHECK:STDOUT:     %self.param: %C = value_param call_param0
+// CHECK:STDOUT:     %Self.ref: type = name_ref Self, constants.%C [concrete = constants.%C]
+// CHECK:STDOUT:     %self: %C = bind_name self, %self.param
+// CHECK:STDOUT:   }
+// CHECK:STDOUT:   %complete_type: <witness> = complete_type_witness %empty_struct_type [concrete = constants.%complete_type]
+// CHECK:STDOUT:   complete_type_witness = %complete_type
+// CHECK:STDOUT:
+// CHECK:STDOUT: !members:
+// CHECK:STDOUT:   .Self = constants.%C
+// CHECK:STDOUT:   .destroy = %destroy.decl
+// CHECK:STDOUT: }
+// CHECK:STDOUT:
+// CHECK:STDOUT: fn @destroy[%self.param_patt: %C]();
+// CHECK:STDOUT:
+// CHECK:STDOUT: fn @Foo() {
+// CHECK:STDOUT: !entry:
+// CHECK:STDOUT:   return
+// CHECK:STDOUT: }
+// CHECK:STDOUT:
+// CHECK:STDOUT: --- fail_invalid_qualifier_no_params.carbon
+// CHECK:STDOUT:
+// CHECK:STDOUT: constants {
+// CHECK:STDOUT:   %C: type = class_type @C [concrete]
+// CHECK:STDOUT:   %destroy.type: type = fn_type @destroy [concrete]
+// CHECK:STDOUT:   %destroy: %destroy.type = struct_value () [concrete]
+// CHECK:STDOUT:   %empty_struct_type: type = struct_type {} [concrete]
+// CHECK:STDOUT:   %complete_type: <witness> = complete_type_witness %empty_struct_type [concrete]
+// CHECK:STDOUT:   %Foo.type: type = fn_type @Foo [concrete]
+// CHECK:STDOUT:   %Foo: %Foo.type = struct_value () [concrete]
+// CHECK:STDOUT: }
+// CHECK:STDOUT:
+// CHECK:STDOUT: file {
+// CHECK:STDOUT:   package: <namespace> = namespace [concrete] {
+// CHECK:STDOUT:     .C = %C.decl
+// CHECK:STDOUT:   }
+// CHECK:STDOUT:   %C.decl: type = class_decl @C [concrete = constants.%C] {} {}
+// CHECK:STDOUT:   %Foo.decl: %Foo.type = fn_decl @Foo [concrete = constants.%Foo] {} {}
+// CHECK:STDOUT: }
+// CHECK:STDOUT:
+// CHECK:STDOUT: class @C {
+// CHECK:STDOUT:   %destroy.decl: %destroy.type = fn_decl @destroy [concrete = constants.%destroy] {
+// CHECK:STDOUT:     %self.patt: %C = binding_pattern self
+// CHECK:STDOUT:     %self.param_patt: %C = value_param_pattern %self.patt, call_param0
+// CHECK:STDOUT:   } {
+// CHECK:STDOUT:     %self.param: %C = value_param call_param0
+// CHECK:STDOUT:     %Self.ref: type = name_ref Self, constants.%C [concrete = constants.%C]
+// CHECK:STDOUT:     %self: %C = bind_name self, %self.param
+// CHECK:STDOUT:   }
+// CHECK:STDOUT:   %complete_type: <witness> = complete_type_witness %empty_struct_type [concrete = constants.%complete_type]
+// CHECK:STDOUT:   complete_type_witness = %complete_type
+// CHECK:STDOUT:
+// CHECK:STDOUT: !members:
+// CHECK:STDOUT:   .Self = constants.%C
+// CHECK:STDOUT:   .destroy = %destroy.decl
+// CHECK:STDOUT: }
+// CHECK:STDOUT:
+// CHECK:STDOUT: fn @destroy[%self.param_patt: %C]();
+// CHECK:STDOUT:
+// CHECK:STDOUT: fn @Foo() {
+// CHECK:STDOUT: !entry:
+// CHECK:STDOUT:   return
+// CHECK:STDOUT: }
+// CHECK:STDOUT:

+ 6 - 0
toolchain/diagnostics/diagnostic_kind.def

@@ -237,6 +237,12 @@ CARBON_DIAGNOSTIC_KIND(MissingReturnStatement)
 CARBON_DIAGNOSTIC_KIND(UnknownBuiltinFunctionName)
 CARBON_DIAGNOSTIC_KIND(InvalidBuiltinSignature)
 
+CARBON_DIAGNOSTIC_KIND(DestroyFunctionOutsideClass)
+CARBON_DIAGNOSTIC_KIND(DestroyFunctionMissingSelf)
+CARBON_DIAGNOSTIC_KIND(DestroyFunctionUnexpectedImplicitParam)
+CARBON_DIAGNOSTIC_KIND(DestroyFunctionNonEmptyExplicitParams)
+CARBON_DIAGNOSTIC_KIND(DestroyFunctionIncorrectReturnType)
+
 // Class checking.
 CARBON_DIAGNOSTIC_KIND(AdaptDeclRepeated)
 CARBON_DIAGNOSTIC_KIND(AdaptWithBase)

+ 2 - 0
toolchain/sem_ir/ids.h

@@ -424,6 +424,8 @@ struct FloatKind : public IdBase<FloatKind> {
   X(ChoiceDiscriminant)                                          \
   /* The name of the package `Core`. */                          \
   X(Core)                                                        \
+  /* The name of `destroy`. */                                   \
+  X(Destroy)                                                     \
   /* The name of `package`. */                                   \
   X(PackageNamespace)                                            \
   /* The name of `.Self`. */                                     \

+ 2 - 0
toolchain/sem_ir/name.cpp

@@ -23,6 +23,8 @@ static auto GetSpecialName(NameId name_id, bool for_ir) -> llvm::StringRef {
       return "discriminant";
     case NameId::SpecialNameId::Core:
       return "Core";
+    case NameId::SpecialNameId::Destroy:
+      return "destroy";
     case NameId::SpecialNameId::PackageNamespace:
       return "package";
     case NameId::SpecialNameId::PeriodSelf: