[ty] Add functions for revealing assignability/subtyping constraints (#20217)

This PR adds two new `ty_extensions` functions,
`reveal_when_assignable_to` and `reveal_when_subtype_of`. These are
closely related to the existing `is_assignable_to` and `is_subtype_of`,
but instead of returning when the property (always) holds, it produces a
diagnostic that describes _when_ the property holds. (This will let us
construct mdtests that print out constraints that are not always true or
always false — though we don't currently have any instances of those.)

I did not replace _every_ occurrence of the `is_property` variants in
the mdtest suite, instead focusing on the generics-related tests where
it will be important to see the full detail of the constraint sets.

As part of this, I also updated the mdtest harness to accept the shorter
`# revealed:` assertion format for more than just `reveal_type`, and
updated the existing uses of `reveal_protocol_interface` to take
advantage of this.
This commit is contained in:
Douglas Creager 2025-09-03 16:44:35 -04:00 committed by GitHub
parent 200349c6e8
commit 77b2cee223
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 309 additions and 199 deletions

View file

@ -4168,6 +4168,24 @@ impl<'db> Type<'db> {
)
.into(),
Some(
KnownFunction::RevealWhenAssignableTo | KnownFunction::RevealWhenSubtypeOf,
) => Binding::single(
self,
Signature::new(
Parameters::new([
Parameter::positional_only(Some(Name::new_static("a")))
.type_form()
.with_annotated_type(Type::any()),
Parameter::positional_only(Some(Name::new_static("b")))
.type_form()
.with_annotated_type(Type::any()),
]),
Some(KnownClass::NoneType.to_instance(db)),
),
)
.into(),
Some(KnownFunction::IsSingleton | KnownFunction::IsSingleValued) => {
Binding::single(
self,

View file

@ -133,9 +133,6 @@ pub(crate) trait Constraints<'db>: Clone + Sized {
}
// This is here so that we can easily print constraint sets when debugging.
// TODO: Add a ty_extensions function to reveal constraint sets so that this is no longer dead
// code, and so that we verify the contents of our rendering.
#[expect(dead_code)]
fn display(&self, db: &'db dyn Db) -> impl Display;
}
@ -345,34 +342,6 @@ impl<'db> ConstraintSet<'db> {
}
}
}
// This is here so that we can easily print constraint sets when debugging.
// TODO: Add a ty_extensions function to reveal constraint sets so that this is no longer dead
// code, and so that we verify the contents of our rendering.
#[expect(dead_code)]
pub(crate) fn display(&self, db: &'db dyn Db) -> impl Display {
struct DisplayConstraintSet<'a, 'db> {
set: &'a ConstraintSet<'db>,
db: &'db dyn Db,
}
impl Display for DisplayConstraintSet<'_, '_> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
if self.set.clauses.is_empty() {
return f.write_str("0");
}
for (i, clause) in self.set.clauses.iter().enumerate() {
if i > 0 {
f.write_str(" ")?;
}
clause.display(self.db).fmt(f)?;
}
Ok(())
}
}
DisplayConstraintSet { set: self, db }
}
}
impl<'db> Constraints<'db> for ConstraintSet<'db> {
@ -411,7 +380,27 @@ impl<'db> Constraints<'db> for ConstraintSet<'db> {
}
fn display(&self, db: &'db dyn Db) -> impl Display {
self.display(db)
struct DisplayConstraintSet<'a, 'db> {
set: &'a ConstraintSet<'db>,
db: &'db dyn Db,
}
impl Display for DisplayConstraintSet<'_, '_> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
if self.set.clauses.is_empty() {
return f.write_str("0");
}
for (i, clause) in self.set.clauses.iter().enumerate() {
if i > 0 {
f.write_str(" ")?;
}
clause.display(self.db).fmt(f)?;
}
Ok(())
}
}
DisplayConstraintSet { set: self, db }
}
}

View file

@ -65,7 +65,7 @@ use crate::semantic_index::definition::Definition;
use crate::semantic_index::scope::ScopeId;
use crate::semantic_index::semantic_index;
use crate::types::call::{Binding, CallArguments};
use crate::types::constraints::Constraints;
use crate::types::constraints::{ConstraintSet, Constraints};
use crate::types::context::InferContext;
use crate::types::diagnostic::{
INVALID_ARGUMENT_TYPE, REDUNDANT_CAST, STATIC_ASSERT_ERROR, TYPE_ASSERTION_FAILURE,
@ -1188,6 +1188,10 @@ pub enum KnownFunction {
HasMember,
/// `ty_extensions.reveal_protocol_interface`
RevealProtocolInterface,
/// `ty_extensions.reveal_when_assignable_to`
RevealWhenAssignableTo,
/// `ty_extensions.reveal_when_subtype_of`
RevealWhenSubtypeOf,
}
impl KnownFunction {
@ -1253,6 +1257,8 @@ impl KnownFunction {
| Self::StaticAssert
| Self::HasMember
| Self::RevealProtocolInterface
| Self::RevealWhenAssignableTo
| Self::RevealWhenSubtypeOf
| Self::AllMembers => module.is_ty_extensions(),
Self::ImportModule => module.is_importlib(),
}
@ -1548,6 +1554,54 @@ impl KnownFunction {
overload.set_return_type(Type::module_literal(db, file, module));
}
KnownFunction::RevealWhenAssignableTo => {
let [Some(ty_a), Some(ty_b)] = overload.parameter_types() else {
return;
};
let constraints = ty_a.when_assignable_to::<ConstraintSet>(db, *ty_b);
let Some(builder) =
context.report_diagnostic(DiagnosticId::RevealedType, Severity::Info)
else {
return;
};
let mut diag = builder.into_diagnostic("Assignability holds");
let span = context.span(call_expression);
if constraints.is_always_satisfied(db) {
diag.annotate(Annotation::primary(span).message("always"));
} else if constraints.is_never_satisfied(db) {
diag.annotate(Annotation::primary(span).message("never"));
} else {
diag.annotate(
Annotation::primary(span)
.message(format_args!("when {}", constraints.display(db))),
);
}
}
KnownFunction::RevealWhenSubtypeOf => {
let [Some(ty_a), Some(ty_b)] = overload.parameter_types() else {
return;
};
let constraints = ty_a.when_subtype_of::<ConstraintSet>(db, *ty_b);
let Some(builder) =
context.report_diagnostic(DiagnosticId::RevealedType, Severity::Info)
else {
return;
};
let mut diag = builder.into_diagnostic("Subtyping holds");
let span = context.span(call_expression);
if constraints.is_always_satisfied(db) {
diag.annotate(Annotation::primary(span).message("always"));
} else if constraints.is_never_satisfied(db) {
diag.annotate(Annotation::primary(span).message("never"));
} else {
diag.annotate(
Annotation::primary(span)
.message(format_args!("when {}", constraints.display(db))),
);
}
}
_ => {}
}
}
@ -1608,6 +1662,8 @@ pub(crate) mod tests {
| KnownFunction::IsEquivalentTo
| KnownFunction::HasMember
| KnownFunction::RevealProtocolInterface
| KnownFunction::RevealWhenAssignableTo
| KnownFunction::RevealWhenSubtypeOf
| KnownFunction::AllMembers => KnownModule::TyExtensions,
KnownFunction::ImportModule => KnownModule::ImportLib,