Skip to content

Commit 5c19da1

Browse files
committed
RFC #80: Simulation task groups.
1 parent 89716e3 commit 5c19da1

File tree

1 file changed

+128
-0
lines changed

1 file changed

+128
-0
lines changed
Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
- Start Date: (fill in with date at which the RFC is merged, YYYY-MM-DD)
2+
- RFC PR: [amaranth-lang/rfcs#80](https://github.com/amaranth-lang/rfcs/pull/80)
3+
- Amaranth Issue: [amaranth-lang/amaranth#0000](https://github.com/amaranth-lang/amaranth/issues/0000)
4+
5+
# Simulation task groups
6+
7+
## Summary
8+
[summary]: #summary
9+
10+
Add task groups to the simulator to allow parallel execution in a testbench.
11+
12+
## Motivation
13+
[motivation]: #motivation
14+
15+
When testing a component, it's common to need to interact with multiple interfaces in parallel.
16+
For instance, when testing a stream component, the output stream must typically be read in parallel with writing the input stream to avoid backpressure from the output stream to propagate back to the input stream and deadlock the simulation.
17+
Currently this must be done by adding independent testbenches, which can be awkward to synchronize.
18+
19+
## Guide-level explanation
20+
[guide-level-explanation]: #guide-level-explanation
21+
22+
Typical testbenches for a stream component can currently look like this:
23+
```python
24+
test_vectors = [...]
25+
26+
async def input_testbench(ctx: SimulatorContext):
27+
for input in test_vectors:
28+
await send_packet(ctx, dut.i, input)
29+
30+
async def output_testbench(ctx: SimulatorContext):
31+
for input in test_vectors:
32+
output = await recv_packet(ctx, dut.o)
33+
assert output == expected_output(input)
34+
```
35+
36+
With task groups, this can instead be written like:
37+
```python
38+
test_vectors = [...]
39+
40+
async def testbench(ctx: SimulatorContext):
41+
for input in test_vectors:
42+
async with ctx.group() as group:
43+
group.start(send_packet(ctx, dut.i, input))
44+
output = await recv_packet(ctx, dut.o)
45+
assert output == expected_output(input)
46+
```
47+
48+
In a similar manner to background testbenches, it is also possible to add background tasks, for tasks that are not intended to run to completion.
49+
This allows code like this:
50+
```python
51+
@asynccontextmanager
52+
async def timeout(ctx, ticks, domain = 'sync'):
53+
async def task():
54+
await ctx.tick(domain).repeat(ticks) # Never returns if the task group ends before `ticks` have elapsed.
55+
raise TimeoutError()
56+
57+
async with ctx.group() as group:
58+
group.start(task(ctx, ticks, domain), background = True)
59+
yield
60+
61+
async def testbench(ctx):
62+
async with timeout(ctx, 100):
63+
... # Some operation expected to take less than 100 ticks
64+
65+
async with timeout(ctx, 200):
66+
... # Some other operation expected to take less than 200 ticks
67+
```
68+
69+
## Reference-level explanation
70+
[reference-level-explanation]: #reference-level-explanation
71+
72+
`SimulatorContext` have the following methods added:
73+
- `group() -> TaskGroup`
74+
- Create a new task group.
75+
- `async gather(coros*) -> tuple`
76+
- Shorthand for creating a task group, starting all `coros`, letting the group run to completion and collecting the return values.
77+
- Example implementation:
78+
```python
79+
async def gather(self, *coros):
80+
async with self.group() as group:
81+
tasks = [group.start(coro) for coro in coros]
82+
return tuple(task.result() for task in tasks)
83+
```
84+
85+
`TaskGroup` is added with the following methods:
86+
- `start(coro, *, background = False) -> Task`
87+
- Create and start a new task.
88+
- A background task can be temporarily made non-background with `ctx.critical()` like a regular testbench.
89+
- `async __aenter__() -> Self`
90+
- Return `self`.
91+
- `async __aexit__(...)`
92+
- Wait for all non-background tasks to run to completion.
93+
- Drop any incomplete background tasks.
94+
95+
`Task` is added with the following methods:
96+
- `result() -> Any`
97+
- Get the return value of a completed task.
98+
- Raise an exception if the task has not completed.
99+
100+
Exception propagation and task cancellation (beyond dropping background tasks when a group is done) are out of scope for this RFC.
101+
If a task raises any unhandled exceptions, this immediately terminates the simulation and propagates out of the simulator as if raised from an independent testbench.
102+
103+
## Drawbacks
104+
[drawbacks]: #drawbacks
105+
106+
- Increased simulator complexity.
107+
108+
## Rationale and alternatives
109+
[rationale-and-alternatives]: #rationale-and-alternatives
110+
111+
Exception propagation and task cancellation was omitted from the scope of this RFC because it would significantly increase complexity, for limited benefit.
112+
It is expected that the desired outcome of an unhandled exception in a task in most cases would be to terminate simulation and therefore don't need the ability for the parent testbench to catch it.
113+
114+
## Prior art
115+
[prior-art]: #prior-art
116+
117+
The proposed API is modelled after `asyncio.TaskGroup` and `asyncio.gather()`.
118+
119+
## Unresolved questions
120+
[unresolved-questions]: #unresolved-questions
121+
122+
- The usual bikeshedding of names.
123+
124+
## Future possibilities
125+
[future-possibilities]: #future-possibilities
126+
127+
- A future RFC could add exception propagation and task cancellation.
128+
- Context managers like the timeout example above could be added for common cases.

0 commit comments

Comments
 (0)