Internal workings

For more detailed information on the Modelverse specification, which this project is implementing, we refer to the Modelverse Specification.

Information on the implementation can be found below.

Modelverse State

The Modelverse State is basically just an implementation of a graph library. As we have a particular kind of graph, this implementation is mostly done by hand at the moment. The notable exception to this is the RDF backend, proving that other implementations can also be used.

The basic implementation just stores everything as dictionaries. All operations are then defined by doing operations on these dictionaries. The most interesting operations here are dictionary operations, which need to traverse these dictionaries in complex ways. To overcome performance problems for these operations, all results are cached (and validated afterwards).

RDF backend

The RDF backend requires the rdflib module in Python. The Modelverse graph is then stored in RDF representation and all operations on it are done using SPARQL queries. Due to this level of indirection, performance is extremely slow. To increase performance, we would likely have to make more composite operations, or even group different requests together internally.

Modelverse Kernel

The Modelverse Kernel is basically a graph transformation kernel. It consists of two parts: a small transformation engine (only a few lines in Python), and a copy of all rules it knows. Most code is an encoding of these transformation rules, and can (theoretically) be automatically generated by the Modelverse itself. This is currently not top priority, but will probably be implemented in the future.

The transformation rules are an encoding of the rules presented in the specification mentioned at the top of this page. In each rule, the MvK needs to have tight communication with the MvS. For this, the rules are encoded using yield operations in Python. Rules therefore act as generators, outputting commands to the MvS, and immediately getting a reply. Each individual yield contains a list of operations to send to the MvS simultaneously. The result will therefore also be a list of results for each operation.

As you can see in these rules, each entry in the list has a specific form. An entry is a tuple containing two elements: the code of the operation to execute, followed by a list of all parameters to pass. The code is a shortened name for the operation, such as CN for create_node. A full mapping can be found in the MvS, the most important ones are shown below.

Code Function
CN create_node
CNV create_nodevalue
CE create_edge
CD create_dict
RV read_value
RE read_edge
RO read_outgoing
RI read_incoming
RD read_dict
RDN read_dict_node
RDE read_dict_edge
RDNE read_dict_node_edge
RR read_root
DN delete_node
DE delete_edge

Note

Since each yield operation works on lists even a single operation needs to be wrapped in a list. Worse, however, is that the return value will also be a list. Instead of having to take the first element each time, Python allows you to write a comma after the variable, to make it expand it automatically. Your syntax thus becomes:

a, = yield [("CN", [])]

Where a will now already contain the created node, and not a list with as first (and only) element the created node.

Primitives

The Modelverse Kernel also defines the primitives users can use. Primitives are necessary since we can’t go deeper at some point. It is the MvK’s responsibility to implement each and every primitive defined in the bootstrap file.

Primitives are implemented in the bootstrap file as having a body with an empty node. When execution goes to this node, the MvK must execute the associated primitive instead. To do this, the MvK must map all these nodes to their corresponding primitive implementation during startup.

Precompiled functions

Similar to primitives, the MvK also supports precompiled functions. A precompiled function is similar to a primitive in terms of implementation, but they do also have an implementation inside of the Modelverse itself. This means that there are two implementations: one hardcoded, and one in action language. Both should obviously be consistent.

Whereas primitives are required for a functional Modelverse, precompiled functions are only a performance optimization, and can be disabled by the user for debugging purposes. This is interesting, since with precompiled functions, no intermediate values will be visible. Additionally, precompiled functions cannot be changed, as they are Python code instead of being explicitly modelled.

Modelling operations

Apart from the simple action language interpreter presented before, the Modelverse State is loaded with modelling logic. This includes common functions, such as conformance checking, model transformation, and instantiation. As the code is loaded in the MvS by default, the MvK will automatically start executing that code. Therefore, most users will only experience the Modelverse as a modelling environment. Nonetheless, the Modelverse Kernel is more generic than this, and would also allow for users to define their own interface. As all aspects of the Modelverse are explicitly modelled, it is possible to alter the pre-loaded model and thus update the built-in behaviour.

Modelverse Interface

Finally, we come to the Modelverse Interface. For the moment, this is only a simple compiler that takes action language or model language files and translates them to something the Modelverse can understand. There are basically two kinds of operations the Modelverse can understand: direct graphs, or a list of constructors. The use of direct graphs is deprecated, as constructors are much more elegant (but slower). Future changes should make constructors as fast as the use of graphs. Nonetheless, which approach is used does not matter for the action language or model language files.

The parser used for this simple front-end is the HUTN parser. It creates an abstract syntax tree, which is then traversed by a series of visitors. The visitors that are used, depends on the selected mode of compilation. We briefly present the most important visitors.

Semantics visitor

The visitor most oftenly used is the Semantics visitor. It traverses the abstract syntax tree and constructs symbol tables and finds all declarations of functions. Additionally, it also translates the use of operators to their Modelverse functions.

This visitor in itself does not output anything, but it modifies the existing abstract syntax tree. It is used as a first step for most other visitors.

Constructors visitor

The most important visitor is the constructors visitor. It traverses the abstract syntax tree and outputs a list of strings that make it possible for the MvK to construct the required code in the Modelverse. The use of constructors has several advantages, but the primary advantage is that the interface does not have to know about the internal representation of action language in the Modelverse. However, the order of constructors and accepted keywords needs to be defined (in action language), and made available to the users. As this interface is explicitly modelled as well, it is possible for users to modify it without affecting the interface. This is bad if the interface suddenly accepts new keywords or removes support for old keywords.

During the execution of the visitor, it would already be possible to start transmitting the list to the Modelverse, such that the Modelverse can create the code in parallel with the actual parsing. While this is possible, we currently split this up in different phases: first the list is generated completely, and then it is transferred to the Modelverse in a single request.

Bootstrap visitor

Despite the primitives visitor being deprecated, the bootstrap visitor still needs access to this kind of output, since it cannot use constructors: during bootstrapping, the Modelverse is not yet available. The actual output format slightly differs from the primitives visitor, but the core concepts are identical.

Model visitor

Finally, the model visitor is similar to the constructor visitor, but goes to a different constructor interface, which supports modelling operations instead of action language operations. This is a very preliminary visitor.

Bootstrapping

To bootstrap, you just have to run the file bootstrap.py. Here, we explain what this file actually does…

The bootstrap script primarily creates the initial graph manually. This manual creation is done by writing data to a file, which is later read by the MvS. The initial graph consists of several parts:

  • The Modelverse Root node;
  • A global definition of all primitives;
  • The stack frame of the initial task task_manager, which manages all other tasks;
  • The code for the task_manager task;
  • The code for all new users, as assigned by the user_manager;
  • Bindings in the compilation manager for bootstrap files.

These are all fairly simple in themselves. For some parts, such as the code, the HUTN compiler is invoked on a temporary piece of code. All bootstrap files are also compiled and made available to the Modelverse.

How to add a primitive

Probably the most important reason why you would want to know about the Modelverse internals, is if you want to add a Modelverse primitive. Primitives are functions implemented in the MvK core, and thus become hardcoded.

Warning

While these functions are hardcoded, their implementation needs to follow strict rules, such that their semantics is identical in all possible programming languages. For example, a primitive getTime() cannot simply be implemented as time.time() in Python, as this gives different results on Linux and Windows.

To add a primitive, follow these steps:

  1. Add the code of the primitive to the MvK implementation by adding it in the file kernel/modelverse_kernel/primitives.py.

    1. Name the function identically to how the primitive will be invoked later on.
    2. Take a set of parameters. Parameters for primitives are always positional and start from a onwards.
    3. The final parameter should be a catch-all element, as it is possible that a high number of additional information is passed. In Python, this is done with a parameter prepended with **.
    4. The parameters are encoded using a dictionary notation, containing the keys id and value. The value key is the cached value of the node, or the value of a node that is still to be materialized.
    5. When asking the MvS for information, use yields, just like the implementation of transformation rules in the MvK.
    6. Instead of returning using Python code, yield a RETURN command, taking one argument: the returnvalue.
  2. Add the signature to bootstrap.py in the topmost dictionary primitives. The key is the name of the function, and the value is a list starting with the return type, followed by all parameter types (as string).

  3. Add the declaration to includes/primitives.alh to make them available everywhere.

  4. Add the definition to primitives.alc and assign the Modelverse reference ?primitives/function_name.

  5. Recreate the bootstrap file by running the script generate_bootstrap.py.

  6. Restart the Modelverse to reload the bootstrap file.

  7. From now on, all files including primitives.alh have access to the defined function.

Adding a precompiled function

Adding a precompiled function is way easier: only step 1 of the addition of a primitive should be done, but in the file kernel/modelverse_kernel/compiled.py instead of kernel/modelverse_kernel/primitives.py. All other steps are done automatically since there is an action language implementation present. The MvK will then automatically pick the precompiled function or the explicitly modelled operation, depending on preferences. A restart of the MvK is needed for Python to pick up the new functions.