Preprocessing Python AST
Before the Python frontend lowers a @dace.program into an SDFG, the function’s
abstract syntax tree (AST) is rewritten by a small pipeline of preprocessing
passes. Preprocessing has three goals:
Resolve every name in the function body to either an SDFG symbol, a data container, a closure constant, or an external Python callback.
Specialize Python constructs that the SDFG IR does not represent natively (for example, list/tuple unpacking, context managers, augmented assignments, or f-strings) into a form the parser can handle.
Reject programs that use language features which DaCe explicitly does not support (see Core Python Language Support).
The pipeline is implemented in dace.frontend.python.preprocessing and
is entered through preprocess_dace_program().
The same entry point is reused recursively when one @dace.program calls
another, which keeps the closure of every nested SDFG self-consistent.
Pipeline overview
The passes run in the following order. Most of them are
ast.NodeTransformer subclasses; a few are ast.NodeVisitor
checks that only validate the program.
Pass |
Role |
|---|---|
Rewrites |
|
Replaces aliased module references ( |
|
Recognizes |
|
Normalizes Python’s modulo operator to DaCe’s C-style semantics (see Core Python Language Support, section 6.7). |
|
Walks the AST and raises a |
|
The largest pass. Resolves every free name to one of: a closure
constant (substituted in-place), a captured array (registered in the
|
|
Rejects assignments that would mutate compile-time constants or walrus-bound names that are visible from the closure. |
|
Statically unrolls |
|
Inlines call expressions whose callee is a pure Python function captured from the closure (including lambdas), as long as the result can be expressed as a single AST expression. |
|
Replaces |
|
Folds |
|
Removes unreachable code after the previous folding step (e.g.,
statements after a |
|
Expands |
|
Discovers all transitively-called |
The closure resolver, conditional resolver, dead-code eliminator, and
expression inliner run together in a fixed-point loop: as long as one of
them rewrites the AST, the loop is run again. The maximum number of passes
is controlled by the frontend.preprocessing_passes configuration entry
(-1 means “run until quiescent”).
The closure
The output of preprocessing is two-fold:
a
PreprocessedASTcontaining the rewritten AST, the source file, and the resolved global namespace; anda
SDFGClosurerecording every external object that the program needs at call time - captured arrays, constants, nested@dace.programcallees, and Python callbacks.
The closure is what allows @dace.program to behave like a regular
Python function from the caller’s point of view while still producing a
fully self-contained SDFG: when the compiled SDFG is invoked, the closure
is used to bind the captured external state to the program’s arguments.
Disabling or inspecting preprocessing
Set the configuration option
frontend.verbose_errors = trueto see the exact pass that raised an exception during preprocessing.Set
frontend.preprocessing_passesto a positive integer to cap the number of fixed-point iterations (useful when debugging an infinite loop in a custom replacement that produces new AST every pass).Use
dace.frontend.python.astutils.unparse()to dump the intermediate AST after any pass during development.
For background on what comes after preprocessing, see Parsing Python Programs to SDFGs and DaCe Python Frontend.