diff --git a/pyrefly/lib/alt/expr.rs b/pyrefly/lib/alt/expr.rs index 9116d74def..8e4c721225 100644 --- a/pyrefly/lib/alt/expr.rs +++ b/pyrefly/lib/alt/expr.rs @@ -38,6 +38,7 @@ use pyrefly_types::shaped_array::index_shape_tensor; use pyrefly_types::shaped_array::shape_to_tuple_carrier; use pyrefly_types::shaped_array::tuple_carrier_to_shape; use pyrefly_types::shaped_array::type_to_dim; +use pyrefly_types::type_alias::TypeAliasData; use pyrefly_types::type_level_dsl::TypeShapeDslDomain; use pyrefly_types::typed_dict::AnonymousTypedDictInner; use pyrefly_types::typed_dict::ExtraItems; @@ -3133,6 +3134,28 @@ impl<'a, Ans: LookupAnswer> AnswersSolver<'a, Ans> { errors: &ErrorCollector, key_present: bool, // true if the key is definitely known to be present type_form_context: TypeFormContext<'_>, + ) -> Type { + let mut aliases = SmallSet::new(); + self.subscript_infer_for_type_with_key_present_inner( + base, + slice, + range, + errors, + key_present, + type_form_context, + &mut aliases, + ) + } + + fn subscript_infer_for_type_with_key_present_inner( + &self, + base: &Type, + slice: &Expr, + range: TextRange, + errors: &ErrorCollector, + key_present: bool, // true if the key is definitely known to be present + type_form_context: TypeFormContext<'_>, + aliases: &mut SmallSet, ) -> Type { let xs = Ast::unpack_slice(slice); let slice_ty = LazyCell::new(|| self.expr_infer(slice, errors)); @@ -3510,13 +3533,14 @@ impl<'a, Ans: LookupAnswer> AnswersSolver<'a, Ans> { } } } - let result = self.subscript_infer_for_type_with_key_present( + let result = self.subscript_infer_for_type_with_key_present_inner( &schema.underlying_type(), slice, range, errors, key_present, type_form_context, + aliases, ); // Preserve the stub's Series class when attaching an element dtype. match (column_dtype, result) { @@ -3532,45 +3556,49 @@ impl<'a, Ans: LookupAnswer> AnswersSolver<'a, Ans> { (_, result) => result, } } - Type::Series(schema) => self.subscript_infer_for_type_with_key_present( + Type::Series(schema) => self.subscript_infer_for_type_with_key_present_inner( &schema.underlying_type(), slice, range, errors, key_present, type_form_context, + aliases, ), Type::Quantified(ref q) if q.is_type_var() && q.restriction().is_restricted() => { match q.restriction() { Restriction::Bound(bound) => self - .subscript_infer_for_type_with_key_present( + .subscript_infer_for_type_with_key_present_inner( bound, slice, range, errors, key_present, type_form_context, + aliases, ), Restriction::Constraints(constraints) => { self.unions(constraints.map(|constraint| { - self.subscript_infer_for_type_with_key_present( + self.subscript_infer_for_type_with_key_present_inner( constraint, slice, range, errors, key_present, type_form_context, + aliases, ) })) } Restriction::Flag(domain) => self - .subscript_infer_for_type_with_key_present( + .subscript_infer_for_type_with_key_present_inner( &domain.as_type(self.stdlib, self.heap), slice, range, errors, key_present, type_form_context, + aliases, ), Restriction::Unrestricted => { unreachable!("restricted TypeVar cannot be unrestricted") @@ -3657,14 +3685,26 @@ impl<'a, Ans: LookupAnswer> AnswersSolver<'a, Ans> { } }) } - Type::UntypedAlias(ta) => self.subscript_infer_for_type_with_key_present( - &self.untype_alias(&ta), - slice, - range, - errors, - key_present, - type_form_context, - ), + Type::UntypedAlias(ta) => { + // Recursive aliases can contain a mapping whose value is the alias itself. + // A subscript on that value cannot be made more precise without expanding + // the alias again, so stop the cycle with an implicit Any. + if !aliases.insert((*ta).clone()) { + self.heap.mk_any_implicit() + } else { + let result = self.subscript_infer_for_type_with_key_present_inner( + &self.untype_alias(&ta), + slice, + range, + errors, + key_present, + type_form_context, + aliases, + ); + aliases.shift_remove(&*ta); + result + } + } t => self.error( errors, range, diff --git a/pyrefly/lib/test/pattern_match.rs b/pyrefly/lib/test/pattern_match.rs index 360cd3a0c5..72348c8093 100644 --- a/pyrefly/lib/test/pattern_match.rs +++ b/pyrefly/lib/test/pattern_match.rs @@ -59,6 +59,22 @@ match None: # E: Missing cases: None "#, ); +// Regression test for https://github.com/facebook/pyrefly/issues/4631. +testcase!( + test_recursive_alias_mapping_pattern_does_not_overflow, + r#" +from typing import Mapping + +def fn(obj: U): + match obj: + case {'a': {'b': []}}: + pass + +T = 'T' | str | Mapping[str, 'T'] # E: `|` union syntax does not work with string literals # E: Found cyclic self-reference in `T` +U = Mapping[str, T] +"#, +); + testcase!( test_match_case_unreachable_for_disjoint_subject_type, r#"