spcl / spcl/dace

Python frontend: scalar assignment `b = a` aliases the container instead of copying the value

Open
#2,491 0 comments 1 reaction 0 assignees View on GitHub
Dominant language
Python
Stars
593
Forks
163
Avg merge
2d 23h
Merged PRs (30d)
60

Description

## Description

In the Python frontend, assigning one scalar variable to another (`b = a`) does not copy the
value: `b` becomes a second name for `a`'s data container. Since Python scalars rebind instead
of mutating, any later write through either name silently corrupts the other. There is no error
or warning and the SDFG is valid -- only the result is wrong.

## Reproducer

```python
import numpy as np
import dace

@dace.program
def plain(out: dace.float64[2]):
a = 1.0
b = a # `b` aliases `a`'s container
b += 1.0
out[0] = a
out[1] = b

out = np.zeros(2)
plain(out=out)
print(out)
```

Expected (NumPy/Python semantics): `[1. 2.]`
Actual: `[2. 2.]`

A more damaging shape of the same bug, where a branch dispatch destroys the source:

```python
@dace.program
def branches(out: dace.float64[6]):
rp0 = 10.0
rp1 = 20.0
rp2 = 30.0
for idir in range(3):
if idir == 0:
pc = rp0 # `pc` aliases rp0
elif idir == 1:
pc = rp1 # writes rp1's value INTO rp0
else:
pc = rp2 # writes rp2's value INTO rp0
out[idir] = pc
out[3] = rp0
out[4] = rp1
out[5] = rp2

out = np.zeros(6)
branches(out=out)
print(out)
```

Expected `[10. 20. 30. 10. 20. 30.]`, actual `[10. 20. 30. 30. 20. 30.]`. The dispatched reads are
all correct, only the original `rp0` is gone; with `simplify=False` the SDFG contains
`{out, pc, rp1, rp2}` -- there is no `rp0` container at all.

The same mechanism makes chained initialisation collapse accumulators: `tmp = 0.0; s0 = tmp;
s1 = tmp` gives `s0` and `s1` one shared container, so an n-way unrolled reduction over-counts
by exactly n.

## Code path

`ProgramVisitor._visit_assign` in `dace/frontend/python/newast.py`. When the target is not yet
defined and the right-hand side names an existing container, a *non*-transient result is copied
via `_add_transient_data`, but a transient one falls through to a bare alias:

```python
elif not result_data.transient or result in self.sdfg.constants_prop:
true_name, new_data = _add_transient_data(self, self.sdfg, result_data, dtype)
self.variables[name] = true_name
defined_vars[name] = true_name
else:
self.variables[name] = result # alias, no copy
defined_vars[name] = result
continue
```

That is correct for arrays -- NumPy aliases arrays too -- but wrong for scalars. The reassignment
guard earlier in the same function explicitly exempts scalars
(`not isinstance(true_array, data.Scalar) and not (true_array.shape == (1, ))`), so the second
`pc = ...` is not caught either and stores straight into the aliased container.

Taking the `_add_transient_data` copy path when `result_data` is a `data.Scalar`, and keeping the
alias only for real arrays, fixes all of the above; every scalar-valued frontend temporary is a
`data.Scalar`, so shape-`(1,)` arrays keep NumPy aliasing semantics.

## Environment

dace `2.0.0a5` (current `main`), Python 3.12, NumPy 2.4, Linux x86_64.

## Workaround

Force a fresh container with a no-op operation (`b = a + 0.0`), or repeat the initialiser at every
target instead of chaining.

Contributor guide

Open the contributing guide

Research direction

Start in dace/frontend/python/newast.py at ProgramVisitor._visit_assign and inspect the transient versus non-transient assignment paths, especially the data.Scalar check. Run the plain and branches reproducers from the issue to confirm the current behavior. Done means scalar assignments produce [1., 2.] and [10., 20., 30., 10., 20., 30.] while real array assignments retain NumPy aliasing semantics.

Written by the indexing model from the issue text.

Assessment

Tech stack
python
Domain
compilers
Issue type
Bug
Difficulty
3/5
Estimated time
1-2 days
Activity status
Quiet
Clarity
Clearly specified
Newbie friendliness
76/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.