[IR Container] Phase 2.5 Copy-Move Semantics#5964
[IR Container] Phase 2.5 Copy-Move Semantics#5964mdavis36 wants to merge 2 commits intomd/phase2-per-fusionfrom
Conversation
|
!test |
|
Review updated until commit 192fd55 Description
|
| Relevant files | |||||
|---|---|---|---|---|---|
| Enhancement |
|
PR Reviewer Guide
Here are some key observations to aid the review process:
| 🧪 PR contains tests |
| ⚡ Recommended focus areas for review |
Memory Management
|
|
!test |
Copy constructor now shares the source's container pointer instead of creating a new one. Fusion::copy clones directly from per-Fusion filtered vals rather than delegating to IrContainer::copy. Swap changed from content-based (IrContainer::swap) to pointer-based with per-Fusion ownership tracking for both same-container and different-container cases.
Move val/expr name counters from IrContainer to Fusion so each Fusion independently tracks name assignment. This fixes CI failures where Fusion::copy left the dest counter at N (number of cloned vals) instead of max(name)+1 when source names were non-sequential, causing newly created TVs to collide with existing names. The fix adds val_type_name_map_ and expr_name_counter_ to Fusion, and updates registerVal/registerExpr to use the Fusion-level counters. Fusion::copy syncs counters from source to dest after cloning. Fusion::swap exchanges counters. Fusion::clear resets them.
192fd55 to
35b7405
Compare
33629cb to
8b162d9
Compare
|
!test |
Greptile SummaryThis PR implements shared-container-aware copy, move, and swap semantics for
Confidence Score: 4/5
Important Files Changed
Flowchart%%{init: {'theme': 'neutral'}}%%
flowchart TD
subgraph CopyCtor["Copy Constructor: Fusion B(A)"]
CC1["Share A's container via shared_ptr"] --> CC2["Register B with container (addFusion)"]
CC2 --> CC3["Fusion::copy(&A, &B)"]
CC3 --> CC4["B.clear() (no-op, B is new)"]
CC4 --> CC5["Clone A's deterministic_vals into shared container"]
CC5 --> CC6["Sync name counters: B.counters = A.counters"]
CC6 --> CC7["Wire up definitions/uses on cloned vals"]
CC7 --> CC8["Remap special vals, inputs, outputs, aliases, metadata"]
end
subgraph SwapLogic["Fusion::swap(A, B)"]
S1["Snapshot owned statements for A and B"] --> S2{Same container?}
S2 -->|Different| S3["transferFusion on both containers"]
S2 -->|Same| S4["Skip transferFusion"]
S3 --> S5["Swap container pointers + all Fusion members"]
S4 --> S5
S5 --> S6["Update Statement::ir_container_ pointers"]
S6 --> S7{Same container?}
S7 -->|Yes| S8["Swap per_fusion_vals/exprs entries in container"]
S7 -->|No| S9["transferStatementOwnership on both containers"]
end
subgraph NameCounters["Per-Fusion Name Counters"]
N1["registerVal → Fusion::getValName(vtype)"] --> N2["Assigns name from Fusion's own counter"]
N3["registerExpr → Fusion::getExprName()"] --> N4["Assigns name from Fusion's own counter"]
N5["Fusion::copy syncs counters after cloning"] --> N6["Prevents name collisions post-copy"]
end
Last reviewed commit: 35b7405 |
| // Transfer Fusion registrations between containers before pointer swap. | ||
| // After swap, a will own b's container and b will own a's container. | ||
| if (a.ir_container_ && b.ir_container_ && | ||
| a.ir_container_.get() != b.ir_container_.get()) { | ||
| a.ir_container_->transferFusion(&a, &b); | ||
| b.ir_container_->transferFusion(&b, &a); | ||
| } |
There was a problem hiding this comment.
Missing sharing_fusions_ update when one container is null
When only one Fusion has a container (e.g., a has a container and b doesn't), transferFusion is skipped because the guard on line 133 requires both containers to be non-null. After the container pointer swap on line 140, the container's sharing_fusions_ set will still reference the old Fusion pointer instead of the new owner.
For example, if a has container C and b has null:
transferFusionis skipped (line 133 fails)- After swap:
a.ir_container_= null,b.ir_container_= C - C's
sharing_fusions_still contains&ainstead of&b inContainer()checks would fail forb's statements
While this is likely unreachable in practice (all constructors initialize ir_container_), the defensive code path on lines 189-192 for transferStatementOwnership suggests this case was considered. If so, it should also handle transferFusion:
if (a.ir_container_ && b.ir_container_ &&
a.ir_container_.get() != b.ir_container_.get()) {
a.ir_container_->transferFusion(&a, &b);
b.ir_container_->transferFusion(&b, &a);
} else if (a.ir_container_ && !b.ir_container_) {
a.ir_container_->transferFusion(&a, &b);
} else if (b.ir_container_ && !a.ir_container_) {
b.ir_container_->transferFusion(&b, &a);
}
| @@ -107,46 +107,90 @@ bool Fusion::sameDefinition(const Fusion& other) const { | |||
| void Fusion::swap(Fusion& a, Fusion& b) noexcept { | |||
There was a problem hiding this comment.
noexcept swap performs allocating operations
swap is marked noexcept but performs operations that can throw std::bad_alloc:
std::vectorconstruction and.assign()on lines 115-128transferFusiondoesunordered_set::insert(line 135-136)per_fusion_vals_[&a]withoperator[]can allocate (line 182)transferStatementOwnershipdoesunordered_set::insertandunordered_map::operator[](lines 186-192)
If any of these throw, std::terminate will be called. This is a pre-existing pattern from the old swap, but the new implementation adds more allocating operations (the std::vector snapshots). Consider either removing noexcept or wrapping in try/catch with a fallback.
| StmtNameType getValName(ValType vtype) { | ||
| if (val_type_name_map_.find(vtype) == val_type_name_map_.end()) { | ||
| val_type_name_map_[vtype] = 0; | ||
| } | ||
| return val_type_name_map_[vtype]++; | ||
| } |
There was a problem hiding this comment.
Consider using try_emplace or direct operator[]
Minor style nit: the find + operator[] pattern can be simplified. Since operator[] on unordered_map default-constructs the value (0 for StmtNameType) if the key is missing, the find check is redundant:
| StmtNameType getValName(ValType vtype) { | |
| if (val_type_name_map_.find(vtype) == val_type_name_map_.end()) { | |
| val_type_name_map_[vtype] = 0; | |
| } | |
| return val_type_name_map_[vtype]++; | |
| } | |
| StmtNameType getValName(ValType vtype) { | |
| return val_type_name_map_[vtype]++; | |
| } |
This produces identical behavior because StmtNameType (which is unsigned int) is value-initialized to 0 when default-constructed by operator[].
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!
Summary
Implement shared-container-aware copy, move, and swap operations, plus per-Fusion name counters that ensure cloned Vals get matching names. This PR combines the originally planned Tasks 3 and 4 — per-Fusion name counters were required to fix CI failures from the copy implementation (553 failures from duplicate TV names when name counter synchronization was missing).
Changes
Copy semantics:
shared_ptr, register with container, delegate toFusion::copyFusion::copy: Clear destination, createIrClonertargeting dest, clone source'sdeterministic_valsinto shared container, clone Fusion-level state (inputs, outputs, axioms, metadata)Move semantics:
Swap:
Per-Fusion name counters:
val_type_name_map_andexpr_name_counter_added as Fusion membersgetValName(ValType)andgetExprName()methods on FusionCopy Semantics in Detail
The copy constructor shares the container (increments
shared_ptrrefcount), then clones A's nodes into the same shared storage. Per-Fusion tracking ensures each Fusion's accessors still return only their own nodes.Swap: Three Cases
Why Name Counters Were Merged Into This PR
The initial implementation of
Fusion::copyreplaced the oldIrContainer::copywith directIrCloner-based cloning but dropped name counter synchronization. Without per-Fusion counters, cloned Vals in a shared container received names starting past the source's last name (e.g., T10–T19 instead of T0–T9), breakingalias_memory.cpp(duplicatetv->name()assertions) and cascading into 553 CI failures across codegen, validation, and numerical checks.The fix — per-Fusion name counters as Fusion members — is architecturally cleaner than the originally planned IrContainer-level maps, avoids indirection, and aligns with the per-Fusion state model established in earlier tasks.
Relationship to Phase 2
Copy/move/swap are the operations that make shared containers usable. Without them, the
shared_ptrand tracking infrastructure from PRs 1–2 are inert. This PR enables the core Phase 2 scenario:Phase 2 establishes the copy/move/swap mechanics. Phase 3 simply changes
makeFusionfrom default-ctor +Fusion::copyto copy-ctor (shared container), and the infrastructure from this PR handles everything correctly.Per-Fusion name counters are critical for cross-clone name correspondence required by
GreedyParams::at(tv->name())andnormalization_utils— both of which look up Vals by name as a map key across clone boundaries.CI Risk
Medium. Copy/move/swap are well-defined operations with clear semantics. The 553-failure CI regression from missing name counters was identified and fixed before merge.