Skip to content

Commit ded2e4d

Browse files
committed
refactor[next]: replace workflow combinators with explicit typed pipelines
The workflow-combinator framework introduced by ADR 0011 had grown to a dozen abstractions to express what is, in the end, function composition. Measured against actual use, only `CachedStep` was a deep module; the rest were shallow wrappers around `Callable[[S], T]`, and the reflection loop in `NamedStepSequence.__call__` was `Any`-typed, defeating the static typing ADR 0011 prized. The named pipelines become plain frozen dataclasses with an explicit, fully typed `__call__`: - `backend.Transforms` keeps its input-dependent step *selection* -- the `match` that used to live in `step_order` now lives in `__call__`, where the order is literally readable -- and the `step_order` method is retained only to raise, so a downstream override fails loudly instead of being silently ignored. - `recipes.OTFCompileWorkflow` becomes `backend.CompilePipeline` and spells out its three steps; `otf.recipes` and `otf.toolchain` are deleted. - Both take over emitting the `stage_hook` added in the previous PR. Names, order, count and artifacts are unchanged; the two instrumentation tests that assert the exact stage sequences pass unmodified, which is the proof. Steps are now plain callables, named by the `workflow.Step[S, T]` alias, and customization stays composition-time via `dataclasses.replace`. Deleted: `Workflow`, `ChainableWorkflowMixin`, `ReplaceEnabledWorkflowMixin`, `NamedStepSequence`, `MultiWorkflow`, `StepSequence`, `make_step`, `.chain`, the three adapters in `otf.toolchain`, and the five `adapted_*_factory` wrappers whose only job was to wrap a function into an adapter. `CachedStep`'s body is unchanged; it loses only the mixin bases, and with them `.replace` and `.chain`. Because steps no longer need to be adapter objects, the seven ffront factories collapse to returning either the bare function or a `CachedStep` around it, and the three per-step callers in `decorator.py` lose their wrap/unwrap dance. What ADR 0011's decisions become: named steps with a visible order are now dataclass fields plus an explicit `__call__`; statically typed composition is checked end-to-end instead of through an `Any`-typed reflection loop; customization at composition time is `dataclasses.replace`; and steps still compose across backends because every existing step already satisfies `Step[S, T]`. No behavior change, and -- unlike the naming PR -- no persistent cache key rotates: fingerprints embed a class's qualified name and fields but never its bases, and neither renamed pipeline is reachable from a persistent cache's fingerprint graph. Breaking, with no compatibility aliases: the deleted combinators and the `otf.recipes` / `otf.toolchain` modules, `.replace()` / `.chain()` on the classes that kept them via the mixins, overriding `Transforms.step_order`, `roundtrip.foast_to_gtir_step` (now a data-only step), and `linter_factory(adapter=...)` (the parameter was accepted and ignored). Claude-Session: https://claude.ai/code/session_01R8zRtFMhdJ8c96XJYCXkRk
1 parent 658a51e commit ded2e4d

32 files changed

Lines changed: 566 additions & 919 deletions

File tree

‎docs/development/ADRs/next/0029-Toolchain-Naming-and-Pipeline-Simplification.md‎

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -109,7 +109,7 @@ Named pipelines become frozen dataclasses with an explicit, fully typed
109109
customization stays composition-time via `dataclasses.replace`. The
110110
combinator framework (both mixins, `NamedStepSequence`, `MultiWorkflow`,
111111
`StepSequence`, `make_step`, `.chain`, the three adapters) is deleted;
112-
`CachedStep` is kept unchanged.
112+
`CachedStep` is kept, minus the mixin-provided `.replace` / `.chain`.
113113

114114
What ADR 0011's decisions become:
115115

@@ -135,16 +135,16 @@ needing a variant step builds a variant pipeline with `dataclasses.replace`.
135135

136136
### Phasing
137137

138-
The decisions in this ADR land as a stack of PRs. The naming decisions above
139-
land first, except the `OTFCompileWorkflow` → `CompilePipeline` rename, which
140-
lands with the pipeline PR; the "Pipeline, not combinators" and "Stage
141-
observability" decisions are implemented in follow-up PRs of the same stack.
138+
The decisions in this ADR landed as a stack of PRs: the naming decisions first
139+
(#2741), then stage observability (#2742), then the pipeline simplification
140+
together with the deferred `OTFCompileWorkflow` → `CompilePipeline` rename
141+
(this stack's final PR).
142142

143143
## Consequences
144144

145145
- The dace `__sdfg__` reach-in (duck-typing through `executor.translation`
146-
and mutating frozen stages) will be replaced by a dace-owned translate-only
147-
step built with `dataclasses.replace`.
146+
and mutating frozen stages) was replaced by a dace-owned translate-only
147+
toolchain built with `dataclasses.replace`.
148148
- Downstream code subclassing the deleted combinators or overriding
149149
`Transforms.step_order` must migrate to explicit `__call__` composition.
150150
- Downstream code must migrate to the new names in the same release: the old

‎docs/user/next/advanced/HackTheToolchain.md‎

Lines changed: 28 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -4,51 +4,36 @@ import typing
44

55
from gt4py import next as gtx
66
from gt4py.next.otf import workflow
7-
from gt4py.next.ffront import field_operator_ast as foast, stages as ff_stages
8-
from gt4py import eve
97
```
108

119
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet"><script src="https://spcl.github.io/dace/webclient2/dist/sdfv.js"></script>
1210
<link href="https://spcl.github.io/dace/webclient2/sdfv.css" rel="stylesheet">
1311

1412
## Replace Steps
1513

16-
```python
17-
cached_lowering_toolchain = gtx.backend.DEFAULT_TRANSFORMS.replace(
18-
past_to_itir=gtx.ffront.past_to_itir.past_to_gtir_factory(cached=False)
19-
)
20-
```
21-
22-
## Skip Steps / Change Order
14+
Pipelines are frozen dataclasses whose fields are the steps, so a variant is built with `dataclasses.replace`.
2315

2416
```python
25-
DUMMY_FOP = workflow.ProgramWithArgs(
26-
definition=ff_stages.DSLFieldOperatorDef(definition=None), args=None
17+
cached_lowering_toolchain = dataclasses.replace(
18+
gtx.backend.DEFAULT_TRANSFORMS,
19+
past_to_itir=gtx.ffront.past_to_itir.past_to_gtir_factory(cached=False),
2720
)
2821
```
2922

30-
```python
31-
gtx.backend.DEFAULT_TRANSFORMS.step_order(DUMMY_FOP)
32-
```
33-
34-
```python
35-
@dataclasses.dataclass(frozen=True)
36-
class SkipLinting(gtx.backend.Transforms):
37-
def step_order(self, inp):
38-
order = super().step_order(inp)
39-
if "past_lint" in order:
40-
order.remove("past_lint") # not running "past_lint"
41-
return order
23+
## Skip Steps
4224

25+
Which steps run is decided by `Transforms.__call__` from the type of the input definition; that selection is not configurable. Behavior is customized by replacing a step, so skipping one means replacing it with an identity step.
4326

44-
same_steps = dataclasses.asdict(gtx.backend.DEFAULT_TRANSFORMS)
45-
skip_linting_transforms = SkipLinting(**same_steps)
46-
skip_linting_transforms.step_order(DUMMY_FOP)
27+
```python
28+
skip_linting_transforms = dataclasses.replace(
29+
gtx.backend.DEFAULT_TRANSFORMS,
30+
past_lint=lambda past_def: past_def, # identity step: linting skipped
31+
)
4732
```
4833

49-
## Alternative Workflow
34+
## Alternative Pipeline
5035

51-
Compile workflows are plain frozen dataclasses, so a variant is built by
36+
Compile pipelines are plain frozen dataclasses, so a variant is built by
5237
replacing the steps you want to change on one the builders produced.
5338

5439
```python
@@ -61,7 +46,7 @@ class MyCodeGen: ...
6146
class Cpp2BindingsGen: ...
6247

6348

64-
pure_cpp2_workflow = dataclasses.replace(
49+
pure_cpp2_pipeline = dataclasses.replace(
6550
gtx.program_processors.runners.gtfn.make_gtfn_compile_workflow(
6651
cmake_build_type=gtx.config.CMakeBuildType.DEBUG
6752
),
@@ -70,7 +55,9 @@ pure_cpp2_workflow = dataclasses.replace(
7055
)
7156
```
7257

73-
## Invent new Workflow Types
58+
## Invent new Pipeline Types
59+
60+
A pipeline is just a frozen dataclass of steps with an explicit, fully typed `__call__`. Nothing else is needed, so a non-linear shape is written the same way as a linear one.
7461

7562
```mermaid
7663
graph LR
@@ -89,15 +76,11 @@ OUT_T = typing.TypeVar("OUT_T")
8976

9077

9178
@dataclasses.dataclass(frozen=True)
92-
class FullyModularDiamond(
93-
workflow.ChainableWorkflowMixin[IN_T, OUT_T],
94-
workflow.ReplaceEnabledWorkflowMixin[IN_T, OUT_T],
95-
typing.Protocol[IN_T, OUT_T, A_T, B_T, X_T, Y_T],
96-
):
97-
split: workflow.Workflow[IN_T, tuple[A_T, X_T]]
98-
track_a: workflow.Workflow[A_T, B_T]
99-
track_x: workflow.Workflow[X_T, Y_T]
100-
combine: workflow.Workflow[tuple[B_T, Y_T], OUT_T]
79+
class Diamond(typing.Generic[IN_T, OUT_T, A_T, B_T, X_T, Y_T]):
80+
split: workflow.Step[IN_T, tuple[A_T, X_T]]
81+
track_a: workflow.Step[A_T, B_T]
82+
track_x: workflow.Step[X_T, Y_T]
83+
combine: workflow.Step[tuple[B_T, Y_T], OUT_T]
10184

10285
def __call__(self, inp: IN_T) -> OUT_T:
10386
a, x = self.split(inp)
@@ -106,20 +89,10 @@ class FullyModularDiamond(
10689
return self.combine((b, y))
10790

10891

109-
@dataclasses.dataclass(frozen=True)
110-
class PartiallyModularDiamond(
111-
workflow.ChainableWorkflowMixin[IN_T, OUT_T],
112-
workflow.ReplaceEnabledWorkflowMixin[IN_T, OUT_T],
113-
typing.Protocol[IN_T, OUT_T, A_T, B_T, X_T, Y_T],
114-
):
115-
track_a: workflow.Workflow[A_T, B_T]
116-
track_x: workflow.Workflow[X_T, Y_T]
117-
118-
def split(inp: IN_T) -> tuple[A_T, X_T]: ...
119-
120-
def combine(b: B_T, y: Y_T) -> OUT_T: ...
121-
122-
def __call__(inp: IN_T) -> OUT_T:
123-
a, x = self.split(inp)
124-
return self.combine(b=self.track_a(a), y=self.track_x(x))
92+
Diamond(
93+
split=lambda inp: (inp, inp),
94+
track_a=lambda a: a + 1,
95+
track_x=lambda x: x * 2,
96+
combine=lambda by: by[0] + by[1],
97+
)(3)
12598
```

0 commit comments

Comments
 (0)