[red knot] Minor follow-up tasks regarding singleton types (#13769)

## Summary

- Do not treat empty tuples as singletons after discussion [1]
- Improve comment regarding intersection types
- Resolve unnecessary TODO in Markdown test

[1]
https://discuss.python.org/t/should-we-specify-in-the-language-reference-that-the-empty-tuple-is-a-singleton/67957

## Test Plan

—
This commit is contained in:
David Peter 2024-10-16 11:30:03 +02:00 committed by GitHub
parent fb1d1e3241
commit b85be6297e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 20 additions and 23 deletions

View file

@ -31,10 +31,9 @@ non-singleton class may occupy different addresses in memory even if
they compare equal. they compare equal.
```py ```py
x = [1] x = 345
y = [1] y = 345
if x is not y: if x is not y:
# TODO: should include type parameter: list[int] reveal_type(x) # revealed: Literal[345]
reveal_type(x) # revealed: list
``` ```

View file

@ -467,7 +467,7 @@ impl<'db> Type<'db> {
/// ///
/// Note: This function aims to have no false positives, but might return `false` /// Note: This function aims to have no false positives, but might return `false`
/// for more complicated types that are actually singletons. /// for more complicated types that are actually singletons.
pub(crate) fn is_singleton(self, db: &'db dyn Db) -> bool { pub(crate) fn is_singleton(self) -> bool {
match self { match self {
Type::Any Type::Any
| Type::Never | Type::Never
@ -485,14 +485,13 @@ impl<'db> Type<'db> {
false false
} }
Type::None | Type::BooleanLiteral(_) | Type::Function(..) | Type::Class(..) | Type::Module(..) => true, Type::None | Type::BooleanLiteral(_) | Type::Function(..) | Type::Class(..) | Type::Module(..) => true,
Type::Tuple(tuple) => { Type::Tuple(..) => {
// We deliberately deviate from the language specification [1] here and claim // The empty tuple is a singleton on CPython and PyPy, but not on other Python
// that the empty tuple type is a singleton type. The reasoning is that `()` // implementations such as GraalPy. Its *use* as a singleton is discouraged and
// is often used as a sentinel value in user code. Declaring the empty tuple to // should not be relied on for type narrowing, so we do not treat it as one.
// be of singleton type allows us to narrow types in `is not ()` conditionals. // See:
// // https://docs.python.org/3/reference/expressions.html#parenthesized-forms
// [1] https://docs.python.org/3/reference/expressions.html#parenthesized-forms false
tuple.elements(db).is_empty()
} }
Type::Union(..) => { Type::Union(..) => {
// A single-element union, where the sole element was a singleton, would itself // A single-element union, where the sole element was a singleton, would itself
@ -501,13 +500,12 @@ impl<'db> Type<'db> {
false false
} }
Type::Intersection(..) => { Type::Intersection(..) => {
// Intersection types are hard to analyze. The following types are technically // Here, we assume that all intersection types that are singletons would have
// all singleton types, but it is not straightforward to compute this. Again, // been reduced to a different form via [`IntersectionBuilder::build`] by now.
// we simply return false. // For example:
// //
// bool & ~Literal[False]` // bool & ~Literal[False] = Literal[True]
// None & (None | int) // None & (None | int) = None | None & int = None
// (A | B) & (B | C) with A, B, C disjunct and B a singleton
// //
false false
} }
@ -1682,23 +1680,23 @@ mod tests {
#[test_case(Ty::None)] #[test_case(Ty::None)]
#[test_case(Ty::BoolLiteral(true))] #[test_case(Ty::BoolLiteral(true))]
#[test_case(Ty::BoolLiteral(false))] #[test_case(Ty::BoolLiteral(false))]
#[test_case(Ty::Tuple(vec![]))]
fn is_singleton(from: Ty) { fn is_singleton(from: Ty) {
let db = setup_db(); let db = setup_db();
assert!(from.into_type(&db).is_singleton(&db)); assert!(from.into_type(&db).is_singleton());
} }
#[test_case(Ty::Never)] #[test_case(Ty::Never)]
#[test_case(Ty::IntLiteral(345))] #[test_case(Ty::IntLiteral(345))]
#[test_case(Ty::BuiltinInstance("str"))] #[test_case(Ty::BuiltinInstance("str"))]
#[test_case(Ty::Union(vec![Ty::IntLiteral(1), Ty::IntLiteral(2)]))] #[test_case(Ty::Union(vec![Ty::IntLiteral(1), Ty::IntLiteral(2)]))]
#[test_case(Ty::Tuple(vec![]))]
#[test_case(Ty::Tuple(vec![Ty::None]))] #[test_case(Ty::Tuple(vec![Ty::None]))]
#[test_case(Ty::Tuple(vec![Ty::None, Ty::BoolLiteral(true)]))] #[test_case(Ty::Tuple(vec![Ty::None, Ty::BoolLiteral(true)]))]
fn is_not_singleton(from: Ty) { fn is_not_singleton(from: Ty) {
let db = setup_db(); let db = setup_db();
assert!(!from.into_type(&db).is_singleton(&db)); assert!(!from.into_type(&db).is_singleton());
} }
#[test_case(Ty::IntLiteral(1); "is_int_literal_truthy")] #[test_case(Ty::IntLiteral(1); "is_int_literal_truthy")]

View file

@ -157,7 +157,7 @@ impl<'db> NarrowingConstraintsBuilder<'db> {
let comp_ty = inference.expression_ty(comparator.scoped_ast_id(self.db, scope)); let comp_ty = inference.expression_ty(comparator.scoped_ast_id(self.db, scope));
match op { match op {
ast::CmpOp::IsNot => { ast::CmpOp::IsNot => {
if comp_ty.is_singleton(self.db) { if comp_ty.is_singleton() {
let ty = IntersectionBuilder::new(self.db) let ty = IntersectionBuilder::new(self.db)
.add_negative(comp_ty) .add_negative(comp_ty)
.build(); .build();