loading...

Creating a simple language using JetBrains MPS

antoine profile image Antoine Gagnon ・12 min read

Presentation of the project

The objective of this project is to construct a language to define mindmaps. These mindmaps have multiple levels of topics and also include a potential marker for each topic. The mindmap file will be able to generate a LaTeXfile that will have section and subsections defined according to the mindmap’s topics. The UML diagram we will follow is this one:

UML Diagram
This tutorial is geared toward programmers with some experience using object-oriented languages and more specifically Java-like languages.

The tutorial can be used in conjunction with this git repository to follow each step with each commit.

Initial setup

Installation

To install the program, the best way to make sure we have the latest stable version is to head on to the official download page and download the version for our operating system. MPS is available on Windows, Mac and most flavors of Linux.

Creating the project

After launching MPS choose the theme and shortcuts we want to use for our editor and click “Create new project”. In this article, we used the default IntelliJ theme and default shortcuts.

As we can see on the left side of the project creation window, we can either create a new language with the Language project, create a solution to use a DSL created with MPS with Solution project or create an empty project and build everything from scratch.

We will be creating a new language so click on Language project, chose a name for our project and a name for our language and make sure we click the Create Sandbox Solution check-box to be able to test our language along the way. Our menu should look like this:

Project creation menu
Now we simply have to click OK to create our project.

Navigating the logical view

Logical view

As we can see the logical view splits the sandbox where the language testing is done and the language module, here simply called Mindmap and identified by the L icon before its name. We will mostly focus on the language part, pay attention to the different aspects that are defined like structure, editor, constraints…etc.

Language creation

We will now focus on language creation. We will edit the language module, and test the effects on the language using the sandbox provided by MPS.

Sandbox use

To be able to use the language in the sandbox, we need to make sure we rebuild the language after changes by right-clicking the language module and clicking Rebuild Language MindMap. After our first concept is created we will be able to create a new mindmap file by right-clicking on the sandbox model inside the MindMap.sandbox module and choosing Newmindmap.

Structure

The structure of our language is the definition of properties and relations between concepts of our language. We will first start by creating a new concept by right-clicking on the structure aspect and selecting NewConcept thus creating an empty concept.

We will first create our root concept of a mindmap. As we can see if we open the concept file, the editing process is very different, this is because MPS is a projectional editor that directly edits the abstract syntax tree (AST) of our program. The main difference is that MPS won’t let us write anywhere we want and will guide us to fill the blanks in our file with elements that comply with its syntax.

Let’s change the name of our concept to Mindmap by editing the empty field, then change the instance can be root property to true. Our mindmap concept is now at the top of our abstract syntax tree and we can build elements from there.

Let’s add a name property to our mindmap by implem enting the INamedConcept interface, we can also use the Ctrl + Space shortcut to autocomplete the name and get a suggestion of different concept we can use. This interface that we can navigate to using Ctrl + B has a name : string property that will be implemented and also adds some functions and refactoring options to the property.

We can add an alias that will be used by default in the editor to define our concept and also a short description that will be shown in the tooltip when using the auto-completion and in other menus when trying to use the Mindmap concept.

Now we need to add topics to our mindmap, we will create a new Topic concept that all other types of topics will derive from. Once our concept is created, we give it its name and also use the Alt + Enter shortcut in the same field to access the class actions where we click the Make abstract option. Let’s also have this Topic implement the INamedConcept interface since we also need a name for our topics.

We also planned on being able to add a marker to our topics, so let’s create a new concept called Marker that simply implements the INamedConcept interface and has an alias and a description.
Now we can come back to our Topic concept and add a marker reference. To do so we start by typing the name of the property, here marker, then press to get to the next field and add the Marker class. We will leave the multiplicity at [0..1] since our topics can only have zero or one marker. We will also add a markers child to the Mindmap concept with a [0..n] multiplicity as we can define
multiple markers that will be referenced by our topics later on.

Now we are able to create topics that aren’t abstract and will be used in the mindmap. By following the UML diagram, we create a new concept called CentralTopic that will extends the Topic class instead of BaseConcept, we give it an alias and a short description.
We can then come back to our mindmap and add a child of the CentralTopic class that we will simply call centralTopic and that will have a [0..1] multiplicity.
By following the same pattern as for CentralTopic we will create a MainTopic concept and add it as a child of CentralTopic with a [0..n] multiplicity, as a central topic can have multiple main topics. We will also do the same with a SubTopic concept that is a child with [0..n] multiplicity of MainTopic but with a twist on its children as they are also of the SubTopic type and are called subSubTopics.

We now have a functional mindmap language that we can test in the sandbox. Although the syntax is rough and sometimes confusing, it lets us create topics at different levels, assign them markers created in the mindmap and give them names. We can also use the node explorer function by pressing Alt + X to watch the different elements composing our mindmap.

Behavior

Behavior in MPS are mostly similar to methods and define functions for a concept. They also offer the possibility to modify the constructor, although the constructor function isn’t able to access the model as it is called before the node is linked to the tree.

In our project we will add an equals function to our Marker concept and a function to analyze the names of the marker in a mindmap and pick the highest one out of the markers using numbers as their names. This will be used to gradually increment the marker’s names when they are created.

To create a new behavior for our Marker concept, we go to its editor page and access the Behavior tab at the bottom of the editor, then we click Click to create new aspect and chose Concept Behavior. In the new aspect we just created, we can see we have access to the constructor, but here we will create a new method by clicking on concept methods and pressing . Our function will have a boolean type and will be called equals, it will also take a node<Marker> otherMarker as parameters. The body of the function will simply test if the two marker’s names are the same as shown here:

Marker's behavior

We will now do the same behavior creation process for the Mindmap concept where we will create a function that will return an integer and will be called getHighestMarkerNumber but won’t have any parameter as it will analyze the calling node itself.

This method will go through all the children markers and try to parse their name as integers, if the name isn’t a number, the exception is caught and discarded while printing on the error pipe that it wasn’t parsed. If it is indeed a number, we compare the value with our max value variable created at the beginning and update our base value with the new one if it is bigger. We finally return the maximum value at the end of the function juste like this:

Mindmap behavior

Constraints

Constraints are rules that define the way our nodes interact with each other and the properties they can have.

In our project, we will implement a constraint that says that two markers defined in the mindmap can’t have the same name. To do so we go in the Marker concept and access the Constraints tab at the bottom of the editor, we can then click Click to create new aspect and choose Concept Constraints. We will edit the can be child function, this function returns either true or false according to the rules defined inside. We can access the model from this function, and we have access to specific functions to help us in this regard. We will access all the marker’s siblings and loop over them with a for loop checking if the current node node and the sibling node n are the same using the equals behavior defined earlier and returning the value. The end code should look like this:

Marker constraints function

If we now try to create two markers with the same name, we will get an error saying these markers can’t be children of the mindmap.

Editor and actions

The editor is here to define the projection of the AST, it can be textual, visual, include a lot of different layouts and rules on what concrete syntax to use. In our case, we will focus on a simplistic indented textual syntax that is similar to the default one.

Similarly to the previous concepts we’re going to add a Concept Editor aspect to our Mindmap concept. For the cell layout we are going to use a collection (indent) layout that is triggered by the [- keyword, or can be found using the Ctrl + Space shortcut. This layout allows us to add new lines for children nodes while increasing indentation.

Inside of our layout we can add the mindmap keyword followed by the {name} property that can be found using the Ctrl + Space shortcut again. We’ll now add the with keyword followed by the %markers% children and finally we can add the about keyword and the child %centralTopic% getting this line:

Mindmap concrete syntax

The CentralTopic concept editor and the other topics are built in the following fashion while the marker editor isn’t modified. This concept editor will also use the indented layout, starting on the first line with the {name} property of the topic, with the with keyword and the %marker% reference, where the editor is asking us for a cell model, this is the way the markers will be displayed and we will choose to use the {name} property. This whole line will be followed by a left bracket before using the Alt + Enter shortcut to add a new line, then pressing to get on the new line before reusing the shortcut to add an indent. Now that we are on a new indented line, we can add the %mainTopics% children, add a new line again, press and finally add a right bracket and a new line to end our CentralTopic concrete syntax. The other topics are built the exact same way, only with different children, for instance, the SubTopic concept editor is built just like this:

SubTopic editor

You can also use the Alt + 2 command on part of the editor to modify an elements properties, such as its background color by editing the no base style part and adding a text-background-color property. We did it on the name element of our MainTopic using the # B3FFCF color and also on our SubTopic by using a small function that checks if the parent node is also a SubTopic or not to get an insight on how deep the topic, this function should end up looking something like this:

Function to pick a color in the editor

Another aspect that can be added is actions, they are functions that are used in the editor. They usually take the form of copy/paste functions wrapper, adding new functionalities to them, or simply make sure the copy/paste functions are working correctly since using a projectional editor can sometimes lead to some trouble using it. In our case we will be using the node factory action that regulates how nodes are created, by assigning them properties and such while being able to access the
model, compared to the Behavior aspect’s constructor that couldn’t.

We can go ahead and access the Marker concept and create a new Node Factories action aspect for it. This action will increment the marker’s name using the behavior function we defined in the mindmap, we will set its name to IncrementMarker. We will create a new set-up and set the newNode’s name that is the marker’s name to the string value of the enclosing mindmap node’s getHighestMarkerNumber function plus one. This will increase the name of the marker everytime we create a new one. The final line of code should look like this:

Marker incrementation function

Now we have a fully functional mindmap editor, with colors, a nice layout, some added features and constraints to the editor. Although it is nice to have a way to create good mindmap file, the end goal is to be able to create a LaTeXfile to write about each topic.

Text generation

Text generation in MPS allows us to use layouts, functions, a complete language to create the perfect text files combined with specific functions dedicated to text generation to append and indent our text.

For our mindmap, we will focus on a relatively simple text generation in LaTeXand will simply use the append function and some conditions for the subtopics. If you don’t know anything about LaTeX, you can check out this tutorial to get started and be able to follow the fields we’ll be using.

To do so, go into the MindMap concept and create a new TextGen aspect with a Concept TextGenDeclaration. All of the fields of our textgen can be filled by pressing to create the function it will use. We will set the file name to node.name and the extension to “tex”, we won’t change the encoding but feel free to do so if you use a special language that requires it. We don’t need context objects and we will directly edit the use textgen of ancestor field to edit it. We will use the append function to create the default lines of our latex document followed by new lines for each tag and we will set the title to be the node.centralTopic.name property, the content of our document will simply be an append of our \${node.topic}, with the end result looking like this:

Mindmap base template for text generation

Now we can go on to our CentralTopic concept and create a textgen. Since this concept isn’t a root concept, we have a lot fewer elements we can modify, so we will simply edit the statement and since we’re already using the topic’s title in our mindmap textgen, here we will simply append the children main topics and a new line using append \$list{node.mainTopics} \n.

We can now add a text gen to our MainTopics concept and edit the statement to append the text \\section{ followed by the \${node.name} and finally the closing bracket } and a newline. On the next line, we can append the subTopics list.

Finally we can create a textgen for our SubTopics concept and add a condition that checks if the parent node is a SubTopic, creates a new subsection if it isn’t, and a new paragraph if it is. This condition is also followed by an append for the subSubTopics children as we can see here:

Text generation of a SubTopic

You can now create a mindmap file, edit it, create your new topic and press Ctrl + Shift + F9 or right-click and use Preview generated text to get access to your structured LaTeXfile !

We now have our fully functional language with a text generation that creates LaTeXfiles according to the elements we created.

What now ?

If you want to get more information on how to create more complex languages, use code generation instead of the simple text generation or simply to get ideas on the capabilities of MPS, you can head on to the https://www.jetbrains.com/mps/learn/.

Discussion

markdown guide