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

Support builtin conversions that internally rely on user-defined conversions (#2881)

There are a few implicit conversions that are implemented by an `impl` of `ImplicitAs` that delegates to code in explorer:

- Converting between tuple types
- Converting from tuples of types to `type`
- Converting from tuples of values to an array type
- Converting between struct types
- Converting from a struct type to a class type

These conversions can all rely on performing more conversions for elements or subobjects, but previously those inner conversions could only be performed if they were built into explorer. This change instead uses the full implicit conversion machinery in explorer to perform these conversions, including searching for a user-defined `impl` of `ImplicitAs` when necessary.

For example, this permits a conversion from `{.a: T}` to `{.a: U}`, or from `(T, T)` to `(U, U)`, or from `(T, T)` to `[U; 2]` when there is a user-defined conversion from `T` to `U`.

Depends on #2878
Richard Smith 2 лет назад
Родитель
Сommit
474b1eb24c

+ 10 - 29
explorer/ast/expression.h

@@ -852,7 +852,7 @@ class ValueLiteral : public ConstantValueLiteral {
   }
 };
 
-class IntrinsicExpression : public Expression {
+class IntrinsicExpression : public RewritableMixin<Expression> {
  public:
   enum class Intrinsic {
     Print,
@@ -881,13 +881,13 @@ class IntrinsicExpression : public Expression {
 
   explicit IntrinsicExpression(Intrinsic intrinsic, Nonnull<TupleLiteral*> args,
                                SourceLocation source_loc)
-      : Expression(AstNodeKind::IntrinsicExpression, source_loc),
+      : RewritableMixin(AstNodeKind::IntrinsicExpression, source_loc),
         intrinsic_(intrinsic),
         args_(args) {}
 
   explicit IntrinsicExpression(CloneContext& context,
                                const IntrinsicExpression& other)
-      : Expression(context, other),
+      : RewritableMixin(context, other),
         intrinsic_(other.intrinsic_),
         args_(context.Clone(other.args_)) {}
 
@@ -1121,22 +1121,17 @@ class WhereExpression : public RewritableMixin<Expression> {
 // created by type-checking when a type conversion is found to be necessary but
 // that conversion is implemented directly rather than by an `ImplicitAs`
 // implementation.
-class BuiltinConvertExpression : public Expression {
+class BuiltinConvertExpression : public RewritableMixin<Expression> {
  public:
-  BuiltinConvertExpression(Nonnull<Expression*> source_expression,
-                           Nonnull<const Value*> destination_type)
-      : Expression(AstNodeKind::BuiltinConvertExpression,
-                   source_expression->source_loc()),
-        source_expression_(source_expression) {
-    set_static_type(destination_type);
-    set_expression_category(ExpressionCategory::Value);
-  }
+  BuiltinConvertExpression(Nonnull<Expression*> source_expression)
+      : RewritableMixin(AstNodeKind::BuiltinConvertExpression,
+                        source_expression->source_loc()),
+        source_expression_(source_expression) {}
 
   explicit BuiltinConvertExpression(CloneContext& context,
                                     const BuiltinConvertExpression& other)
-      : Expression(context, other),
-        source_expression_(context.Clone(other.source_expression_)),
-        rewritten_form_(context.Clone(other.rewritten_form_)) {}
+      : RewritableMixin(context, other),
+        source_expression_(context.Clone(other.source_expression_)) {}
 
   static auto classof(const AstNode* node) -> bool {
     return InheritsFromBuiltinConvertExpression(node->kind());
@@ -1149,22 +1144,8 @@ class BuiltinConvertExpression : public Expression {
     return source_expression_;
   }
 
-  // Set the rewritten form of this expression. Can only be called during type
-  // checking.
-  auto set_rewritten_form(Nonnull<const Expression*> rewritten_form) -> void {
-    CARBON_CHECK(!rewritten_form_.has_value()) << "rewritten form set twice";
-    rewritten_form_ = rewritten_form;
-  }
-
-  // Get the rewritten form of this expression. A rewritten form can be used to
-  // prepare the conversion during type checking.
-  auto rewritten_form() const -> std::optional<Nonnull<const Expression*>> {
-    return rewritten_form_;
-  }
-
  private:
   Nonnull<Expression*> source_expression_;
-  std::optional<Nonnull<const Expression*>> rewritten_form_;
 };
 
 // An expression whose semantics have not been implemented. This can be used

+ 27 - 50
explorer/data/prelude.carbon

@@ -34,65 +34,42 @@ __match_first {
   // Pick up implicit conversions that are built into the compiler.
   // TODO: Split these into individual categories and implement as many as we can
   // in the prelude.
-  impl forall [U:! type, template T:! __intrinsic_implicit_as(U)] T as ImplicitAs(U) {
-    fn Convert[self: Self]() -> U { return __intrinsic_implicit_as_convert(self, U); }
+  impl forall [template U:! type, template T:! __intrinsic_implicit_as(U)]
+      T as ImplicitAs(U) {
+    fn Convert[self: Self]() -> U {
+      return __intrinsic_implicit_as_convert(self, U);
+    }
   }
 
   // Every type implicitly converts to single-step-equal types.
   impl forall [T:! type, U:! type where .Self == T] T as ImplicitAs(U) {
     fn Convert[self: Self]() -> U { return __EqualConvert(self, U); }
   }
+}
 
-  // A tuple implicitly converts to another tuple if all its elements do.
-  // TODO: Simplify this once we have variadics.
-  // TODO: Should these be final?
-  impl forall [U1:! type, T1:! ImplicitAs(U1)]
-      (T1,) as ImplicitAs((U1,)) {
-    fn Convert[self: Self]() -> (U1,) {
-      let (v1: T1,) = self;
-      return (v1.Convert(),);
-    }
-  }
-  impl forall [U1:! type, U2:! type, T1:! ImplicitAs(U1), T2:! ImplicitAs(U2)]
-      (T1, T2) as ImplicitAs((U1, U2)) {
-    fn Convert[self: Self]() -> (U1, U2) {
-      let (v1: T1, v2: T2) = self;
-      return (v1.Convert(), v2.Convert());
-    }
-  }
-  impl forall [U1:! type, U2:! type, U3:! type,
-               T1:! ImplicitAs(U1), T2:! ImplicitAs(U2), T3:! ImplicitAs(U3)]
-      (T1, T2, T3) as ImplicitAs((U1, U2, U3)) {
-    fn Convert[self: Self]() -> (U1, U2, U3) {
-      let (v1: T1, v2: T2, v3: T3) = self;
-      return (v1.Convert(), v2.Convert(), v3.Convert());
-    }
-  }
-
-  // A tuple explicitly converts to another tuple if all its elements do. Note
-  // that this fully overlaps with the previous set of impl declarations for
-  // the case where an implicit conversion is possible.
-  impl forall [U1:! type, T1:! As(U1)]
-      (T1,) as As((U1,)) {
-    fn Convert[self: Self]() -> (U1,) {
-      let (v1: T1,) = self;
-      return (v1.Convert(),);
-    }
+// A tuple explicitly converts to another tuple if all its elements do.
+// TODO: Simplify this once we have variadics.
+// TODO: Should these be final?
+impl forall [U1:! type, T1:! As(U1)]
+    (T1,) as As((U1,)) {
+  fn Convert[self: Self]() -> (U1,) {
+    let (v1: T1,) = self;
+    return (v1.Convert(),);
   }
-  impl forall [U1:! type, U2:! type, T1:! As(U1), T2:! As(U2)]
-      (T1, T2) as As((U1, U2)) {
-    fn Convert[self: Self]() -> (U1, U2) {
-      let (v1: T1, v2: T2) = self;
-      return (v1.Convert(), v2.Convert());
-    }
+}
+impl forall [U1:! type, U2:! type, T1:! As(U1), T2:! As(U2)]
+    (T1, T2) as As((U1, U2)) {
+  fn Convert[self: Self]() -> (U1, U2) {
+    let (v1: T1, v2: T2) = self;
+    return (v1.Convert(), v2.Convert());
   }
-  impl forall [U1:! type, U2:! type, U3:! type,
-               T1:! As(U1), T2:! As(U2), T3:! As(U3)]
-      (T1, T2, T3) as As((U1, U2, U3)) {
-    fn Convert[self: Self]() -> (U1, U2, U3) {
-      let (v1: T1, v2: T2, v3: T3) = self;
-      return (v1.Convert(), v2.Convert(), v3.Convert());
-    }
+}
+impl forall [U1:! type, U2:! type, U3:! type,
+             T1:! As(U1), T2:! As(U2), T3:! As(U3)]
+    (T1, T2, T3) as As((U1, U2, U3)) {
+  fn Convert[self: Self]() -> (U1, U2, U3) {
+    let (v1: T1, v2: T2, v3: T3) = self;
+    return (v1.Convert(), v2.Convert(), v3.Convert());
   }
 }
 

+ 33 - 5
explorer/interpreter/interpreter.cpp

@@ -784,7 +784,6 @@ auto Interpreter::Convert(Nonnull<const Value*> value,
     case Value::Kind::ConstraintImplWitness:
     case Value::Kind::ParameterizedEntityName:
     case Value::Kind::ChoiceType:
-    case Value::Kind::VariableType:
     case Value::Kind::BindingPlaceholderValue:
     case Value::Kind::AddrValue:
     case Value::Kind::AlternativeConstructorValue:
@@ -890,6 +889,34 @@ auto Interpreter::Convert(Nonnull<const Value*> value,
       }
       return arena_->New<TupleValue>(std::move(new_elements));
     }
+    case Value::Kind::VariableType: {
+      std::optional<Nonnull<const Value*>> source_type;
+      // While type-checking a `where` expression, we can evaluate a reference
+      // to its self binding before we know its type. In this case, the self
+      // binding is always a type.
+      //
+      // TODO: Add a conversion kind to BuiltinConvertExpression so that we
+      // don't need to look at the types and reconstruct what kind of
+      // conversion is being performed from here.
+      if (cast<VariableType>(value)->binding().is_type_checked()) {
+        CARBON_ASSIGN_OR_RETURN(
+            source_type,
+            InstantiateType(&cast<VariableType>(value)->binding().static_type(),
+                            source_loc));
+      }
+      if (isa<TypeType, ConstraintType, NamedConstraintType, InterfaceType>(
+              destination_type) &&
+          (!source_type ||
+           isa<TypeType, ConstraintType, NamedConstraintType, InterfaceType>(
+               *source_type))) {
+        // No further conversions are required.
+        return value;
+      }
+      // We need to convert this, and we don't know how because we don't have
+      // the value yet.
+      return ProgramError(source_loc)
+             << "value of generic binding " << *value << " is not known";
+    }
     case Value::Kind::AssociatedConstant: {
       CARBON_ASSIGN_OR_RETURN(
           Nonnull<const Value*> value,
@@ -1575,6 +1602,9 @@ auto Interpreter::StepExp() -> ErrorOr<Success> {
     }
     case ExpressionKind::IntrinsicExpression: {
       const auto& intrinsic = cast<IntrinsicExpression>(exp);
+      if (auto rewrite = intrinsic.rewritten_form()) {
+        return todo_.ReplaceWith(std::make_unique<ExpressionAction>(*rewrite));
+      }
       if (act.pos() == 0) {
         return todo_.Spawn(
             std::make_unique<ExpressionAction>(&intrinsic.args()));
@@ -1722,10 +1752,8 @@ auto Interpreter::StepExp() -> ErrorOr<Success> {
           return todo_.FinishAction(result);
         }
         case IntrinsicExpression::Intrinsic::ImplicitAsConvert: {
-          CARBON_CHECK(args.size() == 2);
-          CARBON_ASSIGN_OR_RETURN(Nonnull<const Value*> result,
-                                  Convert(args[0], args[1], exp.source_loc()));
-          return todo_.FinishAction(result);
+          CARBON_FATAL()
+              << "__intrinsic_implicit_as_convert should have been rewritten";
         }
         case IntrinsicExpression::Intrinsic::IntEq: {
           CARBON_CHECK(args.size() == 2);

+ 361 - 160
explorer/interpreter/type_checker.cpp

@@ -506,40 +506,6 @@ static auto FindField(llvm::ArrayRef<NamedValue> fields,
   return *it;
 }
 
-auto TypeChecker::FieldTypesImplicitlyConvertible(
-    llvm::ArrayRef<NamedValue> source_fields,
-    llvm::ArrayRef<NamedValue> destination_fields,
-    const ImplScope& impl_scope) const -> ErrorOr<bool> {
-  // TODO: If default fields are implemented, the
-  // code must be adapted to skip them.
-  // Ensure every field name exists in the destination.
-  for (const auto& dest_field : destination_fields) {
-    if (!FindField(source_fields, dest_field.name)) {
-      return false;
-    }
-  }
-  for (const auto& source_field : source_fields) {
-    std::optional<NamedValue> destination_field =
-        FindField(destination_fields, source_field.name);
-    if (!destination_field.has_value()) {
-      return false;
-    }
-    CARBON_ASSIGN_OR_RETURN(
-        bool convertible,
-        IsImplicitlyConvertible(source_field.value,
-                                destination_field.value().value, impl_scope,
-                                // TODO: We don't have a way to perform
-                                // user-defined conversions of a struct field
-                                // yet, because we can't write a suitable impl
-                                // for ImplicitAs.
-                                /*allow_user_defined_conversions=*/false));
-    if (!convertible) {
-      return false;
-    }
-  }
-  return true;
-}
-
 auto TypeChecker::FieldTypes(const NominalClassType& class_type) const
     -> ErrorOr<std::vector<NamedValue>> {
   std::vector<NamedValue> field_types;
@@ -564,7 +530,7 @@ auto TypeChecker::FieldTypesWithBase(const NominalClassType& class_type) const
     -> ErrorOr<std::vector<NamedValue>> {
   CARBON_ASSIGN_OR_RETURN(auto fields, FieldTypes(class_type));
   if (const auto base_type = class_type.base()) {
-    CARBON_ASSIGN_OR_RETURN(auto base_fields,
+    CARBON_ASSIGN_OR_RETURN(std::vector<NamedValue> base_fields,
                             FieldTypesWithBase(*base_type.value()));
     fields.emplace_back(NamedValue{std::string(NominalClassValue::BaseField),
                                    base_type.value()});
@@ -576,32 +542,91 @@ auto TypeChecker::IsImplicitlyConvertible(
     Nonnull<const Value*> source, Nonnull<const Value*> destination,
     const ImplScope& impl_scope, bool allow_user_defined_conversions) const
     -> ErrorOr<bool> {
-  // Check for an exact match or for an implicit conversion.
-  // TODO: `impl` definitions of `ImplicitAs` should be provided to cover these
-  // conversions.
+  // Check for an exact match to avoid impl lookup in this common case.
   CARBON_CHECK(IsNonDeduceableType(source));
   CARBON_CHECK(IsNonDeduceableType(destination));
   if (IsSameType(source, destination, impl_scope)) {
     return true;
   }
 
+  // If the source is a type, or a type-like tuple, then it might convert to
+  // another type-of-type. This can't be done by `ImplicitAs` because it
+  // depends on the value, not only on the type.
+  //
+  // TODO: We can't tell whether the conversion to this type-of-type would
+  // work, because we don't have the source value, only its type. So we allow
+  // this conversion if the source converts to `type`, even if it won't convert
+  // to the actual destination type. We'll catch any problems when we actually
+  // come to perform the conversion.
+  if (isa<TupleType>(source) && IsTypeOfType(destination)) {
+    return IsBuiltinConversion(source, arena_->New<TypeType>(), impl_scope,
+                               allow_user_defined_conversions);
+  }
+  if (IsTypeOfType(source) && IsTypeOfType(destination)) {
+    return true;
+  }
+
+  // If we're not supposed to look for a user-defined conversion, check for
+  // builtin conversions, which are normally found by impl lookup.
+  if (!allow_user_defined_conversions) {
+    return IsBuiltinConversion(source, destination, impl_scope,
+                               allow_user_defined_conversions);
+  }
+
+  // We didn't find a builtin implicit conversion. Check if a user-defined one
+  // exists.
+  SourceLocation source_loc = SourceLocation::DiagnosticsIgnored();
+  CARBON_ASSIGN_OR_RETURN(
+      Nonnull<const InterfaceType*> iface_type,
+      GetBuiltinInterfaceType(
+          source_loc, BuiltinInterfaceName{Builtin::ImplicitAs, destination}));
+  CARBON_ASSIGN_OR_RETURN(
+      std::optional<Nonnull<const Witness*>> conversion_witness,
+      impl_scope.TryResolve(iface_type, source, source_loc, *this,
+                            /*bindings=*/{}, /*diagnose_missing_impl=*/false));
+  return conversion_witness.has_value();
+}
+
+auto TypeChecker::IsBuiltinConversion(Nonnull<const Value*> source,
+                                      Nonnull<const Value*> destination,
+                                      const ImplScope& impl_scope,
+                                      bool allow_user_defined_conversions) const
+    -> ErrorOr<bool> {
   switch (source->kind()) {
     case Value::Kind::StructType:
       switch (destination->kind()) {
         case Value::Kind::StructType: {
-          CARBON_ASSIGN_OR_RETURN(
-              bool fields_convertible,
-              FieldTypesImplicitlyConvertible(
-                  cast<StructType>(*source).fields(),
-                  cast<StructType>(*destination).fields(), impl_scope));
-          if (fields_convertible) {
-            return true;
+          llvm::ArrayRef<NamedValue> source_fields =
+              cast<StructType>(*source).fields();
+          llvm::ArrayRef<NamedValue> destination_fields =
+              cast<StructType>(*destination).fields();
+          // Ensure every source field exists in the destination type.
+          for (const auto& source_field : source_fields) {
+            if (!FindField(destination_fields, source_field.name)) {
+              return false;
+            }
           }
-          break;
+          // Ensure every destination field is initialized.
+          for (const auto& destination_field : destination_fields) {
+            std::optional<NamedValue> source_field =
+                FindField(source_fields, destination_field.name);
+            if (!source_field.has_value()) {
+              return false;
+            }
+            CARBON_ASSIGN_OR_RETURN(
+                bool convertible,
+                IsImplicitlyConvertible(source_field->value,
+                                        destination_field.value, impl_scope,
+                                        allow_user_defined_conversions));
+            if (!convertible) {
+              return false;
+            }
+          }
+          return true;
         }
         case Value::Kind::NominalClassType: {
           CARBON_ASSIGN_OR_RETURN(
-              auto field_types,
+              std::vector<NamedValue> field_types,
               FieldTypesWithBase(cast<NominalClassType>(*destination)));
           CARBON_ASSIGN_OR_RETURN(
               bool convertible,
@@ -641,7 +666,7 @@ auto TypeChecker::IsImplicitlyConvertible(
                 bool convertible,
                 IsImplicitlyConvertible(
                     source_tuple.elements()[i], destination_tuple.elements()[i],
-                    impl_scope, /*allow_user_defined_conversions=*/false));
+                    impl_scope, allow_user_defined_conversions));
             if (!convertible) {
               all_ok = false;
               break;
@@ -663,7 +688,7 @@ auto TypeChecker::IsImplicitlyConvertible(
                 bool convertible,
                 IsImplicitlyConvertible(
                     source_element, &destination_array.element_type(),
-                    impl_scope, /*allow_user_defined_conversions=*/false));
+                    impl_scope, allow_user_defined_conversions));
             if (!convertible) {
               all_ok = false;
               break;
@@ -674,18 +699,14 @@ auto TypeChecker::IsImplicitlyConvertible(
           }
           break;
         }
-        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.
+        case Value::Kind::TypeType: {
+          // A tuple value converts to `type` if all of its fields do.
           bool all_types = true;
           for (Nonnull<const Value*> source_element : source_tuple.elements()) {
             CARBON_ASSIGN_OR_RETURN(
                 bool convertible,
-                IsImplicitlyConvertible(
-                    source_element, destination, impl_scope,
-                    /*allow_user_defined_conversions=*/false));
+                IsImplicitlyConvertible(source_element, destination, impl_scope,
+                                        allow_user_defined_conversions));
             if (!convertible) {
               all_types = false;
               break;
@@ -701,14 +722,6 @@ auto TypeChecker::IsImplicitlyConvertible(
       }
       break;
     }
-    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
-      // type.
-      return IsTypeOfType(destination);
     case Value::Kind::PointerType: {
       if (destination->kind() != Value::Kind::PointerType) {
         break;
@@ -729,24 +742,13 @@ auto TypeChecker::IsImplicitlyConvertible(
       break;
   }
 
-  // If we're not supposed to look for a user-defined conversion, we're done.
-  if (!allow_user_defined_conversions) {
-    return false;
-  }
-
-  // We didn't find a builtin implicit conversion. Try a user-defined one.
-  SourceLocation source_loc = SourceLocation::DiagnosticsIgnored();
-  ErrorOr<Nonnull<const InterfaceType*>> iface_type = GetBuiltinInterfaceType(
-      source_loc, BuiltinInterfaceName{Builtin::ImplicitAs, destination});
-  // TODO: If the Resolve call fails with a hard error, don't swallow it.
-  return iface_type.ok() &&
-         impl_scope.Resolve(*iface_type, source, source_loc, *this).ok();
+  return false;
 }
 
 auto TypeChecker::BuildSubtypeConversion(Nonnull<Expression*> source,
                                          Nonnull<const PointerType*> src_ptr,
                                          Nonnull<const PointerType*> dest_ptr)
-    -> ErrorOr<Nonnull<const Expression*>> {
+    -> ErrorOr<Nonnull<Expression*>> {
   const auto* src_class = cast<NominalClassType>(&src_ptr->pointee_type());
   const auto* dest_class = cast<NominalClassType>(&dest_ptr->pointee_type());
   const auto dest = dest_class->declaration().name();
@@ -766,6 +768,186 @@ auto TypeChecker::BuildSubtypeConversion(Nonnull<Expression*> source,
   return last_expr;
 }
 
+auto TypeChecker::BuildBuiltinConversion(Nonnull<Expression*> source,
+                                         Nonnull<const Value*> destination,
+                                         const ImplScope& impl_scope)
+    -> ErrorOr<Nonnull<Expression*>> {
+  Nonnull<const Value*> source_type = &source->static_type();
+
+  if (trace_stream_->is_enabled()) {
+    *trace_stream_ << "building builtin conversion from " << *source_type
+                   << " to " << *destination << "\n";
+  }
+
+  // Build a simple conversion that the interpreter can perform directly.
+  auto make_builtin_conversion = [&](Nonnull<Expression*> from) {
+    auto* result = arena_->New<BuiltinConvertExpression>(from);
+    result->set_static_type(destination);
+    result->set_expression_category(ExpressionCategory::Value);
+    return result;
+  };
+
+  // Report that the conversion was not possible. This error should only be
+  // visible if __builtin_implicit_as_convert is called directly.
+  auto conversion_failed = [&] {
+    return ProgramError(source->source_loc())
+           << "no builtin conversion from " << *source_type << " to "
+           << *destination << " is known";
+  };
+
+  // Note that the conversion expression that we build may evaluate `source`
+  // more than once. This is OK because the __builtin_implicit_as_convert
+  // intrinsic is only intended to be called from within the prelude's impl of
+  // ImplicitAs, where `source` has no side effects.
+
+  switch (source_type->kind()) {
+    case Value::Kind::StructType:
+      switch (destination->kind()) {
+        case Value::Kind::StructType: {
+          llvm::ArrayRef<NamedValue> source_fields =
+              cast<StructType>(*source_type).fields();
+          llvm::ArrayRef<NamedValue> destination_fields =
+              cast<StructType>(*destination).fields();
+          // Ensure every source field exists in the destination type.
+          for (const auto& source_field : source_fields) {
+            if (!FindField(destination_fields, source_field.name)) {
+              return conversion_failed();
+            }
+          }
+          // Initialize every destination field.
+          std::vector<FieldInitializer> result_fields;
+          for (const auto& destination_field : destination_fields) {
+            std::optional<NamedValue> source_field =
+                FindField(source_fields, destination_field.name);
+            if (!source_field.has_value()) {
+              return conversion_failed();
+            }
+            auto* elem = arena_->New<SimpleMemberAccessExpression>(
+                source->source_loc(), source, source_field->name);
+            CARBON_RETURN_IF_ERROR(TypeCheckExp(elem, impl_scope));
+            CARBON_ASSIGN_OR_RETURN(
+                Nonnull<Expression*> converted,
+                ImplicitlyConvert("implicit conversion", impl_scope, elem,
+                                  destination_field.value));
+            result_fields.push_back(
+                FieldInitializer(destination_field.name, converted));
+          }
+          auto* result = arena_->New<StructLiteral>(source->source_loc(),
+                                                    std::move(result_fields));
+          CARBON_RETURN_IF_ERROR(TypeCheckExp(result, impl_scope));
+          return result;
+        }
+        case Value::Kind::NominalClassType: {
+          CARBON_ASSIGN_OR_RETURN(
+              std::vector<NamedValue> field_types,
+              FieldTypesWithBase(cast<NominalClassType>(*destination)));
+          CARBON_ASSIGN_OR_RETURN(
+              Nonnull<Expression*> result,
+              ImplicitlyConvert("implicit conversion", impl_scope, source,
+                                arena_->New<StructType>(field_types)));
+          // Perform a builtin conversion from struct to class.
+          return make_builtin_conversion(result);
+        }
+        case Value::Kind::TypeType:
+          // A value of empty struct type implicitly converts to type `type`.
+          if (cast<StructType>(*source_type).fields().empty()) {
+            return make_builtin_conversion(source);
+          }
+          return conversion_failed();
+        default:
+          return conversion_failed();
+      }
+      return conversion_failed();
+    case Value::Kind::TupleType: {
+      const auto& source_tuple = cast<TupleType>(*source_type);
+      switch (destination->kind()) {
+        case Value::Kind::TupleType: {
+          const auto& destination_tuple = cast<TupleType>(*destination);
+          if (source_tuple.elements().size() !=
+              destination_tuple.elements().size()) {
+            return conversion_failed();
+          }
+          std::vector<Nonnull<Expression*>> converted_elements;
+          for (size_t i = 0; i < source_tuple.elements().size(); ++i) {
+            auto* elem = arena_->New<IndexExpression>(
+                source->source_loc(), source,
+                arena_->New<IntLiteral>(source->source_loc(), i));
+            CARBON_RETURN_IF_ERROR(TypeCheckExp(elem, impl_scope));
+            CARBON_ASSIGN_OR_RETURN(
+                Nonnull<Expression*> converted,
+                ImplicitlyConvert("implicit conversion", impl_scope, elem,
+                                  destination_tuple.elements()[i]));
+            converted_elements.push_back(converted);
+          }
+          auto* result = arena_->New<TupleLiteral>(
+              source->source_loc(), std::move(converted_elements));
+          CARBON_RETURN_IF_ERROR(TypeCheckExp(result, impl_scope));
+          return result;
+        }
+        case Value::Kind::StaticArrayType: {
+          const auto& destination_array = cast<StaticArrayType>(*destination);
+          // First, convert each tuple element to the array element type if
+          // necessary.
+          if (!std::all_of(source_tuple.elements().begin(),
+                           source_tuple.elements().end(),
+                           [&](Nonnull<const Value*> element_type) {
+                             return TypeEqual(element_type,
+                                              &destination_array.element_type(),
+                                              std::nullopt);
+                           })) {
+            auto* destination_tuple_type = arena_->New<TupleType>(std::vector(
+                destination_array.size(), &destination_array.element_type()));
+            CARBON_ASSIGN_OR_RETURN(
+                source, BuildBuiltinConversion(source, destination_tuple_type,
+                                               impl_scope));
+          }
+          // Perform a builtin conversion from tuple to array.
+          return make_builtin_conversion(source);
+        }
+        case Value::Kind::TypeType: {
+          // First, convert each tuple element to 'type' if necessary.
+          if (!std::all_of(source_tuple.elements().begin(),
+                           source_tuple.elements().end(),
+                           [](Nonnull<const Value*> element_type) {
+                             return isa<TypeType>(element_type);
+                           })) {
+            auto* destination_tuple_type = arena_->New<TupleType>(
+                std::vector(source_tuple.elements().size(), destination));
+            CARBON_ASSIGN_OR_RETURN(
+                source, BuildBuiltinConversion(source, destination_tuple_type,
+                                               impl_scope));
+          }
+          // Perform a builtin conversion from tuple of types to type.
+          return make_builtin_conversion(source);
+        }
+        default:
+          return conversion_failed();
+      }
+      return conversion_failed();
+    }
+    case Value::Kind::PointerType: {
+      if (destination->kind() != Value::Kind::PointerType) {
+        return conversion_failed();
+      }
+      const auto* src_ptr = cast<PointerType>(source_type);
+      const auto* dest_ptr = cast<PointerType>(destination);
+      if (src_ptr->pointee_type().kind() != Value::Kind::NominalClassType ||
+          dest_ptr->pointee_type().kind() != Value::Kind::NominalClassType) {
+        return conversion_failed();
+      }
+      const auto& src_class = cast<NominalClassType>(src_ptr->pointee_type());
+      if (src_class.InheritsClass(&dest_ptr->pointee_type())) {
+        return BuildSubtypeConversion(source, src_ptr, dest_ptr);
+      }
+      return conversion_failed();
+    }
+    default:
+      return conversion_failed();
+  }
+
+  CARBON_FATAL() << "unreachable";
+}
+
 auto TypeChecker::ImplicitlyConvert(std::string_view context,
                                     const ImplScope& impl_scope,
                                     Nonnull<Expression*> source,
@@ -774,79 +956,106 @@ auto TypeChecker::ImplicitlyConvert(std::string_view context,
   Nonnull<const Value*> source_type = &source->static_type();
 
   CARBON_RETURN_IF_ERROR(
-      ExpectNonPlaceholderType(source->source_loc(), &source->static_type()));
+      ExpectNonPlaceholderType(source->source_loc(), source_type));
 
-  if (TypeEqual(&source->static_type(), destination, std::nullopt)) {
+  if (TypeEqual(source_type, destination, std::nullopt)) {
     // No conversions are required.
     return source;
   }
 
-  // TODO: This doesn't work for cases of combined built-in and user-defined
-  // conversion, such as converting a struct element via an `ImplicitAs` impl.
-  CARBON_ASSIGN_OR_RETURN(
-      bool convertible,
-      IsImplicitlyConvertible(source_type, destination, impl_scope,
-                              /*allow_user_defined_conversions=*/false));
-  if (convertible) {
-    // A type only implicitly converts to a constraint if there is an impl of
-    // that constraint for that type in scope.
+  // Conversion from a tuple of types to the type `type` is used in the prelude
+  // before the intrinsic impl of `ImplicitAs` is declared. We also need to do
+  // this as a prerequisite to the conversion of tuples to constrained types
+  // below.
+  if (isa<TupleType>(source_type) && IsTypeOfType(destination)) {
+    auto* type_type = arena_->New<TypeType>();
+    CARBON_ASSIGN_OR_RETURN(
+        bool convertible,
+        IsBuiltinConversion(source_type, type_type, impl_scope,
+                            /*allow_user_defined_conversions=*/true));
+    if (convertible) {
+      CARBON_ASSIGN_OR_RETURN(
+          source, BuildBuiltinConversion(source, type_type, impl_scope));
+      source_type = &source->static_type();
+    }
+  }
+
+  // A type of type can be converted to another type of type if the value of
+  // the former satisfies the constraints of the latter. This conversion
+  // depends on the value, not only the type, so isn't supported by
+  // `ImplicitAs`.
+  if (IsTypeOfType(source_type) && IsTypeOfType(destination)) {
+    // Don't require the source value to be constant if the destination is
+    // `type`.
     // 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,
-                                                arena_->New<TypeType>()));
-      CARBON_ASSIGN_OR_RETURN(Nonnull<const Value*> converted_value,
-                              InterpExp(source_as_type));
-      CARBON_ASSIGN_OR_RETURN(
-          Nonnull<const ConstraintType*> destination_constraint,
-          ConvertToConstraintType(source->source_loc(), "implicit conversion",
-                                  destination));
-      destination = destination_constraint;
-      if (trace_stream_->is_enabled()) {
-        *trace_stream_ << "converting type " << *converted_value
-                       << " to constraint " << *destination_constraint
-                       << " for " << context << " in scope " << impl_scope
-                       << "\n";
-      }
-      // Note, we discard the witness. We don't actually need it in order to
-      // perform the conversion, but we do want to know it exists.
-      // TODO: A value of constraint type should carry both the type and the
-      // witness.
-      CARBON_RETURN_IF_ERROR(impl_scope.Resolve(destination_constraint,
-                                                converted_value,
-                                                source->source_loc(), *this));
-      return arena_->New<ValueLiteral>(source->source_loc(), converted_value,
-                                       destination_constraint,
-                                       ExpressionCategory::Value);
-    }
-
-    if (IsTypeOfType(source_type) && IsTypeOfType(destination)) {
-      // No conversion is required.
+    // be required to be constant. That case should also be supported by
+    // `ImplicitAs`.
+    if (isa<TypeType>(destination)) {
       return source;
     }
 
-    // Perform the builtin conversion.
-    auto* convert_expr =
-        arena_->New<BuiltinConvertExpression>(source, destination);
-
-    // For subtyping, rewrite into successive `.base` accesses.
-    if (isa<PointerType>(source_type) && isa<PointerType>(destination) &&
-        cast<PointerType>(destination)->pointee_type().kind() ==
-            Value::Kind::NominalClassType) {
-      CARBON_ASSIGN_OR_RETURN(
-          const auto* rewrite,
-          BuildSubtypeConversion(source, cast<PointerType>(source_type),
-                                 cast<PointerType>(destination)))
-      convert_expr->set_rewritten_form(rewrite);
+    // First convert the source expression to type `type`.
+    CARBON_ASSIGN_OR_RETURN(Nonnull<Expression*> source_as_type,
+                            ImplicitlyConvert(context, impl_scope, source,
+                                              arena_->New<TypeType>()));
+    CARBON_ASSIGN_OR_RETURN(Nonnull<const Value*> converted_value,
+                            InterpExp(source_as_type));
+    CARBON_ASSIGN_OR_RETURN(
+        Nonnull<const ConstraintType*> destination_constraint,
+        ConvertToConstraintType(source->source_loc(), "implicit conversion",
+                                destination));
+    destination = destination_constraint;
+    if (trace_stream_->is_enabled()) {
+      *trace_stream_ << "converting type " << *converted_value
+                     << " to constraint " << *destination_constraint << " for "
+                     << context << " in scope " << impl_scope << "\n";
+    }
+    // Note, we discard the witness. We don't actually need it in order to
+    // perform the conversion, but we do want to know it exists.
+    // TODO: A value of constraint type should carry both the type and the
+    // witness.
+    CARBON_RETURN_IF_ERROR(impl_scope.Resolve(
+        destination_constraint, converted_value, source->source_loc(), *this));
+    return arena_->New<ValueLiteral>(source->source_loc(), converted_value,
+                                     destination_constraint,
+                                     ExpressionCategory::Value);
+  }
+
+  // Conversion from a tuple literal to a tuple type converts each element in
+  // turn, rather than converting the tuple as a whole. This is important in
+  // order to evaluate arguments to a function call in a reasonable order, and
+  // this conversion needs to be built-in because we use it while type-checking
+  // the prelude.
+  if (auto* source_tuple = dyn_cast<TupleLiteral>(source)) {
+    if (auto* destination_tuple = dyn_cast<TupleType>(destination)) {
+      if (source_tuple->fields().size() !=
+          destination_tuple->elements().size()) {
+        return ProgramError(source->source_loc())
+               << "type error in " << context << ": `" << *source_type << "`"
+               << " is not implicitly convertible to tuple type "
+               << "`" << *destination << "` of different length";
+      }
+      std::vector<Nonnull<Expression*>> converted_elements;
+      for (size_t i = 0; i < source_tuple->fields().size(); ++i) {
+        CARBON_ASSIGN_OR_RETURN(
+            Nonnull<Expression*> converted,
+            ImplicitlyConvert("implicit conversion", impl_scope,
+                              source_tuple->fields()[i],
+                              destination_tuple->elements()[i]));
+        converted_elements.push_back(converted);
+      }
+      auto* result = arena_->New<TupleLiteral>(source->source_loc(),
+                                               std::move(converted_elements));
+      // TODO: Should be ExpressionCategory::Initializing.
+      result->set_expression_category(ExpressionCategory::Value);
+      result->set_static_type(destination);
+      return result;
     }
-
-    return convert_expr;
   }
 
+  // Build a call to the conversion function.
   ErrorOr<Nonnull<Expression*>> converted = BuildBuiltinMethodCall(
       impl_scope, source,
       BuiltinInterfaceName{Builtin::ImplicitAs, destination},
@@ -869,9 +1078,15 @@ auto TypeChecker::IsIntrinsicConstraintSatisfied(
     case IntrinsicConstraint::ImplicitAs:
       CARBON_CHECK(constraint.arguments.size() == 1)
           << "wrong number of arguments for `__intrinsic_implicit_as`";
-      return IsImplicitlyConvertible(constraint.type, constraint.arguments[0],
-                                     impl_scope,
-                                     /*allow_user_defined_conversions=*/false);
+      CARBON_ASSIGN_OR_RETURN(
+          bool convertible,
+          IsBuiltinConversion(constraint.type, constraint.arguments[0],
+                              impl_scope,
+                              /*allow_user_defined_conversions=*/true));
+      if (trace_stream_->is_enabled()) {
+        *trace_stream_ << constraint << " evaluated to " << convertible << "\n";
+      }
+      return convertible;
   }
 }
 
@@ -1346,20 +1561,6 @@ auto TypeChecker::ArgumentDeduction::Finish(
         type_checker.Substitute(bindings, &binding->static_type()));
     const auto* first_value = values[0];
     for (const auto* value : values) {
-      // TODO: It's not clear that conversions are or should be possible here.
-      // If they are permitted, we should allow user-defined conversions, and
-      // actually perform the conversion.
-      if (!IsTypeOfType(binding_type)) {
-        CARBON_ASSIGN_OR_RETURN(bool convertible,
-                                type_checker.IsImplicitlyConvertible(
-                                    value, binding_type, impl_scope, false));
-        if (!convertible) {
-          return ProgramError(source_loc_)
-                 << "cannot convert deduced value " << *value << " for "
-                 << binding->name() << " to parameter type " << *binding_type;
-        }
-      }
-
       // All deductions are required to produce the same value. Note that we
       // intentionally don't consider equality constraints here; we need the
       // same symbolic type, otherwise it would be ambiguous which spelling
@@ -3745,10 +3946,10 @@ auto TypeChecker::TypeCheckExpImpl(Nonnull<Expression*> e,
           }
           CARBON_ASSIGN_OR_RETURN(Nonnull<const Value*> result,
                                   TypeCheckTypeExp(args[1], impl_scope));
-          // TODO: Check that the type of args[0] implicitly converts to
-          // args[1].
-          e->set_static_type(result);
-          e->set_expression_category(ExpressionCategory::Value);
+          CARBON_ASSIGN_OR_RETURN(
+              Nonnull<Expression*> converted,
+              BuildBuiltinConversion(args[0], result, impl_scope));
+          cast<IntrinsicExpression>(e)->set_rewritten_form(converted);
           return Success();
         }
         case IntrinsicExpression::Intrinsic::IntEq: {
@@ -5707,7 +5908,7 @@ auto TypeChecker::DeclareImplDeclaration(Nonnull<ImplDeclaration*> impl_decl,
   // processing the interface, in case the interface expression uses `Self`.
   Nonnull<SelfDeclaration*> self = impl_decl->self();
   self->set_constant_value(impl_type_value);
-  self->set_static_type(&impl_decl->impl_type()->static_type());
+  self->set_static_type(arena_->New<TypeType>());
 
   // Check and interpret the interface.
   CARBON_ASSIGN_OR_RETURN(
@@ -5724,7 +5925,7 @@ auto TypeChecker::DeclareImplDeclaration(Nonnull<ImplDeclaration*> impl_decl,
   {
     // TODO: Combine this with the SelfDeclaration.
     auto* self_binding = arena_->New<GenericBinding>(
-        self->source_loc(), "Self", impl_decl->impl_type(),
+        self->source_loc(), "Self", &impl_decl->interface(),
         GenericBinding::BindingKind::Checked);
     self_binding->set_symbolic_identity(impl_type_value);
     self_binding->set_value(impl_type_value);

+ 16 - 1
explorer/interpreter/type_checker.h

@@ -425,6 +425,14 @@ class TypeChecker {
                                bool allow_user_defined_conversions) const
       -> ErrorOr<bool>;
 
+  // Returns true if the conversion from `source` to `destination` is a builtin
+  // conversion that `BuildBuiltinConversion` can perform.
+  auto IsBuiltinConversion(Nonnull<const Value*> source,
+                           Nonnull<const Value*> destination,
+                           const ImplScope& impl_scope,
+                           bool allow_user_defined_conversions) const
+      -> ErrorOr<bool>;
+
   // Attempt to implicitly convert type-checked expression `source` to the type
   // `destination`.
   auto ImplicitlyConvert(std::string_view context, const ImplScope& impl_scope,
@@ -441,7 +449,14 @@ class TypeChecker {
   auto BuildSubtypeConversion(Nonnull<Expression*> source,
                               Nonnull<const PointerType*> src_ptr,
                               Nonnull<const PointerType*> dest_ptr)
-      -> ErrorOr<Nonnull<const Expression*>>;
+      -> ErrorOr<Nonnull<Expression*>>;
+
+  // Build a builtin conversion of `source` to the type `destination`, if
+  // possible.
+  auto BuildBuiltinConversion(Nonnull<Expression*> source,
+                              Nonnull<const Value*> destination,
+                              const ImplScope& impl_scope)
+      -> ErrorOr<Nonnull<Expression*>>;
 
   // Determine whether `type1` and `type2` are considered to be the same type
   // in the given scope. This is true if they're structurally identical or if

+ 33 - 0
explorer/testdata/array/conversion.carbon

@@ -0,0 +1,33 @@
+// 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
+// CHECK:STDOUT: 1: 1
+// CHECK:STDOUT: 2: 2
+// CHECK:STDOUT: 3: 3
+// CHECK:STDOUT: 4: 4
+// CHECK:STDOUT: result: 0
+
+package ExplorerTest api;
+
+class A {
+  var n: i32;
+  impl as ImplicitAs(i32) {
+    fn Convert[self: Self]() -> i32 { return self.n; }
+  }
+}
+impl i32 as ImplicitAs(A) {
+  fn Convert[self: Self]() -> A { return {.n = self}; }
+}
+
+fn Main() -> i32 {
+  var arr1: [i32; 2] = (1, 2 as A);
+  Print("1: {0}", arr1[0]);
+  Print("2: {0}", arr1[1]);
+
+  var arr2: [A; 2] = (3, 4 as A);
+  Print("3: {0}", arr2[0].n);
+  Print("4: {0}", arr2[1].n);
+  return 0;
+}

+ 2 - 2
explorer/testdata/as/intrinsic_convert_non_value.carbon → explorer/testdata/as/fail_intrinsic_convert_non_value.carbon

@@ -3,14 +3,14 @@
 // SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
 //
 // AUTOUPDATE
-// CHECK:STDOUT: result: 0
 
 package ExplorerTest api;
 
 fn F() {}
 
 fn Main() -> i32 {
-  // This should probably fail, but at least validate that it doesn't crash.
+  // Validate that this doesn't crash.
+  // CHECK:STDERR: COMPILATION ERROR: fail_intrinsic_convert_non_value.carbon:[[@LINE+1]]: no builtin conversion from fn () -> () to i32 is known
   __intrinsic_implicit_as_convert(F, i32);
   return 0;
 }

+ 51 - 0
explorer/testdata/class/conversion.carbon

@@ -0,0 +1,51 @@
+// 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
+// CHECK:STDOUT: 1: 1
+// CHECK:STDOUT: 2: 2
+// CHECK:STDOUT: 3: 3
+// CHECK:STDOUT: 4: 4
+// CHECK:STDOUT: 5: 5
+// CHECK:STDOUT: 6: 6
+// CHECK:STDOUT: 7: 7
+// CHECK:STDOUT: result: 0
+
+package ExplorerTest api;
+
+class A {
+  var n: i32;
+  impl as ImplicitAs(i32) {
+    fn Convert[self: Self]() -> i32 { return self.n; }
+  }
+}
+
+impl i32 as ImplicitAs(A) {
+  fn Convert[self: Self]() -> A { return {.n = self}; }
+}
+
+base class B {
+  var a: A;
+  var b: i32;
+}
+
+class C extends B {
+  var c: i32;
+}
+
+fn Main() -> i32 {
+  var x: B = {.a = 1, .b = 2 as A};
+  Print("1: {0}", x.a.n);
+  Print("2: {0}", x.b);
+
+  x = {.a = 3, .b = 4 as A};
+  Print("3: {0}", x.a.n);
+  Print("4: {0}", x.b);
+
+  var y: C = {.base = {.a = 5, .b = 6 as A}, .c = 7 as A};
+  Print("5: {0}", y.a.n);
+  Print("6: {0}", y.b);
+  Print("7: {0}", y.c);
+  return 0;
+}

+ 1 - 1
explorer/testdata/generic_function/fail_wrong_variable_substituation.carbon

@@ -9,11 +9,11 @@ package Foo api;
 class X{
 }
 
+// CHECK:STDERR: COMPILATION ERROR: fail_wrong_variable_substituation.carbon:[[@LINE+1]]: value of generic binding T is not known
 fn Get[T:! ()](n: T) -> i32 {  return 2; }
 
 fn Main() -> i32 {
   var x : X = {};
-  // CHECK:STDERR: COMPILATION ERROR: fail_wrong_variable_substituation.carbon:[[@LINE+1]]: cannot convert deduced value class X for T to parameter type ()
   Get(x);
   return 0;
 }

+ 34 - 0
explorer/testdata/struct/conversion.carbon

@@ -0,0 +1,34 @@
+// 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
+// CHECK:STDOUT: 1
+// CHECK:STDOUT: 2
+// CHECK:STDOUT: 3
+// CHECK:STDOUT: 4
+// CHECK:STDOUT: result: 0
+
+package ExplorerTest api;
+
+class A {
+  var n: i32;
+  impl as ImplicitAs(i32) {
+    fn Convert[self: Self]() -> i32 { return self.n; }
+  }
+}
+impl i32 as ImplicitAs(A) {
+  fn Convert[self: Self]() -> A { return {.n = self}; }
+}
+
+fn Main() -> i32 {
+  var x: {.a: A, .b: i32} = {.a = 1, .b = 2 as A};
+  Print("{0}", x.a.n);
+  Print("{0}", x.b);
+
+  var y: {.a: i32, .b: A} = {.a = 3, .b = 4 as A};
+  x = y;
+  Print("{0}", x.a.n);
+  Print("{0}", x.b);
+  return 0;
+}

+ 1 - 1
explorer/testdata/tuple/fail_type_tuple_as_type.carbon

@@ -6,8 +6,8 @@
 
 package ExplorerTest api;
 
+// CHECK:STDERR: COMPILATION ERROR: fail_type_tuple_as_type.carbon:[[@LINE+1]]: value of generic binding T is not known
 fn F[T:! ((), ())](x: T) -> () {
-  // CHECK:STDERR: COMPILATION ERROR: fail_type_tuple_as_type.carbon:[[@LINE+1]]: only arrays and tuples can be indexed, found T
   return x[0];
 }
 

+ 25 - 0
explorer/testdata/tuple/indirect_to_type.carbon

@@ -0,0 +1,25 @@
+// 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
+// CHECK:STDOUT: 1: 1
+// CHECK:STDOUT: 2: 2
+// CHECK:STDOUT: result: 0
+
+package ExplorerTest api;
+
+class A {
+  impl as ImplicitAs(type) {
+    fn Convert[self: Self]() -> type { return i32; }
+  }
+}
+
+fn MakePair() -> (A, A) { return ({}, {}); }
+
+fn Main() -> i32 {
+  let b: MakePair() = (1, 2);
+  Print("1: {0}", b[0]);
+  Print("2: {0}", b[1]);
+  return 0;
+}