5 ms·
Why do you need to transform to CPS? The answer to your question will depend very much on your answer to that question. In the extreme if you are not doing anyt
by jules 15y ago
Why do you need to transform to CPS? The answer to your question will depend very much on your answer to that question. In the extreme if you are not doing anything with the CPS form, then probably the best optimization of the CPSed form will completely undo the CPS, transforming it back into direct style. Of course if you are actually making use of the CPS form, that's not possible anymore.
Depending on why you want CPS, you can apply the following strategy:
Keep all code in direct style. For each function, designate a special return token CaptureCont that indicates that the continuation should be captured. The source of this token will be callcc. After each function call you check if it returned a token, and if so, extend it with the continuation for the rest of the computation. Then on the main() function of your program, pass the continuation to the appropriate place. This strategy avoids most overhead in the common case: one if instanceof check plus a branch per function call.
For example, take this function:
def foo(x,a):
y = g(x)
return y+a
Transform to:
class CaptureCont:
member target # the function that we will pass the continuation to once we finish constructing it
member partialCont # the part of the continuation captured so far
def extend(f):
newCC = new CaptureCont()
newCC.target = target
newCC.partialCont = lambda v: f(partialCont(v))
def invoke():
target(partialCont)
def foo(x):
y = g(x)
if y is CaptureCont:
# now y is the part of the continuation captured so far
# our job is to extend it with our stack frame
# and then return it so that the callee can build it up further
return y.extend(lambda y2: y2+a)
else:
return y+a
Instead of just invoking main(), do this:
loop:
v = main()
if v is CaptureCont: v.invoke()
else: exit(v) # well I guess this is not necessary in JS
Eventually the CaptureCont will bubble up to main, and at that point main will invoke it so that the continuation is passed to the right target.
Callcc:
def callcc(f):
cc = new CaptureCont()
cc.target = f
cc.partialCont = lambda x: x
return cc
There are various optimizations that can be applied (like eliding the capture code for functions that you statically know don't call callcc and thus will never return CaptureCont), and this approach also (trivially) works for the more general delimited continuations. As described capturing the continuation is O(n) in the size of the stack. You can make it amortized O(1) with some more trickery by lazily rebuilding the stack, but the current approach is already blazingly fast in the case of no continuation capture. I'm really tired now, so there are probably lots of errors in the above description, so tread with caution. I don't know if this approach is new or not, but it probably isn't given that it's a fairly obvious approach to capture the stack. Does anybody know what it's called? Then the OP can find a better description.
- jlongster 15y agoThat's a smart trick, and it's possible I might be able to do something like that. The reason I want to do CPS is for debugging in the browser. I want to be able to control the stack, so when a breakpoint hits I can pause everything and step through the code. The browser is non-blocking so I need explicit control of the stack. You're method is neat. My language doesn't support continuations, so I don't need them until I hit the debugging mode. You've made me curious if I can compile out both CPS-ed and non-CPS-ed versions of the code and somehow switch between them.
- gliese1337 15y agoI did a very similar thing with a python-implemented LISP interpreter, mainly so that I could implement a trampoline so I could do tail call optimization. It required CPSing the interpreter (and built-in functions), but not the interpreted program- this works because the continuation of the interpreted program is contained in the continuation of the interpreter that runs it. The interpreted "stack" ends up as a chain of Python closures. You might be able to get away with compiling a runtime environment slightly more complex than your simple trampoline loop that does the same thing, building a chain of JavaScript closures to represent continuations without actually CPSing the rest of the program. Continuation calls would then show up on the JavaScript stack and allow you to step through them.
- jlongster 15y agoWouldn't that be even slow though? Not only is it CPS code, but it's a program being interpreted by CPS code. I think I see what you're getting at though. Sounds neat; I definitely have a lot of new ideas for implementing CPS in a performant way now. Outlet is a compiler which compiles straight to js, no interpretation.
- gliese1337 15y agoNot sure; I haven't got anything to benchmark my implementation against. My suspicion is that it would be faster because you get chains of interpreted-continuations-implemented-as-interpreting-closures that call each other normally, rather than trampolining absolutely everything. But I would not be surprised if I turn out to be completely wrong about that. > Outlet is a compiler which compiles straight to js, no interpretation. Right; hence, you would have to include the implementation of the CPSed run time environment in the compiled output, just as you have to include the extra trampoline function when you do the current CPS transform.