The purpose of SModel language is to query and modify MPS models.
Treatment of null values
In an OO-language like Java it's pretty common to have a lot of checks for null values in the form of expr == null and expr != null statements. These are necessary to prevent null pointer exceptions from occuring. However, they at the same time increase the code clutter and often make code difficult to read. In order to alleviate this problem, we MPS treats null values in a liberal way. For example, if you ask a null node for a property, you will get back a null value. If you ask a null node for its children list, you will get empty list, etc.
SModel language has the following types:
- node<ConceptType> - corresponds to an AST node
- nlist<ConceptType> - corresponds to a list of AST nodes
- model - corresponds to an instance of the MPS model
- search scope - corresponds to a search scope of a node's reference
- reference - corresponds to an AST node that represents reference instance
- concept<Concept> - corresponds to an AST node that represents a concept
- enummember<Enum Data Type> - corresponds to an AST node that represents an enumeration member
Most of the SModel language operations are applicable to all of these types.
A lot of the operations in the SModel language accept parameters. The parameters can be specified once you open the parameter list by entering < at the end of an operation.
The SModel language can be used to access the following features:
- concept properties
- concept references
To access them, the following syntax is used:
If the feature is a property, then the type of whole expression is the property's type. If the feature is a reference or a child of 0..1 or 1 cardinality, then the type of this expression is node<LinkTarget>, where LinkTarget is the target concept in the reference or child declaration. If the feature is a child of 0..n cardinality, then the type of this expression is nlist<LinkTarget>.
You can use so-called implicit select to access features of the child nodes. For example, the following query:
will be automatically transformed by MPS to something like:
resulting in a plain collection of all non-null model elements accessible through the specified chain of link declarations.
Since nulls are treated liberally in MPS, we need a way to check for null values. The isNull and isNotNull operations are our friends here.
IsInstanceOf check and type casts
Often, we need to check whether a node is an instance of a particular concept. We can't use Java's instanceof operator since it only understands java objects, not our MPS nodes. To perform this type of check, the following syntax should be used:
Also, there's the isExactly operation, which checks whether a node's concept is exactly the one specified by a user.
Once we've checked a node's type against a concept, we usually want to cast an expression to a concept instance and access some of this concept's features. To do so, the following syntax should be used:
Another way to cast node to particular concept instance is by using as cast expression:
The difference between the regular cast (using colon) and the as cast is in a way it handles the situation when the result of the left-side expression cannot be safely cast to the specified Concept instance: A NullPointer exception will be thrown by the regular cast in this case, while null will be returned by the as cast.
In order to find a node's parent, the parent operation is available on every node.
The children operation can be used to access all direct child nodes of the current node. This operation has an optional parameter linkQualifier. With this parameter result of children<linkQualifier> operation is equivalent to node.linkQualifier operation call and so will recall only the children belonging to the linkQualifier group/role.
When you manipulate the AST, you will often want to access a node's siblings (that is, nodes with the same role and parent as the node under consideration). For this task we have the following operations:
- next-sibling/prev-sibling - returns next/previous sibling of a node. If there is no such sibling, null is returned.
- next-siblings/prev-siblings - returns nlist of next/previous siblings of a node. These operations have an optional parameter that specifies whether to include the current node.
- siblings - returns nlist of all siblings of a node. These operations have an optional parameter that specifies whether to include the current node.
During model manipulation, it's common to find all ancestors (parent, parent of a parent, parent of a parent of a parent, etc) of a specified node. For such cases we have two operations:
- ancestor - return a single ancestor of the node
- ancestors - returns all ancestors of the node
Both of them have the following parameters to narrow down the list:
- concept type constraint: concept=Concept, concept in [ConceptList]
- a flag indicating whether to include the current node: +
It's also useful to find all descendants (direct children, children of children etc) of a specified node. We have the descendants operation for such purposes. It has the following parameters:
- concept type constraint: concept=Concept, concept in [ConceptList]
- a flag indicating whether to include current node: +
Containing root and model
To access top-most ancestor node of a specified node you can make use of containing root operation. Containing model is available as a result of the model operation.
Often we want to find all nodes in a model which satisfy a particular condition. We have several operations that are applicable to expressions of model type:
- roots(Concept) - returns all roots in a model which are instances of the specified Concept
- nodes(Concept) - returns all nodes in a model which are instances of the specified Concept
Search scope queries
In some situations, we want to find out which references can be set on a specified node. For such cases we have the search scope operation. It can be invoked with the following syntax:
Often we want to have a reference to a specified concept. For this task we have the concept literal. It has the following syntax:
If you want to find the concept of a specified node, you can call the concept operation on the node.
Concept hierarchy queries
We can query super/sub-concepts of expression with the concept type. The following operations are at your disposal:
- hierarchy - returns inheritance hierarchy of a concept
- super-concepts - returns all super-concepts of the specified concept. There is an option to include/exclude the current concept.
- sub-concepts - returns sub-concepts
Is Role operation
Sometimes we may want to check whether a node has a particular role. For this we have the following syntax:
Containing link queries
If one node was added to another one (parent) using the following expression:
then you can call the following operations to access the containment relationship information:
- containingRole - returns a string representing the child role of the parent node containing this node ("childLinkRole" in above case)
- containingLink - returns node<LinkDeclaration> representing a link declaration of the parent node containing this node
- index - returns int value representing index of this node in a list of children with corresponding role. Identical to the following query upon the model represented above:
Following operation were created co access reference instance representing a reference from source node to target one. Operations are applicable on source node:
- reference< > - returns an instance of reference type representing specified reference. This operation requires "linkQuelifier" parameter used as reference specification. Parameter can be either link declaration of source node's concept or expression returning node<LinkDeclaration> as a result
- references - returns sequence<reference> representing all references specified in source node.
Having an instance of reference type you can call following operations on it:
- linkDeclaration - returns node<LinkDeclaration> representing this reference
- resolveInfo - returns string resolve info object
- role - returns reference role - similar to reference.linkDeclaration.role;
- target - returns node<> representing reference target is it was specified and located in model(s)
Downcast to lower semantic level
SModel language generates code that works with raw MPS classes. These classes are quite low-level for the usual work, but in some exceptional cases we may still need to access them. To access the low-level objects, you should use the downcast to lower semantic level construct. It has the following syntax:
The most commonly used change operation in SModel is the act of changing a feature. In order to set a value of a property, or assign a child or reference node of 0..1 or 1 cardinality, you can use straight assignment (with =) or the set operation. In order to add a child to 0..n or 1..n children collection, you can either use the.add operation from the collection language or call add next-sibling/add prev-sibling operations on a node<> passing another node as a parameter.
New node creation
There are several ways to create a new node:
- new operation: new node<Concept>()
- new instance operation on a model: model.newInstance()
- add new(Concept) and set new(Concept) operations applied to feature expressions
- replace with new(Concept) operation
- new root node(Concept) operation applied to a model. In this case the concept should be rootable
- new next-sibling<Concept>/new prev-sibling<Concept> operations adding new sibling to an existing node
To create a copy of an existing node, you can use the copy operation.
To replace a node with an instance of another node, you can use the 'replace with' operation. If you want to replace and create at the same time, there is a shortcut operation 'replace with new(Concept)', which takes a concept as a parameter.
Delete and detach operations
If you want to completely delete a node from the model, you can use the delete operation. In order to detach a node from it's parent only, so that you can for example attach the node to another parent later, you use the detach operation.