Skip to content

Commit 26f5ee5

Browse files
committed
sim.core: use asyncgen hooks to call aclose()
Async event loops are responsible for cleaning up async generators. This is a best-effort implementation to call aclose() when possible. Reverts #1590 and closes #1638.
1 parent 638edff commit 26f5ee5

File tree

2 files changed

+43
-19
lines changed

2 files changed

+43
-19
lines changed

amaranth/sim/_async.py

Lines changed: 12 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -362,15 +362,12 @@ async def until(self, condition: ValueLike):
362362
raise TypeError(f"The shape of a condition may only be `signed` or `unsigned`, "
363363
f"not {shape!r}")
364364
tick = self.sample(condition).__aiter__()
365-
try:
366-
done = False
367-
while not done:
368-
clk, rst, *values, done = await tick.__anext__()
369-
if rst:
370-
raise DomainReset
371-
return tuple(values)
372-
finally:
373-
await tick.aclose()
365+
done = False
366+
while not done:
367+
clk, rst, *values, done = await tick.__anext__()
368+
if rst:
369+
raise DomainReset
370+
return tuple(values)
374371

375372
async def repeat(self, count: int):
376373
"""Repeat this trigger a specific number of times.
@@ -403,15 +400,12 @@ async def repeat(self, count: int):
403400
if count <= 0:
404401
raise ValueError(f"Repeat count must be a positive integer, not {count!r}")
405402
tick = self.__aiter__()
406-
try:
407-
for _ in range(count):
408-
clk, rst, *values = await tick.__anext__()
409-
if rst:
410-
raise DomainReset
411-
assert clk
412-
return tuple(values)
413-
finally:
414-
await tick.aclose()
403+
for _ in range(count):
404+
clk, rst, *values = await tick.__anext__()
405+
if rst:
406+
raise DomainReset
407+
assert clk
408+
return tuple(values)
415409

416410
def _collect_trigger(self):
417411
clk_polarity = (1 if self._domain.clk_edge == "pos" else 0)

amaranth/sim/core.py

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1+
from contextlib import contextmanager
12
import inspect
3+
import sys
24
import warnings
35

46
from .._utils import deprecated
@@ -341,6 +343,33 @@ def run_until(self, deadline, *, run_passive=None):
341343
while self._engine.now < deadline.femtoseconds:
342344
self.advance()
343345

346+
@contextmanager
347+
def _replace_asyncgen_hooks(self):
348+
# Async generators require hooks for lifetime management. Replace existing hooks with ours
349+
# for the duration of this context manager.
350+
351+
def firstiter(agen):
352+
# Prevent any outer event loop from seeing this generator.
353+
pass
354+
355+
def finalizer(agen):
356+
# Generators can't be closed if they are currently running.
357+
if not agen.ag_running:
358+
# Try to run aclose() once, but skip it if it awaits anything.
359+
try:
360+
coroutine = agen.aclose()
361+
coroutine.send(None)
362+
except StopIteration:
363+
# Success
364+
return
365+
366+
old_hooks = sys.get_asyncgen_hooks()
367+
sys.set_asyncgen_hooks(firstiter=firstiter, finalizer=finalizer)
368+
try:
369+
yield
370+
finally:
371+
sys.set_asyncgen_hooks(*old_hooks)
372+
344373
def advance(self):
345374
"""Advance the simulation.
346375
@@ -356,7 +385,8 @@ def advance(self):
356385
:py:`False` otherwise.
357386
"""
358387
self._running = True
359-
return self._engine.advance()
388+
with self._replace_asyncgen_hooks():
389+
return self._engine.advance()
360390

361391
def write_vcd(self, vcd_file, gtkw_file=None, *, traces=(), fs_per_delta=0):
362392
# `fs_per_delta`` is not currently documented; it is not clear if we want to expose

0 commit comments

Comments
 (0)