Skip to content

[ty] Avoid stack overflow with recursive typevar#23652

Merged
charliermarsh merged 6 commits intomainfrom
charlie/self-recurse
Mar 4, 2026
Merged

[ty] Avoid stack overflow with recursive typevar#23652
charliermarsh merged 6 commits intomainfrom
charlie/self-recurse

Conversation

@charliermarsh
Copy link
Copy Markdown
Member

Summary

Use a syntactic check rather than eagerly resolving the type.

Closes astral-sh/ty#2889

@charliermarsh charliermarsh added bug Something isn't working ty Multi-file analysis & type inference labels Mar 2, 2026
@astral-sh-bot
Copy link
Copy Markdown

astral-sh-bot Bot commented Mar 2, 2026

Typing conformance results

No changes detected ✅

@charliermarsh charliermarsh force-pushed the charlie/self-recurse branch from d971798 to 1c93d1b Compare March 2, 2026 01:30
@astral-sh-bot
Copy link
Copy Markdown

astral-sh-bot Bot commented Mar 2, 2026

mypy_primer results

Changes were detected when running on open source projects
spack (https://github.com/spack/spack)
- lib/spack/spack/test/conftest.py:1918:16: error[unresolved-attribute] Attribute `get` is not defined on `None` in union `Element[str] | None`
+ lib/spack/spack/test/conftest.py:1918:16: error[unresolved-attribute] Attribute `get` is not defined on `None` in union `Element[Unknown] | None`

meson (https://github.com/mesonbuild/meson)
- mesonbuild/mtest.py:1119:17: error[invalid-assignment] Object of type `ElementTree[Element[str]]` is not assignable to attribute `junit` of type `ElementTree[Element[str] | None] | None`
+ mesonbuild/mtest.py:1119:17: error[invalid-assignment] Object of type `ElementTree[Element[Unknown]]` is not assignable to attribute `junit` of type `ElementTree[Element[Unknown] | None] | None`

cloud-init (https://github.com/canonical/cloud-init)
- cloudinit/sources/helpers/azure.py:518:32: error[unresolved-attribute] Attribute `text` is not defined on `None` in union `Element[str] | None`
+ cloudinit/sources/helpers/azure.py:518:32: error[unresolved-attribute] Attribute `text` is not defined on `None` in union `Element[Unknown] | None`

scikit-build-core (https://github.com/scikit-build/scikit-build-core)
- src/scikit_build_core/build/wheel.py:99:20: error[no-matching-overload] No overload of bound method `__init__` matches arguments
- Found 58 diagnostics
+ Found 57 diagnostics

dd-trace-py (https://github.com/DataDog/dd-trace-py)
- tests/contrib/pytest/test_pytest_atr.py:337:16: error[unresolved-attribute] Attribute `attrib` is not defined on `None` in union `Element[str] | None`
+ tests/contrib/pytest/test_pytest_atr.py:337:16: error[unresolved-attribute] Attribute `attrib` is not defined on `None` in union `Element[Unknown] | None`
- tests/contrib/pytest/test_pytest_atr.py:338:16: error[unresolved-attribute] Attribute `attrib` is not defined on `None` in union `Element[str] | None`
+ tests/contrib/pytest/test_pytest_atr.py:338:16: error[unresolved-attribute] Attribute `attrib` is not defined on `None` in union `Element[Unknown] | None`
- tests/contrib/pytest/test_pytest_atr.py:339:16: error[unresolved-attribute] Attribute `attrib` is not defined on `None` in union `Element[str] | None`
+ tests/contrib/pytest/test_pytest_atr.py:339:16: error[unresolved-attribute] Attribute `attrib` is not defined on `None` in union `Element[Unknown] | None`
- tests/contrib/pytest/test_pytest_atr.py:340:16: error[unresolved-attribute] Attribute `attrib` is not defined on `None` in union `Element[str] | None`
+ tests/contrib/pytest/test_pytest_atr.py:340:16: error[unresolved-attribute] Attribute `attrib` is not defined on `None` in union `Element[Unknown] | None`
- tests/contrib/pytest/test_pytest_attempt_to_fix.py:259:16: error[unresolved-attribute] Attribute `attrib` is not defined on `None` in union `Element[str] | None`
+ tests/contrib/pytest/test_pytest_attempt_to_fix.py:259:16: error[unresolved-attribute] Attribute `attrib` is not defined on `None` in union `Element[Unknown] | None`
- tests/contrib/pytest/test_pytest_attempt_to_fix.py:260:16: error[unresolved-attribute] Attribute `attrib` is not defined on `None` in union `Element[str] | None`
+ tests/contrib/pytest/test_pytest_attempt_to_fix.py:260:16: error[unresolved-attribute] Attribute `attrib` is not defined on `None` in union `Element[Unknown] | None`
- tests/contrib/pytest/test_pytest_attempt_to_fix.py:261:16: error[unresolved-attribute] Attribute `attrib` is not defined on `None` in union `Element[str] | None`
+ tests/contrib/pytest/test_pytest_attempt_to_fix.py:261:16: error[unresolved-attribute] Attribute `attrib` is not defined on `None` in union `Element[Unknown] | None`
- tests/contrib/pytest/test_pytest_attempt_to_fix.py:262:16: error[unresolved-attribute] Attribute `attrib` is not defined on `None` in union `Element[str] | None`
+ tests/contrib/pytest/test_pytest_attempt_to_fix.py:262:16: error[unresolved-attribute] Attribute `attrib` is not defined on `None` in union `Element[Unknown] | None`
- tests/contrib/pytest/test_pytest_attempt_to_fix.py:271:16: error[unresolved-attribute] Attribute `attrib` is not defined on `None` in union `Element[str] | None`
+ tests/contrib/pytest/test_pytest_attempt_to_fix.py:271:16: error[unresolved-attribute] Attribute `attrib` is not defined on `None` in union `Element[Unknown] | None`
- tests/contrib/pytest/test_pytest_attempt_to_fix.py:272:16: error[unresolved-attribute] Attribute `attrib` is not defined on `None` in union `Element[str] | None`
+ tests/contrib/pytest/test_pytest_attempt_to_fix.py:272:16: error[unresolved-attribute] Attribute `attrib` is not defined on `None` in union `Element[Unknown] | None`
- tests/contrib/pytest/test_pytest_attempt_to_fix.py:273:16: error[unresolved-attribute] Attribute `attrib` is not defined on `None` in union `Element[str] | None`
+ tests/contrib/pytest/test_pytest_attempt_to_fix.py:273:16: error[unresolved-attribute] Attribute `attrib` is not defined on `None` in union `Element[Unknown] | None`
- tests/contrib/pytest/test_pytest_attempt_to_fix.py:274:16: error[unresolved-attribute] Attribute `attrib` is not defined on `None` in union `Element[str] | None`
+ tests/contrib/pytest/test_pytest_attempt_to_fix.py:274:16: error[unresolved-attribute] Attribute `attrib` is not defined on `None` in union `Element[Unknown] | None`
- tests/contrib/pytest/test_pytest_efd.py:515:16: error[unresolved-attribute] Attribute `attrib` is not defined on `None` in union `Element[str] | None`
+ tests/contrib/pytest/test_pytest_efd.py:515:16: error[unresolved-attribute] Attribute `attrib` is not defined on `None` in union `Element[Unknown] | None`
- tests/contrib/pytest/test_pytest_efd.py:516:16: error[unresolved-attribute] Attribute `attrib` is not defined on `None` in union `Element[str] | None`
+ tests/contrib/pytest/test_pytest_efd.py:516:16: error[unresolved-attribute] Attribute `attrib` is not defined on `None` in union `Element[Unknown] | None`

zulip (https://github.com/zulip/zulip)
- zerver/lib/markdown/nested_code_blocks.py:51:21: error[unresolved-attribute] Attribute `tag` is not defined on `None` in union `Element[str] | None`
+ zerver/lib/markdown/nested_code_blocks.py:51:21: error[unresolved-attribute] Attribute `tag` is not defined on `None` in union `Element[Unknown] | None`

@astral-sh-bot
Copy link
Copy Markdown

astral-sh-bot Bot commented Mar 2, 2026

Memory usage report

Summary

Project Old New Diff Outcome
prefect 692.25MB 693.04MB +0.11% (814.21kB)
sphinx 266.74MB 266.92MB +0.07% (188.02kB)
trio 118.46MB 118.51MB +0.04% (54.07kB)
flake8 48.07MB 48.10MB +0.05% (22.82kB)

Significant changes

Click to expand detailed breakdown

prefect

Name Old New Diff Outcome
infer_definition_types 87.39MB 87.62MB +0.27% (241.16kB)
infer_expression_types_impl 59.67MB 59.86MB +0.31% (191.74kB)
infer_deferred_types 14.17MB 14.24MB +0.50% (72.94kB)
infer_scope_types_impl 51.65MB 51.71MB +0.12% (65.93kB)
is_redundant_with_impl 5.60MB 5.66MB +0.96% (55.08kB)
infer_expression_type_impl 14.17MB 14.21MB +0.28% (40.73kB)
bound_typevar_default_type 0.00B 28.52kB +28.52kB (new)
StaticClassLiteral<'db>::implicit_attribute_inner_ 9.66MB 9.68MB +0.24% (23.44kB)
all_narrowing_constraints_for_expression 6.85MB 6.87MB +0.24% (17.07kB)
FunctionType<'db>::signature_ 3.84MB 3.86MB +0.35% (13.76kB)
FunctionType<'db>::last_definition_signature_ 753.94kB 765.71kB +1.56% (11.78kB)
all_negative_narrowing_constraints_for_expression 2.57MB 2.58MB +0.35% (9.12kB)
Type<'db>::member_lookup_with_policy_ 15.19MB 15.20MB +0.06% (9.01kB)
FunctionType<'db>::last_definition_raw_signature_ 3.16MB 3.17MB +0.23% (7.58kB)
Type<'db>::try_call_dunder_get_ 10.26MB 10.26MB +0.05% (5.39kB)
... 20 more

sphinx

Name Old New Diff Outcome
infer_expression_types_impl 21.79MB 21.84MB +0.22% (50.05kB)
infer_definition_types 24.26MB 24.30MB +0.17% (42.36kB)
infer_deferred_types 5.62MB 5.65MB +0.60% (34.29kB)
bound_typevar_default_type 0.00B 16.22kB +16.22kB (new)
infer_scope_types_impl 15.62MB 15.63MB +0.08% (13.14kB)
is_redundant_with_impl 1.84MB 1.86MB +0.68% (12.76kB)
Type<'db>::try_call_dunder_get_ 4.96MB 4.97MB +0.16% (7.90kB)
FunctionType<'db>::signature_ 2.30MB 2.30MB +0.12% (2.73kB)
FunctionType<'db>::last_definition_signature_ 226.48kB 229.07kB +1.14% (2.59kB)
Type<'db>::class_member_with_policy_ 7.60MB 7.59MB -0.02% (1.77kB)
StaticClassLiteral<'db>::try_mro_ 2.18MB 2.18MB +0.08% (1.75kB)
StaticClassLiteral<'db>::is_typed_dict_ 182.61kB 184.05kB +0.79% (1.44kB)
all_narrowing_constraints_for_expression 2.36MB 2.36MB +0.06% (1.39kB)
code_generator_of_static_class 534.80kB 535.90kB +0.21% (1.10kB)
enum_metadata 737.94kB 738.93kB +0.13% (1020.00B)
... 26 more

trio

Name Old New Diff Outcome
bound_typevar_default_type 0.00B 14.75kB +14.75kB (new)
infer_deferred_types 2.36MB 2.37MB +0.41% (9.94kB)
infer_expression_types_impl 7.15MB 7.15MB +0.07% (5.07kB)
infer_scope_types_impl 4.79MB 4.80MB +0.10% (4.85kB)
infer_definition_types 7.62MB 7.63MB +0.05% (3.83kB)
FunctionType<'db>::signature_ 1.07MB 1.08MB +0.31% (3.39kB)
is_redundant_with_impl 496.30kB 499.46kB +0.64% (3.16kB)
FunctionType<'db>::last_definition_signature_ 242.25kB 245.38kB +1.29% (3.13kB)
Type<'db>::try_call_dunder_get_ 1.39MB 1.39MB +0.12% (1.70kB)
StaticClassLiteral<'db>::try_mro_ 895.07kB 896.73kB +0.19% (1.66kB)
StaticClassLiteral<'db>::is_typed_dict_ 65.84kB 67.30kB +2.21% (1.45kB)
code_generator_of_static_class 212.95kB 214.24kB +0.61% (1.29kB)
enum_metadata 238.44kB 239.44kB +0.42% (1020.00B)
FunctionType<'db>::last_definition_raw_signature_ 600.76kB 601.71kB +0.16% (972.00B)
infer_expression_type_impl 1.49MB 1.49MB -0.05% (804.00B)
... 12 more

flake8

Name Old New Diff Outcome
bound_typevar_default_type 0.00B 6.79kB +6.79kB (new)
infer_deferred_types 690.71kB 695.06kB +0.63% (4.35kB)
infer_definition_types 1.88MB 1.88MB +0.18% (3.48kB)
infer_expression_types_impl 1.07MB 1.07MB +0.18% (2.03kB)
infer_scope_types_impl 1006.95kB 1008.01kB +0.11% (1.07kB)
is_redundant_with_impl 150.35kB 151.28kB +0.62% (948.00B)
FunctionType<'db>::signature_ 361.54kB 362.38kB +0.23% (852.00B)
FunctionType<'db>::last_definition_signature_ 59.29kB 60.05kB +1.28% (780.00B)
StaticClassLiteral<'db>::try_mro_ 346.35kB 347.10kB +0.22% (768.00B)
StaticClassLiteral<'db>::is_typed_dict_ 27.98kB 28.64kB +2.39% (684.00B)
Type<'db>::try_call_dunder_get_ 377.23kB 377.83kB +0.16% (612.00B)
code_generator_of_static_class 60.97kB 61.30kB +0.54% (336.00B)
Type<'db>::class_member_with_policy_ 554.27kB 554.08kB -0.03% (192.00B)
enum_metadata 65.85kB 66.04kB +0.28% (192.00B)
all_negative_narrowing_constraints_for_expression 40.73kB 40.79kB +0.14% (60.00B)
... 7 more

@charliermarsh charliermarsh marked this pull request as ready for review March 2, 2026 01:38
Copy link
Copy Markdown
Member

@AlexWaygood AlexWaygood left a comment

Choose a reason for hiding this comment

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

This fixes the issue, but it feels like it's patching a symptom rather than addressing the underlying cause. TypeVarInstance::default_type is already a wrapper around the low-level field _default_type that's stored directly on the Salsa struct. It feels like we should be adding cycle handling to TypeVarInstance::default_type, since apparently any access of t.default_type(db) is enough to trigger infinite recursion currently, given a sufficiently pathological typevar?

You can take a look at the BinaryComparisonVisitor, and the way we thread that through infer_binary_type_comparison, for an example of how we might add cycle detection to lazy_default

@charliermarsh charliermarsh marked this pull request as draft March 2, 2026 14:47
@charliermarsh
Copy link
Copy Markdown
Member Author

It's proving to be harder to do that here because default_type is called from many disparate sites...

@AlexWaygood
Copy link
Copy Markdown
Member

can you add an inner function to default_type that passes the cycle visitor through, and keep the signature of default_type the same...?

@AlexWaygood AlexWaygood assigned AlexWaygood and unassigned dcreager Mar 2, 2026
@charliermarsh charliermarsh force-pushed the charlie/self-recurse branch from e04f35d to 96e2835 Compare March 3, 2026 00:05
@charliermarsh charliermarsh marked this pull request as ready for review March 3, 2026 01:00
Comment thread crates/ty_python_semantic/src/types.rs Outdated
Comment on lines +7241 to +7246
{
let mut seen_typevars = seen_typevars.borrow_mut();
if !seen_typevars.insert(typevar) {
return false;
}
}
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

can be a bit simpler

Suggested change
{
let mut seen_typevars = seen_typevars.borrow_mut();
if !seen_typevars.insert(typevar) {
return false;
}
}
if !seen_typevars.borrow_mut().insert(typevar) {
return false;
}

(I'm still working through the rest of the PR, just posting this now so I don't forget about it...)

Comment thread crates/ty_python_semantic/src/types.rs Outdated
Comment on lines +7270 to +7275
{
let mut seen_type_aliases = seen_type_aliases.borrow_mut();
if !seen_type_aliases.insert(type_alias) {
return false;
}
}
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Suggested change
{
let mut seen_type_aliases = seen_type_aliases.borrow_mut();
if !seen_type_aliases.insert(type_alias) {
return false;
}
}
if !seen_type_aliases.borrow_mut().insert(type_alias) {
return false;
}

@AlexWaygood
Copy link
Copy Markdown
Member

I think you can improve readability for the type_is_self_referential method by creating a State struct that holds the common variables we're passing around everywhere, e.g.

    fn type_is_self_referential(
        self,
        db: &'db dyn Db,
        ty: Type<'db>,
        visitor: &TypeVarDefaultVisitor<'db>,
    ) -> bool {
        #[derive(Copy, Clone)]
        struct State<'db, 'a> {
            db: &'db dyn Db,
            visitor: &'a TypeVarDefaultVisitor<'db>,
            seen_typevars: &'a RefCell<FxHashSet<TypeVarInstance<'db>>>,
            seen_type_aliases: &'a RefCell<FxHashSet<TypeAliasType<'db>>>,
        }

        fn typevar_default_is_self_referential<'db>(
            state: State<'db, '_>,
            typevar: TypeVarInstance<'db>,
            self_identity: TypeVarIdentity<'db>,
        ) -> bool {
            if typevar.identity(state.db) == self_identity {
                return true;
            }

            if !state.seen_typevars.borrow_mut().insert(typevar) {
                return false;
            }

            typevar
                .default_type_impl(state.db, state.visitor)
                .is_some_and(|default_ty| {
                    type_is_self_referential_impl(state, default_ty, self_identity)
                })
        }

        fn type_alias_is_self_referential<'db>(
            state: State<'db, '_>,
            type_alias: TypeAliasType<'db>,
            self_identity: TypeVarIdentity<'db>,
        ) -> bool {
            if !state.seen_type_aliases.borrow_mut().insert(type_alias) {
                return false;
            }

            type_is_self_referential_impl(state, type_alias.raw_value_type(state.db), self_identity)
        }

        fn type_is_self_referential_impl<'db>(
            state: State<'db, '_>,
            ty: Type<'db>,
            self_identity: TypeVarIdentity<'db>,
        ) -> bool {
            any_over_type(state.db, ty, false, |inner_ty| match inner_ty {
                Type::TypeVar(bound_typevar) => typevar_default_is_self_referential(
                    state,
                    bound_typevar.typevar(state.db),
                    self_identity,
                ),
                Type::KnownInstance(KnownInstanceType::TypeVar(typevar)) => {
                    typevar_default_is_self_referential(state, typevar, self_identity)
                }
                Type::TypeAlias(alias) => {
                    type_alias_is_self_referential(state, alias, self_identity)
                }
                Type::KnownInstance(KnownInstanceType::TypeAliasType(alias)) => {
                    type_alias_is_self_referential(state, alias, self_identity)
                }
                _ => false,
            })
        }

        let seen_typevars = RefCell::new(FxHashSet::default());
        let seen_type_aliases = RefCell::new(FxHashSet::default());

        let state = State {
            db,
            visitor,
            seen_typevars: &seen_typevars,
            seen_type_aliases: &seen_type_aliases,
        };

        type_is_self_referential_impl(state, ty, self.identity(db))
    }

Copy link
Copy Markdown
Member

@AlexWaygood AlexWaygood left a comment

Choose a reason for hiding this comment

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

Modulo my suggestions regarding readability, this LGTM, thanks!

Comment on lines -540 to -543
if typevar.default_type(db).is_some() {
typevar_with_defaults += 1;
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

What's the motivation for this change?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

I think this was a mistake, but re-adding it surfaced another panic, and I had to add cycle handling for bound_typevar_default_type...

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

(I think this specific one at least could be avoided if I just did a syntactic query for the default type presence.)

@charliermarsh charliermarsh enabled auto-merge (squash) March 3, 2026 14:18
@charliermarsh charliermarsh force-pushed the charlie/self-recurse branch from 9469e1f to b4b57eb Compare March 3, 2026 14:56
@charliermarsh charliermarsh disabled auto-merge March 3, 2026 15:43
@carljm carljm removed their request for review March 3, 2026 19:45
@charliermarsh charliermarsh merged commit 1d123a8 into main Mar 4, 2026
51 checks passed
@charliermarsh charliermarsh deleted the charlie/self-recurse branch March 4, 2026 14:06
carljm added a commit that referenced this pull request Mar 16, 2026
* main:
  [ty] Split up `types/class.rs` (#23714)
  [ty] Rework module resolution to be breadth-first instead of depth-first (#22449)
  [ty] Move tests and type-alias-related code out of `types.rs` (#23711)
  [ty] Move TypeVar-related code to a `types::typevar` submodule (#23710)
  Fail CI on new linter ecosystem panics (#23597)
  [ty] Fix handling of non-Python text documents
  [ty] Move `CallableType`, and related methods/types, to a new `types::callable` submodule (#23707)
  [ty] Avoid stack overflow with recursive typevar (#23652)
  [ty] Add a diagnostic for an unused awaitable (#23650)
  [ty] Fix union `*args` binding for optional positional parameters (#23124)
  [`refurb`] Fix `FURB101` and `FURB103` false positives when I/O variable is used later (#23542)
  [ty] Fix type checking for multi-member enums within in a function block (#23683)
  [ty] Improve folding for decorators (#23543)
  [`airflow`] Extract common utilities for use in new rules (#23630)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working ty Multi-file analysis & type inference

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Stack overflow on divergently recursive type statement with typevar with itself as the default

3 participants