From 23133f70b4c9d4a2c3a2ca34221799f0129154c9 Mon Sep 17 00:00:00 2001 From: Kavindu Sachinthe Date: Sun, 23 Aug 2026 17:37:52 +0530 Subject: [PATCH] Fix stack overflow during subscript inference on recursive type aliases Recursive type aliases involved in nested mapping patterns (such as pattern matching on nested dictionary structures) could trigger recursive subscript inferences without termination, causing a stack overflow. Track active type aliases during subscript inference using a cycle-detection set. When an alias is encountered again on the active subscript path, terminate recursion by returning an implicit Any type. --- pyrefly/lib/alt/expr.rs | 66 +++++++++++++++++++++++++------ pyrefly/lib/test/pattern_match.rs | 16 ++++++++ 2 files changed, 69 insertions(+), 13 deletions(-) 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#"