Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
5c71e9d
fix(sdfg): symbols_defined_at includes enclosing LoopRegion loop vari…
ThrudPrimrose May 28, 2026
724e979
fix(sdfg): resolve output-array arg from the memlet path terminal, no…
ThrudPrimrose May 27, 2026
26e1a35
fix(sdfg): resolve exit-write arg from the memlet tree root
ThrudPrimrose May 27, 2026
1cd1a4a
fix(validation): guard veclen lookup on non-AccessNode endpoints
ThrudPrimrose May 20, 2026
620f245
test(validation): regression catcher for veclen guard on non-AccessNo…
ThrudPrimrose May 31, 2026
0650eb9
fix(symbolic): preserve integer-valued floats and round-trip near-max…
ThrudPrimrose May 27, 2026
08c0d6d
Fix sympy floor-vs-int_floor codegen bug + branch_elimination print typo
ThrudPrimrose May 14, 2026
b207a70
test(symbolic): regression catcher for cpp_mode floor recombination
ThrudPrimrose May 31, 2026
738bf78
fix(trivial-tasklet-elimination): preserve read offset when source is…
ThrudPrimrose May 21, 2026
f9efd67
fix(condition_fusion, early_exit): replace hasattr(node, "sdfg") with…
ThrudPrimrose May 30, 2026
32a5701
fix(map_fusion, condition_fusion): InOut connector split + safe paren…
ThrudPrimrose May 30, 2026
1d2c91a
fix(redundant-array-copying): require full identity + out-degree==1 f…
ThrudPrimrose May 31, 2026
072ede2
feat(wcr): detect copy-wrapped read-modify-write in AugAssignToWCR
ThrudPrimrose May 25, 2026
22ffc15
fix(sdfg): only code-block-referenced arrays expand extent symbols (#…
ThrudPrimrose May 27, 2026
2909695
refactor(sdfg): derive used-array extent symbols from read_and_write_…
ThrudPrimrose May 27, 2026
9783b78
test(sdfg): exit-write arglist with source-relative outgoing memlet
ThrudPrimrose May 31, 2026
414765e
refactor(redundant-array-copying): tighten RedundantArrayCopyingIn st…
ThrudPrimrose May 31, 2026
277c514
chore: strip kernel-specific references (TSVC sNNN, cloudsc/ICON) fro…
ThrudPrimrose May 31, 2026
daa0840
style: pre-commit (ruff --fix, yapf) on main-bugfixes touched files
ThrudPrimrose May 31, 2026
de2acd5
fix(symbolic): _format_float must use repr for guaranteed round-trip
ThrudPrimrose May 31, 2026
2b5ba9e
fix(symbol-propagation): same-edge race guard + cross-CFG assert soft…
ThrudPrimrose May 21, 2026
8348db5
test(symbol-propagation): 21 hard adversarial tests (indirection, int…
ThrudPrimrose May 21, 2026
86f28c1
fix(symbol-propagation): resolve edge RHSes (simultaneous semantics) …
ThrudPrimrose May 21, 2026
f9234a0
fix(symbol-propagation): return propagated symbols (or None) so Fixed…
ThrudPrimrose May 21, 2026
89e71ed
test(symprop): cloudsc kidia/kfdia pattern — promote scalar args befo…
ThrudPrimrose May 22, 2026
1f5c3c5
fix(passes): SymbolPropagation -- eliminate dead iedge assignments af…
ThrudPrimrose May 28, 2026
33ea2b6
fix(passes): SymbolPropagation -- substitute symbols into array descr…
ThrudPrimrose May 28, 2026
7598db1
fix(symbol-propagation): invalidate carried value whose RHS reads a s…
ThrudPrimrose May 31, 2026
0df873e
fix(wcr): resolve View descriptor via state.sdfg, not the outer sdfg
ThrudPrimrose May 31, 2026
98a6891
LoopToMap: prove iteration-independence through a NestedSDFG body
ThrudPrimrose May 19, 2026
d220255
LoopToMap: ASCII example of the NestedSDFG iter-index recovery
ThrudPrimrose May 19, 2026
a197ee7
LoopToMap: refuse when loop range reads a body-assigned symbol
ThrudPrimrose May 20, 2026
c1dad83
fix(loop-to-map): a shared affine iteration-var dimension is cross-it…
ThrudPrimrose May 27, 2026
a8776a5
fix(loop-to-map, licm): parallelize the peeled fixed-read pattern
ThrudPrimrose May 26, 2026
ec45444
style: yapf on symbol_propagation_test.py
ThrudPrimrose May 31, 2026
484f7a9
fix(symbolic): route Python float through _format_float in serialize_…
ThrudPrimrose Jun 1, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 9 additions & 12 deletions dace/sdfg/sdfg.py
Original file line number Diff line number Diff line change
Expand Up @@ -1427,20 +1427,17 @@ def _used_symbols_internal(self,
free_syms=free_syms,
used_before_assignment=used_before_assignment,
with_contents=with_contents)
# Expand array-descriptor stride/shape/offset symbols into the free
# set. Without this, a ``ConditionalBlock`` guard or memlet subset
# referencing ``A[i, j]`` leaves the symbols used in ``A`` 's strides
# out of the computed free-symbol set, causing
# ``generate_nsdfg_header`` to emit a nested function signature
# missing those symbols, ceating an invalid SDFG.
# A used array needs its stride/shape/offset symbols in the free set, but a
# merely-declared one must not leak its shape symbol into the signature
# (issue #2382). ``read_and_write_sets`` already reports exactly the arrays
# that are used -- read or written, including those referenced only by a
# code-block guard/condition -- so expand the extent symbols of those alone.
res_free, res_defined, res_before = result
if with_contents:
for desc in self.arrays.values():
res_free |= {str(s) for s in desc.used_symbols(all_symbols)}
# Don't drag in symbols that are genuinely defined inside this
# SDFG (e.g., LoopRegion loop variables); keep only the ones
# outside ``defined_syms``.
res_free -= res_defined
read_set, write_set = self.read_and_write_sets()
for name in (read_set | write_set) & self.arrays.keys():
res_free |= {str(s) for s in self.arrays[name].used_symbols(all_symbols)}
res_free -= res_defined # drop symbols defined inside (e.g. loop vars)
return res_free, res_defined, res_before

def get_all_toplevel_symbols(self) -> Set[str]:
Expand Down
32 changes: 25 additions & 7 deletions dace/sdfg/state.py
Original file line number Diff line number Diff line change
Expand Up @@ -921,16 +921,20 @@ def unordered_arglist(self,
} if top_source_edge.src.data not in descs else {})

elif isinstance(edge.dst, nd.ExitNode) and isinstance(edge.src, (nd.AccessNode, nd.CodeNode)):
# Same case as above, but for outgoing Memlets.
# NOTE: We have to use a memlet tree here, because the data could potentially
# go to multiple sources. We have to do it this way, because if we would call
# `memlet_tree()` here, then we would just get the edge back.
# Same case as above, but for outgoing Memlets. The Memlet leaving the
# scope may be source-relative (naming the inner transient rather than
# the external array), so resolve the written array from the memlet
# tree's root -- the outermost-scope node, i.e. the destination the
# data fans out to (fall back to the Memlet's data otherwise).
additional_descs = {}
connector_to_look = "OUT_" + edge.dst_conn[3:]
for oedge in self.graph.out_edges_by_connector(edge.dst, connector_to_look):
if ((not oedge.data.is_empty()) and (oedge.data.data not in descs)
and (oedge.data.data not in additional_descs)):
additional_descs[oedge.data.data] = sdfg.arrays[oedge.data.data]
if oedge.data.is_empty():
continue
root_dst = self.graph.memlet_tree(oedge).root().edge.dst
dst_name = root_dst.data if isinstance(root_dst, nd.AccessNode) else oedge.data.data
if dst_name not in descs and dst_name not in additional_descs:
additional_descs[dst_name] = sdfg.arrays[dst_name]

else:
# Case is ignored.
Expand Down Expand Up @@ -1643,6 +1647,20 @@ def symbols_defined_at(self, node: nd.Node) -> Dict[str, dtypes.typeclass]:
for e in sdfg.edges():
symbols.update(e.data.new_symbols(sdfg, symbols))

# Add the loop variables of the control-flow loops enclosing this state,
# outermost first. Without this only global, inter-state-edge and dataflow-scope
# (map) symbols are seen; a node inside a LoopRegion must also see the loop
# variable as defined -- e.g. so memlet propagation keeps a ``jk``-indexed
# nested-SDFG access parametric instead of widening it to the whole array.
enclosing_loops = []
cfg = self.parent_graph
while cfg is not None:
if isinstance(cfg, LoopRegion) and cfg.loop_variable:
enclosing_loops.append(cfg)
cfg = cfg.parent_graph
for loop in reversed(enclosing_loops):
symbols.update(loop.new_symbols(symbols))

# Find scopes this node is situated in
sdict = self.scope_dict()
scope_list = []
Expand Down
17 changes: 12 additions & 5 deletions dace/sdfg/validation.py
Original file line number Diff line number Diff line change
Expand Up @@ -889,20 +889,27 @@ def validate_state(state: 'dace.sdfg.SDFGState',
)

# Verify that source and destination subsets contain the same
# number of elements
# number of elements. Only AccessNode endpoints expose a ``.data``
# descriptor whose ``veclen`` participates in this check; scope
# nodes (NestedSDFG, MapEntry/Exit, ConsumeEntry/Exit) route data
# through connectors and contribute ``veclen = 1`` to the count.
if not e.data.allow_oob and e.data.other_subset is not None and not (
(isinstance(src_node, nd.AccessNode) and isinstance(sdfg.arrays[src_node.data], dt.Stream)) or
(isinstance(dst_node, nd.AccessNode) and isinstance(sdfg.arrays[dst_node.data], dt.Stream))):
src_expr = (e.data.src_subset.num_elements() * sdfg.arrays[src_node.data].veclen)
dst_expr = (e.data.dst_subset.num_elements() * sdfg.arrays[dst_node.data].veclen)
src_veclen = sdfg.arrays[src_node.data].veclen if isinstance(src_node, nd.AccessNode) else 1
dst_veclen = sdfg.arrays[dst_node.data].veclen if isinstance(dst_node, nd.AccessNode) else 1
src_expr = e.data.src_subset.num_elements() * src_veclen
dst_expr = e.data.dst_subset.num_elements() * dst_veclen
if symbolic.inequal_symbols(src_expr, dst_expr):
error = InvalidSDFGEdgeError('Dimensionality mismatch between src/dst subsets', sdfg, state_id, eid)
# NOTE: Make an exception for Views and reference sets
from dace.sdfg import utils
if (isinstance(sdfg.arrays[src_node.data], dt.View) and utils.get_view_edge(state, src_node) is e):
if (isinstance(src_node, nd.AccessNode) and isinstance(sdfg.arrays[src_node.data], dt.View)
and utils.get_view_edge(state, src_node) is e):
warnings.warn(error.message)
continue
if (isinstance(sdfg.arrays[dst_node.data], dt.View) and utils.get_view_edge(state, dst_node) is e):
if (isinstance(dst_node, nd.AccessNode) and isinstance(sdfg.arrays[dst_node.data], dt.View)
and utils.get_view_edge(state, dst_node) is e):
warnings.warn(error.message)
continue
if e.dst_conn == 'set':
Expand Down
79 changes: 72 additions & 7 deletions dace/symbolic.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import contextlib
from collections import Counter
from functools import lru_cache
import math
import sympy
import pickle
import re
Expand Down Expand Up @@ -564,11 +565,15 @@ def _typed_constant_suffix(dtype: dtypes.typeclass) -> str:


def _format_float(value: float) -> str:
# Shortest round-trip form, keeping one fractional digit (5.0, not 5 or 5.000...).
s = f'{float(value):.15g}'
# ``repr`` for finite Python floats is the shortest decimal that
# round-trips through ``float()`` -- guaranteed idempotent under
# save->load->save and at most 17 significant digits for fp64.
f = float(value)
s = repr(f)
if 'e' in s or 'E' in s:
return s
if '.' not in s:
# Keep one fractional digit so an integer-valued float stays floating-point.
return s + '.0'
int_part, frac_part = s.split('.')
return f'{int_part}.{frac_part.rstrip("0") or "0"}'
Expand Down Expand Up @@ -985,6 +990,14 @@ def sympy_numeric_fix(expr):
""" Fix for printing out integers as floats with ".00000000".
Converts the float constants in a given expression to integers. """
if not isinstance(expr, sympy.Basic) or isinstance(expr, sympy.Number):
# Preserve a finite float -- sympy.Float, Python float, or numpy float --
# so an integer-valued float like 1.0 stays 1.0 and is never collapsed to
# int 1 below: that would change its type (min(x, 1) mixes double and int)
# and round-trip through SymPy as a mistyped Min/Max. 0.0 was only spared by
# the ``expr != 0`` clause; a Python float 1.0 hit the int() collapse.
# Non-finite values (+-1.8e308 -> inf) fall through to the overflow path.
if isinstance(expr, (sympy.Float, float, numpy.floating)) and math.isfinite(float(expr)):
return expr if isinstance(expr, sympy.Float) else sympy.Float(expr)
try:
# NOTE: If expr is ~ 1.8e308, i.e. infinity, `numpy.int64(expr)`
# will throw OverflowError (which we want).
Expand Down Expand Up @@ -1970,7 +1983,13 @@ def _print_Integer(self, expr):
return super()._print_Integer(expr)

def _print_Float(self, expr):
return _format_float(float(sympy_numeric_fix(expr)))
nf = sympy_numeric_fix(expr)
if not math.isfinite(float(nf)):
# The value exceeds a C double (e.g. Fortran ``HUGE``, just over the max):
# let sympy print its own shortest decimal instead of overflowing through
# ``float()`` to a spurious ``inf`` (which would then render as ``inf.0``).
return super()._print_Float(nf)
return _format_float(float(nf))

def _print_Add(self, expr):
flat_args = []
Expand Down Expand Up @@ -2044,7 +2063,11 @@ def _serialize_symbolic_uncached(expr: Union[SymbolicType, int, float, numpy.num
if isinstance(expr, int) and not isinstance(expr, bool):
return str(expr)
if isinstance(expr, float):
return sympy.printing.str.sstr(expr)
# Route through the shared formatter so a Python float reaches the same
# repr-based shortest-round-trip path the sympy.Float branch uses below.
# Otherwise sympy's default sstr emits a 15-sig-digit form that fails
# the SDFG save->load->save equality check.
return _format_float(expr)
if isinstance(expr, sympy.Basic):
return DaceSympySerializer().doprint(expr)
return str(expr)
Expand Down Expand Up @@ -2223,10 +2246,14 @@ def __init__(self, arrays, cpp_mode=False, *args, **kwargs):
self._settings['full_prec'] = False

def _print_Float(self, expr):
# Shortest round-tripping form, always keeping one fractional digit so an
# integer-valued float stays floating-point (``5.0``, not ``5``).
nf = sympy_numeric_fix(expr)
if isinstance(nf, int) or nf != expr:
return self._print(nf)
return super()._print_Float(expr)
if not math.isfinite(float(nf)):
# Exceeds a C double (e.g. Fortran ``HUGE``): keep sympy's shortest
# decimal rather than overflowing to ``inf`` (rendered as ``inf.0``).
return super()._print_Float(nf)
return _format_float(float(nf))

def _print_TypedConstant(self, expr):
value = self._print(expr.value)
Expand Down Expand Up @@ -2287,6 +2314,44 @@ def _print_Function(self, expr):
def _print_Mod(self, expr):
return '((%s) %% (%s))' % (self._print(expr.args[0]), self._print(expr.args[1]))

def _print_floor(self, expr):
"""sympy ``floor(...)`` printer.

sympy's ``//`` operator on symbolic integers (e.g. ``(LEN - 1) // 8``)
simplifies to ``floor(LEN/8 - 1/8)`` where ``1/8`` becomes a
``Rational(1, 8)``. Without this override the printer emits a literal
``floor(LEN/8 - 1/8)`` which in C++ collapses ``1 / 8`` to ``0`` via
integer division, so the floor argument becomes ``LEN/8`` instead of
``(LEN - 1) / 8`` -- the loop bound silently overshoots by one.

Recombine: if the floor argument is an addition of fractions with a
common denominator, reassemble the numerator and emit a single
``((numerator) / (denominator))`` integer division. Otherwise fall
through to the math-library ``floor(...)`` call.
"""
if not self.cpp_mode:
return super()._print_Function(expr) if hasattr(super(), "_print_Function") else super()._print_floor(expr)
arg = expr.args[0]
# Try to combine to a single ``Rational(num, den)``: when arg is
# ``a/b + c/d + ...`` sympy's ``.together()`` rewrites to a
# single fraction over a common denominator. ``as_numer_denom``
# then splits it cleanly.
try:
arg_together = arg.together()
num, den = arg_together.as_numer_denom()
except Exception:
num, den = None, None
if num is not None and den is not None:
try:
den_int = int(den)
except (TypeError, ValueError):
den_int = None
if den_int is not None and den_int != 1 and den_int != 0:
return '((%s) / (%s))' % (self._print(num), self._print(den))
# Fallback: pure-real floor (e.g. ``floor(sin(x))``); emit the
# math-library call.
return 'floor(%s)' % self._print(arg)

def _print_Equality(self, expr):
return '((%s) == (%s))' % (self._print(expr.args[0]), self._print(expr.args[1]))

Expand Down
Loading
Loading