Child pages
  • Shapes - an introductory MPS tutorial

Versions Compared

Key

  • This line was added.
  • This line was removed.
  • Formatting was changed.

...

First, here's a bit of background knowledge that you should know before going on. This is a piece of code that you will be able to create once you implement the Shapes language fully.

The language that we're building must allow for painting definitions, which consist of individual commands, each on a separate line and each defining a single shape to draw. Our language needs to cover each such command with a Concept. Concepts define the abstract syntax of a language, i.e. the set of allowed language logical constructs. A program then consists of Abstract Syntax Trees, which hold instances of these Concepts.

...

Info

If you misspelled the name of the concept and decide to change it, MPS will prompt you, whether you want to do a proper rename re-factoring, in order to update all possible references and usages of the concept in other parts of your code. Since we've just started and have no references to Shape, just hit the "No" buttonjust go ahead and change it. Renaming things in MPS is just as easy.

It is the time to practice the Alt + Enter keyboard shortcut now.

...

Having created two shapes we can move on to defining a concept to hold them all in a painting. We'll create another concept, called Canvas, that will represent a scene composed of shapes. The user will be able to create multiple scenes (Canvasses), which will be mutually independent and will not share shapes. Each Canvas will hold a name and a list of shapes that it contains.

...

Navigate (using Tab) to the "<none>" text in the implements section and specify INamedConcept (either type it in or use Control + Space). INamedConcept is a Concept Interface that Canvas will now implement. Concept Interfaces can also , just like Concepts, add new capabilities to Concepts that implements implement them. INamedConcept in our case enriches Canvas with the name property, so Canvas instances (called Nodes) will have a name property so the user can easily distinguish between them.

...

Hit Enter to create a new cell. Type "x:" to mark that the following cell contains the value of the x property.

Hit Enter again to create a new cell. Remember to press Enter first each time you want to add a new cell and then select the kind of cell to insert from the completion menu (Control + Space).

Now you have to pick the x property from the code-completion menu to bind the cell to the right property. Do not just type {x}, use Control + Space to display the completion menu and select the x property from the menu: 

Now you can continue on your own to insert cells holding constant text and well as values of the y and radius properties. Remember, Enter will insert new cells, Control + Space will bring up the code-completion menu. You should end up with an editor like this.

...

The editor for the Canvas concept will be slightly different, since it holds a collection of shapes that all need to be given some space on the screen. Canvas thus spreads across multiple lines and holds a collection of child nodes, each line will show one shape from the Canvas' collection of shapes. You, however, start in the same way as before, open the Canvas concept, hit the '+' symbol to create a new Concept Editor, insert an indent layout and enter some text to get the following:


Now bind the red cell to the name property. Since the property is inherited from the _INamedConcept _ INamedConcept concept interface, it will be further down in the listcompletion menu (Control + Space), but it is surely there.

You ensure that you have inserted the name property correctly if you can see the curly braces wrapping it. (Beware, you are not supposed to insert these curly braces yourself, they are added by MPS for all property cells.)
The following cell will hold a collection of shapes that have been added to the Canvas. Note how we compose editors here - Canvas only marks an area, in which individual shapes should be edited, and leaves that up to the shapes how they use that area.
Since the shapes should be organized vertically, each on a single line, you should pick vertical collection layout from the completion menu.

Image Removed

Be careful here. If you have selected a different layout than vertical, the shapes will not be nicely organised one below another, but perhaps one next to another on the same line, which, while quite innovative, may not be the most intuitive way to use your language..

Image Added

You need to bind the red cell to the shapes child collection of Canvas.Image Removed

Image Added


Image RemovedImage Added
Now, to place the collection below the name of the Canvas, you should use the Alt + Enter shortcut (or the light-bulb symbol) the bring up the intention pop up menu and pick "Add On New Line".

Image Removed
Image Added

If you accidentally press "Add New Line", undo that action with Control/Cmd + Z.
You'll get the final editor definition:

Image Removed

...

Image Added

If you version looks different, there may be several reasons:

  • You inserted the name property from the completion menu. If you only typed "name", the cell is most likely not bound to the name property and will only be ever showing the text "name" when used.
  • You set the "Add On New Line" intention. If you accidentally chose "Add New Line" instead, the shapes cell will not be drawn below the "Painting ..." first line, but next to it.
  • If you set the "Add On New Line" property on a different cell that the shapes cell, the layout will also look different from the expected one.
  • You chose a different layout for the shapes collection than vertical. Different layouts use different symbols. Vertical uses "(>", while "(-" and "(/" belong to the indent and horizontal layouts, respectively.

In all these cases repetitive Undo (Control/Cmd + Z) will help.

The second run

Now you can rebuild the project again (right-click on the very top node in the Project View) and look at the sandbox code in  MyDrawing.

...

It is the same code (AST), but it is organized on the screen differently. While previously it was an ugly tree-like text with lots of curly braces, now the code reflects the editor definition that we provided to MPS.

If your view is different, revisit the three editors. Given that they are fairly small, feel free to delete then and do them over, if needed. You can delete a whole editor definition if you locate it in the Project view under the Editor aspect model and press Delete or right-click and choose delete in the context menu. Alternatively, you may keep pressing the Delete and Backspace buttons inside the editor definition until you remove all the suspiciously looking code.

Coloring

Now we should come back to Shape and add support for colors. Since both Circle and Square extend Shape, they will both inherit the color.

...

Since we would like to allow the users of our language to pick the color for the shape from a list of pre-defined colors, we can't just use a textual property to hold the color value. Instead we'll add a reference to Shape and have this reference point to one of the pre-defined color constants. Let's start with creating such constants that will represent pre-defined colors.

Concept for colors

One way would be to use MPS enums to define the colors. This would, however, not allow users to define their own colors. All the colors would be defined in the enum that would be a part of the Shapes language. Instead, we will use full a blown concept for colors and nodes of that concept will define the individual colors. These color nodes can be defined as part of the language (inside so called accessory models) or directly in user models next to instances of the Canvas concept.

First we're going to create a concept that will represent a color constant. We'll call it Color and it will be rootable, so that we can place it inside models:

...

An editor is also needed:

Pre-defined colors

Now, rebuild your project. The Color concept that we have just added needs to be compiled, so that we can use it to create some colors. We need to provide concrete nodes of the Color concept, which will represent the individual color constants and which the user of our language will be able to refer to from their Canvasses. We will utilize Accessory models for this. Accessory models are models inside a language definition that hold arbitrary nodes, which become part of the language and are visible to the language users.

...

The model demands a name. Please make sure the stereotype box is empty, if it is not disabled by MPS:

The Shapes language, which declares the Color concept, is the only language that we need imported in the accessory model:

Switch to the Used Languages tab. It is either empty or contains a devkit that we can leave in the list. Click the plus symbol and select the Shapes language from the list. The language must have been added to the list of used languages.

Then switch to the Advanced tab and select the Do Not Generate checkbox:

Image Added

Then click OK to close the dialog.

Create colors

Now you should be able to create color constants in the newly created accessory model:

Image Removed

...

If you do not see Color in the New menu, you most likely forgot to rebuild the language. Or, you have missed the can be root property of the Color concept, which has to be set to true.

Image Added

The first touch on dependencies

A crucial piece of knowledge in MPS is how to handle dependencies and imported languages. To display the dependencies of a module or a model, you have to navigate to it in the left-hand side Project View panel (Alt/Cmd + 1 to open the panel). Try that for the Shapes language. Select it in the tree:


Hit Alt + Enter (or right-click on it and choose Module/Model Properties) and you'll get a dialog with the properties of the module/model. The Dependencies tab shows the modules/models that the current module/model depends on (aka import in Java, require in Ruby, etc.).

...

Yours is most likely empty now. You add elements using the '+' button. In the small search dialog type a few characters of the name of the desired dependency to narrow down the search and hit Enter whet  when you locate the correct one.

...

We do not need to change anything here, so let's continue by clicking the OK button.

Info

If you, at any point when working with MPS, cannot type the code that we propose in the tutorial, most likely you have not properly set the dependencies. So lease please pay attention to the places, where the tutorial mentions dependencies and suggest that you should add a dependency or import a language.

...

Now our language needs a way to indicate the desired color of a shape in our code. Thus Shape should be referring to one of the color constants in the accessory model of the Shapes language. MPS will automatically populate the completion menu with all available color constants, whenever the user is about to specify a color for a Shape.

Create a new concept in the Structure aspect model and name it ColorReference:

ColorReference keeps a reference (a pointer going across the AST hierarchy) pointing to a single color constant (node of the Color concept).

...

The Shape concept is a good place to put our new ColorReference, since both Circle and Square will inherit it. Open it in the editor an make the change:


Shape may also define an editor component to define the editor for the color and Circle as well as Square will be able to reuse that editor component in their editors thus avoiding duplication.

...

Let's start with the class for Canvas. You need to add a new entry in the root mapping roles, since Canvas is a root concept. Hit Enter to insert a new empty rule:

This will indicate that Canvas nodes should be replaces with a Java class. You need to use an Intention (Alt + Enter) the be able to select "New Root Template" from the pop-up menu.

This rule defines that a Canvas node should be replaced with a Java class. The template defines with what class:

The root template should be generating a Java class, so you need to pick "class".

...

First we need to set the dependencies of the generator module and the generator model as specified below:

Image Removed

 

Image Removed

Remember,  to depend on the JDK module. Without this dependency you would not be able to compile your sandbox eventually.

Note: Remember, Alt + Enter will bring up the properties of the node selected in the left-hand hand Project View.

Image Added

Second, the generator model must depend on java.swing and java.awt, as specified below. Without these dependencies you would not be able to type the Java Swing code needed to implement the generator templates:

 

Image Added

With the dependencies you can start typing the Java code that will be part of the generated Java class. We will then parametrize the code with values from the Canvas, to make it reflect the user's intent.

...

Parametrizing the template

The code in template currently holds no values specified by the user. The does not use any values from the input model. It simply hardcodes the code and so the code generated by the template will always be the same, no matter what shapes are created in the user model. This has to change. The template must react to the input model and the generated code must reflect the shapes, their sizes and colors. The properties and children of Canvas should be inserted into the template through macros. MPS gives you three types of macros*:*

  • property macros - to insert properties from the input model
  • node macros - to replace nodes in the template with nodes from the input model
  • reference macros - to adjust references in the template to point to nodes in the input model

...

Now, please select the placeholder statement including its closing semicolon using the Control/Cmd + Up Arrow key shortcut, hit Alt + Enter and choose the appropriate Node macro option to insert a LOOP macro to loop through all the child Shapes of the current Canvas.

...

Again, the Inspector (Alt + 2) shows the binding code.

If the LOOP macro is underlined in red, most likely you did not select the whole line before applying the intention. Undo, select the whole line including the semicolon and apply the Add LOOP macro intention again.
The LOOP macro will repeat the "System.out.println("Draw here");" statement for each shape listed in node.shapes.

We, however, need to have the "System.out.println("Draw here");" statements replaced with code that draws each of these shapes. The COPY_SRC macro will do just that. Please, select the whole statement again, within the LOOP macro again, including the semicolon, hit Alt + Enter and choose Node macro.

...

Type in COPY_SRC (Control + Space) and you get a macro that will replace "System.out.println("Draw here");" with the current shape for all shapes that the LOOP macro provides.


Make sure your COPY_SRC macro wraps the whole statement, including the semicolon, like it is displayed in the picture. If not, undo, select the whole statement and insert the COPY_SRC macro again.

Generating circles

Now we get Canvas to be translated into a Java class and we also made a place for Shapes to add the code that will draw them. The time is up for us to define the actual translation rules for Shapes themselves, so that we get a "graphics.drawCircle()" method inserted in the generated code as a replacement for the Circle shape. You need to open the main mapping configuration and add a new entry to the "reduction rules" section:

...


Open the reduce_Circle template. We now need to specify the Java code that will replace Circles. Remember, that the Java code will be placed in map_Canvas inside the paintComponent method.

First, we'll enter a BlockStatement, that will wrap our template:

...



Then use Control/Cmd / Up Arraw to select the inner BlockStatement, hit Alt + Enter and  (light-bulb) and pick "Create Template Fragment" from the intentions menu. This will mark the selected fragment of the code as the actual template, which will eventually be placed into map_Canvas

...

First, the generator module has to depend on BaseLanguage in order to be able to refer to the StatifFieldDeclaration StaticFieldDeclaration concept, which is declared in that language:

...

Second, the generator model needs to be able to refer to the concepts defined in structure of the Shapes language:

With these languages imported we should be able to enter the code that discovers the correct static field declaration within the Color class:

Image RemovedImage Added

The node-ptr/.../ construct allows you to obtain a node in the imported models of a specified concept represented with the given name. Since there only exists one Color class in JDK, the reference identified as node-ptr/Color/ will be unique and will be pointing into the model to the Color class.

Note: Make sure you pick the right Color concept  element from the completion menu:

Image Removed

. It must be java.awt.Color, not the Color concept from the Shapes language.

Image Added

Node pointer represent persistent references to nodes. To get a reference to the real node in memory, the node pointer must be resolved in the model repository. Use the following code to obtain the repository and resolve the Color node from it:

Image Added

The downcast operator gives you access to the underlying Java API, which is currently the only way to get a repository in this place.

Image Added

Using the collections language you can complete a concise query:

Image RemovedImage Added

Since node is an instance of the Circle concept, node.color is the circle's reference to the color (an instance of ColorReference) and node.color.target is a Color (an instance of the Shapes.Color concept) from the accessory model.

...

We'll start mimicking how generator is done for Circles. Identically provide the following code for the reduce_Square template.

Hint: Start by inserting a BlockStatement


The values passed into "drawRect" should be replaced with property macros with the upperLeftX, upperLeftY and size properties of the Square.

...

Info

The drawRect() method's third and fourth parameters specify the rectangle's width and height, respectively. Since we want to draw a square, we need to provide the same values for both arguments. This is why the property macros for the third and fourth parameters both specify size.

Generating code

Now we're done defining the generator. If you rebuild the language, open MyDrawing, right-click it and choose "Preview Generated Text",

  Image Added
you'll get a nicely structured Java code that properly initializes a JFrame and draws all the shapes:

Image Added

If your code is different, look into one of your generator templates, they are probably different from the ones presented in the tutorial. Maybe your macros are not attached to the correct pieces of code or the values specified in the Inspector window for the macros differ from the ones in the screen-shots.

If the code does not compile, make sure your generator module depends on the JDK module, as we defined earlier.

A more robust generation for Squares

The way we handled the graphics local variable in the templates was not quite right. We relied, perhaps too optimistically, on the name of the variable to be the same in map_Canvasreduce_Circle and reduce_Square. What if the names of the variable in these three templates were not the same? A more robust solution is needed

As indicated earlier in the section for the Circle generator, we'll use the reduce_Square template to properly tie the graphics local variable with the graphics parameter that the map_Canvas template generates. Relying on name match is not very robust.

...

We could now replicate the retrieval of the graphics parameter for the reduce_Circle template, as well.

Generating code

Now we're done defining the generator. If you rebuild the language, open MyDrawing, right-click it and choose "Preview Generated Text",

  Image Removed
you'll get a nicely structured Java code that properly initializes a JFrame and draws all the shapes:

Image Removed

Running the code

It is nice to see generated code, but you might actually prefer seeing it running. MPS can compile and run generated Java code easily. We only need to indicate that Canvas is generated into a runnable Java class and thus Canvas itself should be treated as runnable, or as a "main" class. We only need to make Canvas implement the IMainClass interface and MPS will take care of the rest. The IMainClass interface comes from the jet brains.mps.execution.util language and so we need to add it to the list of dependencies of our language and set the scope to Extends:

Use the Alt + Enter to get the properties dialogs. Notice that the language needs to be marked as Extends.

...


You will get a running Java application with your drawing on it as a reward for your efforts.

An alternative generator - generating XML

Just to give you an idea how the generator could be utilized to generate code in a declarative language, such as xml, here's a simple generator for the Shapes language generating xml. We start from an empty generator. The Java templates and rules have all been deleted:

Image Added

First, the jetbrains.mps.core.xml language must be imported (Control + L). It is the projectional equivalent to xml, as we know it, just like BaseLanguage is a projectional equivalent to Java.

A root mapping rule must be created to convert Canvases into xml files.

Image Added

Image Added

The template named map_Canvas gets created.

Image Added

Xml code must be typed into the template in order to create the required code:

Image Added

Image Added

In order to insert the name attribute, type "space" followed by "name=":

Image Added

A property macro must be set on the contents of the name attribute value:

Image Added

Image Added

Some more xml needs to be inserted:

Image Added

We create a placeholder for all the shapes:

Image Added

With Control/Cmd + Up select the placeholder xml element:

Image Added

Then insert a COPY_SRCL macro to loop through all the shapes of a Canvas and trigger their reduction rules:

Image Added

Image Added

The name of the generated xml file can also be customized using a property macro.

Image Added

Image Added

Reduction rules for Square and Circle now need to be created:

Image Added

Image Added

The templates for Circle and Square must hold an XmlElement as their root:

Image Added

Then the xml template must be fully constructed:

Image Added

template fragment must be created around the whole xml code:

Image Added

Image Added

The TF symbols indicate the template fragment:

Image Added

Property macros should be used to parametrize the template:

Image Added

The property macro must be further specified in the Inspector so that the correct value is used and converted from integer to string:

Image Added

The same must be repeated for the y xml attribute.

For radius, we will use a dedicated xml element:

Image Added

Image Added

And quire similarly for the color reference:

Image Added

The template for Square will be very similar:

Image Added

The generator should be holding four root nodes now:

Image Added

When you rebuild your language and preview the generated code for your sandbox, you should be getting an xml file similar to this:

Image Added

What to do next

Congratulations! You've just completed your introductory tutorial into MPS.

...