Skip to content
Sébastien Doeraene edited this page Mar 15, 2012 · 8 revisions

Manipulating Nodes, aka VM objects

This page describes the main classes involved in the Object Model, which are nodes.

Nodes

A VM object is represented by a node. There are two types of nodes: StableNode and UnstableNode, representing respectively stable and unstable objects. Internally, both these classes have a single field node of type Node, but this type is private to the object model implementation. These classes are located in the source file store-decl.hh.

A node has a type and a value. The type is a const Type* and is accessible through the method type() of nodes. The value, however, is inaccessible.

Unstable nodes can be created using the method make<T>(), and can be copied from other nodes using the overloads of the method copy(). Stable nodes can only be initialized once out of another node.

For example, the following code creates an unstable node containing an integer, uses it to initialize a stable node, then prints the type of this node:

UnstableNode unstable;
unstable.make<SmallInt>(vm, 5);
StableNode stable;
stable.init(vm, unstable);
cout << stable.type() << endl;

Types

The class Type is defined in file type.hh. It contains information about a type. Two important flags are isCopiable() and isTransient().

A type is copiable if, when copying a node of this type, it is sufficient to copy the two words of the node. A SmallInt is copiable, e.g. Big types and transients cannot be copiable. E.g., Tuple is not copiable, nor is Unbound.

A type is transient if it is meant to be mutated later. The most important example of transients are Unbound variables.

References

The type Reference is somewhat special, and used to encode aliasing. Its value is a StableNode* which is the node it points to. Because of this, the C++ type system will prevent us from making references to unstable nodes.

We illustrate this with the encoding of the following Oz code:

local A B in
   A = B
end

It can be written in C++, with our model, as:

UnstableNode A, B;
StableNode stable;
A.make<Unbound>(vm);
stable.init(vm, A);
B.make<Reference>(vm, &stable);

This code creates an unbound variable in A, then initializes a stable node with that unbound variable. At that point, because Unbound is a non-copiable type, the unbound variable is acquired by the stable node, and the unstable node A is automatically turned into a reference to that node. After that, we create explicitly another unstable node B which a Reference to the unbound.

Actually, because the copy() methods of UnstableNode take care of copiable and non-copiable types, this can be simplified as:

UnstableNode A, B;
A.make<Unbound>(vm);
B.copy(vm, A);

The call to copy() from an UnstableNode into an UnstableNode, of a value of a non-copiable type, will automatically eject that value into a newly allocated StableNode, and turn both A and B into Reference's to that stable node.

Rich nodes

With StableNode and UnstableNode, it is possible to create nodes in the store, copy them, and read their types. But you cannot do anything useful with a node without getting its value.

Actually, it is never possible to access directly the value of a node. However, the class RichNode provides a way to call methods on the value of a node. Actually, it calls (through several layers of indirection) methods of the class Implementation<T> corresponding to the type.

For example, the following code creates an unstable node with an integer. Then, it uses a RichNode to call the method value() inside class Implementation<SmallInt>. The layers of indirection make sure the method is called in the context of the actual value stored in the node, but you cannot see that.

UnstableNode node;
node.make<SmallInt>(vm, 5);
RichNode richNode = node;
cout << richNode.as<SmallInt>().value() << endl;

Note that the as<T>() has the following precondition: richNode.type() == T::type(). It is invalid (bug) to call this method with the wrong static type parameter.

Attention! In this "tutorial", we are using RichNode::as<T>() quite a lot. However, in real code, using this method is prohibited by default (some rare cases need it). Indeed, to use this method, it is necessary to know the exact data type behind a node. Now, it is generally impossible to know that, because several data types implement the same set of methods. Instead, we will use interfaces in real code.

In addition to providing an interface to actual values, rich nodes take care of following chains of Reference's. Since rich nodes are the only interface to actual values, you need never worry about references!

As an illustration, consider the following Oz code:

local A B in
   B = A
   A = 5
   {Show B}
end

This is translated into C++ as:

UnstableNode A, B;
A.make<Unbound>(vm);
B.copy(vm, A); // eject the Unbound into a StableNode
UnstableNode five;
five.make<SmallInt>(vm, 5);
RichNode richA = A; // follows the references
assert(richA.type() == Unbound::type());
richA.as<Unbound>().bind(vm, five); // implicit conversion of five to a RichNode
RichNode richB = B;
cout << richB.type() << endl; // displays "SmallInt"
cout << richB.as<SmallInt>().value() << endl; // displays "5"
Clone this wiki locally