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