Safe Serialization Under Mutual Suspicion/"Reversing" Evaluation
From Erights
As we've seen, we make serializers, unserializers, and other transformers like expression simplifiers by composing a recognizer with a builder. The interface between the two is the DEBuilder API, explained in Appendix A: The Data-E Manual. Since most of the API is a straightforward reflection of the Data-E grammar productions, if you wish, you may safely skip these details and proceed here by example.
Evaluating Data-E
The semantics of Data-E are defined by the semantics of its evaluation as an E program. We could unserialize using the full E evaluator. However, this is inefficient both as an implementation and as an explanation. Instead, here is the Data-E evaluator as a builder, implementing exactly this subset of E's semantics.
pragma.syntax("0.8") def deSubgraphKit { to makeBuilder(scope) :near { # The index of the next temp variable var nextTemp := 0 # The frame of temp variables def temps := [].diverge() # The type returned by "internal" productions and passed as arguments to represent # built subtrees. def Node := any # The type returned by the builder as a whole. def Root := any # DEBuilderOf is a parameterized type constructor. def deSubgraphBuilder implements DEBuilderOf(Node, Root) { to getNodeType() :near { Node } to getRootType() :near { Root } /** Called at the end with the reconstructed root to obtain the value to return. */ to buildRoot(root :Node) :Root { root } /** A literal evaluates to its value. */ to buildLiteral(value) :Node { value } /** A free variable's name is looked up in the scope. */ to buildImport(varName :String) :Node { scope[varName] } /** A temporary variable's index is looked up in the temps frame. */ to buildIbid(tempIndex :int) :Node { temps[tempIndex] } /** Perform the described call. */ to buildCall(rec :Node, verb :String, args :Node[]) :Node { # E.call(..) is E's reflective invocation construct. For example, E.call(2, "add", [3]) # performs the same call as 2.add(3). <u>E.call(rec, verb, args)</u> } /** * Called prior to building the right-hand side of a defexpr, to allocate and bind the * next two temp variables to a promise and its resolver. * * @return the index of the temp holding the promise. The temp holding the * resolver is understood to be this plus one. */ to buildPromise() :int { def promIndex := nextTemp nextTemp += 2 def [prom,res] := Ref.promise() temps[promIndex] := prom temps[promIndex+1] := res promIndex } /** * Called once the right-hand side of a defexpr is built, use the resolver to resolve * the value of the promise. * * @return the value of the right-hand side. */ to buildDefrec(resIndex :int, rValue :Node) :Node { temps[resIndex].resolve(rValue) rValue } # ... buildDefine is an optimization of buildDefrec for known non-cyclic cases. } } # ... other useful tools }
As we see, the E.call(..) underlined above is where all the object construction is done. All the rest is plumbing to hook the up the references among these objects.