Skip to content

[next]: Compiled backends cannot execute a program with an empty tuple output #2856

Description

@tehrengruber

Problem

With variable-length tuple parameters (#2833), the empty tuple is a valid concretization of tuple[T, ...] (zero elements trivially satisfy any element type, see type_info.is_concretizable). Embedded execution handles such a call fine — there is simply nothing to compute or write. Every compiled backend crashes with an internal error instead:

@gtx.field_operator
def scale(tracers: tuple[IField, ...], factor: float64) -> tuple[IField, ...]:
    return tuple(t * factor for t in tracers)

scale((), 2.0, out=(), offset_provider={})  # embedded: OK; any compiled backend: crash

The root cause is that a SetAt for an empty out tuple is unconstructible: both its target and its domain are derived from the out fields, and with zero fields there is neither. The failure therefore surfaces as a cascade of internal errors, one per toolchain layer (each verified on sf_n_tracer_support):

  1. past_to_itir._visit_stencil_call_out_argValueError: not enough values to unpack (expected 2, got 0) while deriving target/domain from the empty out tuple.
  2. If (1) is bypassed by skipping the statement, ir_utils.misc.grid_type_from_program fails on the domain-less program with the (misleading for this case) ValueError: Found 'set_at' with more than one 'GridType': 'set()'.
  3. The roundtrip executor then generates a Python function with an empty body — IndentationError at module load.
  4. The DaCe bindings refuse zero-length tuple unpacking (bindings._unpack_args: "Cannot unpack argument with length zero"), and the DaCe decoration's first-call detection is exception-driven (decoration.py: a TypeError from iterating the None argument vector) — with zero arguments the loop never touches it, so construction is silently skipped and fast_call hits AssertionError: Argument vector was not set properly.

Possible directions

Option A — skip emission: generate no SetAt for an empty out in past_to_itir and make the later stages tolerate a statement-less program (default grid type when no domains exist, non-empty roundtrip function body, zero-length unpack in the DaCe bindings, explicit first-call detection in the DaCe decoration). A working implementation of exactly this exists and passed the full test_tuples.py backend matrix; it was deliberately kept out of #2833 as an unrelated toolchain concern.

Option B — reject with a diagnostic: refuse an empty out at call time with a proper located error instead of supporting the no-op program.

Independent of the choice, the DaCe first-call detection issue in (4) is arguably worth fixing on its own — the exception-driven control flow is fragile by design and the code's own TODO asks for a refactor.

Current state

test_var_len_tuple_comprehension_empty in test_tuples.py (on the #2833 branch) pins the working embedded behavior and xfails on all compiled backends, pointing here. The type-level acceptance is pinned by test_is_concretizable_vararg_to_tuple in test_type_info.py.


Disclaimer: This issue was written largely with the help of AI.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions