Skip to content

feat[next]: a concrete Dimension is a class, identified by its qualified name - #2899

Draft
egparedes wants to merge 20 commits into
mainfrom
connectivities-as-types-2-dimension-classes
Draft

egparedes wants to merge 20 commits into
mainfrom
connectivities-as-types-2-dimension-classes

Conversation

@egparedes

@egparedes egparedes commented Sep 18, 2026 •

Copy link
Copy Markdown
Contributor

A concrete dimension becomes a class, and an index along it an instance of that class, so Field[Dims[IDim], float64] type-checks under any PEP 484 checker with no gt4py mypy plugin (#2503). A dimension's identity is its Python type, and its tag -- the spelling in the IR and generated code -- is its qualified Python name.

Part 1 of 4 of the connectivities as types stack (see Stack). Review the ADR first (docs/development/ADRs/next/0029-Dimensions_As_Nominal_Types.md; numbered 0029 because main's #2808 took 0028): the rest of the diff is largely mechanical once its decisions are accepted.

Cartesian axes are a level below the root

A dimension is one of three things, and the class says which:

class IDim(gtx.CartesianAxisIndex): ...  # a Cartesian axis: `IDim + 1`, `Staggered[IDim]`
class Cell(gtx.DimensionIndex): ...  # a mesh location: no index arithmetic, no staggered partner

AnyCartesianAxisIndex (either cell class of an axis) and CartesianAxisIndex (a declared axis) sit below DimensionIndex, so no type[DimensionIndex] annotation in the tree widens. Staggered[D] is bounded on the declared level and derives from AnyCartesianAxisIndex, and DimensionMeta.__add__/__sub__ take cls: type[AnyCartesianAxisIndex]. As a result these are errors for mypy and pyright, and at runtime as well:

Rejected mypy / pyright runtime
Staggered[Staggered[K]] [type-var] TypeError
Staggered[Cell], staggering a local dimension [type-var] TypeError
Cell + 1, Cell - 1 [operator] TypeError; DSLError in a field operator

Comparisons (Cell < n, which build a Domain for concat_where) stay available on every dimension. Whether a dimension is an axis or a mesh location is a decision per declaration (IDim and Cell are both HORIZONTAL), so the tree's Cartesian dimensions are now declared as CartesianAxisIndex.

Where this diverges from #2844: identity

#2844 chose (name, kind) value equality plus an interning registry, so independently declared same-named dimensions stay interchangeable. This PR chooses nominal identity. That removes the registry, the blanket copyreg hook and the custom fingerprint deconstructor that bridged the gap between the Python type's identity and the IR's. One argument in its favour that the shared proposal itself raises: under (name, kind) equality the typing subscription cache aliases Field[Dims[I]] and Field[Dims[I2]] for two distinct same-named classes, so the static and runtime views disagree exactly there.

The pieces of #2844 that are independent of identity are reused. Each was checked against this tree rather than copied: three were wrong here, the sharpest being its TYPE_BUILTINS change, which would have silently renamed the DSL's Dimension builtin (the alias's __name__ is "Dimension", not "type" as its comment says).

Consequences worth knowing, all recorded in the ADR

  • resolve(tag) is an import, so dimensions reaching the IR must be declared at module level. Printing IR never imports: it goes through common.resolve_loaded, which only looks at loaded modules.
  • Interactive __main__. Compilation runs in spawn workers by default, and a class declared in a notebook pickles in the parent but cannot be unpickled in a worker. The repository's own Quickstart and workshop do exactly this, so this was not left as a documented limitation: the process runner now compiles such a job in the calling thread, with a warning -- the fallback it already takes for an unpicklable executor. Scripts keep parallel compilation.
  • One string per connectivity. A local dimension's tag is now qualified, so the V2EDim = Dimension("V2E") convention that made the offset tag, the local dimension's name and the provider key equal is gone. It is restored symbolically -- FieldOffset(V2EDim.tag, ...), {V2EDim.tag: table} -- so the follow-up that makes connectivities classes changes only the declaration. Until then the Quickstart's connectivity section is more verbose than before.
  • Injective mangling. Dots and brackets are illegal in C++, DaCe and eve identifiers. codegen_name is a prefix escape: the obvious _ -> __, . -> _ is not injective (".." collides with an escaped "_"). Tested exhaustively.
  • Display is __qualname__, identity is tag -- in diagnostics, in order_dimensions (keying it on the tag would make a field's dimension order depend on the module a dimension is declared in), and in __gt_dims__, the interop protocol with gt4py.cartesian, which names axes by bare name.
  • Staggered[D] replaces ADR 0026's _Staggered name prefix, which cannot survive type identity. It cannot be a PEP 695 generic (that yields a _GenericAlias, not a class): it is an interning metaclass plus a TYPE_CHECKING declaration, with a narrow copyreg and a fingerprint deconstructor registered on its metaclass.
  • Cache fingerprints now depend on module paths, a consequence for ADR 0023.
  • Equality between dimensions is identity. DimensionMeta.__eq__ stays for I == 5 (a Domain), but returns NotImplemented for a dimension operand, so I == J falls back to is and is always a bool.

Cleanups the class-based dimensions make possible

  • common.ConstList is the one local dimension of make_const_list results. It replaces the two _CONST_DIM aliases in iterator embedded and the DaCe lowering, which only worked while dimensions compared by (name, kind); their checks now use identity (is).
  • AxisLiteral stores only the tag. AxisLiteral.kind and the new AxisLiteral.dim are derived from it, so the kind can no longer disagree with the dimension's own. It already did in one case: the pretty printer printed a local axis as vertical (ᵥ), so a local axis didn't round-trip through the textual IR. The printer now derives the suffix (ₕ, ᵥ, and the new ₗ), and the parser ignores it.

Test-tree changes that are findings, not churn

Dimensions that were value-equal across modules are now distinct. cases_utils, fvm_nabla_setup, past_common and several tests declared their own copies of the same dimensions and mixed objects built on each; they now import one canonical declaration. The Quickstart and a workshop notebook each declared their cell dimension twice, and the Quickstart later combined fields built on both. What decided equality was the original string, not the variable: test_gtfn_module's IDim was "I" while cases_utils' was "IDim", so those correctly stay distinct.

Not included

Verification

Run locally (CPU) on ea75ee7 (the head adds only an mdformat fix of an ADR table); 0 failures in every row that ran:

Check 3.12 3.13 3.14
test_next (internal, cpu, nomesh): embedded, roundtrip, gtfn 4917 passed 4917 passed 4918 passed
test_next (dace, cpu, nomesh) 1488 passed 1488 passed 1488 passed
src/gt4py/next doctests (in each session) 117 passed
test_typing_exports (mypy cases) 17 passed 17 passed 17 passed
pre-commit run --all-files clean after the head's mdformat fix (the tested SHA failed only that)

GitHub CI on the pushed head passes every check: test_next (internal and dace; nomesh and atlas) on 3.12 and 3.14, test_cartesian, test_eve, test_storage, notebooks, package, typing-exports and code quality. Locally, on this head, 3.13 (4917 internal / 1488 dace), 3.14 (4918 / 1488), atlas (45 / 5) and the notebooks (19) also passed.

Stack

  1. feat[next]: a concrete Dimension is a class, identified by its qualified name #2899 (this PR) -- dimensions as classes (ADR 0029), Cartesian axis levels and Staggered[D: CartesianAxisIndex], ConstList, AxisLiteral without kind.
  2. feat[next]: NeighborConnectivity[Domain, Codomain] declarations, NeighborTableType, and shared local dimensions on every backend #2907 -- NeighborConnectivity[Domain, Codomain] declarations, shared local dimensions on every backend, NeighborTableType, ts.ShiftType, DimensionKind.LOCAL removed (ADR 0030).
  3. feat[next]!: connectivities declared as classes, class-keyed offset providers, table_types; remove FieldOffset #2910 -- breaking: the tree declares connectivities as classes, offset providers are keyed by them, FieldOffset is removed, as_offset takes a Cartesian axis, offset_provider_type becomes table_types; migration script.
  4. refactor[next]: MultiDimensionIndex and typed embedded positions #2912 -- MultiDimensionIndex and typed iterator-embedded positions.

The stack was shortened from 7 PRs to 4: this PR absorbs #2911 (its commits sit on top of this PR's own), #2907 absorbs #2909, and #2910 absorbs #2908.

Implements egparedes/connectivities-as-types, with the Cartesian axis levels specified in gt4py_knowledge#36; supersedes #2844 (closed). Subsumes #2845's test migration, since without a dimension(tag, kind) factory there is no minimal migration form and every declaration takes class form immediately. #2898 (the offset's own tag in shift lowering) is already merged.

@tehrengruber

Copy link
Copy Markdown
Contributor

I like the approach much more than the one in #2844. Maybe we can avoid the biggest downside, the many changes everywhere and the long names, by just defining a common set of dimensions in some module and avoiding the long name for them.

Base automatically changed from connectivities-as-types-1-shift-tag to main September 23, 2026 14:31
@egparedes
egparedes force-pushed the connectivities-as-types-2-dimension-classes branch 2 times, most recently from d38784e to 91e24e2 Compare September 23, 2026 16:54
@egparedes
egparedes removed this pull request from stack #2900 September 24, 2026 10:14
@egparedes
egparedes added this pull request to stack #2917 September 24, 2026 10:14
@egparedes
egparedes requested a balanced review from Copilot September 24, 2026 19:54

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🟡 Changes recommended

Interactive dimension resolution can retain stale classes, and the fallback scan performs avoidable connectivity-file I/O.

Get a fresh assessment by requesting another Copilot review.

Review effort: Balanced
Findings: 2 Medium severity · 1 Low severity

Open (3)
What changed in this PR

Makes gt4py.next dimensions nominal Python classes, with qualified-name tags used across IR, code generation, serialization, and backends.

Changes:

  • Introduces DimensionIndex, nominal identity, resolution, mangling, and staggered dimensions.
  • Migrates frontend, iterator, DaCe, GTFN, embedded execution, docs, and examples.
  • Updates tests and adds interactive compilation fallback coverage.
File Description
docs/​development/​ADRs/​next/​0026-Staggered_Dimensions.md Updates staggered encoding.
docs/​development/​ADRs/​next/​0028-Dimensions_As_Nominal_Types.md Records the architecture.
docs/​development/​ADRs/​next/​README.md Lists ADR 0028.
docs/​user/​next/​QuickstartGuide.md Migrates dimension examples.
docs/​user/​next/​workshop/​exercises/​1_simple_addition.ipynb Migrates notebook dimensions.
docs/​user/​next/​workshop/​exercises/​1_simple_addition_solution.ipynb Migrates solution dimensions.
docs/​user/​next/​workshop/​exercises/​helpers.py Migrates workshop helpers.
docs/​user/​next/​workshop/​slides/​slides_1.ipynb Migrates slide examples.
docs/​user/​next/​workshop/​slides/​slides_2.ipynb Migrates connectivity examples.
docs/​user/​next/​workshop/​slides/​slides_3.ipynb Migrates slide dimensions.
docs/​user/​next/​workshop/​slides/​slides_4.ipynb Migrates slide dimensions.
examples/​lap_cartesian_vs_next.ipynb Migrates example dimensions.
src/​gt4py/​next/​__init__.py Exports the new API.
src/​gt4py/​next/​common.py Implements nominal dimensions.
src/​gt4py/​next/​constructors.py Accepts dimension classes.
src/​gt4py/​next/​custom_layout_allocators.py Uses typed dimension indices.
src/​gt4py/​next/​embedded/​common.py Migrates indexing helpers.
src/​gt4py/​next/​embedded/​nd_array_field.py Migrates field indexing.
src/​gt4py/​next/​embedded/​operators.py Migrates scan positions.
src/​gt4py/​next/​ffront/​decorator.py Updates scan documentation.
src/​gt4py/​next/​ffront/​fbuiltins.py Constructs nominal indices.
src/​gt4py/​next/​ffront/​foast_passes/​type_deduction.py Updates dimension diagnostics.
src/​gt4py/​next/​ffront/​foast_pretty_printer.py Updates examples.
src/​gt4py/​next/​ffront/​foast_to_gtir.py Emits qualified tags.
src/​gt4py/​next/​ffront/​foast_to_past.py Updates examples.
src/​gt4py/​next/​ffront/​func_to_foast.py Updates examples.
src/​gt4py/​next/​ffront/​func_to_past.py Updates examples.
src/​gt4py/​next/​ffront/​past_to_itir.py Lowers qualified dimensions.
src/​gt4py/​next/​ffront/​transform_utils.py Detects dimension metaclasses.
src/​gt4py/​next/​ffront/​type_info.py Adds unknown-dimension placeholder.
src/​gt4py/​next/​field_utils.py Updates field examples.
src/​gt4py/​next/​fingerprinting.py Fingerprints staggered classes.
src/​gt4py/​next/​iterator/​embedded.py Uses shared ConstList.
src/​gt4py/​next/​iterator/​ir.py Derives axis dimensions from tags.
src/​gt4py/​next/​iterator/​ir_utils/​domain_utils.py Resolves axis tags.
src/​gt4py/​next/​iterator/​ir_utils/​ir_makers.py Emits tagged axes.
src/​gt4py/​next/​iterator/​ir_utils/​misc.py Resolves axis literals.
src/​gt4py/​next/​iterator/​pretty_parser.py Parses qualified tags.
src/​gt4py/​next/​iterator/​pretty_printer.py Prints derived axis kinds.
src/​gt4py/​next/​iterator/​tracing.py Traces dimension classes.
src/​gt4py/​next/​iterator/​transforms/​fuse_as_fieldop.py Updates doctests.
src/​gt4py/​next/​iterator/​transforms/​inline_fundefs.py Updates doctests.
src/​gt4py/​next/​iterator/​transforms/​pass_manager.py Uses qualified dimension keys.
src/​gt4py/​next/​iterator/​transforms/​prune_empty_concat_where.py Updates doctests.
src/​gt4py/​next/​iterator/​transforms/​remove_broadcast.py Updates doctests.
src/​gt4py/​next/​iterator/​transforms/​replace_get_domain_range_with_constants.py Matches dimensions by tag.
src/​gt4py/​next/​iterator/​transforms/​unroll_reduce.py Uses local-dimension tags.
src/​gt4py/​next/​iterator/​type_system/​inference.py Resolves axis dimensions.
src/​gt4py/​next/​iterator/​type_system/​type_synthesizer.py Migrates inferred dimensions.
src/​gt4py/​next/​otf/​binding/​nanobind.py Mangles binding dimensions.
src/​gt4py/​next/​otf/​compilation_tasks.py Preserves dimension providers.
src/​gt4py/​next/​otf/​runners.py Adds interactive fallback.
src/​gt4py/​next/​program_processors/​codegens/​gtfn/​codegen.py Mangles generated tags.
src/​gt4py/​next/​program_processors/​codegens/​gtfn/​gtfn_module.py Migrates connectivity tags.
src/​gt4py/​next/​program_processors/​codegens/​gtfn/​itir_to_gtfn_ir.py Declares qualified tag types.
src/​gt4py/​next/​program_processors/​runners/​dace/​lowering/​gtir_python_codegen.py Mangles DaCe symbols.
src/​gt4py/​next/​program_processors/​runners/​dace/​lowering/​gtir_to_sdfg.py Uses local tags.
src/​gt4py/​next/​program_processors/​runners/​dace/​lowering/​gtir_to_sdfg_concat_where.py Resolves local providers.
src/​gt4py/​next/​program_processors/​runners/​dace/​lowering/​gtir_to_sdfg_lambda.py Uses nominal local dimensions.
src/​gt4py/​next/​program_processors/​runners/​dace/​lowering/​gtir_to_sdfg_primitives.py Uses local tags.
src/​gt4py/​next/​program_processors/​runners/​dace/​lowering/​gtir_to_sdfg_scan.py Uses local tags.
src/​gt4py/​next/​program_processors/​runners/​dace/​lowering/​gtir_to_sdfg_utils.py Mangles map variables.
src/​gt4py/​next/​program_processors/​runners/​dace/​sdfg_args.py Mangles SDFG arguments.
src/​gt4py/​next/​program_processors/​runners/​dace/​transformations/​loop_blocking.py Accepts dimension classes.
src/​gt4py/​next/​program_processors/​runners/​dace/​transformations/​map_orderer.py Accepts dimension classes.
src/​gt4py/​next/​program_processors/​runners/​dace/​workflow/​bindings.py Unmangles provider keys.
src/​gt4py/​next/​program_processors/​runners/​roundtrip.py Resolves generated dimensions.
src/​gt4py/​next/​type_system/​mypy_plugin.py Removes dimension hooks.
src/​gt4py/​next/​type_system/​type_info.py Updates dimension diagnostics.
src/​gt4py/​next/​type_system/​type_specifications.py Displays class dimensions.
src/​gt4py/​next/​type_system/​type_translation.py Recognizes dimension classes.
tests/​next_tests/​artifacts/​custom_named_collections.py Migrates fixture dimensions.
tests/​next_tests/​benchmarks/​benchmark_program_call.py Migrates benchmark dimensions.
tests/​next_tests/​fixtures/​past_common.py Reuses canonical dimensions.
tests/​next_tests/​integration_tests/​cases_utils.py Centralizes test dimensions.
tests/​next_tests/​integration_tests/​feature_tests/​dace_tests/​test_orchestration.py Updates mangled connectivity arguments.
tests/​next_tests/​integration_tests/​feature_tests/​dace_tests/​test_write_back_buffer_elimination_lowering.py Migrates connectivity declarations.
tests/​next_tests/​integration_tests/​feature_tests/​ffront_tests/​test_concat_where.py Uses qualified provider keys.
tests/​next_tests/​integration_tests/​feature_tests/​ffront_tests/​test_external_local_field.py Uses qualified provider keys.
tests/​next_tests/​integration_tests/​feature_tests/​ffront_tests/​test_foast_pretty_printer.py Migrates dimensions.
tests/​next_tests/​integration_tests/​feature_tests/​ffront_tests/​test_import_from_mod.py Uses qualified provider keys.
tests/​next_tests/​integration_tests/​feature_tests/​ffront_tests/​test_named_collections.py Uses qualified provider keys.
tests/​next_tests/​integration_tests/​feature_tests/​ffront_tests/​test_reductions.py Migrates reduction coverage.
tests/​next_tests/​integration_tests/​feature_tests/​ffront_tests/​test_staggered.py Updates staggered coverage.
tests/​next_tests/​integration_tests/​feature_tests/​ffront_tests/​test_temporaries_with_sizes.py Uses qualified symbolic sizes.
tests/​next_tests/​integration_tests/​feature_tests/​ffront_tests/​test_tuples.py Uses qualified provider keys.
tests/​next_tests/​integration_tests/​feature_tests/​ffront_tests/​test_type_conversion.py Uses qualified provider keys.
tests/​next_tests/​integration_tests/​feature_tests/​instrumentation_tests/​test_hooks.py Migrates dimensions.
tests/​next_tests/​integration_tests/​feature_tests/​iterator_tests/​test_builtins.py Migrates local dimensions.
tests/​next_tests/​integration_tests/​feature_tests/​iterator_tests/​test_conditional.py Migrates dimensions.
tests/​next_tests/​integration_tests/​feature_tests/​iterator_tests/​test_implicit_fencil.py Migrates dimensions.
tests/​next_tests/​integration_tests/​feature_tests/​iterator_tests/​test_program.py Migrates dimensions.
tests/​next_tests/​integration_tests/​feature_tests/​iterator_tests/​test_strided_offset_provider.py Migrates connectivity dimensions.
tests/​next_tests/​integration_tests/​feature_tests/​iterator_tests/​test_tuple.py Migrates dimensions.
tests/​next_tests/​integration_tests/​multi_feature_tests/​ffront_tests/​test_ffront_fvm_nabla.py Uses qualified provider keys.
tests/​next_tests/​integration_tests/​multi_feature_tests/​ffront_tests/​test_icon_like_scan.py Reuses canonical dimensions.
tests/​next_tests/​integration_tests/​multi_feature_tests/​ffront_tests/​test_multiple_output_domains.py Uses qualified provider keys.
tests/​next_tests/​integration_tests/​multi_feature_tests/​fvm_nabla_setup.py Reuses canonical dimensions.
tests/​next_tests/​integration_tests/​multi_feature_tests/​iterator_tests/​test_anton_toy.py Migrates dimensions.
tests/​next_tests/​integration_tests/​multi_feature_tests/​iterator_tests/​test_fvm_nabla.py Reuses canonical dimensions.
tests/​next_tests/​integration_tests/​multi_feature_tests/​iterator_tests/​test_if_stmt.py Migrates dimensions.
tests/​next_tests/​integration_tests/​multi_feature_tests/​iterator_tests/​test_temporaries.py Migrates dimensions.
tests/​next_tests/​integration_tests/​multi_feature_tests/​iterator_tests/​test_with_toy_connectivity.py Uses qualified provider keys.
tests/​next_tests/​regression_tests/​embedded_tests/​test_domain_pickle.py Migrates pickle dimensions.
tests/​next_tests/​regression_tests/​ffront_tests/​test_offset_dimensions_names.py Updates tag regressions.
tests/​next_tests/​toy_connectivity.py Defines canonical connectivity dimensions.
tests/​next_tests/​unit_tests/​conftest.py Migrates dummy dimensions.
tests/​next_tests/​unit_tests/​embedded_tests/​test_basic_program.py Migrates dimensions.
tests/​next_tests/​unit_tests/​embedded_tests/​test_common.py Migrates index tests.
tests/​next_tests/​unit_tests/​embedded_tests/​test_context.py Migrates range dimensions.
tests/​next_tests/​unit_tests/​embedded_tests/​test_nd_array_field.py Tests nominal field behavior.
tests/​next_tests/​unit_tests/​ffront_tests/​test_decorator_domain_deduction.py Migrates grid deduction tests.
tests/​next_tests/​unit_tests/​ffront_tests/​test_diagnostic_messages.py Migrates diagnostic fixtures.
tests/​next_tests/​unit_tests/​ffront_tests/​test_fbuiltins.py Migrates builtin fixtures.
tests/​next_tests/​unit_tests/​ffront_tests/​test_foast_to_gtir.py Verifies qualified lowering.
tests/​next_tests/​unit_tests/​ffront_tests/​test_func_to_foast.py Migrates parser fixtures.
tests/​next_tests/​unit_tests/​ffront_tests/​test_func_to_foast_error_line_number.py Migrates line-sensitive fixture.
tests/​next_tests/​unit_tests/​ffront_tests/​test_past_to_gtir.py Expects qualified axes.
tests/​next_tests/​unit_tests/​ffront_tests/​test_source_utils.py Migrates closure fixtures.
tests/​next_tests/​unit_tests/​ffront_tests/​test_stages.py Migrates stage fixtures.
tests/​next_tests/​unit_tests/​ffront_tests/​test_type_deduction.py Tests nominal deduction.
tests/​next_tests/​unit_tests/​iterator_tests/​ir_utils_tests/​test_domain_utils.py Tests tagged domains.
tests/​next_tests/​unit_tests/​iterator_tests/​test_embedded_field_with_list.py Tests shared ConstList.
tests/​next_tests/​unit_tests/​iterator_tests/​test_embedded_internals.py Migrates dimensions.
tests/​next_tests/​unit_tests/​iterator_tests/​test_inline_dynamic_shifts.py Migrates dimensions.
tests/​next_tests/​unit_tests/​iterator_tests/​test_pretty_parser.py Tests ignored kind suffixes.
tests/​next_tests/​unit_tests/​iterator_tests/​test_pretty_printer.py Tests derived kind suffixes.
tests/​next_tests/​unit_tests/​iterator_tests/​test_pretty_roundtrip.py Updates axis round trips.
tests/​next_tests/​unit_tests/​iterator_tests/​test_runtime_domain.py Migrates connectivity types.
tests/​next_tests/​unit_tests/​iterator_tests/​test_type_inference.py Tests resolved dimensions.
tests/​next_tests/​unit_tests/​iterator_tests/​transforms_tests/​test_collapse_tuple.py Migrates dimensions.
tests/​next_tests/​unit_tests/​iterator_tests/​transforms_tests/​test_concat_where_canonicalize_domain_args.py Migrates dimensions.
tests/​next_tests/​unit_tests/​iterator_tests/​transforms_tests/​test_concat_where_expand_tuple_args.py Migrates dimensions.
tests/​next_tests/​unit_tests/​iterator_tests/​transforms_tests/​test_concat_where_transform_to_as_fieldop.py Migrates dimensions.
tests/​next_tests/​unit_tests/​iterator_tests/​transforms_tests/​test_cse.py Migrates provider types.
tests/​next_tests/​unit_tests/​iterator_tests/​transforms_tests/​test_dead_code_elimination.py Migrates dimensions.
tests/​next_tests/​unit_tests/​iterator_tests/​transforms_tests/​test_domain_inference.py Uses qualified axis tags.
tests/​next_tests/​unit_tests/​iterator_tests/​transforms_tests/​test_expand_tuple_maps.py Migrates dimensions.
tests/​next_tests/​unit_tests/​iterator_tests/​transforms_tests/​test_fuse_as_fieldop.py Migrates local list dimensions.
tests/​next_tests/​unit_tests/​iterator_tests/​transforms_tests/​test_global_tmps.py Migrates dimensions.
tests/​next_tests/​unit_tests/​iterator_tests/​transforms_tests/​test_inline_scalar.py Migrates dimensions.
tests/​next_tests/​unit_tests/​iterator_tests/​transforms_tests/​test_prune_casts.py Migrates dimensions.
tests/​next_tests/​unit_tests/​iterator_tests/​transforms_tests/​test_prune_empty_concat_where.py Uses qualified provider keys.
tests/​next_tests/​unit_tests/​iterator_tests/​transforms_tests/​test_unroll_reduce.py Tests qualified local tags.
tests/​next_tests/​unit_tests/​otf_tests/​binding_tests/​test_cpp_interface.py Migrates binding dimensions.
tests/​next_tests/​unit_tests/​otf_tests/​compilation_tests/​build_systems_tests/​conftest.py Generates mangled fixture tags.
tests/​next_tests/​unit_tests/​otf_tests/​test_compiled_program.py Migrates dimensions.
tests/​next_tests/​unit_tests/​otf_tests/​test_runners.py Tests interactive fallback.
tests/​next_tests/​unit_tests/​program_processor_tests/​codegens_tests/​gtfn_tests/​test_gtfn_module.py Uses qualified axis tags.
tests/​next_tests/​unit_tests/​program_processor_tests/​runners_tests/​dace_tests/​test_dace.py Uses qualified provider keys.
tests/​next_tests/​unit_tests/​program_processor_tests/​runners_tests/​dace_tests/​test_dace_bindings.py Tests mangled bindings.
tests/​next_tests/​unit_tests/​program_processor_tests/​runners_tests/​dace_tests/​test_dace_translation.py Builds symbolic names through helpers.
tests/​next_tests/​unit_tests/​program_processor_tests/​runners_tests/​dace_tests/​test_gtir_to_sdfg.py Tests nominal DaCe lowering.
tests/​next_tests/​unit_tests/​program_processor_tests/​runners_tests/​dace_tests/​transformation_tests/​test_map_promoter.py Migrates map dimensions.
tests/​next_tests/​unit_tests/​test_common.py Tests nominal dimension semantics.
tests/​next_tests/​unit_tests/​test_constructors.py Uses typed indices.
tests/​next_tests/​unit_tests/​test_custom_layout_allocators.py Tests typed alignment indices.
tests/​next_tests/​unit_tests/​test_field_utils.py Migrates dimensions.
tests/​next_tests/​unit_tests/​test_utils.py Tests dimension-key fingerprints.
tests/​next_tests/​unit_tests/​type_system_tests/​test_type_info.py Migrates type fixtures.
tests/​next_tests/​unit_tests/​type_system_tests/​test_type_translation.py Tests class-based translation.
typing_tests/​test_next.yaml Verifies PEP 484 dimensions.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread src/gt4py/next/common.py
Comment on lines +346 to +347
@functools.cache
def resolve(tag: Tag) -> Dimension:
Comment on lines +257 to +260
compilable = task.construct_compilable(True)
if (
reason is None
and (name := _interactive_main_reference((task.executor, compilable))) is not None
f"{', '.join([dim.value for dim in target_dims])}"
f"{', '.join([dim.__qualname__ for dim in field_type.dims])} from "
f"{source_dim.__qualname__} to target dim(s): "
f"{', '.join([dim.tag for dim in target_dims])}"
Records the design this stack implements, ahead of the code, because it reverses
decisions that are easier to argue about as prose than as a 150-file diff.

A concrete dimension becomes a class and an index an instance of it, so
`Field[Dims[IDim], float64]` type-checks with no gt4py mypy plugin. A
dimension's identity is the Python type and its tag is the qualified Python
name, which is also a unique, valid IR spelling.

The decisions worth reviewing first, each with the reason it is not the obvious
choice:

  * Nominal identity, not `(name, kind)` value equality with an interning
    registry. The registry decouples the Python type's identity from the IR's
    and needs `copyreg` plus a custom fingerprint deconstructor to bridge the
    gap. Under value equality the `typing` subscription cache already aliases
    `Field[Dims[I]]` and `Field[Dims[I2]]` for two distinct same-named classes,
    so the static and runtime views disagree exactly there.
  * `resolve(tag)` is an import, so types reaching the IR must be declared at
    module level. Interactive `__main__` is a documented limitation; `spawn`
    workers re-execute the main script, so file-based `__main__` resolves.
  * Generated identifiers need a *prefix* escape (`_` -> `_u`, `.` -> `_d`).
    The obvious `_` -> `__` then `.` -> `_` is not injective: a dot becomes a
    single underscore, so `".."` collides with an escaped `"_"`.
  * `Staggered[D]` supersedes ADR 0026's `_Staggered` name prefix, which cannot
    survive type identity. It cannot be a PEP 695 generic either -- that yields
    a `_GenericAlias`, not a class -- so it is an interning metaclass paired
    with a `TYPE_CHECKING` declaration.
  * `DimensionMeta` must declare `__hash__` explicitly, since `__eq__` stays for
    the `I == 5` overload and Python would otherwise make every dimension class
    unhashable.

Consequence recorded for ADR 0023: a dimension is fingerprinted by qualified
name, so moving a declaration between modules invalidates compiled artifacts.
A dimension tag becomes a qualified Python name in this stack, so it contains
dots -- illegal in a C++ identifier, in a DaCe symbol, and in `eve`'s
`SymbolName` (`^[a-zA-Z_]\w*$`). `codegen_name` mangles a tag into a valid
identifier and `from_codegen_name` recovers it, for the backends that parse a
generated name back into the dimension it refers to.

The escape is a *prefix* escape (`_` -> `_u`, `.` -> `_d`). The obvious
alternative -- double every underscore, then turn dots into single underscores --
is **not injective**: a dot becomes a single underscore, so `'..'` and `'_'` both
map to `'__'`. Exhaustively: 686 collisions in 1092 inputs over `{a, ., _}` up to
length 6. A collision here would mean two distinct dimensions silently sharing
one generated symbol, i.e. wrong results rather than a crash, so the test is
exhaustive rather than example-based: every string over `{a, ., _, u, d}` up to
length 5 mangles uniquely and round-trips, and the adversarial cases that look
like the escape sequences themselves (`_u`, `_d`, `a_ud.b`) are pinned
separately.

No caller yet; the sites that need it arrive with the dimension classes.
The core of ADR 0028. NOT green: `Dimension("X")` now raises, so every
declaration in the tree has to become a class statement. That sweep is the next
commit; this one is the mechanism it depends on.

  * `DimensionMeta` / `DimensionIndex`: a dimension is a class, an index an
    instance. Identity is the type; `tag` is the qualified Python name and is a
    metaclass *property*, so it cannot drift from what it names. `__hash__` is
    declared explicitly because `__eq__` stays for the `I == 5` overload.
    Dimension-vs-dimension `__eq__` is deliberately *not* overridden -- identity
    is the correct answer, and overriding it is what the ADR rejects.
  * `type Dimension = type[DimensionIndex]`, a PEP 695 alias so the removed
    `Dimension("I")` raises instead of silently evaluating to `str`.
  * Display uses `__qualname__`, not `tag`: 148 tests assert on message text like
    `Field[[IDim], float64]`, and a qualified name there is noise. `repr` carries
    the module.
  * `resolve(tag)` imports and walks the qualname, memoized, with a grammar for
    the parametrized `<owner>[<base>]` form.
  * `Staggered[D]` replaces ADR 0026's `_Staggered` name prefix, which cannot
    survive type identity. An interning metaclass builds a *real* class, paired
    with a `TYPE_CHECKING` declaration -- a PEP 695 generic yields a
    `_GenericAlias`, which is not a class and fails eve's `type[...]` validation.
    Bases are `(Staggered,)` and deliberately not `(Staggered, base)`, so a
    staggered field is not accepted where its base is required. Verified: real
    class, interning stable, kind inherited, tag resolvable, instances work,
    `copyreg` round-trips both the parametrized class and the bare base, and all
    three escape routes (subclassing either level, double subscript) are blocked.
  * `ConstListDim` is declared **once** in `common`. It used to be built
    independently in `iterator/embedded.py` and in the DaCe lowering, which was
    harmless while dimensions compared by `(name, kind)`. Under nominal identity
    those would be two different dimensions and the `offset_type == _CONST_DIM`
    checks in the DaCe lowering would stop matching `ListType`s built by embedded
    execution.
Continues the dimension-classes core. `tests/next_tests/unit_tests` is green
(1752 passed); integration and DaCe suites not yet run.

Source:
  * `.value` -> `.tag` on dimensions, replayed from #2844 by exact line match (88
    sites) plus the lines that had drifted since its base.
  * `isinstance(x, Dimension)` and `case Dimension()` -> `DimensionMeta`, since
    `Dimension` is a PEP 695 alias that neither accepts.
  * `resolve()` at the IR boundaries that rebuild a dimension from a tag.
  * `NamedIndex` removed: an index is an instance of its dimension. Four sites
    tuple-unpacked an index and now read `.dim` / `.value`.
  * Injective mangling applied to generated identifiers in gtfn (the declared
    `TagDefinition`s and the references to them), nanobind, DaCe and the
    roundtrip emitter -- declarations and references must mangle identically, or
    the bindings name a C++ type that was never declared.
  * DaCe no longer synthesizes a local dimension from the offset tag; it uses the
    connectivity's own `neighbor_dim`.

Display vs identity, now applied uniformly: diagnostics print `__qualname__`,
IR and codegen use `tag`. `Staggered` overrides `tag` so its `__qualname__` can
stay short. `order_dimensions` sorts by `__qualname__` too: sorting by the
qualified tag made a field's canonical dimension order depend on *which module*
declared each dimension (recorded in ADR 0028).

Tests:
  * Every `Dimension(...)` declaration became a class (codemod); the 132
    function-local ones were hoisted. No hoist changed behaviour: same-variable
    locals merge into one class, and every same-name/different-variable pair
    already differed in kind.
  * Inline `Dimension("X")` expressions map to one shared class per (name, kind)
    per file, because two such calls used to be equal.
  * One string per connectivity: a local dimension's tag is now qualified, so the
    `V2EDim = Dimension("V2E")` convention that made the offset tag, the local
    dimension and the provider key one string is gone. Restored symbolically --
    `FieldOffset(V2EDim.tag, ...)`, `{V2EDim.tag: table}` -- so PR 4 changes only
    the declaration.
  * Cross-module duplicates: dimensions that were value-equal across modules are
    distinct now. `cases_utils` imports the six unstructured dimensions it shared
    with `toy_connectivity` instead of redeclaring them, and four tests import
    rather than redeclare. A same-*variable* check was not enough: the original
    *strings* decide equality, and `test_gtfn_module`'s `IDim` was `"I"` while
    `cases_utils`' was `"IDim"`, so those stay distinct.
It was committed by accident, via a broad `git add` of `docs/`. It is a working
artifact for planning this stack -- with four rounds of review notes folded in --
not documentation for the repository, and ADR 0028 already records the decisions
that need to live here. It stays available outside the PR.
…reen

`tests/next_tests/unit_tests` green on every backend: 1760 passed without DaCe,
451 with. Integration suite not yet re-run.

The main fix is a consequence of the previous commit that it did not follow
through. Pulling the one-string-per-connectivity invariant into this PR makes
the *offset* keys qualified here too, so the offset-name mangling the plan
scheduled for PR 4 is needed now:

  * gtfn: connectivity parameter names and `generated::<name>_t` keys, the
    `SymRef` to each connectivity, and `axis_literal` literals.
  * DaCe: `connectivity_identifier` mangles, and all three places that parse an
    identifier back (`is_connectivity_identifier`, `_field_symbol`, and the
    generated binding code) unmangle -- the binding uses the mangled name as a
    Python variable but must look the table up by the real tag.
  * DaCe `visit_AxisLiteral`: a dotted tag in a symbol name gets re-parsed as an
    attribute access, leaving bare sympy symbols (no `dtype`) in an array's free
    symbols.
  * `IndexConnectorFmt` was mangled at one of its three uses, so a connector was
    declared under one name and referenced under another.

`codegen_name` also escapes `[` and `]`: `Staggered[pkg.K]` has them and they
survived into identifiers (Python read the emitted name as a subscript). The
exhaustive injectivity test now covers `{a . _ [ ] u d l r}`.

Tests: the DaCe tests that hard-coded symbol names -- 190 in one file -- now
compute them from `sdfg_args`, so they stop depending on a naming scheme they
are not testing. The binding golden test formats both sides, since the longer
names make lines wrap. More dimensions that were value-equal across library
modules are unified: `past_common` and `fvm_nabla_setup` import theirs, and
`test_dace_bindings` takes the `cases` dimensions its fixture is sized on.
…n names

* Strict fingerprinter: `Staggered[K]` has no importable qualified name, so the
  by-reference `type` deconstruction rejected it. Registered on `StaggeredMeta`,
  it is fingerprinted by its base dimension (importable), the same reduction its
  `copyreg` hook uses; the bare `Staggered` stays by reference.
* IR text format: the pretty parser read axis and offset literals as `CNAME`, so
  a qualified tag (dots, and brackets for `Staggered[pkg.K]`) did not round-trip.
  A `TAG` terminal describes a qualified name; unambiguous, since a tag starts
  with a letter and the literal's suffix terminates it.
* gtfn: the scan column axis and sparse-argument tuple-like dimensions still
  named `generated::<tag>_t` with the raw tag.
* The sparse-argument path read `.tag` off a legacy `FieldOffset`, which only
  has `.value` -- a leftover of the #2844 `.value -> .tag` replay.
Everything now passes: unit 1764 + 451 (DaCe), integration and regression
2745 + DaCe, and every notebook `test_examples` runs.

* Notebooks. Compilation runs in `spawn` workers by default, and a class declared
  in an interactive `__main__` (a notebook, the REPL, `python -c`) pickles in the
  parent but cannot be unpickled in a worker, which has no such `__main__` to
  import. Reproduced with CI's own mechanism (`nbmake`):
  "Can't get attribute 'IDim' on <module '__main__'>". The project's own
  Quickstart and workshop do exactly this, so a documented limitation was not
  enough. The process runner now falls back to the calling thread, with a
  warning, when a job references a class from an interactive `__main__` -- the
  same fallback it already takes for an unpicklable executor. Scripts keep
  parallel compilation: spawn re-imports a script's `__main__`. ADR 0028 updated.
* `__gt_dims__` is the interop protocol with `gt4py.cartesian`, which names axes
  by bare name ("I", "J", "K"), so it returns `__qualname__`, not the qualified
  tag -- otherwise cartesian transposes the array wrongly. No test covered it;
  one does now, and fails with the fix reverted.
* Docs, workshop notebooks and `examples/` migrated; notebook code cells only,
  stored outputs untouched. Both the Quickstart and `slides_2` declared the cell
  dimension twice, harmless while declarations compared equal; the second one is
  dropped, since the Quickstart later combined fields built on each.
* mypy clean on `src/`. `_is_field_axis` called `isinstance` on a PEP 695 alias,
  which raises at runtime; only an `assert` reaches it. `TYPE_BUILTINS` keeps the
  `common.Dimension` alias: its `__name__` is "Dimension", the DSL builtin's
  name. (#2844 swapped in `DimensionIndex`, which would rename the builtin.)
* DaCe orchestration test and two diagnostics that joined tags into messages.
The `src/` doctests were the one suite not covered locally: `pytest tests/` does
not collect them, and nox runs them last, after the main suite. The Python 3.13
and 3.14 nox sessions caught 40 failures there -- all `Dimension("I")` calls on
what is now a non-callable alias. They pass now on 3.12, 3.13 and 3.14 (116).

* 60 doctest declarations become `class X(DimensionIndex): ...`, and 8 inline
  `Dimension("I")` expressions get a declaration hoisted in front of them.
* Expected outputs showing the old `Dimension(value='I', ...)` repr are updated to
  the actual output. Applied only where the difference is purely a dimension's
  spelling, checked mechanically; one update the check refused was a false
  positive and was verified by eye.
* A class declared in a doctest runs in a *copy* of the module's globals, so it
  is not an attribute of the real module and `resolve()` cannot import it. Four
  doctests run an IR pass that resolves a dimension; they bind the class onto the
  module explicitly, with a comment -- the same consequence of nominal identity
  as interactive `__main__`, confined to doctests.
* Those bindings are separate `>>>` statements: written as `import sys; ...` on
  one line, ruff's docstring formatter split them into a continuation that
  doctest compiles as a single statement.
With the dimension half of the mypy plugin gone, a dimension bound to a variable
(`IDim = gtx.Dimension("I")`) is no longer valid in an annotation: only a class
is. `test_typing_exports` runs these snippets in CI and was not covered by any
local suite.

Taken from #2844 unchanged. Static typing depends only on dimensions being
classes, which both designs share; nothing in these snippets touches identity
semantics, and main has not changed the file since #2844's base. 16 passed.
`pre-commit run --all-files` (what CI runs) reformats four lines that now fit on
one line after `.value` -> `.tag`; the per-commit runs only covered staged files.
- a dimension cannot be staggered twice: the check reads the *argument*
- intern staggered dimensions with 'setdefault', since compilation runs in threads
- 'resolve' rejects a bracketed tag whose owner is not a parametrized dimension
- the 'Dim + offset' hint quotes the dimension's display name, not '.value'
- ADR 0026: the name-prefix encoding is superseded by ADR 0028
- drop the FieldOffset tag/name analysis: the knowledge base keeps that record
- 'runtimes': say where an unpicklable job is actually reported
- common.ConstListDim becomes common.ConstList, the local dimension of
  make_const_list, used directly by iterator embedded and the DaCe lowering
  (identity checks)
- AxisLiteral stores only the tag; kind and dim are resolved from it. The pretty
  printer derives the suffix (now with ₗ for local dimensions, which used to be
  printed as vertical), and the parser ignores it.
The pretty printer takes an axis literal's kind from its inferred type, or from a
dimension that is already loaded (common.resolve_loaded), and never imports.
gtfn's domain canonicalization resolves through dim_from_axis_literal.
- ADR 0028's date, which this PR's edits had outrun
- say why 'ListType.offset_type' can be 'None' while embedded uses 'ConstList'
- test 'resolve_loaded', which is what keeps printing IR import-free
ADR 0028 on main is now 'Plain Builders Instead of factory-boy Factories' (#2808),
so the two ADRs of this stack move up by one.
…ed on a declared axis

Add `AnyCartesianAxisIndex` (either cell class of a Cartesian axis) and
`CartesianAxisIndex` (a declared axis) below `DimensionIndex`, both exported as
`gtx.*`. `Staggered[D]` derives from `AnyCartesianAxisIndex` and its parameter is
bounded on `CartesianAxisIndex`, so a doubly staggered dimension, a staggered mesh
location and a staggered local dimension are `[type-var]` errors for mypy and
pyright and `TypeError`s at runtime. `DimensionMeta.__add__` / `__sub__` carry the
self-type `type[AnyCartesianAxisIndex]`, so `Cell + 1` on a mesh location is an
`[operator]` error, a `TypeError` at runtime and a `DSLError` in a field operator.
Comparisons (`D == n`, `D < n`) stay available on every dimension.

The tree's Cartesian dimensions (`IDim`, `KDim`, ...) are declared as
`CartesianAxisIndex`; mesh locations stay `DimensionIndex`. ADR 0029 records the
levels and the equality decision (dimension-vs-dimension `==` is identity).
A dimension's `kind` decides a field's layout order and the scan axis, so a
dimension redefined under the same name with another kind must not reuse
compiled artifacts. Staggered dimensions follow through their base.

This branch has not been deployed

No deployments
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.

3 participants