Files
carbon-lang/explorer/interpreter/impl_scope.cpp
T
Richard SmithandJon Ross-Perkins 28946d4b87 Order impl matching by type structure (#2691)
As described in [the generics design](https://github.com/carbon-language/carbon-lang/blob/trunk/docs/design/generics/details.md#type-structure-of-an-impl-declaration), `impl` declarations are prioritized by type structure. Given two `impl` declarations that match a `type as interface` query, the one that describes the longest prefix of the query without using placeholders is preferred.

We implement this by putting all impls in a total order, first by type structure equivalence classes and then by lexical order. When matching an impl, we walk this total order, and stop once we find a match and reach the end of its equivalence class.

Equivalence classes are determined by finding the locations of the "holes" (the positions where deduced parameters appear) within the type structure, viewed as a tree. Two impls are in the same equivalence class if their holes are in the same place, and equivalence classes are ordered based on a reverse lexicographical ordering of their holes.

Explorer doesn't keep the `Bindings` list for a parameterized type in any particular order, but the type structure rule requires that we consider them in lexical order. In order to support this, we now track an index on the declared parameters of each generic. This is a simple numbering of enclosing generic parameters, both on that generic and on all lexically enclosing generics.

Co-authored-by: Jon Ross-Perkins <jperkins@google.com>
2023-03-17 16:50:06 -07:00

434 lines
17 KiB
C++

// Part of the Carbon Language project, under the Apache License v2.0 with LLVM
// Exceptions. See /LICENSE for license information.
// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
#include "explorer/interpreter/impl_scope.h"
#include "explorer/ast/value.h"
#include "explorer/interpreter/type_checker.h"
#include "llvm/ADT/StringExtras.h"
#include "llvm/Support/Casting.h"
using llvm::cast;
using llvm::dyn_cast;
using llvm::isa;
namespace Carbon {
void ImplScope::Add(Nonnull<const Value*> iface, Nonnull<const Value*> type,
Nonnull<const Witness*> witness,
const TypeChecker& type_checker) {
Add(iface, {}, type, {}, witness, type_checker);
}
void ImplScope::Add(Nonnull<const Value*> iface,
llvm::ArrayRef<Nonnull<const GenericBinding*>> deduced,
Nonnull<const Value*> type,
llvm::ArrayRef<Nonnull<const ImplBinding*>> impl_bindings,
Nonnull<const Witness*> witness,
const TypeChecker& type_checker,
std::optional<TypeStructureSortKey> sort_key) {
if (const auto* constraint = dyn_cast<ConstraintType>(iface)) {
CARBON_CHECK(!sort_key)
<< "should only be given a sort key for an impl of an interface";
// The caller should have substituted `.Self` for `type` already.
Add(constraint->impl_constraints(), deduced, impl_bindings, witness,
type_checker);
// A parameterized impl declaration doesn't contribute any equality
// constraints to the scope. Instead, we'll resolve the equality
// constraints by resolving a witness when needed.
if (deduced.empty()) {
for (const auto& equality_constraint :
constraint->equality_constraints()) {
equalities_.push_back(&equality_constraint);
}
}
return;
}
Impl new_impl = {.interface = cast<InterfaceType>(iface),
.deduced = deduced,
.type = type,
.impl_bindings = impl_bindings,
.witness = witness,
.sort_key = std::move(sort_key)};
// Find the first impl that's more specific than this one, and place this
// impl right before it. This keeps the impls with the same type structure
// sorted in lexical order, which is important for `match_first` semantics.
auto insert_pos = std::upper_bound(
impls_.begin(), impls_.end(), new_impl,
[](const Impl& a, const Impl& b) { return a.sort_key < b.sort_key; });
impls_.insert(insert_pos, std::move(new_impl));
}
void ImplScope::Add(llvm::ArrayRef<ImplConstraint> impls,
llvm::ArrayRef<Nonnull<const GenericBinding*>> deduced,
llvm::ArrayRef<Nonnull<const ImplBinding*>> impl_bindings,
Nonnull<const Witness*> witness,
const TypeChecker& type_checker) {
for (size_t i = 0; i != impls.size(); ++i) {
ImplConstraint impl = impls[i];
Add(impl.interface, deduced, impl.type, impl_bindings,
type_checker.MakeConstraintWitnessAccess(witness, i), type_checker);
}
}
// Diagnose that `a_evaluated != b_evaluated` for the purpose of an equality
// constraint.
static auto DiagnoseUnequalValues(SourceLocation source_loc,
Nonnull<const Value*> a_written,
Nonnull<const Value*> a_evaluated,
Nonnull<const Value*> b_written,
Nonnull<const Value*> b_evaluated,
Nonnull<const EqualityContext*> equality_ctx)
-> Error {
CARBON_CHECK(!ValueEqual(a_evaluated, b_evaluated, equality_ctx))
<< "expected unequal values";
auto error = ProgramError(source_loc);
error << "constraint requires that " << *a_written;
if (!ValueEqual(a_written, a_evaluated, std::nullopt)) {
error << " (with value " << *a_evaluated << ")";
}
error << " == " << *b_written;
if (!ValueEqual(b_written, b_evaluated, std::nullopt)) {
error << " (with value " << *b_evaluated << ")";
}
error << ", which is not known to be true";
return std::move(error);
}
auto ImplScope::Resolve(Nonnull<const Value*> constraint_type,
Nonnull<const Value*> impl_type,
SourceLocation source_loc,
const TypeChecker& type_checker,
const Bindings& bindings) const
-> ErrorOr<Nonnull<const Witness*>> {
CARBON_ASSIGN_OR_RETURN(
std::optional<Nonnull<const Witness*>> witness,
TryResolve(constraint_type, impl_type, source_loc, type_checker, bindings,
/*diagnose_missing_impl=*/true));
CARBON_CHECK(witness) << "should have diagnosed missing impl";
return *witness;
}
auto ImplScope::TryResolve(Nonnull<const Value*> constraint_type,
Nonnull<const Value*> impl_type,
SourceLocation source_loc,
const TypeChecker& type_checker,
const Bindings& bindings,
bool diagnose_missing_impl) const
-> ErrorOr<std::optional<Nonnull<const Witness*>>> {
if (const auto* iface_type = dyn_cast<InterfaceType>(constraint_type)) {
iface_type =
cast<InterfaceType>(type_checker.Substitute(bindings, iface_type));
return TryResolveInterface(iface_type, impl_type, source_loc, type_checker,
diagnose_missing_impl);
}
if (const auto* constraint = dyn_cast<ConstraintType>(constraint_type)) {
std::vector<Nonnull<const Witness*>> witnesses;
for (auto impl : constraint->impl_constraints()) {
// Note that later impl constraints can refer to earlier impl constraints
// via impl bindings. For example, in
// `C where .Self.AssocType is D`,
// ... the `.Self.AssocType is D` constraint refers to the `.Self is C`
// constraint when naming `AssocType`. So incrementally build up a
// partial constraint witness as we go.
std::optional<Nonnull<const Witness*>> witness;
if (constraint->self_binding()->impl_binding()) {
// Note, this is a partial impl binding covering only the impl
// constraints that we've already seen. Earlier impl constraints should
// not be able to refer to impl bindings for later impl constraints.
witness = type_checker.MakeConstraintWitness(witnesses);
}
Bindings local_bindings = bindings;
local_bindings.Add(constraint->self_binding(), impl_type, witness);
CARBON_ASSIGN_OR_RETURN(
std::optional<Nonnull<const Witness*>> result,
TryResolveInterface(
cast<InterfaceType>(
type_checker.Substitute(local_bindings, impl.interface)),
type_checker.Substitute(local_bindings, impl.type), source_loc,
type_checker, diagnose_missing_impl));
if (!result) {
return {std::nullopt};
}
witnesses.push_back(*result);
}
// Check that all intrinsic, equality, and rewrite constraints
// are satisfied in this scope.
llvm::ArrayRef<IntrinsicConstraint> intrinsics =
constraint->intrinsic_constraints();
llvm::ArrayRef<EqualityConstraint> equals =
constraint->equality_constraints();
llvm::ArrayRef<RewriteConstraint> rewrites =
constraint->rewrite_constraints();
if (!intrinsics.empty() || !equals.empty() || !rewrites.empty()) {
std::optional<Nonnull<const Witness*>> witness;
if (constraint->self_binding()->impl_binding()) {
witness = type_checker.MakeConstraintWitness(witnesses);
}
Bindings local_bindings = bindings;
local_bindings.Add(constraint->self_binding(), impl_type, witness);
SingleStepEqualityContext equality_ctx(this);
for (const auto& intrinsic : intrinsics) {
IntrinsicConstraint converted = {
.type = type_checker.Substitute(local_bindings, intrinsic.type),
.kind = intrinsic.kind,
.arguments = {}};
converted.arguments.reserve(intrinsic.arguments.size());
for (Nonnull<const Value*> argument : intrinsic.arguments) {
converted.arguments.push_back(
type_checker.Substitute(local_bindings, argument));
}
if (!type_checker.IsIntrinsicConstraintSatisfied(converted, *this)) {
if (!diagnose_missing_impl) {
return {std::nullopt};
}
return ProgramError(source_loc)
<< "constraint requires that " << converted;
}
}
for (const auto& equal : equals) {
auto it = equal.values.begin();
Nonnull<const Value*> first =
type_checker.Substitute(local_bindings, *it++);
for (; it != equal.values.end(); ++it) {
Nonnull<const Value*> current =
type_checker.Substitute(local_bindings, *it);
if (!ValueEqual(first, current, &equality_ctx)) {
if (!diagnose_missing_impl) {
return {std::nullopt};
}
return DiagnoseUnequalValues(source_loc, equal.values.front(),
first, *it, current, &equality_ctx);
}
}
}
for (const auto& rewrite : rewrites) {
Nonnull<const Value*> constant =
type_checker.Substitute(local_bindings, rewrite.constant);
Nonnull<const Value*> value = type_checker.Substitute(
local_bindings, rewrite.converted_replacement);
if (!ValueEqual(constant, value, &equality_ctx)) {
if (!diagnose_missing_impl) {
return {std::nullopt};
}
return DiagnoseUnequalValues(source_loc, rewrite.constant, constant,
rewrite.converted_replacement, value,
&equality_ctx);
}
}
}
return {type_checker.MakeConstraintWitness(std::move(witnesses))};
}
CARBON_FATAL() << "expected a constraint, not " << *constraint_type;
}
auto ImplScope::VisitEqualValues(
Nonnull<const Value*> value,
llvm::function_ref<bool(Nonnull<const Value*>)> visitor) const -> bool {
for (Nonnull<const EqualityConstraint*> eq : equalities_) {
if (!eq->VisitEqualValues(value, visitor)) {
return false;
}
}
if (parent_scope_ && !(*parent_scope_)->VisitEqualValues(value, visitor)) {
return false;
}
return true;
}
auto ImplScope::TryResolveInterface(Nonnull<const InterfaceType*> iface_type,
Nonnull<const Value*> type,
SourceLocation source_loc,
const TypeChecker& type_checker,
bool diagnose_missing_impl) const
-> ErrorOr<std::optional<Nonnull<const Witness*>>> {
CARBON_ASSIGN_OR_RETURN(
std::optional<ResolveResult> result,
TryResolveInterfaceRecursively(iface_type, type, source_loc, *this,
type_checker));
if (!result.has_value() && diagnose_missing_impl) {
return ProgramError(source_loc) << "could not find implementation of "
<< *iface_type << " for " << *type;
}
return result ? std::optional(result->witness) : std::nullopt;
}
// Do these two witnesses refer to `impl` declarations in the same
// `match_first` block?
static auto InSameMatchFirst(Nonnull<const Witness*> a,
Nonnull<const Witness*> b) -> bool {
const auto* impl_a = dyn_cast<ImplWitness>(a);
const auto* impl_b = dyn_cast<ImplWitness>(b);
if (!impl_b || !impl_b) {
return false;
}
// TODO: Once we support an impl being declared more than once, we will need
// to check this more carefully.
return impl_a->declaration().match_first() &&
impl_a->declaration().match_first() ==
impl_b->declaration().match_first();
}
// Determine whether this result is definitely right -- that there can be no
// specialization that would give a better match.
static auto IsEffectivelyFinal(ImplScope::ResolveResult result) -> bool {
// TODO: Once we support 'final', check whether this is a final impl
// declaration if it's parameterized.
return result.impl->deduced.empty();
}
// Combines the results of two impl lookups. In the event of a tie, arbitrarily
// prefer `a` over `b`.
static auto CombineResults(Nonnull<const InterfaceType*> iface_type,
Nonnull<const Value*> type,
SourceLocation source_loc,
std::optional<ImplScope::ResolveResult> a,
std::optional<ImplScope::ResolveResult> b)
-> ErrorOr<std::optional<ImplScope::ResolveResult>> {
// If only one lookup succeeded, return that.
if (!b) {
return a;
}
if (!a) {
return b;
}
// If exactly one of them is effectively final, prefer that result.
bool a_is_final = IsEffectivelyFinal(*a);
bool b_is_final = IsEffectivelyFinal(*b);
if (a_is_final && !b_is_final) {
return a;
} else if (b_is_final && !a_is_final) {
return b;
}
const auto* impl_a = dyn_cast<ImplWitness>(a->witness);
const auto* impl_b = dyn_cast<ImplWitness>(b->witness);
// If both are effectively final, prefer an impl declaration over a
// symbolic ImplBinding, because we get more information from the impl
// declaration. If they're both symbolic, arbitrarily pick a.
if (a_is_final && b_is_final) {
if (!impl_b) {
return a;
}
if (!impl_a) {
return b;
}
}
CARBON_CHECK(impl_a && impl_b) << "non-final impl should not be symbolic";
// At this point, we're comparing two `impl` declarations, and either they're
// both final or neither of them is.
// TODO: We should reject the case where both are final when checking their
// declarations, but we don't do so yet, so for now we report it as an
// ambiguity.
//
// If they refer to the same `impl` declaration, it doesn't matter which one
// we pick, so we pick `a`.
// TODO: Compare the identities of the `impl`s, not the declarations.
if (&impl_a->declaration() == &impl_b->declaration()) {
return a;
}
return ProgramError(source_loc)
<< "ambiguous implementations of " << *iface_type << " for " << *type;
}
auto ImplScope::TryResolveInterfaceRecursively(
Nonnull<const InterfaceType*> iface_type, Nonnull<const Value*> type,
SourceLocation source_loc, const ImplScope& original_scope,
const TypeChecker& type_checker) const
-> ErrorOr<std::optional<ResolveResult>> {
CARBON_ASSIGN_OR_RETURN(
std::optional<ResolveResult> result,
TryResolveInterfaceHere(iface_type, type, source_loc, original_scope,
type_checker));
if (parent_scope_) {
CARBON_ASSIGN_OR_RETURN(
std::optional<ResolveResult> parent_result,
(*parent_scope_)
->TryResolveInterfaceRecursively(iface_type, type, source_loc,
original_scope, type_checker));
CARBON_ASSIGN_OR_RETURN(result, CombineResults(iface_type, type, source_loc,
result, parent_result));
}
return result;
}
auto ImplScope::TryResolveInterfaceHere(
Nonnull<const InterfaceType*> iface_type, Nonnull<const Value*> impl_type,
SourceLocation source_loc, const ImplScope& original_scope,
const TypeChecker& type_checker) const
-> ErrorOr<std::optional<ResolveResult>> {
std::optional<ResolveResult> result = std::nullopt;
for (const Impl& impl : impls_) {
// If we've passed the final impl with a sort key matching our best impl,
// all further impls are worse and don't need to be checked.
if (result && result->impl->sort_key < impl.sort_key) {
break;
}
// If this impl appears later in the same match_first block as our best
// result, we should not consider it.
//
// TODO: This should apply transitively: if we have
// match_first { impl a; impl b; }
// match_first { impl b; impl c; }
// then we should not consider c once we match a. For now, because each
// impl is only declared once, this is not a problem.
if (result && InSameMatchFirst(result->impl->witness, impl.witness)) {
continue;
}
// Try matching this impl against our query.
CARBON_ASSIGN_OR_RETURN(std::optional<Nonnull<const Witness*>> witness,
type_checker.MatchImpl(*iface_type, impl_type, impl,
original_scope, source_loc));
if (witness) {
CARBON_ASSIGN_OR_RETURN(
result,
CombineResults(iface_type, impl_type, source_loc, result,
ResolveResult{.impl = &impl, .witness = *witness}));
}
}
return result;
}
// TODO: Add indentation when printing the parents.
void ImplScope::Print(llvm::raw_ostream& out) const {
out << "impls: ";
llvm::ListSeparator sep;
for (const Impl& impl : impls_) {
out << sep << *(impl.type) << " as " << *(impl.interface);
if (impl.sort_key) {
out << " " << *impl.sort_key;
}
}
for (Nonnull<const EqualityConstraint*> eq : equalities_) {
out << sep;
llvm::ListSeparator equal(" == ");
for (Nonnull<const Value*> value : eq->values) {
out << equal << *value;
}
}
out << "\n";
if (parent_scope_) {
out << **parent_scope_;
}
}
auto SingleStepEqualityContext::VisitEqualValues(
Nonnull<const Value*> value,
llvm::function_ref<bool(Nonnull<const Value*>)> visitor) const -> bool {
return impl_scope_->VisitEqualValues(value, visitor);
}
} // namespace Carbon