Skip to content

Conversation

@dhruvmanila
Copy link
Member

@dhruvmanila dhruvmanila commented Jun 9, 2025

Summary

This is to support #18607.

This PR adds support for generating the top materialization (or upper bound materialization) and the bottom materialization (or lower bound materialization) of a type. This is the most general and the most specific form of the type which is fully static, respectively.

More concretely, T', the top materialization of T, is the type T with all occurrences
of dynamic type (Any, Unknown, @Todo) replaced as follows:

  • In covariant position, it's replaced with object
  • In contravariant position, it's replaced with Never
  • In invariant position, it's replaced with an unresolved type variable

(For an invariant position, it should actually be replaced with an existential type, but this is not currently representable in our type system, so we use an unresolved type variable for now instead.)

The bottom materialization is implemented in the same way, except we start out in "contravariant" position.

Test Plan

Add test cases for various types.

@dhruvmanila dhruvmanila added the ty Multi-file analysis & type inference label Jun 9, 2025
@github-actions
Copy link
Contributor

github-actions bot commented Jun 9, 2025

mypy_primer results

No ecosystem changes detected ✅

@dhruvmanila dhruvmanila force-pushed the dhruv/top-materialization branch from e4abd62 to 7342d3c Compare June 10, 2025 03:30
@dhruvmanila dhruvmanila marked this pull request as ready for review June 10, 2025 10:03
Copy link
Contributor

@carljm carljm left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is looking good! I think the main thing is we need to ensure nesting is right, by always flipping the polarity of the "outer" variance when we hit a contravariant context, not hardcoding variance.

Another observation here is that the implementation of top_materialization and bottom_materialization should be identical -- the only difference is whether we start with a variance of "Covariant" (top materialization) or start with a variance of "Contravariant" (bottom materialization). It might make sense to rename the method added in the current PR to just materialize, and then have Type::top_materialization and Type::bottom_materialization just be thin wrappers that call materialize with the right initial variance.

@dhruvmanila dhruvmanila changed the title [ty] Generate the top materialization of a type [ty] Generate the top and bottom materialization of a type Jun 11, 2025
@dhruvmanila dhruvmanila force-pushed the dhruv/top-materialization branch from 77d8c4b to 4a9739f Compare June 11, 2025 09:57
@dhruvmanila
Copy link
Member Author

dhruvmanila commented Jun 11, 2025

Post review changes

  • Add top_materialization, bottom_materialization, and a common materialize methods on Type
  • Add TypeVarVariance::flip method that flips the polarity of the variance
  • Apply the flip in negation type and callable parameter type
  • Update SubclassOf to use variance and return Never for when contravariant and type for other variance
  • Expanded the test suite to check bottom_materialization as well
  • Add property tests, it's failing and I've a couple of questions regarding that I intend to ask in our 1:1

Edit:

  • Change SubclassOf to use T: type when it's used in an invariant position

@dhruvmanila dhruvmanila requested a review from carljm June 11, 2025 16:23
* Rename `T` to `T_all`
* Pass in the surrounding context for materializing the specialization
* Update test suite for the above and add additional test using callable
* Add TODO for materializing generic protocol class
Copy link
Contributor

@carljm carljm left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks great!

@dhruvmanila dhruvmanila merged commit ef4108a into main Jun 12, 2025
35 checks passed
@dhruvmanila dhruvmanila deleted the dhruv/top-materialization branch June 12, 2025 06:36
dcreager added a commit that referenced this pull request Jun 12, 2025
* main:
  [ty] Add some "inside string" tests for `object.<CURSOR>` completions
  [ty] Pull types on synthesized Python files created by mdtest (#18539)
  Update Rust crate anstyle to v1.0.11 (#18583)
  [`pyupgrade`] Fix `super(__class__, self)` detection in UP008 (super-call-with-parameters) (#18478)
  [ty] Generate the top and bottom materialization of a type (#18594)
  `SourceOrderVisitor` should visit the `Identifier` part of the `PatternKeyword` node (#18635)
  Update salsa (#18636)
  [ty] Update mypy_primer doc (#18638)
  [ty] Improve support for `object.<CURSOR>` completions
  [ty] Add `CoveringNode::find_last`
  [ty] Refactor covering node representation
  [ty] Infer the Python version from `--python=<system installation>` on Unix (#18550)
  [`flake8-return`] Fix `RET504` autofix generating a syntax error (#18428)
  Fix incorrect salsa `return_ref` attribute (#18605)
  Move corpus tests to `ty_python_semantic` (#18609)
  [`pyupgrade`] Don't offer fix for `Optional[None]` in non-pep604-annotation-optional (`UP045)` or non-pep604-annotation-union (`UP007`) (#18545)
  [`pep8-naming`] Suppress fix for `N804` and `N805` if the recommend name is already used (#18472)
  [`ruff`] skip fix for `RUF059` if dummy name is already bound (unused-unpacked-variable) (#18509)
Type::Callable(callable_type) => {
Type::Callable(callable_type.materialize(db, variance))
}
Type::SubclassOf(subclass_of_type) => subclass_of_type.materialize(db, variance),

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just snooping around here but is this missing a Type::SubclassOf in the return value?

Copy link
Member

@AlexWaygood AlexWaygood Jun 17, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nah, the function here delegates to SubclassOfType::materialize(), which does work to figure out which Type variant it needs to return (it won't always be a Type::SubclassOf type due to some eager normalizations we do):

pub(super) fn materialize(self, db: &'db dyn Db, variance: TypeVarVariance) -> Type<'db> {
match self.subclass_of {
SubclassOfInner::Dynamic(_) => match variance {
TypeVarVariance::Covariant => KnownClass::Type.to_instance(db),
TypeVarVariance::Contravariant => Type::Never,
TypeVarVariance::Invariant => {
// We need to materialize this to `type[T]` but that isn't representable so
// we instead use a type variable with an upper bound of `type`.
Type::TypeVar(TypeVarInstance::new(
db,
Name::new_static("T_all"),
None,
Some(TypeVarBoundOrConstraints::UpperBound(
KnownClass::Type.to_instance(db),
)),
variance,
None,
TypeVarKind::Pep695,
))
}
TypeVarVariance::Bivariant => unreachable!(),
},
SubclassOfInner::Class(_) => Type::SubclassOf(self),
}
}

Copy link
Contributor

@carljm carljm Jun 17, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, but this is subtle and probably should have a comment. SubclassOfType::materialize returns a Type for the overall materialization of the subclass-of type, not just for the inner type. The reason for this is that we don't currently have a direct representation for type[T] (that is, subclass-of a typevar), but the materialization of type[Any] in an invariant position should be type[T] where T has no upper bound or constraints, and it turns out that this is equivalent to just T' where T': type -- so that's how we represent this type[T] we need to synthesize.

I think I did just spot another bug in looking at SubclassOfType::materialize, though. That method currently acts as if type[...] itself were bivariant (imposed no variance constraints). But in fact it is covariant. So both the Contravariant and Invariant branches there should be returning the type variable. EDIT: never mind, this is wrong -- I was thinking in terms of collecting constraints in variance inference, but here we are just flipping variance, not collecting constraints. So type[] being covariant just means it doesn't change the outer variance context.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ty Multi-file analysis & type inference

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants