MLton not quite safe-for-space

Stephen Weeks MLton@sourcelight.com
Wed, 15 Nov 2000 16:00:36 -0800 (PST)


> Currently  MLton is not quite safe-for-space.  The problem case is a non-tail
> function call where the continuation `obviously' never returns.  I.e.,
> 
>     fun f depth =
>            if depth = 0
>               then ()
>               else (f (depth - 1);
>                     let fun spin () = spin ()
>                     in spin ()
>                     end)
> 
> The point is that in a compile  using  full  CPS,  I  would  claim  that  the
> continuation  passed  to  f  in  the  recursive  calls  would NOT include the
> continuation passed in to the outer f.  I.e., the CPS version of f would be
> 
>     fun f (depth, k) =
>            if depth = 0
>               then k ()
>               else let fun k' _ =
>                               let fun spin () = spin ()
>                               in spin ()
>                               end
>                    in f (depth - 1, k')
>                    end
> 
> Note that k' is NOT closed over k.  (This is NOT an optimization.  It is  the
> fact  that  the  function spin never returns and hence is NOT closed over its
> continuation.)

I think you CPS converted incorrectly.  Here is the correct CPS conversion of
the program.

fun f (depth, k) =
   if depth = 0
      then k ()
   else f (depth - 1,
	   fn _ =>
	   let fun spin k = spin k
	   in spin k
	   end)

The point is that you did not CPS convert spin.  The continuation passed to f is 
indeed closed over k.  You performed a useless variable optimization and
eliminated spin's argument.  It is correct, but not required.

> Thus a safe-for-space implementation must execute f in  constant  space,  but
> MLton  requires  space  proportional to argument to f.  Fixing this (if it is
> worth fixing) is slightly non-trivial because of exceptions.   On  the  other
> hand,  exceptions  also  are  an  argument that it IS worth fixing because it
> isn't  only  non-termination  that  can  cause  you  to  not  refer  to  your
> continuation.  I.e., consider
> 
>     fun g depth =
>            if depth = 0
>               then ()
>               else (g (depth - 1);
>                     raise ???)
> 
> Again,  the  continuation  to  the recursive call to g is NOT closed over the
> continuation passed in to the outer g.

I find this example more plausible.  Suppose that CPS conversion in the presence 
of exceptions adds two arguments to every function, one continuation to be
called in the case of normal returns and one continuation to be called in the
case of exceptional returns.  Then, the CPS conversion of g looks like:

fun g (depth, k, k') =
   if depth = 0
      then k ()
   else g (depth - 1,
	   fn _ => k' ???,
	   k')

So, both the normal and exception continuations don't refer to k, and g should
execute in constant space. 

> I think a not too bad fix for this would be to simply mark a  frame  (in  the
> live-variables  thing)  as indicating that it will never return.  Then the GC
> would realize that this means that  all  frames  below  it  up  to  the  next
> exception  handler  are garbage.  The advantage here is that it has no effect
> on any thing except the GC.
> 
> Is this too horrible to contemplate?

You hit the nail right on the head in that the problem is that there is no way
for a frame to refer to just the exception continuation.  I think your solution
isn't too costly.  It would certainly be possible to add an extra bit in the
frame info table for each frame.  The analysis to determine which continuations
never return in the CPS IL is dead simple and linear time.

I would prefer to have a solution that expressed the information in the CPS IL,
but I can't see one right now.

Anyways, it's on the todo list, but very low, unless it's really biting you.

Hmmm, now that I think about it, the information about what frames never return
is known at compile time, so you could emit code just before such frames are
pushed that slides the frame down to just above the toplevel handler.  Then the
gc wouldn't have to get involved at all.  I guess this optimization would
address your first example as well.