async: self tail calls loop inside the coroutine, and Async.race [] gets a named error - #119
Merged
Conversation
…ets a named error
Found by the game agent: the cookbook's recursive agent loop awaited a
fresh coroutine per message on the same stack, so ~1,000 messages was a
RecursionError - fatal for the one thing an agent is written to be,
long-lived. The 5.4 rewrite now has an async form: when a body is the
def f(a): async def g(): ...; return g() wrapper, the awaited tail
calls in g rebind the outer parameters through a nonlocal and loop a
while True: inside the one coroutine, under the sync pass's
preconditions and with the same rejection notes. To make the tail call
visible, return! of a match/if in an async block now returns per arm
(the descent a sync tail match already had) instead of assigning a temp
and awaiting it once. The cookbook agent is reworked to the loop-safe
shape (a sync heard helper computes the next state, keeping the
recursive call direct); 100,000 messages complete in the e2e test.
Async.race [] raised asyncio's ValueError about an empty set; racing
nothing would wait forever, so it now raises ValueError('Async.race
needs at least one value') at the await, the module's one partial
member, documented on hover and in DESIGN 6. DESIGN 5.4 and ROADMAP
(feedback-round items 21-22) updated.
Merged
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #118.
The game agent's feedback-round finding: the cookbook's recursive agent loop (
return! agentLoop inbox (count + 1)) nested an await per message, so ~1,000 messages hitRecursionError— a clock postingTickevery second died in under twenty minutes, and an agent is precisely the thing written to live indefinitely. Also from the same note:Async.race []raised asyncio's error about an empty set.The §5.4 rewrite gets an async form
let f a = async { … }lowers todef f(a): async def g(): …; return g(), and its self tail call isreturn await f(…)insideg— a fresh coroutine awaited on the same stack per iteration. When a body ends in exactly that wrapper shape, the awaited tail calls now rebind the outer parameters (they areg's closure, so anonlocalmakes them writable) and go round awhile True:inside the one coroutine:The sync pass's preconditions apply to
g's body unchanged (name not rebound, no generator, no closure capturing a rebound name), with the same rejection notes, so a refused rewrite still tells the author why it kept recursing.Making the tail call visible.
return! (match …)in anasync { }block used to lower the match in value position (_pf_t0 = agentLoop(...)…return await _pf_t0), which hid the call from any tail-position analysis. It now returns per arm — the same descent a sync function's tailmatchalready had — which is also simply better output.return (match …)gets the synclower_returndescent for the same reason.The cookbook agent is reworked to the loop-safe shape: a sync
heard count shelper performs the effect and returns the next count, so the recursive call stays a direct tail call (case Said s: agentLoop inbox (heard count s)); a helper returning the nextAsyncwould be mutual recursion, which stays out of scope as in the sync pass, andDESIGN.md§5.4 now says so. Output unchanged.Async.race []Racing nothing would wait forever, so the empty list raises
ValueError("Async.race needs at least one value")at the await — the module's one partial member, documented on hover and inDESIGN.md§6 — instead ofasyncio.wait's message about an empty set.Async.catchreports it like any await-time failure.Tests
Shape: the loop with
nonlocal, per-armreturn await, no recursive call left; the cookbook example asserted to loop. Behaviour: 100,000 awaited tail calls complete e2e (past the ~1,000 limit by two orders of magnitude); the closure-capture rejection keeps the recursive form and emits the note; the empty race prints the named error throughAsync.catch. ROADMAP records both as feedback-round items 21–22.cargo test,cargo clippy --all-targetsandcargo fmt --checkare clean.