Metamodelling
=============

Defining languages in the Modelverse is done through metamodelling.
With metamodelling, a language is itself a model, which can be created through the usual operations.
This explains why language engineers need to have the same knowledge of the Modelverse as modellers: language engineering is modelling in a domain-specific language for languages.

Creating a metamodel
--------------------

To create a metamodel, you have to instantiate the *SimpleClassDiagrams* metamodel.
While there is no restriction that this has to be *SimpleClassDiagrams*, for now we assume that this is the only possible meta-language.
Therefore, to create the PetriNets language, we execute::

    >>> model_add("formalisms/PetriNets", "models/SimpleClassDiagrams")

From then on, we can simply use the PetriNets metamodel as if it were a model.
Again, the *model_add* operation can take an optional third parameter, specifying the textual representation of the model.
We will go deeper into this in one of the next sections.

Instantiating a metamodel
-------------------------

To actually define a metamodel, we need to add elements to it.
These elements are tightly related to the metamodel of the model, being most likely *SimpleClassDiagrams*.
In this language, there exist four major components: *Class*, *Association*, *Inheritance*, and attributes.

Class
^^^^^

Creating a class is easy, and very similar to usual modelling.
To define the *Place* concept, which can later on be used in models, we merely define this as a new class::

    >>> instantiate("formalisms/PetriNets", "Class", ID="Place")

Note that here we must define an ID to get a sensible name to use.
Of course, this ID does not need to be used, and the returned (randomized) ID can just as well be used.
Nonetheless, if it is used, it becomes non-intuitive to use in future requests.
In the instance of this newly created language, we can immediately use the new concept::

    >>> instantiate("formalisms/PetriNets", "Class", ID="Place")
    >>> instantiate("my_pn", "Place")

If the ID was not used, we must remember the ID that was provided to us::

    >>> pn_place = instantiate("formalisms/PetriNets", "Class")
    >>> instantiate("models/my_pn", pn_place)

While this is more reliable, it becomes difficult to refer to the concept in the future without any clue as to what it resembles.
Later on, this can be solved with concrete syntax, in which case modellers can identify the concepts based on their visual representation.

Association
^^^^^^^^^^^

Creating a link between two concepts is very similar.
For example, to create the *P2T* association, merely create an instance of association, using the same remarks as before::

    >>> instantiate("formalisms/PetriNets", "Association", edge=("Place", "Transition"), ID="P2T")

Inheritance
^^^^^^^^^^^

A useful concept is the use of inheritance, whereby the inheriting class can take on the responsibilities of the inherited class.
To create this, simply instantiate the *Inheritance* association as usual.
For example, it is possible to structure the *PetriNets* model (in an admittedly strange way) such that there is a generic concept of *NamedElement* and *Arc*::

    >>> instantiate("formalisms/PetriNets", "Class", ID="NamedElement")
    >>> instantiate("formalisms/PetriNets", "Class", ID="Place")
    >>> instantiate("formalisms/PetriNets", "Class", ID="Transition")
    >>> instantiate("formalisms/PetriNets", "Inheritance", edge=("Place", "NamedElement"))
    >>> instantiate("formalisms/PetriNets", "Inheritance", edge=("Transition", "NamedElement"))

In the Modelverse, inheritance is possible between any two instances, including associations.
To define the *Arc* relation, we act similar::

    >>> instantiate("formalisms/PetriNets", "Association", edge=("NamedElement", "NamedElement"), ID="Arc")
    >>> instantiate("formalisms/PetriNets", "Association", edge=("Place", "Transition"), ID="P2T")
    >>> instantiate("formalisms/PetriNets", "Association", edge=("Transition", "Place"), ID="T2P")
    >>> instantiate("formalisms/PetriNets", "Inheritance", edge=("P2T", "Arc"))
    >>> instantiate("formalisms/PetriNets", "Inheritance", edge=("T2P", "Arc"))

Instantiating attributes
^^^^^^^^^^^^^^^^^^^^^^^^

Instantiating attributes is similar as it was before::

    >>> attr_assign("formalisms/PetriNets", "NamedElement", "name", "NamedElement")
    >>> attr_assign("formalisms/PetriNets", "Transition", "name", "Transition")
    >>> attr_assign("formalisms/PetriNets", "Place", "name", "Place")

Note that this *name* attribute is in no way related to the *name* attributes at the *my_pn* level.
Indeed, the *name* in *PetriNets* specifies the name of the class.
Other attributes that can be set on classes are:

1. *lower_cardinality* specifies the number (integer) of instances that must at least exist of this element.
2. *upper_cardinality* specifies the number (integer) of instances that must at most exist of this element.
3. *constraint* specifies additional constraints on the instances of this class.

For associations, we have the same three attributes, augmented with:

4. *source_lower_cardinality* specifies the number (integer) of incoming edges that are at least required for the target.
5. *source_upper_cardinality* specifies the number (integer) of incoming edges that are at most required for the target.
6. *target_lower_cardinality* specifies the number (integer) of outgoing edges that are at least required for the source.
7. *target_upper_cardinality* specifies the number (integer) of outgoing edges that are at most required for the source.

Constraints
^^^^^^^^^^^

A constraint specifies additional constraints on the instances of this model, which could not be expressed before.
We distinguish between two types of constraints: local and global constraints.

Local constraints are defined at the level of a single class or association.
For example, if the number of tokens must always be non-negative, this can be specified as follows::

    >>> attr_assign_code("formalisms/PetriNets", "Place", "constraints", \
    ...     """
    ...     String function constraint(model : Element, name : String):
    ...         if (integer_gte(read_attribute(model, name, "tokens"), 0)):
    ...             return "OK"!
    ...         else:
    ...             return "Negative number of tokens specified"!

Global constraints operate over the complete model, and are therefore not attached to a single element.
Instead, a specific *GlobalConstraint* element must be instantiated.
This element has a *constraint* attribute, which specifies the constraint to evaluate in the global context.
For example, when, for some reason, the number of places must always be larger than the number of transitions::

    >>> constraint = instantiate("formalisms/PetriNets", "GlobalConstraint")
    >>> attr_assign_code("formalisms/PetriNets", constraint, "constraint", \
    ...     """
    ...     String function constraint(model : Element):
    ...         if (set_len(allInstances(model, "Place")) > set_len(allInstances(model, "Transition"))):
    ...             return "OK"!
    ...         else:
    ...             return "Needs more places than transitions"!

While all constraints can be specified as global constraints, using local constraints is preferred.

.. note::
   Currently, all constraints are only evaluated upon explicit user request.
   More specifically: during the *verify* operation.

Defining attributes
^^^^^^^^^^^^^^^^^^^

Defining attributes is an important consideration in a metamodel.
Up to now, our *Place* and *Transition* were just nodes without any attributes.
A meaningful (marked) petrinet has tokens in places, and names for the places and transitions.
To define these attributes, we must take two steps:

1. Define the type that will be used (e.g., String, Integer)
2. Define the name and type of the attribute (e.g., *tokens* is an Integer, *name* is a String)

To define the type that we want to use, we must manually define it.
While many other tools provide a set of primitives, which cannot be altered in any way, the Modelverse leaves all this up to the language engineer.
If the language engineer wants the concepts of a *Natural*, for example, it is not required that the attribute is specified as an Integer, with an additional constraint on each and every attribute that is defined this way.
It is possible to define a new attribute type as follows::

    >>> instantiate("formalisms/PetriNets", "SimpleAttribute", ID="Natural")

But now, the attribute is constrained in no way: it is merely called Natural, but can even contain a string.
The attribute type must be constrained::

    >>> attr_assign_code("formalisms/PetriNets", "Natural", "constraint", \
    ...     """
    ...     String function constraint(value : Element):
    ...         if (is_physical_integer(value)):
    ...             if (integer_geq(value, 0)):
    ...                 return "OK"!
    ...             else:
    ...                 return "Not a positive value"!
    ...         else:
    ...             return "Not a numeric value!"!

Now, the *Natural* is correctly specified and can be used throughout the model.
All uses of *Natural* will now make sure that the value is positive.
This offers additional possibilities, such as defining complex attributes as if they were primitives, for example complex numbers.

The next step is to define the attribute itself.
This can be easily done as follows::

    >>> define_attribute("formalisms/PetriNets", "Place", "tokens", "Natural")

For the name, *String* can be defined similarly (using *is_physical_string*).
