Building an interpreter cookbook
Check out the Shapes sample project, which is bundled with MPS since version 3.1. It can do some fancy tricks in the editor.
By default, the editor shows plain code that consists of commands to draw visual shapes of given sizes and colors on a canvas.
The code can be generated into Java and run. This will start a new Java process running the application that just has been generated from the code above:
MPS can, however, also interpret the code without generating Java. Just hit Alt+Enter anywhere in the code to invoke the Intentions pop-up menu and choose "Preview Scene". You'll get a new frame containing your scene, which is interpreting the code:
Instant preview
With the ability to interpret code the editor can now serve the results of the interpreter back to the developer through the editor. It can either interpret individual shapes and draw them in the editor next to the code that defines each of them:
Or the whole scene can be drawn next to the code and react instantaneously to the changes made to the code.
To try these capabilities yourself, right-click the editor, pick "Push Editor Hints" and select the hints that will indicate to the editor, which of the preview capabilities you want to enable:
This was a quick teaser, now we can have a look at how to build such simple interpreting capabilities into your language.
Defining an Interpreter
MPS is primarily focused on code generation. Programs represented as ASTs get translated into code in a target language, which then may get compiled and run. There are, however, situations when interpreting your code directly may be a better option. You could give the developers some sort of Preview of what the code does and thus providing instant feedback to their code changes. You could detect and report errors that only show at run-time Or you could just want to speed up code-run-fix cycle turnaround by bypassing the generation and compilation phases.
MPS does not come with any infrastructure for building interpreters at the moment, but the Behavior aspect of your language gives you some power to build interpreters on your own. Ideally, just like with code generators, you would would create a runtime solution holding the interpreter runtime classes. The Behavior aspect of your language concepts would then cooperate with the runtime classed to navigate the AST properly and interpret all nodes in the mode.
We'll have a look at how the Shapes language enabled interpreting its code.
A whole-scene Interpreter
Let's start with the scenario when the user invokes the interpreter explicitly by an Intention. First, the intention needs to be created:
The intention invokes the interpret() method on the Canvas node, which is defined in the Behavior aspect of Canvas.
The method builds a new Frame that will hold a panel with a customized paintComponent() method. The panel is being reused by other preview functionality, so it has been extracted into a separate helper class PreviewFactory. For other than trivial cases the class should be placed into a runtime solution, but here we've just put it directly into the Behavior aspect of the language.
The current model is traversed with the code: thisCanvas.shapes.forEach({~it => it.drawShape(graphics)});. This is where the interpreting happens. The drawShape() method is implemented by each Shape subconcept so that they can get rendered on the screen. Notice that the traversal code is wrapped inside a ReadAction to be guaranteed a read permission to the model.
The drawShape() method has to be implemented by all Shapes:
This is enough to get our little interpreter off the ground.
Shape preview
To preview individual shapes next to their definition in code, we need to change the editor or define a new one with a different editor hint , so that it holds a swing component containing the shape's visualization. In the sample we chose to leverage multiple projections (see the Multiple Projections video to see how multiple projections work in MPS) and thus we created a new editor with the ShapePreview hint specified in the header. Only when the user enables the ShapePreview hint, this editor will be used instead of the default text-only one.
The swing component defines a JPanel, which in the paintComponent() method obtains a read lock on the model and then draws the current Shape node at a specific location. Notice that a new drawShapeAt() method has been added to Shapes to draw the Shapes ignoring their defined position within the canvas.
Scene preview
To preview a whole scene, we'll need to create a new editor for the Canvas concept and hook it to the ScenePreview editor hint.
The swing component cell contains a customized JPanel, which is again created by the PreviewFactory class just like when we were interpreting on explicit user request earlier on.
Now, this is it. I hope you liked it.