1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
|
<h1 align="center">AnyOutput Mechanism</h1>
<p align="center">
How AnyOutput and ChainProcess work
</p>
What data is passed between the Dispatcher → Chain → Renderer stages?
A Chain's output could be a successful result, an error, or something that still needs to go to the next Chain—these types are all different. How does the pipeline route them to the right place without knowing the concrete types at compile time?
## AnyOutput: Type Erasure + Group Tag
Mingling's solution is to **erase all types into the same wrapper**, then use an **enum tag** to distinguish them:
```
AnyOutput<G>
├── inner: Box<dyn Any + Send> ← the real data, type erased
├── type_id: TypeId ← runtime type ID, for safe downcast
└── member_id: G ← enum tag, marks "who this is"
```
Here `G` is the program enum generated by `gen_program!()` (i.e., `ThisProgram` as you know it).
Each type annotated with `pack!` or `#[derive(Groupped)]` is assigned to one variant of this enum.
## ChainProcess: Data + Routing
On top of `AnyOutput`, `ChainProcess<G>` adds **routing info**:
```
ChainProcess<G>
├── Ok(AnyOutput<G>, NextProcess) ← carries data, tells the dispatcher where to go next
│ ├── NextProcess::Chain ← "not done yet, pass to the next Chain"
│ └── NextProcess::Renderer ← "got a result, show it to the user"
└── Err(ChainProcessError) ← "something went wrong, abort"
```
This is why a Chain function returns `ChainProcess` instead of raw data—it bundles **"where to go next"** and **"the data"** together.
The dispatcher reads `NextProcess` to decide whether to continue the loop or exit to rendering.
## Groupped: Who Is Who
How does the dispatcher know whether an `AnyOutput` holds a `ResultName` or an `ErrorUserBlocked`? The answer is the `Groupped` trait:
```
trait Groupped<G> {
fn member_id() -> G;
}
```
When you use `pack!(ResultName = String)`, the macro automatically implements `Groupped` for `ResultName`, and `member_id()` returns the corresponding enum variant. The dispatcher looks at `member_id` and finds the matching Chain or Renderer.
`to_chain()` and `to_render()` are essentially convenience methods on `AnyOutput` that construct `ChainProcess::Ok(any, Chain)` and `ChainProcess::Ok(any, Renderer)` respectively.
## How Dispatching Works
At runtime, the main loop does this:
1. Check the current `AnyOutput`'s `member_id`
2. Look up whether this variant has a Chain → if yes, execute it, get a new `AnyOutput` and `NextProcess`
3. If `NextProcess` is `Chain` → go back to step 1
4. If `NextProcess` is `Renderer` → exit the loop, render
This mechanism ensures **type safety**: the dispatch code generated by `gen_program!()` only does `restore` (converting from `Box<dyn Any>` back to a concrete type) inside the matching `member_id` branch, so it's impossible to unwrap a `ResultName`'s data as if it were `ErrorUserBlocked`.
> [!TIP]
> In day-to-day dev, you don't need to manually touch `AnyOutput` or `ChainProcess`.
>
> Macros like `pack!`, `#[chain]`, and `#[renderer]` handle all the wrapping and unwrapping for you.
<p align="center" style="font-size: 0.85em; color: gray;">
Written by @Weicao-CatilGrass
</p>
|