Skip to content

shared: dimensions as types - #27

Open
havogt wants to merge 1 commit into
mainfrom
dimensions-as-types-shared
Open

shared: dimensions as types#27
havogt wants to merge 1 commit into
mainfrom
dimensions-as-types-shared

Conversation

@havogt

@havogt havogt commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

Moves the base of the dimension redesign — a concrete dimension is a type, not
an instance
— out of the personal investigation and into shared/, scoped to
that alone.

Why now

Field[Dims[I], float64] is not a valid annotation today (GridTools/gt4py#2503),
and the shipped answer is a compatibility shim: mypy_plugin.py, wired in via
pyproject.toml. It caps tracking at four distinct dimensions per run globally
(keyed by argument name, so same-named dims in different modules collide), makes
TypeVars over dimensions impossible, serves only mypy, and blurs dtypes in a
way that erases exactly what dtype generics need. The success criterion for this
proposal is that the dimension half of that plugin gets deleted rather than
maintained.

Scope

In: dimension is a class; value-level behaviour preserved on the class object via
DimensionMeta; old- and new-style dimensions coexist by name-based equality;
dimension types usable as TypeVar bounds.

Out, each with its own proposal: staggering syntax and index conventions,
dimension variables, dependent local dimensions, Dims ordering semantics.

The extension point

The two-level split — root DimensionBase (anything usable in Dims[...]) above
Dimension (user-declarable) — exists so parameterized dimension types can
occupy the first without the second. Both known follow-ups need precisely that
slot, and both already have mypy-verified prototypes: Staggered[D]
(parameterized by a dimension) and Local[C] (parameterized by a connectivity).
Neither is decided here. That is the whole forward-compatibility claim, and it is
narrow enough to check.

On the shipped _Staggered name prefix

ADR 0026's rationale holds under today's model: a dimension is its name, so a
flag would have to be threaded through common.Dimension, itir.AxisLiteral,
and backend tag strings. Dimensions-as-types removes that premise — identity
becomes nominal and the name demotes to what crosses the IR boundary.

Step 1 keeps the prefix and costs nothing: Staggered[I] materializes with
value == "_StaggeredI", and every staggering helper reads only .value/.kind
(is_staggered, flip_staggered, as_non_staggered, check_dims,
order_dimensions), while gtfn's _add_staggered_aliases rebuilds a Dimension
from a string — so none of them change. Established by reading those call sites,
not by running a migration.

Step 2 — dropping the string sniffing — is a separate change behind this surface.
The document is explicit that distinct axes still need distinct identifiers at the
IR boundary, so some mangling survives there; what goes away is
startswith("_Staggered") and the fact that the prefix is user-observable.

Also in this PR

personal/havogt/dimension-generic-fields loses §3 (replaced by a stub keeping
only what Parts II and III refer back to) so the two documents do not duplicate,
and its suggested ADR numbers move 0024/00250027/0028 — the originals
are taken by Compilation Runners and Crash Consistent Build Caches.

Open before the group commits

pyright is untested. The metaclass operator overloads rely on a mypy quirk
(def-site rejection, call-site correctness) suppressed with
# type: ignore[misc]. The fallback is checker-agnostic free functions, so this
is a notation risk rather than a design risk — but it should be settled before
adopting the I + 1/2 spelling.

havogt added a commit that referenced this pull request Aug 5, 2026
Part II of `dimension-generic-fields` spelled a staggering shift as `a(I
+ 1/2)`
for `a: Field[Dims[I]]` — naming the dimension the field is *on*. gt4py
as
shipped (#2667, ADR 0026) does the opposite, and the proposal's spelling
is not
a different result there but a hard error:

```
I  + 0.5   -> domain(result)=I            codomain(field)=_StaggeredI   offset=1
IH + 0.5   -> domain(result)=_StaggeredI  codomain(field)=I             offset=0

a on I:  a(IH + 0.5) -> Domain(_StaggeredI=(0:10))   OK
         a(I  + 0.5) -> ValueError: Incompatible dimensions in the connectivity codomain(s)
```

The implementation is right and the proposal moves.

## Why: a shift is precomposition

A field is a map `f: I → value`, a connectivity a map `φ: D → I`;
applying one
is `f ∘ φ`, a field on `D`. The connectivity's domain is therefore the
*result's* dimension and its codomain the field's — forced by
composition, not
chosen. The index expression is written in the result's index space and
must
evaluate to an index of the field's own dimension, which is exactly what
the
in-tree test comment states: `a(IHalfDim + 0.5) # always pass an I-index
to an
IField`.

The unstructured connectivities already encode this in their names —
`V2E` is
"for each Vertex, its Edges", domain `Vertex`, codomain `Edge`, so it
consumes
an edge field and produces a vertex field. Staggering is merely the
first place
the convention becomes observable, because integer Cartesian shifts have
domain == codomain.

§4.2 records the discarded alternative (source-naming) with its one real
advantage and the reason it loses: it gives no rule for `a(E2C[0])`, so
unstructured shifts would have to become the exception.

## What changed

- **§1, §4.1–§4.3**: the spelling flips (`a(Staggered[I] + 1/2)`), the
position
convention becomes the shipped one (staggered point `i` at `i - 1/2`),
and the
index rule is restated as `ceil` when the shift names an unstaggered
dimension
/ `floor` when it names a staggered one — keyed on the result's
dimension,
  which is what `connectivity_for_cartesian_shift` already implements.
- **Prototype** (`typed_dimensions.py`, `static_checks.py`,
`static_errors.py`,
`test_typed_dimensions.py`): overloads, runtime rule and every
expectation
flipped. **19/19 pass, mypy clean, the error oracle fires on exactly the
  marked lines.**
- **`dual` is now a typed function.** This is the part that cost more
than
swapping two type arguments: because the shift names the result grid, a
dual-generic body cannot shift on its own dimension and needs
`dual(dim)` —
the type-level counterpart of gt4py's value-level `flip_staggered`. It
gets
  the same overload pair, so it stays precise under a dimension variable
  (`to_staggered` in `static_checks.py`).

## Three corrections found while verifying

- **§4.4 "backends are unaffected" was wrong.** gtfn needs
`_add_staggered_aliases` (`itir_to_gtfn_ir.py`), emitting the staggered
tag as
a C++ `using` alias of its base tag, because a gtfn shift can offset a
SID but
  cannot *rename* its axis. The FOAST bullet also had `source`/`target`
  inverted; it now matches the shipped `source=conn.codomain,
  target=(conn.domain_dim,)`.
- **The `I + 0.7` gap is mypy-only**, not runtime-only: the DSL already
rejects
  it at FOAST time with a `DSLError` and a hint.
- **`check_dims` is not statically expressible** — a dimension and its
staggered
  counterpart may not co-occur, but `Field[Dims[I, Staggered[I]]]` stays
  writable for mypy. Added as a known gap.

## Incidental

The prototype's `mypy.ini` pinned `python_version = 3.10`, which numpy's
stubs
now reject outright, so both mypy-oracle tests were failing on `main`
for
reasons unrelated to any of this. Bumped to 3.12 (gt4py has dropped
3.10/3.11).
With that fixed, **mypy 2.3 reproduces the metaclass quirk and its
suppression
exactly as documented** — §10 risk 1's "could break on a mypy upgrade"
has not
materialised; pyright remains untested. `.gitignore` gained
`__pycache__/`,
since the vendored prototypes are runnable.

## Note for merge order

Touches `dimension-generic-fields.md` alongside #27 (which stubs out
§3). The
edits are in disjoint sections; the only adjacency is the `>
**Prototypes**:`
block near the top, where #27 inserts a note and this branch edits the
mypy
version on the following line. Whichever lands second may need a trivial
rebase.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant