Resolve cycles in .Self replacement in nested designators (#7183)

A nested designator like `.(X.X1).(Y.Y1)` results in nested
ImplWitnessAccess instructions, which can produce cycles in the
toolchain easily when replacing `.Self`.

First, when constructing a facet type like `V:! Z where .Z1 impls (Y
where .Y1 = U)` we substitute replace `.Self` in the nested facet type,
and in this case we replace `.Self` with `.Z1` which contains a `.Self`
of its own. This was coming from us being lazy about replacing `.Self`
in an `impl as` declaration, such as `impl C as Z where .Z1 = .Self`.
The self type is known there, so we can more eagerly replace `.Self` as
we do in a `require impls` declaration. Then the replacement for `.Self`
never comes with a `.Self` that needs to also be replaced. Any resulting
`.Self` would always be the top-level one.

Second, when evaluating ImplWitnessAccess, we were replacing .Self in
the LHS of rewrite constraints, but the `.Self` may itself have a type
that contains rewrite constraints. If one of those rewrite constraints
has nested ImplWitnessAccess instructions, we evaluate the new
ImplWitnessAccess, which again finds rewrite constraints to replace
`.Self` in, and we repeat forever. For this one we just stop replacing
.Self in the LHS of rewrite constraints. Since they are always against
.Self, we can always look in the access facet's type for a value.

While fixing ImplWitness access, also correct the lookup to search
through the types of nested ImplWitnessAccess instructions to find a
rewrite value, since it may find it at any level up to the eventual
`.Self`.

---------

Co-authored-by: Richard Smith <richard@metafoo.co.uk>
This commit is contained in:
Dana Jansens
2026-05-13 16:11:20 +00:00
committed by GitHub
co-authored by Richard Smith
parent 798b177fc0
commit 6326bbdbe1
23 changed files with 820 additions and 534 deletions
+106 -86
View File
@@ -285,6 +285,94 @@ auto EvalConstantInst(Context& context, SemIR::InstId inst_id,
return ConstantEvalResult::NewSamePhase(inst);
}
// Given a SpecificInterface and an index of an associated constant in that
// interface, try find a value for that constant in the rewrite constraints of
// the type of `search_facet`.
static auto TryFindValueInRewriteConstraints(
Context& context, SemIR::LocId loc_id,
SemIR::SpecificInterfaceId specific_interface_id,
SemIR::ElementIndex interface_index, SemIR::InstId search_facet)
-> SemIR::ConstantId {
auto access_self_type_id = context.insts().Get(search_facet).type_id();
if (context.types().Is<SemIR::TypeType>(access_self_type_id)) {
// A self facet of type `type` has no rewrite constraints to look in.
return SemIR::ConstantId::None;
}
// The `ImplWitnessAccess` is accessing a value, by index, for this `self
// impls interface` combination.
auto access_interface =
context.specific_interfaces().Get(specific_interface_id);
auto access_self_facet_type_id =
context.types()
.GetAs<SemIR::FacetType>(access_self_type_id)
.facet_type_id;
// TODO: We could consider something better than linear search here, such as a
// map. However that would probably require heap allocations which may be
// worse overall since the number of rewrite constraints is generally low. If
// the `rewrite_constraints` were sorted so that associated constants are
// grouped together, as in ResolveFacetTypeRewriteConstraints(), and limited
// to just the `ImplWitnessAccess` entries, then a binary search may work
// here.
for (const auto& rewrite : context.facet_types()
.Get(access_self_facet_type_id)
.rewrite_constraints) {
// Look at each rewrite constraint in the self facet's type. If the LHS is
// an `ImplWitnessAccess` into the same interface that `inst` is indexing
// into, then we can use its RHS as the value.
auto rewrite_lhs_access =
context.insts().TryGetAs<SemIR::ImplWitnessAccess>(rewrite.lhs_id);
if (!rewrite_lhs_access) {
continue;
}
if (rewrite_lhs_access->index != interface_index) {
continue;
}
// Witnesses come from impl lookup, and the operands are from
// IdentifiedFacetTypes, so `.Self` is replaced. However rewrite constraints
// are not part of an IdentifiedFacetType, so they are not replaced. We have
// to do the same replacement in the rewrite's LHS witness in order to
// compare it with the access witness.
//
// However we don't substitute the witness directly as that would
// re-evaluate it and cause us to do an impl lookup. Instead we substitute
// and compare its operands.
auto rewrite_lhs_witness = context.insts().GetAs<SemIR::LookupImplWitness>(
rewrite_lhs_access->witness_id);
auto self_const_id = context.constant_values().Get(search_facet);
// The LHS of the rewrite might be `.Self` or it could be one or more nested
// ImplWitnessAccess instructions that eventually bottom out in `.Self`.
// Rewrite constraints must modify `.Self` so we know the target of the
// rewrite is ultimately always `.Self` which refers to the `search_facet`.
// So we don't have to substitute the `.Self` and do any comparison.
auto rewrite_lhs_interface =
SubstPeriodSelf(context, loc_id,
context.specific_interfaces().Get(
rewrite_lhs_witness.query_specific_interface_id),
self_const_id);
if (rewrite_lhs_interface != access_interface) {
// This rewrite is into a different interface than the access query.
continue;
}
// The `ImplWitnessAccess` evaluates to the RHS from the witness self facet
// value's type. Any `.Self` references in the RHS are also replaced with
// the self type of the access.
auto rewrite_rhs = SubstPeriodSelf(
context, loc_id, context.constant_values().Get(rewrite.rhs_id),
self_const_id);
return rewrite_rhs;
}
return SemIR::ConstantId::None;
}
auto EvalConstantInst(Context& context, SemIR::InstId inst_id,
SemIR::ImplWitnessAccess inst) -> ConstantEvalResult {
CARBON_DIAGNOSTIC(ImplAccessMemberBeforeSet, Error,
@@ -332,94 +420,26 @@ auto EvalConstantInst(Context& context, SemIR::InstId inst_id,
// If the witness is symbolic but has a self type that is a FacetType, it
// can pull rewrite values from the self's facet type. If the access is
// for one of those rewrites, evaluate to the RHS of the rewrite.
// The type of the query self type (a FacetType or TypeType).
auto access_self_type_id =
context.insts().Get(witness.query_self_inst_id).type_id();
if (context.types().Is<SemIR::TypeType>(access_self_type_id)) {
// A self facet of type `type` has no rewrite constraints to look in.
return ConstantEvalResult::NewSamePhase(inst);
}
// The `ImplWitnessAccess` is accessing a value, by index, for this
// `self impls interface` combination.
auto access_self =
context.constant_values().Get(witness.query_self_inst_id);
auto access_interface = context.specific_interfaces().Get(
witness.query_specific_interface_id);
auto access_self_facet_type_id =
context.types()
.GetAs<SemIR::FacetType>(access_self_type_id)
.facet_type_id;
// TODO: We could consider something better than linear search here, such
// as a map. However that would probably require heap allocations which
// may be worse overall since the number of rewrite constraints is
// generally low. If the `rewrite_constraints` were sorted so that
// associated constants are grouped together, as in
// ResolveFacetTypeRewriteConstraints(), and limited to just the
// `ImplWitnessAccess` entries, then a binary search may work here.
for (const auto& rewrite : context.facet_types()
.Get(access_self_facet_type_id)
.rewrite_constraints) {
// Look at each rewrite constraint in the self facet's type. If the LHS
// is an `ImplWitnessAccess` into the same interface that `inst` is
// indexing into, then we can use its RHS as the value.
auto rewrite_lhs_access =
context.insts().TryGetAs<SemIR::ImplWitnessAccess>(rewrite.lhs_id);
if (!rewrite_lhs_access) {
continue;
//
// If we have a nested `.X1.Y1.Z1` we start with the facet type of .Y1 to
// look for a rewrite constraint that provides the value for .Z1. But if
// we don't find it, we try .X1 and .Self.
auto search_facet = witness.query_self_inst_id;
while (true) {
auto const_id = TryFindValueInRewriteConstraints(
context, SemIR::LocId(inst_id), witness.query_specific_interface_id,
inst.index, search_facet);
if (const_id.has_value()) {
return ConstantEvalResult::Existing(const_id);
}
if (rewrite_lhs_access->index != inst.index) {
continue;
if (auto access = context.insts().TryGetAs<SemIR::ImplWitnessAccess>(
search_facet)) {
auto witness = context.insts().GetAs<SemIR::LookupImplWitness>(
access->witness_id);
search_facet = witness.query_self_inst_id;
} else {
break;
}
// Witnesses come from impl lookup, and the operands are from
// IdentifiedFacetTypes, so `.Self` is replaced. However rewrite
// constraints are not part of an IdentifiedFacetType, so they are not
// replaced. We have to do the same replacement in the rewrite's LHS
// witness in order to compare it with the access witness.
//
// However we don't substitute the witness directly as that would
// re-evaluate it and cause us to do an impl lookup. Instead we
// substitute and compare its operands.
auto rewrite_lhs_witness =
context.insts().GetAs<SemIR::LookupImplWitness>(
rewrite_lhs_access->witness_id);
SubstPeriodSelfCallbacks callbacks(&context, SemIR::LocId(inst_id),
access_self);
auto rewrite_lhs_self = context.constant_values().Get(
rewrite_lhs_witness.query_self_inst_id);
rewrite_lhs_self =
SubstPeriodSelf(context, callbacks, rewrite_lhs_self);
// Witnesses have a canonicalized self value. Perform the same
// canonicalization here so that we can compare them.
rewrite_lhs_self = GetCanonicalQuerySelfForLookupImplWitness(
context, rewrite_lhs_self);
if (rewrite_lhs_self != access_self) {
// This rewrite is into a different self type than the access query.
continue;
}
auto rewrite_lhs_interface = SubstPeriodSelf(
context, callbacks,
context.specific_interfaces().Get(
rewrite_lhs_witness.query_specific_interface_id));
if (rewrite_lhs_interface != access_interface) {
// This rewrite is into a different interface than the access query.
continue;
}
// The `ImplWitnessAccess` evaluates to the RHS from the witness self
// facet value's type. Any `.Self` references in the RHS are also
// replaced with the self type of the access.
auto rewrite_rhs = SubstPeriodSelf(
context, callbacks, context.constant_values().Get(rewrite.rhs_id));
return ConstantEvalResult::Existing(rewrite_rhs);
}
break;
}