Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
91 commits
Select commit Hold shift + click to select a range
1d24a78
replacement type inference
tbennun Apr 12, 2026
fe49990
tests
tbennun Apr 12, 2026
2321866
stree node updates (WIP)
tbennun Apr 12, 2026
345074f
Several type inference replacements
tbennun Apr 12, 2026
6cd1fc2
Python: Preprocessing normalization passes for iterators, comprehensi…
tbennun Apr 12, 2026
54f49b4
Very WIP schedule tree-based Python frontend
tbennun Apr 12, 2026
ac945fe
API
tbennun Apr 12, 2026
e0c6882
Some change in treenodes that will be reverted later
tbennun Apr 12, 2026
916e25a
More API
tbennun Apr 12, 2026
a11b4c2
Remove special case
tbennun Apr 13, 2026
56ee83a
Better pyobject support
tbennun Apr 13, 2026
3c6fba5
Support async functions
tbennun Apr 13, 2026
c1c78f4
Initial match/case support
tbennun Apr 13, 2026
e9286da
As much as we can possibly do with match/case in DaCe
tbennun Apr 13, 2026
d53a70c
Lambda and devirtualization support
tbennun Apr 13, 2026
e19b2df
support nested functions
tbennun Apr 13, 2026
5cda6e6
Clarify nested class definitions
tbennun Apr 13, 2026
f30d691
Add pydata
tbennun Apr 13, 2026
d9e0239
Move tests
tbennun Apr 13, 2026
da23eff
global/nonlocal support with explicit usage boundaries
tbennun Apr 14, 2026
eaedfae
`raise` support
tbennun Apr 14, 2026
35f827d
type alias preprocessing
tbennun Apr 14, 2026
2942dea
Better f-string support
tbennun Apr 16, 2026
e3e4fa7
iter/next and free generator support
tbennun Apr 17, 2026
dc862cd
Emit callbacks for uninlinable context managers
tbennun Apr 17, 2026
8c1d9ca
Schedule-tree-convertible object support
tbennun Apr 17, 2026
209d3e0
Support explicit SDFG/SDFGConvertible object calls
tbennun Apr 17, 2026
394f493
Support keyword and tuple expansions whenever they are compile-time
tbennun Apr 17, 2026
0808810
Support iterating over arrays
tbennun Apr 17, 2026
44c5b79
Attribute/descriptor cleanup
tbennun Apr 18, 2026
b25c72f
Cleanup tuple expansion
tbennun Apr 18, 2026
eb2208e
Cleanup and reuse descriptor from structure
tbennun Apr 18, 2026
ff71fe0
Cleanup lambda resolution
tbennun Apr 18, 2026
0bb210d
More callable cleanup
tbennun Apr 18, 2026
efd4fe2
Reuse SDFG/stree callable detection
tbennun Apr 18, 2026
af06758
More callable cleanup
tbennun Apr 18, 2026
e9cd6e2
NumPy: Better support for complex and nested subscripts (e.g., nested…
tbennun Apr 19, 2026
11a2ba8
Support for negative indices in expressions
tbennun Apr 19, 2026
ebf8c5d
testing for ellipses in expressions
tbennun Apr 19, 2026
c29f724
Clean up callbacks and implement callback outliner
tbennun Apr 20, 2026
ac9173a
Fix warning
tbennun Apr 20, 2026
9b15ff1
Test should not just exec strings
tbennun Apr 20, 2026
5607ccd
Cleaner dict support
tbennun Apr 20, 2026
b0c62d3
Initial support for structures/classes
tbennun Apr 20, 2026
6fcf829
Support for nested attributes
tbennun Apr 20, 2026
9560b4b
Support creating data containers from literals with numpy.array()
tbennun Apr 21, 2026
a3a7c0f
Support `np.arange` with arbitrary data containers
tbennun Apr 21, 2026
71a3bd9
Support widening dictionary type if assignments break type contracts
tbennun Apr 21, 2026
cc56cb9
Runtime negative index code generation support
tbennun Apr 21, 2026
8af92bd
Support for implicit numpy arrays from list/tuple literals
tbennun Apr 21, 2026
b9f0cad
"Clean up" dict support
tbennun Apr 21, 2026
78962f7
"Clean up" stree frontend
tbennun Apr 21, 2026
7da21d6
Start work on PythonClass
tbennun Apr 22, 2026
b1f29f9
Fix ReturnNode to only contain data descriptors
tbennun Apr 22, 2026
dcbc423
Reuse closure for dace.method object fields, keep `self`/methodobj as…
tbennun Apr 22, 2026
ac45bba
Clarify PythonClass vs. Structure, frontend error when Structure is i…
tbennun Apr 22, 2026
5b398a3
Annotated classes become Structure or PythonClass, depending on their…
tbennun Apr 22, 2026
3adbdc8
PythonClass propagates outwards on inline
tbennun Apr 23, 2026
024617f
Minor fix
tbennun Apr 23, 2026
1c4b362
dynscope promotion pass
tbennun Apr 23, 2026
6d0f68a
Dunder method support
tbennun Apr 23, 2026
24bff1a
Remove self argument from methods
tbennun Apr 23, 2026
2e03cb1
PythonClass support in code generation and pyfrontend, reassigning sc…
tbennun Apr 24, 2026
f2a3313
Schedule tree frontend PythonClass support
tbennun Apr 24, 2026
8a71e13
Extend descriptor inference repository to parity with replacements
tbennun Apr 24, 2026
22fc052
stree frontend: Use `optional_progressbar` for large applications
tbennun Apr 24, 2026
0ac04f6
Implement type inference for all replacements
tbennun Apr 24, 2026
59d2632
Merge branch 'main' into frontend2-wip
tbennun Apr 24, 2026
f5d429b
Minor fix
tbennun Apr 24, 2026
5513d78
Fix pre-commit
tbennun Apr 24, 2026
0808bcb
import fixes
tbennun Apr 24, 2026
5e226d5
Revert removed function
tbennun Apr 24, 2026
e592faf
Revert bad change
tbennun Apr 24, 2026
abd67f9
Fix branch ordering, improve memlet parser for nested memlets, remove…
tbennun Apr 25, 2026
016689b
Bad change was not entirely bad, reverted revert and fixed
tbennun Apr 25, 2026
1238cbe
Fix tuple/list literal assignment flattening
tbennun Apr 25, 2026
f4ab61c
Minor fix
tbennun Apr 25, 2026
d0e2a88
Python 3.10 fixes
tbennun Apr 26, 2026
906b639
Add parallel stree lowering path with validity checks
tbennun Apr 26, 2026
e87153a
If the function call cannot be inferred directly, use descriptor type
tbennun Apr 26, 2026
77a7327
Fewer deepcopies, more `copy_tree`
tbennun Apr 26, 2026
a6c2927
Tuple unpacking/packing lowering pass and multiple-output ufunc support
tbennun Apr 27, 2026
e09d980
Fix over-eager descriptor dunder attribute rewriting
tbennun Apr 27, 2026
4a5862a
Fix descriptor inference for processgrids
tbennun Apr 27, 2026
9b43d60
Fix ONNX descriptor inference
tbennun Apr 27, 2026
3b4271e
Revert "Fix descriptor inference for processgrids"
tbennun Apr 27, 2026
81279aa
Support `__array__` and `numpy.asarray`
tbennun Apr 29, 2026
bbe42c1
Merge branch 'main' into pyfrontend2
tbennun Jun 1, 2026
5617eaf
Merge remote-tracking branch 'origin/pyfrontend2' into frontend2-wip
tbennun Jun 1, 2026
8f5da4d
Fix merge conflict
tbennun Jun 1, 2026
839b7b8
Fix merge conflict 2
tbennun Jun 7, 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
2 changes: 2 additions & 0 deletions dace/codegen/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -58,9 +58,11 @@ include_directories(${DACE_RUNTIME_DIR}/include)
# Global DaCe external dependencies
find_package(Threads REQUIRED)
find_package(OpenMP REQUIRED COMPONENTS CXX)
find_package(Python REQUIRED COMPONENTS Development)

list(APPEND DACE_LIBS Threads::Threads)
list(APPEND DACE_LIBS OpenMP::OpenMP_CXX)
list(APPEND DACE_LIBS Python::Python)

add_definitions(-DDACE_BINARY_DIR=\"${CMAKE_BINARY_DIR}\")

Expand Down
20 changes: 17 additions & 3 deletions dace/codegen/targets/cpp.py
Original file line number Diff line number Diff line change
Expand Up @@ -62,10 +62,12 @@ def copy_expr(
name_override=None,
):
data_desc = sdfg.arrays[data_name]

# NOTE: Are there any cases where a mix of '.' and '->' is needed when traversing nested structs?
# TODO: Study this when changing Structures to be (optionally?) non-pointers.
tokens = data_name.split('.')
if len(tokens) > 1 and tokens[0] in sdfg.arrays and isinstance(sdfg.arrays[tokens[0]], data.Structure):
if (len(tokens) > 1 and tokens[0] in sdfg.arrays and isinstance(sdfg.arrays[tokens[0]], data.Structure)
and not isinstance(sdfg.arrays[tokens[0]], data.PythonClass)):
name = data_name.replace('.', '->')
else:
name = data_name
Expand Down Expand Up @@ -244,6 +246,8 @@ def ptr(name: str, desc: data.Data, sdfg: SDFG = None, framecode: 'DaCeCodeGener

if '.' in name:
root = name.split('.')[0]
if root in sdfg.arrays and isinstance(sdfg.arrays[root], data.PythonClass):
return pyobject_member_expr(root, name.split('.', 1)[1], desc)
if root in sdfg.arrays and isinstance(sdfg.arrays[root], data.Structure):
name = name.replace('.', '->')

Expand Down Expand Up @@ -273,6 +277,14 @@ def ptr(name: str, desc: data.Data, sdfg: SDFG = None, framecode: 'DaCeCodeGener
return name


def pyobject_member_expr(root_name: str, attr_path: str, desc: data.Data) -> str:
if isinstance(desc, data.Array):
return f'dace_get_pyobject_attr_ptr<{desc.dtype.ctype}>({root_name}, "{attr_path}")'
if isinstance(desc, data.Scalar):
return f'dace_get_pyobject_attr<{desc.dtype.ctype}>({root_name}, "{attr_path}")'
raise TypeError(f'Unsupported PythonClass member descriptor: {type(desc).__name__}')


def emit_memlet_reference(dispatcher: 'TargetDispatcher',
sdfg: SDFG,
memlet: mmlt.Memlet,
Expand Down Expand Up @@ -371,7 +383,8 @@ def make_const(expr: str) -> str:

# NOTE: `expr` may only be a name or a sequence of names and dots. The latter indicates nested data and structures.
# NOTE: Since structures are implemented as pointers, we replace dots with arrows.
expr = expr.replace('.', '->')
if 'dace_get_pyobject_attr' not in expr:
expr = expr.replace('.', '->')

return (typedef + ref, pointer_name, expr)

Expand Down Expand Up @@ -543,7 +556,8 @@ def cpp_array_expr(sdfg,
# NOTE: Are there any cases where a mix of '.' and '->' is needed when traversing nested structs?
# TODO: Study this when changing Structures to be (optionally?) non-pointers.
tokens = memlet.data.split('.')
if len(tokens) > 1 and tokens[0] in sdfg.arrays and isinstance(sdfg.arrays[tokens[0]], data.Structure):
if (len(tokens) > 1 and tokens[0] in sdfg.arrays and isinstance(sdfg.arrays[tokens[0]], data.Structure)
and not isinstance(sdfg.arrays[tokens[0]], data.PythonClass)):
name = memlet.data.replace('.', '->')
else:
name = memlet.data
Expand Down
98 changes: 96 additions & 2 deletions dace/codegen/targets/cpu.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
dynamic_map_inputs)
from dace.sdfg.scope import is_devicelevel_gpu, is_in_scope
from dace.sdfg.validation import validate_memlet_data
from typing import TYPE_CHECKING, Optional, Tuple, Union
from typing import TYPE_CHECKING, Optional, Sequence, Tuple, Union

if TYPE_CHECKING:
from dace.codegen.targets.framecode import DaCeCodeGenerator
Expand All @@ -44,10 +44,23 @@ def _visit_structure(struct: data.Structure, args: dict, prefix: str = ''):
if isinstance(v, data.Data):
args[f'{prefix}->{k}'] = v

def _visit_pythonclass_members(struct: data.Structure, args: dict, root_name: str, prefix: str = ''):
for k, v in struct.members.items():
member_path = f'{prefix}.{k}' if prefix else k
if isinstance(v, data.Structure):
_visit_pythonclass_members(v, args, root_name, member_path)
elif isinstance(v, data.ContainerArray):
_visit_pythonclass_members(v.stype, args, root_name, member_path)

if isinstance(v, (data.Array, data.Scalar)):
args[cpp.pyobject_member_expr(root_name, member_path, v)] = v

# Keeps track of generated connectors, so we know how to access them in nested scopes
args = dict(arglist)
for name, arg_type in arglist.items():
if isinstance(arg_type, data.Structure):
if isinstance(arg_type, data.PythonClass):
_visit_pythonclass_members(arg_type, args, name)
elif isinstance(arg_type, data.Structure):
desc = sdfg.arrays[name]
_visit_structure(arg_type, args, name)
elif isinstance(arg_type, data.ContainerArray):
Expand All @@ -56,6 +69,15 @@ def _visit_structure(struct: data.Structure, args: dict, prefix: str = ''):
if isinstance(desc, data.Structure):
_visit_structure(desc, args, name)

for name in sdfg.arrays.keys():
desc = sdfg.arrays[name]
if '.' not in name or not isinstance(desc, (data.Array, data.Scalar)):
continue
root_name, member_path = name.split('.', 1)
root_desc = sdfg.arrays.get(root_name)
if isinstance(root_desc, data.PythonClass):
args[cpp.pyobject_member_expr(root_name, member_path, desc)] = desc

for name, arg_type in args.items():
if isinstance(arg_type, data.Scalar):
# GPU global memory is only accessed via pointers
Expand All @@ -72,6 +94,8 @@ def _visit_structure(struct: data.Structure, args: dict, prefix: str = ''):
self._dispatcher.defined_vars.add(name, DefinedType.StreamArray, arg_type.as_arg(name=''))
else:
self._dispatcher.defined_vars.add(name, DefinedType.Stream, arg_type.as_arg(name=''))
elif isinstance(arg_type, data.PythonClass):
self._dispatcher.defined_vars.add(name, DefinedType.Object, arg_type.dtype.ctype)
elif isinstance(arg_type, data.Structure):
self._dispatcher.defined_vars.add(name, DefinedType.Pointer, arg_type.dtype.ctype)
else:
Expand Down Expand Up @@ -705,6 +729,15 @@ def _emit_copy(
src_nodedesc = src_node.desc(sdfg)
dst_nodedesc = dst_node.desc(sdfg)

if (write and isinstance(dst_nodedesc, data.Scalar) and '.' in dst_node.data
and isinstance(sdfg.arrays[dst_node.data.split('.')[0]], data.PythonClass)):
self._emit_pythonclass_scalar_setter(
dst_node.data, dst_nodedesc.dtype.ctype,
self._pythonclass_scalar_source_expr(sdfg,
cfg.nodes()[state_id], edge, src_node, dst_node), stream, cfg,
state_id, [src_node, dst_node])
return

if write:
vconn = self.ptr(dst_node.data, dst_nodedesc, sdfg)
ctype = dst_nodedesc.dtype.ctype
Expand All @@ -714,6 +747,11 @@ def _emit_copy(

# Setting a reference
if isinstance(dst_nodedesc, data.Reference) and orig_vconn == 'set':
if '.' in dst_node.data and isinstance(sdfg.arrays[dst_node.data.split('.')[0]], data.PythonClass):
self._emit_pythonclass_array_reference_set(sdfg,
cfg.nodes()[state_id], edge, src_node, dst_node,
src_nodedesc, stream, cfg, state_id)
return
srcptr = self.ptr(src_node.data, src_nodedesc, sdfg)
defined_type, _ = self._dispatcher.defined_vars.get(srcptr)
stream.write(
Expand Down Expand Up @@ -795,6 +833,10 @@ def _emit_copy(

state_dfg: SDFGState = cfg.nodes()[state_id]

if (isinstance(dst_nodedesc, data.Reference) and edge.dst_conn == 'set' and '.' in dst_node.data
and isinstance(sdfg.arrays[dst_node.data.split('.')[0]], data.PythonClass)):
return

copy_shape, src_strides, dst_strides, src_expr, dst_expr = cpp.memlet_copy_to_absolute_strides(
self._dispatcher, sdfg, state_dfg, edge, src_node, dst_node)

Expand Down Expand Up @@ -1043,8 +1085,14 @@ def process_out_memlets(self,
is_scalar = True # Pointer to pointer assignment
is_stream = isinstance(sdfg.arrays[memlet.data], data.Stream)
is_refset = isinstance(sdfg.arrays[memlet.data], data.Reference) and dst_edge.dst_conn == 'set'
is_pythonclass_scalar = (is_scalar and '.' in memlet.data
and isinstance(sdfg.arrays[memlet.data], data.Scalar)
and isinstance(sdfg.arrays[memlet.data.split('.')[0]], data.PythonClass))

if (is_scalar and not memlet.dynamic and not is_stream) or is_refset:
if (is_refset and '.' in memlet.data
and isinstance(sdfg.arrays[memlet.data.split('.')[0]], data.PythonClass)):
continue
out_local_name = " __" + uconn
in_local_name = uconn
if not locals_defined:
Expand All @@ -1063,6 +1111,11 @@ def process_out_memlets(self,
# which we skip since the memlets are references
continue
desc = sdfg.arrays[memlet.data]
if is_pythonclass_scalar:
write_expr = self._emit_pythonclass_scalar_setter_expr(memlet.data, desc.dtype.ctype,
in_local_name)
result.write(write_expr, cfg, state_id, node)
continue
ptrname = codegen.ptr(memlet.data, desc, sdfg)
is_global = desc.lifetime in (dtypes.AllocationLifetime.Global,
dtypes.AllocationLifetime.Persistent,
Expand Down Expand Up @@ -1113,6 +1166,41 @@ def make_ptr_assignment(self, src_expr, src_dtype, dst_expr, dst_dtype, codegen=
dst_expr = codegen.make_ptr_vector_cast(dst_expr, dst_dtype, src_dtype, True, DefinedType.Pointer)
return f"{dst_expr} = {src_expr};"

def _emit_pythonclass_array_reference_set(self, sdfg: SDFG, state_dfg: SDFGState,
edge: MultiConnectorEdge[mmlt.Memlet], src_node: nodes.AccessNode,
dst_node: nodes.AccessNode, src_nodedesc: data.Data, stream: CodeIOStream,
cfg: ControlFlowRegion, state_id: int) -> None:
copy_shape, src_strides, _, src_expr, _ = cpp.memlet_copy_to_absolute_strides(
self._dispatcher, sdfg, state_dfg, edge, src_node, dst_node)
attr_path = dst_node.data.split('.', 1)[1]
root_name = dst_node.data.split('.', 1)[0]
shape = ', '.join(cpp.sym2cpp(s) for s in copy_shape)
strides = ', '.join(cpp.sym2cpp(s * src_nodedesc.dtype.bytes) for s in src_strides)
stream.write(
f'''{{
Py_ssize_t __shape[{len(copy_shape)}] = {{{shape}}};
Py_ssize_t __strides[{len(copy_shape)}] = {{{strides}}};
dace_set_pyobject_attr_array<{src_nodedesc.dtype.ctype}>({root_name}, "{attr_path}", {src_expr}, __shape,
__strides, {len(copy_shape)});
}}''', cfg, state_id, [src_node, dst_node])

def _pythonclass_scalar_source_expr(self, sdfg: SDFG, state_dfg: SDFGState, edge: MultiConnectorEdge[mmlt.Memlet],
src_node: nodes.AccessNode, dst_node: nodes.AccessNode) -> str:
_, _, _, src_expr, _ = cpp.memlet_copy_to_absolute_strides(self._dispatcher, sdfg, state_dfg, edge, src_node,
dst_node)
return f'*({src_expr})'

def _emit_pythonclass_scalar_setter(self, dst_data: str, ctype: str, value_expr: str, stream: CodeIOStream,
cfg: ControlFlowRegion, state_id: int,
nodes_to_track: Sequence[nodes.Node]) -> None:
stream.write(self._emit_pythonclass_scalar_setter_expr(dst_data, ctype, value_expr), cfg, state_id,
nodes_to_track)

def _emit_pythonclass_scalar_setter_expr(self, dst_data: str, ctype: str, value_expr: str) -> str:
attr_path = dst_data.split('.', 1)[1]
root_name = dst_data.split('.', 1)[0]
return f'dace_set_pyobject_attr<{ctype}>({root_name}, "{attr_path}", static_cast<{ctype}>({value_expr}));'

def memlet_view_ctor(self, sdfg: SDFG, memlet: mmlt.Memlet, dtype, is_output: bool) -> str:
memlet_params = []

Expand Down Expand Up @@ -1288,6 +1376,12 @@ def memlet_definition(self,
memlet_type = ctypedef
result += "{} &{} = {};".format(memlet_type, local_name, expr)
defined = DefinedType.Stream
elif var_type == DefinedType.Object:
if output:
result += "{} {};".format(ctypedef, local_name)
else:
result += "{} &{} = {};".format(ctypedef, local_name, expr)
defined = DefinedType.Object
else:
raise TypeError("Unknown variable type: {}".format(var_type))

Expand Down
27 changes: 27 additions & 0 deletions dace/config_schema.yml
Original file line number Diff line number Diff line change
Expand Up @@ -593,6 +593,33 @@ required:
Raise all errors out of nested function parsing contexts
instead of trying to create a callback implicitly.

raise_statements:
type: str
title: Schedule-tree raise handling
default: support
description: >
Controls how ``raise`` statements are handled by the direct
schedule-tree frontend. ``support`` lowers directly supported
exception classes to ``RaiseNode`` and falls back to Python
callbacks for unsupported dynamic cases. ``ignore_dynamic``
keeps directly supported ``RaiseNode`` cases but skips
dynamic raises that would otherwise require a callback.
``ignore_all`` skips all ``raise`` statements.

runtime_negative_indices:
type: bool
title: Runtime negative-index wrapping
default: false
description: >
If enabled, array indices whose sign cannot be proven
nonnegative are normalized with symbolic runtime accessors
during frontend lowering. Uncertain scalar element indices
use ``pyindex(ind, size)``, while uncertain slice bounds use
conditional negative-offset normalization so positive bounds
such as ``stop == size`` keep Python semantics. This is
disabled by default; definitely negative cases are still
normalized statically during preprocessing.

verbose_errors:
type: bool
title: Show preprocessed AST on parsing errors
Expand Down
5 changes: 5 additions & 0 deletions dace/data/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
StructureReference,
ContainerArrayReference,
)
from dace.data.pydata import PythonClass, PythonDict, PythonList, PythonTuple

# Import prod from utils and expose as _prod for backward compatibility
from dace.utils import prod as _prod
Expand Down Expand Up @@ -86,6 +87,10 @@
'ArrayReference',
'StructureReference',
'ContainerArrayReference',
'PythonList',
'PythonTuple',
'PythonDict',
'PythonClass',
# Tensor support
'TensorIterationTypes',
'TensorAssemblyType',
Expand Down
Loading
Loading