domain specific language creation using jetbrains mps - part 2
In the previous post (see here) we described a problem case, and we gave an solution to this case in Java. However, as the title implies, we should develop a solution that will use a Domain Specific Language (DSL), from which we are going to generate Java code. This will be the purpose of this blog post.
Creating a DSL is definitely not an easy task. There are a lot of aspects that need to be considered before someone starts developing the DSL. Apart from this, one should really consider if it is really worth to invest time and money into developing a DSL. The first and most important step in this procedure is to understand and model the domain you are working on. Based on this model and the terminology of your domain, you can start creating your DSL.
There are various tools which can help you with the development of your DSL. Two well known tools for this task are Epsilon from Eclipse, and Meta Programming System from JetBrains. In this post we are going to use the latter one. As this post is not intended to be an introduction to MPS, it is highly recommended to familiarize your self with MPS.
Before proceeding with the steps needed to create the DSL, let’s have a quick look on how this language will look like. As the printing company said to us, their employees know nothing about programming and, thus, they want to configure and initialize the printer in a simple way. As our understanding of “simple way” could be different from their understanding, we interviewed most of their employees in order to get a better idea on what is simple for them. They told us that an acceptable (for them) way to program the printer could look like:
printer test {
product : 100
unit unit1 : a
unit unit2 : b
unit unit3 : c
}
This should translate as: “We have 100 products that need to be printed. The printer has 3 printing units and each unit is responsible for printing a specific item on a product (in this example unit 1 prints ‘a’, unit 2 prints ‘b’, and so on).”
Since our customer’s requirements are clear to us, we begin to create this specific DSL on MPS.
Let’s start by creating a new project on MPS of type Language Project. For this project, we specify the project name as TUE_Print
, the language as PrintingLanguage
, and we choose on MPS to generate both runtime and sandbox solutions.
Now that we created our project, it’s time start creating the structure of our language. In order to do that, we need to create two concepts; one for the Printer, and one for the PrintingUnit. In order to do so, right-click the “structure” folder under the “PrintingLanguage” and select “New > Concept”.
This concept will be named “PintingUnit”. We would like to be able to give to this concept a name and in order to do this, we make this concept implement the INamedConcept. Furthermore, each PrintingUnit should also have a character that will print on the products. This can be set in the properties of the concept, where we define a property of type string, named as “prints”. The setup of your concept should look like this:
concept PrintingUnit extends BaseConcept
implements INamedConcept
instance can be root: false
alias: <no alias>
short description: <no short description>
properties:
prints : string
children:
<< ... >>
references:
<< ... >>
The next concept that needs to be defined is Printer, and we proceed in a similar way. However, there are two things that we need to take into account. The first point is that the printer is going to be a root concept in our DSL. The second point is that, as we explained before, a printer consists of (zero or more) PrintingUnits and, as a result, we need to define the PrintingUnit as children of our concept. In addition to these two points, we need to remember that we also need to define the number of products that this printer is going to process. This can be done by setting a property (named “product”) of type integer.
concept Printer extends BaseConcept
implements INamedConcept
instance can be root: true
alias: printer
short description: <no short description>
properties:
product : integer
children:
listOfPrintingUnits : PrintingUnit[0..n]
references:
<< ... >>
The third step is to modify the appearance of our concepts. In other words, we need to define the way these concepts will appear in an editor. If we skip this step, we will get the default appearance, which looks like:
printer test {
product : 30
list of printing units :
printing unit test {
prints : a
}
}
This appearance, however, is not the one that our client wants.
In order to adjust the appearance of our concepts, we need to make changes in each concept’s editor. In order to do this, click on the PrintingUnit_Editor. If you see no editor defined, right-click in the gray area and select “new concept editor”. For the concept of PrintingUnit, we make it’s editor look as following:
<default> editor for concept PrintingUnit
node cell layout:
[- unit { name } : { prints } -]
inspected cell layout:
<choose cell model>
This means that each unit will be shown in a separate line with the format: unit {name} : {prints}, exactly as our customer requested.
Next, we need to make the following changes to the editor of the concept Printer:
<default> editor for concept Printer
node cell layout:
[-
[- printer {name} {-]
[- product : {product} -]
[/
(/ % listOfPrintingUnits % /)
/empty cell: <default>
}
/]
-]
inspected cell layout:
<choose cell model>
Let’s see some things about the notation used above:
[-
means that we’ll start presenting a collection using the indent layout.[/
means that we’ll start presenting a collection in a vertical way.(\
means that we’ll start presenting child nodes in a vertical way.You can find more information about the notation of MPS editors in the following website: https://confluence.jetbrains.com/display/MPSD33/Editor
Right click on the PrintingLanguage project and select “Rebuild language”.
Go to your project’s sandbox and create a new sandbox for the concept Printer.
You can start writing using this DSL in the sandbox editor.
For example, you can define the following printer:
printer test {
product : 30
unit test : a
unit unit1 : b
unit unit2 : c
}
We have defined our DSL, but we have not defined yet how the Java code will be generated.
This can be achieved by creating a generator in MPS:
Our goal here is to make this class look like the Printer class of the previous blog post. Let’s have a look again at this class:
package me.eparon;
import java.util.List;
import java.util.ArrayList;
import me.eparon.machine.*;
import me.eparon.product.*;
public class Main {
public static void main(String[] args) {
List<Product> products = new ArrayList<Product>();
for (int i = 0; i < 24; i++) {
products.add(new Product());
}
PrintingUnit unit1 = new PrintingUnit();
PrintingUnit unit2 = new PrintingUnit();
PrintingUnit unit3 = new PrintingUnit();
unit1.initialize('a');
unit2.initialize('b');
unit3.initialize('c');
Printer printer = new Printer();
printer.addPrintingUnit(unit1);
printer.addPrintingUnit(unit2);
printer.addPrintingUnit(unit3);
printer.initializeProduction(products);
System.out.println("results");
printer.performPrinting();
}
}
Our code generator has to create this class based on the parameters specified by the user in the DSL. That being said, we need to dynamically generate products, PrintingUnits, and then start the printing.
Let’s start by right-clicking at the “main@generator” and choosing “Model properties.” In the “Dependencies” tab, add a dependency to “java.util@java_stub”, and then rebuild your generator. This will allow MPS to access java’s libraries from our class.
Apart from Java’s libraries, we would also like to use the classes we created in the previous blog post, as they are essential for creating the Main class from the code generator. To be able to use these classes, we need to let the generator know about them. We can do this by right-clicking on “generator/PrintingLanguage”, selecting “Module properties”, and clicking “Add Model Root” in the “Common” tab. Select “javasource_stubs” click on the new item that appears in the list. Next, right-click on the “home” icon and choose “change root folder”. Find the folder of the package we created in the previous post, select the folder, click on the “Models” blue-folder icon, and then press OK. Rebuild the generator. The last step is to right-click on “main@generator”, select “Module properties” and add a dependency to our newly added package.
Now we can create our generator, which will look like:
public class Main {
public static void main(String[] args) {
List<Product> products = new ArrayList<Product>();
int noOfProd = $0;
for (int i = 0; i < noOfProd; i++) {
products.add(new Product());
}
$LOOP$[PrintingUnit $[unit] = new PrintingUnit();]
$LOOP$[->$[unit].initialize('$[c]');]
Printer printer = new Printer();
$LOOP$[printer.addPrintingUnit(->$[unit]);]
printer.initializeProduction(products);
System.out.println("results");
printer.performPrinting();
}
}
In this code;
$0
is a property macro and refers to the product property of the printer. In other words, this macro replaces 0 with the number of products the user defined in the DSL.$LOOP$[PrintingUnit $[unit] = new PrintingUnit();]
is a LOOP macro that iterates over the items of the collection listOfPrintingUnits
.$[unit]
is a property macro that gets the name of each unit of the loop.->$[unit]
is a reference macro that refers to the name of each PrintingUnit.Let’s rebuild our generator. Now go to your sandbox and find the sample code we have written in our DSL, right-click on the code, and select “Preview Generated Text”. In the new tab that opens you can see the Java code that was generated based on our DSL and looks like:
package PrintingLanguage.sandbox;
/*Generated by MPS */
import java.util.List;
import me.eparon.product.Product;
import java.util.ArrayList;
import me.eparon.machine.PrintingUnit;
import me.eparon.machine.Printer;
public class Main {
public static void main(String[] args) {
List<Product> products = new ArrayList<Product>();
int noOfProd = 30;
for (int i = 0; i < noOfProd; i++) {
products.add(new Product());
}
PrintingUnit test = new PrintingUnit();
PrintingUnit unit1 = new PrintingUnit();
PrintingUnit unit2 = new PrintingUnit();
test.initialize('a');
unit1.initialize('b');
unit2.initialize('c');
Printer printer = new Printer();
printer.addPrintingUnit(test);
printer.addPrintingUnit(unit1);
printer.addPrintingUnit(unit2);
printer.initializeProduction(products);
System.out.println("results");
printer.performPrinting();
}
}
That’s all for now! In the next post (see here), we’re going to see how we can specify “Recipes” for the Printer, which will allow us to use only specific PrintingUnits each time to print on products!