Browse Source

Implement support for named constraints (#2359)

Basic support for named constraints as described in [the generics design](https://github.com/carbon-language/carbon-lang/blob/trunk/docs/design/generics/details.md#named-constraints). Support is provided for `extends` and `impl` declarations in `constraint`, but not yet for member aliases.

The missing prelude named constraints `Add`, `Mul`, `Ordered`, etc. are also added.
Richard Smith 3 năm trước cách đây
mục cha
commit
9e8cacae00
31 tập tin đã thay đổi với 685 bổ sung198 xóa
  1. 6 1
      common/fuzzing/carbon.proto
  2. 13 1
      common/fuzzing/proto_to_carbon.cpp
  3. 3 1
      explorer/ast/ast_rtti.txt
  4. 10 3
      explorer/ast/declaration.cpp
  5. 46 7
      explorer/ast/declaration.h
  6. 38 28
      explorer/data/prelude.carbon
  7. 10 2
      explorer/fuzzing/ast_to_proto.cpp
  8. 20 3
      explorer/interpreter/interpreter.cpp
  9. 3 2
      explorer/interpreter/resolve_control_flow.cpp
  10. 7 5
      explorer/interpreter/resolve_names.cpp
  11. 1 0
      explorer/interpreter/resolve_unformed.cpp
  12. 211 116
      explorer/interpreter/type_checker.cpp
  13. 7 13
      explorer/interpreter/type_checker.h
  14. 21 4
      explorer/interpreter/value.cpp
  15. 26 0
      explorer/interpreter/value.h
  16. 2 0
      explorer/syntax/lexer.lpp
  17. 6 0
      explorer/syntax/parser.ypp
  18. 1 1
      explorer/testdata/constraint/fail_combine_equality.carbon
  19. 1 1
      explorer/testdata/constraint/fail_missing_member.carbon
  20. 2 2
      explorer/testdata/function/auto_return/fail_multiple_returns.carbon
  21. 2 2
      explorer/testdata/function/auto_return/fail_separate_decl.carbon
  22. 1 1
      explorer/testdata/interface/fail_interface_missing_member.carbon
  23. 53 0
      explorer/testdata/named_constraint/extends.carbon
  24. 16 0
      explorer/testdata/named_constraint/fail_associated_constant.carbon
  25. 16 0
      explorer/testdata/named_constraint/fail_associated_function.carbon
  26. 32 0
      explorer/testdata/named_constraint/fail_compound_member_access.carbon
  27. 26 0
      explorer/testdata/named_constraint/fail_impl_as_member_access.carbon
  28. 41 0
      explorer/testdata/named_constraint/impl_as.carbon
  29. 37 0
      explorer/testdata/named_constraint/impl_constraint.carbon
  30. 5 5
      explorer/testdata/operators/add.carbon
  31. 22 0
      explorer/testdata/operators/add_with.carbon

+ 6 - 1
common/fuzzing/carbon.proto

@@ -390,7 +390,11 @@ message InterfaceImplDeclaration {
 message InterfaceDeclaration {
   optional string name = 1;
   repeated Declaration members = 2;
-  optional GenericBinding self = 3;
+}
+
+message ConstraintDeclaration {
+  optional string name = 1;
+  repeated Declaration members = 2;
 }
 
 message ImplDeclaration {
@@ -440,6 +444,7 @@ message Declaration {
     DestructorDeclaration destructor = 11;
     InterfaceExtendsDeclaration interface_extends = 12;
     InterfaceImplDeclaration interface_impl = 13;
+    ConstraintDeclaration constraint = 14;
   }
 }
 

+ 13 - 1
common/fuzzing/proto_to_carbon.cpp

@@ -855,7 +855,19 @@ static auto DeclarationToCarbon(const Fuzzing::Declaration& declaration,
         out << "\n";
       }
       out << "}";
-      // TODO: need to handle interface.self()?
+      break;
+    }
+
+    case Fuzzing::Declaration::kConstraint: {
+      const auto& constraint = declaration.constraint();
+      out << "constraint ";
+      IdentifierToCarbon(constraint.name(), out);
+      out << " {\n";
+      for (const auto& member : constraint.members()) {
+        DeclarationToCarbon(member, out);
+        out << "\n";
+      }
+      out << "}";
       break;
     }
 

+ 3 - 1
explorer/ast/ast_rtti.txt

@@ -22,7 +22,9 @@ abstract class Declaration : AstNode;
   class MixDeclaration : Declaration;
   class ChoiceDeclaration : Declaration;
   class VariableDeclaration : Declaration;
-  class InterfaceDeclaration : Declaration;
+  abstract class ConstraintTypeDeclaration : Declaration;
+    class InterfaceDeclaration : ConstraintTypeDeclaration;
+    class ConstraintDeclaration : ConstraintTypeDeclaration;
   class InterfaceExtendsDeclaration : Declaration;
   class InterfaceImplDeclaration : Declaration;
   class AssociatedConstantDeclaration : Declaration;

+ 10 - 3
explorer/ast/declaration.cpp

@@ -15,8 +15,9 @@ Declaration::~Declaration() = default;
 
 void Declaration::Print(llvm::raw_ostream& out) const {
   switch (kind()) {
-    case DeclarationKind::InterfaceDeclaration: {
-      const auto& iface_decl = cast<InterfaceDeclaration>(*this);
+    case DeclarationKind::InterfaceDeclaration:
+    case DeclarationKind::ConstraintDeclaration: {
+      const auto& iface_decl = cast<ConstraintTypeDeclaration>(*this);
       PrintID(out);
       out << " {\n";
       for (Nonnull<Declaration*> m : iface_decl.members()) {
@@ -120,6 +121,11 @@ void Declaration::PrintID(llvm::raw_ostream& out) const {
       out << "interface " << iface_decl.name();
       break;
     }
+    case DeclarationKind::ConstraintDeclaration: {
+      const auto& constraint_decl = cast<ConstraintDeclaration>(*this);
+      out << "constraint " << constraint_decl.name();
+      break;
+    }
     case DeclarationKind::ImplDeclaration: {
       const auto& impl_decl = cast<ImplDeclaration>(*this);
       switch (impl_decl.kind()) {
@@ -217,7 +223,8 @@ auto GetName(const Declaration& declaration)
     case DeclarationKind::ChoiceDeclaration:
       return cast<ChoiceDeclaration>(declaration).name();
     case DeclarationKind::InterfaceDeclaration:
-      return cast<InterfaceDeclaration>(declaration).name();
+    case DeclarationKind::ConstraintDeclaration:
+      return cast<ConstraintTypeDeclaration>(declaration).name();
     case DeclarationKind::VariableDeclaration:
       return cast<VariableDeclaration>(declaration).binding().name();
     case DeclarationKind::AssociatedConstantDeclaration:

+ 46 - 7
explorer/ast/declaration.h

@@ -487,15 +487,18 @@ class VariableDeclaration : public Declaration {
   ValueCategory value_category_;
 };
 
-class InterfaceDeclaration : public Declaration {
+// Base class for constraint and interface declarations. Interfaces and named
+// constraints behave the same in most respects, but only interfaces can
+// introduce new associated functions and constants.
+class ConstraintTypeDeclaration : public Declaration {
  public:
   using ImplementsCarbonValueNode = void;
 
-  InterfaceDeclaration(Nonnull<Arena*> arena, SourceLocation source_loc,
-                       std::string name,
-                       std::optional<Nonnull<TuplePattern*>> params,
-                       std::vector<Nonnull<Declaration*>> members)
-      : Declaration(AstNodeKind::InterfaceDeclaration, source_loc),
+  ConstraintTypeDeclaration(AstNodeKind kind, Nonnull<Arena*> arena,
+                            SourceLocation source_loc, std::string name,
+                            std::optional<Nonnull<TuplePattern*>> params,
+                            std::vector<Nonnull<Declaration*>> members)
+      : Declaration(kind, source_loc),
         name_(std::move(name)),
         params_(params),
         self_type_(arena->New<SelfDeclaration>(source_loc)),
@@ -507,7 +510,7 @@ class InterfaceDeclaration : public Declaration {
   }
 
   static auto classof(const AstNode* node) -> bool {
-    return InheritsFromInterfaceDeclaration(node->kind());
+    return InheritsFromConstraintTypeDeclaration(node->kind());
   }
 
   auto name() const -> const std::string& { return name_; }
@@ -553,6 +556,42 @@ class InterfaceDeclaration : public Declaration {
   std::optional<Nonnull<const ConstraintType*>> constraint_type_;
 };
 
+// A `interface` declaration.
+class InterfaceDeclaration : public ConstraintTypeDeclaration {
+ public:
+  using ImplementsCarbonValueNode = void;
+
+  InterfaceDeclaration(Nonnull<Arena*> arena, SourceLocation source_loc,
+                       std::string name,
+                       std::optional<Nonnull<TuplePattern*>> params,
+                       std::vector<Nonnull<Declaration*>> members)
+      : ConstraintTypeDeclaration(AstNodeKind::InterfaceDeclaration, arena,
+                                  source_loc, std::move(name), params,
+                                  std::move(members)) {}
+
+  static auto classof(const AstNode* node) -> bool {
+    return InheritsFromInterfaceDeclaration(node->kind());
+  }
+};
+
+// A `constraint` declaration, such as `constraint X { impl as Y; }`.
+class ConstraintDeclaration : public ConstraintTypeDeclaration {
+ public:
+  using ImplementsCarbonValueNode = void;
+
+  ConstraintDeclaration(Nonnull<Arena*> arena, SourceLocation source_loc,
+                        std::string name,
+                        std::optional<Nonnull<TuplePattern*>> params,
+                        std::vector<Nonnull<Declaration*>> members)
+      : ConstraintTypeDeclaration(AstNodeKind::ConstraintDeclaration, arena,
+                                  source_loc, std::move(name), params,
+                                  std::move(members)) {}
+
+  static auto classof(const AstNode* node) -> bool {
+    return InheritsFromConstraintDeclaration(node->kind());
+  }
+};
+
 // An `extends` declaration in an interface.
 class InterfaceExtendsDeclaration : public Declaration {
  public:

+ 38 - 28
explorer/data/prelude.carbon

@@ -18,7 +18,7 @@ interface ImplicitAs(T:! Type) {
   extends As(T);
 }
 
-// TODO: Should we just use an intrinsic for this?
+// TODO: This should be private.
 interface __EqualConverter {
   let T:! Type;
   fn Convert(t: T) -> Self;
@@ -72,8 +72,10 @@ interface EqWith(U:! Type) {
   fn Equal[me: Self](other: U) -> bool;
   fn NotEqual[me: Self](other: U) -> bool;
 }
-// TODO: constraint Eq { ... }
 
+constraint Eq {
+  extends EqWith(Self);
+}
 
 // TODO: Simplify this once we have variadics
 impl forall [T2:! Type, U2:! Type, T1:! EqWith(T2), U1:! EqWith(U2)]
@@ -130,11 +132,14 @@ choice Ordering {
   Incomparable
 }
 
+// TODO: Per the design, this should be named `OrderedWith`.
 interface CompareWith(U:! Type) {
   fn Compare[me: Self](u: U) -> Ordering;
   // TODO: Add `default fn` for Less, LessOrEquivalent, Greater, and GreaterOrEquivalent once it's available.
 }
-// TODO: constraint Ordered { ... }
+constraint Ordered {
+  extends CompareWith(Self);
+}
 
 impl i32 as CompareWith(Self) {
   fn Compare[me: Self](other: Self) -> Ordering {
@@ -308,35 +313,45 @@ interface AddWith(U:! Type) {
   let Result:! Type;
   fn Op[me: Self](other: U) -> Result;
 }
-// TODO: constraint Add { ... }
+constraint Add {
+  extends AddWith(Self) where .Result = Self;
+}
 
 interface SubWith(U:! Type) {
   // TODO: = Self
   let Result:! Type;
   fn Op[me: Self](other: U) -> Result;
 }
-// TODO: constraint Sub { ... }
+constraint Sub {
+  extends SubWith(Self) where .Result = Self;
+}
 
 interface MulWith(U:! Type) {
   // TODO: = Self
   let Result:! Type;
   fn Op[me: Self](other: U) -> Result;
 }
-// TODO: constraint Mul { ... }
+constraint Mul {
+  extends MulWith(Self) where .Result = Self;
+}
 
 interface DivWith(U:! Type) {
   // TODO: = Self
   let Result:! Type;
   fn Op[me: Self](other: U) -> Result;
 }
-// TODO: constraint Div { ... }
+constraint Div {
+  extends DivWith(Self) where .Result = Self;
+}
 
 interface ModWith(U:! Type) {
   // TODO: = Self
   let Result:! Type;
   fn Op[me: Self](other: U) -> Result;
 }
-// TODO: constraint Mod { ... }
+constraint Mod {
+  extends ModWith(Self) where .Result = Self;
+}
 
 // Note, these impls use the builtin addition for i32.
 external impl i32 as Negate where .Result = i32 {
@@ -375,10 +390,9 @@ interface BitAndWith(U:! Type) {
   let Result:! Type;
   fn Op[me: Self](other: U) -> Result;
 }
-// TODO:
-// constraint BitAnd {
-//   extends BitAndWith(Self) where .Result = Self;
-// }
+constraint BitAnd {
+  extends BitAndWith(Self) where .Result = Self;
+}
 
 // Binary `|`.
 interface BitOrWith(U:! Type) {
@@ -386,10 +400,9 @@ interface BitOrWith(U:! Type) {
   let Result:! Type;
   fn Op[me: Self](other: U) -> Result;
 }
-// TODO:
-// constraint BitOr {
-//   extends BitOrWith(Self) where .Result = Self;
-// }
+constraint BitOr {
+  extends BitOrWith(Self) where .Result = Self;
+}
 
 // Binary `^`.
 interface BitXorWith(U:! Type) {
@@ -397,10 +410,9 @@ interface BitXorWith(U:! Type) {
   let Result:! Type;
   fn Op[me: Self](other: U) -> Result;
 }
-// TODO:
-// constraint BitXor {
-//   extends BitXorWith(Self) where .Result = Self;
-// }
+constraint BitXor {
+  extends BitXorWith(Self) where .Result = Self;
+}
 
 // Binary `<<`.
 interface LeftShiftWith(U:! Type) {
@@ -408,10 +420,9 @@ interface LeftShiftWith(U:! Type) {
   let Result:! Type;
   fn Op[me: Self](other: U) -> Result;
 }
-// TODO:
-// constraint LeftShift {
-//   extends LeftShiftWith(Self) where .Result = Self;
-// }
+constraint LeftShift {
+  extends LeftShiftWith(Self) where .Result = Self;
+}
 
 // Binary `>>`.
 interface RightShiftWith(U:! Type) {
@@ -419,10 +430,9 @@ interface RightShiftWith(U:! Type) {
   let Result:! Type;
   fn Op[me: Self](other: U) -> Result;
 }
-// TODO:
-// constraint RightShift {
-//   extends RightShiftWith(Self) where .Result = Self;
-// }
+constraint RightShift {
+  extends RightShiftWith(Self) where .Result = Self;
+}
 
 external impl i32 as BitComplement where .Result = i32 {
   fn Op[me: i32]() -> i32 {

+ 10 - 2
explorer/fuzzing/ast_to_proto.cpp

@@ -739,8 +739,16 @@ static auto DeclarationToProto(const Declaration& declaration)
       for (const auto& member : interface.members()) {
         *interface_proto->add_members() = DeclarationToProto(*member);
       }
-      *interface_proto->mutable_self() =
-          GenericBindingToProto(*interface.self());
+      break;
+    }
+
+    case DeclarationKind::ConstraintDeclaration: {
+      const auto& constraint = cast<ConstraintDeclaration>(declaration);
+      auto* constraint_proto = declaration_proto.mutable_constraint();
+      constraint_proto->set_name(constraint.name());
+      for (const auto& member : constraint.members()) {
+        *constraint_proto->add_members() = DeclarationToProto(*member);
+      }
       break;
     }
 

+ 20 - 3
explorer/interpreter/interpreter.cpp

@@ -602,6 +602,14 @@ auto Interpreter::InstantiateType(Nonnull<const Value*> type,
       return arena_->New<InterfaceType>(&interface_type.declaration(),
                                         bindings);
     }
+    case Value::Kind::NamedConstraintType: {
+      const auto& constraint_type = cast<NamedConstraintType>(*type);
+      CARBON_ASSIGN_OR_RETURN(
+          Nonnull<const Bindings*> bindings,
+          InstantiateBindings(&constraint_type.bindings(), source_loc));
+      return arena_->New<NamedConstraintType>(&constraint_type.declaration(),
+                                              bindings);
+    }
     case Value::Kind::NominalClassType: {
       const auto& class_type = cast<NominalClassType>(*type);
       CARBON_ASSIGN_OR_RETURN(
@@ -681,6 +689,7 @@ auto Interpreter::Convert(Nonnull<const Value*> value,
     case Value::Kind::NominalClassType:
     case Value::Kind::MixinPseudoType:
     case Value::Kind::InterfaceType:
+    case Value::Kind::NamedConstraintType:
     case Value::Kind::ConstraintType:
     case Value::Kind::ImplWitness:
     case Value::Kind::BindingWitness:
@@ -732,6 +741,7 @@ auto Interpreter::Convert(Nonnull<const Value*> value,
         }
         case Value::Kind::TypeType:
         case Value::Kind::ConstraintType:
+        case Value::Kind::NamedConstraintType:
         case Value::Kind::InterfaceType: {
           CARBON_CHECK(struct_val.elements().empty())
               << "only empty structs convert to Type";
@@ -762,6 +772,7 @@ auto Interpreter::Convert(Nonnull<const Value*> value,
         }
         case Value::Kind::TypeType:
         case Value::Kind::ConstraintType:
+        case Value::Kind::NamedConstraintType:
         case Value::Kind::InterfaceType: {
           std::vector<Nonnull<const Value*>> new_elements;
           Nonnull<const Value*> type_type = arena_->New<TypeType>();
@@ -798,8 +809,9 @@ auto Interpreter::Convert(Nonnull<const Value*> value,
           EvalAssociatedConstant(cast<AssociatedConstant>(value), source_loc));
       if (auto* new_const = dyn_cast<AssociatedConstant>(value)) {
         // TODO: Detect whether conversions are required in type-checking.
-        if (isa<TypeType, ConstraintType, InterfaceType>(destination_type) &&
-            isa<TypeType, ConstraintType, InterfaceType>(
+        if (isa<TypeType, ConstraintType, NamedConstraintType, InterfaceType>(
+                destination_type) &&
+            isa<TypeType, ConstraintType, NamedConstraintType, InterfaceType>(
                 new_const->constant().static_type())) {
           // No further conversions are required.
           return value;
@@ -958,6 +970,9 @@ auto Interpreter::CallFunction(const CallExpression& call,
         case DeclarationKind::InterfaceDeclaration:
           return todo_.FinishAction(arena_->New<InterfaceType>(
               &cast<InterfaceDeclaration>(decl), bindings));
+        case DeclarationKind::ConstraintDeclaration:
+          return todo_.FinishAction(arena_->New<NamedConstraintType>(
+              &cast<ConstraintDeclaration>(decl), bindings));
         case DeclarationKind::ChoiceDeclaration:
           return todo_.FinishAction(arena_->New<ChoiceType>(
               &cast<ChoiceDeclaration>(decl), bindings));
@@ -1063,7 +1078,8 @@ auto Interpreter::StepExp() -> ErrorOr<Success> {
           CARBON_CHECK(phase() == Phase::CompileTime)
               << "should not form MemberNames at runtime";
           std::optional<const Value*> type_result;
-          if (!isa<InterfaceType, ConstraintType>(act.results()[0])) {
+          if (!isa<InterfaceType, NamedConstraintType, ConstraintType>(
+                  act.results()[0])) {
             type_result = act.results()[0];
           }
           MemberName* member_name = arena_->New<MemberName>(
@@ -2001,6 +2017,7 @@ auto Interpreter::StepDeclaration() -> ErrorOr<Success> {
     case DeclarationKind::MixDeclaration:
     case DeclarationKind::ChoiceDeclaration:
     case DeclarationKind::InterfaceDeclaration:
+    case DeclarationKind::ConstraintDeclaration:
     case DeclarationKind::InterfaceExtendsDeclaration:
     case DeclarationKind::InterfaceImplDeclaration:
     case DeclarationKind::AssociatedConstantDeclaration:

+ 3 - 2
explorer/interpreter/resolve_control_flow.cpp

@@ -163,8 +163,9 @@ auto ResolveControlFlow(Nonnull<Declaration*> declaration) -> ErrorOr<Success> {
       }
       break;
     }
-    case DeclarationKind::InterfaceDeclaration: {
-      auto& iface_decl = cast<InterfaceDeclaration>(*declaration);
+    case DeclarationKind::InterfaceDeclaration:
+    case DeclarationKind::ConstraintDeclaration: {
+      auto& iface_decl = cast<ConstraintTypeDeclaration>(*declaration);
       for (Nonnull<Declaration*> member : iface_decl.members()) {
         CARBON_RETURN_IF_ERROR(ResolveControlFlow(member));
       }

+ 7 - 5
explorer/interpreter/resolve_names.cpp

@@ -22,8 +22,9 @@ namespace Carbon {
 static auto AddExposedNames(const Declaration& declaration,
                             StaticScope& enclosing_scope) -> ErrorOr<Success> {
   switch (declaration.kind()) {
-    case DeclarationKind::InterfaceDeclaration: {
-      const auto& iface_decl = cast<InterfaceDeclaration>(declaration);
+    case DeclarationKind::InterfaceDeclaration:
+    case DeclarationKind::ConstraintDeclaration: {
+      const auto& iface_decl = cast<ConstraintTypeDeclaration>(declaration);
       CARBON_RETURN_IF_ERROR(
           enclosing_scope.Add(iface_decl.name(), &iface_decl,
                               StaticScope::NameStatus::KnownButNotDeclared));
@@ -517,8 +518,9 @@ static auto ResolveMemberNames(llvm::ArrayRef<Nonnull<Declaration*>> members,
 static auto ResolveNames(Declaration& declaration, StaticScope& enclosing_scope,
                          ResolveFunctionBodies bodies) -> ErrorOr<Success> {
   switch (declaration.kind()) {
-    case DeclarationKind::InterfaceDeclaration: {
-      auto& iface = cast<InterfaceDeclaration>(declaration);
+    case DeclarationKind::InterfaceDeclaration:
+    case DeclarationKind::ConstraintDeclaration: {
+      auto& iface = cast<ConstraintTypeDeclaration>(declaration);
       StaticScope iface_scope;
       iface_scope.AddParent(&enclosing_scope);
       enclosing_scope.MarkDeclared(iface.name());
@@ -527,7 +529,7 @@ static auto ResolveNames(Declaration& declaration, StaticScope& enclosing_scope,
       }
       enclosing_scope.MarkUsable(iface.name());
       // Don't resolve names in the type of the self binding. The
-      // InterfaceDeclaration constructor already did that.
+      // ConstraintTypeDeclaration constructor already did that.
       CARBON_RETURN_IF_ERROR(iface_scope.Add("Self", iface.self()));
       CARBON_RETURN_IF_ERROR(
           ResolveMemberNames(iface.members(), iface_scope, bodies));

+ 1 - 0
explorer/interpreter/resolve_unformed.cpp

@@ -303,6 +303,7 @@ static auto ResolveUnformed(Nonnull<const Declaration*> declaration)
     case DeclarationKind::MixDeclaration:
     case DeclarationKind::MixinDeclaration:
     case DeclarationKind::InterfaceDeclaration:
+    case DeclarationKind::ConstraintDeclaration:
     case DeclarationKind::ImplDeclaration:
     case DeclarationKind::ChoiceDeclaration:
     case DeclarationKind::VariableDeclaration:

+ 211 - 116
explorer/interpreter/type_checker.cpp

@@ -138,6 +138,7 @@ static auto IsTypeOfType(Nonnull<const Value*> value) -> bool {
       return false;
     case Value::Kind::TypeType:
     case Value::Kind::InterfaceType:
+    case Value::Kind::NamedConstraintType:
     case Value::Kind::ConstraintType:
       // A value of one of these types is itself always a type.
       return true;
@@ -182,6 +183,7 @@ static auto IsType(Nonnull<const Value*> value) -> bool {
     case Value::Kind::TupleType:
     case Value::Kind::NominalClassType:
     case Value::Kind::InterfaceType:
+    case Value::Kind::NamedConstraintType:
     case Value::Kind::ConstraintType:
     case Value::Kind::ChoiceType:
     case Value::Kind::ContinuationType:
@@ -281,6 +283,12 @@ static auto ExpectCompleteType(SourceLocation source_loc,
       }
       break;
     }
+    case Value::Kind::NamedConstraintType: {
+      if (cast<NamedConstraintType>(type)->declaration().is_declared()) {
+        return Success();
+      }
+      break;
+    }
     case Value::Kind::InterfaceType: {
       if (cast<InterfaceType>(type)->declaration().is_declared()) {
         return Success();
@@ -348,6 +356,7 @@ static auto TypeContainsAuto(Nonnull<const Value*> type) -> bool {
     case Value::Kind::FunctionType:
     case Value::Kind::NominalClassType:
     case Value::Kind::InterfaceType:
+    case Value::Kind::NamedConstraintType:
     case Value::Kind::ConstraintType:
     case Value::Kind::ChoiceType:
     case Value::Kind::ContinuationType:
@@ -494,6 +503,7 @@ auto TypeChecker::IsImplicitlyConvertible(
           break;
         case Value::Kind::TypeType:
         case Value::Kind::InterfaceType:
+        case Value::Kind::NamedConstraintType:
         case Value::Kind::ConstraintType:
           // A value of empty struct type implicitly converts to a type.
           if (cast<StructType>(*source).fields().empty()) {
@@ -548,6 +558,7 @@ auto TypeChecker::IsImplicitlyConvertible(
         }
         case Value::Kind::TypeType:
         case Value::Kind::InterfaceType:
+        case Value::Kind::NamedConstraintType:
         case Value::Kind::ConstraintType: {
           // A tuple value converts to a type if all of its fields do.
           bool all_types = true;
@@ -571,6 +582,7 @@ auto TypeChecker::IsImplicitlyConvertible(
     }
     case Value::Kind::TypeType:
     case Value::Kind::InterfaceType:
+    case Value::Kind::NamedConstraintType:
     case Value::Kind::ConstraintType:
       // TODO: We can't tell whether the conversion to this type-of-type would
       // work, because that depends on the source value, and we only have its
@@ -614,7 +626,11 @@ auto TypeChecker::ImplicitlyConvert(std::string_view context,
                               /*allow_user_defined_conversions=*/false)) {
     // A type only implicitly converts to a constraint if there is an impl of
     // that constraint for that type in scope.
-    if (isa<InterfaceType, ConstraintType>(destination)) {
+    // TODO: Instead of excluding the special case where the destination is
+    // `Type`, we should check if the source type has a subset of the
+    // constraints of the destination type. In that case, the source should not
+    // be required to be constant.
+    if (IsTypeOfType(destination) && !isa<TypeType>(destination)) {
       // First convert the source expression to type `Type`.
       CARBON_ASSIGN_OR_RETURN(Nonnull<Expression*> source_as_type,
                               ImplicitlyConvert(context, impl_scope, source,
@@ -1025,6 +1041,24 @@ auto TypeChecker::ArgumentDeduction::Deduce(Nonnull<const Value*> param,
       }
       return Success();
     }
+    case Value::Kind::NamedConstraintType: {
+      const auto& param_constraint_type = cast<NamedConstraintType>(*param);
+      if (arg->kind() != Value::Kind::NamedConstraintType) {
+        return handle_non_deduced_type();
+      }
+      const auto& arg_constraint_type = cast<NamedConstraintType>(*arg);
+      if (param_constraint_type.declaration().name() !=
+          arg_constraint_type.declaration().name()) {
+        return handle_non_deduced_type();
+      }
+      for (const auto& [ty, param_ty] :
+           param_constraint_type.bindings().args()) {
+        CARBON_RETURN_IF_ERROR(
+            Deduce(param_ty, arg_constraint_type.bindings().args().at(ty),
+                   /*allow_implicit_conversion=*/false));
+      }
+      return Success();
+    }
     // For the following cases, we check the type matches.
     case Value::Kind::StaticArrayType:
       // TODO: We could deduce the array type from an array or tuple argument.
@@ -1886,6 +1920,14 @@ auto TypeChecker::SubstituteImpl(const Bindings& bindings,
           substitute_into_bindings(&iface_type.bindings()));
       return new_iface_type;
     }
+    case Value::Kind::NamedConstraintType: {
+      const auto& constraint_type = cast<NamedConstraintType>(*type);
+      Nonnull<const NamedConstraintType*> new_constraint_type =
+          arena_->New<NamedConstraintType>(
+              &constraint_type.declaration(),
+              substitute_into_bindings(&constraint_type.bindings()));
+      return new_constraint_type;
+    }
     case Value::Kind::ConstraintType: {
       const auto& constraint = cast<ConstraintType>(*type);
       if (auto it = bindings.args().find(constraint.self_binding());
@@ -2110,27 +2152,6 @@ auto TypeChecker::MakeConstraintWitnessAccess(Nonnull<const Witness*> witness,
   return ConstraintImplWitness::Make(arena_, witness, impl_offset);
 }
 
-auto TypeChecker::MakeConstraintForInterface(
-    SourceLocation source_loc, Nonnull<const InterfaceType*> iface_type) const
-    -> ErrorOr<Nonnull<const ConstraintType*>> {
-  CARBON_RETURN_IF_ERROR(
-      ExpectCompleteType(source_loc, "constraint", iface_type));
-
-  auto constraint_type = iface_type->declaration().constraint_type();
-  CARBON_CHECK(constraint_type)
-      << "complete interface should have a constraint type";
-
-  if (iface_type->bindings().empty()) {
-    return *constraint_type;
-  }
-
-  ConstraintTypeBuilder builder(arena_, source_loc);
-  builder.AddAndSubstitute(*this, *constraint_type, builder.GetSelfType(),
-                           builder.GetSelfWitness(), iface_type->bindings(),
-                           /*add_lookup_contexts=*/true);
-  return std::move(builder).Build();
-}
-
 auto TypeChecker::ConvertToConstraintType(
     SourceLocation source_loc, std::string_view context,
     Nonnull<const Value*> constraint) const
@@ -2139,7 +2160,17 @@ auto TypeChecker::ConvertToConstraintType(
     return constraint_type;
   }
   if (const auto* iface_type = dyn_cast<InterfaceType>(constraint)) {
-    return MakeConstraintForInterface(source_loc, iface_type);
+    CARBON_RETURN_IF_ERROR(
+        ExpectCompleteType(source_loc, "constraint", iface_type));
+    return cast<ConstraintType>(Substitute(
+        iface_type->bindings(), *iface_type->declaration().constraint_type()));
+  }
+  if (const auto* constraint_type = dyn_cast<NamedConstraintType>(constraint)) {
+    CARBON_RETURN_IF_ERROR(
+        ExpectCompleteType(source_loc, "constraint", constraint_type));
+    return cast<ConstraintType>(
+        Substitute(constraint_type->bindings(),
+                   *constraint_type->declaration().constraint_type()));
   }
   if (isa<TypeType>(constraint)) {
     // TODO: Should we build this once and cache it?
@@ -2600,8 +2631,8 @@ auto TypeChecker::TypeCheckExp(Nonnull<Expression*> e,
           // the impl scope, to find the witness.
           CARBON_ASSIGN_OR_RETURN(
               Nonnull<const ConstraintType*> iface_constraint,
-              MakeConstraintForInterface(access.source_loc(),
-                                         result.interface));
+              ConvertToConstraintType(access.source_loc(), "member access",
+                                      result.interface));
           CARBON_ASSIGN_OR_RETURN(
               Nonnull<const Witness*> witness,
               impl_scope.Resolve(iface_constraint, &object_type,
@@ -2630,6 +2661,7 @@ auto TypeChecker::TypeCheckExp(Nonnull<Expression*> e,
           return Success();
         }
         case Value::Kind::InterfaceType:
+        case Value::Kind::NamedConstraintType:
         case Value::Kind::ConstraintType: {
           // This case handles access to a class function from a constrained
           // type variable. If `T` is a type variable and `foo` is a class
@@ -2653,8 +2685,8 @@ auto TypeChecker::TypeCheckExp(Nonnull<Expression*> e,
           }
           CARBON_ASSIGN_OR_RETURN(
               Nonnull<const ConstraintType*> iface_constraint,
-              MakeConstraintForInterface(access.source_loc(),
-                                         result.interface));
+              ConvertToConstraintType(access.source_loc(), "member access",
+                                      result.interface));
           CARBON_ASSIGN_OR_RETURN(Nonnull<const Witness*> witness,
                                   impl_scope.Resolve(iface_constraint, type,
                                                      e->source_loc(), *this));
@@ -2772,6 +2804,7 @@ auto TypeChecker::TypeCheckExp(Nonnull<Expression*> e,
               }
             }
             case Value::Kind::InterfaceType:
+            case Value::Kind::NamedConstraintType:
             case Value::Kind::ConstraintType: {
               CARBON_ASSIGN_OR_RETURN(
                   ConstraintLookupResult result,
@@ -2855,7 +2888,8 @@ auto TypeChecker::TypeCheckExp(Nonnull<Expression*> e,
 
         CARBON_ASSIGN_OR_RETURN(
             Nonnull<const ConstraintType*> iface_constraint,
-            MakeConstraintForInterface(access.source_loc(), *iface));
+            ConvertToConstraintType(access.source_loc(),
+                                    "compound member access", *iface));
         CARBON_ASSIGN_OR_RETURN(witness,
                                 impl_scope.Resolve(iface_constraint, *base_type,
                                                    e->source_loc(), *this));
@@ -3222,8 +3256,8 @@ auto TypeChecker::TypeCheckExp(Nonnull<Expression*> e,
           // Currently the only kinds of parameterized entities we support are
           // types.
           CARBON_CHECK(
-              isa<ClassDeclaration, InterfaceDeclaration, ChoiceDeclaration>(
-                  param_name.declaration()))
+              isa<ClassDeclaration, InterfaceDeclaration, ConstraintDeclaration,
+                  ChoiceDeclaration>(param_name.declaration()))
               << "unknown type of ParameterizedEntityName for " << param_name;
           call.set_static_type(arena_->New<TypeType>());
           call.set_value_category(ValueCategory::Let);
@@ -3980,7 +4014,7 @@ auto TypeChecker::TypeCheckGenericBinding(GenericBinding& binding,
   }
 
   // Create an impl binding if we have a constraint.
-  if (isa<ConstraintType, InterfaceType>(type)) {
+  if (IsTypeOfType(type) && !isa<TypeType>(type)) {
     CARBON_ASSIGN_OR_RETURN(
         Nonnull<const ConstraintType*> constraint,
         ConvertToConstraintType(binding.source_loc(), context, type));
@@ -4694,67 +4728,95 @@ auto TypeChecker::TypeCheckMixDeclaration(
   return Success();
 }
 
-auto TypeChecker::DeclareInterfaceDeclaration(
-    Nonnull<InterfaceDeclaration*> iface_decl, const ScopeInfo& scope_info)
-    -> ErrorOr<Success> {
+auto TypeChecker::DeclareConstraintTypeDeclaration(
+    Nonnull<ConstraintTypeDeclaration*> constraint_decl,
+    const ScopeInfo& scope_info) -> ErrorOr<Success> {
+  CARBON_CHECK(
+      isa<InterfaceDeclaration, ConstraintDeclaration>(constraint_decl))
+      << "unexpected kind of constraint type declaration";
+  bool is_interface = isa<InterfaceDeclaration>(constraint_decl);
+
   if (trace_stream_) {
-    **trace_stream_ << "** declaring interface " << iface_decl->name() << "\n";
+    **trace_stream_ << "** declaring ";
+    constraint_decl->PrintID(**trace_stream_);
+    **trace_stream_ << "\n";
   }
-  ImplScope iface_scope;
-  iface_scope.AddParent(scope_info.innermost_scope);
+  ImplScope constraint_scope;
+  constraint_scope.AddParent(scope_info.innermost_scope);
 
-  Nonnull<InterfaceType*> iface_type;
-  if (iface_decl->params().has_value()) {
-    CARBON_RETURN_IF_ERROR(TypeCheckPattern(*iface_decl->params(), std::nullopt,
-                                            iface_scope, ValueCategory::Let));
+  // Type-check the parameters and find the set of bindings that are in scope.
+  std::vector<Nonnull<const GenericBinding*>> bindings = scope_info.bindings;
+  if (constraint_decl->params().has_value()) {
+    CARBON_RETURN_IF_ERROR(TypeCheckPattern(*constraint_decl->params(),
+                                            std::nullopt, constraint_scope,
+                                            ValueCategory::Let));
     if (trace_stream_) {
-      **trace_stream_ << iface_scope;
+      **trace_stream_ << constraint_scope;
     }
+    CollectGenericBindingsInPattern(*constraint_decl->params(), bindings);
+  }
 
+  // Form the full symbolic type of the interface or named constraint. This is
+  // used as part of the value of associated constants, if they're referenced
+  // within their interface, and as the symbolic value of the declaration.
+  Nonnull<const Value*> constraint_type;
+  if (is_interface) {
+    constraint_type = arena_->New<InterfaceType>(
+        cast<InterfaceDeclaration>(constraint_decl),
+        Bindings::SymbolicIdentity(arena_, bindings));
+  } else {
+    constraint_type = arena_->New<NamedConstraintType>(
+        cast<ConstraintDeclaration>(constraint_decl),
+        Bindings::SymbolicIdentity(arena_, bindings));
+  }
+
+  // Set up the meaning of the declaration when used as an identifier.
+  if (constraint_decl->params().has_value()) {
     Nonnull<ParameterizedEntityName*> param_name =
-        arena_->New<ParameterizedEntityName>(iface_decl, *iface_decl->params());
-    iface_decl->set_static_type(
+        arena_->New<ParameterizedEntityName>(constraint_decl,
+                                             *constraint_decl->params());
+    constraint_decl->set_static_type(
         arena_->New<TypeOfParameterizedEntityName>(param_name));
-    iface_decl->set_constant_value(param_name);
-
-    // Form the full symbolic type of the interface. This is used as part of
-    // the value of associated constants, if they're referenced within the
-    // interface itself.
-    std::vector<Nonnull<const GenericBinding*>> bindings = scope_info.bindings;
-    CollectGenericBindingsInPattern(*iface_decl->params(), bindings);
-    iface_type = arena_->New<InterfaceType>(
-        iface_decl, Bindings::SymbolicIdentity(arena_, bindings));
+    constraint_decl->set_constant_value(param_name);
   } else {
-    iface_type = arena_->New<InterfaceType>(iface_decl);
-    iface_decl->set_static_type(arena_->New<TypeType>());
-    iface_decl->set_constant_value(iface_type);
+    constraint_decl->set_static_type(arena_->New<TypeType>());
+    constraint_decl->set_constant_value(constraint_type);
   }
 
-  // Set the type of Self to be the instantiated interface.
-  Nonnull<SelfDeclaration*> self_type = iface_decl->self_type();
+  // Set the type of Self to be the instantiated constraint type.
+  Nonnull<SelfDeclaration*> self_type = constraint_decl->self_type();
   self_type->set_static_type(arena_->New<TypeType>());
-  self_type->set_constant_value(iface_type);
+  self_type->set_constant_value(constraint_type);
 
-  // Build a constraint corresponding to this interface.
-  ConstraintTypeBuilder::PrepareSelfBinding(arena_, iface_decl->self());
-  ConstraintTypeBuilder builder(arena_, iface_decl->self());
+  // Build a constraint corresponding to this constraint type.
+  ConstraintTypeBuilder::PrepareSelfBinding(arena_, constraint_decl->self());
+  ConstraintTypeBuilder builder(arena_, constraint_decl->self());
   ConstraintTypeBuilder::ConstraintsInScopeTracker constraint_tracker;
-  iface_decl->self()->set_static_type(iface_type);
-
-  // The impl constraint says only that the direct members of the interface are
-  // available. For any indirect constraints, we need to add separate entries
-  // to the constraint type. This ensures that all indirect constraints are
-  // lifted to the top level so they can be accessed directly and resolved
-  // independently if necessary.
-  int index = builder.AddImplConstraint(
-      {.type = builder.GetSelfType(), .interface = iface_type});
-  builder.AddLookupContext({.context = iface_type});
-  const auto* impl_witness =
-      MakeConstraintWitnessAccess(builder.GetSelfWitness(), index);
-
-  ScopeInfo iface_scope_info = ScopeInfo::ForNonClassScope(&iface_scope);
-  for (Nonnull<Declaration*> m : iface_decl->members()) {
-    CARBON_RETURN_IF_ERROR(DeclareDeclaration(m, iface_scope_info));
+  constraint_decl->self()->set_static_type(constraint_type);
+
+  // Lookups into this constraint type look in this declaration.
+  builder.AddLookupContext({.context = constraint_type});
+
+  // If this is an interface, this is a symbolic witness that Self implements
+  // this interface.
+  std::optional<Nonnull<const Witness*>> iface_impl_witness;
+  if (is_interface) {
+    // The impl constraint says only that the direct members of the interface
+    // are available. For any indirect constraints, we need to add separate
+    // entries to the constraint type. This ensures that all indirect
+    // constraints are lifted to the top level so they can be accessed directly
+    // and resolved independently if necessary.
+    int index = builder.AddImplConstraint(
+        {.type = builder.GetSelfType(),
+         .interface = cast<InterfaceType>(constraint_type)});
+    iface_impl_witness =
+        MakeConstraintWitnessAccess(builder.GetSelfWitness(), index);
+  }
+
+  ScopeInfo constraint_scope_info =
+      ScopeInfo::ForNonClassScope(&constraint_scope);
+  for (Nonnull<Declaration*> m : constraint_decl->members()) {
+    CARBON_RETURN_IF_ERROR(DeclareDeclaration(m, constraint_scope_info));
 
     // TODO: This should probably live in `DeclareDeclaration`, but it needs
     // to update state that's not available from there.
@@ -4762,8 +4824,9 @@ auto TypeChecker::DeclareInterfaceDeclaration(
       case DeclarationKind::InterfaceExtendsDeclaration: {
         // For an `extends C;` declaration, add `Self is C` to our constraint.
         auto* extends = cast<InterfaceExtendsDeclaration>(m);
-        CARBON_ASSIGN_OR_RETURN(Nonnull<const Value*> base,
-                                TypeCheckTypeExp(extends->base(), iface_scope));
+        CARBON_ASSIGN_OR_RETURN(
+            Nonnull<const Value*> base,
+            TypeCheckTypeExp(extends->base(), constraint_scope));
         CARBON_ASSIGN_OR_RETURN(
             Nonnull<const ConstraintType*> constraint_type,
             ConvertToConstraintType(m->source_loc(), "extends declaration",
@@ -4779,10 +4842,10 @@ auto TypeChecker::DeclareInterfaceDeclaration(
         auto* impl = cast<InterfaceImplDeclaration>(m);
         CARBON_ASSIGN_OR_RETURN(
             Nonnull<const Value*> impl_type,
-            TypeCheckTypeExp(impl->impl_type(), iface_scope));
+            TypeCheckTypeExp(impl->impl_type(), constraint_scope));
         CARBON_ASSIGN_OR_RETURN(
             Nonnull<const Value*> constraint,
-            TypeCheckTypeExp(impl->constraint(), iface_scope));
+            TypeCheckTypeExp(impl->constraint(), constraint_scope));
         CARBON_ASSIGN_OR_RETURN(
             Nonnull<const ConstraintType*> constraint_type,
             ConvertToConstraintType(m->source_loc(), "impl as declaration",
@@ -4795,24 +4858,31 @@ auto TypeChecker::DeclareInterfaceDeclaration(
 
       case DeclarationKind::AssociatedConstantDeclaration: {
         auto* assoc = cast<AssociatedConstantDeclaration>(m);
+        if (!is_interface) {
+          // TODO: Template constraints can have associated constants.
+          return ProgramError(assoc->source_loc())
+                 << "associated constant not permitted in named constraint";
+        }
+
         CARBON_RETURN_IF_ERROR(TypeCheckGenericBinding(
-            assoc->binding(), "associated constant", iface_scope));
+            assoc->binding(), "associated constant", constraint_scope));
         Nonnull<const Value*> constraint = &assoc->binding().static_type();
         assoc->set_static_type(constraint);
 
         // The constant value is used if the constant is named later in the
-        // same interface. Note that this differs from the symbolic identity of
-        // the binding, which was set in TypeCheckGenericBinding to a
-        // VariableType naming the binding so that .Self resolves to the
+        // same constraint type. Note that this differs from the symbolic
+        // identity of the binding, which was set in TypeCheckGenericBinding to
+        // a VariableType naming the binding so that .Self resolves to the
         // binding itself.
         auto* assoc_value = arena_->New<AssociatedConstant>(
-            &iface_decl->self()->value(), iface_type, assoc, impl_witness);
+            &constraint_decl->self()->value(),
+            cast<InterfaceType>(constraint_type), assoc, *iface_impl_witness);
         assoc->set_constant_value(assoc_value);
 
         // The type specified for the associated constant becomes a
-        // constraint for the interface: `let X:! Interface` adds a `Self.X
-        // is Interface` constraint that `impl`s must satisfy and users of
-        // the interface can rely on.
+        // constraint for the constraint type: `let X:! Interface` adds a
+        // `Self.X is Interface` constraint that `impl`s must satisfy and users
+        // of the constraint type can rely on.
         if (auto* constraint_type = dyn_cast<ConstraintType>(constraint)) {
           builder.AddAndSubstitute(*this, constraint_type, assoc_value,
                                    builder.GetSelfWitness(), Bindings(),
@@ -4821,44 +4891,62 @@ auto TypeChecker::DeclareInterfaceDeclaration(
         break;
       }
 
+      case DeclarationKind::FunctionDeclaration: {
+        if (!is_interface) {
+          // TODO: Template constraints can have associated functions.
+          return ProgramError(m->source_loc())
+                 << "associated function not permitted in named constraint";
+        }
+        break;
+      }
+
       default: {
+        CARBON_FATAL()
+            << "unexpected declaration in constraint type declaration:\n"
+            << *m;
         break;
       }
     }
 
     // Add any new impl constraints to the scope.
-    builder.BringConstraintsIntoScope(*this, &iface_scope, &constraint_tracker);
+    builder.BringConstraintsIntoScope(*this, &constraint_scope,
+                                      &constraint_tracker);
   }
 
-  iface_decl->set_constraint_type(std::move(builder).Build());
+  constraint_decl->set_constraint_type(std::move(builder).Build());
 
   if (trace_stream_) {
-    **trace_stream_ << "** finished declaring interface " << iface_decl->name()
-                    << "\n";
+    **trace_stream_ << "** finished declaring ";
+    constraint_decl->PrintID(**trace_stream_);
+    **trace_stream_ << "\n";
   }
   return Success();
 }
 
-auto TypeChecker::TypeCheckInterfaceDeclaration(
-    Nonnull<InterfaceDeclaration*> iface_decl, const ImplScope& impl_scope)
-    -> ErrorOr<Success> {
+auto TypeChecker::TypeCheckConstraintTypeDeclaration(
+    Nonnull<ConstraintTypeDeclaration*> constraint_decl,
+    const ImplScope& impl_scope) -> ErrorOr<Success> {
   if (trace_stream_) {
-    **trace_stream_ << "** checking interface " << iface_decl->name() << "\n";
+    **trace_stream_ << "** checking ";
+    constraint_decl->PrintID(**trace_stream_);
+    **trace_stream_ << "\n";
   }
-  ImplScope iface_scope;
-  iface_scope.AddParent(&impl_scope);
-  if (iface_decl->params().has_value()) {
-    BringPatternImplsIntoScope(*iface_decl->params(), iface_scope);
+  ImplScope constraint_scope;
+  constraint_scope.AddParent(&impl_scope);
+  if (constraint_decl->params().has_value()) {
+    BringPatternImplsIntoScope(*constraint_decl->params(), constraint_scope);
   }
   if (trace_stream_) {
-    **trace_stream_ << iface_scope;
+    **trace_stream_ << constraint_scope;
   }
-  for (Nonnull<Declaration*> m : iface_decl->members()) {
-    CARBON_RETURN_IF_ERROR(TypeCheckDeclaration(m, iface_scope, iface_decl));
+  for (Nonnull<Declaration*> m : constraint_decl->members()) {
+    CARBON_RETURN_IF_ERROR(
+        TypeCheckDeclaration(m, constraint_scope, constraint_decl));
   }
   if (trace_stream_) {
-    **trace_stream_ << "** finished checking interface " << iface_decl->name()
-                    << "\n";
+    **trace_stream_ << "** finished checking ";
+    constraint_decl->PrintID(**trace_stream_);
+    **trace_stream_ << "\n";
   }
   return Success();
 }
@@ -4964,7 +5052,8 @@ auto TypeChecker::CheckAndAddImplBindings(
                        *this);
         CARBON_ASSIGN_OR_RETURN(
             Nonnull<const ConstraintType*> iface_constraint,
-            MakeConstraintForInterface(impl_decl->source_loc(), iface_type));
+            ConvertToConstraintType(impl_decl->source_loc(), "impl declaration",
+                                    iface_type));
         CARBON_ASSIGN_OR_RETURN(
             iface_witness, impl_scope.Resolve(iface_constraint, impl_type,
                                               impl_decl->source_loc(), *this));
@@ -4980,6 +5069,9 @@ auto TypeChecker::CheckAndAddImplBindings(
       scope_info.innermost_non_class_scope->Add(
           iface_type, deduced_bindings, impl_type, impl_decl->impl_bindings(),
           self_witness, *this);
+    } else if (isa<NamedConstraintType>(lookup.context)) {
+      // Nothing to check here, since a named constraint can't introduce any
+      // associated entities.
     } else {
       // TODO: Add support for implementing `adapter`s.
       return ProgramError(impl_decl->source_loc())
@@ -5278,6 +5370,7 @@ static auto IsValidTypeForAliasTarget(Nonnull<const Value*> type) -> bool {
 
     case Value::Kind::FunctionType:
     case Value::Kind::InterfaceType:
+    case Value::Kind::NamedConstraintType:
     case Value::Kind::ConstraintType:
     case Value::Kind::TypeType:
     case Value::Kind::TypeOfParameterizedEntityName:
@@ -5334,9 +5427,10 @@ auto TypeChecker::TypeCheckDeclaration(
     **trace_stream_ << "checking " << DeclarationKindName(d->kind()) << "\n";
   }
   switch (d->kind()) {
-    case DeclarationKind::InterfaceDeclaration: {
-      CARBON_RETURN_IF_ERROR(TypeCheckInterfaceDeclaration(
-          &cast<InterfaceDeclaration>(*d), impl_scope));
+    case DeclarationKind::InterfaceDeclaration:
+    case DeclarationKind::ConstraintDeclaration: {
+      CARBON_RETURN_IF_ERROR(TypeCheckConstraintTypeDeclaration(
+          &cast<ConstraintTypeDeclaration>(*d), impl_scope));
       break;
     }
     case DeclarationKind::ImplDeclaration: {
@@ -5391,7 +5485,7 @@ auto TypeChecker::TypeCheckDeclaration(
     case DeclarationKind::InterfaceExtendsDeclaration:
     case DeclarationKind::InterfaceImplDeclaration:
     case DeclarationKind::AssociatedConstantDeclaration: {
-      // Checked in DeclareInterfaceDeclaration.
+      // Checked in DeclareConstraintTypeDeclaration.
       break;
     }
     case DeclarationKind::SelfDeclaration: {
@@ -5409,10 +5503,11 @@ auto TypeChecker::DeclareDeclaration(Nonnull<Declaration*> d,
                                      const ScopeInfo& scope_info)
     -> ErrorOr<Success> {
   switch (d->kind()) {
-    case DeclarationKind::InterfaceDeclaration: {
-      auto& iface_decl = cast<InterfaceDeclaration>(*d);
+    case DeclarationKind::InterfaceDeclaration:
+    case DeclarationKind::ConstraintDeclaration: {
+      auto& iface_decl = cast<ConstraintTypeDeclaration>(*d);
       CARBON_RETURN_IF_ERROR(
-          DeclareInterfaceDeclaration(&iface_decl, scope_info));
+          DeclareConstraintTypeDeclaration(&iface_decl, scope_info));
       break;
     }
     case DeclarationKind::ImplDeclaration: {
@@ -5481,7 +5576,7 @@ auto TypeChecker::DeclareDeclaration(Nonnull<Declaration*> d,
     case DeclarationKind::InterfaceExtendsDeclaration:
     case DeclarationKind::InterfaceImplDeclaration:
     case DeclarationKind::AssociatedConstantDeclaration: {
-      // The semantic effects are handled by DeclareInterfaceDeclaration.
+      // The semantic effects are handled by DeclareConstraintTypeDeclaration.
       break;
     }
 

+ 7 - 13
explorer/interpreter/type_checker.h

@@ -210,9 +210,9 @@ class TypeChecker {
   auto DeclareMixinDeclaration(Nonnull<MixinDeclaration*> mixin_decl,
                                const ScopeInfo& scope_info) -> ErrorOr<Success>;
 
-  auto DeclareInterfaceDeclaration(Nonnull<InterfaceDeclaration*> iface_decl,
-                                   const ScopeInfo& scope_info)
-      -> ErrorOr<Success>;
+  auto DeclareConstraintTypeDeclaration(
+      Nonnull<ConstraintTypeDeclaration*> constraint_decl,
+      const ScopeInfo& scope_info) -> ErrorOr<Success>;
 
   // Check that the deduced parameters of an impl are actually deducible from
   // the form of the interface, for a declaration of the form
@@ -307,10 +307,10 @@ class TypeChecker {
       std::optional<Nonnull<const Declaration*>> enclosing_decl)
       -> ErrorOr<Success>;
 
-  // Type check all the members of the interface.
-  auto TypeCheckInterfaceDeclaration(Nonnull<InterfaceDeclaration*> iface_decl,
-                                     const ImplScope& impl_scope)
-      -> ErrorOr<Success>;
+  // Type check all the members of the interface or named constraint.
+  auto TypeCheckConstraintTypeDeclaration(
+      Nonnull<ConstraintTypeDeclaration*> constraint_decl,
+      const ImplScope& impl_scope) -> ErrorOr<Success>;
 
   // Bring the associated constants in `constraint` that constrain the
   // implementation of `interface` for `self` into `scope`.
@@ -428,12 +428,6 @@ class TypeChecker {
                                BuiltinInterfaceName interface) const
       -> ErrorOr<Nonnull<const InterfaceType*>>;
 
-  // Given an interface type, form a corresponding constraint type. The
-  // interface must be a complete type.
-  auto MakeConstraintForInterface(
-      SourceLocation source_loc, Nonnull<const InterfaceType*> iface_type) const
-      -> ErrorOr<Nonnull<const ConstraintType*>>;
-
   // Convert a value that is expected to represent a constraint into a
   // `ConstraintType`.
   auto ConvertToConstraintType(SourceLocation source_loc,

+ 21 - 4
explorer/interpreter/value.cpp

@@ -423,12 +423,19 @@ void Value::Print(llvm::raw_ostream& out) const {
     case Value::Kind::InterfaceType: {
       const auto& iface_type = cast<InterfaceType>(*this);
       out << "interface ";
-      PrintNameWithBindings(out, &iface_type.declaration(), iface_type.args());
+      PrintNameWithBindings(out, &iface_type.declaration(),
+                            iface_type.bindings().args());
+      break;
+    }
+    case Value::Kind::NamedConstraintType: {
+      const auto& constraint_type = cast<NamedConstraintType>(*this);
+      out << "constraint ";
+      PrintNameWithBindings(out, &constraint_type.declaration(),
+                            constraint_type.bindings().args());
       break;
     }
     case Value::Kind::ConstraintType: {
       const auto& constraint = cast<ConstraintType>(*this);
-      out << "constraint ";
       llvm::ListSeparator combine(" & ");
       for (const LookupContext& ctx : constraint.lookup_contexts()) {
         out << combine << *ctx.context;
@@ -653,14 +660,23 @@ auto TypeEqual(Nonnull<const Value*> t1, Nonnull<const Value*> t2,
       const auto& class1 = cast<NominalClassType>(*t1);
       const auto& class2 = cast<NominalClassType>(*t2);
       return class1.declaration().name() == class2.declaration().name() &&
-             BindingMapEqual(class1.type_args(), class2.type_args(),
+             BindingMapEqual(class1.bindings().args(), class2.bindings().args(),
                              equality_ctx);
     }
     case Value::Kind::InterfaceType: {
       const auto& iface1 = cast<InterfaceType>(*t1);
       const auto& iface2 = cast<InterfaceType>(*t2);
       return iface1.declaration().name() == iface2.declaration().name() &&
-             BindingMapEqual(iface1.args(), iface2.args(), equality_ctx);
+             BindingMapEqual(iface1.bindings().args(), iface2.bindings().args(),
+                             equality_ctx);
+    }
+    case Value::Kind::NamedConstraintType: {
+      const auto& constraint1 = cast<NamedConstraintType>(*t1);
+      const auto& constraint2 = cast<NamedConstraintType>(*t2);
+      return constraint1.declaration().name() ==
+                 constraint2.declaration().name() &&
+             BindingMapEqual(constraint1.bindings().args(),
+                             constraint2.bindings().args(), equality_ctx);
     }
     case Value::Kind::AssociatedConstant:
       // Associated constants are sometimes types.
@@ -869,6 +885,7 @@ auto ValueStructurallyEqual(
     case Value::Kind::NominalClassType:
     case Value::Kind::MixinPseudoType:
     case Value::Kind::InterfaceType:
+    case Value::Kind::NamedConstraintType:
     case Value::Kind::ConstraintType:
     case Value::Kind::ImplWitness:
     case Value::Kind::BindingWitness:

+ 26 - 0
explorer/interpreter/value.h

@@ -64,6 +64,7 @@ class Value {
     TupleType,
     MixinPseudoType,
     InterfaceType,
+    NamedConstraintType,
     ConstraintType,
     ChoiceType,
     ContinuationType,  // The type of a continuation.
@@ -772,6 +773,31 @@ class InterfaceType : public Value {
   Nonnull<const Bindings*> bindings_ = Bindings::None();
 };
 
+// A named constraint type.
+class NamedConstraintType : public Value {
+ public:
+  explicit NamedConstraintType(
+      Nonnull<const ConstraintDeclaration*> declaration,
+      Nonnull<const Bindings*> bindings)
+      : Value(Kind::NamedConstraintType),
+        declaration_(declaration),
+        bindings_(bindings) {}
+
+  static auto classof(const Value* value) -> bool {
+    return value->kind() == Kind::NamedConstraintType;
+  }
+
+  auto declaration() const -> const ConstraintDeclaration& {
+    return *declaration_;
+  }
+
+  auto bindings() const -> const Bindings& { return *bindings_; }
+
+ private:
+  Nonnull<const ConstraintDeclaration*> declaration_;
+  Nonnull<const Bindings*> bindings_ = Bindings::None();
+};
+
 // A constraint that requires implementation of an interface.
 struct ImplConstraint {
   // The type that is required to implement the interface.

+ 2 - 0
explorer/syntax/lexer.lpp

@@ -51,6 +51,7 @@ CLASS                "class"
 COLON                ":"
 COLON_BANG           ":!"
 COMMA                ","
+CONSTRAINT           "constraint"
 CONTINUATION         "__continuation"
 CONTINUATION_TYPE    "__Continuation"
 CONTINUE             "continue"
@@ -157,6 +158,7 @@ operand_start         [(A-Za-z0-9_\"]
 {COLON_BANG}          { return CARBON_SIMPLE_TOKEN(COLON_BANG);          }
 {COLON}               { return CARBON_SIMPLE_TOKEN(COLON);               }
 {COMMA}               { return CARBON_SIMPLE_TOKEN(COMMA);               }
+{CONSTRAINT}          { return CARBON_SIMPLE_TOKEN(CONSTRAINT);          }
 {CONTINUATION_TYPE}   { return CARBON_SIMPLE_TOKEN(CONTINUATION_TYPE);   }
 {CONTINUATION}        { return CARBON_SIMPLE_TOKEN(CONTINUATION);        }
 {CONTINUE}            { return CARBON_SIMPLE_TOKEN(CONTINUE);            }

+ 6 - 0
explorer/syntax/parser.ypp

@@ -219,6 +219,7 @@
   COLON
   COLON_BANG
   COMMA
+  CONSTRAINT
   CONTINUATION
   CONTINUATION_TYPE
   CONTINUE
@@ -1177,6 +1178,11 @@ declaration:
       $$ = arena->New<InterfaceDeclaration>(arena, context.source_loc(), $2, $3,
                                             $5);
     }
+| CONSTRAINT identifier type_params LEFT_CURLY_BRACE interface_body RIGHT_CURLY_BRACE
+    {
+      $$ = arena->New<ConstraintDeclaration>(arena, context.source_loc(), $2,
+                                             $3, $5);
+    }
 | impl_kind IMPL impl_deduced_params impl_type AS type_or_where_expression LEFT_CURLY_BRACE impl_body RIGHT_CURLY_BRACE
     {
       ErrorOr<ImplDeclaration*> impl = ImplDeclaration::Create(

+ 1 - 1
explorer/testdata/constraint/fail_combine_equality.carbon

@@ -13,7 +13,7 @@ impl i32 as I {}
 
 fn F(A:! i32, B:! i32, C:! i32, D:! i32, E:! i32,
      T:! I where A == B and C == D and C == E and B == D) {
-  // CHECK:STDERR: COMPILATION ERROR: {{.*}}/explorer/testdata/constraint/fail_combine_equality.carbon:[[@LINE+1]]: member access, F not in constraint interface I where T is interface I and A == B and C == D and C == E and B == D
+  // CHECK:STDERR: COMPILATION ERROR: {{.*}}/explorer/testdata/constraint/fail_combine_equality.carbon:[[@LINE+1]]: member access, F not in interface I where T is interface I and A == B and C == D and C == E and B == D
   T.F();
 }
 

+ 1 - 1
explorer/testdata/constraint/fail_missing_member.carbon

@@ -11,7 +11,7 @@ package ExplorerTest api;
 interface A { fn F() -> i32; }
 interface B { fn G() -> i32; }
 
-// CHECK:STDERR: COMPILATION ERROR: {{.*}}/explorer/testdata/constraint/fail_missing_member.carbon:[[@LINE+1]]: member access, H not in constraint interface A & interface B where T is interface A and T is interface B
+// CHECK:STDERR: COMPILATION ERROR: {{.*}}/explorer/testdata/constraint/fail_missing_member.carbon:[[@LINE+1]]: member access, H not in interface A & interface B where T is interface A and T is interface B
 fn Get[T:! A & B](n: T) -> i32 { return n.H(); }
 
 impl i32 as A {

+ 2 - 2
explorer/testdata/function/auto_return/fail_multiple_returns.carbon

@@ -8,7 +8,7 @@
 
 package ExplorerTest api;
 
-fn Add(x: i32, y: i32) -> auto {
+fn ComputeSum(x: i32, y: i32) -> auto {
   if (x == 0) {
     return x;
   } else if (y == 0) {
@@ -20,5 +20,5 @@ fn Add(x: i32, y: i32) -> auto {
 }
 
 fn Main() -> i32 {
-  return Add(1, 2) - 3;
+  return ComputeSum(1, 2) - 3;
 }

+ 2 - 2
explorer/testdata/function/auto_return/fail_separate_decl.carbon

@@ -10,8 +10,8 @@ package ExplorerTest api;
 
 // This declaration is not allowed.
 // CHECK:STDERR: COMPILATION ERROR: {{.*}}/explorer/testdata/function/auto_return/fail_separate_decl.carbon:[[@LINE+1]]: Function declaration has deduced return type but no body
-fn Add(x: i32, y: i32) -> auto;
+fn ComputeSum(x: i32, y: i32) -> auto;
 
 fn Main() -> i32 {
-  return Add(1, 2) - 3;
+  return ComputeSum(1, 2) - 3;
 }

+ 1 - 1
explorer/testdata/interface/fail_interface_missing_member.carbon

@@ -13,7 +13,7 @@ interface Vector {
 }
 
 fn ScaleGeneric[T:! Vector](a: T, s: i32) -> T {
-  // CHECK:STDERR: COMPILATION ERROR: {{.*}}/explorer/testdata/interface/fail_interface_missing_member.carbon:[[@LINE+1]]: member access, Scale not in constraint interface Vector where T is interface Vector
+  // CHECK:STDERR: COMPILATION ERROR: {{.*}}/explorer/testdata/interface/fail_interface_missing_member.carbon:[[@LINE+1]]: member access, Scale not in interface Vector where T is interface Vector
   return a.Scale(s);
 }
 

+ 53 - 0
explorer/testdata/named_constraint/extends.carbon

@@ -0,0 +1,53 @@
+// 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
+// RUN: %{explorer-run}
+// RUN: %{explorer-run-trace}
+// CHECK:STDOUT: 5
+// CHECK:STDOUT: 5
+// CHECK:STDOUT: 5
+// CHECK:STDOUT: 5
+// CHECK:STDOUT: 5
+// CHECK:STDOUT: result: 0
+
+package ExplorerTest api;
+
+interface Maker(T:! Type) {
+  fn Make[me: Self]() -> T;
+}
+
+constraint IndirectMaker(T:! Type) {
+  extends Maker(T*);
+}
+
+constraint MoreIndirectMaker {
+  extends IndirectMaker(i32);
+}
+
+class PointerFactory {
+  var p: i32*;
+  external impl as Maker(i32*) {
+    fn Make[me: Self]() -> i32* { return me.p; }
+  }
+}
+
+fn CallIndirect[T:! IndirectMaker(i32)](x: T) -> i32 {
+  return *x.Make();
+}
+
+fn CallMoreIndirect[T:! MoreIndirectMaker](x: T) -> i32 {
+  return *x.Make();
+}
+
+fn Main() -> i32 {
+  var n: i32 = 5;
+  var f: PointerFactory = {.p = &n};
+  Print("{0}", *f.(Maker(i32*).Make)());
+  Print("{0}", *f.(IndirectMaker(i32).Make)());
+  Print("{0}", *f.(MoreIndirectMaker.Make)());
+  Print("{0}", CallIndirect(f));
+  Print("{0}", CallMoreIndirect(f));
+  return 0;
+}

+ 16 - 0
explorer/testdata/named_constraint/fail_associated_constant.carbon

@@ -0,0 +1,16 @@
+// 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
+// RUN: %{not} %{explorer-run}
+// RUN: %{not} %{explorer-run-trace}
+
+package ExplorerTest api;
+
+constraint X {
+  // CHECK:STDERR: COMPILATION ERROR: {{.*}}/explorer/testdata/named_constraint/fail_associated_constant.carbon:[[@LINE+1]]: associated constant not permitted in named constraint
+  let N:! Type;
+}
+
+fn Main() -> i32 { return 0; }

+ 16 - 0
explorer/testdata/named_constraint/fail_associated_function.carbon

@@ -0,0 +1,16 @@
+// 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
+// RUN: %{not} %{explorer-run}
+// RUN: %{not} %{explorer-run-trace}
+
+package ExplorerTest api;
+
+constraint X {
+  // CHECK:STDERR: COMPILATION ERROR: {{.*}}/explorer/testdata/named_constraint/fail_associated_function.carbon:[[@LINE+1]]: associated function not permitted in named constraint
+  fn F();
+}
+
+fn Main() -> i32 { return 0; }

+ 32 - 0
explorer/testdata/named_constraint/fail_compound_member_access.carbon

@@ -0,0 +1,32 @@
+// 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
+// RUN: %{not} %{explorer-run}
+// RUN: %{not} %{explorer-run-trace}
+
+package ExplorerTest api;
+
+interface MyAddWith(T:! Type) {
+  let Result:! Type;
+  fn Op[me: Self](other: T) -> Result;
+}
+
+constraint MyAdd {
+  extends MyAddWith(Self) where .Result = Self;
+}
+
+external impl i32 as MyAdd {
+  fn Op[me: i32](other: i32) -> i32 { return me + other; }
+}
+
+fn Main() -> i32 {
+  let n: i32 = 1;
+  // TODO: This should be valid, but isn't representable in our current
+  // MemberName value. We will likely need MemberName to remember the
+  // constraint as written, not only the interface in which the member was
+  // found.
+  // CHECK:STDERR: COMPILATION ERROR: {{.*}}/explorer/testdata/named_constraint/fail_compound_member_access.carbon:[[@LINE+1]]: could not find implementation of interface MyAddWith(T = Self) for i32
+  return n.(MyAdd.Op)(n);
+}

+ 26 - 0
explorer/testdata/named_constraint/fail_impl_as_member_access.carbon

@@ -0,0 +1,26 @@
+// 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
+// RUN: %{not} %{explorer-run}
+// RUN: %{not} %{explorer-run-trace}
+
+package ExplorerTest api;
+
+interface HasFoo {
+  fn Foo();
+}
+
+constraint ImplAsHasFoo {
+  impl as HasFoo;
+}
+
+fn CallFoo[T:! ImplAsHasFoo](x: T) {
+  // OK, T is HasFoo.
+  x.(HasFoo.Foo)();
+  // CHECK:STDERR: COMPILATION ERROR: {{.*}}/explorer/testdata/named_constraint/fail_impl_as_member_access.carbon:[[@LINE+1]]: member access, Foo not in constraint ImplAsHasFoo where T is interface HasFoo
+  x.Foo();
+}
+
+fn Main() -> i32 { return 0; }

+ 41 - 0
explorer/testdata/named_constraint/impl_as.carbon

@@ -0,0 +1,41 @@
+// 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
+// RUN: %{explorer-run}
+// RUN: %{explorer-run-trace}
+// CHECK:STDOUT: result: 4
+
+package ExplorerTest api;
+
+interface MyHashable {
+  let Result:! Type;
+  fn Hash[me: Self]() -> Result;
+}
+
+constraint HashToIntConvertible {
+  impl as MyHashable;
+  impl Self.(MyHashable.Result) as ImplicitAs(i32);
+}
+
+class MyHashValue {
+  external impl as ImplicitAs(i32) {
+    fn Convert[me: Self]() -> i32 { return 4; }
+  }
+}
+
+class Widget {
+  external impl as MyHashable where .Result = MyHashValue {
+    fn Hash[me: Self]() -> MyHashValue { return {}; }
+  }
+}
+
+fn MakeSmallHash[T:! HashToIntConvertible](x: T) -> i32 {
+  return x.(MyHashable.Hash)();
+}
+
+fn Main() -> i32 {
+  var w: Widget = {};
+  return MakeSmallHash(w);
+}

+ 37 - 0
explorer/testdata/named_constraint/impl_constraint.carbon

@@ -0,0 +1,37 @@
+// 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
+// RUN: %{explorer-run}
+// RUN: %{explorer-run-trace}
+// CHECK:STDOUT: result: 12
+
+package ExplorerTest api;
+
+interface Runnable {
+  fn Run[me: Self]() -> i32;
+}
+
+interface Walkable {
+  fn Walk[me: Self]() -> i32;
+}
+
+constraint Traversible {
+  extends Runnable;
+  extends Walkable;
+}
+
+external impl i32 as Traversible {
+  fn Run[me: i32]() -> i32 {
+    return 10 * me;
+  }
+  fn Walk[me: i32]() -> i32 {
+    return me + 1;
+  }
+}
+
+fn Main() -> i32 {
+  var n: i32 = 1;
+  return n.(Runnable.Run)() + n.(Walkable.Walk)();
+}

+ 5 - 5
explorer/testdata/operators/add.carbon

@@ -5,18 +5,18 @@
 // AUTOUPDATE
 // RUN: %{explorer-run}
 // RUN: %{explorer-run-trace}
-// CHECK:STDOUT: result: 6
+// CHECK:STDOUT: result: 12
 
 package ExplorerTest api;
 
 class A { var n: i32; }
 
-external impl A as AddWith(i32) where .Result = A {
-  fn Op[me: Self](rhs: i32) -> A { return {.n = me.n + rhs}; }
+external impl A as Add {
+  fn Op[me: Self](rhs: A) -> A { return {.n = me.n + rhs.n}; }
 }
 
 fn Main() -> i32 {
   var a: A = {.n = 5};
-  a = a + 1;
-  return a.n;
+  var b: A = {.n = 7};
+  return (a + b).n;
 }

+ 22 - 0
explorer/testdata/operators/add_with.carbon

@@ -0,0 +1,22 @@
+// 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
+// RUN: %{explorer-run}
+// RUN: %{explorer-run-trace}
+// CHECK:STDOUT: result: 6
+
+package ExplorerTest api;
+
+class A { var n: i32; }
+
+external impl A as AddWith(i32) where .Result = A {
+  fn Op[me: Self](rhs: i32) -> A { return {.n = me.n + rhs}; }
+}
+
+fn Main() -> i32 {
+  var a: A = {.n = 5};
+  a = a + 1;
+  return a.n;
+}