serialization and globalization

Stephen Weeks sweeks@wasabi.epr.com
Mon, 16 Aug 1999 16:33:55 -0700 (PDT)


> We  certainly  had  this kind of problem with Kali.  I.e., you send something
> closed over a mutable object and the receiver gets  a  copy  of  the  mutable
> object,  which  is  not quite what they want.  One solution in the Kali world
> was the handle, but I don't think you want that in MLton.

But what is going on here is even more confusing.  The mutable object
is, in effect, not being sent at all, solely because of some
optimization deep in the bowels of the Cps optimizer.

> I don't quite see why you'd consider this example a bug.  Since a globalizable
> mutable object is only created once, I don't see how deserializing a piece of
> state that is closed over this object effects the program's semantics.  

Because what you deserialize doesn't end up using the value of the mutable 
object stored in the serialized bits; instead, it refers to the globalized
mutable object.

>In other
> words, can you give a context in which (de)serialization allows you to tell the
> difference between a program containing a mutable object recorded in some state
> vector, and the corresponding program reflecting the globalization of the object?
> 

Following is a program that demonstrates what I think is wrong.  In
the code f and g are essentially the same function, except that g is
carefully constructed so that the optimizer can't globalize the ref
cell r.

--------------------------------------------------------------------------------

(* z.sml *)

open MLton OS.FileSys TextIO

fun p(s, r) = print(concat[s, " ", Int.toString(!r), "\n"])

fun nontail(f) =
   let
      fun loop n =
	 if n = 0
	    then (0, f())
	 else let val (m, z) = loop(n - 1)
	      in (m + 1, z)
	      end
   in #2(loop 13)
   end

val f = let val r = ref 13
	in fn () => (r := !r + 1; p("global", r))
	end

val g = nontail(fn () => let val r = ref 13
			 in fn () => (r := !r + 1; p("local", r))
			 end)
   
val file = "/tmp/z.bug"

type bits = (unit -> unit) * (unit -> unit)
   
fun restore() =
   let
      val ins = openIn file
      fun loop() =
	 case input1 ins of
	    NONE => []
	  | SOME c => c :: loop()
      val s = implode(loop())
   in closeIn ins
      ; (deserialize(Byte.stringToBytes(s))): bits
   end

fun save(f, g) =
   let val out = openOut file
   in output(out, Byte.bytesToString(serialize((f, g): bits)))
      ; closeOut out
   end

val (f, g) =
   (if access(file, [A_READ]) then () else save(f, g)
    ; restore())
   
val _ = f()
val _ = g()
val _ = save(f, g)

--------------------------------------------------------------------------------

If we now compile and run this program, we see that f and g have
different behavior.

/tmp% make
/home/sweeks/mlton/bin/mlton  z.sml
rm -f z.bug
/tmp% z
global 14
local 14
/tmp% z
global 14
local 15
/tmp% z
global 14
local 16

I don't see how to give a sensible semantics that explains this
behavior.  In particular, I point out that if I merely turn off the
constant propagation pass (which does globalization as well) of the
Cps optimizer, then the program will have different behavior, namely

/tmp% z
global 14
local 14
/tmp% z
global 15
local 15
/tmp% z
global 16
local 16