Skip to content

Conversation

@mdavis36
Copy link
Collaborator

No description provided.

@mdavis36
Copy link
Collaborator Author

!test

@github-actions
Copy link

github-actions bot commented Feb 11, 2026

Review updated until commit 1588d37

Description

  • Moved special values (zero_val_, one_val_, true_val_, false_val_, magic_zero_val_) from IrContainer to Fusion class

  • Implemented lazy creation pattern for all special value accessors in Fusion

  • Updated Fusion copy/swap/clear operations to handle per-Fusion special values

  • Added StatementGuard support to prevent dangling special value pointers

  • Removed special value methods from IrContainer class

  • Added regression test for lazy special vals with StatementGuard

Changes walkthrough

Relevant files
Enhancement
fusion.cpp
Move special values to Fusion with lazy creation                 

csrc/fusion.cpp

  • Added includes for type.h, ir/builder.h, ir/internal_nodes.h
  • Updated swap() to handle per-Fusion special values
  • Updated copy() to remap cached special val pointers
  • Updated clear() to reset special value pointers
  • Updated removeVal() to protect cached special vals
  • Added removeStatementsCreatedAfter() with special value cache handling
  • Implemented lazy creation accessors for all special values
  • Added metadataOf(), axioms(), assumePositive(), assumeNonNegative()
    methods
  • +211/-0 
    container.cpp
    Remove special value methods from IrContainer                       

    csrc/ir/container.cpp

  • Removed all special value methods (zeroVal, oneVal, etc.)
  • Removed axioms and metadata methods
  • Removed removeStatementsCreatedAfter() method
  • Updated swap() to not handle special values (now in Fusion)
  • Updated copy() to not copy special values (now in Fusion)
  • Updated removeVal() to not check for special values
  • Updated clear() to not clear special values
  • +2/-180 
    utils.cpp
    Optimize SubstituteInExpr to skip unnecessary mutations   

    csrc/ir/utils.cpp

    • Added check to avoid unnecessary mutations in SubstituteInExpr
    +3/-1     
    statement_guard.cpp
    Update StatementGuard for new numVals() signature               

    csrc/statement_guard.cpp

    • Changed numVals() call to not pass include_shortcuts parameter
    +1/-1     
    fusion.h
    Add special value members and methods to Fusion class       

    csrc/fusion.h

  • Added NamedScalar forward declaration
  • Moved special value accessors from IrContainer to Fusion
  • Added new member variables for per-Fusion special values
  • Added axioms and metadata member variables to Fusion
  • Updated method declarations for moved functionality
  • +25/-46 
    container.h
    Remove special value methods from IrContainer interface   

    csrc/ir/container.h

  • Removed all special value method declarations
  • Removed axioms and metadata method declarations
  • Removed removeStatementsCreatedAfter() method declaration
  • Removed member variables for special values, axioms, and metadata
  • Updated numVals() to not have include_shortcuts parameter
  • +2/-50   
    Tests
    test_indexing.cpp
    Update test expectations for reshape test                               

    tests/cpp/test_indexing.cpp

    • Updated expected string output for reshape test
    +2/-2     
    test_statement_guard.cpp
    Add regression test for lazy special vals with StatementGuard

    tests/cpp/test_statement_guard.cpp

  • Added LazySpecialValsNotDangling regression test
  • Test verifies special vals work correctly with StatementGuard rollback
  • +51/-0   

    PR Reviewer Guide

    Here are some key observations to aid the review process:

    🧪 PR contains tests
    ⚡ Recommended focus areas for review
    Memory Management

    The PR moves special value ownership from IrContainer to Fusion, but I need to verify that the memory management is correct. The special values are now stored as raw pointers in Fusion, and there's careful handling in copy/swap/clear operations. The lazy creation pattern in methods like zeroVal() looks correct, but I should verify there are no memory leaks or dangling pointers.

    Val* Fusion::zeroVal() {
      if (!zero_val_) {
        zero_val_ = IrBuilder::createInContainer<Val>(this, 0L, DataType::Index);
      }
      return zero_val_;
    }
    
    Val* Fusion::oneVal() {
      if (!one_val_) {
        one_val_ = IrBuilder::createInContainer<Val>(this, 1L, DataType::Index);
      }
      return one_val_;
    }
    
    Val* Fusion::falseVal() {
      if (!false_val_) {
        false_val_ = IrBuilder::createInContainer<Val>(this, false, DataType::Bool);
      }
      return false_val_;
    }
    
    Val* Fusion::trueVal() {
      if (!true_val_) {
        true_val_ = IrBuilder::createInContainer<Val>(this, true, DataType::Bool);
      }
      return true_val_;
    }
    
    NamedScalar* Fusion::magicZeroVal() {
      if (!magic_zero_val_) {
        magic_zero_val_ = IrBuilder::createInContainer<NamedScalar>(
            this, kMagicZeroName, DataType::Index);
      }
      return magic_zero_val_;
    }
    StatementGuard Integration

    The removeStatementsCreatedAfter function now nulls out special value caches to prevent dangling pointers when special vals are lazily created inside a StatementGuard scope. This is a critical fix, but I need to verify the logic is sound and that the test case properly exercises this scenario.

    // Null out any special value caches that point to vals about to be destroyed.
    // This prevents dangling pointers when special vals are lazily created inside
    // a StatementGuard scope.
    while (std::ssize(c->vals_up_) > num_vals_before) {
      Val* v = c->vals_up_.back().get();
      if (v == zero_val_) {
        zero_val_ = nullptr;
      } else if (v == one_val_) {
        one_val_ = nullptr;
      } else if (v == true_val_) {
        true_val_ = nullptr;
      } else if (v == false_val_) {
        false_val_ = nullptr;
      } else if (v == magic_zero_val_) {
        magic_zero_val_ = nullptr;
      }
      c->vals_.erase(v);
      c->vals_up_.pop_back();
    }
    API Consistency

    The PR removes shortcut methods from IrContainer and moves them to Fusion. This breaks the existing API where users could call container->zeroVal(). I need to check if this is intentional and if there are any remaining calls to the old API that need to be updated.

    IrCloner IrContainer::copy(const IrContainer* from, IrContainer* to) {
      to->clear();
    
      IrCloner ir_cloner(to->parent());
    
      // Copy values in deterministic order
      for (auto val : from->deterministic_vals()) {
        if (from->vals().count(val) > 0) {
          to->vals_.insert(ir_cloner.clone(val));
        }
      }
    
      // Copy expressions in deterministic order
      for (auto expr : from->deterministic_exprs()) {
        if (from->unordered_exprs().count(expr) > 0) {
          to->exprs_.insert(ir_cloner.clone(expr));
        }
      }
    
      to->val_type_name_map_ = from->val_type_name_map_;
      to->expr_name_counter_ = from->expr_name_counter_;
    
      return ir_cloner;
    }

    Moved special values (`zero_val_`, `one_val_`, `true_val_`,
    `false_val_`, `magic_zero_val_`) from `IrContainer` to the `Fusion`
    class. This ensures that with shared containers, each Fusion has its own
    special values, preventing ownership conflicts when one Fusion is
    destroyed.
    
    **Option Implemented:** Option A (Move Special Values to Fusion) as
    recommended in the prompt.
    
    Added private members and public accessors to Fusion class:
    
    ```cpp
    // Phase 2: Per-Fusion special values
    // With shared containers, each Fusion needs its own special values.
    // These are raw pointers - memory is owned by IrContainer's vals_up_.
    // Destroying this Fusion removes these vals via
    removeStatementsOwnedBy().
    Val* zero_val_ = nullptr;
    Val* one_val_ = nullptr;
    Val* true_val_ = nullptr;
    Val* false_val_ = nullptr;
    NamedScalar* magic_zero_val_ = nullptr;
    ```
    
    Public accessors:
    - `Val* zeroVal()` - Returns Index 0
    - `Val* oneVal()` - Returns Index 1
    - `Val* falseVal()` - Returns Bool false
    - `Val* trueVal()` - Returns Bool true
    - `NamedScalar* magicZeroVal()` - Returns magic zero named scalar
    - `Val* zeroVal(DataType dtype)` - Returns 0 for specified dtype
    - `Val* oneVal(DataType dtype)` - Returns 1 for specified dtype
    
    Implemented lazy creation pattern for all special value accessors:
    
    ```cpp
    Val* Fusion::zeroVal() {
      if (!zero_val_) {
        zero_val_ = IrBuilder::createInContainer<Val>(this, 0L,
    DataType::Index);
      }
      return zero_val_;
    }
    // Similar implementations for oneVal(), falseVal(), trueVal(),
    magicZeroVal()
    ```
    
    Updated `Fusion::clear()` to reset special value pointers:
    
    ```cpp
    // Reset per-Fusion special values (they'll be recreated lazily if
    needed)
    // The actual Val objects were removed by removeStatementsOwnedBy above.
    zero_val_ = nullptr;
    one_val_ = nullptr;
    true_val_ = nullptr;
    false_val_ = nullptr;
    magic_zero_val_ = nullptr;
    ```
    
    Removed special value members and added documentation comment:
    
    ```cpp
    // Note: Special values (zero_val_, one_val_, true_val_, false_val_,
    // magic_zero_val_) are now per-Fusion, stored in Fusion class.
    // This avoids ownership conflicts when multiple Fusions share an
    IrContainer.
    // See Fusion::zeroVal(), etc. for the per-Fusion implementation.
    ```
    
    Removed special value accessor implementations (they're now in Fusion).
    
    All call sites were already updated to use `fusion->zeroVal()` instead
    of `ir_container()->zeroVal()`. Verified with grep that no call sites
    remain using the old pattern.
    
    Added 8 new unit tests for Task 7:
    
    1. **PerFusionSpecialValuesBasic** - Tests that special values are
    created and owned by the Fusion
    2. **SpecialValuesOwnedByFusion** - Tests that special values are
    tracked in `ownedVals()`
    3. **SeparateFusionsHaveOwnSpecialValues** - Tests that two Fusions have
    different special value objects
    4. **DestroyFusionDoesNotAffectOther** - Tests that destroying one
    Fusion doesn't affect another's special values
    5. **SpecialValuesLazyCreation** - Tests that same value is returned on
    repeated calls
    6. **AllSpecialValuesPerFusion** - Tests all five special value
    accessors
    7. **SpecialValuesClearedOnFusionClear** - Tests that `clear()` resets
    special values
    8. **SpecialValuesWithDtype** - Tests `zeroVal(dtype)` and
    `oneVal(dtype)` accessors
    
    ```
    [==========] Running 34 tests from 3 test suites.
    [  PASSED  ] 34 tests.
    ```
    
    ```
    [==========] Running 26 tests from 1 test suite.
    [  PASSED  ] 26 tests.
    ```
    
    Including 8 new Task 7 tests:
    - `Phase2ContainerTest.PerFusionSpecialValuesBasic` - PASSED
    - `Phase2ContainerTest.SpecialValuesOwnedByFusion` - PASSED
    - `Phase2ContainerTest.SeparateFusionsHaveOwnSpecialValues` - PASSED
    - `Phase2ContainerTest.DestroyFusionDoesNotAffectOther` - PASSED
    - `Phase2ContainerTest.SpecialValuesLazyCreation` - PASSED
    - `Phase2ContainerTest.AllSpecialValuesPerFusion` - PASSED
    - `Phase2ContainerTest.SpecialValuesClearedOnFusionClear` - PASSED
    - `Phase2ContainerTest.SpecialValuesWithDtype` - PASSED
    
    - `csrc/fusion.h` - Added special value members and accessors
    - `csrc/fusion.cpp` - Added accessor implementations, updated `clear()`
    - `csrc/ir/container.h` - Removed special values, added comment
    - `csrc/ir/container.cpp` - Removed accessor implementations
    - `tests/cpp/test_phase2_container_sharing.cpp` - Added 8 unit tests
    
    - [x] Each Fusion has its own special values
    - [x] Destroying Fusion A doesn't affect Fusion B's special values
    - [x] Special value accessors (`zeroVal()`, `oneVal()`, etc.) return
    this Fusion's values
    - [x] Lazy creation still works (create on first access)
    - [x] Smoke tests pass (34/34)
    - [x] Unit tests added (8 tests)
    - [x] Unit tests pass (26/26 Phase 2 tests)
    - [x] Code compiles without errors
    - [x] REPORT.md delivered
    
    1. **Memory ownership:** Special values are raw pointers stored in
    Fusion, but the actual memory is owned by IrContainer's `vals_up_`. When
    a Fusion is destroyed, `removeStatementsOwnedBy()` cleans up these vals.
    
    2. **Lazy creation pattern:** Special values are created on first
    access. This matches the original IrContainer behavior and avoids
    creating values that aren't needed.
    
    3. **Clear handling:** `Fusion::clear()` now resets special value
    pointers to nullptr after `removeStatementsOwnedBy()` removes the actual
    Val objects. This ensures lazy recreation works correctly after clear.
    
    4. **Copy/move handling:** Will be addressed in Tasks 5 and 6. This task
    just moves the members and accessors.
    Moved `axioms_` and `metadata_` from `IrContainer` to the `Fusion` class.
    This completes the deprecation of `parent_` usage for val-creating methods,
    which was necessary because `parent_` implies a 1-1 relationship
    (container → Fusion), but Phase 2 has 1-many (shared containers).
    
    Methods that used `parent_` to create vals were moved to Fusion:
    - `metadataOf(Val*)` - Now uses `v->container()` to get owning Fusion
    - `axioms()` - Now creates axiom vals owned by `this` Fusion
    - `assumePositive/assumeNonNegative` - Now adds to `this` Fusion's axioms
    
    - Added `axioms_` and `metadata_` private members
    - Changed method declarations from forwarding to actual implementations
    
    - Added includes for `ir/builder.h` and `ir/internal_nodes.h`
    - Implemented `metadataOf()`, `axioms()`, `assumePositive()`,
      `assumeNonNegative()` methods
    - Updated `clear()` to reset `axioms_` and `metadata_`
    
    - Removed `metadataOf()`, `axioms()`, `assumePositive()`,
      `assumeNonNegative()` declarations
    - Removed `lazyInitAxioms()` declaration
    - Removed `axioms_` and `metadata_` members
    
    - Removed implementations of above methods
    - Updated `IrContainer::swap` to remove axioms_/metadata_ swapping
    - Updated `IrContainer::copy` to remove axioms_/metadata_ handling
    - Updated `IrContainer::clear` to remove axioms_/metadata_ clearing
    
    Each Fusion now has its own axioms and metadata cache. This ensures:
    1. No ownership conflicts when multiple Fusions share an IrContainer
    2. Correct behavior when one Fusion is destroyed (doesn't affect others)
    3. Lazy creation pattern preserved (create on first access)
    
    This is a prerequisite for the copy/move semantics changes which will
    swap/transfer these per-Fusion members.
    - Add missing swap of axioms_ and metadata_ in Fusion::swap to prevent
      dangling pointers after move/assignment
    - Add missing cloning of axioms_ and metadata_ in Fusion::copy to
      preserve custom assumptions and metadata cache across copies
    - Guard Fusion::removeVal against removing cached special vals
    - Use std::unique_ptr for special vals and steal from vals_up_ to
      preserve the original invariant (shortcuts in vals_ but not vals_up_)
    - Fix metadataOf to use 'this' instead of v->container()
    @mdavis36 mdavis36 force-pushed the md/dep-special-types branch from 50c6cd1 to edcb6dc Compare February 11, 2026 20:01
    @mdavis36
    Copy link
    Collaborator Author

    !test

    The old IrContainer approach popped special vals (zeroVal, oneVal, etc.)
    from vals_up_ after creation. During Fusion::copy, these vals were not
    cloned through the normal deterministic_vals() path. Instead, they were
    first cloned during axiom cloning, which happened AFTER
    val_type_name_map_
    was overridden from the source — causing the name counter to be
    incremented 1 past the source value.
    
    Now that special vals remain in vals_up_, they are properly cloned
    before
    the counter override, so the counter stays accurate. This shifts loop
    index val names down by 1 (e.g., i113 instead of i114). The index
    expression structure is unchanged.
    @mdavis36
    Copy link
    Collaborator Author

    !test

    Special vals (trueVal, falseVal, oneVal, etc.) can be lazily created
    inside a StatementGuard scope (e.g. by simplifyExpr called from
    haveDifferentShardings). When the guard rolls back, it pops vals_up_
    back to the snapshot, destroying those vals while the Fusion cache
    pointers still reference them. Subsequent calls return dangling pointers
    causing UB — this manifested as LoopShardedSplitReshapeIds incorrectly
    classifying a reshape as resharding on CI.
    
    Fusion::removeStatementsCreatedAfter now nulls out any special val cache
    pointers that are about to be destroyed, so they get re-created on next
    access.
    SubstituteInExpr directly sets mutations_[reference] = substitute
    without checking reference == substitute, unlike registerMutation
    which guards against this. With per-Fusion special vals, Fusion::copy
    now maps zero_val_ through the cloner so that IterDomain extents and
    zero_val_ share the same pointer. When concretizeEmptyExtents finds
    an extent that IS zero_val_, SubstituteInExpr created a self-mapping
    that tripped the two-hop assertion in maybeMutated.
    
    Why this didn't happen before:
    
      Old code (main):
        zero_val_ was stored in a separate unique_ptr, popped from
        vals_up_. Fusion::copy didn't wire it up — B->zeroVal() lazily
        created a brand new Val, so ext != zero always held.
    
      New code (this branch):
        zero_val_ lives in vals_up_ like any other Val. Fusion::copy
        remaps it via ir_cloner.clone(), so B->zero_val_ IS the same
        cloned Val that IterDomain extents reference:
    
          Fusion A                         Fusion B (clone)
          ┌─────────────────┐             ┌──────────────────┐
          │ zero_val_ ─► 0x1111           │ zero_val_ ─► 0x2222
          │ id->extent() ─► 0x1111        │ id->extent() ─► 0x2222
          └─────────────────┘             └──────────────────┘
                                clone maps 0x1111 → 0x2222
    
        So ext == zero, and SubstituteInExpr(ext, zero) created:
          mutations_[0x2222] = 0x2222  (self-mapping)
        Then maybeMutated looked up 0x2222, found itself, treated
        it as a two-hop chain, and asserted.
    @mdavis36 mdavis36 force-pushed the md/dep-special-types branch from 8112846 to 1588d37 Compare February 12, 2026 01:37
    @mdavis36
    Copy link
    Collaborator Author

    !test

    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