Skip to content

[IR Container] Phase 2.6 Concurrency & Thread Safety#5971

Open
mdavis36 wants to merge 3 commits intomd/phase2-copy-movefrom
md/phase2-thread-safety
Open

[IR Container] Phase 2.6 Concurrency & Thread Safety#5971
mdavis36 wants to merge 3 commits intomd/phase2-copy-movefrom
md/phase2-thread-safety

Conversation

@mdavis36
Copy link
Collaborator

@mdavis36 mdavis36 commented Feb 18, 2026

Summary

Add std::shared_mutex to IrContainer for concurrent read access during parallel compilation, remove the kPhase2DisableParallelCompile serialization guard introduced in PR 1, and validate that the full test suite passes with parallel compilation re-enabled.

This is a future-proofing and defensive correctness change. Phase 2's makeFusion path does NOT share containers (each segment gets its own container via the default constructor), so parallel compilation is technically safe without the mutex. However, Phase 3 will change makeFusion to use the copy constructor (shared container), at which point multiple threads will write to the same IrContainer concurrently. The mutex must be in place before Phase 3 can enable that.

The Nested Call Problem and ContainerMutator

Five Fusion methods directly access IrContainer's internal fields because statement registration was moved from IrContainer to Fusion previously:

registerVal()  → writes vals_up_, vals_, per_fusion_vals_
registerExpr() → writes exprs_up_, exprs_, per_fusion_exprs_   (calls removeExpr for SSA)
removeVal()    → writes vals_up_, vals_, per_fusion_vals_      (calls removeExpr)
removeExpr()   → writes exprs_up_, exprs_, per_fusion_exprs_
removeStatementsCreatedAfter() → calls all of the above

removeVal() calls removeExpr(), and registerExpr() also calls removeExpr(). Since std::shared_mutex is not recursive, acquiring unique_lock in both the outer and inner methods would deadlock.

The solution is a two-layer locking architecture:

Layer 1: IrContainer public methods (self-locking)
  Read methods:  std::shared_lock(mutex_)    — concurrent reads OK
  Write methods: std::unique_lock(mutex_)   — exclusive access

Layer 2: Fusion methods that bypass IrContainer (ContainerMutator)
  Public method: acquires std::unique_lock(ir_container()->mutex_)
  Delegates to:  ContainerMutator static methods (lock-free, direct field access)
  Nested calls:  go through ContainerMutator → safe, already under lock

ContainerMutator is forward-declared in fusion.h (2 lines) and fully defined in fusion.cpp. This keeps the header clean and makes the locking architecture self-documenting: everything inside ContainerMutator assumes the lock is already held.

Thread Safety Analysis

                        Phase 2                          Phase 3
                        ───────                          ───────
makeFusion behavior:    Default ctor + Fusion::copy      Copy ctor (shared container)
Container sharing:      No (each segment gets its own)   Yes (scalars reused)
Thread safety needed:   No (reads only on completeFusion) Yes (concurrent writes)

Dead Code Removal

Investigation revealed that IrContainer::copy() and IrContainer::swap() have zero call sites — all copy/move/swap semantics are handled at the Fusion level after previous work. Removing them eliminates ~45 lines of dead code and avoids complex dual-locking patterns.

Relationship to Phase 2

This PR completes the Phase 2 architectural work. With thread safety in place, the full shared scalar infrastructure is ready for Phase 3:

CI Risk

Low-medium. This is the first CI run with parallel compilation re-enabled since PR #5961 serialized it. Any latent concurrency issues would surface here. The parallel compilation path doesn't share containers in Phase 2, so the mutex is defensive — but re-enabling parallelism exercises the full concurrent codegen pipeline.

@mdavis36
Copy link
Collaborator Author

!test

@github-actions
Copy link

github-actions bot commented Feb 18, 2026

Review updated until commit 0a32c16

Description

  • Add std::shared_mutex to IrContainer for concurrent read access during parallel compilation

  • Implement ContainerMutator PIMPL struct for lock-free mutation methods, avoiding self-deadlock on nested calls (removeVal → removeExpr)

  • Fusion methods (registerVal/Expr, removeVal/Expr, removeStatementsCreatedAfter) now acquire unique_lock then delegate to ContainerMutator

  • Remove kPhase2DisableParallelCompile guard to re-enable parallel compilation; delete dead IrContainer::copy() and swap() methods

Changes walkthrough

Relevant files

PR Reviewer Guide

Here are some key observations to aid the review process:

🧪 PR contains tests
⚡ Recommended focus areas for review
Duplicate code line

In the swap function around line 380-381, there is a duplicate line: auto* c = a.ir_container_.get(); appears twice - once before and once after std::unique_lock lock(c->mutex_);. The second occurrence should be removed as it redefines the variable after the lock is acquired.

std::unique_lock lock(c->mutex_);
auto* c = a.ir_container_.get();

@mdavis36 mdavis36 force-pushed the md/phase2-thread-safety branch from 2cac45c to 8fb976b Compare February 18, 2026 03:13
@mdavis36 mdavis36 force-pushed the md/phase2-copy-move branch from 192fd55 to 35b7405 Compare February 18, 2026 03:13
@mdavis36 mdavis36 changed the title [IR Container] Phase2 thread safety [IR Container] Phase 2.6 Concurrency & Thread Safety Feb 18, 2026
@mdavis36
Copy link
Collaborator Author

!test

@mdavis36 mdavis36 marked this pull request as ready for review February 18, 2026 06:38
@greptile-apps
Copy link
Contributor

greptile-apps bot commented Feb 18, 2026

Greptile Summary

This PR completes Phase 2 thread-safety infrastructure by adding std::shared_mutex to IrContainer, introducing a ContainerMutator PIMPL to break the nested-lock deadlock that would arise from removeVal → removeExpr and registerExpr → removeExpr call chains, removing dead IrContainer::copy()/swap() code, and re-enabling parallel compilation that was serialized in the previous PR.

Key changes and observations:

  • Two-tier locking architecture is well-designed: public IrContainer methods self-lock, ContainerMutator static helpers are lock-free and must be called with the lock already held. This correctly solves the non-recursive shared_mutex deadlock problem.
  • operator[] side-effect in removeVal: c->per_fusion_exprs_[self] uses std::unordered_map::operator[], which silently inserts an empty set for self if not already present. This unintended mutation should be replaced with .find() to avoid spurious entries and future Phase 3 contention.
  • getValName/getExprName lack locking contract documentation: These inline protected methods mutate shared counters without acquiring mutex_. They are safely invoked only from ContainerMutator (under unique_lock), but this invariant is not documented in code — a pitfall for future contributors.
  • Fusion::swap different-containers path has an unprotected window: Between transferFusion and transferStatementOwnership lock phases, the two containers are transiently inconsistent. The ir_container_ pointers are swapped and Statement pointers updated while unlocked. Safe in Phase 2 (containers not shared), but should be documented as a known limitation before Phase 3 work.

Confidence Score: 3/5

  • Safe to merge for Phase 2 goals; the two-tier locking design correctly prevents deadlock and parallel compilation is safely re-enabled. However, three correctness gaps should be addressed before Phase 3 work builds on this foundation.
  • The core locking architecture is sound and well-reasoned, and all stated Phase 2 goals are met. However, three issues reduce confidence: (1) the operator[] mutation in removeVal creates spurious entries that will increase Phase 3 contention, (2) getValName/getExprName lack locking contract documentation making them easy to misuse, and (3) the different-containers swap path has an unprotected window that creates a known limitation needing Phase 3 revisit. None are show-stoppers for Phase 2, but they should be fixed before Phase 3 concurrent writes.
  • csrc/fusion.cpp needs fixes for the operator[] issue in removeVal and the unprotected window in the different-containers Fusion::swap path. csrc/ir/container.h should add a locking contract comment on getValName/getExprName.

Important Files Changed

Filename Overview
csrc/fusion.cpp Introduces ContainerMutator struct with lock-free mutation helpers that delegates registerVal, registerExpr, removeVal, removeExpr, and removeStatementsCreatedAfter operations under unique_lock. Two issues identified: (1) per_fusion_exprs_[self] in removeVal silently inserts via operator[], creating spurious empty entries, (2) the different-containers Fusion::swap path has an unprotected window between lock/unlock phases where containers are transiently inconsistent.
csrc/ir/container.h Adds mutable std::shared_mutex mutex_ to IrContainer; removes dead copy()/swap() static methods; moves unordered_exprs, vals, numExprs, numVals to .cpp implementations; adds inContainerImpl and assertInContainerImpl private lock-free helpers. The inline getValName/getExprName methods lack a contract comment documenting that callers must hold mutex_.
csrc/ir/container.cpp Adds shared_lock to all read paths and unique_lock to all write paths. Removes dead IrContainer::copy() and IrContainer::swap(). Introduces two-tier lock design (inContainer locks, inContainerImpl is lock-free). Implementation is correct.
csrc/fusion.h Adds forward declaration of ContainerMutator struct and friend struct ContainerMutator; moves numValsExcludingShortcuts out of the header. Changes are minimal and correct.
csrc/runtime/fusion_kernel_runtime.cpp Removes the kPhase2DisableParallelCompile flag and its two usage sites, re-enabling parallel compilation now that IrContainer has mutex protection. Change is clean and straightforward.

Sequence Diagram

sequenceDiagram
    participant Caller as Caller Thread
    participant Fusion as Fusion (public API)
    participant CM as ContainerMutator (lock-free)
    participant IC as IrContainer

    Note over Fusion, IC: Layer 1 — Public write methods acquire unique_lock
    Caller->>Fusion: registerVal(val)
    Fusion->>IC: unique_lock(mutex_)
    Fusion->>CM: ContainerMutator::registerVal(self, val)
    CM->>IC: inContainerImpl(val)  [no re-lock]
    CM->>IC: vals_up_.emplace_back / vals_.insert
    CM->>IC: per_fusion_vals_[self].insert
    CM->>IC: getValName() [no re-lock, caller holds lock]
    Fusion->>IC: unique_lock released

    Note over Fusion, IC: Nested call path — removeVal → removeExpr (no deadlock)
    Caller->>Fusion: removeVal(val)
    Fusion->>IC: unique_lock(mutex_)
    Fusion->>CM: ContainerMutator::removeVal(self, val)
    CM->>CM: removeExpr(self, val->definition()) [lock-free, no re-entry]
    CM->>IC: per_fusion_exprs_[self].erase / exprs_.erase
    CM->>IC: per_fusion_vals_[self].erase / vals_.erase
    Fusion->>IC: unique_lock released

    Note over Fusion, IC: Layer 1 — Public read methods acquire shared_lock
    Caller->>IC: unordered_exprs() / vals()
    IC->>IC: shared_lock(mutex_)
    IC-->>Caller: const ref returned (lock released)
Loading

Last reviewed commit: 55401d6

Comments Outside Diff (3)

  1. csrc/fusion.cpp, line undefined (link)

    c->per_fusion_exprs_[self] uses std::unordered_map::operator[], which silently inserts a default-constructed (empty) unordered_set for key self if it doesn't already exist. This means calling Fusion::removeVal() on a Fusion that has never registered any expressions will mutate per_fusion_exprs_ by creating a spurious empty entry.

    While the downstream iteration is harmless, the map grows unexpectedly. In Phase 3 when per_fusion_exprs_ is accessed concurrently, spurious entries increase contention.

    Use .find() to perform a non-mutating lookup:

  2. csrc/ir/container.h, line 87-96 (link)

    getValName and getExprName modify val_type_name_map_ and expr_name_counter_ respectively without acquiring mutex_. They are only invoked from ContainerMutator methods under std::unique_lock(mutex_), but the inline header definitions carry no indication of this requirement. Adding a contract comment makes the invariant self-documenting and prevents misuse by future contributors:

  3. csrc/fusion.cpp, line 343-397 (link)

    When a.ir_container_ != b.ir_container_, the swap proceeds in three phases:

    1. Locked (lines 344–345): transferFusion — acquires and releases each mutex.
    2. Unlocked (lines 349–384): Container pointer swap, Fusion-level field swaps, and Statement::ir_container_ pointer updates.
    3. Locked (lines 396–397): transferStatementOwnership — acquires and releases each mutex.

    The unlocked section includes swapping ir_container_ pointers themselves. Any concurrent reader holding shared_lock on either container can see a transient inconsistency where Statement::ir_container_ pointers have been redirected to the new containers but per_fusion_vals_ and per_fusion_exprs_ still map the old Fusion keys.

    While safe in Phase 2 (containers are not shared across threads), the same-container branch already correctly holds unique_lock for its entire mutation. Consider documenting this gap with a comment before Phase 3 enables concurrent writes to shared containers:

Copy link
Contributor

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

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

5 files reviewed, 2 comments

Edit Code Review Agent Settings | Greptile

@greptile-apps
Copy link
Contributor

greptile-apps bot commented Feb 18, 2026

Additional Comments (1)

csrc/ir/container.cpp
clear() does not acquire the mutex

IrContainer::clear() modifies all internal data structures (vals_, exprs_, per_fusion_vals_, etc.) without acquiring mutex_. While clear() is only called from IrContainer::~IrContainer() and Fusion::copy() (which is called in the copy constructor/assignment), there is no lock protection if a concurrent thread holds a shared lock on the same container. Since clear() is protected, this may be intentional (caller guarantees exclusive access), but it's worth a brief comment to document that assumption for Phase 3 safety.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

@mdavis36
Copy link
Collaborator Author

!test

@mdavis36 mdavis36 force-pushed the md/phase2-thread-safety branch from 8fb976b to 31bccb9 Compare February 26, 2026 00:29
@mdavis36 mdavis36 force-pushed the md/phase2-copy-move branch from 35b7405 to 88b2e60 Compare February 26, 2026 00:29
mdavis36 added a commit that referenced this pull request Feb 26, 2026
…tainers

Update Fusion::removeStatementsCreatedAfter to compare per-Fusion counts
(from exprsOwnedBy(this) and numValsExcludingShortcuts()) instead of global
deque sizes. This correctly handles shared containers where other Fusions'
statements would inflate the global counts.

Add NVF_ERROR assertions to verify the LIFO invariant: the tail element
of the global deque must belong to this Fusion. If violated, another
Fusion appended concurrently (should be prevented by PR #5971 locking).

Remove now-unnecessary deque size validation checks.
@mdavis36 mdavis36 force-pushed the md/phase2-copy-move branch 2 times, most recently from a9c62ea to 46080be Compare March 3, 2026 02:51
@mdavis36 mdavis36 force-pushed the md/phase2-thread-safety branch from 31bccb9 to 0a32c16 Compare March 3, 2026 22:18
@mdavis36
Copy link
Collaborator Author

mdavis36 commented Mar 3, 2026

!test

@greptile-apps
Copy link
Contributor

greptile-apps bot commented Mar 3, 2026

Additional Comments (2)

csrc/fusion.cpp, line 381
Duplicate variable declaration — compile error

auto* c is already declared on line 379 (the context line preserved from before the diff). Line 381 redeclares it in the same scope, which is ill-formed in C++ and will fail to compile. The second declaration should be removed.

    std::unique_lock lock(c->mutex_);

csrc/fusion.cpp, line 194
Deadlock: locking public method called while unique_lock is held

ContainerMutator::removeStatementsCreatedAfter is invoked from Fusion::removeStatementsCreatedAfter which already holds std::unique_lock lock(ir_container()->mutex_).

IrContainer::exprsOwnedBy (a public method added in this PR) internally acquires std::shared_lock lock(mutex_). std::shared_mutex is not recursive: a thread that already owns a unique_lock and attempts to acquire any further lock (unique or shared) on the same mutex is undefined behaviour and will deadlock in practice.

The same problem recurs one loop below at line 209 via self->numValsExcludingShortcuts(), which calls ir_container()->valsOwnedBy(this), and valsOwnedBy also acquires a shared_lock.

The fix is to access the container fields directly, bypassing the locking wrappers, as is done everywhere else inside ContainerMutator:

// line 194 – replace locking accessor with direct field access
while (std::ssize(c->per_fusion_exprs_[self]) > num_exprs_before) {
// line 209 – inline the shortcut-adjusted count using direct field access
int64_t numVals = std::ssize(c->per_fusion_vals_[self])
    - (self->zero_val_ != nullptr) - (self->one_val_ != nullptr)
    - (self->true_val_ != nullptr) - (self->false_val_ != nullptr)
    - (self->magic_zero_val_ != nullptr);
while (numVals > num_vals_before) {
    ...
    numVals--;
}

@mdavis36 mdavis36 force-pushed the md/phase2-thread-safety branch from 0a32c16 to b62d5ff Compare March 4, 2026 01:13
@mdavis36
Copy link
Collaborator Author

mdavis36 commented Mar 4, 2026

!test

@greptile-apps
Copy link
Contributor

greptile-apps bot commented Mar 4, 2026

Additional Comments (1)

csrc/fusion.cpp, line 193
Deadlock: numValsExcludingShortcuts calls the locking public API while the caller holds unique_lock

ContainerMutator::removeStatementsCreatedAfter is entered while holding unique_lock(mutex_) (acquired at Fusion::removeStatementsCreatedAfter, line 608). Its val-removal loop (line 217) calls self->numValsExcludingShortcuts(), which routes here and calls self->ir_container()->valsOwnedBy(self). valsOwnedBy itself acquires std::shared_lock lock(mutex_) (line 190 of container.cpp). Since std::shared_mutex is not recursive, attempting to acquire any lock mode while the calling thread already holds unique_lock on the same mutex is undefined behavior and deadlocks on all major implementations.

Every other static method in ContainerMutator accesses per_fusion_* fields directly (e.g., c->per_fusion_exprs_[self]). This method is the only one that round-trips through the public, self-locking API, making it inconsistent with the lock-free contract of ContainerMutator.

The fix is to bypass the public accessor and read per_fusion_vals_ directly — exactly what the expr-removal loop already does with per_fusion_exprs_[self]:

static int64_t numValsExcludingShortcuts(const Fusion* self) noexcept {
  auto* c = self->ir_container();
  auto it = c->per_fusion_vals_.find(self);
  int64_t count = it != c->per_fusion_vals_.end() ? std::ssize(it->second) : 0;
  count -= (self->zero_val_ != nullptr) + (self->one_val_ != nullptr) +
      (self->true_val_ != nullptr) + (self->false_val_ != nullptr) +
      (self->magic_zero_val_ != nullptr);
  return count;
}

Note: Fusion::numValsExcludingShortcuts() (a public method, line 613) delegates here and is called without a lock held in other contexts, so it will also work correctly with the direct field access once the fix is applied.

@mdavis36 mdavis36 force-pushed the md/phase2-thread-safety branch from b62d5ff to 6eda820 Compare March 4, 2026 01:32
@mdavis36
Copy link
Collaborator Author

mdavis36 commented Mar 4, 2026

!test

@greptile-apps
Copy link
Contributor

greptile-apps bot commented Mar 4, 2026

Additional Comments (1)

csrc/fusion.cpp, line 193
Deadlock: valsOwnedBy re-acquires mutex while unique_lock is held

ContainerMutator::numValsExcludingShortcuts calls self->ir_container()->valsOwnedBy(self) (line 188), which internally acquires a std::shared_lock on mutex_. However, this function is called from ContainerMutator::removeStatementsCreatedAfter (line 217), which is itself called from Fusion::removeStatementsCreatedAfter that already holds a std::unique_lock on the same mutex (line 608).

std::shared_mutex is not recursive. Per the C++ standard, calling lock_shared() from a thread that already owns the mutex (in exclusive mode via unique_lock) is undefined behavior, and on every mainstream implementation this deadlocks immediately — the lock_shared() call blocks forever waiting for the exclusive lock to release, which is the same thread.

All other loop conditions in removeStatementsCreatedAfter correctly bypass the locking accessors by reading c->per_fusion_exprs_[self] directly, but the val-count loop goes through the locking valsOwnedBy path.

The fix is to read the per-fusion val count directly from the container field, matching the pattern used for the expr loop just above:

static int64_t numValsExcludingShortcuts(const Fusion* self) noexcept {
    auto* c = self->ir_container();
    // Use direct field access (caller must hold mutex_, or container must not
    // be shared). Avoids re-entering valsOwnedBy() which acquires shared_lock.
    const auto it = c->per_fusion_vals_.find(self);
    int64_t count = it != c->per_fusion_vals_.end()
        ? static_cast<int64_t>(it->second.size())
        : 0;
    count -= (self->zero_val_ != nullptr) + (self->one_val_ != nullptr) +
        (self->true_val_ != nullptr) + (self->false_val_ != nullptr) +
        (self->magic_zero_val_ != nullptr);
    return count;
}

The public Fusion::numValsExcludingShortcuts() (called without the lock) also delegates here, so this change keeps both call sites correct: the direct field read is safe under unique_lock, and the mutex-free caller path remains safe because Phase 2 containers are not shared.

@mdavis36 mdavis36 force-pushed the md/phase2-thread-safety branch from 6eda820 to 23bddaf Compare March 4, 2026 01:48
@mdavis36
Copy link
Collaborator Author

mdavis36 commented Mar 4, 2026

!test

@mdavis36 mdavis36 force-pushed the md/phase2-copy-move branch 2 times, most recently from d145d9e to 3b9acdb Compare March 5, 2026 00:07
@mdavis36 mdavis36 force-pushed the md/phase2-thread-safety branch from 23bddaf to d4478b1 Compare March 5, 2026 00:08
@greptile-apps
Copy link
Contributor

greptile-apps bot commented Mar 5, 2026

Additional Comments (1)

csrc/fusion.cpp, line 628
Unprotected read in public method numValsExcludingShortcuts

This public method calls directly into ContainerMutator::numValsExcludingShortcuts() without acquiring a lock. The static helper accesses per_fusion_vals_ without synchronization. If any external caller invokes this public method concurrently with a writer holding unique_lock(ir_container()->mutex_), this becomes an unsynchronized read — a data race under the C++ memory model.

In Phase 2, container sharing doesn't happen, so concurrent access to a single Fusion is unlikely. However, Phase 3 will introduce shared containers and concurrent writes, at which point this unprotected read path becomes unsafe.

Suggested fix: Acquire shared_lock in the public method before delegating to the static helper, consistent with all other public read methods:

int64_t Fusion::numValsExcludingShortcuts() const noexcept {
  std::shared_lock lock(ir_container()->mutex_);
  return ContainerMutator::numValsExcludingShortcuts(this);
}

The static helper's lock-free property is preserved for the removeStatementsCreatedAfter path (which calls the static method directly while already holding the lock), while the public path becomes properly guarded.

@mdavis36 mdavis36 force-pushed the md/phase2-copy-move branch from 3b9acdb to c8ffe1d Compare March 5, 2026 00:47
mdavis36 added 2 commits March 4, 2026 17:08
Add std::shared_mutex to IrContainer to protect shared mutable state
during concurrent access from parallel compilation threads.

- IrContainer public methods self-lock (shared_lock for reads,
  unique_lock for writes)
- Fusion mutation methods (registerVal/Expr, removeVal/Expr,
  removeStatementsCreatedAfter) acquire unique_lock then delegate to
  lock-free ContainerMutator static methods, avoiding self-deadlock
  on nested calls (e.g., removeVal → removeExpr)
- ContainerMutator is a PIMPL struct defined only in fusion.cpp,
  keeping lock-free impl methods out of the header
- Remove kPhase2DisableParallelCompile guard, re-enabling parallel
  compilation now that the mutex is in place
- Delete dead IrContainer::copy() and IrContainer::swap() methods
@mdavis36 mdavis36 force-pushed the md/phase2-thread-safety branch from d4478b1 to 5d12b11 Compare March 5, 2026 01:08
@greptile-apps
Copy link
Contributor

greptile-apps bot commented Mar 5, 2026

Additional Comments (1)

csrc/fusion.cpp, line 626
numValsExcludingShortcuts() public method bypasses shared_lock

The public Fusion::numValsExcludingShortcuts() delegates to ContainerMutator::numValsExcludingShortcuts, which accesses per_fusion_vals_ directly without acquiring mutex_ (line 189). The previous implementation properly acquired a shared_lock via ir_container()->valsOwnedBy(this).

ContainerMutator::numValsExcludingShortcuts is intentionally lock-free (line 187–188 acknowledges this) because it's called from removeStatementsCreatedAfter which already holds unique_lock. However, the public wrapper is called from StatementGuard's constructor (statement_guard.cpp:23) outside any critical section, making this the only public read accessor on IrContainer that doesn't acquire shared_lock. Every other read method (vals(), unordered_exprs(), numVals(), numExprs(), inContainer(), etc.) acquires the lock consistently.

This API inconsistency will produce a data race in Phase 3 when containers are shared across threads. Consider wrapping the call with shared_lock:

int64_t Fusion::numValsExcludingShortcuts() const noexcept {
  std::shared_lock lock(ir_container()->mutex_);
  return ContainerMutator::numValsExcludingShortcuts(this);
}

@mdavis36
Copy link
Collaborator Author

mdavis36 commented Mar 5, 2026

one verified issue: Fusion::numValsExcludingShortcuts() is the only public read method that bypasses shared_lock, creating an API inconsistency.

This is fixed in #5983 as it is removed entirely.

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.

1 participant