MPS User's Guide (one page)

Skip to end of metadata
Go to start of metadata

Basic notions

This chapter describes the basic MPS notions: nodes, concepts, and languages. These are key to proper understanding of how MPS works. They all only make sense when combined with the others and so we must talk about them all together. This section aims to give you the essence of each of the elements. For further details, you may consider checking out the sections devoted to nodes, concept (structure language), and languages (project structure).

Abstract Syntax Tree (AST)

MPS differentiates itself from many other language workbenches by avoiding the text form. Your programs are always represented by an AST. You edit the code as an AST, you save it as an AST you compile it as, well, as an AST. This allows you to avoid defining grammar and building a parser for your languages. You instead define your language in terms of types of AST nodes and rules for their mutual relationships. Almost everything you work with in the MPS editor is an AST-node, belonging to an Abstract Syntax Tree (AST). In this documentation we use a shorter name, node, for AST-node.

Node

Nodes form a tree. Each node has a parent node (except for root nodes), child nodes, properties, and references to other nodes.

The AST-nodes are organized into models. The nodes that don't have a parent, called root nodes. These are the top-most elements of a language. For example, in BaseLanguage (MPS' counterpart of Java), the root nodes are classes, interfaces, and enums.

Concept

Nodes can be very different from one another. Each node stores a reference to its declaration, its concept. A concept sets a "type" of connected nodes. It defines the class of nodes and coins the structure of nodes in that class. It specifies which children, properties, and references an instance of a node can have. Concept declarations form an inheritance hierarchy. If one concept extends another, it inherits all children, properties, and references from its parent.
Since everything in MPS revolves around AST, concept declarations are AST-nodes themselves. In fact, they are instances of a particular concept, ConceptDeclaration.

Language

Finally we get the language definition. A language in MPS is a set of concepts with some additional information. The additional information includes details on editors, completion menu, intentions, typesystem, generator, etc. associated with the language. This information forms several language aspects.
Obviously, a language can extend another language. An extending language can use any concepts defined in the extended language as types for its children or references, and its concepts can inherit from any concept of the extended language. You see, languages in MPS form fully reusable components.

Next 

MPS Project Structure

Introduction

When designing languages and writing code, good structure helps you navigate around and combine the pieces together. MPS is similar to other IDEs in this regard.

Project

Project is the main organizational unit in MPS. Projects consist of one or more modules, which themselves consist of models. Model is the smallest unit for generation/compilation. We describe these concepts in detail right below.

Models

Here's a major difference that MPS brings along - programs are not in text form. Ever.
You might be used to the fact that any programming is done in text. You edit text. The text is than parsed by a parser to build an AST. Grammars are typically used to define parsers. AST is then used as the core data structure to work with your program further, either by the compiler to generate runnable code or by an IDE to give you clever code assistance, refactorings and static code analysis.
Now, seeing that AST is such a useful, flexible and powerful data structure, how would it help if we could work with AST from the very beginning, avoiding text, grammar and parsers altogether? Well, this is exactly what MPS does.

To give your code some structure, programs in MPS are organized into models. Think of models as somewhat similar to compilation units in text based languages. To give you an example, BaseLanguage, the bottom-line language in MPS, which builds on Java and extends it in many ways, uses models so that each model represents a Java package. Models typically consist of root nodes, which represent top level declarations, and non-root nodes. For example, in BaseLanguage classes, interfaces, and enums are root nodes. (You can read more about nodes here ).

Models need to hold their meta information:

  • models they use (imported models)
  • languages they are written in (in used languages section)
  • devkits they use (devkits section)

This meta information can be altered in Model Properties of the model's pop-up menu or using Alt + Enter when positioned on the model.

Modules

Models themselves are the most fine-grained grouping elements. Modules organize models into higher level entities. A module typically consists of several models acompanied with meta information describing module's properties and dependencies. MPS distinguishes several types of modules: solutions, languages, devkits, and generators.
We'll now talk about the meta-information structure as well as the individual module types in detail.

Module meta information

Now when we have stuff organized into modules, we need a way to combine the modules together for better good. Relationships between modules are described through meta information they hold. The possible relationships among modules can be categorized into several groups:

  • Dependency - if one module depends on another, and so models inside the former can import models from the latter. The reexport property of the dependency relationship indicates whenther the dependency is transitive or not. If module A depends on module B with the reexport property set to true, every other module that declares depency on A automatically depends on B as well.
  • Used language - if module A uses language L, then models inside A can use language L.
  • Used devkit - if module A uses devkit D, then models inside A can use devkit D.
  • Extended language - if language L extends language M, then every concept from M can be used inside L as a target of a role or an extended concept. Also, all the aspects from language M are available for use and extension in the corresponding aspects of language L.
  • Generator output path - generator output path is a folder where all newly generated files will be placed. This is the place you can look for the stuff MPS generates for you.

Now we'll look at the different types of modules you can find in MPS.

Solutions

Solution is the simplest possible kind of module in MPS. It is just a set of models unified under a common name.

Languages

Language is a module that is more complex than a solution and represents a reusable language. It consists of several models, each defining a certain aspect of the language: structure, editor, actions, typesystem, etc.
Languages can extend other languages. An extending language can then use all concepts from the extended language - derive its own concepts, use inherited concepts as targets for references and also place inherited concepts directly as children inside its own concepts.

Languages frequently have runtime dependencies on third-party libraries or solutions. You may, for example, create a language wrapping any Java library, such as Hibernate or Swt. Your language will then give the users a better and smoother alternative to the standard Java API that these libraries come with.
Now, for your language to work, you need to include the wrapped library with your language. You do it either through a runtime classpath or through a runtime solution. Runtime classpath is suitable for typical scenarios, such as Java-written libraries, while runtime solutions should be prefered for more complex scenarios.

  • Runtime classpath - makes library classes available as stubs language generators
  • Runtime solutions - these models are visible to all models inside the generator

Language aspects

Language aspects describe different facets of a language:

  • structure - describes the nodes and structure of the language AST. This is the only mandatory aspect of any language.
  • editor - describes how a language will be presented and edited in the editor
  • actions - describes the completion menu customizations specific to a language, i.e. what happens when you type Control + Space
  • constraints - describes the constraints on AST: where a node is applicable, which property and reference are allowed, etc.
  • behavior - describes the behavioral aspect of AST, i.e. AST methods
  • typesystem - describes the rules for calculating types in a language
  • intentions - describes intentions (context dependent actions available when light bulb pops up or when the user types Alt + Enter)
  • plugin - allows a language to integrate into MPS IDE
  • data flow - describes the intented flow of data in code. It allows you to find unreachable statements, uninitialized reads etc.

You can read more about each aspect in the corresponding section of this guide.

Generators

Generators define possible transformations of a language into something else, typically into another languages. Generators may depend on other generators. Since the order in which generators are applied to code is important, ordering constraints can be set on generators. You can read more about generation in the corresponding section.

DevKits

DevKits have been created to make your life easier. If you have a large group of interconnected languages, you certainly appreciate a way to treat them as a single unit. For example, you may want to import them without listing all of the individual languages. DevKits make this possible. When building a DevKit, you simply list languages to include.
As expected, DevKits can extend other DevKits. The extending DevKit will then carry along all the inherited languages as if they were its own ones.

Projects

This one is easy. A project simply wraps modules that you need to group together and work with them as a unit.

Java compilation

MPS was born from Java and is frequently used in Java environment. Since MPS models are often generated into java files, a way to compile java is needed before we can run our programs. There are generally two options:

  • Compiling in MPS (recommended)
  • Compiling in IntelliJ IDEA (requires IntelliJ IDEA)

When you compile your classes in MPS, you have to set the module's source path. The source files will be compiled each time the module gets generated, or whenever you invoke compilation manually by the make or rebuild actions.
Previous Next

Since MPS frees you from defining a grammar for your intented languages, you obviously need different ways to specify the structure of your languages. This is where the Structure Language comes in handy. It gives you all the means to define the language structure. As we discussed earlier, when coding in MPS you're effectively building the AST directly, so the structure of your language needs to specify the elements, the bricks, you use to build the AST.

The bricks are called Concepts and the Structure Language exposes concepts and concept interfaces as well as their members: properties, references, children, concept(-wide) properties, and concept(-wide) links.

Concepts and Concept Interfaces

Now let's look at those in more detail. A Concept defines the structure of a concept instance, a node of the future AST representing code written using your language. The Concept says which properties the nodes might contain, which nodes may be referred to, and what children nodes are allowed (for more information about nodes see the Basic notionssection). Concepts also define concept-wide members - concept properties and concept links, which are shared among all nodes of the particular Concept. You may think of them as "static" members.

Apart from Concepts, there are also Concept Interfaces. Concept interfaces represent independent traits, which can be inherited and implemented by many different concepts. You typically use them to bring orthogonal concepts together in a single concept. For example, if your Concept instance has a name by which it can be identified, you can implement the INamedConcept interface in your Concept and you get the name property plus associated behavior and constraints added to your Concept.

Concepts inheritance

Just like in OO programming, a Concept can extend another Concept, and implement many Concept Interfaces. A Concept Interface can extend multiple other Concept Interfaces. This system is similar to Java classes, where a class can have only one super-class but many implemented interfaces, and where interfaces may extend many other interfaces.

If a concept extends another concept or implements a concept interface, it transitively inherits all members (i.e if A has member m, A is extended by B and B is extended by C, then C also has the member m)

Concept interfaces with special meaning

There are several concept interfaces in MPS that have a special meaning or behavior when implemented by your concepts. Here's a list of the most useful ones:

Concept Interface Meaning
IDeprecatable Used if instances of your concept can be deprecated. Sets a strikeout style if isDeprecated set to true.
INamedConcept Used if instances of your concept have an identifying name. This name appears in the code completion list.
IType Is used to mark all concepts representing types
IWrapper Deleting a node whose immediate parent is an instance of IWrapper deletes the parent node as well.

Concept members

Properties

Property is a value stored inside a concept instance. Each property must have a type, which for properties is limited to: primitives, such as boolean, string and integer; enumerations, which can have a value from a predefined set; and constrained data types (strings constrained by a regular expression).

References

Holding scalar values would not get as far. To increase expressiveness of our languages nodes are allowed to store references to other nodes. Each reference has a name, a type, and a cardinality. The type restricts the allowed type of a reference target. Cardinality defines how many references of this kind a node can have. References can only have two types of cardinalities: 1:0..1 and 1:1.

Smart references

A node containing a single reference of 1:1 cardinality is called a smart reference. These are somewhat special references. They do their best to hide from the language user and be as transparent as possible. MPS treats the node as if it was a the actual reference itself, which simplifies code editing and code-completion. For example, default completion items are created whenever the completion menu is required: for each possible reference target, a menu item is created with matching text equal to the presentation of a target node.

Children

To compose nodes into trees, we need to allow children to be hooked up to them. Each child declaration holds a target concept, its role and cardinality. Target concept specifies the type of children. Role specifies the name for this group of children. Finally, cardinality specifies how many children from this group can be contained in a single node. There are 4 allowed types of cardinality: 1:1, 1:0..1, 1:0..n, and 1:1..n.

Specialized references and children

Sometimes, when one concept extends another, we not only want to inherit all of its members but also want to override some of its traits. This is possible with children and references specialization. When you specialize a child or reference, you narrow its target type. For example, if you have concept A which extends B, and have a reference r in concept C with target type B, you might narrow the type of reference r in C's subconcepts. It works the same way for concept's children.

Concept links and properties

Sometimes we want to define concept-wide members. For example, we might want to describe how all instances of a particular type will be presented in a completion menu. Or we frequently need to mark a concept as abstract. Also concept-wide members can help you express some domain specific purpose of a concept. This is what concept links and properties are for. Concept link and properties are declared in a concept declaration and the value you assign to them are valid for all instances of that concept. Also, if you have a concept-wide member declaration specified in a particular concept, different concrete values can then be set to the member in its subconcept. For example, the "abstract" concept property, which is of a boolean type, is declared in the BaseConcept concept. Some sub-concepts, like BaseAssignmentExpression have the "abstract" concept-wide property set to true, while others, like e.g. AssignmentExpression not. It is then up to the language designer to reflect the property values in the language behavior.

Annotation concepts

TO DO..

Previous Next

The Structure Language may sometimes be insufficient to express advanced constraints on the language structure. The Constraints aspect gives you a way to define such additional constraints.

Can be child/parent/ancestor/root

These are the first knobs to turn when defining constraints for a concept. They determine whether instances of this concept can be hooked as children (parents, ancestors) nodes of other nodes or root nodes in models.You specify them as boolean closures to invoke each time when evaluating allowed possition for a node in the AST.

can be child

Return false if an instance of the concept is not allowed to be a child of specific nodes.

parameter description
operationContext IOperationContext
scope current context (IScope)
parentNode the parent node we are checking
childConcept concept of the child node (can be a subconcept of this concept)
link LinkDeclaration of the child node (child role can be taken from there)

can be parent

Return false if an instance of concept is not allowed to be a parent of specific concept node (in a given role).

parameter description
operationContext IOperationContext
scope context (IScope)
node the parent node we are checking (instance of this concept)
childConcept the concept of the child node we are checking
link LinkDeclaration of the child node

can be ancestor

Return false if an instance of the concept is not allowed to be an ancestor of specific nodes.

parameter description
operationContext IOperationContext
scope context (IScope)
node the ancestor node we are checking (instance of this concept)
childConcept the concept of the descendant node

can be root

This constraint is available only for rootable concepts (instance can be root is true in the concept structure description). Return false if instance of concept cannot be a root in the given model.

parameter description
operationContext IOperationContext
scope context (IScope)
model model of the root

Property constraints

Technically speaking, "pure" concept properties are not properties in its original meaning, but only public fields. Property constraints allow you to make them real properties. Using these constraints, the behavior of concept's properties can be customized. Each propertz constraint is applied to a single specified property.

property - the property to which this constraint is applied.

get - this method is executed to get property value every time property is accessed.

parameter description
node node to get property from
scope context (IScope)

set - this method is executed to set property value on every write. The property value is guaranteed to be valid.

parameter description
node node to set property
propertyValue new property value
scope context (IScope)

is valid - this method should determine whether the value of the property is valid. This method is executed every time before changing the value, and if it returns false, the set() method is not executed.

parameter description
node node to check property
propertyValue value to be checked
scope context (IScope)

Referent constraints

Constraints of this type help to add behavior to concept's links and make them look more properties-like.

referent set handler - if specified, this method is executed on every set of this link.

parameter description
referenceNode node that contains link.
oldReferentNode old value of the reference.
newReferentNode new value of the reference.
scope context: IScope interface to object that shows you models, languages and devkits you can see from the code.


search scope - defines the set of nodes to which this link can point. The method can return either a sequence<node<>> or ISearchScope.

parameter description
model the model that contains the node with the link. This is included for convenience, since both referenceNode and enclosingNode keep the model too.
scope IScope interface that shows models, languages and devkits you can see from the code.
referenceNode the node that contains the actual link. It can be null when a new node is being created for a concept with smart reference. In this situation smart reference is used to determine what type of node to create in the context of enclosingNode, so the search scope method is called with a null referenceNode.
enclosingNode parent of the node that contains the actual link, null for root nodes. Both referenceNode and enclosingNode cannot be null at the same time.
linkTarget the concept that this link can refer to. Usually it is a concept of the reference, so it is known statically. If we specialize reference in subconcept and do not define search scope for specialized reference, then linkTarget parameter can be used to determine what reference specialization is required.

If search scope is not set for the reference then default scope from the referenced concept is used. If the default search scope is also not set then "global" scope is used: all instances of referent concept from all imported models.


validator (since 1.5) - Each reference is checked against it's search scope and if, after changes in the model, a reference ends up pointing out of the search scope, MPS marks such a reference with an error message. Sometimes it is not efficient to build the whole search scope just to check if the reference is in scope. The search scope can be big or it may be much easier to check if the given node is in scope than to calculate what nodes are in scope. You can create quick reference check procedure here to speed up reference validation in such situations.

parameter description
model
scope
referenceNode
enclosingNode
linkTarget
context of reference usage, the same meaning as in the search scope method. The main difference: referenceNode cannot be null here because validator is not used during node creation.
checkedNode the node to be validated (referenceNode has a reference to checkedNode of type linkTarget)

It is possible to create validation routine only if you have created a list of nodes in the corresponding search scope. If ISearchScope is returned from search scope method, then isInScope(SNode) method of ISearchScope interface will be used for validation, you should override this method with your validation routine.
It is not possible to define validation routine without defining search scope.


presentation - here you specify how the reference will look like in the editor and in the completion list. Sometimes it is convenient to show reference differently depending on context. For example, in Java all references to an instance field f should be shown as this.f, if the field is being shadowed by the local variable declaration with the same name. By default, if no presentation is set, the name of the reference node will be used as its presentation (provided it is an INamedConcept).

parameter description
model
scope
referenceNode
enclosingNode
linkTarget
the context of reference usage, the same meaning as in the search scope function.
parameterNode the node to be presented (referenceNode has a reference to parameterNode of type linkTarget)
visible true - presentation of existing node, false - for new node (to be created after selection in completion menu)
smartReference true - node is presented in the smart reference
inEditor true - presentation for editor, false - for completion menu
ISearchScope

Low level interface that can be implemented to support search scope. We recommend to subclass AbstractSearchScope (implements ISearchScope) abstract class instead of direct implementation of ISearchScope interface. The only abstract method in AbstractSearchScope class is
@NotNull List<SNode> getNodes(Condition<SNode> condition) - return list of nodes in the current search scope satisfying condition, similar to [search scope] but with the additional condition.
Other useful methods to override:
boolean isInScope(SNode node) - the same function as in [validator] method.
IReferenceInfoResolver getReferenceInfoResolver(SNode, AbstractConceptDeclaration);

resolve info - TODO

Default scope

Suppose we have a link pointing to an instance of concept C and we have no search scope defined for this link in referent constraints. When you edit this link, all instances of concept C from all imported models are visible by default. If you want to restrict set of visible instances for all links to concept C you can set default scope for the concept. As in referent constraint you can set search scope, validator and presentation methods. All the parameters are the same.

Previous Next

During syntax tree manipulation, common operations are often extracted to utility methods in order to simplify the task and reuse functionality. It is possible to extract such utilities into static methods or create node wrappers holding the utility code in virtual methods. However, in MPS a better solution is available: the behavior language aspect. It makes it possible to create virtual and non-virtual instance methods, static methods, and concept instance constructors on nodes.

Concept instance methods

A Concept instance method is a method, which can be invoked on any specified concept instance. They can be both virtual and non-virtual. While virtual methods can be overridden in extending concepts, non-virtual ones cannot. Also a virtual concept method can be declared abstract, forcing the inheritors to provide an implementation.

Concept instance methods can be implemented both in concept declarations and in concept interfaces. This may lead to some method resolution issues. When MPS decides which virtual method to invoke in the inheritance hierarchy, the following algorithm is applied:

  • If the current concept implements a matching method, invoke it. Return the computed value.
  • Invoke the algorithm for an extended concept, if there is one. In case of success return the computed value.
  • Invoke the algorithm for all implemented concept interfaces. In case of success return the computed value.
  • Return failure.

Concept constructors

When a concept instance is created, it is often useful to initialize some properties/references/children to the default values. This is what concept constructors can be used for. The code inside the concept construction is invoked on each instantiation of a new node of a particular concept.

Concept static methods

Some utility methods do not belong to concept instances and so should not be created as instance methods. For concept-wide functionality, MPS provides static concept methods. See also Constraints

Previous Next

SModel Language

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.

Types

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.

Operation parameters

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.

Queries

Features access

The SModel language can be used to access the following features:

  • children
  • properties
  • references
  • 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.

Null checks

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.

Parent

In order to find a node's parent, the parent operation is available on every node.

Children

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.

Sibling queries

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.

Ancestors

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: +

Descendants

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.

Model queries

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:

Concept literal

Often we want to have a reference to a specified concept. For this task we have the concept literal. It has the following syntax:

Concept operation

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:

Reference operations

Accessing references

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.

Working with

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:

Modification operations

Feature changes

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

Copy

To create a copy of an existing node, you can use the copy operation.

Replace with

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.

Previous Next

Pattern Language

The pattern language has a single purpose - to define patterns of model structures. Those patterns form visual representations of nodes you want to match. A pattern matches a node if the node's property values are equal to those specified in the pattern, node's references point to the same targets that the ones of the pattern do and the corresponding children match the appropriate children of the pattern.

Also patterns may contain variables for nodes, references and properties, which then match any node/reference/property. On top of that the variables will hold the actual values upon a successful match.

PatternExpression

The single most important concept of the pattern language is PatternExpression. It contains a pattern as its single arbitrary node. Also, the node can specify the following variables:

  • #name - a node variable, a placeholder for a node. Stores the matching node
  • #name - a reference variable, a placeholder for a reference. Stores the reference's target, i.e. a node.
  • $name - a property variable, a placeholder for a property value. Stores the property value, i.e. a string.
  • *name - a list variable, a placeholder for nodes in the same role. Stores the list of nodes.

Antiquotations may be in particular useful when used inside a pattern, just like inside quotations (see Antiquotations).

Examples

1. The following pattern matches against any InstanceMethodDeclaration without parameters and a return type:




Captured variables:

$methodName string method's name
#statementList node<StatementList> statements

2. The following pattern matches against a ClassifierType with the actual classifier specified inside an antiquotation expression and with any quantity of any type parameters:


Captured variables:

*l nlist<Type> class type's parameters
#ignored node<Type> used as wildcard, its contents is ignored. Means that parameters are arbitrary

Using patterns

Match statement

Patterns are typically used as conditions in match statements. Pattern variables can be referenced from inside of the match statement.
For example:

this piece of code examines a node n and checks whether it satisfies the first or the second condition. Then the statement in the corresponding (matching) block is executed. A pattern variable $name is used in a first block to print out the name of a node. In our case the node holds a variable declaration.

Other usages

Patterns are also used in several other language constructs in MPS. They may appear:

  • as conditions on applicable nodes of typesystem/replacement/subtyping/other rules of typesystem language. (See Inference rules)
  • as supertype patterns in coerce statement and coerce expression. (See Coerce)
  • as conditions on node in generator rules.

You can also use patterns in your own languages.
Basically what  happens is that a class is generated from a PatternExpression and the expression itself is reduced to a constructor of this class. This class extends GeneratedMatchingPattern and has a boolean method match(SNode), which returns a boolean value indicating whether the node matches the pattern. It also holds a method getFieldValue(Stirng) to get the values stored in pattern variables after a successful match.
So to develop your own language constructs using patterns, you can call these two methods in the generator template for your constructs.


Previous Next

Editor


Once the structure for your language is defined, you will probably go and create means to allow developers to build ASTs with it. Manipulating the ASTs directly would not be very intuitive nor productive. To hide the AST and offer the user comfortable and intuitive interaction is the role for language editors.

Editor Overview

An editor for a node serves as its view as well as its controller. An editor displays the node and lets the user modify, replace, delete it and so on. Nodes of different concepts have different editors. A language designer should create an editor for every concept in his/her language.

In MPS, an editor consists of cells, which themselves contain other cells, some text, or a UI component. Each editor has its concept for which it is specified. A concept may have no more than one editor declaration (or can have none). If a concept does not have an editor declaration, its instances will be edited with an editor for the concept's nearest ancestor that has an editor declaration.

To describe an editor for a certain concept (i.e. which cells have to appear in an editor for nodes of this concept), a language designer will use a dedicated language simply called editor language. You see, MPS applies the Language Oriented Programming principles to itself.

The description of an editor consists of descriptions for cells it holds. We call such descriptions "cell models." For instance, if you want your editor to consist of a unique cell with unmodifiable text, you create in your editor description a constant cell model and specify that text. If you want your editor to consist of several cells, you create a collection cell model and then, inside it, you specify cell models for its elements. And so on.

Types Of Cell Models

Constant cell
This model describes a cell which will always contain the same text. Constant cells typically mirror "keywords" in text-based progamming languages.
Collection cell
A cell which contains other cells. Can be horizontal (cells in a collection are arranged in a row), vertical (cells are on top of each other) or have so-called "indent layout" (cells are arranged horizontally but if a line is too long it is wrapped like text to the next line, with indent before each next line).
Property cell
This cell model describes a cell which will show a value of a certain property of a node. The value of a property can be edited in a property cell, therefore, a property cell serves not only as a view also but as a controller. In an inspector, you can specify whether the property cell will be read-only or will allow its property value to be edited.
Child cell
This cell model contains a reference to a certain link declaration in a node's concept. The resulting cell will contain an editor for the link's target (almost always for the child, not the referent). For example if you have a binary operation, say " + ", with two children, "leftOperand" and "rightOperand", an editor model for your operation will be the following: a indent collection cell containing a referenced node cell for the left operand, a constant cell with " + ", and a referenced node cell for the right operand. It will be rendered as an editor for the right operand, then a cell with " + ", and then an editor for the left operand, arranged in a row. As we have seen, and as follows from its name, this type of cell model is typically used to show editors for children.
Referent cell
Used mainly to show reference targets. The main difference between a referent cell and a child cell is that we don't need, or don't want, to show the whole editor for a reference target. For example, when a certain node, say, a class type, has a reference to a java class, we don't want to show the whole editor for that class with its methods, fields, etc - we just want to show its name. Therefore child cells cannot be used for such a purpose. One should use referent cells.
Referent cell allows you to show a different inlined editor for a reference target, instead of using target's own editor. In most cases it's very simple: a cell for a reference target usually consists only of a property cell with the target's name.
Child list cell
This cell is a collection containing multiple child cells for a node's children of the same role. For instance, an editor for a method call will contain a child list cell for rendering its actual arguments. Child list can be indent (text like), horizontal or vertical.
The cell generated from this cell model supports insertion and deletion of the children of the role given, thus serving both as a view and as a controller. The default keys for insertion are Insert and Enter (to insert a child before or after the selected one, respectively), and the default key for deletion is Delete. You also can specify a separator for your list.
A separator is a character which will be shown in constant cells between cells for the children. When you are inside the cell list and you press a key with this character, a new child will be inserted after the selected child. For instance, a separator for a list representing actual parameters in a method call is a comma.
In inspector, you can specify whether the resulting cell list will use folding or not, and whether it will use braces or not. Folding allows your cell list to contract into a single cell (fold) and to expand from it (unfold) when necessary. It is useful for a programmer writing in your language when editing a large root: he/she is able to fold some cells and hide all the information in editor that is not necessary for the current task at the moment. For instance, when editing a large class, one can fold all method bodies except the method he/she is editing at the moment.
Indent cell
An indent cell model will be generated into a non-selectable constant cell containing a whitespace. The main difference between a cell generated from an indent cell and one generated from a constant cell model containing whitespaces as its text is that the width of an indent cell will vary according to user-defined global editor settings. For instance, if a user defines an indent to be 4 spaces long, then every indent cell will occupy a space of 4 characters; if 2 spaces long, then every indent cell will be 2 characters.
UI component cell
This cell model allows a language designer to insert an arbitrary UI component inside an editor for a node. A language designer should write a function that returns a JComponent, and that component will be inserted into the generated cell. Note that such a component will be re-created every time an editor is rebuilt, so don't try to keep any state inside your component. Every state should be taken from and written into a model (i.e. node, its properties and references) - not a view (your component).
A good use case for such a cell model is when you keep a path to some file in a property, and your component is a button which activates a modal file chooser. The default selected path in a file chooser is read from the above-mentioned property, and the file path chosen by the user is written to that property.
Model access
A model access cell model is a generalization of a property cell and, therefore, is more flexible. While a property cell simply shows the value of a property and allows the user to change that value, a model access cell may show an arbitrary text based on the node's state and modify the node in an arbitrary way based on what changes the user has made to the cell's text.
While making a property cell work requires you only to specify a property to access via that cell, making a model access cell work requires a language designer to write three methods: "get," "set," and "validate." The latter two are somewhat optional.
A "get" method takes a node and should return a String, which will be shown as the cell's text. A "set" method takes a String - the cell's text - and should modify a node according to this String, if necessary. A "validate" method takes the cell's text and returns whether it is valid or not. If a text in a cell becomes invalid after a user change, then it is marked red and is not passed to the "set" method.
If a "validate" method is not specified, a cell will always be valid. If a "set" method is not specified, no changes in a cell's text will affect its node itself.
Custom cell
If other cell models are not enough for a language designer to create the editor he/she wants, there's one more option left for him/her: to create a cell provider which will return an arbitrary custom cell. The only restriction is that it should implement an "EditorCell" interface.

Editor Components and editor component cells

Sometimes two or more editor declarations for different concepts have a common part, which is duplicated in each of those editors. To avoid redundancy, there's a mechanism called editor components. You specify a concept for which an editor component is created and create a cell model, just as in concept editor declaration. When written, the component could then be used in editor declarations for any of the specified concept's descendants. To use an editor component inside your editor declarations, one will create a specific cell model: editor component cell model, and set your editor component declaration as the target of this cell model's reference.

Cell layouts

Each collection cell has property "cell layout", which describes how child nodes will be placed. There is several layouts:

  • indent layout - places cells like text.
  • horizontal layout - places cells horizontally in row.
  • vertical layout - places cells vertically.

Styles

Each cell model has some appearance settings that determine the cell's presentation. They are, for instance, font color, font style, whether a cell is selectable, and some others. Those settings are combined into an entity called stylesheet. A stylesheet could be either inline, i.e. be described together with a particular cell model, or it could be declared separately and used in many cell models.

It is good practice to declare a few stylesheets for different purposes. Another good practice is to have a style guideline in mind when developing an editor for your language, as well as when developing extensions for your language. For example, in Base Language there are styles for keywords (applied to those constant cells in Base Language editor which correspond to keywords in Java), static fields (applied to static field declarations and static field references), instance fields, numeric literals, string literals, and so forth. When developing an extension to Base Language, you should apply keyword style to new keywords, field style to new types of fields, and so forth.

A stylesheet is quite similar to CSS stylesheets; it consists of a list of style classes, in which the values for some style properties are specified. A style class may extend another style class: if some style property value is unspecified in a descendant class, its value will be inherited from the extended class.

Style properties

Boolean style properties

  • Is selectable - whether the cell can be selected. True by default.
  • Is editable - whether one can modify text in a cell or not. By default is false for constant cell models, true for other cell models.
  • Draw border - whether border will be drawn around a cell
  • Draw brackets - whether brackets will be drawn around a cell
  • First position allowed/last position allowed - for text-containing cells, specifies whether it is allowed that a caret is on the first/last position (i.e. before/after the whole text of a cell)
    You can either choose a property value from a completion menu or specify a query i.e. a function which returns a boolean value.

Padding properties.

  • Left/right padding. It's a floating point number which specifies the padding of a text cell, i.e. how much space will be between cell's text and cell's left and right sides, respectively.

Punctuation properties.

All cells in collection are separated with one space by default. Sometimes we need cells placed together.

  • Punctuation left - if this property is true, space from left side of the cell is deleted and first position in cell becomes not allowed.
  • Punctuation right - if this property is true, space from right side of the cell is deleted and last position in cell becomes not allowed.
  • Horizontal gap - specifies gap size between cells in collection. Default value is 1 space.

For example in code

we don't want spaces between "(" and "1", and between "1" and ")". So we should add property punctuation-right to the cell "(", and property
punctuation-left to the cell ")".

Color style properties

  • Text foreground color - cell text's color (affect text cells only)
  • Text background color - cell text's background color (affects text cells only)
  • Background color - the background color of a cell. Affects any cell. If a text cell has non-zero padding and some text background color, the cell's background color will be the color of its margins.
    You can either choose a color from the completion menu or specify a query i.e. a function which returns a color.

Indent layout properties

  • Indent layout indented - all lines will be placed with indent. This property can be used for indent in code block.


  • Indent layout new line - after this cell will be placed new line.


  • Indent layout on new line - this cell will be placed on new line
  • Indent layout children new line - all children of collection will be placed on new line


  • Indent layout no wrap - line won't be wrapped before this cell

Other style properties

  • Font size
  • Font style. Can be either plain, bold, italic, or bold italic.
  • Layout constraint.
    • For flow layout
      • none - default behavior
      • punctation - means that previous item in flow layout should always be placed on the same line as the item which this constraint is assigned to.
      • noflow - excludes a cell from flow layout. Current line is finished and item is placed below it. After this item a new line is started and normal flow layout is applied. This style can be used to embed a picture inside of text.
  • Underlined. Can be either underlined, not underlined, or as is ('as is' means it depends on properties of the enclosing cell collection).

Cell actions

Every cell model may have some actions associated with it. Such actions are meant to improve usability of editing. You can specify them in an inspector of any cell model.

Key maps

You may specify a reference to a key map for your cell model. A key map is a root concept - a set of key map items each consisting of a keystroke and an action to perform. A cell generated from a cell model with a reference to a certain key map will execute appropriate actions on keystrokes.

In a key map you must specify a concept for which a key map is applicable. For instance, if you want to do some actions with an expression, you must specify Expression as an applicable concept; then you may specify such a key map only for those cell models which are contained inside editor declarations for descendants of Expression, otherwise it is a type error.

If a key map property "everyModel" is "true," then this key map behaves as if it is specified for every cell in the editor. It is useful when you have many descendants of a certain concept which have many different editors, and your key map is applicable to their ancestor. You need not specify such a key map in every editor if you mark it as an "every model" key map.

A key map item consists of the following features:

  • A function which is executed when a key map item is triggered (returns nothing)
  • A set of keystrokes which trigger this key map item
  • A boolean function which determines if a key map item is applicable here (if not specified, then it's always applicable). If a key map item is not applicable the moment it is triggered, then it will not perform an action.
  • You may specify caret policy for a key map item. Caret policy says where in a cell a caret should be located to make this key map item enabled. Caret policy may be either first position, last position, intermediate position, or any position. By default, caret policy is "any position." If a caret in a cell does not match the caret policy of a key map item the moment it is triggered, then this key map item will not perform an action.

Action maps

A cell model may contain a reference to an action map. An action map overrides some default cell actions (delete and right transform) for a certain concept. An action map consists of several action map items. In an action map, you must specify a concept for which the action map is applicable.

An action map item contains:

  • an action description which is a string,
  • and a function which performs an action (returns nothing).

An action map item may override one of two default actions: default delete action or right transform (see Actions). For instance, when you have a return statement without any action maps in its editor, and you press Delete on a cell with the keyword "return," the whole statement is deleted. But you may specify an action map containing a delete action map item, which instead of just deleting return statement replaces it with an expression statement containing the same expression as the deleted return statement.

action DELETE description : <no description>
              execute : (node, editorContext)->void {
                           node < ExpressionStatement > expressionStatement = node . replace with new ( ExpressionStatement ) ;
                           expressionStatement . expression . set ( node . expression ) ;
                        }

Cell menus

One may specify a custom completion menu for a certain cell. Open an inspector for your cell declaration, find a table named Common, find a row named menu, create a new cell menu descriptor. Cell menu descriptor consists of menu parts, which are of differend kinds, which are discussed below.

Property values menu part

This menu part is available on property cells, it specifies a list of property values for your property which will be shown in completion. One should write a function which returns a value of type list<String>.

Property postfix hints menu part

This menu part is available on property cells, it specifies a list of strings which serve as "good" postfixes for your property value. In such a menu part one should write a function which returns a value of type list<String>. Such a menu is useful if you want MPS to "guess" a good value for a property. For instance, one may decide that it will be a good variable name which is a variable type name but with the first letter being lowercased, or which ends with its type name: for a variable of type "Foo" good names will be "foo", "aFoo", "firstFoo", "goodFoo", etc. So one should write in a variable declaration's editor in a menu for property cell for variable name such a menu part:

property postfix hints
   postfixes : (scope, operationContext, node)->list<String> {
                  list < String > result ;
                  node < Type > nodeType = node . type ;
                  if ( nodeType != null ) {
                     result = MyUtil.splitByCamels( nodeType . getPresentation() );
                  } else {
                     result = new list < String > { empty } ;
                  }
                  return  result ;
               }

where splitByCamels() will be a function which returns a list of postfixes of a string starting with capitals (for instance MyFooBar -> MyFooBar, FooBar, Bar).

Primary replace child menu

It's a cell menu part which returns primary actions for child (those by default, as if no cell menu exists).

Primary choose referent menu

It's a cell menu part which returns primary actions for referent (those by default, as if no cell menu exists).

Replace node menu (custom node's concept)

This kind of cell menu parts allows to replace an edited node (i.e. node on which a completion menu is called) with instances of a certain specified concept and its subconcepts. Such a cell menu part is useful, for example, when you want a particular cell of your node's editor to be responsible for replacement of a whole node. For instance, consider an editor for binary operations. There's a common editor for all binary operations which consists of a cell for left operand, a cell for operation sign which is a cell for concept property "alias" and a cell for right operand.

[> % leftExpression % ^{{ alias }} % rightExpression % <]

It is natural to create a cell menu for a cell with operation sign, which will allow to replace an operation sign with another one, (by replacing a whole node of course). For such a purpose one will write in the cell for operation sign a replace node menu part:

replace node (custom node concept)
   replace with : BinaryOperation

The former left child and right child are added to newly created BinaryOperation according to Node Factories for BinaryOperation concept.

Replace child menu (custom child's concept)

Such a cell menu part is applicable to a cell for a certain child and specifies a specific concept which and subconcepts of which will be shown in completion menu (and instantiated when chosen and the instance will be set as a child). To specify that concept one should write a function which returns a value of a type node<ConceptDeclaration>.

Replace child menu (custom action).

This kind of cell menu parts is applicable to a cell for a certain child and allows one to customize not only child concept, but the whole replace child action: matching text (text which will be shown in completion menu), description text (a description of an action, shown in the right part of completion menu), and the function which creates a child node when the action is selected from completion menu. Hence, to write such a menu one should specify matching text, description text and write a function returning a node (this node should be an instance of a target concept specified in a respective child link).

Generic menu item

This kind of cell menu part allows one to make MPS perform an arbitrary action when a respective menu item will be selected in a completion menu. One should specify matching text for a menu item and write a function which does what one wants. For instance, one may not want to show a child list cell for class fields if no class fields exist. Hence one can't use its default actions to create a new field. Instead, one can create somewhere in a class' editor a generic menu item with matching text "create field" which creates a new field for a class.

generic item
   matching text : add field
   handler : (node, model, scope, operationContext)->void {
                node . field . add new ( <default> ) ;
             }

Action groups

An action group is a cell menu part which returns a group of custom actions. At runtime, during the menu construction, several objects of a certain type, which are called parameter objects, are collected or created. For that parameter object type of an action group functions, which return their matching text and description text, are specified. A function which is triggered when a menu item with a parameter object is chosen is specified also.

Thus, an action group description consists of:

  • a parameter object type;
  • a function which returns a list of parameter objects of a specified type (takes an edited node, scope and operation context);
  • a function which takes a parameter object of a specified type and returns matching text (a text which will be shown in a completion menu);
  • a function which takes a parameter object of a specified type and returns description text for a parameter object;
  • a function which performs an action when parameter object is chosen in a completion menu.

A function which performs an action may be of different kinds, so there are three different kinds of cell action group menu parts:

  • Generic action group. Its action function, given a parameter object, performs an arbitrary action. Besides the parameter object, the function is provided with edited node, its model, scope and operation context.
  • Replace child group. It is applicable to child cells and its action function, given a parameter object, returns a new child, which must have a type specified in a respective child link declaration. Besides the parameter object, the function is provided with edited node, its model, current child(i.e. a child being replaced), scope and operation context.
  • Replace node group. Its action function, given a parameter object, returns a node. Usually it is some referent of an edited node (i.e. node on which a completion menu is called). Besides the parameter object, the function is provided with edited node, its model, scope and operation context.

Cell menu components

When some menu parts in different cells are equal one may want to extract them into a separate and unique entity, to avoid duplications. For such a purpose cell menu components are meant. A cell menu component consists of a cell menu descriptor (a container for cell menu parts) and a specification of an applicable feature. A specification of applicable feature contains a reference to a feature (i.e. child link declaration, reference link declaration or property declaration), to which a menu is applicable. For instance if your menu component will be used to replace some child its child link declaration should be specified here; etc.

When a cell menu component is created, it can be used in cell menus via cell menu component menu part, which is a cell menu part which contains a reference to a certain menu component.SModel language

Previous Next

Actions

MPS editor has quite sensible defaults in completion actions, node creation policy. But when you want to customize them, you have to work with the actions language. This language is also used to define Left and Right Transform actions (RT/LT-actions for short), which allows editing binary operations in a text-like way.

Substitute Actions

Substitute actions are actions which are available when you press Ctrl+Space in the editor. MPS has the following default behavior for them:

  • If your selection is inside of a position which allows concept A, then all enabled subconcepts of A will be available in the completion menu.
  • All abstract concepts are excluded
  • All concepts with dontSubstituteByDefault property are removed
  • All concepts for which the 'can be a child' constraint returns false are excluded
  • All concepts for which the 'can be a parent' constraint of a parent node returns false are excluded
  • If a concept has a 1:1 reference, then it is not added to the completion menu. Instead, an item is added for each element of a scope for that reference. We use a name smart reference for such items.

When you want to customize this behavior, you have to create a node substitute actions root. Inside it you can create substitute actions builders. Each of them has a substitute node concept, where you type a name of a concept which you want to substitute. It has a condition; when this condition is satisfied, your actions will be added to the completion menu. Also it has an actions part where action behavior is specified. Each action has an associated concept which can be used for action filtering (see remove by condition).

Add concept

If a concept isn't available because of default settings, you can add it with the 'add concept' construct.

Remove defaults

Use this construct if you want to completely override defaults. It removes all default actions and adds only those actions which are specified in the actions language.

Remove By Condition

Use this construct if you want to remove only some of the actions. It can be useful when you extend a language and want to remove some of its actions in a particular context.

Custom items

If you aren't satisfied with default items, you can create your own. In this case you can override everything: output concept, matching text, description text, icon, and behavior on item invocation. When you create custom items, you have to specify output concept so it can be used to filter out your actions from extending language.

Simple

Simple item adds one item to substitute menu. You can specify all the properties of substitute action (matching text, description, icon etc). It can be useful for entering literals (boolean, integer, float, string, char).

Parametrized

This concept allows you to create an item in substitute menu based on a query. A query should return a list of something. For example, if you want to create completion for files in a directory, you can use it. This concept is similar to simple item but it has additional parameter parameterObject in all of its blocks.

Wrapper

Sometimes we want to add all the completion items from a context of one type of concepts into a context of other concepts. Let's consider a couple of examples from baseLanguage. For example, we want to see all available items for expression in statement's context, since we may wrap all of them in ExpressionStatement. Or we can add all items of Type's completion menu, since we can create a local variable declaration. Wrapper blocks has a concept from whose context we want to add completion items. It also has a wrapper block with a nodeToWrap parameter, which the author of wrapper block should wrap.

Concepts Menu

Sometimes you want to add items for subconcepts of a particular item but want to override its handler. Concepts menu block allows you to do so.

Generic

If the above actions are not suitable, you can resort to generic item. It has a block which returns a list of INodeSubstitueAction items.

Side Transform Actions

When you edit code in a text editor, you can type it either from left to right:

1 <caret> press +
1+<caret> press 2
1+2<caret>

or from right to left

<caret>1 press 2
2<caret>1 press +
2+<caret>1

In order to emulate this behavior, MPS has side transform actions: left and right transforms. They allow you to create actions which will be available when you type on left or right part of your cell. For example, in MPS you can do the following:

1<caret> press + (red cell with + inside appears)
1+<caret> press 2 (red cell disappear)
1+2<caret>

or the following:

<caret>1 press + (red cell with + inside appears)
+<caret>1 press 2 (red cell disappear)
2<caret>+1

The first case is called right transform. The second case is called left transform.

In order to create transformation actions you have to create transform menu actions root. Inside it, you can create transform action builders. You can specify the following:

  • whether it is left or right transform
  • a concept which instance you want to transform
  • a condition which defines where your actions will be applicable

Add custom items

Custom items is similar to its counterpart in substitute actions part of a language. It allows you to add either one or many items to menu. Let's consider them in detail.

Simple item

Simple item adds an item with matching text, description, icon, and substitute handler.

Parametrized item.

Parametrized item adds a group of items based on a query which returning a list of something. It's similar to simple item but has additional parameterObject parameter in every block.

Add concept

Add concept adds an item for every non-abstract subconcept of a specified concept. This item has a handler block where you can replace sourceNode with a newly created node. For example, this is useful when you want to create a transformation for each subconcept of a BinaryOperation, such as +, -, *, or /. The code for replacing sourceNode is the same in each of these cases - the only difference is the resulting concept.

Include transform for

This construct allows you to include all the right transform action associated with a particular node.

Remove By Condition

This allows you to filter actions in case of a language extension.

Remove Concept

This allows you remove all the actions associated with a particular concept.

Node Factories


When you have a baseLanguage expression selected, press Ctrl+Space on it and choose (expr). You will have your expressions surrounded by parenthesis. Node factories allows you to implement this and similar functionality by customizing instantiation of a new node. In order to create node factory, you first have to create a new node factories root node. Inside of this root you can create node factories for concepts. Each node factory consists of node creation block which has the following parameters: newNode (created node), sampleNode (currently substituted node; can be null), enclosing node (a node which will be a parent of newNode), and a model.

Previous Next

Generator User Guide

Introduction

Generator is a part of language specification that defines the denotational semantics for a language's concepts.

MPS follows the model-to-model transformation approach. Generator specifies the translation of constructions in the input language to constructions in the output language. The process of model-to-model transformation may involve many intermediate models and results in the output model where all constructions are in language whose semantics are already defined elsewhere.

For instance, most concepts in baseLanguage (classes, methods etc) are "machine understandable", wherefore baseLanguage is often used as an output language.

Target assets are created by applying model-to-text transformation, which must be supported by the output language. The language aspect for that is called TextGen and is available as a separate tab in concept's editor. MPS provides destructive update of generated assets only.

For instance, baseLanguage's TextGen aspect generates *.java files at the following location:
<generator output path>\<model name>\<ClassName>.java
where:
Generator output path - is specified in the module which owns the input model (see MPS modules).
Model name - is a path segment created by replacing '.' with the file separator in the input model's name.

Overview

Generator Module

Unlike any other language aspect, the generator aspect is not a single model. Generator specification can comprise many generator models as well as utility models. Generator model contains templates, mapping configurations and other constructions of the generator language.

Generator model is distinguished from a regular model by the model stereotype - 'generator' (shown after the model name as <name>@generator).
The screenshot below shows the generator module of the smodel language as an example.

Research bundled languages yourself
You can research the smodel (and any other) language's generator by yourself:
  • download MPS (here);
  • create new project (can be empty project);
  • use the Go To -> Go to Language command in the main menu to navigate to the smodel language (its full name is jetbrains.mps.lang.smodel)

Creating a New Generator

New generator is created by using the New -> Generator command in the language's popup menu.

Technically, it is possible to create more than one generator for one language, but at the time of writing MPS does not provide full support for this feature. Therefore languages normally have only one (if any) generator. For that reason, the generator's name is not important. Everywhere in the MPS GUI a generator module can be identified by its language name.

When creating a new generator module, MPS will also create the generator model 'main@generator' containing an empty mapping configuration node.

Generator Properties

As a module, generator can depend on other modules, have used languages and used devkits (see Module meta-information).

The generator properties dialog also has two additional properties:

Generating Generator

MPS generator engine (or the Generator language runtime) uses mixed compilation/interpretation mode for transformation execution.

Templates are interpreted and filled at runtime, but all functions in rules, macros, and scripts must be pre-compiled.

To avoid any confusion, always follow this rule: after any changes made to the generator model, the model must be re-generated (Shift+F9). Even better is to use Ctrl+F9, which will re-generate all modified models in the generator module.

Transformation

The transformation is described by means of templates. Templates are written using output language and are edited using the same cell editor as any other 'regular code' in that language. Therefore, the 'template editor' right away has the same level of tooling support - syntax/error highlighting, auto-completion, etc.

Templates applicability is defined using Generator Rules, which are grouped into Mapping Configurations.

Mapping Configurations

Mapping Configuration is a minimal unit, which can form a generation step. It contains Generator Rules, defines mapping labels and may include pre- and post-processing scripts.

Generator Rules

Applicability of each transformation is defined by generator rules.
There are six types of generator rules:

  • conditional root rule
  • root mapping rule
  • weaving rule
  • reduction rule
  • pattern rule
  • abandon root rule

Each generator rule consists of premise and consequence (except for the abandon root rule, whose consequence is predefined and is not specified by the user).

All rules except for the conditional root rule contain a reference on concept of input node (or just input concept) in its premises. All rule premises also contain an optional condition function.

Rule consequence commonly contains a reference to an external template (i.e. a template declared as a root node in the same or different model) or so-called in-line template (conditional root rule and root mapping rule can only have reference to an external template). There are also several other versions of consequences.

The following screenshot shows the contents of a generator model and a mapping configuration example.

Macros

Template code can be parameterized by means of macros. The generator language defines three kinds of macros:

  • property macro - computes property value;
  • reference macro - computes the target (node) of a reference;
  • node macro - is used to control template filling at generation time. There are several versions of node macro - LOOP-macro is an example.

Macro implements a special kind of so-called annotation concept and can wrap property, reference or node cells (depending on the kind of macro) in a template code.

Code wrapping (i.e. the creation of a new macro) is done by pressing Ctrl+Shift+M or by applying the 'Create macro' intention.

The following screenshot shows an example of a property macro.

Macro functions and other parameterization options are edited in the inspector view. Property macro, for instance, requires specifying the value function, which will provide the value of the property at generation time. In the example above, output class node will get the same name that the input node has.

The node parameter in all functions of the generator language always represents the context node to which the transformation is currently being applied (the input node).

Some macros (such as LOOP and SWITCH-macro) can replace the input node with a new one, so that subsequent template code (i.e. code that is wrapped by those macros) will be applied to the new input node.

External Templates

External templates are created as a root node in the generator model.

There are two kinds of external templates in MPS.

One of them is root template. Any root node created in generator model is treated as a root template unless this node is a part of the generator language (i.e. mapping configuration is not a root template). Root template is created as a normal root node (via Create Root Node menu in the model's popup).

The following screenshot shows an example of a root template.

This root template will transform input node (a Document) into a class (baseLanguage). The root template header is added automatically upon creation, but the concept of input node is specified by the user.

It is good practice to specify the input concept, because this allows MPS to perform a static type checking in a macro function's code.

Root template (reference) can be used as a consequence in conditional root rule and root mapping rule. ( When used in a conditional root rule, the input node is not available).

The second kind of template is defined in the generator language and its concept name is 'TemplateDeclaration'. It is created via the 'template declaration' action in the Create Root Node menu.

The following screenshot shows an example of template declaration.

The actual template code is 'wrapped' in a template fragment. Any code outside template fragment is not used in transformation and serves as a context (for example you can have a Java class, but export only one of its method as a template).

Template declaration can have parameters, declared in the header. Parameters are accessible through the generation context.

Template declaration is used in consequence of weaving, reduction and pattern rules. It is also used as an included template in INCLUDE-macro (only for templates without parameters) or as a callee in CALL-macro.

Template Switches

A template switch is used when two or more alternative transformations are possible in a certain place in template code. In that case, the template code that allows alternatives is wrapped in a SWITCH-macro, which has reference to a Template Switch. Template Switch is created as a root node in generator model via the Create Root Node menu (this command can be seen on the 'menu' screenshot above).

The following screenshot shows an example of a template switch.


Generator Language Reference

Mapping Configuration

Mapping Configuration is a container for generator rules, mapping label declarations and references on pre- and post-processing scripts. A generator model can contain any number of mapping configurations - all of them will be involved in generation process if the owner generator module is involved. Mapping configuration is a minimal generator unit that can be referenced in mapping priority rules (see Generation Process: Defining the Order of Priorities).

Generator Rule

Generator Rule specifies a transformation of an input node to an output node (except for the conditional root rule which doesn't have an input node). All rules consist of two parts - premise and consequence (except for the abandon root rule which doesn't have a consequence). Any generator rule can be tagged by a mapping label.

All generator rules functions have following parameters:

  • node - current input node (all except condition-function in conditional root rule)
  • genContext - generation context - allows searching of output nodes, generating of unique names and others (see generation context)

Generator Rules:

Rule Description Premise Consequence
conditional root rule Generates root node in output model. Applied only one time (max) during the generation step. condition function (optional), missing condition function is equivalent to a function always returning true. root template (ref)
root mapping rule Generates a root node in output model. concept - applicable concept (concept of input node)
inheritors - if true then the rule is applicable to the specified concept and all its sub-concepts. If false (default) then the sub-concepts are not applicable.
condition function (optional) - see conditional root rule above.
keep input root - if false then the input root node (if it's a root node) will be dropped. If true then input root will be copied to output model.
root template (ref)
weaving rule Allows to insert additional child nodes into output model. Weaving rules are processed at the end of generation micro-step just before map_src and reference resolving. Rule is applied on each input node of specified concept. Parent node for insertion should be provided by context function.
(see Model Transformation)
concept - same as above
inheritors - same as above
condition function (optional) - same as above
  • external template (ref)
  • weave-each
    context function - computes (parent) output node into which the output node(s) generated by this rule will be inserted.
reduction rule Transforms input node while this node is being copied to output model. concept - same as above
inheritors - same as above
condition function (optional) - same as above
  • external template (ref)
  • in-line template
  • in-line switch
  • dismiss top rule
  • abandon input
pattern rule Transforms input node, which matches pattern. pattern - pattern expression
condition function (optional) - same as above
  • external template (ref)
  • in-line template
  • dismiss top rule
  • abandon input
abandon root rule Allows to drop an input root node with otherwise would be copied into output model. applicable concept ( including all its sub-concepts)
condition function (optional) - same as above
n/a

Rule Consequences:

Consequence Usage Description
root template (ref)
  • conditional root rule
  • (root) mapping rule
Applies root template
external template (ref)
  • weaving rule
  • reduction rule
  • pattern rule
Applies an external template. Parameters should be passed if required, can be one of:
  • pattern captured variable (starting with # sign)
  • integer or string literal
  • null, true, false
  • query function
weave-each weaving rule Applies an external template to a set of input nodes.
Weave-each consequence consists of:
  • foreach function - returns a sequence of input nodes
  • reference on an external template
in-line template
  • reduction rule
  • pattern rule
Applies the template code which is written right here.
in-line switch reduction rule Consists of set of conditional cases and a default case.
Each case specify a consequence, which can be one of:
  • external template (ref)
  • in-line template
  • dismiss top rule
  • abandon input
dismiss top rule
  • reduction rule
  • pattern rule
Drops all reduction-transformations up to the point where this sequence of transformations has been initiated by an attempt to copy input node to output model. The input node will be copied 'as is' (unless some other reduction rules are applicable). User can also specify an error, warning or information message.
abandon input
  • reduction rule
  • pattern rule
Prevents input node from being copied into output model.

Root Template

Root Template is used in conditional root rules and (root) mapping rules. Generator language doesn't define specific concept for root template. Any root node in output language is treated as a Root Template when created in generator model. The generator language only defines a special kind of annotation - root template header, which is automatically added to each new root template. The root template header is used for specifying of an expected input concept (i.e. concept of input node). MPS use this setting to perform a static type checking in a code in various macro-functions which are used in the root template.

External Template

External Template is a concept defined in the generator language. It is used in weaving rules and reduction rules.

In external template user specifies the template name, input concept, parameters and a content node.

The content node can be any node in output language. The actual template code in external templates is surrounded by template fragment 'tags' (the template fragment is also a special kind of annotation concept). The code outside template fragment serves as a framework (or context) for the real template code (template fragment) and is ignored by the generator. In external template for weaving rule, the template's context node is required (it is a design-time representation of the rule's context node), while template for reduction rule can be just one context-free template fragment. External template for a reduction rule must contain exactly one template fragment, while a weaving rule's template can contain more than one template fragments.

Template fragment has following properties (edited in inspector view):

  • mapping label
  • fragment context - optional function returning new context node which will replace main context node while applying the code in fragment. Can only be used in weaving rules.

Mapping Label

Mapping Label is declared in mapping configuration and references on this declaration are used to label generator rules, macros and template fragments. Such marks allow finding of an output node by input node (see generation context).

Properties:

  • name
  • input concept (optional) - expected concept of input node of transformation performed by the tagged rule, macro or template fragment
  • output concept (optional) - expected concept of output node of transformation performed by the tagged rule, macro or template fragment

MPS makes use of the input/output concept settings to perform static type checking in get output ... operations (see generation context).

Macro

Macro is a special kind of an annotation concept which can be attached to any node in template code. Macro brings dynamic aspect into otherwise static template-based model transformation.

Property- and reference-macro is attached to property- and reference-cell, and node-macro (which comes in many variations - LOOP, IF etc.) is attached to a cell representing the whole node in cell-editor. All properties of macro are edited using inspector view.

All macro have the mapping label property - reference on a mapping label declaration. All macro can be parameterized by various macro-functions - depending on type of the macro. Any macro-function has at least three following parameters:

  • node - current input node;
  • genContext - generation context - allows searching of output nodes, generating of unique names and others;
  • operationContext - instance of jetbrains.mps.smodel.IOperationContext interface (used rarely).

Many of macro have mapped node or mapped nodes function. This function computes new input node - substitution for current input node. If mapped node function returns null or mapped nodes function returns an empty sequence, then generator will skip this macro altogether. I.e. no output will be generated in this place.

Macro Description Properties (if not mentioned above)
Property macro Computes value of property. value function:
  • return type - string, boolean or int - depending on the property type.
  • parameters - standard + templateValue - value in template code wrapped by the macro.
Reference macro Computes referent node in output model.
Normally executed in the end of a generation micro-step, when the output model (tree) is already constructed.
Can also be executed earlier if a user code is trying to obtain the target of the reference.
referent function:
  • return type - node (type depends on the reference link declaration) or, in many cases, string identifying the target node (see note).
  • parameters - standard + outputNode - source of the reference link (in output model).
IF Wrapped template code is applied only if condition is true. Otherwise the template code is ignored and an 'alternative consequence' (if any) is applied. condition function
alternative consequence (optional) - any of:
  • external template (ref)
  • in-line template
  • abandon input
  • dismiss top rule
LOOP Computes new input nodes and applies the wrapped template to each of them. mapped nodes function
INCLUDE Wrapped template code is ignored (it only serves as an anchor for the INCLUDE-macro), a reusable external template will be used instead. mapped node function (optional)
include template - reference on reusable external template
CALL Invokes template, replaces wrapped template code with the result of template invocation. Supports templates with parameters. mapped node function (optional)
call template - reference on reusable external template

argument - one of
  • pattern captured variable
  • integer or string literal
  • null, true, false
  • query function
SWITCH Provides a way to many alternative transformations in the given place in template code.
The wrapped template code is applied if none of switch cases is applicable and no default consequence is specified in template switch.
mapped node function (optional)
template switch - reference on template switch
COPY-SRC Copies input node to output model. The wrapped template code is ignored. mapped node function - computes the input node to be copied.
COPY-SRCL Copies input nodes to output model. The wrapped template code is ignored.
Can be used only for children with multiple aggregation cardinality.
mapped nodes function - computes the input nodes to be copied.
MAP-SRC Multifunctional macro, can be used for:
  • marking a template code with mapping label;
  • replacing current input node with new one;
  • perform none-template based transformation;
  • accessing output node for some reason.
    MAP-SRC macro is executed in the end of generator micro-step - after all node- and property-macro but before reference-macro.
mapped node function (optional)
mapping func function (optional) - performes none-template based transformation.
If defined then the wrapped template code will be ignored.
Parameters: standard + parentOutputNode - parent node in output model.
post-processing function (optional) - give an access to output node.
Parameters: standard + outputNode
MAP-SRCL Same as MAP-SRC but can handle many new input nodes (similar to LOOP-macro) mapped nodes function
mapping func function (optional)
post-processing function (optional)

Note
Reference resolving by identifier is only supported in BaseLanguage.
The identifier string for classes and class constructors may require (if class is not in the same output model) package name in square brackets preceding the class name:
[package.name]ClassName

Template Switch

Template switch is used in pair with SWITCH-macro (can be re-used in many different SWITCH-macro). Template switch consists of set of cases and one default case. Each switch case is a reduction rule, i.e. template switch contains list of reduction rules actually (see reduction rule).

The default case consequence can be one of:

  • external template (ref)
  • in-line template
  • abandon input
  • dismiss top rule
    .. or can be omitted. In this case the template code surrounded by corresponding SWITCH-macro will be applied.

Template switch can inherit reduction rules from other switches via the extends property. When generator is executing a SWITCH-macro it tries to find most specific template switch (available in scope). Therefore the actually executed template switch is not necessarily the same as it is defined in template switch property in SWITCH-macro.

In the null-input message property user can specify an error, warning or info message, which will be shown in MPS messages view in case when the mapped node function in SWITCH-macro returns null (by default no messages are shown and macro is skipped altogether).

Generation Context (operations)

Generation context (genContext parameter in macro- and rule-functions) allows finding of nodes in output model, generating unique names and provides other useful functionality.

Generation context can be used not only in generator models but also in utility models - as a variable of type gencontext.

Operations of genContext are invoked using familiar dot-notation: genContext.operation

Finding Output Node

get output <mapping label> Returns output node generated by labeled conditional root rule.
Issues an error if there are more than one matching output nodes.
get output <mapping label> for ( <input node> ) Returns output node generated from the input node by labeled generator rule, macro or template fragment.
Issues an error if there are more than one matching output nodes.
pick output <mapping label> for ( <input node> ) only used in context of the referent function in reference-macro and only if the required output node is target of reference which is being resolved by that reference-macro.
Returns output node generated from the input node by labeled generator rule, macro or template fragment. Difference with previous operation is that this operation can automatically resolve the many-output-nodes conflict - it picks the output node which is visible in the given context (see search scope).
get output list <mapping label> for ( <input node> ) Returns list of output nodes generated from the input node by labeled generator rule, macro or template fragment.
get copied output for ( <input node> ) Returns output node which has been created by copying of the input node. If during the copying, the input node has been reduced but concept of output node is the same (i.e. it wasn't reduced into something totally different), then this is still considered 'copying'.
Issues an error if there are more than one matching output nodes.

Generating of Unique Name

unique name from <base name> in context <node>

The uniqueness is secured throughout the whole generation session.
Clashing with names that wasn't generated using this service is still possible.

The context node is optional, though we recommend to specify it to guarantee generation stability. If specified, then MPS tries its best to generated names 'contained' in a scope (usually a root node). Then when names are re-calculated (due to changes in input model or in generator model), this won't affect other names outside the scope.

Template Parameters

#patternvar Value of captured pattern variable
available only in rule consequence
param Value of template parameter
available only in external template

Getting Contextual Info

inputModel Current input model
originalModel Original input model
outputModel Current output model
invocation context Operation context (jetbrains.mps.smodel.IOperationContext java interface) associated with module - owner of the original input model
scope Scope - jetbrains.mps.smodel.IScope java interface
templateNode Template code surrounded by macro.
Only used in macro-functions
get prev input <mapping label> Returns input node that has been used for enclosing template code surrounded by the labeled macro.
Only used in macro-functions.

Transferring User Data

During a generation MPS maintains three maps user objects, each have different life span:

  • session object - kept throughout whole generation session;
  • step object - kept through a generation step;
  • transient object - only live during a micro step.

Developer can access user object maps using an array (square brackets) notation:

The key can be any object (java.lang.Object).

binding user data with particular node
The session- and step-object cannot be used to pass a data associated with a particular input node across steps and micro-steps because neither an input node nor its id can serve as a key (output nodes always have different id).
To pass such data use methods: putUserObject, getUserObject and removeUserObject defined in class jetbrains.mps.smodel.SNode.
The data will be transferred to all output copies of the input node. The data will be also transferred to output node if a slight reduction (i.e. without changing of the node concept) took place while the node copying.

Logging

Creates message in MPS message view. If the node parameter is specified then clicking on the message will navigate to that node. In case of an error message, MPS will also output some additional diagnostic info.

Utilities (Re-usable Code)

If you have duplicated code (in rules, macros, etc.) and want to say, extract it to re-usable static methods, then you must create this class in a separate, non-generator model.

If you create an utility class in the generator model (i.e. in a model with the 'generator' stereotype), then it will be treated as a root template (unused) and no code will be generated from it.

Mapping Script

Mapping script is a user code which is executed either before a model transformation (pre-processing script) or after it (post-processing script). It should be referenced from Mapping Configuration to be invoked as a part of it's generation step. Mapping script provides the ability to perform a non-template based model transformation.

Pre-processing scripts are also commonly used for collecting certain information from input model that can be later used in the course of template-based transformation. The information collected by script is saved as a transient-, step- or session-object (see generation context).

Script sample:

Properties:

script kind
  • pre-process input model - script is executed in the beginning of generation step, before template-based transformation;
  • post-process output model - script is executed at the end of generation step, after template-based transformation.
modifies model only available if script kind = pre-process input model
If set true and input model is the original input model, then MPS will create a transient input model before applying the script.
If set false but script tries to modify input model, then MPS will issue an error.

Code context:

model Current model
genContext Generation context to access transient/session or step objects.
invocation context Operation context (jetbrains.mps.smodel.IOperationContext java interface) associated with module - owner of the original input model

The Generator Algorithm

The process of generation of target assets from an input model (generation session) includes 5 stages:

  • Defining all generators that must be involved
  • Defining the order of priorities of transformations
  • Step-by-step model transformation
  • Generating text and saving it to a file (for each root in output model)
  • Post-processing assets: compiling, etc.

We will discuss the first three stages of this process in detail.

Defining the Generators Involved

To define the required generators, MPS examines the input model and determines which languages are used in it. Doing this job MPS doesn't make use of 'Used Languages' specified in the model properties dialog. Instead MPS examines each node in the model and gathers languages that are actually used.

From each 'used language' MPS obtains its generator module. If there are more than one generator module in a language, MPS chooses the first one (multiple generators for the same language are not fully supported in the current version of MPS). If any generator in this list depends on other generators (as specified in the 'depends on generators' property), then those generators are added to the list as well.

After MPS obtains the initial list of generators, it begins to scan the generator's templates in order to determine what languages will be used in intermediate (transient) models. The languages detected this way are handled in the same manner as the languages used in the original input model. This procedure is repeated until no more 'used languages' can be detected.

Explicit Engagement

In some rare cases, MPS is unable to detect the language whose generator must be involved in the model transformation. This may happen if that language is not used in the input model or in the template code of other (detected) languages. In this case, you can explicitly specify the generator engagement via the Languages Engaged on Generation section in the input model's properties dialog (Advanced tab).

Defining the Order of Priorities

As we discussed earlier, a generator module contains generator models, and generator models contain mapping configurations. Mapping configuration (mapping for short) is a set of generator rules. It is often required that some mappings must be applied before (or not later than, or together with) some other mappings. The language developer specifies such a relationship between mappings by means of mapping constraints in the generator properties dialog (see also Mapping Priorities and the Dividing Generation Process into Steps demo).

After MPS builds the list of involved generators, it divides all mappings into groups, according to the mapping priorities specified. All mappings for which no priority has been specified fall into the last (least-priority) group.

tip
You can check the mapping partitioning for any (input) model by selecting Show Mapping Partitioning action in the model's popup menu.
The result of partitioning will be shown in the MPS Output View.

Model Transformation

Each group of mappings is applied in a separate generation step. The entire generation session consists of as many generation steps as there were mapping groups formed during the mapping partitioning. The generation step includes three phases:

  • Executing pre-mapping scripts
  • Template-based model transformation
  • Executing post-mapping scripts

The template-based model transformation phase consists of one or more micro-steps. The micro-step is a single-pass model transformation of input model into a transient (output) model.

While executing micro-step MPS follows the next procedure:

  1. Apply conditional root rules (only once - on the 1-st micro-step)
  2. Apply root mapping rules
  3. Copy input roots for which no explicit root mapping is specified (this can be overridden by means of the 'keep input root' option in root mapping rules and by the 'abandon root' rules)
  4. Apply weaving rules
  5. Apply delayed mappings (from MAP_SRC macro)
  6. Revalidate references in the output model (all reference-macro are executed here)

There is no separate stage for the application of reduction and pattern rules. Instead, every time MPS copies an input node into the output model, it attempts to find an applicable reduction (or pattern) rule. MPS performs the node copying when it is either copying a root node or executing a COPY_SRC-macro. Therefore, the reduction can occur at either stage of the model transformation.

MPS uses the same rule set (mapping group) for all micro-steps within the generation step. After a micro-step is completed and some transformations have taken place during its execution, MPS starts the next micro-step and passes the output model of the previous micro-step as input to the next micro-step. The whole generation step is considered completed if no transformations have occurred during the execution of the last micro-step, that is, when there are no more rules in the current rule set that are applicable to nodes in the current input model.

The next generation step (if any) will receive the output model of previous generation step as its input.

tip
Intermediate models (transient models) that are the output/input of generation steps and micro-steps are normally destroyed immediately after their transformation to the next model is completed.
To keep transient models, enable the following option:
Settings -> Generator Settings -> Save transient models on generation

See also:


Mapping Priorities

Mapping priorities are a set of mapping priority rules specifying an order of priority between sets of generator rules (mapping configurations).

Mapping priorities are specified in generator module property dialog.
See also: Generator Properties, Generation Process: Defining the Order of Priorities, Demo 6: Dividing Generation Process into Steps

Each mapping priority rule consists of left part, right part and priority symbol in the middle.
For instance:

Left part of priority rule can only refer to mapping configurations in this generator.

Notation Refers to
* All mappings in this generator
modelName . * All mappings in model
modelName . mappingName Mapping configuration
{ ... } Set of mappings.
An entry can be either
modelName . * or
modelName . mappingName

Right part of priority rule can refer to mapping configurations in this generator as well as other generators visible in scope (see Generator Properties: dependency on other generators).

Notation Refers to
* All mappings in this generator
modelName . * All mappings in model
modelName . mappingName Mapping configuration
{ ... } Set of mappings.
An entry can be either
modelName . * or
modelName . mappingName
[languageName / generatorName : * ] All mapping in external generator
[languageName / generatorName : modelName] All mapping in external generator model
[languageName / generatorName : modelName . mappingName] External mapping configuration
[languageName / generatorName : { ... }] All mapping in external generator
[*:*] All possible mappings

Priority symbol

Notation Description
< Mappings in left part are processed before mappings in right part
<= Mappings in left part are processed not later than mappings in right part
== Mappings in left part and right part are processed together

Examples

If you're feeling like it's time for more practical experience, check out the generator demos.
The demos contain examples of usage of all concepts discussed above.


Previous Next

Defining A Typesystem For Your Language

What is a typesystem?

A typesystem is a part of a language definition used to assign types to the nodes in the models written in your language. The typesystem language is also used to check certain constraints on nodes and their types. Information about types of nodes is useful for:

  • finding type errors
  • checking conditions on nodes' types during generation to apply only appropriate generator rules
  • providing information required for certain refactorings (e.g. for the "extract variable" refactoring)
  • and more

Types

Any MPS node may serve as a type. To enable MPS to assign types to nodes of your language, you should create a language aspect for typesystem. The typesystem model for your language will be written in the typesystem language.

Inference Rules

The main concept of the typesystem language is an inference rule. An inference rule for a certain concept is mainly responsible for computing a type for instances of that concept.

An inference rule consists of a condition and a body. A condition is used to determine whether a rule is applicable to a certain node. A condition may be of two kinds: a concept reference or a pattern. A rule with a condition in the form of concept reference is applicable to every instance of that concept and its subconcepts. A rule with a pattern is applicable to nodes that match the pattern. A node matches a pattern if it has the same properties and references as the pattern, and if its children match the pattern's children. A pattern also may contain several variables which match everything.

The body of an inference rule is a list of statements which are executed when a rule is applied to a node. The main kind of statements of typesystem language are statements used for creating equations and inequations between types.

Inference Methods

To avoid duplications, one may want to extract identical parts of code of several inference rules to a method. An inference method is just a simple Base Language method marked with an annotation "@InferenceMethod". There are several language constructions you may use only inside inference rules and replacement rules and inference methods, they are: "typeof" expressions, equations and inequations, "when concrete" statements, type variable declarations and type variable references, and invocations of inference methods. That is made for not to use such constructions in arbitrary methods which may be called in arbitrary context, maybe not during type checking.

Equations And Inequations

The main process which is performed by typesystem engine is the process of solving equations and inequations between types. A language designer tells the engine which equations it should solve by writing them in inference rules. To add an equation into an engine, the following statement is used: expr1 :==: expr2, where expr1 and expr2 are expressions which evaluate to a node.

Consider the following use case. You want to say that the type of a local variable reference is equal to a type of variable declaration. So, you write typeof (varRef) :==: typeof (varRef.localVariableDeclaration), and that's all. The typesystem engine will solve such equations automatically.

The above-mentioned expression typeof(expr) (where expr must evaluate to an MPS node) is a language construction which returns a so-called type variable which serves as a type of that node. Type variables become concrete types during the process of equation solving.

In certain situations you want to say that a certain type doesn't have to exactly equal another type, but also may be a subtype or a supertype of that type. For instance, the type of the actual parameter of a method call does not necessarily have to be the same type as that of the method's formal parameter - it can be its subtype. That is, a method which requires Object as a parameter may be applied to a String.

To express such a constraint, you may use an inequation instead of an equation. An inequation expresses the fact that a certain type should be a subtype of another type. It is written as follows: expr1 :<=: expr2.

Weak And Strong Subtyping

A relationship of subtyping is useful for several different cases. You want a type of an actual parameter to be a subtype of formal parameter type, or you want a type of an assigned value to be a subtype of variable's declared type; in method calls or field access operations you want a type of an operand to be a subtype of a method's declaring class.

Sometimes such demands are somewhat controversial: consider, for instance, two types, int and Integer, which you want to be interchangeable when you pass parameters of such types to a method: if a method is doSomething(int i), it is both legal to call doSomething(1) and doSomething(new Integer(1)). But when these types are used as types of operand of, say, method call, the situation is the different: you shouldn't be able to call a method of an expression of type int, of an integer constant for example. So, we have to conclude that in one sense int and Integer are subtypes of each other, while in other sense they are not.

For solving such a controversy, we introduce two relationships of subtyping: namely, weak and strong subtyping. Weak subtyping will follow from strong subtyping: if a node is a strong subtype of another node, then it is it's weak subtype also.

Then, we can say about our example, that int and Integer are weak subtypes of each other, but they are not strong subtypes. Assignment and parameters passing require weak subtyping only, method calls require strong subtyping.

When you create an inequation in you typesystem, you may choose it to be a strong or weak inequation. Also subtyping rules, those which state subtyping relationship (see below), can be either weak or strong. A weak inequation looks like :<=:, a strong inequation looks like :<<=:

In most cases you want to state strong subtyping, and to check weak subtyping. If you are not sure which subtyping you need, use weak one for inequations, strong one for subtyping rules.

Subtyping Rules

When the typesystem engine solves inequations, it requires information about whether a type is a subtype of another type. But how does the typesystem engine know about that? It uses subtyping rules. Subtyping rules are used to express subtyping relationship between types. In fact, a subtyping rule is a function which, given a type, returns its immediate supertypes.

A subtyping rule consists of a condition (which can be either a concept reference or a pattern) and a body, which is a list of statements that compute and return a node or a list of nodes that are immediate supertypes of the given node. When checking whether some type A is a supertype of another type B, the typesystem engine applies subtyping rules to B and computes its immediate supertypes, then applies subtyping rules to those supertypes and so on. If type A is among the computed supertypes of type B, the answer is "yes".

By default, subtyping stated by subtyping rules is a strong one. If you want to state only weak subtyping, set "is weak" property of a rule to "true".

Comparison Inequations And Comparison Rules

Consider you want to write a rule for EqualsExpression (operator == in Java, BaseLanguage and some other languages): you want left operand and right operand of EqualsExpression to be comparable, that is either type of a left operand should be a (non-strict) subtype of a right operand, or vice versa. To express this, you write a comparison inequation, in a form expr1 :~: expr2, where expr1 and expr2 are expressions which represent types. Such an inequation is fulfilled if expr1 is a subtype of expr2 (expr1 <: expr2), or expr2 <: expr1.

Then, consider that, say, any Java interfaces should also be comparable, even if such interfaces are not subtypes of one another. That is because there always can be written a class which implements both of interfaces, so variables of interface types can contain the same node, and variable of an interface type can be cast to any other interface. hence a equation, cast, instanceof expressions with both types being interface types should be legal (and in Java they are such).

To state such a comparability which does not follow from subtyping relationship, one should use comparison rules. A comparison rule consists of two conditions for the two applicable types and a body which returns true if the types are comparable or false if they are not.

Here's the comparison rule for interface types:

comparison rule interfaces_are_comparable

applicable for  concept = ClassifierType as classifierType1 , concept = ClassifierType as classifierType2

rule {
  if (classifierType1.classifier.isInstanceOf(Interface) && classifierType2.classifier.isInstanceOf(Interface)) {
    return true;
  } else {
    return false;
  }
}

Quotations

A quotation is a language construction that lets you easily create a node with a required structure. Of course, you can create a node using smodelLanguage and then populate it with appropriate children, properties and references, using the same smodelLanguage. But there's a simpler - and more visual - way to accomplish that.

A quotation is an expression whose value is the MPS node written inside the quotation. Think about a quotation as a "node literal", a construction similar to numeric constants and string literals. That is, you write a literal if you statically know what value do you mean. So inside a quotation you don't write an expression which evaluates to a node, you rather write the node itself. That is, an expression 2 + 3 evaluates to 5, an expression < 2 + 3 > (angled braces being quotation braces) evaluates to a node PlusExpression with leftOperand being IntegerConstant 3 and rightOperand being IntegerConstant 5.

Antiquotations

For it is a literal, a value of quotation should be known statically. So in case where you know some parts (i.e. children, referents or properties) of your node only dynamically i.e. those parts should be evaluated in runtime and are not known in design time, then you can't use just a quotation to create a node with such parts.

The good news, however, is that if you know the most part of a node statically and you want to replace only several parts of by dynamically-evaluated nodes you can use antiquotations. An antiquotation can be of 4 types: child, reference, property and list antiquotation. They both contain an expression which evaluates dynamically to replace a part of quoted node by its result. Child and referent antiquotations evaluate to a node, property antiquotation evaluates to string and list antiquotation evaluates to list of nodes.

For instance, you want to create a ClassifierType with the class ArrayList, but its type parameter is known only dynamically, for instance by calling a method, say, "computeMyTypeParameter()".

Thus, you write the following expression: < ArrayList < %( computeMyTypeParameter() )% > >. The construction %(...)% here is that very antiquotation.

One also may antiquotate reference targets and property values, with ^(...)^ and $(...)$, respectively; or a list of children of one role, using *(...)* .

a) If you want to replace a node somewhere inside a quoted node with a node evaluated by an expression, you use node antiquotation, that is %( )%. As you may guess there's no sense to replace the whole quoted node with an antiquotation with an expression inside, because you can write such an expression simply in your program instead.

So such an antiquotation is used to replace children, grandchildren, great-grandchildren and other descendants of a quoted node. Thus, an expression inside of antiquotation should return a node. To write such an antiquotation, position your caret on a cell for a child and type "%".

b) If you want to replace a target of a reference from somewhere inside a quoted node with a node evaluated by an expression, you use reference antiquotation, that is ^(...)^. To write such an antiquotation, position your caret on a cell for a referent and type "^".

c) If you want to replace a child (or some more deeply located descendant) which is of a multiple-cardinality role, and if for that reason you may want to replace it not with a single node but rather with several ones, then use child list (simply list for brevity) antiquotations, *( )*. An expression inside a list antiquotation should return a list of nodes, that is of type nlist<..> or compatible type (i.e. list<node<..>> is ok, too, as well as some others). To write such an antiquotation, position your caret on a cell for a child inside a child collection and type "*". You can not use it on an empty child collection, so before you press "*" you have to enter a single child inside it.

d) If you want to replace a property value of a quoted node by a dynamicаlly calculated value, use property antiquotation $( )$. An expression inside a quotation should return string, which will be a value for an antiquoted property of a quoted node. To write such an antiquotation, position your caret on a cell for a property and type "$".

Examples Of Inference Rules

Here are the simplest basic use cases of an inference rule:

  • to assign the same type to all instances of a concept (useful mainly for literals):
    applicable to concept = StringLiteral as nodeToCheck
    {
      typeof (nodeToCheck) :==: < String >
    }
    
  • to equate a type of a declaration and the references to it (for example, for variables and their usages):
    applicable to concept = VariableReference as nodeToCheck
    {
      typeof (nodeToCheck) :==: typeof (nodeToCheck.variableDeclaration)
    }
    
  • to give a type to a node with a type annotation (for example, type of a variable declaration):
    applicable to concept = VariableDeclaration as nodeToCheck
    {
      typeof (nodeToCheck) :==: nodeToCheck.type
    }
    
  • to establish a restriction for a type of a certain node: useful for actual parameters of a method, an initializer of a type variable, the right-hand part of an assignment, etc.
    applicable to concept = AssignmentExpression as nodeToCheck
    {
      typeof (nodeToCheck.rValue) :<=: typeof (nodeToCheck.lValue)
    }
    

Type Variables

Inside the typesystem engine, a type may be either a concrete type (a node) or a so-called type variable. Also, it may be a node which contains some type variables as its children or further descendants. A type variable is an undefined type which may then become a concrete type, as a result of solving equations that contain this type variable.

Type variables appear at runtime mainly as a result of a "typeof" operation, but you can create them manually if you want to. There's a statement TypeVarDeclaration in typesystem lanuage to do so. You write it like "var T" or "var X" or "var V", i.e. "var" followed by the name of a type variable. Then you may use your variable, for example, in antiquotations to create a node with type variables inside.

Example: an inference rule for "for each" loop. A "for each" loop in Java consists of a loop body, an iterable to iterate over, and a variable into which the next member of an iterable is assigned before the next iteration. An iterable should be either an instance of a subclass of the Iterable interface, or an array. To simplify the example, we don't consider the case where the iterable is an array. Therefore, we need to express the following: an iterable's type should be a subtype of an Iterable of something, and the variable's type should be a supertype of that very something. For instance, you can  write the following:

for (String s : new ArrayList<String>(...)) {
  ...
}

or the following:

for (Object o : new ArrayList<String>(...)) {
  ...
}

Iterables in both examples above have the type ArrayList<String> which is a subtype of Iterable<String>. Variables have types String and Object, respectively, both of which are subtypes of String.

As we see, an iterable's type should be a subtype of an Iterable of something, and the variable's type should be a supertype of that very something. But how to say "that very something" in typesystem language? The answer is, it's a type variable that we use to express the link between the type of an iterable and the type of a variable. So we write the following inference rule:

applicable for concept = ForeachStatement as nodeToCheck
{
  var T ;
  typeof ( nodeToCheck . iterable ) :<=:  Iterable < %( T )% >;
  typeof ( nodeToCheck . variable ) :>=:  T ;
}

Meet and Join types

Meet and Join types are special types which are treated differently by the typesystem engine. Technically Meet and Join types are instances of MeetType and JoinType concepts, respectively. They may have an arbitrary number of argument types which could be any nodes. Semantically, a Join type is a type which is a supertype of all its arguments, and a node which has a type Join(T1|T2|..Tn) can be regarded as if it has type T1 or type T2 or... or type Tn. a Meet type is a type which is a subtype of its every argument, so one can say that a node which has a type Meet(T1&T2&..&Tn) inhabits type T1 and type T2 and.. and type Tn. The separators of arguments of Join and Meet types (i.e. "|" and "&") are chosen respectively to serve as a mnemonics.

Meet and Join types are not unuseful. Meet types appear even in MPS Base Language (which is very close to Java), for instance the type of such an expression:

true ? new Integer(1) : "hello"

is Meet(Serializable & Comparable), because both Integer (the type of new Integer(1)) and String (the type of "hello") implement both Serializable and Comparable.

Join type is useful when, say, you want some function-like concept return values of two different types (node or list of nodes, for instance). Then you should make type of its invocation be Join(node<> | list<node<>>).

You can create Meet and Join types by yourself, if you need to. Use quotations to create them, just as with other types and other nodes. The concepts of Meet and Join types are MeetType and JoinType, as it is said above.

"When Concrete" Blocks

Sometimes one may want not only to write equations and inequations for a certain types, but to perform some complex analysis with type structure. That is, inspect inner structure of a concrete type: its children, children of children, referents, etc.

It may seem that one just may write typeof(some expression), and then analyze this type. The problem is, however, that one can't just inspect a result of "typeof" expression because it may be a type variable at that moment. Although a type variable usually will become a concrete type at some moment, it can't be guaranteed that it is concrete in some given point of your typesystem code.

To solve such a problem you can use a "when concrete" block.

when concrete ( expr as var ) {
  body
}

Here, "expr" is an expression which will evaluate to a mere type you want to inspect (not to a node type of which you want to inspect), and "var" is a variable to which an expression will be assigned. Then this variable may be used inside a body of "when concrete" block. A body is a list of statements which will be executed only when a type denoted by "expr" becomes concrete, thus inside the body of a when concrete block you may safely inspect its children, properties, etc. if you need to.

If you have written a when concrete block and look into its inspector you will see two options: "is shallow" and "skip error". If you set "is shallow" to "true", the body of your when concrete block will be executed when expression becomes shallowly concrete, i.e. becomes not a type variable itself but possibly has type variables as children or referents. Normally, if your expression in condition of when concrete block is never concrete, then an error is reported. If it is normal for a type denoted by your expression to never become a concrete type, you can disable such error reporting by setting "skip error" to true.

Overloaded Operators

Sometimes an operator (like +, -, etc.) has different semantics when applied to different values. For example, + in Java means addition when applied to numbers, and it means string concatenation if one of its operands is of type String. When the semantics of an operator depends on the types of its operands, it's called operator overloading. In fact, we have many different operators denoted by the same syntactic construction.

Let's try to write an inference rule for a plus expression. First, we should inspect the types of operands, because if we don't know the types of operands (whether they are numbers or Strings), we cannot choose the type for an operation (it will be either a number or a String). To be sure that types of operands are concrete we'll surround our code with two when concrete blocks, one for left operand's type and another one for right operand's type.

when concrete(typeof(plusExpression.leftExpression) as leftType) {
  when concrete(typeof(plusExpression.rightExpression) as rightType) {
    ...
  }
}

Then, we can write some inspections, where we check whether our types are strings or numbers and choose an appropriate type of operation. But there will be a problem here: if someone writes an extension of baseLanguage, where one wants to use plus expression for addition of some other entities, say, matrices or dates, this person won't be able to use plus expression because types for plus expression are hard-coded in the inference rule for it. So, we need an extension point to allow language-developers to overload existing binary operations.

Typesystem language has such an extension point. It consists of:

  • overloading operation rules and
  • a construct which provides a type of operation by operation and types of its operands.

For instance, a rule for PlusExpression in baseLanguage is written as follows:

when concrete(typeof(plusExpression.leftExpression) as leftType) {
  when concrete(typeof(plusExpression.rightExpression) as rightType) {
    node<> opType = operation type( plusExpression , leftType , rightType );
    if (opType.isNotNull) {
      typeof(plusExpression) :==: opType;
    } else {
      error "+ can't be applied to these operands" -> plusExpression;
    }
  }
}

Here, "operation type" is a construct which provides a type of an operation according to types of operation's left operand's type, right operand's type and the operation itself. For such a purpose it uses overloading operation rules.

Overloaded Operation Rules

Overloaded operation rules reside within a root node of concept OverloadedOpRulesContainer. Each overloaded operation rule consists of:

  • an applicable operation concept, i.e. a reference to a concept of operation to which a rule is applicable (e.g. PlusExpression);
  • left and right operand type restrictions, which contain a type which restricts a type of left/right operand, respectively. A restriction can be either exact or not, which means that a type of an operand should be exactly a type in a restriction(if restriction is exact), or its subtype(if not exact), for a rule to be applicable to such operand types;
  • a function itself, which returns a type of operation by operation, left and right operand types.

Here's an example of one of overloading operation rules for PlusExpression in baseLanguage:

operation concept: PlusExpression
left operand type: <Numeric>.descriptor is exact: false
right operand type: <Numeric>.descriptor is exact: false
operation type:
(operation, leftOperandType, rightOperandType)->node< > {
  if (leftOperandType.isInstanceOf(NullType) || rightOperandType.isInstanceOf(NullType)) {
    return null;
  } else {
    return Queries.getBinaryOperationType(leftOperandType, rightOperandType);
  }
}

Replacement Rules

Motivation

Consider the following use case: you have types for functions in your language, e.g. (a 1, a 2,...a N ) -> r, where a 1, a 2, .., a N, and r are types: a K is a type of K-th function argument and r is a type of a result of a function. Then you want to say that your function types are covariant by their return types and countervariant by their argument types. That is, a function type F = (T 1, .., T N) -> R is a subtype of a function type G = (S 1, .., S N) -> Q (written as F <: G) if and only if R <: Q (covariant by return type) and for any K from 1 to N, T K :> S K (that is, countervariant by arguments' types).

The problem is, how to express covariance and countervariance in typesystem language? Using subtyping rules you may express covariance by writing something like this:

nlist <  > result = new nlist <  > ;
for ( node <  > returnTypeSupertype : immediateSupertypes ( functionType . returnType ) ) {
  node <FunctionType> ft = functionType . copy;
  ft . returnType = returnTypeSupertype;
  result . add ( ft ) ;
}
return  result ;

Okay, we have collected all immediate supertypes for a function's return type and have created a list of function types with those collected types as return types and with original argument types. But, first, if we have many supertypes of return type, it's not very efficient to perform such an action each time we need to solve an inequation and second, although now we have covariance by function's return type, we still don't have countervariance by function's arguments' types. We can't collect immediate subtypes of a certain type because subtyping rules give us supertypes, not subtypes.

In fact, we just want to express the abovementioned property: F = (T 1, .., T N) -> R <: G = (S 1, .., S N) -> Q (written as F <: G) if and only if R <: Q and for any K from 1 to N, T K :> S K . For this and similar purposes typesystem language has a notion called "replacement rule."

What's a replacement rule?

A replacement rule is another way to solve inequations. While the standard way is to transitively apply subtyping rules to a should-be-subtype until a should-be-supertype is found among the results (or is never found among the results), a replacement rule, if applicable to a inequation, removes the inequation and then executes its body (which usually contains "create equation" and "create inequation" statements).

Examples

A replacement rule for above-mentioned example is written as follows:

replacement rule  FunctionType_subtypeOf_FunctionType

applicable for  concept = FunctionType as functionSubType <: concept = FunctionType as functionSuperType

rule {
   if ( functionSubType . parameterType . count != functionSuperType . parameterType . count ) {
      error " different parameter numbers " -> equationInfo . getNodeWithError (  ) ;
      return ;
   }
   functionSubType . returnType :<=:  functionSuperType . returnType ;
   foreach ( node <  > paramType1 : functionSubType . parameterType ; node <  > paramType2 : functionSuperType . parameterType ) {
       paramType2 :<=:  paramType1 ;
   }
}

Here we say that a rule is applicable to a should-be-subtype of concept FunctionType and a should-be-supertype of concept FunctionType. The body of a rule ensures that the number of parameter types of function types are equal, otherwise it reports an error and returns. If the numbers of parameter types of both function types are equal, a rule creates an inequation with return types and appropriate inequation for appropriate parameter types.

Another simple example of replacement rules usage is a rule which states that a Null type (a type of null literal) is a subtype of every type except primitive ones. Of course, we can't write a subtyping rule for Null type which returns a list of all types. Instead, we write the following replacement rule:

replacement rule  any_type_supertypeof_nulltype

applicable for  concept = NullType as nullType <: concept = BaseConcept as baseConcept

rule {
   if ( baseConcept . isInstanceOf ( PrimitiveType ) ) {
      error " null type is not a subtype of primitive type " -> equationInfo . getNodeWithError (  ) ;
   }
}

This rule is applicable to any should-be-supertype and to those should-be-subtypes which are Null types. The only thing this rule does is checking whether should-be-supertype is an instance of PrimitiveType concept. If it is, the rule reports an error. If is not, the rule does nothing, therefore the inequation to solve is simply removed from the typesystem engine with no further effects.

Different semantics

A semantic of a replacement rule, as explained above, is to replace an inequation with some other equations and inequations or to perform some other actions when applied. This semantic really doesn't state that a certain type is a subtype of another type under some conditions. It just defines how to solve an inequation with those two types.

For example, suppose that during generation you need to inspect whether some statically unknown type is a subtype of String. What will an engine answer when a type to inspect is Null type? When we have an inequation, a replacement rule can say that it is true, but for this case its abovementioned semantics is unuseful: we have no inequations, we have a question to answer yes or no. With function types, it is worse because a rule says that we should create some inequations. So, what do we have to do with them in our use case?

To make replacement rules usable when we want to inspect whether a type is a subtype of an another type, a different semantic is given to replacement rules in such a case.

This semantic is as follows: each "add equation" statement is treated as an inspection of whether two nodes match; each "add inequation" statement is treated as an inspection of whether one node is a subtype of another; each report error statement is treated as "return false."

Consider the above replacement rule for function types:

replacement rule  FunctionType_subtypeOf_FunctionType

applicable for  concept = FunctionType as functionSubType <: concept = FunctionType as functionSuperType

rule {
   if ( functionSubType . parameterType . count != functionSuperType . parameterType . count ) {
      error " different parameter numbers " -> equationInfo . getNodeWithError (  ) ;
      return ;
   }
   functionSubType . returnType :<=:  functionSuperType . returnType ;
   foreach ( node <  > paramType1 : functionSubType . parameterType ; node <  > paramType2 : functionSuperType . parameterType ) {
       paramType2 :<=:  paramType1 ;
   }
}

In a different semantic, it will be treated as follows:

boolean result = true;
if ( functionSubType . parameterType . count != functionSuperType . parameterType . count ) {
      result = false;
      return result;
   }
   result = result && isSubtype( functionSubType . returnType <:  functionSuperType . returnType );
   foreach ( node <  > paramType1 : functionSubType . parameterType ; node <  > paramType2 : functionSuperType . parameterType ) {
       result = result && isSubtype (paramType2 <: paramType1) ;
   }
return result;

So, as we can see, another semantic is quite an intuitive mapping between creating equations/inequations and performing inspections.

Advanced features of typesystem language

Check-only inequations

Basically, inequations may affect nodes' types, for instance if one of the inequation part is a type variable, it may become a concrete type because of this inequation. But, sometimes one does not want a certain inequation to create types, only to check whether such an inequation is satisfied. We call such inequations check-only inequations. To mark an inequation as a check-only, one should go to this inequation's inspector and should set a flag "check-only" to "true". An "less or equals" sign for check-only inequation visually is gray, while for normal ones it is black, so you can see whether an inequation is check-only without looking at its inspector.

Dependencies

When writing a generator for a certain language (see generator), one may want to ask for a type of a certain node in generator queries. When generator generates a model, such a query will make typesystem engine do some typechecking to find out the type needed. Performing full typechecking of a node's containing root to obtain the node's type is expensive and almost always unnecessary. In most cases, the typechecker should check only the node given. In more difficult cases, obtaining the type of a given node may require checking its parent or maybe a further ancestor. The typechecking engine will check a given node if the computed type is not fully concrete (i.e. contains one or more type variables); then the typechecker will check the node's parent, and so on.

Sometimes there's an even more complex case: the type of a certain node being computed in isolation is fully concrete; and the type of the same node - in a certain environment - is fully concrete also but differs from the first one. In such a case, the abovementioned algorithm will break, returning the type of an node as if being isolated, which is not the correct type for the given node.

To solve this kind of problem, you can give some hints to the typechecker. Such hints are called dependencies - they express a fact that a node's type depends on some other node. Thus, when computing a type of a certain node during generation, if this node has some dependencies they will be checked also, so the node will be type-checked in an appropriate environment.

A dependency consists of a "target" concept (a concept of a node being checked, whose type depends on some other node), an optional "source" concept (a concept of another node on which a type of a node depends), and a query which returns dependencies for a node which is being checked, i.e. a query returns a node or a set of nodes.

For example, sometimes a type of a variable initializer should be checked with the enclosing variable declaration to obtain the correct type. A dependency which implements such a behavior may be written as follows:

target concept: Expression  find source: (targetNode)->JOIN(node< > | Set<node< >>) {
                                            if ( targetNode / . getRole_ (  ) . equals ( " initializer " ) ) {
                                               return  targetNode . parent ;
                                            }
                                            return  null ;
                                         }
source concept(optional): <auto>

That means the following: if the typechecker is asked for a type of a certain Expression during generation, it will check whether such an expression is of a role initializer, and if it is, will say that not only the given Expression but also its parent should be checked to get the correct type for the given Expression.

Previous Next

Using a typesystem

If you have defined a typesystem for a language, a typechecker will automatically use it in editors to highlight opened nodes with errors and warnings. You may additionally want also to use the information about types in queries, like editor actions, generator queries, etc. You may want to use the type of a node, or you may want to know whether a certain type is a subtype of another one, or you may want to find a supertype of a type which has a given form.

Type Operation

You may obtain a type of a node in your queries using type operation. Just write <expr>.type, where <expr> is an expression which is evalouated to a node.

Do not use type operation inside inference rules and inference methods! Inference rules are used to compute types, and type operation returns an already computed type.

Is Subtype expression

To inspect whether one type is a subtype of another one, use "is subtype" expression. Write isSubtype( type1 :< type2 ) or isStrongSubtype( type1 :<< type2 ), it will return "true" if type1 is a subtype of type2, or if type1 is a strong subtype of type2, respectively.

Coerce expression

A result of a coerce expression is a boolean value which says whether a certain type may be coerced to a given form, i.e. whether this type has a supertype which has a given form (satisfies a certain condition). A condition could be written either as a reference to a concept declaration, that means that a sought-for supertype should be an instance of this concept; or as a pattern, which a sought-for supertype matches with.
A coerce expression is written coerce( type :< condition ) or coerceStrong( type :<< condition ), what is a condition has just been discussed above.

Coerce Statement

A coerce statement consists of a list of statements which are executed if a certain type can be coerced to a certain form. It is written as following:

coerce ( type :< condition ) {
  ...
} else {
  ...
}

If a type can be coerced so as to satisfy a condition, first block will be executed, otherwise a block after "else" will be executed. The supertype to which a type is coerced can be used inside the first block of coerce statement. If a condition is a pattern and contains some pattern variables which match with parts of the supertype to which a type is coerced, such pattern variables can also be used inside the first block of the coerce statement.

Previous Next

Data Flow

A language's data flow aspect allows you to find unreachable statements, detect unused assignments, check whether a variable might not be initialized before it's read, and so on. It also allows performing some code transformations, for example the 'extract method' refactoring.

Most users of data flow analyses aren't interested in details of its inner working, but are interested in getting the results they need. They want to know which of their statements are unreachable, and what can be read before it's initialized. In order to shield a user from the complexities of these analyses, we provide assembly-like intermediate language into which you translate your program. After translation, this intermediate presentation is analyzed and a user can find which of the statements of original language are unreachable etc.

For example here is the translation of a 'for' loop from baseLanguage:

First, we translate the expression for node.iterable. Then we emit a label so we can jump after it. Then we perform a conditional jump after the current node. Then we emit code for writing to node.variable. This means that we change the value of node.variable on each iteration. We don't need to know what we write to node.variable, since this information isn't used by our analysis. Finally, we emit code for the loop's body, and jump to the previously emitted label.

Commands of intermediate language

Here are the commands of our intermediate language:

  • read x - reads a variable x
  • write x - writes to variable x
  • jump before node - jumps before node
  • jump after node - jumps after node
  • jump label - jumps to label
  • ifjump ((before|after)node)| label - conditional jump before/after node / to label
  • code for node - insert code for node
  • ret - returns from current subroutine

Can be unreachable

Some commands shouldn't be highlighted as unreachable. For example we might want to write some code like this:

If you generate data flow intermediate code for this statement, the last command: jump after condition will be unreachable. On the other hand, it's a legal base language statement, so we want to ignore this command during reachable statement analysis. To do so we mark it is as 'may be unreachable,' which is indicated by curly braces around it. You can toggle this settings with the appropriate intention.

Links:

http://www.itu.dk/people/brabrand/UFPE/Data-Flow-Analysis/static.pdf - good introduction to static analyses including data flow and type systems.

Previous Next

TextGen language aspect

Introduction

The TextGen language aspect defines a model to text transformation. It comes in handy each time you need to convert your models into the text form directly. The language contains constructs to print out text, transform nodes into text values and give the output some reasonable layout.

Operations

The append command performs the transformation and adds resulting text to the output. You can use found error command to report problems in the model. The with indent command demarcates blocks with increased indentation. Alternatively, the increase depth and decrease depth commands manipulate the current indentation depth without being limited to a block structure. The indent buffer command applies the current indentation (as specified by with ident or increase/decrease depth) for the current line.

Operation Arguments
append any number of:
  • {string value}
  • \n
  • $list{node.list} - list without separator
  • $list{node.list with ,} - with separator
  • $ref{node.reference}
  • ${node.child}
found error error text
decrease depth decrease indentation level from now onwards
increase depth increase indentation level from now on
indent buffer apply indentation to the current line
with indent { <code> } increase indentation level for the <code>

Examples

  1. Here is an example of the text gen component for the ForeachStatement (jetbrain.mps.baseLanguage).
  1. This is an artificial example of the text gen:

    producing following code block containing a number of lines with indentation:


Previous Next

In order to integrate with MPS IDE, you can use plugin aspect of your language. There are a number of entities that can be used in your language's plugin. This chapter describes all of them.

Actions and action groups

One can add custom actions to any menu in MPS by using action and action group entities.

The action describes one concrete action. Action groups are named lists of actions intended for structuring of actions - adding them to other groups and MPS groups (which represent menus themselves) and combining them into popup menus. You can also create groups with dynamically changing contents.

Tutorial
A quick and simple tutorial by Federico Tomassetti on how to create action and show it in a context menu is available here:
http://www.federico-tomassetti.it/tutorial-how-to-add-an-action-to-the-jetbrains-metaprogramming-system/

How to add new actions to existing groups?

In order to add new actions to existing groups, the following should be done:

  • actions should be described
  • described actions should be composed into groups
  • these groups should be added to existing groups (e.g. to predefined MPS groups to add new actions to MPS menus).

Predefined MPS groups are stored in jetbrains.mps.ide.actions model, which is an accessory model to pluginLanguage, so you don't need to import it into your model. 

Action structure

Action properties

Name - The name of an action. You can give any name you want, the only obvious constraint is that the names must be unique in scope of the model.

Mnemonic -  if mnemonic is specified, the action will be available via alt+mnemonic shortcut when any group that contains this action is displayed. Note that the mnemonic (if specified) must be one of the chars in action's caption. Mnemonic is displayed as an underlined symbol in action's caption.

Execute outside command - all operations with MPS models are executed within commands. Command is an item in the undo list (you don't control it manually, MPS does it for you), so the user can undo changes brought into the model by action's execution. Also, all the code executed in a command, has read-write access to the model but if you show dialogs inside of command, it can cause deadlock, so, if you are not using UI in your action, set this to false, otherwise it should be set to true and control read/write access manually with read action and command statements.

Caption - the string representing an action in menus

Description - this string (if specified) will be displayed in the status bar when this action is active (selected in any menu)

Keystroke - if specified, action will be available via this keystroke even if the containing group is not open (butthis action must be accessible through a chain of menus in current place). The first field of the keystroke editor specifies, which control keys should be pressed, while the second one goes for keycode.

Icon - this icon will be displayed near the action in all menus. You can select the icon file by pressing "..." button. Note that the icon must be placed near your language (because it's stored not as an image, but as a path relative to the language's root)

Construction parameters

Each action can be parameterized at construction time using construction parameters. This can be any data determining action's behavior. Thus, a single action that uses construction parameters can represent multiple different behaviors. To manage actions and handle keymaps MPS needs a unique identifier for each concrete behavior represented by an action. So, toString function was introduced for each construction parameter (can be seen in inspector). For primitive types there is no need to specify this function explicitly - MPS can do it automatically. For more complex parameters, you need to write this function explicitly so that for each concrete behavior of an action there is a different set of values returned from toString() functions.

Enable/disable action control

Is always visible flag - if you want your action to be visible even in disabled state, set this to true, otherwise to false.

Context parameters - specifies which items must be present in current context for the action to be able to execute. They are extracted from the context before any method is executed. There are two types of context parameters - optional and non-optional, this can be set in inspector. If some non-optional parameters were not extracted, the action state is set to disabled and isApplicable/update/execute methods are not executed. If all action parameters were extracted, you can use their values in all action methods. There are 2 types of action parameters - simple and complex action parameters.

  • Simple action parameters allow to simply extract all available data from current data context. The data is provided "by key", so you should specify the name and the key in declaration. The type of the parameter will be set automatically.
  • Complex action parameters were introduced to perform some frequently used checks and typecasts. Now there are 3 types available for context parameter of this type: node<concept>, nlist<concept>, model.
    • node<concept> - currently selected node which is an instance of specified concept. Action won't be enabled if selected node isn't an instance of this concept.
    • nlist<concept> - currently selected nodes. It is checked that all nodes are instances of concept (if specified). As with node<concept>, the action won't be enabled if the check fails.
    • model - current model

Is Applicable / update - In cooperation with context parameters, this controls enabled/disabled state of the action. The isApplicable method returns the new state of an action, update method is designed to update the state manually. You can also update any of your action's properties (caption, icon etc.) by accessing action's presentation via event.getPresentation(). This method is executed only if all context parameters were extracted from the context.

Note
Use isApplicable only if you don't modify presentation manually. This will work in isApplicable, but update method is  more suitable place for complex presentation manipulations.



Execute - this method is executed when the action is performed. It is guaranteed that it is executed only if the action's update method for the same event left the action in active state (or isApplicable returned true) and all required context parameters are present in current context and were filled in.

Methods - in this section you can declare utility methods.

Group structure

Group describes a set of actions and provides the information about how to modify other groups with current group.

Presentation

Name - The name of the group. You can give any name you want, the only obvious constraint is that the names must be unique in scope of the model.

isPopup - if this is true, the group represents a popup menu, otherwise it represents a list of actions.

Caption (popup=true) - string that will be displayed as a name of a popup menu

Mnemonic (popup=true) - if mnemonic is specified, popup menu will be available via alt+mnemonic shortcut when any group that contains it is displayed. Note that the mnemonic (if specified) must be one of the chars in caption. Mnemonic is displayed as an underlined symbol in popum menu caption.
Is invisible when disabled - if set to true, group will not be shown in case it has no enabled actions or is disabled manually in update().

Contents

There are 3 possibilities to describe group contents:

Element list - this is just a static list of actions, groups and labels (see modifications). The available elements are:

  • ->name - an anchors. Anchors are used for modifying one group with another. See Add statement section for details.
  • <---> - separator
  • ActionName[parameters] - an actions.

Build - this alternative should be used in groups, which contents are static, but depend on some initial conditions - the group is builded once and is not updated. Use add statement to add elements inside build block.

Update - this goes for dynamically changing groups. Group is updated every time before it is rendered.

Note
In update/build blocks use add statement to add group members.


Modifications and labels

Add to <group> at position <position> - this statement adds current group to <group> at the given position. Every group has a <default> position, which tells to add the current group to the end of the target group. Some groups can provide additional positions by adding so-called anchors into themselves. Adding anchors is described in contents section. The anchor itself is invisible and represents a position, in which a group can be added.

Note
  • You shouldn't care about group creation order and modifications order - this statement is fully declarative.
  • If A is added into B, B into C, C will contain A

actionGroup <...> expression

There is a specific expression available in pluginLanguage to access any registered group - actionGroup<group> expression.

Shortcuts

All the actions added by plugins are visible in Settings->Keymap and Settings->Menus and Toolbars. This means that any user can customize shortcuts for all MPS actions. The shortcut specified in action is the one that will be added to default shortcuts scheme.

Note
  • For now, shortcuts for an action A work only in those places, where A can be accessed from popup menu.
  • For now, shortcuts will not work if the action is added using group with update contents.
  • We plan to fix it later by providing an ability of explicit specifying the places where the action can be called using its shortcuts.

Editor Tabs

Look at any concept declaration. Have you noticed the tabs at the bottom of the editor? You are able to add the same functionality to concepts of your language.

What is the meaning of these tabs? The answer is pretty simple - they contain editors for some aspects of "base" node. You can also create corresponding aspect nodes from some of them. Each tab can be either single-tabbed (which means that only one node is displayed in it, e.g. editor tab) or multi-tabbed (if multiple nodes can be created for this aspect of the base node, see Typesystem tab for example).

How the editor for node is created? When you open some node, call it N, MPS tries to find the "base" node for N. If there isn't any base node, MPS just opens the editor for selected node. If the node is found (call it B), MPS opens some tabs for it, containing editors for some subordinate nodes. Then it selects the tab for N and sets the top icon and caption corresponding to B.

When you create tabbed editors, you actually provide rules for: finding base node, finding subordinate nodes and (optionally) algorithms of subordinate nodes creation.

Editor Tab Structure

Name - The name of the rule. You can give any name you want, the only obvious constraint is that the names must be unique in scope of the model.

Main Concept - the concept of base node.

Base Node - this is a rule for searching for "base" node. It should return null if the base node is not found or this TabbedEditor can't be applied.

Tabs

   Single-tabbed tab

       Caption - the caption of the tab

       Empty text - this will be written in this tab's panel if there are no nodes to display in it

       getNode - should return the node to edit in this tab

       create - if specified, this wil be executed when user asks to create node from this tab

   Multi-tabbed tab

       Caption - the caption of the tab

       Empty text - this will be written in this tab's panel if there are no nodes to display in it

       getNodes - should return nodes to edit in this tab

       getCaption- should return caption for inner tab for the given node

       create - if specified, this wil be executed when user asks to create node from this tab

Tools

Tool is an instrument that has a graphical presentation and aimed to perform some specific tasks. For example, Usages View, Todo Viewer, Model and Module repository Viewers are all tools. MPS has rich UI support for tools - you can move it by drag-and-drop from one edge of the window to another, hide, show and perform many other actions.

Tools are created "per project". They are initialized/disposed on class reloading (after language generation, on "reload all" action etc.)

Tool structure

Name - The name of the tool. You can give any name you want, the only obvious constraint is that the names must be unique in scope of the model.

Caption - this string will be displayed in tool's header and on the tool's button in tools pane

Number - if specified, alt+number becomes a shortcut for showing this tool (f it s available)

Icon - the icon to be displayed on the tool's button. You can select the icon file by pressing "..." button. Note that the icon mustbe placed near your language (because it's stored not as an image, but as a path relative to the language's root)

Init - initialize tool instance here

Dispose - dispose all tool resources here

getComponent - should return a Swing component (instance of a class which extends JComponent) to display inside the tool's window. If you are planning to create tabs in your tool and you are familiar with tools framework in IDEA, it's better to use IDEA's support for tabs. Using this framework greatly improves tabs functionality and UI.

Fields and methods - regular fields and methods, you can use then in your tool and in external code.

Tool operation

We added an operation to simply access a tool in some project. You can access it as project.tool<toolName>, where project is an IDEA Project. Do not forget to import pluginLanguage to use it.

Be careful
This operation can't currently be used in dispose() method

Preferences components

Sometimes you may want to be able to edit and save some settings (e.g. your tools' settings) between MPS startups. We have introduced preferences components for these purposes.

Each preferences component includes a number of preferences pages and a number of persistent fields.Preferences page is a dialog for editing user preferences. They are accessible through File->Settings.

Persistent fields are saved to .iws files when project is closed and restored from them on project open. The saving process uses reflection, so you don't need to care about serialization/deserialization in most cases.

Note
Only primitive types and non-abstract classes can be used as types of persistent fields. If you want to store some complex data, create a persistent field of type org.jdom.Element (do not forget to import model org.jdom), annotate it with com.intellij.util.xmlb.annotations.Tag and serialize/deserialize your data manually in after read / before write


Preferences component structure

name - component name. You can give any name you want, the only obvious constraint is that the names must be unique in scope of the model.

fields - these are persistent fields. They are initialized before after read and pages creation, so their values will be correct in every moment they can be accessed. Default values can be specified for them.

after read / before write - these blocks are used for custom serialization purposes and for applying/collecting preferences, which have no corresponding preferences pages (e.g. tool dimensions)

pages - preferences pages

Preferences page structure

name - the string to be used as a caption in Settings page. The name must be unique within a model.

component - UI component to edit preferences.

Hint
uiLanguage components can be used here

icon - the icon to show in Settings window. The size of the icon can be up to 32x32

reset - reset preferences values in UI component when this method is called.

commit - in this method preferences should be collected from the UI component and commited to wherever they are used.

isModified - if this method returns false, commit won't be executed. This is for preferences pages with long-running commit method.

PreferenceComponent expression

We added an expression to simply access a PreferenceComponent in some project. You can access it as project.preferenceComponent<componentName>, where project is an IDEA Project. Do not forget to import pluginLanguage to use it.

Be careful
This operation can't currently be used in dispose() method

Custom plugin parts

Custom plugin parts are custom actions performed on plugin initializing/disposing. They behave exactly like plugins. You can create as many custom plugins for your language as you want. There are two types of custom plugins - project and application custom plugins. The project custom plugin is instantiated once per project, while the application custom plugin is instantiated once per application and therefore it doesn't have a project parameter.

File Generators

File Generators provide an ability of changing the place to generate files to. This can be used, for example, to store application database settings in application's root directory regardless of the model in which they were described.

File Generator structure

generateFile - this method is used to create files. You are provided with contents (String) and some additional information and need to create resulting files.
isDefault / overridesDefault - these two methods are used to better control file generator applicability. Each file generator can be either "default" or "non-default". Defult file generators can be overridden by non-default.

fields, methods, extended class - some additional stuff can be declared here

Generation Listeners

Using generation listeners you can perform custom activities on generation events.

These events are:

  • before generation (models) - occures before generation
  • on models generated (success,models) - occures just after all models were generated
  • after generation(models) - occures after generation has finished

Generate Plugins concept

Note that when one creates the plugin aspect, he doesn't have to explicity write the plugin class, instead of it, he describes only the parts of the plugin itself. The plugin class is generated automatically, but only for plugin aspects of languages. If plugin language is used in a model, the plugin class won't be generated for it automatically. To generate the class, create Generate Plugin node in this model and specify which plugin classes should be generated.

Previous Next

Run Configurations

Introduction

Run configurations allow you to define how to execute programs, written in your language.

An existing run configuration can be executed either from run configurations box, located on the main toolbar,

by the "Run" menu item in the main menu

or through the run/debug popup (Alt+Shift+F10/Alt+Shift+F9).

Also run configurations could be executed/created for nodes, models, modules and project. For example, JUnit run configuration could run all tests in selected project, module or model. See Run configurations creators on how to implement such behavior for you run configurations.

To summarize, run configurations define the following things:

  • On creation stage:
    • configurations name, caption, icon;
    • configurations kind;
    • how to create a configuration from node(s), model(s), module(s), project.
  • On configuration stage:
    • persistent parameters;
    • editor for persistent parameters;
    • checker of persistent parameters validity.
  • On execution stage:
    • proccess which is actually executed;
    • console with all its tabs, action buttons and actual console window;
    • things required for debugging this configuration (if it is possible).
This section describes new execution languages usage. To see old run configuration languages, visit MPS 1.5 Documentation.

The following languages are introduced in MPS 2.0 to support run configurations in MPS.

  • jetbrains.mps.execution.common (common language) – contains concepts utilized by the other execution* languages;
  • jetbrains.mps.execution.settings (settings language) – language for defining different setting editors;
  • jetbrains.mps.execution.commands (command languages) – processes invocation from java;
  • jetbrains.mps.execution.configurations (configurations language) – run configurations definition;

Settings

Settings language allows to create setting editors and integrate them into one another. What we need from settings editor is the following:

  • fields to edit;
  • validation of fields correctness;
  • editor UI component;
  • apply/reset functions to apply settings from UI component and to reset settings in the UI component to the saved state;
  • dispose function to destroy UI component when it is no longer needed.

As you can see, settings have UI components. Usually, one UI component is created for multiple instances of settings. In the settings language settings are usually called "configurations" and their UI components are called "editors".

The main concept of settings language is PersistentConfigurationTemplate. It has the following sections:

  • persistent properties. This section describes the actual settings we are editing. Since we want also to persist this settings (i.e. to write to xml/read from xml) and to clone our configurations there is a restriction on their type: each property should be either Cloneable or String or any primitive type. There is also a special kind of property named template persistent property, but they are going to be discussed later.
  • editor. This section describes the editor of the configuration. It has functions create, apply to, reset from, dispose. Section also can define fields to store some objects in the editor. create function should return a swing component – the main UI component of the editor. apply to/reset from functions apply or reset settings in the editor to/from configuration given as a parameter. dispose function disposes the editor.
  • check. In this section persistent properties are checked for correctness. If some properties are not valid, a report error statement can be used. Essentially, this statement throws RuntimeConfigurationException.
  • additional methods. This section is for methods, used in the configurations. Essentially, this methods are configuration instance methods.

Persistent properties

It was said above that persistent properties could be either Cloneable or String or any primitive type. But if one uses settings language inside run configurations, those properties should also support xml persistent. Strings and primitives are persisted as usual. For objects persistent is more complicated. Two types of properties are persisted for an object: public instance fields and properties with setXXX and getXXX methods. So, if one wish to use some complex type in persistent property, he should either make all important field public or provide setXXX and getXXX for whatever he wants to persist.

Integrating configurations into one another

One of the two basic features of settings language is the ability to easy integration of one configuration into another. For that template persistent properties are used.

Template parameters

The second basic feature of settings language is template parameters. These are like constructor parameters in java. for example, if one creates a configuration for choosing a node, he may want to parametrize the configuration with nodes concept. The concept is not a persistent parameter in this case: it is not chosen by the user. This is a parameter specified on configuration creation.

Commands

Commands language allows to start up processes from the code in a way it is done from command line. The main concept of the language is CommandDeclaration. In the declaration, command parameters and the way to start process with this parameters are specified. Also, commands can have debugger parameters and some utility methods.

Execute command sections

Each command can have several execute sections. Each of this sections defines several execution parameters. There are parameters of two types: required and optional. Optional parameters can have default values and could be ommited when the command is started, while required can not have default values and are mandatory. Two execute sections of the command should have different (by types) lists of required parameters. One execute section can invoke another execute section. Each execute section should return either values of process or ProcessHandler types.

ProcessBuilderExpression

To start a process from a command execute section ProcessBuilderExpression isused. It is a simple list of command parts. Each part is either ProcessBuilderPart which consists of an expression of type string or list<string>, or a ProcessBuilderKeyPart which represents a parameter with a key (like "-classpath /path/to/classes"). When the code generated from ProcessBuilderExpression is invoked, each part is tested for being null or empty and ommited if so. Then, each part is splitted into many parts by spaces. So if you would like to provide a command part with a space in it and do not wish it to be splitted (for example, you have a file path with spaces), you have to put it into double quotes ("). Working directory of created process could be specified in inspector.

Debugger integration

To integrate a command with the debugger, two things are required to be specified:

  • the specific debugger to integrate with;
  • the command line arguments for a process.
    To specify a debugger one can use DebuggerReference – an expression of debugger type in jetbrains.mps.debug.apiLang – to reference a specific debugger. Debugger settings must be an object of type IDebuggerSettings.

Configurations

Configurations language allows to create run configurations. To create a run configuration, on should create an instance of RunConfiguration (essentially, configuration from settings language) and provide a RunConfigurationExecutor for it. One also may need a RunConfigurationKind to specify a kind of this configuration, RunConfigurationProducer to provide a way of creating this configuration from nodes, models, modules etc and a BeforeTask to specify how to prepare a configuration before execution.

Executors

Executor is a node which describes how a process is started for this run configuration. It takes settings user entered and created a process from it. So, the executor's execute methods should return an instance of type process. This is done via StartProcessHandlerStatement. Anything which has type process or ProcessHandler could be passed to it. A process could be created in three different ways:

  1. via command;
  2. via ProcessBuilderExpression (recommended to use in commands only);
  3. by creating new instance of ProcessHandler class; this method is recommended only if the above two do not fit you, for example when you creating a run configuration for remote debug and do not really need to start a process.

The executor itself consists of the following sections:

  1. "for" section where the configuration this executor is for and an alias for it is specified;
  2. "can" section where the ability of run/debug this configuration is specified; if the command is not used in this executor, one must provide an instance of DebuggerConfiguration here;
  3. "before" section with the call of tasks which could be executed before this configuration run, such as Make;
  4. "execute" section where the process itself is created.

Debugger integration

If a command is used to start a process, nothing should be done apart from specifying a configuration as debuggable (by selecting "debug" in the executor). However, if a custom debugger integration is required, it is done the same way as in command declaration.

Useful examples

In this section you can find some useful tips and examples of run configurations usages.

Person Editor

In this example a editor for a "Person" is created. This editor edits two properties of a person: name and e-mail address.

PersonEditor could be used from java code in the following way:

Exec command

This is an example of a simple command which starts a given executable with programParameters in a given workingDirectory. You can find this example in model jetbrains.mps.samples.nanoc.plugin in project nanocProject in MPS samples.

Compile with gcc before task

This is anexample of a BeforeTask wich does compilation of a source file with gcc command. It also demonstrates how to use commands outside of run configurations executors.

Note that this is just a toy example, in real life one should show a progress window while compiling.

Java Executor

This is an actual executor of Java run configuration from MPS.

Running a node, generated into java class

Lets suppose you have a node of a concept which is generated into a java class with a main method and you wish to run this node from MPS. Then you do not have to create a run confgiuration in this case, but you should do the following:

  1. The concept you wish to run should implement IMainClass concept from jetbrains.mps.execution.util language. To specify when the node can be executed, override isNodeRunnable method.
  2. Unit information should be generated for the concept. Unit information is required to correctly determine the class name which is to be executed. You can read more about unit information, as whell as about all trace information, in Debugger section of MPS documentation. To ensure this check that one of the following conditions are satisfied:
    1. a ClassConcept from jetbrains.mps.baseLanguage is generated from the node;
    2. a node is generated into text (the language uses textGen aspect for generation) and the concept of the node implements UnitConcept interface from jetbrains.mps.traceable;
    3. a node is genearted into a concept for which one of the three specified conditions is satisfied.

Previous Next

Intentions are a very good example of how MPS enables language authors to smoothen the user experience of people using their language. Intentions provide fast access to the most used operations with syntactical constructions of a language, such as "negate boolean", "invert if condition," etc. If you've ever used IntelliJ IDEA's intentions or similar features of any modern IDEs, you will find MPS intentions very familiar.

Using intentions


Like in IDEA, if there are avaliable intentions applicable to the code at the current position, a light bulb is shown. To view the list of avaliable intentions, press Alt+Enter or click the light bulb. To apply an intention, either click it or select it and press Enter. This will trigger the intention and alter the code accordingly.
Example: list of applicable intentions

Intention types

All intentions are "shortcuts" of a sort, bringing some operations on node structure closer to the user. Three kinds of intentions can be distinguished: regular intentions, "surround with" intentions and "generate" intentions.
Generally speaking, there is no technical difference between these types of intentions. They only differ in how they are typically used by the user.

regular intentions are listed on the intentions list (the light bulb) and they directly perform transformations on a node without asking the user for parameters customizing the operations.

"surround with" intentions are used to implement a special kind of transformation - surrounding some node(s) with another construct (e.g. "surround with parenthesis"). These intentions are not offered to the users unless they press ctrl-alt-T (the surround with command) on a node. Neither they are shown in general intentions pop-up menu.

"generate" intentions are used to produce some new nodes using user-specified parameters. In contrast with regular intentions, intentions of this type can show some interactive UI. This type of intentions has its own shortcut - alt-insert (ctrl-n on Macs)

Common Intention Structure

name

The name of an intention. You can choose any name you like, the only obvious constraint being that names must be unique in scope of the model. 

for concept

Intention will be tested for applicability only to nodes that are instances of this concept and its subconcepts.

available in child nodes

Suppose N is a node for which the intention can be applied. If this flag is set to false, intention will be visible only when the cursor is over the node N itself. If set to true, it will be also visible in N's descendants (but still will be applied to N)

child filter

Used to show an intention only in some children. E.g. "make method final" intention is better not to be shown inside method's body, but preferrably to be shown in the whole header, including "public" child.

description

The value returned by this function is what users will see in the list of intentions.

isApplicable

Intentions that have passed the "for concept" test are tested for applicability to the current node. If this method returns "true," the intention is shown in the list and can be applied. Otherwise the intention is not shown in the list. The node argument of this method is guaranteed to be an instance of the concept specified in "for concept" or one of its subconcepts.

execute

This method performs a code transformation. It is guaranteed that the node parameter has passed the "for concept" and "is applicable" tests.

Regular Intentions

is error intention - This flag is responsible for an intention's presentation. It distinguishes two types of intentions - "error" intentions which correct some errors in the code (e.g. a missing 'cast') and "regular" intentions, which are intended to help the user perform some genuine code transformations. To visually distinguish the two types, error intentions are shown with a red bulb, instead of an orange one, and are placed above regular intentions in the applicable intentions list.

Parameterized regular intentions

Intentions can sometimes be very close to one another. They may all need to perform the same transformation with a node, just slightly differently. E.g. all "Add ... macro" intentions in the generator ultimately add a macro, but the added macro itself is different for different intentions. This is the case when parameterized intention is needed. Instead of creating separate intentions, you create a single intention and allow for its parametrization. The intention has a parameter function, which returns a list of parameter values. Based on the list, a number of intentions are created , each with a diferent parameter value. The parameter values can then be accessed in almost every intention's method.

Note
You don't have an access to the parameter in the isApplicable function. This is because of performance reasons. As isApplicable is executed very often and delays would quickly become noticeable by the user, you should perform only base checks in isApplicable. All parameter-dependent checks should be performed in the parameter function, and if a check was not passed, this parameter should not be returned

Surround With - Intentions

This type of intentions is very similar to regular intentions and all the mentioned details apply to these intentions as well.

Generate - Intentions

The main difference from the general intentions is the executeUI function, which is executed before regular execute and is supposed to present some UI to the user. If the UI returns false, no further processing is done - the execute function is not performed

Note
It is not a good idea to show UI in the execute function - such a code will mostly likely throw an exception at runtime

Where to store my intentions?

You can create intentions in any model by importing the intentions language. However, MPS collects intentions only from the Intentions language aspects. If you want your intentions to be used by the MPS intentions subsystem, they must be stored in the Intentions aspect of your language.

Examples

You can find some intentions examples in jetbrains.mps.baseLanguage.intentions.

You can also see all intentions by going to the IntentionDeclaration concept (press Ctrl+N, type "IntentionDeclaration", press ENTER) and finding all instances of this concept (pres Alt+F7, check instances, check Global Scope).

[Previous] Next

In MPS, any model consists of nodes. Nodes can have many types of relations. These relations may be expressed in a node structure (e.g. "class descendants" relation on classes) or not (e.g. "overriding method" relation on methods). Find Usages is a tool to display some specifically related nodes for a given node.

In MPS, the Find Usages system is fully customizable - you can write your own entities, so-called finders, which represent algorithms for finding related nodes. For every type of relation there is a corresponding finder.

This is how "find usages" result looks like:

Using Find Usages Subsystem

You can press Alt+F7 on a node (no matter where - in the editor or in the project tree) to see what kind of usages MPS can search for.

You can also right-click a node and select "Find Usages" to open the "Find usages" window.


 

 Finders - select the categories of usages you want to search for

 Scope - this lets you select where you want to search for usages - in concrete model, module, current project or everywhere.

 View Options - additional view options

After adjusting your search, click OK to run it. Results will be shown in the Find Usages Tool as shown above.

Finders

To implement your own mechanism for finding related nodes, you should become familiar with Finders. For every relation there is a specific Finder that provides all the information about the search process.

Where to store my finders?

Finders can be created in any model by importing findUsages language. However, MPS collects finders only from findUsages language aspects. So, if you want your finder to be used by the MPS Find Usages subsystem, it must be stored in the findUsages aspect of your language.

Finder structure

name
The name of a finder. You can choose any name you want, the only obvious constraint being that the names must be unique in the scope of the model.
for concept
Finder will be tested for applicability only to those nodes that are instances of this concept and its subconcepts.
description
This string represents the finder in the list of finders. Should be rather short.
long description
If it's not clear from the description string what exactly the finder does, you can add a long description, which will be shown as a tooltip for the finder in the list of finders.
is visible
Determines whether the finder is visible for the current node. For example, a finder that finds ancestor classes of some class should not be visible when this class has no parent.
is applicable
Finders that have passed for concept are tested for applicability to the current node. If this method returns true, the finder is shown in the list of available finders; otherwise it is not shown. The node argument of this method is guaranteed to be an instance of the concept specified in "for concept" or its subconcepts.
Please note the difference between is visible and is applicable. The first one is responsible only for viewing. The second one represents a "valid call" contract between the finder and its caller. This is important because we have an execute statement in findUsagesLanguage, which will be described later. See execute section below for details.
find
This method should find given node usages in a given scope. For each found usage, use the add result statement to register it.
searched nodes
This method returns nodes for which the finder searched. These nodes are shown in searched nodes subtree in the tool.
For each node to display, use the add node statement to register it.
get category
There are a number of variants to group found nodes in the tool. One of them is grouping by category, that is given for every found node by the finder that has found it. This method gives a category to each node found by this finder.

What does the MPS Find Usages subsystem do automatically? 

  • Stores search options between multiple invocations and between MPS runs
  • Stores search results between MPS runs
  • Automatically handles deleted nodes
  • All the visualization and operations with found nodes is done by the subsystem, not by finders

Specific Statements

execute

Finders can be reused thanks to the execute statement. The execution of this statement consists of 2 steps: validating the search query (checking for concept and isApplicable), and executing the find method. That's where you can see the difference between isApplicable and isShown. If you use isApplicable for cases when the finder should be applicable, but not shown, you can get an error when using this finder in the execute statement.

Examples

You can see some finder examples in jetbrains.mps.baseLanguage.findUsages

You can also find all finders by going to the FinderDeclaration concept (Ctrl+N, type "FinderDeclaration", then press ENTER) and finding all instances of this concept (Alt+F7, check instances, then check Global Scope).

Previous Next

Suppose we want to describe one of the existing languages inside of MPS (for example, we can roughly consider baseLanguage as a description of Java in MPS). In this case, the libraries written for this language should be accessible from MPS. The stubs aspect of a language helps to create stub models for such libraries. These models are used to reference the code outside of MPS.

Stubs Language

There are two things that can be described using stubs aspect
1. The StubCreator describes how to parse existing code and convert it into models
2. The StubSolutions describe standard libraries for the language and how MPS can find them on the user's machine (e.g. for baseLanguage, there are 2 stub solutions - JDK for JDK classes and MPS.Classpath for MPS classes)

Stub Creator Declaration

name

Any descriptive name. Note that this name should not be changed after this stub manager is published, otherwise your users will get broken libraries in their modules

modelDescriptors

Should return modelDescriptors for models that can be loaded from the given location. Do not parse models here

import on creation

Languages that will be used by models produced by this stub creator. It's just a simplier way to import required languages into stub models

updateModel

The given model should be updated using data from given location. This means that all entities from this location should be added to the models.

rootNodeDescriptors

This is used for "Go to Node" action to find the names of root nodes quickly, without loading a model

Library Stub Descriptor

module

Module to add stub models to. The module can be created by "ctrl-space -> create new..." or via "Language Properties -> Design Time -> StubSolutions"

creator

Stub Creator to be used to load model of this solution

export

This will be used for distinguishing the public API from non-public API.

init

In this method, a corresponding solution can be adjusted (used languages, imported modules etc.)

roots

Should return a list of paths to load models from

Note that you can't re-create a stub solution after you have published it (even with the same name!). This happens because these solutions are referenced from user modules by automatically generated moduleId, which can't be preserved between deletion and creation.

Note that there's no need for using stub solutions for libraries that you distribute together with your language. Such libraries can be simply added as runtime or design-time libraries in language properties. Stub solutions are only applicable when paths to load stubs from can't be statically specified (like JDK paths, which can be different on different computers)

Usage

Let's consider how stubs are used.

Author of the language does the following things:
1. Creates StubCreator that can load stub models for his language
2. Creates StubSolutions describing standard libraries for his language (the libraries that are pre-installed on the user's machine and are not included into the language's packaging)

After the things mentioned are done, any user of this language can
1. Use the described libraries directly (import stub models, view and reference them, etc.)
2. Use his own libraries that can be loaded by the created StubCreator.

Let's see how to add a new library of a new type to solution:
1. Go to Solution Properties -> External code -> Libraries
2. Add a new library (select a path where it is situated)
3. Choose an appropriate manager for this library

Performance

For now, stubs are reloaded on classes reload.
Stubs are invalidated and loaded lazily. This means that

  • the contents of stub model are not loaded before they are queried
  • if neither source files, nor the stub manager for some stub model were changed since the last reload, the model won't be reloaded

Despite the optimizations considered, it is better to mention that there are still some bottlenecks.
1. The list of models is queried every time the classes are reloaded, which means that modelDescriptors block of Stub Manager is better to execute fast.
2. As the aim of stub models is to reference the outside code and help to compute types, there's no need to include anything non-referenceable into stub model. E.g. nothing inside Java method can be referenced from outside the method, so there's no need to include method bodies into stub models. This will only make the loading slower and the models bigger.

Warning: Never set stubs containing same models in different modules. Most probably, this won't work.

Previous Next

Debugger

MPS provides an API for creating custom debuggers as well as integrating with debugger for java. See Debugger Usage page for a description of MPS debugger features.

Integration with java debugger

To integrate your java-generated language with java debugger, provided by MPS, you should specify:

  • on which nodes breakpoints could be created;
  • nodes which should be traced;
  • how to start your application under debug;
  • custom viewers for your data.

Not all of those steps are absolutely necessary; which are – depends on the language. See next parts for details.

Nodes to trace and breakpoints

Suppose you have a language, let's call it highLevelLanguage, which generates code on some lowLevelLanguage, which in turn is generated directly into text (there can be several other languages between highLevelLanguage and lowLevelLanguage, it does not really metter). Suppose that the text generated from lowLevelLanguage is essentially java, and you want to have your highLevelLanguage integrated with java debugger. See the following explanatory table:

  lowLevelLanguage is baseLanguage lowLevelLanguage is not baseLanguage
highLevelLanguage extends baseLanguage
(uses concepts Statement,Expression, BaseMethodDeclaration etc)
Do not have to do anything. Specify traceable concepts for lowLevelLanguage.
highLevelLanguage does not extend baseLanguage Specify breakpointable concepts for highLevelLanguage. Specify traceable concepts for lowLevelLanguage.
Specify breakpointable concepts in DebugInfoInitializer for highLevelLanguage.

Startup of a run configuration under java debugger

MPS provides a special language for creating run configurations for languages generated into java – jetbrains.mps.baseLanguage.runConfigurations. Those run configurations are able to start under debugger automatically. See Run configurations for languages generated into java for details.

Custom viewers

When one views variables and fields in a variable view, one may want to define one's own way to show certain values. For instance, collections could be shown as a collection of elements rather than as an ordinary object with all its internal structure.

For creating custom viewers MPS has jetbrains.mps.debug.customViewers language.

A language jetbrains.mps.debug.customViewers enables one to write one's own viewers for data of certain form.

A main concept of customViewers language is a custom data viewer. It receives a raw java value (an objects on stack) and returns a list of so-called watchables. A watchable is a pair of a value and its label (a string which cathegorizes a value, i.e. whether a value is a method, a field, an element, a size etc.) Labels for watchables are defined in custom watchables container. Each label could be assigned an icon.

The viewer for a specific type is defined in a custom viewer root. In the following table custom viewer parts are described:

Part Description
for type A type for which this viewer is intended.
can wrap An additional filter for viewed objects.
get presentation A string representation of an object.
get custom watchables Subvalues of this object. Result of this funtion must be of type watchable list.

Custom Viewers language introduces two new types: watchable list and watchable.

This is the custom viewer specification for java.util.Map.Entry class:

And here we see how a map entry is displayed in debugger view:

Creating a non-java debugger

You can create a non-java debugger using the API provided by MPS. You can see how it is done in jetbrains.mps.samples.nanoc language – a toy language generated into C. This language is supplied with MPS among other sample languages. The project location is %HOME_PATH%/MPSSamples.2.0/nanoc/nanocProject/nanocProject.mpr.

Traceable Nodes

To upgrade from MPS 1.5 use migration script "Upgrade Trace Info Generation" (Tools->Scripts->By Language->jetbrains.mps.lang.plugin->Upgrade Trace Info Generation) and regenerate textGen and plugin model of your language

This section describes how to spesify which nodes require to save some additional information in trace.info file (like information about positions text, generated from the node, visible variables, name of the file the node was generated into etc.). trace.info files contain information allowing to connect nodes in MPS with generated text. For example, if a breakpoint is hit, java debugger tells MPS the line number in source file and to get the actual node from this information MPS uses information from trace.info files.

Specifically, trace.info files contain the following information:

  • position information: name of text file and position in it where the node was generated;
  • scope information: for each "scope" node (such that has some variables, associated with it and visible in the scope of the node) – names and ids of variables visible in the scope;
  • unit information: for each "unit node" (such that represent some unit of a language, for example a class in java) – name of the unit the node is generated into.

Concepts TraceableConcept, ScopeConcept and UnitConcept of language jetbrains.mps.lang.traceable are used for that purpose. To save some information into trace.info file, user should derive from one of those concepts and implement the specific behavior method. The concepts are described in the table below.

Concept Description Behavior method to implement Example
TraceableConcept Concepts for which location in text is saved and for which breakpoints could be created. getTraceableProperty – some property to be saved into trace.info file.
ScopeConcept Concepts which have some local variables, visible in the scope. getScopeVariables – variable declarations in the scope.
UnitConcept Concepts which are generated into separate units, like classes or inner classes in java. getUnitName – name of the generated unit.

trace.info files are created on the last stage of generation – while generating text. So the decsribed concepts are only to be used in languages generated into text.

Breakpoint Creators

To specify how breakpoints are created on various nodes, root breakpoint creators is used. This is a root of concept BreakpointCreator from jetbrains.mps.debug.apiLang language. The root should be located in the language plugin model. It contains a list of BreakpointableNodeItem, each of them specify a list of concept to create breakpoint for and a method actually creating a breakpoint. jetbrains.mps.debug.apiLanguage provides several concepts to operate with debuggers, and specifially to create breakpoints. They are described below.

  • DebuggerReference – a reference to a specific debugger, like java debugger;
  • CreateBreakpointOperation – an operation which creates a location breakpoint of specified kind on a given node for a given project;
  • DebuggerType – a special type for references to debuggers.

On the following example breakpoint creators node from baseLanguage is shown.

Previous Next

Changes highlighting

Changes highlighting is a handy way to show changes which were made since last update from version control system.
Since MPS 1.5 changes for models are highlighted in the following places:

Project tree view

Models, nodes, properties and references are highlighted.
green means new items, blue means modified items, brown means unversioned items.

Editor tabs

Highlighting appears for all of the editor tabs: for language aspect tabs of a concept and also for custom tabbed editors declared in plugin aspect of a language (see Plugin: Editor Tabs).

Editor

Every kind of changes are highlighted in MPS editor: changing properties and references, adding, deleting and replacing nodes.

If you hover mouse cursor over the highlighter's strip on the left margin of editor, the corresponding changes become highlighted in editor pane.
If you want to have your changes highlighted in editor pane all the time (not only on hovering mouse cursor over highlighter's strip), you can select "Highlight Nodes With Changes Relative to Base Version" option in IDE Settings → Editor.

If you click on highlighter's strip on the left margin, there appears a panel with three buttons: "Go to Previous Change", "Go to Next Change" and "Rollback".

If you click "Rollback", all the corresponding changes are reverted.
This feature allows you to feel free to make any changes to MPS model in editor without any fear, because at any moment you can revert it right from the editor.

Previous Next

Base Language

The BaseLanguage is an MPS' counterpart to Java, since it shares with Java almost the same set of constructs. MPS is the most common target of code generation and the most extensively extended language at the same time.

In order to simplify integration with Java, it is possible to specify the classpath for all modules in MPS. Classes found on the classpath will then be automatically imported into @java_stub models and so can be used directly in programs that use the BaseLanguage.

The frequently extended concepts of MPS include:

  • Expression. Constructs, which are evaluated to some results like 1, "abc", etc.
  • Statement. Constructs, which can be contained on a method level like if/while/synchronized statement.
  • Type. Types of variables, like int, double.
  • IOperation. Constructs, which can be placed after a dot like in node.parent. The parent element is a IOperation here.
  • GenericCreator. Constructs, which can be used to instantiate various elements.

Previous Next

Base Language is by far the most widely extended language in MPS. Since it is very likely that a typical MPS project will use a lot of different extensions from different sources or language vendors, the community might benefit from having a unified style across all languages. In this document we describe the conventions that creators should apply to all Base Language extensions.

Quick Reference

If you use... Set its style to...
Dot
LeftBracket
RightBracket
LeftBrace
RightBrace
Operator
KeyWord

Keywords

A keyword is a widely used string, which identifies important concepts from a language. For example, all the primitive types from Base Language are keywords. Also names of statements such as ifStatement, forStatement are keywords. Use the KeyWord style from base language's stylesheet for keywords.

Curly braces

Curly braces are often used to demarcate a block of code inside of a containing construction. If you create an if-like construct, place opening curly brace on the same line as the construct header. I.e. use:

instead of

Use the LeftBrace and RightBrace styles to set correct offsets. Make sure that the space between a character which is to left to opening curly brace and the curly brace itself is equal to 1 space. You can do so with a help of padding-left/padding-right styles.

Parentheses

When you use parentheses, set the LeftParen/RightParen styles to the left/right parenthesis. If a parenthesis cell's sibling is a named node's property, disable the first/last position of a parenthesis with first/last-position-allowed style.

Identifiers

When you use named nodes: methods, variables, fields, etc, it's advisable to make their name properties have 0 left and right padding. Making identifier declaration and reference holding the same color is also a good idea. For example, in Base Language, field declarations and references have the same color.

Punctuation

If you have a semicolon somewhere, set its style to Semicolon. If you have a dot, use the Dot style. If you have a binary operator, use the Operator style for it.

Previous Next

Closures

Introduction

Closures are a handy extension to the base language. Not only they make code more consise, but you can use them as a vehicle to carry you through the lands of functional paradigm in programming. You can treat functions as first-class citizens in your programs - store them in variables, pass them around to methods as arguments or have methods and functions return other functions. The MPS Closures Support allows to you employ closures in your own languages. In fact, MPS itself uses closures heavily, for example, in the collections language.


This language loosely follows the "BGGA" proposal specification for closures in Java12. However, you don't need Java 7 to run code with MPS closures. The actual implementation uses anonymous inner classes, so any recent version of Java starting with 1.5 will run the generated code without problems. Only the closures runtime jar file is required to be on the classpath of the generated solutions.

Function type

{ Type1, Type2... => ReturnType }

Let's start with a trivial example of function type declaration. It declares a function that accepts no parameters and returns no value.

Subtyping rules

A function type is covariant by its return type and contravariant by parameter types.

For example, given we have defined a method that accepts {String => Number} :

we can pass an instance of {Object => Integer} (a function that accepts Object and returns int) to this method:

Simply put, you can use different actual types of parameters and the return value so long as you keep the promise made in the super-type's signature.

Notice the int type automatically converted to boxed type Integer.

Closure literal

Closure literal is created simply by entering a following construct: { <parameter decls> => <body> }. No "new" operator is necessary.

The result type is calculated following one or more of these rules:

  • last statement, if it's an ExpressionStatement;
  • return statement with an expression;
  • yield statement.

Note: it's impossible to combine return and yield within a single closure literal.

Closure invocation

The invoke operation is the only method you can call on a closure. Instead of entering

To invoke a closure, it is recommended to use the simplified version of this operation - parentheses enclosing the parameter list.

Invoking a closure then looks like a regular method call.

Some examples of closure literal definitions.

Recursion

Functional programing without recursion would be like making coffe without water, so obviously you have a natural way to call recursively a closure from within its body:

A standalone invoke within the closure's body calls the current closure.

Closure conversion


For practical purposes a closure literal can be used in places where an instance of a single-method interface is expected, and vice versa3.

The generated code is exactly the same as when using anonymous class:

Think of all the places where Java requires instances of Runnable, Callable or various observer or listener classes:

Updated for MPS 1.5
The following changes are applicable to the upcoming 1.5 version of MPS.

As with interfaces, an abstract class containing exactly one abstract method can also be adapted to from a closure literal. This can help, for example, in smooth transition to a new API, when existing interfaces serving as functions can be changed to abstract classes implementing the new interfaces.

Yield statement

The yield statement allows closures populate collections. If a yield statement is encountered within the body of a closure literal, the following are the consequences:

  • if the type of yield statement expression is Type, then the result type of the closure literal is sequence<Type>;
  • all control statements within the body are converted into a switch statement within an infinite do-while loop at the generation;
  • usage of return statement is forbidden and the the value of last ExpressionStatement is ignored.

Functions that return functions

A little bit of functional programming for the functional hearts out there:

The curry() method is defined as follows:

Runtime

In order to run the code generated by the closures language, it's necessary to add to the classpath of the solution the closures runtime library. This jar file contains the synthetic interfaces needed to support variables of function type and some utility classes. It's located in:

Differences from the BGGA proposal

  • No messing up with control flow. This means no support for control flow statements that break the boundaries of a closure literal.
  • No "early return" problem: since MPS allows return to be used anywhere within the body.
  • The yield statement.


[1] Closures for the Java Programming Language


[3] Version 0.5 of the BGGA closures specification is partially supported


[3] This is no longer true: only closure literal to interface conversion is supported, as an optimization measure.

Previous Next

Collections Language

An extension to the Base Language that adds support for collections.

Introduction

Collection language provides a set of abstractions that enable the use of a few most commonly used containers, as well as a set of powerful tools to construct queries. The fundamental type provided by the collections is sequence, which is an abstraction analogous to Iterable in Java, or IEnumerable in .NET . The containers include list (both array-based and linked list), set and map. The collections language also provides the means to build expressive queries using closures, in a way similar to what LINQ does.

Null handling

Collections language has a set of relaxed rules regarding null elements and null sequences.

Null sequence is still a sequence

Null is a perfectly accepted value that can be assigned to a sequence variable. This results simply in an empty sequence.

Null is returned instead of exception throwing

Whereas the standard collections framework would have to throw an exception as a result of calling a method that cannot successfully complete, the collection language's sequence and its subtypes would return null value. For example, invoking first operation on an empty sequence will yield a null value instead of throwing an exception.

Skip and stop statements

skip

Applicable within a selectMany or forEach closure. The effect of the skip statement is that the processing of the current input element stops, and the next element (if available) is immediately selected.

stop

Applicable within a selectMany closure or a sequence initializer closure. The stop statement causes the construction of the output sequence to end immediately, ignoring all the remaining elements in the input sequence (if any).

Collections Runtime

Collections language uses a runtime library as its back end, which is designed to be extensible. Prior to version 1.5, the collections runtime library was written in Java and used only standard Java APIs. The release 1.5 brings a change: now the runtime library is available as an MPS model and uses constructs from jetbrains.mps.baseLanguage.closures language to facilitate passing of function-type parameters around.

Important change!
In order to make the transition from Java interfaces to abstract function types possible, several of the former Java interfaces in the collections runtime library have been changed into abstract classes. While no existing code that uses collections runtime will be broken, unfortunately this breaks the so called binary compatibility, which means that a complete recompilation of all the generated code is required to avoid incompatibility with the changed classes in the runtime.

The classes which constitute the collections runtime library can be found in the collections.runtime solution, which is available from the jetbrains.mps.baseLanguage.collections language.


Sequence

Sequence is an abstraction of the order defined on a collection of elements of some type. The only operation that is allowed on a sequence is iterating its elements from first to last. A sequence is immutable. All operations defined in the following subsections and declared to return a sequence, always return a new instance of a sequence or the original sequence.

Although it is possible to create a sequence that produces infinite number of elements, it is not recommended. Some operations may require one or two full traversals of the sequence in order to compute, and invoking such an operation on an infinite sequence would never yield result.

Sequence type

sequence<Type>

Subtypes Supertypes Comparable types
list<Type>
set<Type>
none java.lang.Iterable<Type>

Creation

new sequence

Parameter type Result type
{ => sequence<Type> } sequence<Type>

Sequence can be created with initializer.

closure invocation

Result type
sequence<Type>

A sequence may be returned from a closure (see Closures).

array as a sequence

Operand type Parameter type Result type
Type[] none sequence<Type>

An array can be used as a sequence.

A list, a set and a map are sequences, too. All operations defined on a sequence are also available on an instance of any of these types.

Sequence type is assignable to a variable of type java.lang.Iterable. The opposite is also true.

Operations on sequence

Iteration and querying
foreach statement

Loop statement

is equivalent to

forEach
Operand type Parameter type Result type
sequence<Type> { Type => void } void

The code passed as a parameter (as a closure literal or by reference) is executed once for each element.

size
Operand type Parameter type Result type
sequence<Type> none int

Gives number of elements in a sequence.

isEmpty
Operand type Parameter type Result type
sequence<Type> none boolean

Test whether a sequence is empty, that is its size is 0.

isNotEmpty
Operand type Parameter type Result type
sequence<Type> none boolean

Test whether a sequence contains any elements.

indexOf
Operand type Parameter type Result type
sequence<Type> Type int

Gives the index of a first occurrence in a sequence of an element that is passed to it as a parameter.

contains
Operand type Parameter type Result type
sequence<Type> Type boolean

Produces boolean value, indicating whether or not a sequence contains the specified element.

any / all
Operand type Parameter type Result type
sequence<Type> { Type => boolean } boolean

Produces boolean value that indicates whether any (in case of any operation) or all (in case of all) of the elements in the input sequence match the condition specified by the closure.

iterator
Operand type Parameter type Result type
sequence<Type> none iterator<Type>

Produces an iterator.

enumerator
Operand type Parameter type Result type
sequence<Type> none enumerator<Type>

Produces an enumerator.

Selection and filtering
first
Operand type Parameter type Result type
sequence<Type> none Type

Yields the first element.

last
Operand type Parameter type Result type
sequence<Type> none Type

Yields the last element.

take
Operand type Parameter type Result type
sequence<Type> int sequence<Type>

Produces a sequence that is sub-sequence of the original one, starting from first element and of size count.

skip
Operand type Parameter type Result type
sequence<Type> int sequence<Type>

Produces a sequence that is sub-sequence of the original one, containing all elements starting with the element at index count.

cut
Operand type Parameter type Result type
sequence<Type> int sequence<Type>

Produces a sequence that is a sub-sequence of the original one, containing all elements starting with first and up to (but not including) the element at index size minus count. In other words, this operation returns a sequence with all elements from the original one except the last count elements.

tail
Operand type Parameter type Result type
sequence<Type> int sequence<Type>

Produces a sequence that is a sub-sequence of the original one, containing all elements starting with the element at index size minus count. In other words, this operations returns a sequence with count elements from the end of the original sequence, in the original order.

page
Operand type Parameter type Result type
sequence<Type> int
int
sequence<Type>

Results in a sequence that is a sub-sequence of the original one, containing all elements starting with the element at index start and up to (but not including) the element at index end.


This is equivalent to

Where skip = start, count = end - start .

where
Operand type Parameter type Result type
sequence<Type> { Type => boolean } sequence<Type>

Produces a sequence that is a sub-sequence of the original one, with all elements for which the code passed as a parameter returns true.

findFirst
Operand type Parameter type Result type
sequence<Type> { Type => boolean } Type

Results in the first element that matches the parameter closure.

findLast
Operand type Parameter type Result type
sequence<Type> { Type => boolean } Type

Results in the last element that matches the parameter closure.

Transformation and sorting
select
Operand type Parameter type Result type
sequence<Type> { Type => Type2 } sequence<Type2>

Results in a sequence consisting of elements, each of which is the result of applying the parameter function to each element of the original sequence in turn.

selectMany
Operand type Parameter type Result type
sequence<Type> { Type => sequence<Type2> } sequence<Type2>

Produces a sequence that is a concatenation of all sequences, which are all the results of applying the parameter closure to each element of the original sequence in turn. The statements skip and stop are available within the parameter closure.

distinct
Operand type Parameter type Result type
sequence<Type> none sequence<Type>

Produces a sequence, which contains all elements from the original sequence in the original order, with all the elements having cardinality exactly 1. Of all occurrences of an element in the original sequence only the first occurrence is included in the resulting sequence.

sortBy
Operand type Parameter type Result type
sequence<Type> { Type => Type2 }
boolean
sequence<Type>

Produces a sequence with all elements from the original one in the order, which corresponds to an order induced by an imaginary sequence produced by applying the selector function to each element in the original sequence in turn. The selector function can be thought of as returning a key, which is used to sort elements in a sequence. The ascending parameter controls the sort order.

alsoSortBy
Operand type Parameter type Result type
sequence<Type> { Type => Type2 }
boolean
sequence<Type>

Equivalent to sortBy, unless used as a chain operation immediately following sortBy or another alsoSortBy. The result is a sequence sorted with a compound key, with the first component taken from previous sortBy or alsoSortBy (which is also a compound key), and the last component taken from this operation.

sort
Operand type Parameter type Result type
sequence<Type> { Type, Type => int }
boolean sequence<Type>

Produces a sequence containing all elements from the original one in the order produced by applying the comparator function (passed as a closure literal or by reference) to a list with elements from the original sequence. The ascending parameter controls the sort order (order is reversed if the value is false).

Binary operations
intersect
Operand type Parameter type Result type
sequence<Type> sequence<Type> sequence<Type>

Produces a sequence containing elements contained both by the original sequence and the parameter sequence.

except
Operand type Parameter type Result type
sequence<Type> sequence<Type> sequence<Type>

Produces a sequence containing all elements from the original sequence that are not also members of the parameter sequence.

union
Operand type Parameter type Result type
sequence<Type> sequence<Type> sequence<Type>

Produces a sequence containing elements both from the original sequence and the one passed as a parameter.

disjunction
Operand type Parameter type Result type
sequence<Type> sequence<Type> sequence<Type>

Produces exclusive disjunction (symmetric difference) of the original sequence and the one passed as a parameter.

concat
Operand type Parameter type Result type
sequence<Type> sequence<Type> } sequence<Type>

Produces a sequence, which is a concatenation of the original one with the sequence passed as a parameter

Conversion
reduceLeft / reduceRight
Operand type Parameter type Result type
sequence<Type> { Type, Type => Type } Type

Operation reduceLeft/reduceRight applies the combinator function passed as a parameter to all elements of the sequence in turn. One of the function parameters is a sequence element, and another is the result of previous application of the function. Operation reduceLeft takes first two sequence elements and applies the function to them, then takes the result of the first application and the third sequence element, etc. Operation reduceRight does the same, but moving from the sequence's tail backwards.

  • reduceLeft
  • reduceRight
foldLeft / foldRight
Operand type Parameter type Result type Applicable for
seed
sequence<Type>
{ Z, Type => Z } Z foldLeft
seed
sequence<Type>
{ Type, Z => Z } Z foldRight

Operation foldLeft/foldRight behaves similarly to reduceLeft/reduceRight, with the difference that it also accepts a seed value. Also the combinator function is asymmetric (it takes a Type and a Z parameters and returns a Z value). The result of the operation is of type Z.

  • foldLeft
  • foldRight
join
Operand type Parameter type Result type
sequence< ? extends string > string (optional) string

This operation is only available on a sequence of strings. The result is a string that is produced by concatenating all elements with the optional separator. The default separator is " " (single space).

toList
Operand type Parameter type Result type
sequence<Type> none list<Type>

Returns new list containing all the elements from the original sequence.

toArray
Operand type Parameter type Result type
sequence<Type> none Type*[]*

Returns new array containing all the elements from the original sequence.


List

A basic list container backed by either array list or linked list.

List type

list<Type>

Subtypes Supertypes Comparable types
none sequence<Type> java.util.List<Type>

List creation

new arraylist
new linkedlist

Parameter type Result type
Type...
{*}sequence<? extends *Type>
list<Type>

Creates an empty list. Optionally, initial values may be specified right in the new list creation expression.


Alternatively, a sequence may be specified that is used to copy elements from.

Operations on list

iterator
Operand type Parameter type Result type
sequence<Type> none modifying_iterator<Type>

This operation is redefined for list to return a modifying_iterator.

get
Operand type Parameter type Result type
list<Type> int Type

Yields the element at index position.

  • indexed access
    set
    Operand type Parameter type Result type
    list<Type> int
    Type
    Type

    Sets the element at index position to the specified value. Yields the new value.

  • indexed access
    add
    Operand type Parameter type Result type
    list<Type> Type Type

    Adds an element to the list.

    addFirst
    Operand type Parameter type Result type
    list<Type> Type Type

    Adds an element to the list as the first element.

    addLast
    Operand type Parameter type Result type
    list<Type> Type Type

    Adds an element to the list as the last element.

    insert
    Operand type Parameter type Result type
    list<Type> int
    Type
    Type

    Inserts an element into the list at the position index.

    remove
    Operand type Parameter type Result type
    list<Type> Type Type

    Removes an element from the list.

    removeFirst
    Operand type Parameter type Result type
    list<Type> none Type

    Removes the first element from the list.

    removeLast
    Operand type Parameter type Result type
    list<Type> none Type

    Removes the last element from the list.

    removeAt
    Operand type Parameter type Result type
    list<Type> int Type

    Removes an element from the list located at the position index.

    addAll
    Operand type Parameter type Result type
    list<Type> sequence<Type> list<Type>

    Adds all elements in the parameter sequence to the list.

    removeAll
    Operand type Parameter type Result type
    list<Type> sequence<Type> list<Type>

    Removes all elements in the parameter sequence from the list.

    clear
    Operand type Parameter type Result type
    list<Type> none void

    Clears all elements from the list.

reverse
Operand type Parameter type Result type
list<Type> none list<Type>

Produces a list with all elements from the original list in the reversed order.

Important
The reverse operation does not modify the original list, but rather produces another list.

Set

A basic set container backed by either hash set or linked hash set.

Set type

set<Type>

Subtypes Supertypes Comparable types
sorted_set<Type> sequence<Type> java.util.Set<Type>

Set creation

new hashset
new linked_hashset

Parameter type Result type
Type...
sequence<? extends Type>
set<Type>

Creates an empty set. Optionally, initial values may be specified right in the new set creation expression.


Alternatively, a sequence may be specified that is used to copy elements from.

Operations on set

iterator
Operand type Parameter type Result type
sequence<Type> none modifying_iterator<Type>

This operation is redefined for set to return a modifying_iterator.

add
Operand type Parameter type Result type
set<Type> Type Type

Adds an element to the set.

addAll
Operand type Parameter type Result type
set<Type> sequence<Type> set<Type>

Adds all elements in the parameter sequence to the set.

remove
Operand type Parameter type Result type
set<Type> Type Type

Removes an element from the set.

removeAll
Operand type Parameter type Result type
set<Type> sequence<Type> set<Type>

Removes all elements in the parameter sequence from the set.

clear
Operand type Parameter type Result type
set<Type> none void

Clears all elements from the set.


Sorted Set

A subtype of set that provides iteration over its elements in the natural sorting order, backed by a tree set.

Sorted Set type

set<Type>

Subtypes Supertypes Comparable types
none set<Type> java.util.SortedSet<Type>

Sorted set creation

new treeset

Parameter type Result type
Type...
sequence<? extends Type>
set<Type>

Creates an empty set. Optionally, initial values may be specified right in the new set creation expression.


Alternatively, a sequence may be specified that is used to copy elements from.

Operations on sorted set

headSet
Operand type Parameter type Result type
sorted_set<Type> Type sorted_set<Type>

Results in a sorted_set that is a subset of all elements from the original set in the original sorting order, starting with the first element and up to but not including the specified element.

tailSet
Operand type Parameter type Result type
sorted_set<Type> Type sorted_set<Type>

Results in a sorted_set that is a subset of all elements from the original set in the original sorting order, starting with the specified element.

subSet
Operand type Parameter type Result type
sorted_set<Type> Type, Type sorted_set<Type>

Results in a sorted_set that is a subset of all elements from the original set in the original sorting order, starting with the first specified element and up to but not including the second specified element.


Map

A map container backed by either a hash map or a linked hash map.

Map type

map<KeyType, ValueType>

Subtypes Supertypes Comparable types
sorted_map<KeyType, ValueType> sequence< mapping<KeyType, ValueType> > java.util.Map<KeyType, ValueType>


The map type is retrofitted to be a subtype of sequence.

Map creation

new hashmap
new linked_hashmap

Parameter type Result type
(KeyType => ValueType)... map<KeyType, ValueType>

Creates an empty map. Optionally, initial values may be specified right in the new map creation expression.

Operations on map

get value by key
Operand type Parameter type Result type
map<KeyType, ValueType> KeyType ValueType
keys
Operand type Parameter type Result type
map<KeyType, ValueType> none sequence<KeyType>

Results in a sequence containing all the keys in the map.

containsKey
Operand type Parameter type Result type
map<KeyType, ValueType> KeyType boolean

Returns true if the map contains a mapping for the specified key, false otherwise.

values
Operand type Parameter type Result type
map<KeyType, ValueType> none sequence<ValueType>

Results in a sequence containing all the values in the map.

containsValue
Operand type Parameter type Result type
map<KeyType, ValueType> ValueType boolean

Returns true if the map contains a mapping with the specified value, false otherwise.


mappings
Operand type Parameter type Result type
map<KeyType, ValueType> none set< mapping<KeyType, ValueType> >

Results in a set of mappings contained by this map. The mappings can be removed from the set, but not added.

assign value to a key
Operand type Parameter type Result type
map<KeyType, ValueType> KeyType ValueType
remove
Operand type Parameter type Result type
map<KeyType, ValueType> KeyType void

Removes the specified key and the associated value from the map.

clear
Operand type Parameter type Result type
map<KeyType, ValueType> none void

Clears all key-value pairs from the map.

putAll
Operand type Parameter type Result type
map<KeyType, ValueType> map<KeyType, ValueType> void

Puts all mappings from the map specified as a parameter into this map, replacing existing mappings.

Sorted Map

A subtype of map that provides iteration over keys conforming to the natural sorting order, backed by a tree map.

Sorted map type

map<KeyType, ValueType>

Subtypes Supertypes Comparable types
none map<KeyType, ValueType> java.util.SortedMap<KeyType, ValueType>

Sorted map creation

new treemap

Parameter type Result type
(KeyType => ValueType)... map<KeyType, ValueType>

Creates an empty tree map. Optionally, initial values may be specified right in the new map creation expression.

Operations on sorted map

headMap
Operand type Parameter type Result type
sorted_map<KeyType, ValueType> KeyType sorted_map<KeyType, ValueType>

Results in a sorted_map that is a submap of the original map, containing all the mappings in the original sorting order, starting with the first key and up to but not including the specified key.

tailMap
Operand type Parameter type Result type
sorted_map<KeyType, ValueType> KeyType sorted_map<KeyType, ValueType>

Results in a sorted_map that is a submap of the original map, containing all the mappings in the original sorting order, starting with the specified key.

subMap
Operand type Parameter type Result type
sorted_map<KeyType, ValueType> KeyType, KeyType sorted_map<KeyType, ValueType>

Results in a sorted_map that is a submap of the original map, containing all the mappings in the original sorting order, starting with the first specified key and up to but not including the second specified key.

Stack

A simple stack abstraction, backed by linked list.

Stack type

stack<Type>

Subtypes Supertypes Comparable types
deque<Type> sequence<Type> java.util.Deque<Type>

Stack creation

new linkedlist

Parameter type Result type
Type...
sequence<? extends Type>
stack<Type>

Creates an empty stack. Optionally, initial values may be specified right in the new linked list creation expression.


Alternatively, a sequence may be specified that is used to copy elements from.

Operations on stack

iterator
Operand type Parameter type Result type
sequence<Type> none modifying_iterator<Type>

This operation is redefined for stack to return a modifying_iterator.

addFirst / push
Operand type Parameter type Result type
stack<Type> Type Type

Appends an element to the head of the stack.

removeFirst / pop
Operand type Parameter type Result type
stack<Type>   Type

Removes an element from the head of the stack.

first / peek
Operand type Parameter type Result type
stack<Type>   Type

Retrieves the first element at the head of the stack without removing it.

Queue

A simple queue abstraction, backed by linked list or priority queue.

Queue type

queue<Type>

Subtypes Supertypes Comparable types
deque<Type> sequence<Type> java.util.Deque<Type>

Queue creation

new linkedlist
new priority_queue

Parameter type Result type
Type...
sequence<? extends Type>
queue<Type>

Creates an empty queue. Optionally, initial values may be specified right in the new linked list creation expression.


Alternatively, a sequence may be specified that is used to copy elements from.

Operations on queue

iterator
Operand type Parameter type Result type
sequence<Type> none modifying_iterator<Type>

This operation is redefined for queue to return a modifying_iterator.

addLast
Operand type Parameter type Result type
queue<Type> Type Type

Appends an element to the tail of the queue.

removeFirst
Operand type Parameter type Result type
queue<Type>   Type

Removes an element from the head of the queue.

first
Operand type Parameter type Result type
queue<Type>   Type

Retrieves the first element at the head of the queue without removing it.

last
Operand type Parameter type Result type
queue<Type>   Type

Retrieves the first element at the tail of the queue without removing it.

Deque

A simple double-linked queue abstraction, backed by linked list.

Deque type

queue<Type>

Subtypes Supertypes Comparable types
  sequence<Type>
queue<Type>
stack<Type>
java.util.Deque<Type>

Deque creation

new linkedlist

Parameter type Result type
Type...
sequence<? extends Type>
deque<Type>

Creates an empty deque. Optionally, initial values may be specified right in the new linked list creation expression.


Alternatively, a sequence may be specified that is used to copy elements from.

Operations on deque

iterator
Operand type Parameter type Result type
sequence<Type> none modifying_iterator<Type>

This operation is redefined for deque to return a modifying_iterator.

addFirst / push
Operand type Parameter type Result type
deque<Type> Type Type

Appends an element to the head of the deque.

addLast
Operand type Parameter type Result type
deque<Type> Type Type

Appends an element to the tail of the deque.

removeFirst / pop
Operand type Parameter type Result type
deque<Type>   Type

Removes an element from the head of the deque.

first
Operand type Parameter type Result type
queue<Type>   Type

Retrieves the first element at the head of the deque without removing it.

last
Operand type Parameter type Result type
queue<Type>   Type

Retrieves the first element at the tail of the deque without removing it.

Iterator

A helper type that is analogous to java.util.Iterator. An instance of the iterator can be obtained with the iterator operation on a sequence.

Iterator type

iterator<Type>

Subtypes Supertypes Comparable types
modifying_iterator<Type> none java.util.Iterator<Type>

Operations on iterator

hasNext
Operand type Parameter type Result type
iterator<Type> none boolean

Tests if there is an element available.

next
Operand type Parameter type Result type
iterator<Type> none Type

Returns the next element.


Modifying Iterator

A subtype of iterator that supports remove operation.

Modifying Iterator type

modifying_iterator<Type>

Subtypes Supertypes Comparable types
none iterator<Type> java.util.Iterator<Type>

Operations on modifying iterator

remove
Operand type Parameter type Result type
modifying_iterator<Type> none none

Removes the element this iterator is currently positioned at.


Enumerator

An alternative to the iterator, a helper type that works similar to .NET's IEnumerator. An instance of the enumerator can be obtained with the enumerator operation on a sequence.

Enumerator type

enumerator<Type>

Subtypes Supertypes Comparable types
none none none

Operations on enumerator

moveNext
Operand type Parameter type Result type
enumerator<Type> none boolean

Moves to the next element. Returns true if there is an element available.

current
Operand type Parameter type Result type
enumerator<Type> none Type

Returns the element this enumerator is currently positioned at.


Mapping

A helper type used by map and sorted_map.

Mapping type

mapping<KeyType, ValueType>

Subtypes Supertypes Comparable types
none none none

Operations on mapping

get value
Operand type Parameter type Result type
mapping<KeyType, ValueType> none ValueType
set value
Operand type Parameter type Result type
mapping<KeyType, ValueType> ValueType ValueType
get key
Operand type Parameter type Result type
map<KeyType, ValueType> none KeyType

Custom Containers

Custom containers is a simple way to provide own implementation of standard container types, thus allowing for easy extensibility of the collections language.

Example: weakHashMap

Provided the following declaration is reachable from the model currently being edited...

... one can use the weak version of hashmap thusly:

Custom Containers Declaration

A root node of concept CustomContainers may have one or more declarations.

declaration part allowed contents
containerName any valid identifier
container_type one of the existing container types of the collections language
runtime type Java classifier which represent implementation of the container
factory (optional) container creation expression;
the classifier's default constructor used if undefined

Primitive Containers

Collections framework include a set of custom containers designed to work with primitive data types. Using primitive types helps optimize speed and/or size of the containers. These containers are available with a separate language jetbrains.mps.baseLanguage.collections.trove.

Primitive list containers

list<?,?>

byteArrayList list<byte>
doubleArrayList list<double>
floatArrayList list<float>
intArrayList list<int>
longArrayList list<long>
shortArrayList list<short>

Primitive set containers

set<?,?>

byteHashSet set<byte>
doubleHashSet set<double>
floatHashSet set<float>
intHashSet set<int>
longHashSet set<long>
shortHashSet set<short>

Primitive maps

map<byte,?>

byteByteHashMap map<byte, byte>
byteDoubleHashMap map<byte, double>
byteFloatHashMap map<byte, float>
byteIntHashMap map<byte, int>
byteLongHashMap map<byte, long>
byteShortHashMap map<byte, short>

map<double,?>

doubleByteHashMap map<double, byte>
doubleDoubleHashMap map<double, double>
doubleFloatHashMap map<double, float>
doubleIntHashMap map<double, int>
doubleLongHashMap map<double, long>
doubleShortHashMap map<double, short>

map<float,?>

floatByteHashMap map<float, byte>
floatDoubleHashMap map<float, double>
floatFloatHashMap map<float, float>
floatIntHashMap map<float, int>
floatLongHashMap map<float, long>
floatShortHashMap map<float, short>

map<int,?>

intByteHashMap map<int, byte>
intDoubleHashMap map<int, double>
intFloatHashMap map<int, float>
intIntHashMap map<int, int>
intLongHashMap map<int, long>
intShortHashMap map<int, short>

map<long,?>

longByteHashMap map<long, byte>
longDoubleHashMap map<long, double>
longFloatHashMap map<long, float>
longIntHashMap map<long, int>
longLongHashMap map<long, long>
longShortHashMap map<long, short>

map<short,?>

shortByteHashMap map<short, byte>
shortDoubleHashMap map<short, double>
shortFloatHashMap map<short, float>
shortIntHashMap map<short, int>
shortLongHashMap map<short, long>
shortShortHashMap map<short, short>

<K> map<K,?>

ObjectByteHashMap<K> map<K, byte>
ObjectDoubleHashMap<K> map<K, double>
ObjectFloatHashMap<K> map<K, float>
ObjectIntHashMap<K> map<K, int>
ObjectLongHashMap<K> map<K, long>
ObjectShortHashMap<K> map<K, short>

Previous Next

Tuples

Tuples give you a way to group related data of different types into small collection-like data structures. In MPS, tuples are available within the jetbrains.mps.baseLanguage.tuples language.

Indexed tuples

Indexed tuple is a structure, which can contain several elements of arbitrary types and elements of which can be accessed by an index. The MPS implementation represents a tuple instance by a Java object. The usual meaning of '=' and '==' operations on Java objects within MPS remains unchanged.

Named tuples

Named tuples are similar to indexed tuples, with the difference that elements are accessed by name instead of by index. To use named tuples in the model you first need to explicitly define them in your model (new ->jetbrains.mps.baseLanguage.tuples/tuple).

Declaration of Pair:

Named tuple declaration

A root node of concept NamedTupleDeclaration contains a single declaration.

declaration part allowed contents
tupleName any valid identifier
elementType either a primitive type or any type that reduces to Java classifier
elementName any valid identifier

Previous Next

Dates language

An extension to the Base Language that adds support for dates.

Introduction

The Dates Language provides dedicated facilities to operate with date and time. Dates creation, comparison or adding and subtracting periods are done in a natural way, using common conventions and standard operators. As the backend implementation vehicle the Joda Time library is used.

Types

The following types are defined:

  • instant represents the number of milliseconds passed since the epoch, 1970-01-01T00:00Z. It's represented with a long. Thus an instant can be exchanged freely for a long value and vice versa.
  • datetime represents a date and time with time zone.
  • duration represents an interval of time measured in milliseconds.
  • period is a representation of a time interval defined in terms of fields: seconds, minutes, etc. A period is constructed using + (plus) operator.
  • timezone represents a time zone.
    Changes in 1.1
    Old datetime type is renamed to instant. New datetime type contains timezone information. Use in expression to convert instant to datetime.

Predefined values

A special reserved keyword now represents the current time.

A reserved keyword never represents an instant before the beginning of times. All instants are placed after never on the time axis. Its actual representation is null.

Period constant contains of a number and a property. They can be summed up together to form more complicated periods.

Time zone can be created in different ways. All predefined values are available in completion menu. default timezone is a shortcut to the computer local time zone.

Converting between types

Datetime can be obtained from an instant by providing a time zone. For reverse conversion simply get the datetime's instant property.

Period can be converted to duration using toDuration operation. It converts the period to a duration assuming a 7 day week, 24 hour day, 60 minute hour and 60 second minute.

For compatibility there are ways to convert datetime or instant types to java.util.Date and java.util.Calendar.

Reverse conversion:

Properties

Each individual datetime field can be queried using dot expression.

To replace a field, use with expression.

Each period can be re-evaluated in terms of any field (with rounding if needed) leveraging the in operator.

Operations

Arithmetic

A period can be added to or substracted from a datetime. The result is a datetime.

Two datetimes can be subtracted, the result is a period.

A duration can be added to or subtracted from an instant. The result is an instant.

Two instants can be subtracted, the result is a duration.

Comparison

Two values of the same type (instant, datetime, period or duration) can be compared using standard operators: <, >, ==, etc.

Another form of comparison can be used for datetime by adding a keyword by following the field specification. In this case values are compared rounded to the field (see Rounding).

Minimum and maximum operations are defined for instant and datetime types.

Rounding

Datetime values can be rounded to the nearest whole unit of the specified field (second, minute, hour, day, month, etc). There are several ways of rounding:

  • round returns the datetime that is closest to the original
  • round down to returns the largest datetime that does not exceed the original
  • round up to returns the smallest datetime that is not less than the original

Printing and Parsing

To print or parse datetime value we use date format describing its textual representation. The following formats are available by default:

  • defaultFormat, rssDate
  • shortDate, shortDateTime, shortTime, fullDate, longDate, etc (defined in Joda library)

Date format consists of one or more format tokens. The following kinds of tokens are supported:

  • literal (referenced with a single quote) - any text, commonly used to insert dash or space
  • datetime property (referenced with the name of a property in curly braces) - is replaced with the value of the property when printed
  • switch - composite token, which may vary the format depending on the date
  • offset (referenced as days ago, months ago, etc.) - calculates the difference between the provided datetime and the current moment
  • reference (the name in angle brackets) - a way to include existing format

Additional date formats can be introduced using the j.m.baseLanguage.dates.DateFormatsTable root concept. Each format has a name and visibility. Formats with private visibility are local for the table.

Datetime instance can be printed in the form of existing format by # operation.

Another possibility is to use {{#} operation, which allows to define format in-place.

Both printing operations accept optional locale argument in parentheses.

Parse operation accepts string, date format, timezone and two optional parameters: default value and locale.

Changes in 1.1
New print/parse expressions operate on datetime instead of instant. Use intention to convert deprecated expressions to a new ones.

Previous Next

Regular expressions language

Regular expressions is one of the earliest DSLs in wide use. Nearly all modern programing languages these days support regular expressions in one way or another. MPS is no exception. Regular expression support in MPS is implemented through a base language extension.

We also recommend checking out the Regular Expressions Cookbook, to get a more thorough introduction into the language.

Defining regular expression.

Regexp language allows you to create an instance of java.util.regex.Pattern class using a special pattern expression: /regexp/. In the generated code, MPS creates for each defined pattern expression a final static field in the outermost class, so the pattern is compiled only once during application runtime.

There are three options, you can add after the ending slash of the regexp.

/i Case-insensitive matching
/s Treat string as single line, dot character class will include newline delimiters
/m Multiline mode: ^ and $ characters matches any line within the string (instead of start or end of the string)

The options can be turned on/off by typing or deleting the character in the editor, or through the Inspector. Generated regular expression preview is available in the Inspector.

Re-using Definitions

To reuse a regular expression for a frequently used pattern accross your project, create a separate root:

model -> New -> jetbrains.mp.baseLanguage.regexp -> Regexps

Each reusable regular expression should have a name and optionally a description.

Pattern Match Operator

The =~ operator returns true if the string matches against the specified pattern.

Capturing Text

Optional use of parentheses in the expression creates a capture group. To be able to refer to the group later, the best practice is to give it a name.

Examples

If the pattern matches against the string, the matched value is captured into identifier and can be accessed in the if-block.

Don't forget to check out the Regular Expressions Cookbook, to get a more thorough introduction into the language.

Previous Next

GText language

TODO

Previous Next

Unit Test Language

TODO

Previous Next

Type Extension Methods

The language jetbrains.mps.extensionMethods provides a way to extend any valid MPS type with newly defined or overriden methods, akin to Java static methods.

Whereas static methods never become an internal part of the extended class and one has to always specify the "extended" object to operate on as one of the parameters to the extended method, with an extension method the new method gets added directly to the list of operations available on the target type.

So, provided we wanted to add a reverse method to the string type, instead of the good old "static method" way:

we would create new Extension Methods through New -> j.m.baseLanguage.extensionMethods/type extension, define the new method and tie it to the string class:

The very same mechanism can be used to override existing methods. And when in need to call the original method, just call it on this:

Since MPS does a good job to visually distinguish the original and overriden methods through the extension methods mechanism, you can't make a mistake picking the right one from the drop-down list.
Obviously this mechanism can be used to implement orthgonal concepts on your own domain objects as well:

With the declaration as above, one could write an operation on type my_type:

Root Nodes

There are two equally good ways to extend types with methods. Type Extension allows to you to add methods to a single type in one place, while Simple Extension Method Container comes in handy, when you need one place to implement an orthogonal concept for multiple different types.

Type Extension

This root contains declarations of extension methods for a single type.

Extension method declaration.

Simple Extension Method Container

Extension method declaration. The target type is specified per method.

declaration part allowed contents
containerName any valid identifier
extendedType any valid MPS type

Both roots may contain one or more static fields. These are available to all methods in the container.

Previous Next

XML Language

TODO

Previous Next

Here we introduce some handy BaseLanguage extensions

Checked dots

Language: jetbrains.mps.baseLanguage.checkedDots

A Checked Dot Expression is an dot expression extended with null checks on the operand.

If the operand is null, the value of the whole checked dot expression becomes null, otherwise it evaluates to the value of corresponding dot expression.

Ways to create a Checked Dot Expression
  • The Make dot expression checked intention
  • Enter "?" after dot, e.g. customer.?address.?street
  • Left transform of operation with "?"

You can transform checked dot expressions to the usual dot expressions using the Make dot expression not checked intention

Overloaded operators

Language: jetbrains.mps.baseLanguage.overloadedOperators

This language provides a way to overload binary operators.

Overloaded operator declarations are stored in an OverloadedOperatorContainer.

If there are several overloaded versions for one operator the most relevant is chosen.

Note that if an overloaded operators' usage is in the other model than its declaration, overloadedOperators language should be added to "languages engaged on generation" of usage's model.

Examples

Overloading plus operator for class Complex:

 

Also, you can define your own custom operators. Assume we want to create a binary boolean operator for strings, which tells if one string contains another:

 

Now, we can simply use this operator:


Custom constructors (since 1.5.1)

Language: jetbrains.mps.baseLanguage.constructors

Custom constructors provide a simple way to create complex objects. They are stored in a special root node - CustomConstructorsContainer.

Example

Assume we need a faster way to create rectangle.


Now, let's create a rectangle:

Previous Next

Language test language

Introduction

Language test language provides useful interface for making test for languages. It lets describe nodes properties using annotations.

Tests creation

In order to create tests LanguageTest aspect should be created. In popup menu of language chose New->New LanguageTest Aspect.

Then create NodesTestCase concept in model languageTest.

Language test consists of three sections:

  • List of nodes to test. This nodes are not generated into code and remain as is. This nodes can be referenced by adding adding annotations.
  • Methods witch performs testing.
  • Utility methods, than can be used to avoid code duplicates.

Simple tests

In order to check node operations reference to node is created at first. To refer to node TestAnnotation is used. Annotation is added to node by intention, clicking or pressing (Alt + Enter).

Each annotation has name and can be referenced by this name from code.

Then test code should be written in this code is performing operations with nodes. In order to write assertions of Unit test language assertion are used. To compare nodes new assertion added:

 assert <node1,node2, ...> match <node1,node2, ...>

compares two lists of nodes.

Example of test:

Type tests

In order to check type of node following instruction can be used.

 assert node has type t

It checks that node has type t.

In order to check node and in descendants for errors following operation is used.

check node type errors;

Properties, witch should be checked are specified by annotations, witch can be added by intention Add node properties annotation.
Node can have following properties:

  • Node has error;
  • Node has warning;
  • Node has specified type.

If node hasn't any annotation it checked for absence of errors and warnings.

There is example of type checking:

Instead of check ... type errors statement, operation annotation can be used. It can be added by Add test operation annotation intention.

Data flow tests

To check data flow following statement should be added.

check node dataflow

It calculates dataflow for node, and check properties specified by annotations. Following properties can be checked.

  • reachable - this node reachable;
  • unreachable - this node unreachable;
  • variable initialized - variable initialized in this node;
  • variable live - variable live in this node.

Instead if using check node dataflow check dataflow operation can be added.

Tests running

Model should be generated before running tests. To start tests shortcut (Ctrl + Shift + F10) or pop-up menu item.

Previous Next

Build Languages

MPS has a group of languages responsible for build process. Those languages are:

  • jetbrains.mps.buildlanguage (build language) – an MPS counterpart of Apache Ant;
  • jetbrains.mps.build.property (property language) – an extension to build language designed for writing property files;
  • jetbrains.mps.build.packaging (packaging language) – a high-level language for building software packages, including languages made in MPS.
  • jetbrains.mps.build.custommps (custom mps language) – an extension to packaging language, allowing to build your own custom MPS distribution.

Table of contents

Previous Next

Build Language

Introduction

Build language is an MPS counterpart of Apache Ant. Like in Ant, the each build script in build language is a project. A project can contain property and target declarations, imports of property files and other projects, task calls. A build language task library has all core Ant tasks and most of all optional Ant tasks.

On the above screenshot you can see an example project on build language – HelloWorldProject. It has a property declaration – hello.world.text property and a target declaration – default target, which is default target for this project. The default target calls echo task to output the value of the hello.world.text property to the console. Here is the Ant script, generated from HelloWorldProject.

You can see, that the generated Ant file looks very similar to input project.

Features

Though the build language seems to be very similar to Ant, it has some differences with it. Those differences are described below.

Typesystem

When calling a task in Ant, values are set to it's attributes. In build language a typesystem is declared for validating those values. Each attribute has a type. Available types are: boolean, file, integer, reference and string. Some attribute's values are restricted with enumerations of some type. For example Ant task zip has an attribute duplicate which allow string values "add", "preserve" and "fail".

Properties also have types. For example, as you can see on the screenshot of HelloWorldProject, property hello.world.text has type string.

Expressions

An expression can be assigned to an attribute. Simple expressions are string, integer and boolean literals, files, references to properties, tasks and targets. Two types of composite expressions are available: plus operation, which is a simple concatenation of two expressions and a multi-line expression which is also a concatenation but written in several lines.

External Properties

External properties are useful when a build environment can pass to the Ant script some values, like build number or vcs revision number. They provide ability to use those values in the script safely.

External property is a special kind of property. They also can be defined in the beginning of build language project or task and be used along with ordinary properties, but they can't be assigned a value. A useful feature of an external property is that they are automatically checked and if they are not set by the environment, the script or task fail.

Let's see a simple script, demonstrating several kinds of external properties.

As you can see, three external properties are defined in the beginning on the project and one in the beginning of the default task. Property some.really.important.property is a checked property. This means, that when the Ant build would fail on start if it wont be set by build environment. The other two properties are checked only in tasks where they are used.

Property some.internal.property is visible in the default task and checked on it's start.

Here is the Ant script, generated from the project.

Unknown macro: {h3. Tasks}

Buildlanguage allows user to use standart ant tasks and some optional tasks like junit. Tasks are located in models jetbrains.mps.build.generictasks.generated and jetbrains.mps.build.generictasks.optional. To use this tasks one should import language jetbrains.mps.build.generictasks.

Using Build Language

This section describes stuff, which makes build language usage more comfortable.

Running Build Language Project

For executing build language projects MPS has special run configuration "Build". It could be created from "Run/Debug Configurations" dialog:

Also a project could be runned from context menu:

Build output is shown in "Run" tool window:

Previous Next

Packaging Language

Introduction

When we think about building a project, we often do not think about a process of building, we usually imaging the result we want to have, for example, an archive with a program, documentation and libraries. We want a tool, which could take a description of the desired artifacts and generated a build script, for example an Ant file. That is one of the reasons why packaging language was made. The second reason for creating this language was lack of ability of simple deployment procedure for languages created in MPS.

Language Structure

This section describes the packaging language structure.

Defining Build Structure

Each packaging language script consists of two sections. The first one is a place for build properties, like base directory, and some build stuff, like variables or configurations. The second part is used for defining a build structure itself.
Let's talk more about the second part.

The build structure is written exactly like it should be in the resulting program distribution. There are several language elements you can use to describe it, for example, Folder, Zip, Jar and others (see all language elements). Some element can contain others, like in your distribution an archive could contain files in it.

On the above screenshot you can see an example of packaging language script. In the build structure part a Zip element project.zip contains Folder element project, which contains Folder element copied from folder sources, and this element also contains File copied from build.number. This means that we want a build script to create a zip archive project.zip with a folder project, copy sources folder with a copy of build.number inside into project folder. As you can see, the definition is very literal.

Language elements, used in build structure description, are listed in language elements table. Next sections are considering some special features of packaging language.

Build Configurations

Sometimes build should be done in several slightly different ways. For example, we want to create two types of build: for other developers – with sources and some additional tools and for users. Packaging language allow to deal with this problem using build configurations. Let's see how it is done for already mentioned example.

On the above screenshot a build script example is shown. Configurations are defined in configurations sections. As it can be seen, the script defines two configuration: dev and external for two build types. Some build elements marked with them. Elements, which are not marked at all, are included in all types of build. Those elements are Folder "project" and Folder "languages". Other elements marked with configuration dev. They are included in build for developers and not included in build for users. Those elements are Folder "devtools" and Folder "src".

Variables

Variables allow to use properties, passed to the script through command line, in elements names. Variables are declared in variables section of the build script. To declare a variable, you must define it's name and a name of ant property, which would be passed to the script by your build environment. On the below screenshot you can see an example of how variables declarations look like.

The shown section defines three variables: build, revision and configuration.

Variables can be used in titles of all elements in script. For example, having variables, defined on a previous screenshot, one can write a script like shown on a screenshot.

In this script a build number is used in a name of a zip archive with the project and build parameters are written into a file build.info.

You can use not only variables, defined in the script, but also a number of predefined variables which are listed in the following table.

Variable Name Ant Name Description
basedir basedir Base directory of the script.
date DSTAMP Current date.
\n line.separator System line separator.
/ file.separator System file separator.
: path.separator System path separator.

Macro

Some packaging language elements, such as folder or copy allow to enter a source path – a name of some file or directory. In many cases leaving those paths absolute would be a bad idea. That is why packaging language afford an opportunity to use macro in names of files and directories.

Two types of macro exists: path variables defined in "Settings" -> "IDE Settings" -> "Path Variables" and predefined macro: basedir – a base directory of build script (specified in the script beginning) and mps_home – an MPS installation directory. Build script base directory definition also allow usage of macros but with exception of basedir.

Macro used in a build script, will be generated to checked external properties of build language. This means, they will be checked in a build start and a build would fail, if their values were not specified.

Of course, packaging language does not force users using macro. To enter an absolute path you can select no macro item from macro menu.

Blocks

Blocks were introduced for structuring large builds. Essentially, a block is a part of build, a group of build elements. A block could be referenced from the main script. During generation, a contents of a block is copied to places if is referenced from. On the following screenshot there is a block defining a part of MPS build script.

Apart from a title and contents, a block can define which build script it is referenced from. This way variables from main build script could be referenced from the block.

Language Elements Table

This table gives a brief description of packaging language elements.

Element Notation Description
Antcall
antcall [target declatation] from [project] <excludes patterns> <includes patterns>
A call of an ant target.
  • target declaration – a link to declaration of target to call.
  • project – an build language concept in which to look for the specified target.
  • patterns – a coma- or space-separated patterns to exclude or include (can be omitted).
Block Reference
-> [block name]
A reference to a block. One could use blocks to split big scripts in parts.
Copy
copy from [source] <excludes patterns> <includes patterns>
A copy of folder contents.
  • source – a folder, which contents needs to be copied.
  • patterns – a coma- or space-separated patterns to exclude or include (can be omitted).
Echo
echo [message] > <title>
Echoes some message to the output stream or to the file.
  • title – file name (can be omitted).
  • message – a message to echo.
File
file <title> from <source>
A file.
  • title – a name of file.
  • source – a file to copy
    One of those parameters can be omitted.
Folder
folder <title> from <source> <excludes patterns> <includes patterns>
    <entries>
A folder.
  • title – a folder name.
  • source – a folder to copy.
    One of those parameter can be omitted.
  • entries – a list of elements located in this folder (can be omitted).
  • patterns – a coma- or space-separated patterns to exclude or include (can be omitted).
Jar
jar [title] <excludes patterns> <includes patterns>
    <entries>
A jar archive.
  • title – an archive name.
  • entries – a list of elements located in this jar archive (can be omitted).
  • patterns – a coma- or space-separated patterns to exclude or include (can be omitted).
Module
module [name]
A packaged language, solution or descriptor.
  • name – module name.
Replace
replace <title> from [source]
    <pairs token-value>
Make replacements in file.
  • title – a new name of resulting file.
  • source – a source, from which file need to be taken.
  • pairs token-value – pairs to replace.
Plugin
plugin <title> from [source]
Idea plugin jar.
  • title – plugin title. If omitted, same as source folder name.
  • source – a folder where plugin files a located. Classes of the plugin should be located in source/classes and plugin.xml file in soucre/META-INF directory. Only classes and plugin.xml are packed in the jar.
Zip
zip [title] <excludes patterns> <includes patterns>
    <entries>
A zip archive.
  • title – an archive name.
  • entries – a list on elements located in this zip archive (can be omitted).
  • patterns – a coma- or space-separated patterns to exclude or include (can be omitted).

Using Packaging Language

This section describes usage of packaging language.

Generating Build Scripts For Your Project

MPS allow to easily generate a build script on a packaging language for languages in your project. This functionality is very useful when you have a really big project and you do not want to enumerate every module name by hands.

Let's see, how it is done using complex language as an example.

Let's open a complex language project which is located in file %MPS_HOME%/samples/complexLanguage/Complex.mpr. A first step in generating build process is calling a build generation wizard, by selecting "New" -> "Build Script" in project popup menu.

At first, the wizard asks to select, whether a new solution would be created for a build script, or an old one should be picked. For this example we select to create a solution named Complex.build.

The next step is to choose a model for a build script. If we selected an existing solution on previous step, we could use one of the models in that solution. But as we create a new solution for our build script, we have only one option: to create a new model. Let's call the new model Complex.build.

The last step is to select modules to include into build. We choose only two modules: the language jetbrains.mps.samples.complex and it's runtime solution jetbrains.mps.samples.complex.runtime.

After pressing finish button, a resulting script is opened. Here is a build for complex language project.

Generating Files From Build Script

For generating files from a build script a "Generate Build Files" action can be used.

It generates the model with build and place output files to build script's base directory, so the files a ready to use. For complex language build script, created in previous section, those files would be:

File Purpose
Complex-default.xml The main script for building default configuration.
Complex-compile.xml Compilation script.
Complex-languages.xml Script for packaging modules into jars.
Complex.properties Property file.

Running Build Scripts

To run build scripts from MPS it has a run configuration "Packaging Script". It could be created via "Run/Debug Configurations" dialog:

Build script could also be runned from context menu:

The build output is shown in "Run" tool window:

Previous Next

Custom MPS Language

Introduction

The purpose of custom MPS language is to allow a programmer to create MPS distribution containing his own languages, so that users would be able to use those languages straight away.

The language adds two constructions into packaging language: mps-build and library. library is a special folder inside of MPS build, where packaged modules are located. While building MPS, a special configuration file is created and added to the build where all library folders are listed, so MPS will know, where to look for modules.

Building custom MPS for your project

In this section we will build a custom MPS distribution for a sample project. Our project will have three simple languages.

Creating custom MPS build script using a wizard

To create a custom MPS build script for the project, select "New" -> "Custom MPS Build Script" from project popup menu.

This action starts a wizard. The first step is to select a location of special zip file, MPS-buildTools.zip, containing files required to build MPS. This file can be downloaded from http://jetbrains.com/mps/download.

Next, the wizard asks to select, whether a new solution would be created for a build script, or an old one should be picked. For this example we select to create a solution named SimpleProject.build.

The next step is to choose a model for a build script. If we selected an existing solution on previous step, we could use one of the models in that solution. But as we create a new solution for our build script, we have only one option: to create a new model. Let's call the new model SimpleProject.build.

The last step is to select modules, included in build. Let's select all languages in project and exclude a solution jetbrains.mps.simple1.sandbox.

After pressing "OK" button the wizard creates a build script.

As you can see, resulting script has two libraries: simplenamespace library for modules in a namespace simpleNamespace and simpleproject library for modules with empty namespace.

Running custom MPS build script from MPS

To run custom MPS build script use "Custom MPS Script" run configuration.

You can also use a context menu item:

Running custom MPS build script manually

To run custom MPS script by hands you should generate build files by selecting "Generate Build Files" action from model's popup menu.

The files are generated into a folder inside of build script base directory. In our case it's folder "build" in project base directory.

~/MPSProjects/SimpleProject/build$ ls
help-build.xml  installer.nsi  mps.sh         SimpleProject-compile.xml       SimpleProject-default.xml    SimpleProject.properties
Info.plist      mps.bat        mps.vmoptions  SimpleProject-default-dist.xml  SimpleProject-languages.xml

Here is the table with description of generated files.

File Description
help-build.xml Script with different utility targets.
Info.plist Configuration file for Mac OS X.
installer.nsi Script for generating windows installer.
mps.bat Windows startup file.
mps.sh Unix startup file.
mps.vmoptions File with java VM options.
{YourProjectName}-compile.xml Compilation script.
{YourProjectName}-default-dist.xml Main script, packaging MPS for different platforms.
{YourProjectName}-default.xml Script for creating folder with MPS.
{YourProjectName}-languages.xml Script for packaging modules into jars.
{YourProjectName}.properties Properties file.

To start custom MPS build you should use {YourProjectName}-default-dist.xml file (in our case it's SimpleProject-default-dist.xml). It requires mps_home property to be set to the directory, containing MPS. To run build from console you can use the following command.

ant -f {YourProjectName}-default-dist.xml -Dmps_home={PathToMPS}

Build results

Custom MPS build script creates folder "artifacts" in script base directory (in our case it's project home directory) with MPS build for different platforms. They all are marked with number of MPS build used when running build script.

Artifact Name Description
MPS-{build.numer}.zip Crossplatform build with bat and sh startup scripts.
MPS-{build.numer}-windows.exe Windows installer (created only when running build script on windows).
MPS-{build.numer}-macos.zip Zip for MacOsX.
MPS-{build.numer}-linux.tar.gz Gzipped tar archive for Linux.

Let's unpack MPS-{build.numer}.zip and see, what is inside it.

~/MPSProjects/SimpleProject/artifacts/MPS$ ls
about.txt  build.number  entryPoints.xml  license  mps.sh    plugin   readme.mps.txt  samples.zip      SimpleProject
bin        core          lib              mps.bat  platform  plugins  readme.txt      simpleNamespace  workbench

Here we see folders "simpleNamespace" and "SimpleProject".

~/MPSProjects/SimpleProject/artifacts/MPS/simpleNamespace$ ls
jetbrains.mps.simple2.mpsarch.jar  jetbrains.mps.simple3.mpsarch.jar

Folder "simpleNamespace" contains two packed languages jetbrains.mps.simple2 and jetbrains.mps.simple3.

~/MPSProjects/SimpleProject/artifacts/MPS/SimpleProject$ ls
jetbrains.mps.simple1.mpsarch.jar

Folder "SimpleProject" contains packed language jetbrains.mps.simple1.

Let's start MPS to make sure that those languages are available in it. Select "File" -> "Settings" -> "Ide Settings" -> "MPS Library Manager". You can see, that there are new simplenamespace and simpleproject libraries pointing to "simpleNamespace" and "SimpleProject" directories respectively.

Let's create a test project and try to write something on jetbrains.mps.simple1 language. Let's create a solution "Test.sandbox" with a model "Test.sandbox.sandbox" and import
jetbrains.mps.simple1 into it. All three languages we wanted to pack are loaded by MPS and available for import.

Now we can create SimpleConcept1 instance in a model.

Previous Next

GWT Support

The language jetbrains.mps.gwt.client provides basic support for creating and editing of GWT Module XML files. Module manifests are represented as root nodes interlinked with source code and other manifests, including ones from .jar files. The jar files for gwt-dev.jar and gwt-user.jar are included with the language.

Supported GWT SDK versions are 2.0 onward.

GWT Module root

Supported elements. Please refer to GWT SDK documentation for details.

inherits

Specify the module to inherit settings from. In order to inherit from a GWT module contained in another model, that model must be imported first.

entry-point

Specify a class that will be used as the entry point. Must implement com.google.gwt.core.client.EntryPoint.

Filtering paths

The following three elements support following features, analogous to GWT module XML file's elements.

  • excludes and includes parameters
  • casesensitive parameter
  • defaultexcludes parameter
  • include and exclude sub-elements

If no path is explicitly specified, the current model is assumed.

public

Define a path with public resources.

source

Define a source path (package).

super-source

Define a source path stripping off the prefix.

Previous Next

(C) Copyright JetBrains, s.r.o. 2003, 2008. All Rights Reserved

JetBrains MPS includes software developed by

You can find licenses for these software in the license directory under a MPS installation.
Previous


Labels:
None
Enter labels to add to this page:
Please wait 
Looking for a label? Just start typing.