Skip to content

abstractinterpret: Fix data race on shared binding_states#840

Merged
aviatesk merged 2 commits into
aviatesk:masterfrom
PatrickHaecker:needs_locking
Jul 19, 2026
Merged

abstractinterpret: Fix data race on shared binding_states#840
aviatesk merged 2 commits into
aviatesk:masterfrom
PatrickHaecker:needs_locking

Conversation

@PatrickHaecker

Copy link
Copy Markdown
Contributor

report_package analyzes method signatures concurrently via Threads.@spawn (#785). Each task gets its own analyzer with refreshed local caches, but every task still shares a single binding_states::IdDict held in AnalyzerState. That map is mutated during inference whenever a method assigns to a global (e.g. in global_assignment_rt_exct), so concurrent setindex!/rehash corrupts it. It typically surfaces later as:

UndefRefError: access to undefined reference

from the unique! over the collected reports.

The shared write itself dates back to #698, but it was benign while report_package was single-threaded; #785 made the analysis concurrent without isolating or guarding it. Binding partitions are a Julia 1.12 feature, so this affects both 1.12 and 1.13.

Wrap binding_states in an AbstractBindings type that pairs the IdDict with a ReentrantLock and forwards getindex, setindex!, haskey, and get through it. The wrapper is also made lockable so the read-modify-write in const_assignment_rt_exct can run under one @lock and stay atomic. This keeps the shared-and-reused design intact (no per-task isolation, parallelism preserved) rather than giving each task a private map.

Accesses are infrequent (global assignments and partition lookups) and the lock is uncontended in the common case, so the overhead is negligible. test_typeinfer and test_virtualprocess pass.

I used Claude Opus 4.8 while implementing this.

@codecov

codecov Bot commented Jul 15, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 84.10%. Comparing base (23d7d7c) to head (05b71fe).
⚠️ Report is 1 commits behind head on master.

Additional details and impacted files
@@            Coverage Diff             @@
##           master     #840      +/-   ##
==========================================
+ Coverage   84.05%   84.10%   +0.05%     
==========================================
  Files          12       12              
  Lines        3160     3170      +10     
==========================================
+ Hits         2656     2666      +10     
  Misses        504      504              

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@aviatesk aviatesk left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

Thanks for fixing the issue! That was very helpful. Also, sorry for taking so long to review it.

That map is mutated during inference whenever a method assigns to a global (e.g. in global_assignment_rt_exct)

This isn’t exactly correct.
Since v0.11, report_package starts its analysis from the method signatures collected by Revise, so it generally shouldn’t infer top-level :const expressions.
That said, report_file still takes this hacky code path for practical reasons.

Comment thread src/abstractinterpret/abstractanalyzer.jl Outdated
Patrick Häcker and others added 2 commits July 19, 2026 23:01
`report_package` analyzes method signatures concurrently via
`Threads.@spawn` (aviatesk#785). Each task gets its own
analyzer with refreshed local caches, but every task still shares a
single `binding_states::IdDict` held in `AnalyzerState`. That map is
mutated during inference whenever a method assigns to a global (e.g.
in `global_assignment_rt_exct`), so concurrent `setindex!`/rehash
corrupts it. It typically surfaces later as:

    UndefRefError: access to undefined reference

from the `unique!` over the collected reports.

The shared write itself dates back to aviatesk#698, but it was
benign while `report_package` was single-threaded; aviatesk#785
made the analysis concurrent without isolating or guarding it. Binding
partitions are a Julia 1.12 feature, so this affects both 1.12 and
1.13.

Wrap `binding_states` in an `AbstractBindings` type that pairs the
`IdDict` with a `ReentrantLock` and forwards `getindex`, `setindex!`,
`haskey`, and `get` through it. The wrapper is also made lockable so
the read-modify-write in `const_assignment_rt_exct` can run under one
`@lock` and stay atomic. This keeps the shared-and-reused design
intact (no per-task isolation, parallelism preserved) rather than
giving each task a private map.

Accesses are infrequent (global assignments and partition lookups)
and the lock is uncontended in the common case, so the overhead is
negligible. `test_typeinfer` and `test_virtualprocess` pass.

I used Claude Opus 4.8 while implementing this.
@aviatesk
aviatesk merged commit ed62587 into aviatesk:master Jul 19, 2026
1 of 20 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants