Symbol-dependent transient allocated before its shape symbol is defined (shape symbol assigned inside a ConditionalBlock)
- Dominant language
- Python
- Stars
- 593
- Forks
- 163
- Avg merge
- 2d 23h
- Merged PRs (30d)
- 60
Description
## Summary
A transient whose **shape depends on a symbol that is assigned inside a `ConditionalBlock`** is allocated **before that symbol is defined**, when the transient is accessed in two or more blocks after the conditional. The generated `new T[s]` runs with `s` still uninitialized, producing a wrong-size allocation and out-of-bounds writes (observed downstream as glibc `free(): invalid next size` / `corrupted size vs. prev_size`).
## Reproducer (pure DaCe, no frontend)
```python
import dace
from dace.sdfg.state import LoopRegion, ConditionalBlock, ControlFlowRegion
from dace.properties import CodeBlock
sdfg = dace.SDFG('alloc_before_symbol')
for sym in ('cond', 'n', 'm'):
sdfg.add_symbol(sym, dace.int64)
sdfg.add_symbol('s', dace.int64)
sdfg.add_array('out', [10], dace.float64)
sdfg.add_transient('a', ['s'], dace.float64) # shape symbol `s`
pre = sdfg.add_state('pre', is_start_block=True)
# Conditional that defines `s` on its branch interstate edges.
cb = ConditionalBlock('cb')
sdfg.add_node(cb)
sdfg.add_edge(pre, cb, dace.InterstateEdge())
then_reg = ControlFlowRegion('then_reg', sdfg=sdfg)
t0 = then_reg.add_state('t0', is_start_block=True); t1 = then_reg.add_state('t1')
then_reg.add_edge(t0, t1, dace.InterstateEdge(assignments={'s': 'n'}))
cb.add_branch(CodeBlock('cond > 0'), then_reg)
else_reg = ControlFlowRegion('else_reg', sdfg=sdfg)
e0 = else_reg.add_state('e0', is_start_block=True); e1 = else_reg.add_state('e1')
else_reg.add_edge(e0, e1, dace.InterstateEdge(assignments={'s': 'm'}))
cb.add_branch(CodeBlock('not (cond > 0)'), else_reg)
# Loop after the conditional -> first access of `a`.
loop = LoopRegion('loop', loop_var='i', initialize_expr='i = 0',
condition_expr='i < s', update_expr='i = i + 1')
sdfg.add_node(loop)
sdfg.add_edge(cb, loop, dace.InterstateEdge())
body = loop.add_state('body', is_start_block=True)
w = body.add_tasklet('w', {}, {'o'}, 'o = 1.0')
body.add_edge(w, 'o', body.add_access('a'), None, dace.Memlet('a[i]'))
# Post-loop -> second access of `a`.
post = sdfg.add_state('post')
sdfg.add_edge(loop, post, dace.InterstateEdge())
r = post.add_tasklet('r', {'x'}, {'o'}, 'o = x')
post.add_edge(post.add_access('a'), None, r, 'x', dace.Memlet('a[0]'))
post.add_edge(r, 'o', post.add_access('out'), None, dace.Memlet('out[0]'))
sdfg.validate()
print('\n'.join(c.clean_code for c in sdfg.generate_code()))
```
## Generated code (wrong order)
```cpp
a = new double DACE_ALIGN(64)[s]; // allocation
...
s = n; // shape symbol defined here (then-branch)
...
s = m; // ... or here (else-branch)
```
`a` is sized by `s`, but `s` is assigned only *after* the allocation, so the buffer is allocated with an uninitialized `s`.
## Root cause
In `dace/codegen/targets/framecode.py`, `determine_allocation_lifetime` correctly flags `a` as symbol-dependent (`is_nonfree_sym_dependent` returns `True`, since `s` is not a free SDFG symbol). Because `a` is accessed in two blocks (`body`, `post`), it takes the multi-state path and calls `_get_dominator_and_postdominator`, which returns the **pre-conditional** state (`pre`) as the closest common dominator. The allocation is then emitted at that dominator — upstream of where `s` is assigned (inside the `ConditionalBlock` branches).
`_get_dominator_and_postdominator` already anticipates this:
```python
# TODO(later): If any of the symbols were not yet defined, or have changed afterwards, fail
# raise NotImplementedError
```
i.e. the case where the chosen dominator does not have the descriptor's shape symbols defined is known to be unhandled.
## Expected behaviour / proposed fix
If a symbol-dependent transient's allocation lifetime is `S1 -> S2`, the allocation must be emitted **before `S1` starts but after all interstate-edge assignments that precede `S1`** — in particular, the chosen `S1` must post-dominate every interstate-edge assignment to the descriptor's shape symbols. For the reproducer, `S1` must be a block reached *after* the `ConditionalBlock` (where `s` is merged-defined), not the pre-conditional state.
Concretely, the symbol-dependent branch in `determine_allocation_lifetime` (resp. `_get_dominator_and_postdominator`) should advance the allocation state forward until all of the descriptor's shape symbols are defined on entry (e.g. to the post-dominator of their definitions that still dominates the accesses), instead of allocating at a dominator where they are not yet defined.
(Validated locally: inserting a single guard state on the edge out of the symbol-defining `ConditionalBlock` moves the allocation after `s = n/m` and produces correct code — confirming the placement is the issue.)
## Environment
- DaCe: commit `8dc9e92a4` (branch `FaCe`)
- Python: 3.13.7
## Attached SDFG
The reproducer SDFG is attached as `dace_alloc_bug_repro.sdfg.txt` (GitHub
rejects the `.sdfg` extension; rename to `.sdfg` to load via
`dace.SDFG.from_file`). The inline Python script above regenerates it.
Contributor guide
Assessment
This issue has not been assessed yet.