Design an asyncio-Style Event Loop
Design a single-threaded event loop that runs coroutines as tasks, with sleep, gather, cancellation, and a clock tests can fake.
For this exercise, assume you are building a small concurrency library in the style of Python's asyncio, for one process and one thread. User code is written as coroutines (async functions) that pause at await points, and one event loop runs them. There are no threads inside the loop.
Library users must be able to: run a top-level coroutine until it finishes and get its result; start another coroutine as a task that runs concurrently with the caller; pause with await sleep(seconds); await a task to get its result or have its error raised; gather several awaitables and get their results in input order; and cancel a task.
Assume these rules. Only one task runs at a time, and a task keeps running until its next await, because nothing preempts it. A task finishes exactly once: with a result, with an error, or as cancelled. Tasks that become ready are resumed in first-in, first-out order.
A timer never fires before its deadline, measured by a monotonic clock that tests can replace with a fake clock. A sleeping task uses no CPU, and the loop never busy-waits. Awaiting a failed task raises its error again.
If one child of gather fails, gather fails with the first error, and the other children keep running. cancel() only asks for cancellation and returns true when the request is accepted: the task sees a cancellation error at the await where it is paused when it next resumes, and it may catch that error to clean up.
Cancelling a finished task does nothing and returns false. Cancelling a task also cancels the one thing it is waiting on, such as its sleep or a child task it awaits. When the top-level coroutine finishes, any tasks still pending are cancelled and allowed to finish their cleanup before run returns.
Design the objects, responsibilities, interfaces, and interactions for this library. Two follow-ups add timeouts and then waiting for socket readiness. Real network and file I/O (until that follow-up), thread pools, calls from other threads, several loops, async generators, and task priorities are out of scope.