Complete futures in closure finally (fix #415) (#449)

* Complete in closure finally

* cleanup tests, add comment

* handle defects

* don't complete future on defect

* complete future in test to avoid failure

* fix with strict exceptions

* fix regressions

* fix nim 1.6
This commit is contained in:
Tanguy 2023-10-16 10:38:11 +02:00 committed by GitHub
parent 2e8551b0d9
commit 253bc3cfc0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 271 additions and 110 deletions

View File

@ -311,57 +311,30 @@ proc internalContinue(fut: pointer) {.raises: [], gcsafe.} =
proc futureContinue*(fut: FutureBase) {.raises: [], gcsafe.} =
# This function is responsible for calling the closure iterator generated by
# the `{.async.}` transformation either until it has completed its iteration
# or raised and error / been cancelled.
#
# Every call to an `{.async.}` proc is redirected to call this function
# instead with its original body captured in `fut.closure`.
var next: FutureBase
template iterate =
while true:
# Call closure to make progress on `fut` until it reaches `yield` (inside
# `await` typically) or completes / fails / is cancelled
next = fut.internalClosure(fut)
if fut.internalClosure.finished(): # Reached the end of the transformed proc
break
while true:
# Call closure to make progress on `fut` until it reaches `yield` (inside
# `await` typically) or completes / fails / is cancelled
let next: FutureBase = fut.internalClosure(fut)
if fut.internalClosure.finished(): # Reached the end of the transformed proc
break
if next == nil:
raiseAssert "Async procedure (" & ($fut.location[LocationKind.Create]) &
") yielded `nil`, are you await'ing a `nil` Future?"
if next == nil:
raiseAssert "Async procedure (" & ($fut.location[LocationKind.Create]) &
") yielded `nil`, are you await'ing a `nil` Future?"
if not next.finished():
# We cannot make progress on `fut` until `next` has finished - schedule
# `fut` to continue running when that happens
GC_ref(fut)
next.addCallback(CallbackFunc(internalContinue), cast[pointer](fut))
if not next.finished():
# We cannot make progress on `fut` until `next` has finished - schedule
# `fut` to continue running when that happens
GC_ref(fut)
next.addCallback(CallbackFunc(internalContinue), cast[pointer](fut))
# return here so that we don't remove the closure below
return
# return here so that we don't remove the closure below
return
# Continue while the yielded future is already finished.
when chronosStrictException:
try:
iterate
except CancelledError:
fut.cancelAndSchedule()
except CatchableError as exc:
fut.fail(exc)
finally:
next = nil # GC hygiene
else:
try:
iterate
except CancelledError:
fut.cancelAndSchedule()
except CatchableError as exc:
fut.fail(exc)
except Exception as exc:
if exc of Defect:
raise (ref Defect)(exc)
fut.fail((ref ValueError)(msg: exc.msg, parent: exc))
finally:
next = nil # GC hygiene
# Continue while the yielded future is already finished.
# `futureContinue` will not be called any more for this future so we can
# clean it up

View File

@ -9,60 +9,14 @@
import std/[macros]
# `quote do` will ruin line numbers so we avoid it using these helpers
proc completeWithResult(fut, baseType: NimNode): NimNode {.compileTime.} =
# when `baseType` is void:
# complete(`fut`)
# else:
# complete(`fut`, result)
if baseType.eqIdent("void"):
# Shortcut if we know baseType at macro expansion time
newCall(ident "complete", fut)
else:
# `baseType` might be generic and resolve to `void`
nnkWhenStmt.newTree(
nnkElifExpr.newTree(
nnkInfix.newTree(ident "is", baseType, ident "void"),
newCall(ident "complete", fut)
),
nnkElseExpr.newTree(
newCall(ident "complete", fut, ident "result")
)
)
proc completeWithNode(fut, baseType, node: NimNode): NimNode {.compileTime.} =
# when typeof(`node`) is void:
# `node` # statement / explicit return
# -> completeWithResult(fut, baseType)
# else: # expression / implicit return
# complete(`fut`, `node`)
if node.kind == nnkEmpty: # shortcut when known at macro expanstion time
completeWithResult(fut, baseType)
else:
# Handle both expressions and statements - since the type is not know at
# macro expansion time, we delegate this choice to a later compilation stage
# with `when`.
nnkWhenStmt.newTree(
nnkElifExpr.newTree(
nnkInfix.newTree(
ident "is", nnkTypeOfExpr.newTree(node), ident "void"),
newStmtList(
node,
completeWithResult(fut, baseType)
)
),
nnkElseExpr.newTree(
newCall(ident "complete", fut, node)
)
)
proc processBody(node, fut, baseType: NimNode): NimNode {.compileTime.} =
proc processBody(node, setResultSym, baseType: NimNode): NimNode {.compileTime.} =
#echo(node.treeRepr)
case node.kind
of nnkReturnStmt:
let
res = newNimNode(nnkStmtList, node)
res.add completeWithNode(fut, baseType, processBody(node[0], fut, baseType))
if node[0].kind != nnkEmpty:
res.add newCall(setResultSym, processBody(node[0], setResultSym, baseType))
res.add newNimNode(nnkReturnStmt, node).add(newNilLit())
res
@ -71,12 +25,89 @@ proc processBody(node, fut, baseType: NimNode): NimNode {.compileTime.} =
node
else:
for i in 0 ..< node.len:
# We must not transform nested procedures of any form, otherwise
# `fut` will be used for all nested procedures as their own
# `retFuture`.
node[i] = processBody(node[i], fut, baseType)
# We must not transform nested procedures of any form, since their
# returns are not meant for our futures
node[i] = processBody(node[i], setResultSym, baseType)
node
proc wrapInTryFinally(fut, baseType, body: NimNode): NimNode {.compileTime.} =
# creates:
# var closureSucceeded = true
# try: `body`
# except CancelledError: closureSucceeded = false; `castFutureSym`.cancelAndSchedule()
# except CatchableError as exc: closureSucceeded = false; `castFutureSym`.fail(exc)
# except Defect as exc:
# closureSucceeded = false
# raise exc
# finally:
# if closureSucceeded:
# `castFutureSym`.complete(result)
# we are completing inside finally to make sure the completion happens even
# after a `return`
let closureSucceeded = genSym(nskVar, "closureSucceeded")
var nTry = nnkTryStmt.newTree(body)
nTry.add nnkExceptBranch.newTree(
ident"CancelledError",
nnkStmtList.newTree(
nnkAsgn.newTree(closureSucceeded, ident"false"),
newCall(ident "cancelAndSchedule", fut)
)
)
nTry.add nnkExceptBranch.newTree(
nnkInfix.newTree(ident"as", ident"CatchableError", ident"exc"),
nnkStmtList.newTree(
nnkAsgn.newTree(closureSucceeded, ident"false"),
newCall(ident "fail", fut, ident"exc")
)
)
nTry.add nnkExceptBranch.newTree(
nnkInfix.newTree(ident"as", ident"Defect", ident"exc"),
nnkStmtList.newTree(
nnkAsgn.newTree(closureSucceeded, ident"false"),
nnkRaiseStmt.newTree(ident"exc")
)
)
when not chronosStrictException:
# adds
# except Exception as exc:
# closureSucceeded = false
# fut.fail((ref ValueError)(msg: exc.msg, parent: exc))
let excName = ident"exc"
nTry.add nnkExceptBranch.newTree(
nnkInfix.newTree(ident"as", ident"Exception", ident"exc"),
nnkStmtList.newTree(
nnkAsgn.newTree(closureSucceeded, ident"false"),
newCall(ident "fail", fut,
quote do: (ref ValueError)(msg: `excName`.msg, parent: `excName`)),
)
)
nTry.add nnkFinally.newTree(
nnkIfStmt.newTree(
nnkElifBranch.newTree(
closureSucceeded,
nnkWhenStmt.newTree(
nnkElifExpr.newTree(
nnkInfix.newTree(ident "is", baseType, ident "void"),
newCall(ident "complete", fut)
),
nnkElseExpr.newTree(
newCall(ident "complete", fut, ident "result")
)
)
)
)
)
return nnkStmtList.newTree(
newVarStmt(closureSucceeded, ident"true"),
nTry
)
proc getName(node: NimNode): string {.compileTime.} =
case node.kind
of nnkSym:
@ -153,8 +184,9 @@ proc asyncSingleProc(prc: NimNode): NimNode {.compileTime.} =
if baseTypeIsVoid: futureVoidType
else: returnType
castFutureSym = nnkCast.newTree(internalFutureType, internalFutureSym)
setResultSym = ident"setResult"
procBody = prc.body.processBody(castFutureSym, baseType)
procBody = prc.body.processBody(setResultSym, baseType)
# don't do anything with forward bodies (empty)
if procBody.kind != nnkEmpty:
@ -199,9 +231,44 @@ proc asyncSingleProc(prc: NimNode): NimNode {.compileTime.} =
)
)
completeDecl = completeWithNode(castFutureSym, baseType, procBodyBlck)
# generates:
# template `setResultSym`(code: untyped) {.used.} =
# when typeof(code) is void: code
# else: result = code
#
# this is useful to handle implicit returns, but also
# to bind the `result` to the one we declare here
setResultDecl =
nnkTemplateDef.newTree(
setResultSym,
newEmptyNode(), newEmptyNode(),
nnkFormalParams.newTree(
newEmptyNode(),
nnkIdentDefs.newTree(
ident"code",
ident"untyped",
newEmptyNode(),
)
),
nnkPragma.newTree(ident"used"),
newEmptyNode(),
nnkWhenStmt.newTree(
nnkElifBranch.newTree(
nnkInfix.newTree(ident"is", nnkTypeOfExpr.newTree(ident"code"), ident"void"),
ident"code"
),
nnkElse.newTree(
newAssignment(ident"result", ident"code")
)
)
)
closureBody = newStmtList(resultDecl, completeDecl)
completeDecl = wrapInTryFinally(
castFutureSym, baseType,
newCall(setResultSym, procBodyBlck)
)
closureBody = newStmtList(resultDecl, setResultDecl, completeDecl)
internalFutureParameter = nnkIdentDefs.newTree(
internalFutureSym, newIdentNode("FutureBase"), newEmptyNode())
@ -225,10 +292,6 @@ proc asyncSingleProc(prc: NimNode): NimNode {.compileTime.} =
# here the possibility of transporting more specific error types here
# for example by casting exceptions coming out of `await`..
let raises = nnkBracket.newTree()
when chronosStrictException:
raises.add(newIdentNode("CatchableError"))
else:
raises.add(newIdentNode("Exception"))
closureIterator.addPragma(nnkExprColonExpr.newTree(
newIdentNode("raises"),

View File

@ -17,11 +17,6 @@ export srcloc
when chronosStackTrace:
type StackTrace = string
when chronosStrictException:
{.pragma: closureIter, raises: [CatchableError], gcsafe.}
else:
{.pragma: closureIter, raises: [Exception], gcsafe.}
type
LocationKind* {.pure.} = enum
Create
@ -54,7 +49,7 @@ type
internalState*: FutureState
internalFlags*: FutureFlags
internalError*: ref CatchableError ## Stored exception
internalClosure*: iterator(f: FutureBase): FutureBase {.closureIter.}
internalClosure*: iterator(f: FutureBase): FutureBase {.raises: [], gcsafe.}
when chronosFutureId:
internalId*: uint

View File

@ -94,6 +94,11 @@ proc testAwaitne(): Future[bool] {.async.} =
return true
template returner =
# can't use `return 5`
result = 5
return
suite "Macro transformations test suite":
test "`await` command test":
check waitFor(testAwait()) == true
@ -136,6 +141,131 @@ suite "Macro transformations test suite":
check:
waitFor(gen(int)) == default(int)
test "Nested return":
proc nr: Future[int] {.async.} =
return
if 1 == 1:
return 42
else:
33
check waitFor(nr()) == 42
suite "Macro transformations - completions":
test "Run closure to completion on return": # issue #415
var x = 0
proc test415 {.async.} =
try:
return
finally:
await sleepAsync(1.milliseconds)
x = 5
waitFor(test415())
check: x == 5
test "Run closure to completion on defer":
var x = 0
proc testDefer {.async.} =
defer:
await sleepAsync(1.milliseconds)
x = 5
return
waitFor(testDefer())
check: x == 5
test "Run closure to completion with exceptions":
var x = 0
proc testExceptionHandling {.async.} =
try:
return
finally:
try:
await sleepAsync(1.milliseconds)
raise newException(ValueError, "")
except ValueError:
await sleepAsync(1.milliseconds)
await sleepAsync(1.milliseconds)
x = 5
waitFor(testExceptionHandling())
check: x == 5
test "Correct return value when updating result after return":
proc testWeirdCase: int =
try: return 33
finally: result = 55
proc testWeirdCaseAsync: Future[int] {.async.} =
try:
await sleepAsync(1.milliseconds)
return 33
finally: result = 55
check:
testWeirdCase() == waitFor(testWeirdCaseAsync())
testWeirdCase() == 55
test "Generic & finally calling async":
proc testGeneric(T: type): Future[T] {.async.} =
try:
try:
await sleepAsync(1.milliseconds)
return
finally:
await sleepAsync(1.milliseconds)
await sleepAsync(1.milliseconds)
result = 11
finally:
await sleepAsync(1.milliseconds)
await sleepAsync(1.milliseconds)
result = 12
check waitFor(testGeneric(int)) == 12
proc testFinallyCallsAsync(T: type): Future[T] {.async.} =
try:
await sleepAsync(1.milliseconds)
return
finally:
result = await testGeneric(T)
check waitFor(testFinallyCallsAsync(int)) == 12
test "templates returning":
proc testReturner: Future[int] {.async.} =
returner
doAssert false
check waitFor(testReturner()) == 5
proc testReturner2: Future[int] {.async.} =
template returner2 =
return 6
returner2
doAssert false
check waitFor(testReturner2()) == 6
test "raising defects":
proc raiser {.async.} =
# sleeping to make sure our caller is the poll loop
await sleepAsync(0.milliseconds)
raise newException(Defect, "uh-oh")
let fut = raiser()
expect(Defect): waitFor(fut)
check not fut.completed()
fut.complete()
test "return result":
proc returnResult: Future[int] {.async.} =
var result: int
result = 12
return result
check waitFor(returnResult()) == 12
test "async in async":
proc asyncInAsync: Future[int] {.async.} =
proc a2: Future[int] {.async.} =
result = 12
result = await a2()
check waitFor(asyncInAsync()) == 12
suite "Macro transformations - implicit returns":
test "Implicit return":
proc implicit(): Future[int] {.async.} =
42