Support implicit returns (#401)

* based on https://github.com/nim-lang/Nim/pull/21898
* also fixes generic Future[T] where T ends up being `void`
This commit is contained in:
Jacek Sieka 2023-06-05 13:02:13 +02:00 committed by GitHub
parent 2c7774d982
commit 157ca4fea5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 182 additions and 102 deletions

View File

@ -9,46 +9,73 @@
import std/[macros]
proc skipUntilStmtList(node: NimNode): NimNode {.compileTime.} =
# Skips a nest of StmtList's.
if node[0].kind == nnkStmtList:
skipUntilStmtList(node[0])
# `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:
node
# `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 processBody(node, retFutureSym: NimNode,
baseTypeIsVoid: bool): NimNode {.compileTime.} =
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.} =
#echo(node.treeRepr)
result = node
case node.kind
of nnkReturnStmt:
result = newNimNode(nnkStmtList, node)
let
res = newNimNode(nnkStmtList, node)
res.add completeWithNode(fut, baseType, processBody(node[0], fut, baseType))
res.add newNimNode(nnkReturnStmt, node).add(newNilLit())
# As I've painfully found out, the order here really DOES matter.
if node[0].kind == nnkEmpty:
if not baseTypeIsVoid:
result.add newCall(newIdentNode("complete"), retFutureSym,
newIdentNode("result"))
else:
result.add newCall(newIdentNode("complete"), retFutureSym)
else:
let x = node[0].processBody(retFutureSym, baseTypeIsVoid)
if x.kind == nnkYieldStmt: result.add x
else:
result.add newCall(newIdentNode("complete"), retFutureSym, x)
result.add newNimNode(nnkReturnStmt, node).add(newNilLit())
return # Don't process the children of this return stmt
res
of RoutineNodes-{nnkTemplateDef}:
# skip all the nested procedure definitions
return node
else: discard
for i in 0 ..< result.len:
# We must not transform nested procedures of any form, otherwise
# `retFutureSym` will be used for all nested procedures as their own
# `retFuture`.
result[i] = processBody(result[i], retFutureSym, baseTypeIsVoid)
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)
node
proc getName(node: NimNode): string {.compileTime.} =
case node.kind
@ -63,10 +90,6 @@ proc getName(node: NimNode): string {.compileTime.} =
else:
error("Unknown name.")
proc verifyReturnType(typeName: string) {.compileTime.} =
if typeName != "Future":
error("Expected return type of 'Future' got '" & typeName & "'")
macro unsupported(s: static[string]): untyped =
error s
@ -95,22 +118,25 @@ proc asyncSingleProc(prc: NimNode): NimNode {.compileTime.} =
## The ``async`` macro supports a stmtList holding multiple async procedures.
if prc.kind notin {nnkProcTy, nnkProcDef, nnkLambda, nnkMethodDef, nnkDo}:
error("Cannot transform " & $prc.kind & " into an async proc." &
" proc/method definition or lambda node expected.")
" proc/method definition or lambda node expected.", prc)
let returnType = cleanupOpenSymChoice(prc.params2[0])
# Verify that the return type is a Future[T]
let baseType =
if returnType.kind == nnkBracketExpr:
let fut = repr(returnType[0])
verifyReturnType(fut)
returnType[1]
elif returnType.kind == nnkEmpty:
ident("void")
if returnType.kind == nnkEmpty:
ident "void"
elif not (
returnType.kind == nnkBracketExpr and eqIdent(returnType[0], "Future")):
error(
"Expected return type of 'Future' got '" & repr(returnType) & "'", prc)
return
else:
raiseAssert("Unhandled async return type: " & $prc.kind)
returnType[1]
let baseTypeIsVoid = baseType.eqIdent("void")
let
baseTypeIsVoid = baseType.eqIdent("void")
futureVoidType = nnkBracketExpr.newTree(ident "Future", ident "void")
if prc.kind in {nnkProcDef, nnkLambda, nnkMethodDef, nnkDo}:
let
@ -124,63 +150,50 @@ proc asyncSingleProc(prc: NimNode): NimNode {.compileTime.} =
let
internalFutureSym = ident "chronosInternalRetFuture"
internalFutureType =
if baseTypeIsVoid:
newNimNode(nnkBracketExpr, prc).add(newIdentNode("Future")).add(newIdentNode("void"))
if baseTypeIsVoid: futureVoidType
else: returnType
castFutureSym = quote do:
cast[`internalFutureType`](`internalFutureSym`)
procBody = prc.body.processBody(castFutureSym, baseTypeIsVoid)
castFutureSym = nnkCast.newTree(internalFutureType, internalFutureSym)
procBody = prc.body.processBody(castFutureSym, baseType)
# don't do anything with forward bodies (empty)
if procBody.kind != nnkEmpty:
# fix #13899, `defer` should not escape its original scope
let procBodyBlck =
newStmtList(newTree(nnkBlockStmt, newEmptyNode(), procBody))
# Avoid too much quote do to not lose original line numbers
let closureBody = if baseTypeIsVoid:
let resultTemplate = quote do:
template result: auto {.used.} =
{.fatal: "You should not reference the `result` variable inside" &
" a void async proc".}
# -> complete(chronosInternalRetFuture)
let complete =
newCall(newIdentNode("complete"), castFutureSym)
newStmtList(resultTemplate, procBodyBlck, complete)
else:
# -> iterator nameIter(chronosInternalRetFuture: Future[T]): FutureBase {.closure.} =
# -> {.push warning[resultshadowed]: off.}
# -> var result: T
# -> {.pop.}
# -> <proc_body>
# -> complete(chronosInternalRetFuture, result)
newStmtList(
# -> {.push warning[resultshadowed]: off.}
newNimNode(nnkPragma).add(newIdentNode("push"),
newNimNode(nnkExprColonExpr).add(newNimNode(nnkBracketExpr).add(
newIdentNode("warning"), newIdentNode("resultshadowed")),
newIdentNode("off"))),
# -> var result: T
newNimNode(nnkVarSection, prc.body).add(
newIdentDefs(newIdentNode("result"), baseType)),
# -> {.pop.})
newNimNode(nnkPragma).add(
newIdentNode("pop")),
procBodyBlck,
# -> complete(chronosInternalRetFuture, result)
newCall(newIdentNode("complete"),
castFutureSym, newIdentNode("result")))
let
internalFutureParameter = nnkIdentDefs.newTree(internalFutureSym, newIdentNode("FutureBase"), newEmptyNode())
# fix #13899, `defer` should not escape its original scope
procBodyBlck = nnkBlockStmt.newTree(newEmptyNode(), procBody)
resultDecl = nnkWhenStmt.newTree(
# when `baseType` is void:
nnkElifExpr.newTree(
nnkInfix.newTree(ident "is", baseType, ident "void"),
quote do:
template result: auto {.used.} =
{.fatal: "You should not reference the `result` variable inside" &
" a void async proc".}
),
# else:
nnkElseExpr.newTree(
newStmtList(
quote do: {.push warning[resultshadowed]: off.},
# var result: `baseType`
nnkVarSection.newTree(
nnkIdentDefs.newTree(ident "result", baseType, newEmptyNode())),
quote do: {.pop.},
)
)
)
completeDecl = completeWithNode(castFutureSym, baseType, procBodyBlck)
closureBody = newStmtList(resultDecl, completeDecl)
internalFutureParameter = nnkIdentDefs.newTree(
internalFutureSym, newIdentNode("FutureBase"), newEmptyNode())
iteratorNameSym = genSym(nskIterator, $prcName)
closureIterator = newProc(iteratorNameSym, [newIdentNode("FutureBase"), internalFutureParameter],
closureBody, nnkIteratorDef)
closureIterator = newProc(
iteratorNameSym,
[newIdentNode("FutureBase"), internalFutureParameter],
closureBody, nnkIteratorDef)
iteratorNameSym.copyLineInfo(prc)
@ -221,6 +234,7 @@ proc asyncSingleProc(prc: NimNode): NimNode {.compileTime.} =
# doesn't reference it, avoid cyclic ref (#203)
let
retFutureSym = ident "resultFuture"
retFutureSym.copyLineInfo(prc)
# Do not change this code to `quote do` version because `instantiationInfo`
# will be broken for `newFuture()` call.
outerProcBody.add(
@ -230,7 +244,6 @@ proc asyncSingleProc(prc: NimNode): NimNode {.compileTime.} =
newLit(prcName))
)
)
# -> resultFuture.closure = iterator
outerProcBody.add(
newAssignment(
@ -266,10 +279,7 @@ proc asyncSingleProc(prc: NimNode): NimNode {.compileTime.} =
if baseTypeIsVoid:
if returnType.kind == nnkEmpty:
# Add Future[void]
prc.params2[0] =
newNimNode(nnkBracketExpr, prc)
.add(newIdentNode("Future"))
.add(newIdentNode("void"))
prc.params2[0] = futureVoidType
prc

View File

@ -5,8 +5,8 @@
# Licensed under either of
# Apache License, version 2.0, (LICENSE-APACHEv2)
# MIT license (LICENSE-MIT)
import std/[macros, strutils]
import unittest2
import macros
import ../chronos
{.used.}
@ -123,6 +123,76 @@ suite "Macro transformations test suite":
macroAsync2(testMacro2, seq, Opt, Result, OpenObject, cstring)
check waitFor(testMacro2()).len == 0
test "Future with generics":
proc gen(T: typedesc): Future[T] {.async.} =
proc testproc(): Future[T] {.async.} =
when T is void:
return
else:
return default(T)
await testproc()
waitFor gen(void)
check:
waitFor(gen(int)) == default(int)
test "Implicit return":
proc implicit(): Future[int] {.async.} =
42
proc implicit2(): Future[int] {.async.} =
block:
42
proc implicit3(): Future[int] {.async.} =
try:
parseInt("error")
except ValueError:
42
proc implicit4(v: bool): Future[int] {.async.} =
case v
of false: 5
of true: 42
proc implicit5(v: bool): Future[int] {.async.} =
if v: 42
else: 5
proc implicit6(v: ref int): Future[int] {.async.} =
try:
parseInt("error")
except ValueError:
42
finally:
v[] = 42
proc implicit7(v: bool): Future[int] {.async.} =
case v
of false: return 33
of true: 42
proc implicit8(v: bool): Future[int] {.async.} =
case v
of false: await implicit7(v)
of true: 42
let fin = new int
check:
waitFor(implicit()) == 42
waitFor(implicit2()) == 42
waitFor(implicit3()) == 42
waitFor(implicit4(true)) == 42
waitFor(implicit5(true)) == 42
waitFor(implicit5(false)) == 5
waitFor(implicit6(fin)) == 42
fin[] == 42
waitFor(implicit7(true)) == 42
waitFor(implicit7(false)) == 33
waitFor(implicit8(true)) == 42
waitFor(implicit8(false)) == 33
suite "Closure iterator's exception transformation issues":
test "Nested defer/finally not called on return":
# issue #288