Skip to content

Remove name conflicts in loop iterators#2326

Open
ThrudPrimrose wants to merge 22 commits into
mainfrom
ssa_loop_iterators
Open

Remove name conflicts in loop iterators#2326
ThrudPrimrose wants to merge 22 commits into
mainfrom
ssa_loop_iterators

Conversation

@ThrudPrimrose
Copy link
Copy Markdown
Collaborator

@ThrudPrimrose ThrudPrimrose commented Mar 16, 2026

This pass ensures that there are no name conflicts among loop-range iterators in the SDFG.

@ThrudPrimrose ThrudPrimrose marked this pull request as ready for review April 15, 2026 20:50
@tbennun tbennun changed the title SSA loop iterators Remove name conflicts in loop iterators May 12, 2026
Copy link
Copy Markdown
Collaborator

@tbennun tbennun left a comment

Choose a reason for hiding this comment

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

Some changes

from typing import Union


def replace_symbol_by_name(expr: sp.Basic, old_name: str, new: Union[str, sp.Basic]) -> sp.Basic:
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

we have symbolic replace and subs etc.
Why the new function?

Comment thread dace/transformation/passes/ssa_loop_iterators.py Outdated

@dace.properties.make_properties
@explicit_cf_compatible
class SSALoopIterators(ppl.Pass):
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

same for this class name

@explicit_cf_compatible
class SSALoopIterators(ppl.Pass):
loop_var_counter = 0
FOR_IT_NAME = "_it"
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

should this be a class variable or just a global constant

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

I guess this one is a global constant, I updated accordingly.

Comment thread dace/transformation/passes/ssa_loop_iterators.py Outdated
def should_reapply(self, modified: ppl.Modifies) -> bool:
return False

def _repl_recursive(self, cfg: ControlFlowRegion | dace.SDFG, loop_var: str, next_ssa_loop_var: str):
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

I feel like dace.sdfg.replace.replace_dict would do a good job here, if applied selectively

Comment thread dace/transformation/passes/ssa_loop_iterators.py Outdated
The rename in ``_apply_recursive`` is scoped to the LoopRegion via
``_rename_one_loop_var(cfg, ...)``, so after a successful pass the
LoopRegion's body, init / condition / update statements, and every
nested-SDFG ``symbol_mapping`` referencing the old name have been
rewritten to ``_loop_it_<N>``. The default
``assign_loop_iterator_post_value=False`` path stops there.

Problem: the frontend declares the original loop variable in the
**enclosing** SDFG's symbol table (e.g. ``sdfg.symbols['i']``), one
level above the LoopRegion. The scoped rename never touches that
declaration. With the post-value epilogue disabled there is also no
``<old_name> = <post_value>`` interstate assignment to keep the symbol
alive, so the declaration is dead -- yet it still leaks as a phantom
free symbol on the enclosing NestedSDFG boundary, producing
``Missing symbols on nested SDFG: ['i']`` at validation time and
blocking any pass that re-checks the SDFG (LoopToMap, MapFission, …).

Fix: in the no-postamble branch only, after the rename, drop the
declaration when it is provably dead -- ``old_name in sdfg.symbols``
AND ``old_name not in sdfg.free_symbols`` (the second clause is what
prevents accidental removal if some reader survived). The default
``assign_loop_iterator_post_value=True`` path is unaffected: the
epilogue's assignment keeps the symbol live, the ``not in
sdfg.free_symbols`` check would refuse anyway, and -- to make the
distinction structural rather than implicit -- the cleanup sits in the
``elif`` branch of the existing if/else on the property.

Tests:
- ``test_no_postamble_drops_dead_symbol_declaration``: with
  ``assign_loop_iterator_post_value=False`` the dead ``i`` declaration
  is gone from every body NestedSDFG and ``sdfg.validate()`` succeeds
  (without the fix it raises ``Missing symbols on nested SDFG: ['i']``).
- ``test_postamble_preserves_symbol_declaration``: with the default
  ``True`` the postamble's assignment keeps ``i`` alive, the cleanup
  must NOT trigger; the declaration must survive.
The yakup/dev branch refactored test_postamble_preserves_symbol_declaration
to use UniqueLoopIterators() directly (default post-value=True) instead
of explicitly setting the attribute. Same semantics, 3 lines tighter.

10/10 unique_loop_iterators tests still pass.
…symbols``

The no-postamble cleanup branch in ``UniqueLoopIterators._apply_recursive``
removes ``old_name`` from ``sdfg.symbols`` once it is no longer referenced
by any rename target. It gated on ``old_name not in sdfg.free_symbols``,
but ``SDFG.free_symbols`` calls ``used_symbols(all_symbols=True)``, and
``ControlFlowRegion._used_symbols_internal`` unconditionally folds
``self.symbols.keys()`` back into the "free" set when ``all_symbols`` is
True (the ``if all_symbols: free_syms |= set(self.symbols.keys())``
branch). The gate is therefore circular: the declared symbol always
appears "free" by virtue of being declared, the cleanup never fires, and
the dead declaration lingers until the validator catches it with
``Missing symbols on nested SDFG: ['<old_name>']`` on the enclosing
NestedSDFG.

Switch the gate to ``used_symbols(all_symbols=False)``, which excludes
the ``sdfg.symbols.keys()`` self-fold and reflects only actual
code-generation usage. With this gate the cleanup correctly removes
``old_name`` exactly when no memlet subset, tasklet body, interstate-edge
condition/assignment or descriptor expression still references it.

Reproducer: a per-row reduction with an inner accumulator
(``for i: s = ...; for j: s += a[i, j-1] + a[i, j] + a[i, j+1]; b[i] = s``).
Before this fix, the inner ``j`` lingered in the body NestedSDFG's
``sdfg.symbols`` post-rename and the SDFG failed validation. After the
fix the declaration is removed, validation passes, and the numerical
result matches the pure-numpy oracle.
…; fixes nested-SDFG symbol-mapping crash)

The pass re-renamed every LoopRegion iterator on each invocation, including
loops already in ``_loop_it_<N>`` form from an earlier run (the canonicalize
pipeline runs UniqueLoopIterators twice: the ``clean`` and ``ssa`` stages).
Re-renaming an already-unique iterator that a deeply-nested SDFG imports drops
the import from the grandchild NSDFG's ``symbol_mapping`` (the re-key does not
survive a second rename of an already-imported iterator), leaving a phantom
free symbol -> "Missing symbols on nested SDFG: ['_loop_it_<N>']" at validation.

Fix: skip renaming a loop variable already in ``_loop_it_*`` form. This makes
the pass idempotent (a second application is a no-op) and avoids the
destructive double-rename. New test test_idempotent_skips_already_unique_iterators
covers it; the existing suite stays green.
…sambiguation

Two improvements to the pass:

* Replace the class-static ``_loop_var_counter`` (mutable global state that made
  iterator names depend on call/test order) with a per-call counter seeded just
  past any ``_loop_it_<N>`` already present anywhere in the SDFG tree
  (``_first_free_id``). Renames never collide with an existing unique name and
  the numbering is deterministic for a given SDFG regardless of prior runs.
  A fresh SDFG still yields ``_loop_it_0, _loop_it_1, ...``.

* Re-disambiguate DUPLICATE ``_loop_it_<N>`` names: the idempotency skip-guard
  now skips only iterators that are unique, so two LoopRegions that share a name
  (e.g. left behind by a cloning pass) are renamed apart instead of aliasing
  (which would block downstream passes such as LoopToMap).

Tests: drop the now-pointless ``_loop_var_counter`` resets, switch ``__main__``
to ``pytest.main``, and add four cases -- triply-nested counted loops (deep
cascade + value preservation), a negative-step counted DO, a scan-seed
collision (existing ``_loop_it_5`` beside ``i`` -> ``i`` becomes
``_loop_it_6``), and duplicate-iterator disambiguation (two LoopRegions sharing
``_loop_it_0`` -> distinct names).
…ue as a constructor argument

UniqueLoopIterators had no __init__, so the only way to set the post-value knob
was a post-construction attribute assignment. Add an __init__ taking
assign_loop_iterator_post_value (default True, matching the Property) and switch
the unit-test call sites to the constructor keyword.
… too

``_first_free_id`` scanned only ``LoopRegion`` loop variables when picking the
next ``_loop_it_<N>`` id, ignoring ``MapEntry`` parameters. ``LoopToMap`` keeps
a loop's ``_loop_it_<N>`` name as the map parameter, so when ``UniqueLoopIterators``
runs after a ``LoopToMap`` stage a map carries that name; reusing the id for a
freshly-renamed loop aliased two distinct iteration variables and silently
corrupted results (e.g. canonicalize's early-LoopToMap path on velocity_advection).
Scan map parameters as well so the seeded counter avoids both.

Adds two unit tests: a structural seed-past-map-parameter test and an
end-to-end loop+map coexistence value-preservation test.

(cherry picked from commit cf64e5d5e3b43843f33e655a0b0ff74c2de9fd70)
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