- Notifications
You must be signed in to change notification settings - Fork17
Continuation-Passing Style for Nim 🔗
License
nim-works/cps
Folders and files
Name | Name | Last commit message | Last commit date | |
---|---|---|---|---|
Repository files navigation
It's acps
pragma you annotate a procedure declaration with in order toautomagically rewrite it as a type which holds 1) all the procedure's locals,and 2) a pointer to run the continuation.
You instantiate continuations by calling your function with arguments, or viatyped continuation callbacks. You may extend the Continuation type to addcustom data and functionality.
The macro provides comfort for automatic chaining, stack tracing, drying upfunction color, and handling exceptions. Hooks enable precisely-scoped controlof much of the machinery, including memory management.
Acps
proc macro enables enjoyment of the CPS abstraction for free, hiding allthe verbosity and spaghetti code paths of hand-written continuation passing. Ittakes your standard procedure and rewrites it at compile-time into a continuationform.
The macro performs only the control-flow rewrite. You may define your owncontinuation types. To manage them, you can choose from available dispatchersor customize your own.
As CPS is essentially a safe transformation of Nim to equivalent Nim, you canuse it anywhere you can use Nim,or more accurately, C, C++, or JavaScript.
The continuations produced by this macro aretiny -- as little as 32 byteseach -- and run at the speed of any native function call; they…
- compose efficient and idiomatic asynchronous code
- are over a thousand times lighter than threads
- are leak-free under Nim's modern memory management
- may be extended with custom inheritance-based types
- may be managed using custom dispatchers (executors)
- may be moved between threads to parallelize execution
- have no backend or runtime library requirements
- exploit no unsafe features of the language (
cast
,ptr
,addr
,emit
)
The project isn't dead; it simply hasn't needed much development. We have a fewsmall improvements we want to make before a 1.0 major release, but the generalconsensus is that the current implementation accomplishes our original goalsquite well.
Major future development will revolve around implementing CPS directly intheNimskull compiler.
A substantial effort to demystify this style of programming, and what it mayenable, livesin the docs/ subdirectory.
We also havea tutorial to help new users on their way to get starting withCPS.
For a description of the origins of our approach, seethe includedpapers andnim-lang/RFCs#295, where we write in more depth aboutwhy the implementation exists, goals for future development, etc.
The implementation is comprised of two moving parts:
- acontinuation is a bespoke type made to carry all locals in a procedure-- theenvironment -- plus a function pointer with which the continuationis poised to continue, orresume:
typeContinuation=refobject# all continuations inherit from this next:proc(c:Continuation):Continuation
- adispatcher is a procedure which resumes a continuation by invokingthe continuation's function pointer with the continuation itself as input.It follows that tosuspend, the function simply returns control to thedispatcher.
c= c.next(c)# we often replace the continuation with its result
Atrampoline is common form of dispatcher which resumes a continuation in aloop; it bounces control up to the continuation, which returns back down to thetrampoline when it's ready to suspend.
whiletrue:if c.isNil:# 'dismissed': a nil continuation resultbreakifnot c.next.isNil:# 'finished': a nil continuation functionbreak c= c.next(c)# resuming a 'running' (suspended) continuation
We use a procedure definition to define the continuation. The type we want tobase the continuation upon is supplied as the only argument to ourcps
pragmamacro. You can simply use theContinuation
type itself, if you prefer.
proczoom() {.cps:Continuation.}=## a very fast continuationdiscard
Calling the procedure (with arguments, if required) runs the continuation tocompletion.
zoom()echo"that was fast!"
You can extend theContinuation
type to carry state during the execution ofyour continuation.
typeCount=refobjectofContinuation labels:Table[string,ContinuationFn]
Here we've introduced a table that maps strings to a continuation "leg", orslice of control-flow that is executed as a unit.
Now we'll introduce two magical procedures for manipulating the table.
proclabel(c:Count; name:string):Count {.cpsMagic.}=## Record a label by `name` which we can later goto(). c.labels[name]= c.fnreturn c# control resumes in the continuationprocgoto(c:Count; name:string):Count {.cpsMagic.}=## Resume execution at the `name` label in the code. c.fn= c.labels[name]return c# control resumes in the continuation
These pragma'd procedures act as continuation legs and we can use them in ourcontinuations without supplying the initialCount
continuation argument.
Some people call this "colorless" syntax, since calls look the same whethermade inside or outside of a continuation.
proccount(upto:int):int {.cps:Count.}=## deploy the Count to make counting fun again;## this continuation returns the number of trips through the gotovar number=0 label:"again!"inc numberecho number,"!"echo number," loops, ah ah ah!"if number< upto:goto"again!"echo"whew!"return numberconst many=1_000_000_000assert 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, maybea bit later, after your nap. In that case, you can usewhelp
to instantiateyour continuation with arguments, without actually invoking it.
When you're ready, thetrampoline
will run your continuation to completionand bounce it back to you.
var later=whelpcount(1_000_000)sleep30*60*1000echo"it's later! time to count!"later=trampoline later
Continuations have a simplestate(c: Continuation): State
enum that is helpedintorunning()
,finished()
, anddismissed()
boolean predicates.
assert later.finished,"counting incomplete!"
Continuations can themselves be called in order to retrieve their result.
echo"i counted",later()," trips through the goto"
Such a call will run the continuation on your behalf if it has not already runto completion.
var later=whelpcount(1_000_000)echo"we counted",later()," trips through the goto"
$ nimble install https://github.com/nim-works/cps
examples/coroutine.nim(walkthrough): A simple coroutine implementation ofcoroutines on top of CPS communicating with each other.
Seethe documentation for the cps module as generated directly from the source.
At last count, there are no less than211 unique tests of the cpslibrary which confirm successful handling of myriad semantic formsand complex continuation composition. The tests are arguably the most valuablepiece of code in the project and, as per usual, serve as the most completedocumentation of the specification.
An example dispatcher with support for yields, I/O, and timers was included inthe past, but demonstrating dispatch conflated the roles ofcontinuation anddispatcher, confusing newcomers.
For a robust dispatcher implementation targeting broad OS support and modernasync features, take a look athttps://github.com/alaviss/nim-sys.
A small collection of examples provides good demonstration of multiple patternsof CPS composition. Each example runs independently, with no other requirements,yet demonstrates different exploits ofcps
.
Example | Description |
---|---|
Channels | A channel connects sender and receiver continuations |
Goto | Implementation oflabel andgoto statements using CPS |
Iterator | A simple demonstration of a CPS-based iterator |
Coroutines | A pair of continuations communicate as coroutines.Walkthrough. |
Lazy | Lazy streams are composed by continuations in a functional style |
Pipes | Coroutines compose streams which connect arbitrarily |
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 |
Here are a few projects which demonstrate CPS library integration.
Project | Description |
---|---|
WebServer | Zevv's "Real World Test" WebServer And More |
Background | Run any function on a background thread |
Passenger | Compose graph visualizations of CPS control-flow |
HttpLeast | A simple web-server for benchmarking CPS |
solo5_dispatcher | A simple CPS scheduler for Solo5 unikernels |
Project | Description |
---|---|
Balls | A threaded test runner based upon CPS |
Sys | Next-generation operating system service abstractions |
Actors | Zevv's experimental project to create a threaded, share-nothing actor based framework on top of CPS |
InsideOut | Another experimental concurrency library which supports CPS over threads |
Taps | A portable, asynchronous, network abstraction library |
Syndicate | The Syndicated Actor Model for Nim |
Seethis list of open Nim issues surfaced by CPSdevelopment; some repercussions include the following:
Exceptions are evaluated differently under
panics:on
andpanics:off
, soyou may need to usepanics:on
in order to produce reliably correct code.Expressions are evaluated differently under
gc:[ao]rc
, so you may need touse those memory managers in order to produce reliably correct code.The
cpp
backend often doesn't work, particularly due to faulty codegen butalso, perhaps, due toexceptions:goto
assumptions that we rely upon.var
/openArray
/varargs
parameters to procedures with thecps
pragmaare not supported.Nim's
for
loops work, but you cannot perform any CPS control-flow inside ofthem; if in doubt, use awhile
loop instead.
Use--define:cpsNoCallOperator
to explicitly disable the()
operator.
If you are not running withdefine:danger
andgc:arc
andpanics:on
thenperformance considerations really aren't your primary consideration, right?
There are two tracing systems that are enabled by default via Nim's built-instackTrace:on
compiler option, which defaults toon
outside ofrelease
anddanger
builds.
Stack semantics are implemented by a singleTraceFrame
object stored on eachcontinuation and these serve to capture the progress of control-flow throughmultiple levels of CPS calls just as they do so for normal stack-based code.
TherenderStackFrames()
andwriteStackFrames()
procedures return a sequenceof strings or write them to the standard message stream, respectively. You canrun these procedures without arguments in any continuation.
You can extend theStack
hook to alter its behavior.
Force-enable the stack frame support with--define:cpsStackFrames=on
.
The trace deque system stores the lastN hooks executed by the continuation,whereN defaults to4096
(see below). A single continuation has multiplehooks executed during its lifecycle, so these traces can be very detailed.
TherenderTraceDeque()
andwriteTraceDeque()
procedures return a sequenceof strings or write them to the standard message stream, respectively. You canrun these procedures without arguments in any continuation.
You can extend theTrace
hook to alter its behavior.
Force-enable the trace deque support with--define:cpsTraceDeque=on
andalter the size of the deque with e.g.--define:traceDequeSize=100
.
Add--define:cpsDebug=SomePass
whereSomePass
matches one of the CPStransformation passes; this will output Nim codegen corresponding to therewrite phase. Interesting places to start include the following:
cpsTransform
cpsResolver
cpsJump
cpsContinuationJump
cpsManageException
cpsTryFinally
- etc.
Note that only one--define:cpsDebug=...
can be enabled at a time - multiple defines will use only the final one.
Implementtrace
and it will be called at each continuation leg;see the documentation for details.
Use--define:cpsNoTrace
to disable thetrace
code generation.
MIT
About
Continuation-Passing Style for Nim 🔗