-
Notifications
You must be signed in to change notification settings - Fork 39
add delimited continuations #1530
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: devel
Are you sure you want to change the base?
Conversation
It's a bit too magic, and dedicated abstract (and concrete) syntax would be probably be better in the longer term. Nonetheless, it's possible to make due with a magic call for now.
Most of the rules in the manual are not yet enforced.
The call expression is invoked as if it were the last expression in the body, so a tail call is possible.
They are the MIR operators `suspend` will be implemented with.
It's easiest to lower `suspend` into `fork` + `land` directly in `mirgen`, which also means that the proper control flow graph is available right away.
It also acts as a test for the compiler.
@@ -410,6 +410,100 @@ proc semPrivateAccess(c: PContext, n: PNode): PNode = | |||
c.currentScope.allowPrivateAccess.add t.sym | |||
result = newNodeIT(nkEmpty, n.info, getSysType(c.graph, n.info, tyVoid)) | |||
|
|||
proc semSuspend(c: PContext, n: PNode, s: PSym, flags: TExprFlags): PNode = | |||
## Analyzes a 'suspend' magic call, producing a typed AST or an error. If | |||
## the call doesn't have the right shape, analysis fall back to overload |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
## the call doesn't have the right shape, analysis fall back to overload | |
## the call doesn't have the right shape, analysis falls back to overload |
Possible typo.
|
||
c.openScope() | ||
# create a let section and type that. This makes sure the symbol is properly | ||
# registered everywhere, and retyping is also taken care. The initializer |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
# registered everywhere, and retyping is also taken care. The initializer | |
# registered everywhere, and retyping is also taken care of. The initializer |
Minor typo.
7. let `ctx` refer to the saved context (a value of type `Ctx`) | ||
8. let `call` refer to the third argument expression | ||
|
||
`call` is typed as if would the expression were the following: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
`call` is typed as if would the expression were the following: | |
`call` is typed as if the expression were the following: |
|
||
If `ctx` (or a copy thereof) goes out scope without having been consumed by a | ||
call to the continuation, cleanup happens as if unwinding would take place | ||
right the `suspend` call, but without `finally`:idx: clauses being visited. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
right the `suspend` call, but without `finally`:idx: clauses being visited. | |
right after the `suspend` call, but without `finally`:idx: clauses being visited. |
I think that's what was meant?
@@ -3070,6 +3070,12 @@ when defined(nimDebugUtils): | |||
proc `not`*[T: ref or ptr](a: typedesc[T], b: typeof(nil)): typedesc {.magic: "TypeTrait", noSideEffect.} | |||
## Constructs a `not nil` type. | |||
|
|||
when defined(nimskullHasSuspend): | |||
proc suspend*[T](with: typedesc[T], name, call: untyped): T {.magic: "Suspend", noSideEffect.} = |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As of this moment, reading the PR, reminding myself or redex vs continuation definitions (Lexi's talk), and seeing as suspend
creates a reified continuation, I'm inclined to say continuation
is a better name than suspend
.
Beyond, continuation is largely what would follow in a continuation(...): ...
call, I have a feeling that suspend
is a bit too evocative of something like async/await or coroutines.
The above is my current thing, and undoubtedly as I reread this PR as it evolves, and especially the docs, I'll end up with a different suggestion(s) and preference. I would just treat this as a data point.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Here's an alternative interface I made to look less magical:
proc suspend[T; Prc: proc](with: typedesc[T], name, prc: Prc, args: tupleFromArgs(Prc)): T
where tupleFromArgs()
is a "magic" that convert the argument list into a tuple type
Re: the name suspend
, I think it's a good one, as when viewed from the writer's PoV, you "stop" the procedure at the specified point, and then resume it elsewhere. The fact that it creates a continuation is merely an implementation detail so you can do the resuming part.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I like the generality of tupleFromArgs
, it'd be handy for template/macro writing. I do think writing suspend
directly will be less pleasant. Perhaps overloading can help there, with something like a typed macro extracting the caller from a call.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I agree that having tupleFromArgs
(though I'd name it differently) would be useful. It's possible to implement via a macro, but this is error prone and effectively requires using the unstable macro type API.
Here's an alternative interface I made to look less magical:
proc suspend[T; Prc: proc](with: typedesc[T], name, prc: Prc, args: tupleFromArgs(Prc)): T
As is, this won't work, for multiple reasons:
name
beinguntyped
was deliberate, it's not a procedure: the parameter accepts an untyped identifier. However, changingname
tountyped
in your signature also won't work- proper parameter passing cannot be emulated with a tuple. Of course, one could require the argument being an immediate tuple construction and then construct a call from it, but this a lot more "magical" - for both the compiler and user - than the current interface
- using overloaded routines as the receiver (i.e.
prc
) is not possible
There's also another internally-facing problem: there's no call of prc
in the typed AST, which means that multiple places in the compiler would need special handling for suspend
(which the current interface gets around).
Finally, keep in mind that suspend
is a non-overloadable magic at the moment, and with its current shape, it cannot be anything else. In other words, the signature you see in system is largely irrelevant to the compiler -- it's only there for documentation purposes.
Ultimately, I believe that dedicated abstract (and concrete) syntax would be a better fit for this feature, though I'm rather reluctant to take that route.
Evaluation of `suspend`:idx: works as follows: | ||
1. the current local context is saved | ||
2. `local` is intialized with the tuple `(ctx, prc)` | ||
3. all local variables (except `local`) or sink parameters of the caller used |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
3. all local variables (except `local`) or sink parameters of the caller used | |
3. all local variables (except `local`) and sink parameters of the caller used |
I believe this was the intention
Hm, there's one thing I didn't consider: address stability. The address of a local doesn't stay the same across a proc test() =
var a = 0
var b = addr a
suspend ...
echo b[] is going to invoked undefined behaviour upon evaluation The only solution to this I can think of is disallowing taking the address of a location where the address value might be live beyond a proc test() =
var a = 0
block:
var b = 0
discard addr(b) # allowed
discard addr(a) # disallowed
suspend ...
discard addr(a) # allowed, because there's no following `suspend` Of course, this rule won't be able to reject code such as: proc hiddenAddr(x: var int): ptr int =
addr x
proc test() =
var a = 0
var b = hiddenAddr(a)
suspend ...
echo b[] Finally, implementing this rule is going to be a nuisance: either |
Summary
Add the
suspend
magic operator, which creates a reified continuation (a procedure + context object) out of the remainder of a routine.Details
suspend
is the best interface I could come up with so far, but I don't particularly like it and it doesn't have to be final.The internal primitives that the language-level interface (currently
suspend
) is implemented with arefork
andland
:land
marks a resumption pointfork
saves the current local context and returns it as anobject
together with the reified continuation for the forked-to resumption pointEach
fork
must be paired with exactly one distinctland
, and the latter must not be reachable from the former.fork
andland
use a backend-agnostic MIR lowering and are thus automatically supported by all MIR- and CGIR-based code generators (i.e., all current backends).To-Do
fork
/land
lowering pass.tailcall
lowering a MIR pass (so that it can happen afterfork
/land
lowering)land
toresume
(the existingmnkResume
needs to be renamed tomnkUnwind
)suspend
as described in the manualsuspend