Do not return to scheduler while completing {.async.} proc#668
Open
etan-status wants to merge 17 commits into
Open
Do not return to scheduler while completing {.async.} proc#668etan-status wants to merge 17 commits into
etan-status wants to merge 17 commits into
Conversation
In contrast to other languages, Chronos has implicit yield points on
every stack frame when an inner proc yielded; it runs synchronously
if there are solely non-yielding `await`. For example, here, an extra
yield point is introduced when returning from `foo`, and side effects
that execute while unwinding the stack modify the state so that the
returned value is already stale by the time it is consumed.
```nim
import chronos
proc a(x: ref int) =
x[] = 42
echo "set to " & $x[]
proc foo(x: ref int): Future[int] {.async.} =
await sleepAsync(ZeroDuration)
callSoon do(arg: pointer):
a(x)
x[] = 69
echo "set to " & $x[]
x[]
proc bar(x: ref int) {.async.} =
let res = await foo(x)
echo "foo returned " & $res & " but x is now " & $x[]
var x = new int
let fut = bar(x)
while not fut.completed:
echo "--- poll"
poll()
```
While this is deterministic, it needs extra consideration and can be
confusing as these extra yield points are implicit, i.e., invisible
to the user. As other languages schedule the continuation synchronously
across the entire unwind process, this adds extra confusion because the
keywords look the same but behave slightly different.
This is changed so that unwinding from an `{.async.}` proc no longer
schedules the callback last into the next event loop poll, but instead
executes it immediately at the next opportunity (before other callbacks,
events or timers). Any other continuations still are scheduled normally
to minimize risk of starving the event loop with endless continuations.
Contributor
Author
|
|
etan-status
commented
Jun 23, 2026
cheatfate
reviewed
Jun 23, 2026
etan-status
commented
Jun 23, 2026
Contributor
Author
|
Example bug that is addressed by this PR: #273 Obviously, it is possible to write correct code even without this PR. But when translating from other languages, doing spec copying, or using LLMs, subtle bugs can creep in. These bugs are hard to find in a review because they come from hidden implicit behaviour rather than visibly incorrect lines of code. This PR simply removes this class of footgun. How much it makes it easier to accidentally stall the event loop, remains to be seen. |
etan-status
commented
Jun 24, 2026
arnetheduck
reviewed
Jun 25, 2026
arnetheduck
reviewed
Jun 25, 2026
arnetheduck
reviewed
Jun 25, 2026
Co-authored-by: Jacek Sieka <jacek@status.im>
etan-status
commented
Jun 25, 2026
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.
Chronos can have an extra implicit return to scheduler when an
{.async.}proc completes.Output:
Since
innerdoes not complete synchronously, Chronos adds an extra return to scheduler between C -> D, during which different callbacks / events may be processed. However, the{.async.}transformation macro hides this extra return to scheduler from the user: Visually this looks like a regularreturnto the outer proc, even though potential side effects similar to those from an explicitawaithave to be considered.This PR removes the extra return to scheduler when completing an
{.async.}proc, even when the proc did not fully run to completion synchronously. For other, explicitly added callbacks, existing behaviour is unchanged; a newFutureFlag.SyncContinuationscan be used to emulate the{.async.}proc behaviour.chronosSyncContinuationsis enabled.This is in line with other popular languages, where continuations across returns commonly complete without returning to the scheduler. Users more familiar with these other languages as well as LLMs may introduce subtle bugs, because the keywords and control flow look the same, while semantically behaving slightly different in Chronos in certain situations.
A B C competitor DA B C D competitorA B C D competitorA B C D competitorA B C competitor DA B C D competitorA B C D competitorA B C D competitorA B C D competitorNote the two JavaScript rows, where the
queueMicrotaskAPI enables queueing the competitor ahead of the return-continuation. In contrast to Chronos, this still does not allow timers and I/O events to be executed (they are macrotasks).LLM-generated translations.
C#
Python
JavaScript (setTimeout)
JavaScript (queueMicrotask)
Rust
C++
Swift
Kotlin