-
Notifications
You must be signed in to change notification settings - Fork 96
Manipulating Nodes
This page describes the main classes involved in the Object Model, which are nodes.
A VM object is represented by a node. There are two types of nodes: StableNode
and UnstableNode
, representing respectively stable and unstable objects. Both these classes inherit from the base class Node
, but you should never use directly this class (anyway, everything is private in Node
). These classes are located in the source file store-decl.hh
.
A node has a type of type Type
and a value of type MemWord
. Type
is a small struct that (currently) holds a const TypeInfo*
, i.e., a pointer to the RTTI of the type. MemWord
is a union of a bunch of one memory word-types. The type of this union is dependent on the actual type of the node. However, these are not accessible. Both StableNode
and UnstableNode
are "write-only" data structures. To access their content, they must be converted into a RichNode
first (see later).
Unstable nodes can be created using the static method T::build()
(for a given data type 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, using the method init()
.
For example, the following code creates an unstable node containing an integer, then copies it into another unstable node:
UnstableNode A, B;
A = SmallInt::build(vm, 5);
B.copy(vm, A);
As already mentioned, Type
is a struct holding a const TypeInfo*
. The class TypeInfo
is the base class for all classes containing the RunTime Type Information (aka RTTI) of data types. Type
is defined in type-decl.hh
, and TypeInfo
in typeinfo-decl.hh
.
Two important flags in the RTTI are isCopyable()
and isTransient()
.
A type is copyable if, when copying a node of this type, it is sufficient to copy the two words of the node. E.g, a SmallInt
is copyable. Big types, transients, and other types requiring a node-based identity cannot be copyable. E.g., Tuple
is not copyable, nor is Variable
.
A type is transient if it is meant to be mutated later. The most important example of transients are Variable
's.
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 = new (vm) StableNode;
stable->init(vm, OptVar::build(vm));
A = Reference::build(vm, stable);
B = Reference::build(vm, stable);
This code first allocates a new StableNode
in the VM memory space (subject to garbage-collection). UnstableNode
's can be declared statically and allocated on the stack, because it is impossible to create a reference to them. However, StableNode
's must always be allocated in the VM memory space.
After having allocated the StableNode
, we initialize it with an (optimized) unbound variable. Afterwards, we create explicitly two Reference
's to stable
. Hence, A
and B
effectively refer to the same OptVar
variable.
Actually, because the copy()
methods of UnstableNode
take care of copyable and non-copyable types, as well as ensuring that Reference
's point to stable nodes only, this can be simplified as:
UnstableNode A, B;
A = OptVar::build(vm);
B.copy(vm, A);
The call to copy()
from an UnstableNode
into an UnstableNode
, of a value of a non-copyable type, will automatically eject that value into a newly allocated StableNode
, and turn both A
and B
into Reference
's to that stable node.
Because of this, it is very rare that you need to worry about allocating a StableNode
or building Reference
's yourself. It gets done automatically.
With StableNode
and UnstableNode
, it is possible to create nodes in the store and copy them. But you cannot do anything useful with a node without getting its value or its type. The class RichNode
provides a way to read the type of a node, and call methods on its value. Actually, it calls (through several layers of indirection) methods of the data type class T
(e.g., SmallInt
).
For example, the following code creates an unstable node with an integer. Then, it uses a RichNode
to call the method value()
inside class 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 = SmallInt::build(vm, 5);
RichNode richNode = node;
assert(richNode.is<SmallInt>());
cout << richNode.as<SmallInt>().value() << endl;
Note that the method as<T>()
has is<T>()
as precondition. is<T>()
itself tests the following condition: richNode.type() == T::type()
. It is invalid (i.e., it is a bug) to call as<T>()
with the wrong static type parameter.
Attention! In this "tutorial", we are using RichNode::is<T>()
and RichNode::as<T>()
quite a lot. However, in real code, using these methods is prohibited by default (some rare cases need it). Indeed, to use these 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, five;
A = OptVar::build(vm);
B.copy(vm, A); // remember: this ejects the OptVar into a StableNode
five = SmallInt::build(vm, 5);
RichNode richA = A; // follows the references
assert(richA.is<OptVar>());
richA.as<OptVar>().bind(vm, five); // implicit conversion of five to a RichNode
RichNode richB = B;
cout << richB.type()->getName() << endl; // displays "SmallInt"
cout << richB.as<SmallInt>().value() << endl; // displays "5"