Thanks to visit codestin.com
Credit goes to github.com

Skip to content

[ty] Avoid stack overflows in reachability analysis#26272

Merged
charliermarsh merged 4 commits into
mainfrom
charlie/fix-3822-stacker
Jun 24, 2026
Merged

[ty] Avoid stack overflows in reachability analysis#26272
charliermarsh merged 4 commits into
mainfrom
charlie/fix-3822-stacker

Conversation

@charliermarsh

@charliermarsh charliermarsh commented Jun 23, 2026

Copy link
Copy Markdown
Member

Summary

We record a reachability predicate for every statement-level call because the call may return Never. Predicate IDs follow source order, but reachability decision diagrams put later predicates before earlier ones. In a large generated method, analyzing a late call could infer its receiver, re-enter reachability for the preceding call, and continue backward through thousands of calls until the worker stack overflowed.

For example, given:

call_a()  # predicate 0
call_b()  # predicate 1
call_c()  # predicate 2

Reachability previously discovered the dependencies backward:

analyze call_c
└─ analyze call_b
   └─ analyze call_a

Before evaluating a reachability constraint, we now force analysis to process the relevant statement-level calls one by one in source order:

analyze call_a → cached
analyze call_b → call_a is already cached
analyze call_c → call_b is already cached

Non-terminal-call analysis is a tracked Salsa query, so each result is cached. When inferring a later call asks about an earlier call, it gets that cached result instead of adding another nested inference and reachability frame.

Inferring a call can itself re-enter reachability, so a scope-keyed guard prevents the same source-order pass from starting again. A nested scope is still allowed to perform its own pass. The ordinary decision-diagram evaluation and Never-based narrowing behavior remain unchanged.

The exact PySide6 reproduction now completes successfully instead of aborting with a stack overflow.

Closes astral-sh/ty#3822.

@charliermarsh charliermarsh added the ty Multi-file analysis & type inference label Jun 23, 2026
@astral-sh-bot

astral-sh-bot Bot commented Jun 23, 2026

Copy link
Copy Markdown

Typing conformance results

No changes detected ✅

Current numbers
The percentage of diagnostics emitted that were expected errors held steady at 94.37%. The percentage of expected errors that received a diagnostic held steady at 89.00%. The number of fully passing files held steady at 94/134.

@astral-sh-bot

astral-sh-bot Bot commented Jun 23, 2026

Copy link
Copy Markdown

Memory usage report

Summary

Project Old New Diff Outcome
prefect 516.31MB 521.19MB +0.95% (4.89MB)
sphinx 193.39MB 195.55MB +1.12% (2.16MB)
trio 77.51MB 78.31MB +1.03% (815.64kB)
flake8 30.75MB 30.84MB +0.29% (91.77kB)

Significant changes

Click to expand detailed breakdown

prefect

Name Old New Diff Outcome
analyze_non_terminal_call 0.00B 2.55MB +2.55MB (new)
analyze_non_terminal_call::interned_arguments 0.00B 1.79MB +1.79MB (new)
infer_expression_types_impl 55.93MB 56.29MB +0.65% (372.23kB)
infer_definition_types 69.15MB 69.24MB +0.14% (97.94kB)
all_narrowing_constraints_for_expression 6.58MB 6.63MB +0.72% (48.71kB)
CallableType 2.85MB 2.87MB +0.72% (20.92kB)
infer_scope_types_impl 38.84MB 38.82MB -0.05% (19.86kB)
BoundMethodType<'db>::bound_signatures_ 441.50kB 458.96kB +3.95% (17.45kB)
infer_unpack_types 976.86kB 991.76kB +1.53% (14.91kB)
loop_header_reachability 429.53kB 440.01kB +2.44% (10.48kB)
BoundMethodType<'db>::into_callable_type_ 207.06kB 214.78kB +3.73% (7.72kB)
FunctionType<'db>::signature_ 2.91MB 2.90MB -0.26% (7.61kB)
infer_statement_types_impl 1.51MB 1.52MB +0.21% (3.23kB)
infer_expression_type_impl 385.26kB 387.92kB +0.69% (2.66kB)
when_constraint_set_assignable_to_owned_impl 2.88MB 2.88MB -0.07% (1.93kB)
... 43 more

sphinx

Name Old New Diff Outcome
analyze_non_terminal_call 0.00B 996.91kB +996.91kB (new)
analyze_non_terminal_call::interned_arguments 0.00B 610.95kB +610.95kB (new)
infer_expression_types_impl 22.98MB 23.30MB +1.37% (321.50kB)
infer_definition_types 19.55MB 19.69MB +0.71% (142.79kB)
infer_expression_type_impl 277.54kB 351.71kB +26.72% (74.17kB)
member_lookup_with_policy_inner 6.10MB 6.14MB +0.68% (42.67kB)
loop_header_reachability 382.71kB 390.48kB +2.03% (7.77kB)
infer_scope_types_impl 10.51MB 10.50MB -0.07% (7.68kB)
BoundMethodType<'db>::bound_signatures_ 386.74kB 393.93kB +1.86% (7.19kB)
CallableType 1.47MB 1.48MB +0.42% (6.31kB)
BoundMethodType<'db>::into_callable_type_ 188.38kB 191.68kB +1.75% (3.30kB)
all_narrowing_constraints_for_expression 2.70MB 2.70MB +0.11% (3.16kB)
StaticClassLiteral<'db>::implicit_attribute_inner_ 781.12kB 783.41kB +0.29% (2.30kB)
parsed_module 18.37MB 18.37MB +0.01% (2.13kB)
FunctionType<'db>::signature_ 1.79MB 1.79MB -0.09% (1.56kB)
... 29 more

trio

Name Old New Diff Outcome
analyze_non_terminal_call 0.00B 583.62kB +583.62kB (new)
analyze_non_terminal_call::interned_arguments 0.00B 300.94kB +300.94kB (new)
infer_expression_types_impl 6.51MB 6.43MB -1.15% (76.86kB)
infer_scope_types_impl 3.19MB 3.18MB -0.33% (10.76kB)
infer_definition_types 5.59MB 5.60MB +0.09% (5.18kB)
all_narrowing_constraints_for_expression 613.27kB 617.68kB +0.72% (4.41kB)
parsed_module 15.05MB 15.05MB +0.01% (1.30kB)
FunctionType<'db>::signature_ 723.71kB 722.50kB -0.17% (1.20kB)
BoundMethodType<'db>::bound_signatures_ 90.00kB 91.08kB +1.20% (1.08kB)
Type<'db>::apply_specialization_inner_ 509.12kB 510.09kB +0.19% (988.00B)
Type<'db>::apply_specialization_inner_::interned_arguments 504.06kB 504.92kB +0.17% (880.00B)
member_lookup_with_policy_inner 1.52MB 1.52MB +0.05% (788.00B)
member_lookup_with_policy_inner::interned_arguments 763.71kB 764.41kB +0.09% (720.00B)
FunctionType 737.48kB 738.11kB +0.08% (640.00B)
infer_statement_types_impl 56.54kB 57.07kB +0.93% (540.00B)
... 23 more

flake8

Name Old New Diff Outcome
analyze_non_terminal_call 0.00B 40.57kB +40.57kB (new)
analyze_non_terminal_call::interned_arguments 0.00B 27.98kB +27.98kB (new)
infer_definition_types 1.35MB 1.36MB +0.93% (12.92kB)
parsed_module 9.78MB 9.77MB -0.11% (10.66kB)
CallableType 205.83kB 209.30kB +1.69% (3.47kB)
loop_header_reachability 12.43kB 15.75kB +26.74% (3.32kB)
all_narrowing_constraints_for_expression 87.14kB 90.20kB +3.51% (3.06kB)
infer_expression_types_impl 960.88kB 963.70kB +0.29% (2.82kB)
is_redundant_with_impl 73.70kB 76.30kB +3.53% (2.60kB)
infer_scope_types_impl 672.01kB 670.80kB -0.18% (1.21kB)
is_redundant_with_impl::interned_arguments 87.14kB 88.34kB +1.38% (1.20kB)
BoundMethodType<'db>::bound_signatures_ 29.86kB 30.67kB +2.71% (828.00B)
FunctionType 226.06kB 226.75kB +0.30% (704.00B)
cached_protocol_interface 52.28kB 52.91kB +1.21% (648.00B)
StaticClassLiteral<'db>::try_mro_ 294.78kB 295.36kB +0.20% (600.00B)
... 14 more

@astral-sh-bot

astral-sh-bot Bot commented Jun 23, 2026

Copy link
Copy Markdown

ecosystem-analyzer results

No diagnostic changes detected ✅

Flaky changes detected. This PR summary excludes flaky changes; see the HTML report for details.

Full report with detailed diff (timing results)

@astral-sh-bot

astral-sh-bot Bot commented Jun 23, 2026

Copy link
Copy Markdown

ruff-ecosystem results

Linter (stable)

✅ ecosystem check detected no linter changes.

Linter (preview)

✅ ecosystem check detected no linter changes.

Formatter (stable)

✅ ecosystem check detected no format changes.

Formatter (preview)

✅ ecosystem check detected no format changes.

@AlexWaygood AlexWaygood added the bug Something isn't working label Jun 23, 2026
@charliermarsh charliermarsh force-pushed the charlie/fix-3822-stacker branch 2 times, most recently from 3a765ce to 4037ee8 Compare June 23, 2026 16:59
@codspeed-hq

codspeed-hq Bot commented Jun 23, 2026

Copy link
Copy Markdown

Merging this PR will improve performance by 34.19%

⚠️ Different runtime environments detected

Some benchmarks with significant performance changes were compared across different runtime environments,
which may affect the accuracy of the results.

Open the report in CodSpeed to investigate

⚡ 2 improved benchmarks
✅ 69 untouched benchmarks
⏩ 64 skipped benchmarks1

Performance Changes

Mode Benchmark BASE HEAD Efficiency
Simulation ty_micro[typevar_mapping_small_accumulations] 177.6 ms 104.4 ms +70.1%
Memory ty_micro[typevar_mapping_small_accumulations] 12.2 MB 11.5 MB +5.87%

Tip

Curious why this is faster? Use the CodSpeed MCP and ask your agent.


Comparing charlie/fix-3822-stacker (9501f4c) with main (ce46714)

Open in CodSpeed

Footnotes

  1. 64 benchmarks were skipped, so the baseline results were used instead. If they were deleted from the codebase, click here and archive them to remove them from the performance reports.

@charliermarsh charliermarsh force-pushed the charlie/fix-3822-stacker branch from 4037ee8 to c922d90 Compare June 23, 2026 17:25
@charliermarsh charliermarsh marked this pull request as ready for review June 23, 2026 17:47
@charliermarsh charliermarsh requested a review from a team as a code owner June 23, 2026 17:47
@charliermarsh charliermarsh force-pushed the charlie/fix-3822-stacker branch from c922d90 to 3c23580 Compare June 23, 2026 19:29
@MichaReiser

Copy link
Copy Markdown
Member

I don’t think I’m the right reviewer for this. I’ve zero context on this code

@charliermarsh charliermarsh removed the request for review from MichaReiser June 23, 2026 21:40
@charliermarsh charliermarsh marked this pull request as draft June 23, 2026 21:40
@charliermarsh charliermarsh force-pushed the charlie/fix-3822-stacker branch 3 times, most recently from 7bca03a to fe5f30b Compare June 23, 2026 23:18
@charliermarsh charliermarsh marked this pull request as ready for review June 23, 2026 23:40
@astral-sh-bot astral-sh-bot Bot requested a review from dhruvmanila June 23, 2026 23:40
@carljm

carljm commented Jun 23, 2026

Copy link
Copy Markdown
Contributor

Looks like this still has conflicts vs main?

@charliermarsh charliermarsh force-pushed the charlie/fix-3822-stacker branch from fe5f30b to f68348a Compare June 23, 2026 23:43
@charliermarsh charliermarsh requested review from carljm and removed request for dhruvmanila June 23, 2026 23:44
@charliermarsh charliermarsh marked this pull request as draft June 23, 2026 23:51
@charliermarsh charliermarsh marked this pull request as ready for review June 23, 2026 23:51
@charliermarsh charliermarsh marked this pull request as draft June 23, 2026 23:51
@charliermarsh

Copy link
Copy Markdown
Member Author

No more conflicts but I'm exploring one other implementation first.

@charliermarsh

Copy link
Copy Markdown
Member Author

Opening up for review.

@carljm

carljm commented Jun 24, 2026

Copy link
Copy Markdown
Contributor

Does that mean you decided this approach is preferable to #26310 ?

@charliermarsh

Copy link
Copy Markdown
Member Author

Yeah. They're ultimately pretty similar but I found this one easier to understand.

@carljm carljm left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Looks good, thank you!

Comment on lines +570 to +573
for index in range {
let predicate = &predicates[ScopedPredicateId::new(index)];
if matches!(predicate.node, PredicateNode::IsNonTerminalCall(_)) {
analyze_single(db, predicate);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This approach of pre-warming all IsNonTerminalCall predicates that appear prior to us in the predicate array means that in branch-heavy code we may well be pre-warming a bunch of predicates we won't actually need, because they occur in different (earlier in source order) control-flow branches than the one we are in. This seems like probably an OK tradeoff for now? The more precise approach would be a graph walk and a demand-driven work-list, which would definitely be more complicated. But I think it's at least worth documenting this choice and its limitations explicitly in a comment. (It's possible that this is contributing to the extra memory usage in this PR -- though for code we are actually checking, I expect we'll eventually exercise all the predicates in a scope anyway.)

@charliermarsh charliermarsh force-pushed the charlie/fix-3822-stacker branch from f68348a to 9501f4c Compare June 24, 2026 02:18
@charliermarsh charliermarsh enabled auto-merge (squash) June 24, 2026 02:18
@carljm

carljm commented Jun 24, 2026

Copy link
Copy Markdown
Contributor

Hmm, Codex says this test still stack overflows:

#[test]
fn early_call_reentering_late_implicit_attribute_does_not_overflow_stack() -> anyhow::Result<()> {
    let handle = std::thread::Builder::new()
        .name("early-late-implicit-attribute-stack-test".into())
        .stack_size(ruff_db::STACK_SIZE)
        .spawn(|| {
            let mut db = setup_db();
            let mut ui = String::from(
                r#"from widgets import Widget

class Ui:
    def __init__(self):
        self.target = Widget()

    def setup(self):
        self.target.configure()
        self.early = Widget()
"#,
            );

            for index in 0..400 {
                ui.push_str(&format!(
                    concat!(
                        "        self.widget_{index} = Widget()\n",
                        "        self.widget_{index}.configure()\n",
                        "        self.widget_{index}.configure()\n",
                        "        self.widget_{index}.configure()\n",
                    ),
                    index = index,
                ));
            }
            ui.push_str("        self.target = Widget()\n");

            db.write_files([
                (
                    "/src/widgets.py",
                    r#"class Widget:
    def configure(self) -> None: ...
"#,
                ),
                ("/src/ui.py", &ui),
                (
                    "/src/consumer.py",
                    r#"from typing_extensions import reveal_type
from ui import Ui
from widgets import Widget

class Form(Ui):
    def early_widget(self) -> Widget:
        reveal_type(self.early)
        return self.early
"#,
                ),
            ])?;

            assert_revealed_type(&db, "/src/consumer.py", "Widget");

            Ok(())
        })?;

    handle.join().expect("regression test thread panicked")
}

By way of explanation, it says:

If an early call resolves an implicit attribute assigned later in the same method, the nested later-prefix pass is skipped, leaving thousands of calls to be analyzed backward.

This makes sense to me -- seems like an issue that an actual graph walk might fix?

But we can also wait and see if it comes up in real code.

@charliermarsh charliermarsh merged commit aa8c227 into main Jun 24, 2026
62 checks passed
@charliermarsh charliermarsh deleted the charlie/fix-3822-stacker branch June 24, 2026 02:23
@charliermarsh

Copy link
Copy Markdown
Member Author

That makes sense. I’ll keep looking into it tomorrow.

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 when type-checking a class that inherits from a pyside6-uic generated UI mixin

4 participants