Compare commits

...

24 Commits

Author SHA1 Message Date
Douglas Creager
bb07775617 only use constraint sets for protocols 2026-01-08 11:22:02 -05:00
Douglas Creager
180b38987c clippy 2026-01-08 09:42:43 -05:00
Douglas Creager
7bbe3d73fc merge main 2026-01-08 09:36:53 -05:00
Douglas Creager
d5089ccd4d estimate size of distributed upper bound 2026-01-08 09:30:43 -05:00
Douglas Creager
1c247af1cf don't hard-code the threshold 2026-01-08 09:00:18 -05:00
Douglas Creager
f735222ed6 punt on very large union upper bounds for now 2026-01-07 11:34:26 -05:00
Douglas Creager
f1188c74b1 fix merge conflicts 2026-01-06 21:19:19 -05:00
Douglas Creager
e5533da00f prune types when intersecting into upper bound 2026-01-06 20:59:41 -05:00
Douglas Creager
138bf79857 paramspec args/kwargs don't have a bound 2026-01-05 21:24:51 -05:00
Douglas Creager
e39f4654cb materialize the bounds too 2026-01-05 21:13:26 -05:00
Douglas Creager
2c95befaff clippy 2026-01-05 21:13:26 -05:00
Douglas Creager
61aafffab5 prefer informative upper bound again 2026-01-05 21:13:26 -05:00
Douglas Creager
47840fdd0c constrain with bounds/constraints as post-processing step 2026-01-05 21:13:26 -05:00
Douglas Creager
acd1e6c466 don't overwrite source_order 2026-01-05 13:52:11 -05:00
Douglas Creager
9ca1207667 no duplicate errors anymore 2026-01-05 13:52:11 -05:00
Douglas Creager
83378f01b1 only error when there are typevars 2026-01-05 13:52:10 -05:00
Douglas Creager
be4e7e773d check protocols nominally, too 2026-01-05 13:52:10 -05:00
Douglas Creager
6df88e8fd2 fix test expectations and TODOs 2026-01-05 13:52:07 -05:00
Douglas Creager
8888c3ce1d add todos 2026-01-05 13:41:45 -05:00
Douglas Creager
b6ca2d9050 error message 2026-01-05 13:41:45 -05:00
Douglas Creager
2fa8636a2e make these tests consistent 2026-01-05 13:41:45 -05:00
Douglas Creager
c8664f68bb add TODO for Never/Unknown 2026-01-05 13:41:45 -05:00
Douglas Creager
4ff828d996 these are legit 2026-01-05 13:41:45 -05:00
Douglas Creager
517566a8ca use csa for instance checks 2026-01-05 13:41:45 -05:00
10 changed files with 265 additions and 80 deletions

View File

@@ -17,8 +17,7 @@ from datetime import time
t = time(12, 0, 0)
t = replace(t, minute=30)
# TODO: this should be `time`, once we support specialization of generic protocols
reveal_type(t) # revealed: Unknown
reveal_type(t) # revealed: time
```
## The `__replace__` protocol
@@ -48,8 +47,7 @@ b = a.__replace__(x=3, y=4)
reveal_type(b) # revealed: Point
b = replace(a, x=3, y=4)
# TODO: this should be `Point`, once we support specialization of generic protocols
reveal_type(b) # revealed: Unknown
reveal_type(b) # revealed: Point
```
A call to `replace` does not require all keyword arguments:
@@ -59,8 +57,7 @@ c = a.__replace__(y=4)
reveal_type(c) # revealed: Point
d = replace(a, y=4)
# TODO: this should be `Point`, once we support specialization of generic protocols
reveal_type(d) # revealed: Unknown
reveal_type(d) # revealed: Point
```
Invalid calls to `__replace__` or `replace` will raise an error:
@@ -96,8 +93,7 @@ b = a.__replace__(x=3, y=4)
reveal_type(b) # revealed: Point
b = replace(a, x=3, y=4)
# TODO: this should be `Point`, once we support specialization of generic protocols
reveal_type(b) # revealed: Unknown
reveal_type(b) # revealed: Point
```
Invalid calls to `__replace__` will raise an error:

View File

@@ -561,7 +561,7 @@ from typing_extensions import overload, Generic, TypeVar
from ty_extensions import generic_context, into_callable
T = TypeVar("T")
U = TypeVar("U")
U = TypeVar("U", covariant=True)
class C(Generic[T]):
@overload
@@ -611,9 +611,9 @@ reveal_type(generic_context(D))
# revealed: ty_extensions.GenericContext[T@D, U@D]
reveal_type(generic_context(into_callable(D)))
reveal_type(D("string")) # revealed: D[str, str]
reveal_type(D(1)) # revealed: D[str, int]
reveal_type(D(1, "string")) # revealed: D[int, str]
reveal_type(D("string")) # revealed: D[str, Literal["string"]]
reveal_type(D(1)) # revealed: D[str, Literal[1]]
reveal_type(D(1, "string")) # revealed: D[int, Literal["string"]]
```
### Synthesized methods with dataclasses

View File

@@ -90,13 +90,11 @@ def takes_in_protocol(x: CanIndex[T]) -> T:
def deep_list(x: list[str]) -> None:
reveal_type(takes_in_list(x)) # revealed: list[str]
# TODO: revealed: str
reveal_type(takes_in_protocol(x)) # revealed: Unknown
reveal_type(takes_in_protocol(x)) # revealed: str
def deeper_list(x: list[set[str]]) -> None:
reveal_type(takes_in_list(x)) # revealed: list[set[str]]
# TODO: revealed: set[str]
reveal_type(takes_in_protocol(x)) # revealed: Unknown
reveal_type(takes_in_protocol(x)) # revealed: set[str]
def deep_explicit(x: ExplicitlyImplements[str]) -> None:
reveal_type(takes_in_protocol(x)) # revealed: str
@@ -129,12 +127,10 @@ class Sub(list[int]): ...
class GenericSub(list[T]): ...
reveal_type(takes_in_list(Sub())) # revealed: list[int]
# TODO: revealed: int
reveal_type(takes_in_protocol(Sub())) # revealed: Unknown
reveal_type(takes_in_protocol(Sub())) # revealed: int
reveal_type(takes_in_list(GenericSub[str]())) # revealed: list[str]
# TODO: revealed: str
reveal_type(takes_in_protocol(GenericSub[str]())) # revealed: Unknown
reveal_type(takes_in_protocol(GenericSub[str]())) # revealed: str
class ExplicitSub(ExplicitlyImplements[int]): ...
class ExplicitGenericSub(ExplicitlyImplements[T]): ...

View File

@@ -538,6 +538,10 @@ C[None](b"bytes") # error: [no-matching-overload]
C[None](12)
class D[T, U]:
# we need to use the type variable or else the class is bivariant in T, and
# specializations become meaningless
x: T
@overload
def __init__(self: "D[str, U]", u: U) -> None: ...
@overload
@@ -551,7 +555,7 @@ reveal_type(generic_context(into_callable(D)))
reveal_type(D("string")) # revealed: D[str, Literal["string"]]
reveal_type(D(1)) # revealed: D[str, Literal[1]]
reveal_type(D(1, "string")) # revealed: D[Literal[1], Literal["string"]]
reveal_type(D(1, "string")) # revealed: D[int, Literal["string"]]
```
### Synthesized methods with dataclasses

View File

@@ -85,13 +85,11 @@ def takes_in_protocol[T](x: CanIndex[T]) -> T:
def deep_list(x: list[str]) -> None:
reveal_type(takes_in_list(x)) # revealed: list[str]
# TODO: revealed: str
reveal_type(takes_in_protocol(x)) # revealed: Unknown
reveal_type(takes_in_protocol(x)) # revealed: str
def deeper_list(x: list[set[str]]) -> None:
reveal_type(takes_in_list(x)) # revealed: list[set[str]]
# TODO: revealed: set[str]
reveal_type(takes_in_protocol(x)) # revealed: Unknown
reveal_type(takes_in_protocol(x)) # revealed: set[str]
def deep_explicit(x: ExplicitlyImplements[str]) -> None:
reveal_type(takes_in_protocol(x)) # revealed: str
@@ -124,12 +122,10 @@ class Sub(list[int]): ...
class GenericSub[T](list[T]): ...
reveal_type(takes_in_list(Sub())) # revealed: list[int]
# TODO: revealed: int
reveal_type(takes_in_protocol(Sub())) # revealed: Unknown
reveal_type(takes_in_protocol(Sub())) # revealed: int
reveal_type(takes_in_list(GenericSub[str]())) # revealed: list[str]
# TODO: revealed: str
reveal_type(takes_in_protocol(GenericSub[str]())) # revealed: Unknown
reveal_type(takes_in_protocol(GenericSub[str]())) # revealed: str
class ExplicitSub(ExplicitlyImplements[int]): ...
class ExplicitGenericSub[T](ExplicitlyImplements[T]): ...

View File

@@ -1369,6 +1369,31 @@ impl<'db> Type<'db> {
}
}
/// Returns the number of union clauses in this type. If the type is not a union, returns 1.
pub(crate) fn union_size(self, db: &'db dyn Db) -> usize {
self.as_union()
.map(|union_type| union_type.elements(db).len())
.unwrap_or(1)
}
/// Returns the number of intersection clauses in this type. If the type is a union, this is
/// the maximum of the `intersection_size` of each union element. If the type is not a union
/// nor an intersection, returns 1.
pub(crate) fn intersection_size(self, db: &'db dyn Db) -> usize {
match self {
Type::Intersection(intersection) => {
intersection.positive(db).len() + intersection.negative(db).len()
}
Type::Union(union_type) => union_type
.elements(db)
.iter()
.map(|element| element.intersection_size(db))
.max()
.unwrap_or(1),
_ => 1,
}
}
pub(crate) const fn as_function_literal(self) -> Option<FunctionType<'db>> {
match self {
Type::FunctionLiteral(function_type) => Some(function_type),
@@ -7895,6 +7920,16 @@ impl<'db> TypeVarInstance<'db> {
})
}
/// Returns the bounds or constraints of this typevar. If the typevar is unbounded, returns
/// `object` as its upper bound.
pub(crate) fn require_bound_or_constraints(
self,
db: &'db dyn Db,
) -> TypeVarBoundOrConstraints<'db> {
self.bound_or_constraints(db)
.unwrap_or_else(|| TypeVarBoundOrConstraints::UpperBound(Type::object()))
}
pub(crate) fn default_type(self, db: &'db dyn Db) -> Option<Type<'db>> {
self._default(db).and_then(|d| match d {
TypeVarDefaultEvaluation::Eager(ty) => Some(ty),

View File

@@ -4489,7 +4489,6 @@ impl<'db> BindingError<'db> {
return;
};
let typevar = error.bound_typevar().typevar(context.db());
let argument_type = error.argument_type();
let argument_ty_display = argument_type.display(context.db());
@@ -4502,21 +4501,51 @@ impl<'db> BindingError<'db> {
}
));
let typevar_name = typevar.name(context.db());
match error {
SpecializationError::MismatchedBound { .. } => {
diag.set_primary_message(format_args!("Argument type `{argument_ty_display}` does not satisfy upper bound `{}` of type variable `{typevar_name}`",
typevar.upper_bound(context.db()).expect("type variable should have an upper bound if this error occurs").display(context.db())
));
SpecializationError::NoSolution { parameter, .. } => {
diag.set_primary_message(format_args!(
"Argument type `{argument_ty_display}` does not \
satisfy generic parameter annotation `{}",
parameter.display(context.db()),
));
}
SpecializationError::MismatchedConstraint { .. } => {
diag.set_primary_message(format_args!("Argument type `{argument_ty_display}` does not satisfy constraints ({}) of type variable `{typevar_name}`",
typevar.constraints(context.db()).expect("type variable should have constraints if this error occurs").iter().map(|ty| format!("`{}`", ty.display(context.db()))).join(", ")
));
SpecializationError::MismatchedBound { bound_typevar, .. } => {
let typevar = bound_typevar.typevar(context.db());
let typevar_name = typevar.name(context.db());
diag.set_primary_message(format_args!(
"Argument type `{argument_ty_display}` does not \
satisfy upper bound `{}` of type variable `{typevar_name}`",
typevar
.upper_bound(context.db())
.expect(
"type variable should have an upper bound if this error occurs"
)
.display(context.db())
));
}
SpecializationError::MismatchedConstraint { bound_typevar, .. } => {
let typevar = bound_typevar.typevar(context.db());
let typevar_name = typevar.name(context.db());
diag.set_primary_message(format_args!(
"Argument type `{argument_ty_display}` does not \
satisfy constraints ({}) of type variable `{typevar_name}`",
typevar
.constraints(context.db())
.expect(
"type variable should have constraints if this error occurs"
)
.iter()
.format_with(", ", |ty, f| f(&format_args!(
"`{}`",
ty.display(context.db())
)))
));
}
}
if let Some(typevar_definition) = typevar.definition(context.db()) {
if let Some(typevar_definition) = error.bound_typevar().and_then(|bound_typevar| {
bound_typevar.typevar(context.db()).definition(context.db())
}) {
let module = parsed_module(context.db(), typevar_definition.file(context.db()))
.load(context.db());
let typevar_range = typevar_definition.full_range(context.db(), &module);

View File

@@ -72,6 +72,7 @@ use std::fmt::Display;
use std::ops::Range;
use itertools::Itertools;
use ordermap::map::Entry;
use rustc_hash::{FxHashMap, FxHashSet};
use salsa::plumbing::AsId;
@@ -757,9 +758,25 @@ impl<'db> ConstrainedTypeVar<'db> {
/// Returns the intersection of two range constraints, or `None` if the intersection is empty.
fn intersect(self, db: &'db dyn Db, other: Self) -> IntersectionResult<'db> {
// TODO: For now, we treat some upper bounds as unsimplifiable if they become "too big".
// When intersecting constraints, the upper bounds are also intersected together. If the
// lhs and rhs upper bounds are unions of intersections (e.g. `(a & b) | (c & d)`), then
// intersecting them together will require distributing across every pair of union
// elements. That can quickly balloon in size. We are looking at a better representation
// that would let us model this case more directly, but for now, we punt.
const MAX_UPPER_BOUND_SIZE: usize = 4;
let self_upper = self.upper(db);
let other_upper = other.upper(db);
let estimated_upper_bound_size = self_upper.union_size(db)
* other_upper.union_size(db)
* (self_upper.intersection_size(db) + other_upper.intersection_size(db));
if estimated_upper_bound_size >= MAX_UPPER_BOUND_SIZE {
return IntersectionResult::CannotSimplify;
}
// (s₁ ≤ α ≤ t₁) ∧ (s₂ ≤ α ≤ t₂) = (s₁ s₂) ≤ α ≤ (t₁ ∩ t₂))
let lower = UnionType::from_elements(db, [self.lower(db), other.lower(db)]);
let upper = IntersectionType::from_elements(db, [self.upper(db), other.upper(db)]);
let upper = IntersectionType::from_elements(db, [self_upper, other_upper]);
// If `lower ≰ upper`, then the intersection is empty, since there is no type that is both
// greater than `lower`, and less than `upper`.
@@ -767,6 +784,8 @@ impl<'db> ConstrainedTypeVar<'db> {
return IntersectionResult::Disjoint;
}
// We do not create lower bounds that are unions, or upper bounds that are intersections,
// since those can be broken apart into BDDs over simpler constraints.
if lower.is_union() || upper.is_nontrivial_intersection(db) {
return IntersectionResult::CannotSimplify;
}
@@ -3437,9 +3456,11 @@ impl<'db> PathAssignments<'db> {
);
return Err(PathAssignmentConflict);
}
if self.assignments.insert(assignment, source_order).is_some() {
return Ok(());
}
match self.assignments.entry(assignment) {
Entry::Vacant(entry) => entry.insert(source_order),
Entry::Occupied(_) => return Ok(()),
};
// Then use our sequents to add additional facts that we know to be true. We currently
// reuse the `source_order` of the "real" constraint passed into `walk_edge` when we add
@@ -3741,6 +3762,11 @@ impl<'db> BoundTypeVarInstance<'db> {
/// when this typevar is in inferable position, where we only need _some_ specialization to
/// satisfy the constraint set.
fn valid_specializations(self, db: &'db dyn Db) -> Node<'db> {
if self.paramspec_attr(db).is_some() {
// P.args and P.kwargs are variadic, and do not have an upper bound or constraints.
return Node::AlwaysTrue;
}
// For gradual upper bounds and constraints, we are free to choose any materialization that
// makes the check succeed. In inferable positions, it is most helpful to choose a
// materialization that is as permissive as possible, since that maximizes the number of

View File

@@ -13,7 +13,6 @@ use crate::semantic_index::{SemanticIndex, semantic_index};
use crate::types::class::ClassType;
use crate::types::class_base::ClassBase;
use crate::types::constraints::{ConstraintSet, IteratorConstraintsExtension};
use crate::types::instance::{Protocol, ProtocolInstanceType};
use crate::types::relation::{
HasRelationToVisitor, IsDisjointVisitor, IsEquivalentVisitor, TypeRelation,
};
@@ -1634,12 +1633,43 @@ impl<'db> SpecializationBuilder<'db> {
upper: FxOrderSet<Type<'db>>,
}
impl<'db> Bounds<'db> {
fn add_lower(&mut self, _db: &'db dyn Db, ty: Type<'db>) {
// Lower bounds are unioned. Our type representation is in DNF, so unioning a new
// element is typically cheap (in that it does not involve a combinatorial
// explosion from distributing the clause through an existing disjunction). So we
// don't need to be as clever here as in `add_upper`.
self.lower.insert(ty);
}
fn add_upper(&mut self, db: &'db dyn Db, ty: Type<'db>) {
// Upper bounds are intersectioned. If `ty` is a union, that involves distributing
// the union elements through the existing type. That makes it worth checking first
// whether any of the types in the upper bound are redundant.
// First check if there's an existing upper bound clause that is a subtype of the
// new type. If so, adding the new type does nothing to the intersection.
if self
.upper
.iter()
.any(|existing| existing.is_subtype_of(db, ty))
{
return;
}
// Otherwise remove any existing clauses that are a supertype of the new type,
// since the intersection will clip them to the new type.
self.upper
.retain(|existing| !ty.is_subtype_of(db, *existing));
self.upper.insert(ty);
}
}
// Sort the constraints in each path by their `source_order`s, to ensure that we construct
// any unions or intersections in our type mappings in a stable order. Constraints might
// come out of `PathAssignment`s with identical `source_order`s, but if they do, those
// "tied" constraints will still be ordered in a stable way. So we need a stable sort to
// retain that stable per-tie ordering.
let constraints = constraints.limit_to_valid_specializations(self.db);
let mut sorted_paths = Vec::new();
constraints.for_each_path(self.db, |path| {
let mut path: Vec<_> = path.positive_constraints().collect();
@@ -1660,33 +1690,68 @@ impl<'db> SpecializationBuilder<'db> {
let lower = constraint.lower(self.db);
let upper = constraint.upper(self.db);
let bounds = mappings.entry(typevar).or_default();
bounds.lower.insert(lower);
bounds.upper.insert(upper);
bounds.add_lower(self.db, lower);
bounds.add_upper(self.db, upper);
if let Type::TypeVar(lower_bound_typevar) = lower {
let bounds = mappings.entry(lower_bound_typevar).or_default();
bounds.upper.insert(Type::TypeVar(typevar));
bounds.add_upper(self.db, Type::TypeVar(typevar));
}
if let Type::TypeVar(upper_bound_typevar) = upper {
let bounds = mappings.entry(upper_bound_typevar).or_default();
bounds.lower.insert(Type::TypeVar(typevar));
bounds.add_lower(self.db, Type::TypeVar(typevar));
}
}
for (bound_typevar, bounds) in mappings.drain() {
let variance = formal.variance_of(self.db, bound_typevar);
// Prefer the lower bound (often the concrete actual type seen) over the
// upper bound (which may include TypeVar bounds/constraints). The upper bound
// should only be used as a fallback when no concrete type was inferred.
let lower = UnionType::from_elements(self.db, bounds.lower);
if !lower.is_never() {
self.add_type_mapping(bound_typevar, lower, variance, &mut f);
continue;
}
let upper = IntersectionType::from_elements(self.db, bounds.upper);
if !upper.is_object() {
self.add_type_mapping(bound_typevar, upper, variance, &mut f);
match bound_typevar
.typevar(self.db)
.require_bound_or_constraints(self.db)
{
TypeVarBoundOrConstraints::UpperBound(bound) => {
let bound = bound.top_materialization(self.db);
let lower = UnionType::from_elements(self.db, bounds.lower);
if !lower.is_assignable_to(self.db, bound) {
// This path does not satisfy the typevar's upper bound, and is
// therefore not a valid specialization.
continue;
}
let upper = IntersectionType::from_elements(
self.db,
bounds.upper.into_iter().chain([bound]),
);
if upper != bound {
self.add_type_mapping(bound_typevar, upper, variance, &mut f);
} else if !lower.is_never() {
self.add_type_mapping(bound_typevar, lower, variance, &mut f);
}
}
TypeVarBoundOrConstraints::Constraints(constraints) => {
// Filter out the typevar constraints that aren't satisfied by this path.
let lower = UnionType::from_elements(self.db, bounds.lower);
let upper = IntersectionType::from_elements(self.db, bounds.upper);
let compatible_constraints =
constraints.elements(self.db).iter().filter(|constraint| {
let constraint_lower = constraint.bottom_materialization(self.db);
let constraint_upper = constraint.top_materialization(self.db);
lower.is_assignable_to(self.db, constraint_lower)
&& constraint_upper.is_assignable_to(self.db, upper)
});
// If only one constraint remains, that's our specialization for this path.
if let Ok(compatible_constraint) = compatible_constraints.exactly_one() {
self.add_type_mapping(
bound_typevar,
*compatible_constraint,
variance,
&mut f,
);
}
}
}
}
}
@@ -1967,14 +2032,31 @@ impl<'db> SpecializationBuilder<'db> {
Type::NominalInstance(formal_nominal) => {
formal_nominal.class(self.db).into_generic_alias()
}
// TODO: This will only handle classes that explicit implement a generic protocol
// by listing it as a base class. To handle classes that implicitly implement a
// generic protocol, we will need to check the types of the protocol members to be
// able to infer the specialization of the protocol that the class implements.
Type::ProtocolInstance(ProtocolInstanceType {
inner: Protocol::FromClass(class),
..
}) => class.into_generic_alias(),
Type::ProtocolInstance(_) => {
// TODO: For protocols, we use the new constraint set implementation, which
// will handle implicitly implemented protocols and generic protocols. We
// eventually want this logic to be used for _all_ nominal instances
// (replacing the logic below).
let when = actual.when_constraint_set_assignable_to(
self.db,
formal,
self.inferable,
);
if when
.limit_to_valid_specializations(self.db)
.is_never_satisfied(self.db)
&& (formal.has_typevar(self.db) || actual.has_typevar(self.db))
{
return Err(SpecializationError::NoSolution {
parameter: formal,
argument: actual,
});
}
self.add_type_mappings_from_constraint_set(formal, when, &mut f);
return Ok(());
}
_ => None,
};
@@ -2132,6 +2214,10 @@ impl<'db> SpecializationBuilder<'db> {
#[derive(Clone, Debug, Eq, PartialEq)]
pub(crate) enum SpecializationError<'db> {
NoSolution {
parameter: Type<'db>,
argument: Type<'db>,
},
MismatchedBound {
bound_typevar: BoundTypeVarInstance<'db>,
argument: Type<'db>,
@@ -2143,15 +2229,17 @@ pub(crate) enum SpecializationError<'db> {
}
impl<'db> SpecializationError<'db> {
pub(crate) fn bound_typevar(&self) -> BoundTypeVarInstance<'db> {
pub(crate) fn bound_typevar(&self) -> Option<BoundTypeVarInstance<'db>> {
match self {
Self::MismatchedBound { bound_typevar, .. } => *bound_typevar,
Self::MismatchedConstraint { bound_typevar, .. } => *bound_typevar,
Self::NoSolution { .. } => None,
Self::MismatchedBound { bound_typevar, .. } => Some(*bound_typevar),
Self::MismatchedConstraint { bound_typevar, .. } => Some(*bound_typevar),
}
}
pub(crate) fn argument_type(&self) -> Type<'db> {
match self {
Self::NoSolution { argument, .. } => *argument,
Self::MismatchedBound { argument, .. } => *argument,
Self::MismatchedConstraint { argument, .. } => *argument,
}

View File

@@ -134,14 +134,29 @@ impl<'db> Type<'db> {
disjointness_visitor: &IsDisjointVisitor<'db>,
) -> ConstraintSet<'db> {
let structurally_satisfied = if let Type::ProtocolInstance(self_protocol) = self {
self_protocol.interface(db).has_relation_to_impl(
db,
protocol.interface(db),
inferable,
relation,
relation_visitor,
disjointness_visitor,
)
let self_as_nominal = self_protocol.to_nominal_instance();
let other_as_nominal = protocol.to_nominal_instance();
let nominal_match = match self_as_nominal.zip(other_as_nominal) {
Some((self_as_nominal, other_as_nominal)) => self_as_nominal.has_relation_to_impl(
db,
other_as_nominal,
inferable,
relation,
relation_visitor,
disjointness_visitor,
),
_ => ConstraintSet::from(false),
};
nominal_match.or(db, || {
self_protocol.interface(db).has_relation_to_impl(
db,
protocol.interface(db),
inferable,
relation,
relation_visitor,
disjointness_visitor,
)
})
} else {
protocol
.inner