Skip to content

Commit 706dcba

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 gone. - `recipes.OTFCompileWorkflow` is renamed `backend.CompilePipeline` (the rename ADR 0027 deferred to this PR) and spells out its three steps. - 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` is kept as-is. `otf.recipes` and `otf.toolchain` become one-release shims. 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 for downstream: subclassing the deleted combinators, 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 1c59da0 commit 706dcba

32 files changed

Lines changed: 606 additions & 892 deletions

File tree

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

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

8989
What ADR 0011's decisions become:
9090

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

111111
### Phasing
112112

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

118118
## Consequences
119119

120120
- The dace `__sdfg__` reach-in (duck-typing through `executor.translation`
121-
and mutating frozen stages) will be replaced by a dace-owned translate-only
122-
step built with `dataclasses.replace`.
121+
and mutating frozen stages) was replaced by a dace-owned translate-only
122+
toolchain built with `dataclasses.replace`.
123123
- Downstream code subclassing the deleted combinators or overriding
124124
`Transforms.step_order` must migrate to explicit `__call__` composition.
125125
- ADR 0011's `otf.step_types` naming section no longer describes the code.

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

Lines changed: 29 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -4,46 +4,31 @@ 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

4934
## Alternative Factory
@@ -56,18 +41,20 @@ class Cpp2BindingsGen: ...
5641

5742

5843
class PureCpp2WorkflowFactory(gtx.program_processors.runners.gtfn.GTFNCompileWorkflowFactory):
59-
translation: workflow.Workflow[
44+
translation: workflow.Step[
6045
gtx.otf.stages.CompilableProgram, gtx.otf.artifacts.ProgramSource
6146
] = MyCodeGen()
62-
bindings: workflow.Workflow[
63-
gtx.otf.artifacts.ProgramSource, gtx.otf.artifacts.ExtensionSource
64-
] = Cpp2BindingsGen()
47+
bindings: workflow.Step[gtx.otf.artifacts.ProgramSource, gtx.otf.artifacts.ExtensionSource] = (
48+
Cpp2BindingsGen()
49+
)
6550

6651

6752
PureCpp2WorkflowFactory(cmake_build_type=gtx.config.CMAKE_BUILD_TYPE.DEBUG)
6853
```
6954

70-
## Invent new Workflow Types
55+
## Invent new Pipeline Types
56+
57+
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.
7158

7259
```mermaid
7360
graph LR
@@ -86,15 +73,11 @@ OUT_T = typing.TypeVar("OUT_T")
8673

8774

8875
@dataclasses.dataclass(frozen=True)
89-
class FullyModularDiamond(
90-
workflow.ChainableWorkflowMixin[IN_T, OUT_T],
91-
workflow.ReplaceEnabledWorkflowMixin[IN_T, OUT_T],
92-
typing.Protocol[IN_T, OUT_T, A_T, B_T, X_T, Y_T],
93-
):
94-
split: workflow.Workflow[IN_T, tuple[A_T, X_T]]
95-
track_a: workflow.Workflow[A_T, B_T]
96-
track_x: workflow.Workflow[X_T, Y_T]
97-
combine: workflow.Workflow[tuple[B_T, Y_T], OUT_T]
76+
class Diamond(typing.Generic[IN_T, OUT_T, A_T, B_T, X_T, Y_T]):
77+
split: workflow.Step[IN_T, tuple[A_T, X_T]]
78+
track_a: workflow.Step[A_T, B_T]
79+
track_x: workflow.Step[X_T, Y_T]
80+
combine: workflow.Step[tuple[B_T, Y_T], OUT_T]
9881

9982
def __call__(self, inp: IN_T) -> OUT_T:
10083
a, x = self.split(inp)
@@ -103,20 +86,10 @@ class FullyModularDiamond(
10386
return self.combine((b, y))
10487

10588

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

0 commit comments

Comments
 (0)