Skip to content

Commit ba3e523

Browse files
Fix stack overflow with mutually recursive type aliases
Summary: Fixes #2982 Cyclic type aliases like type T = U; type U = T were correctly detected and reported as errors, but the cyclic UntypedAlias(Ref(...)) was still emitted as the alias's type. Downstream operations (attribute lookup, subset checks) would then infinitely bounce between the two refs, overflowing the stack. Fix: when check_type_alias_for_cyclic_reference finds a cycle, short-circuit wrap_type_alias to return an error type instead of the cyclic body. {F1987625179} Reviewed By: rchen152 Differential Revision: D99314495 fbshipit-source-id: 87937d7fca8c556eaadb641160a23cfc63b0a5a8
1 parent c08dfa0 commit ba3e523

2 files changed

Lines changed: 24 additions & 3 deletions

File tree

pyrefly/lib/alt/solve.rs

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1252,13 +1252,14 @@ impl<'a, Ans: LookupAnswer> AnswersSolver<'a, Ans> {
12521252
/// user-defined `class C[T]: x: T | None` makes `type A = C[A]`
12531253
/// inhabitable (e.g. `C(x=C(x=None))`), so we can't assume all generic
12541254
/// containers require their type parameter.
1255+
/// Returns `true` if a cyclic self-reference was found.
12551256
fn check_type_alias_for_cyclic_reference(
12561257
&self,
12571258
name: &Name,
12581259
ta: &TypeAlias,
12591260
range: TextRange,
12601261
errors: &ErrorCollector,
1261-
) {
1262+
) -> bool {
12621263
// Unwrap the type[body] wrapper. We operate on the inner body because
12631264
// map_over_union wraps inner union members in type[...] when traversing
12641265
// inside Type::Type, which would prevent matching UntypedAlias nodes.
@@ -1267,7 +1268,7 @@ impl<'a, Ans: LookupAnswer> AnswersSolver<'a, Ans> {
12671268
let ty = ta.as_type();
12681269
let body = match &ty {
12691270
Type::Type(inner) => inner.as_ref(),
1270-
_ => return,
1271+
_ => return false,
12711272
};
12721273
let is_self_ref = |ty: &Type| matches!(ty, Type::UntypedAlias(ta) if ta.name() == name);
12731274

@@ -1338,7 +1339,9 @@ impl<'a, Ans: LookupAnswer> AnswersSolver<'a, Ans> {
13381339
ErrorInfo::Kind(ErrorKind::InvalidTypeAlias),
13391340
format!("Found cyclic self-reference in `{name}`"),
13401341
);
1342+
return true;
13411343
}
1344+
false
13421345
}
13431346

13441347
/// `typealiastype_tparams` refers specifically to the elements of the tuple literal passed to the `TypeAliasType` constructor
@@ -1371,7 +1374,12 @@ impl<'a, Ans: LookupAnswer> AnswersSolver<'a, Ans> {
13711374
}
13721375

13731376
// Step 2: Check for cyclic self-references after expansion.
1374-
self.check_type_alias_for_cyclic_reference(name, &ta, range, errors);
1377+
// If a cycle is found, replace the body with an error type to prevent
1378+
// infinite recursion when downstream operations (e.g. attribute lookup,
1379+
// subset checks) try to resolve the alias.
1380+
if self.check_type_alias_for_cyclic_reference(name, &ta, range, errors) {
1381+
return self.heap.mk_any_error();
1382+
}
13751383

13761384
// Step 3: Extract type parameters from the (now expanded) body.
13771385
let mut seen_type_vars = SmallMap::new();

pyrefly/lib/test/recursive_alias.rs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -258,6 +258,19 @@ type Z = int | Y # E: cyclic self-reference in `Z`
258258
"#,
259259
);
260260

261+
testcase!(
262+
test_cyclic_mutual_usage,
263+
r#"
264+
# Mutually recursive type aliases with no base case should not stack overflow
265+
# when the aliases are used in expressions.
266+
type T = U # E: cyclic self-reference in `T`
267+
type U = T # E: cyclic self-reference in `U`
268+
269+
x: T = 1
270+
not x
271+
"#,
272+
);
273+
261274
testcase!(
262275
test_cyclic_no_base_case,
263276
r#"

0 commit comments

Comments
 (0)