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.
  4. abstract specifies whether the class is to be abstract or not.

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

  1. source_lower_cardinality specifies the number (integer) of incoming edges that are at least required for the target.
  2. source_upper_cardinality specifies the number (integer) of incoming edges that are at most required for the target.
  3. target_lower_cardinality specifies the number (integer) of outgoing edges that are at least required for the source.
  4. 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", "constraint", \
...     """
...     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_gte(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 and omitting the check greater than 0).