This project provides a cps
pragma which you can add to a procedure to
automatically rewrite it to use continuations for control-flow. This provides
the benefits of CPS while abstracting away the verbosity of continuations.
The cps
pragma performs only the control-flow rewrite; you implement or
import a dispatcher to define both the type and behavior of your continuations,
so there is virtually no API and no limitations on composition.
A substantial effort to demystify this style of programming, and what it may enable, lives in the docs/ subdirectory.
We also have a tutorial to help new users on their way to get starting with CPS.
For a description of the origins of our approach, see the included papers and nim-lang/RFCs#295, where we write in more depth about why the implementation exists, goals for future development, etc.
The continuations produced by this macro...
- compose efficient and idiomatic asynchronous code
- are over a thousand times lighter than threads
- are leak-free under Nim's ARC/ORC memory management
- may be based upon your own custom
ref object
type - may be dispatched using your own custom dispatcher
- may be moved between threads to parallelize execution
- are faster and lighter than async/await futures
- are 5-15% faster than native closure iterators
- exploit no unsafe features of the language (
cast
,ptr
,addr
,emit
)
The macro itself should be considered beta quality. Corner-cases are being nailed down and the API is being adjusted as demonstration applications are built and rough edges are identified.
Yes. We need more people building toys and small projects and giving feedback on what works well and what doesn't.
The Continuation
type may be changed into a generic soon. If that doesn't
scare you, then you are as safe to use CPS in larger projects as the quality of
your tests; the API won't break you badly.
The implementation is comprised of two concepts:
- an environment is a bespoke type made to carry all locals in a procedure,
plus a pointer
fn
to a continuation leg, and amom
pointer to any parent continuation:
type
Continuation* = ref object of RootObj
fn*: proc(c: Continuation): Continuation {.nimcall.}
mom*: Continuation
- a trampoline is a while loop that looks like this:
result = input_continuation
while result != nil and result.fn != nil:
result = result.fn(result)
We call the instantiated environment with its fn
pointer a continuation,
and we call anything that invokes a continuation's fn
pointer a dispatcher.
The cps
macro is applied to procedure definitions, and it takes a single
argument: the type you wish your continuations to inherit from. This
parent type must be a ref object of Continuation
. You can simply use the
Continuation
type itself, if you prefer.
type
Whoosh = ref object of Continuation
We use a procedure definition to define the continuation. The type we want to
base the continuation upon is supplied as the only argument to our cps
pragma
macro.
type
Whoosh = ref object of Continuation
proc zoom() {.cps: Whoosh.} =
## a very fast continuation
discard
Calling the procedure (with arguments, as required) runs the continuation to completion.
type
Whoosh = ref object of Continuation
proc zoom() {.cps: Whoosh.} =
## a very fast continuation
discard
zoom()
echo "that was fast!"
You can extend the Continuation
type to carry state during the execution of
your continuation.
type
Count = ref object of Continuation
labels: Table[string, Continuation.fn]
Here we've introduced a table that maps strings to a continuation "leg", or slice of control-flow that is executed as a unit.
Now we'll introduce two magical procedures for manipulating the table.
proc label(c: Count; name: string): Count {.cpsMagic.} =
c.labels[name] = c.fn
result = c
proc goto(c: Count; name: string): Count {.cpsMagic.} =
c.fn = c.labels[name]
result = c
These pragma'd procedures act as continuation legs and we can use them in our
continuations without supplying the initial Count
argument.
proc count(upto: int): int {.cps: Count.} =
## deploy the Count to make counting fun again;
## this continuation returns the number of trips through the goto
var number = 0
label: "again!"
inc number
echo number, "!"
echo number, " loops, ah ah ah!"
if number < upto:
goto "again!"
echo "whew!"
return number
const many = 1_000_000_000
assert many + 1 == count(many) # (this might take awhile to finish)
Sometimes you don't want to do a lot of counting right away, but, y'know, maybe
a bit later, after your nap. In that case, you can use whelp
to instantiate
your continuation with arguments, without actually running it.
var later = whelp count(1_000_000)
echo "nap time!"
sleep 30*60*1000
When you're ready, the trampoline
will run your continuation to completion
and bounce it back to you.
var later = whelp count(1_000_000)
sleep 30*60*1000
echo "it's later! time to count!"
later = trampoline later
Continuations have a simple state(c: Continuation): State
enum that is helped
into running()
, finished()
, and dismissed()
boolean predicates.
var later = whelp count(1_000_000)
sleep 30*60*1000
echo "it's later! time to count!"
later = trampoline later
assert later.finished, "laws of physics lost their sway"
Continuations can themselves be called in order to retrieve their return value.
var later = whelp count(1_000_000)
sleep 30*60*1000
echo "it's later! time to count!"
later = trampoline later
assert later.finished, "laws of physics lost their sway"
echo "i counted ", later(), " trips through the goto"
In fact, such a call will run the continuation on your behalf, as well.
var later = whelp count(1_000_000)
sleep 30*60*1000
echo "it's later! time to count!"
echo "i counted ", later(), " trips through the goto"
We'll talk about voodoo here and walk through the coroutine demo, since it pulls together prior concepts and adds voodoo and multiple continuations.
An example dispatcher was included in the past, but demonstrating dispatch
conflated the purpose of the cps
macro and made misconceptions about the role
of continuation-versus-dispatcher common. The reference dispatcher can now be
found at https://github.com/disruptek/eventqueue and you can also jump directly
to the documentation.
- https://github.com/alaviss/nim-sys -- next generation OS services
- https://github.com/disruptek/passenger -- create a graph of continuation runtime
- https://github.com/disruptek/supervisors -- simple dispatch patterns for composition
See the documentation for the cps module as generated directly from the source.
A small collection of examples provides good demonstration of multiple patterns
of CPS composition. Each example runs independently, with no other requirements,
yet demonstrates different exploits of cps
.
Example | Description |
---|---|
Channels | A channel connects sender and receiver continuations |
Goto | Implementation of label and goto statements using CPS |
Iterator | A simple demonstration of a CPS-based iterator |
Coroutines | A pair of continuations communicate as coroutines |
Lazy | Lazy streams are composed by continuations in a functional style |
TryCatch | Exception handling is reimplemented using only CPS |
CpsCps | Continuations can efficiently call other continuations |
Work | Implementation of a simple continuation scheduler |
LuaCoroutines | Coroutines implemented in the style of Lua |
ThreadPool | 1,000,000 continuations run across all your CPU cores |
WebServer | Zevv's "Real World Test" WebServer And More |
See this list of open Nim issues surfaced by CPS development; some repercussions include the following:
-
Exceptions are evaluated differently under
panics:on
andpanics:off
, so you may need to usepanics:on
in order to produce correct code. -
Expressions are evaluated differently under
gc:[ao]rc
, so you may need to use those memory managers in order to produce correct code. -
The
cpp
backend often doesn't work, particularly due to faulty codegen but also, perhaps, due toexceptions:goto
assumptions that we rely upon. -
var
/openArray
/varargs
parameters to procedures with thecps
pragma are not supported. -
Nim's
for
loops work, but you cannot perform any CPS control-flow inside of them; if in doubt, use awhile
loop instead. -
Generic continuations such as the following won't work without changes to the compiler.
type
MyContinuation[T] = ref object of Continuation
something: T
If you are not running with define:danger
and gc:arc
and panics:on
then
performance considerations really aren't your primary consideration, right?
There are two tracing systems that are enabled by default via Nim's built-in
stackTrace:on
compiler option, which defaults to on
outside of release
and danger
builds.
Stack semantics are implemented by a single TraceFrame
object stored on each
continuation and these serve to capture the progress of control-flow through
multiple levels of CPS calls just as they do so for normal stack-based code.
The renderStackFrames()
and writeStackFrames()
procedures return a sequence
of strings or write them to the standard message stream, respectively. You can
run these procedures without arguments in any continuation.
You can extend the Stack
hook to alter its behavior.
Force-enable the stack frame support with --define:cpsStackFrames=on
.
The trace deque system stores the last N hooks executed by the continuation,
where N defaults to 4096
(see below). A single continuation has multiple
hooks executed during its lifecycle, so these traces can be very detailed.
The renderTraceDeque()
and writeTraceDeque()
procedures return a sequence
of strings or write them to the standard message stream, respectively. You can
run these procedures without arguments in any continuation.
You can extend the Trace
hook to alter its behavior.
Force-enable the trace deque support with --define:cpsTraceDeque=on
and
alter the size of the deque with e.g. --define:traceDequeSize=100
.
Add --define:cpsDebug=SomePass
where SomePass
matches one of the CPS
transformation passes; this will output Nim codegen corresponding to the
rewrite phase. Interesting places to start include the following:
cpsTransform
cpsResolver
cpsJump
cpsContinuationJump
cpsManageException
cpsTryFinally
- etc.
Implement trace
and it will be called at each continuation leg; see the documentation for details.
MIT