Alongside the usual language aspects, such as Structure, Editor, Type-system, etc., it is possible for language authors to create custom language aspects (e.g. interpreter, alternative type-system, etc.), have them generated into a language runtime and then use these generated aspects from code.
What is a custom aspect?
Language definitions in MPS can be thought of as a collection of aspects: structure, editor, typesystem, generator. Each of the aspects consists of declarations used by the corresponding aspect subsystem. For example, the type-system aspect consists of type-system rules and is used by the type-system engine.
Each aspect of a language is now defined in a separate aspect model. For example, the editor aspect of language L is defined in the L.editor model.
Each aspect is described using a set of aspect's main languages. E.g. there's the j.m.lang.editor language to describe the editor aspect.
Declarations in an aspect model may or may not be bound to some concept (like an editor of the concept is bound to a concept, mapping configuration in the generator aspect is not bound).
The aspect can be generated into a language's aspect runtime, which represents this aspect at runtime, in other words, when the language is used.
Since version 3.3, MPS allows language authors to define new aspects for its languages.
Development cycle of custom aspects
- Create a language to describe the aspect - you may reuse existing languages or create ones specific to the needs of the aspect. For example, each of the core MPS aspects uses its own set of languages, plus a few common ones, such as BaseLanguage or smodel.
- Declare that this language (and maybe some others) describes some aspect of other languages - create an aspect descriptor
- Develop a generator in the created language to generate aspect's runtime classes (if needed)
- Develop the aspect subsystem that uses the aspect's runtime
We'll further go through each step in detail.
Look around the sample project
If you open the customAspect sample project, you will get five modules.
The documentation language and its runtime solution are used to define new documentation aspects for any other language, sampleLanguage utilizes this new aspect - it uses it to document its concepts. The the sandbox solution shows the sampleLanguage's usage to view its documentation ability. The aspect subsystem is represented by the pluginSolution, which defines an action that shows the documentation for the concept of the currently focused node.
Before we move on, let's consider for a second how language aspects work at runtime.
- For each language in MPS, a language descriptor is generated (the class is called Language.java).
- Given an aspect interface, the language descriptor returns a concrete implementation of this aspect in this language (let's call it AspectDescriptor).
- The AspectDescriptor can be any class with any methods, the only restriction is that it should implement a marker interface ILanguageAspect. We suggest that an AspectDescriptor contains no code except getters for entities described by this aspect.
This is how a typical language runtime looks like:
The createAspect() method checks the type of the parameter expecting one of interfaces declared in aspects and returns a corresponding newly instantiated implementation.
This is how the interfaces defined in aspects may look like (this example is defined in the Intentions aspect):
Using the language aspects
Now, let's suppose we would like to use some of the aspects. E.g. while working with the editor, we'd like to acquire a list of intentions, which could be applied to the currently selected node.
- We first find all the language runtimes corresponding to the languages imported
- then get the intentions descriptors for each of them
- and finally get all the intentions from the descriptors and check their for applicability to the current node
The overall scheme is: Languages->LanguageRuntimes->Required aspect->Get what you want from this aspect
So your custom aspect need to hook into this discovery mechanism so that the callers can get hold of it.
Implementing custom aspect
Let's look in detail into the steps necessary to implement your custom aspect using the customAspect sample project:
- To make MPS treat some special model as a documentation aspect (that is our new custom aspect), an aspect declaration should be created in the documentation language. To do so, we create a plugin aspect in the language and import the customAspect language.
- Create an aspect declaration in the plugin model of the language and fill in its fields. This tells MPS that this language can be used to implement a new custom aspect for other languages.
- After making/rebuilding the documentation language, it's already possible to create a documentation aspect in the sample language and create a doc concept in it.
- Now, we should move to the language runtime in order to specify the functionality of the new aspect as it should work inside MPS. In our example, let's create an interface that would be able to retrieve and return the documentation for a chosen concept. To do so, we create a runtime solution, add it as a runtime module of our documentation language and create an interface in it. Note that the runtime class must implement the ILanguageAspect interface. To satisfy our needs, the method must take a concept as a parameter and return a string with the corresponding documentation text.
- In the generator for the documentation language we now need to have an implementation of the interface from above generated. A conditional root rule and the following template will do the trick and generate the documentation descriptor class:
The condition ensures that the rule only triggers for the models of your custom aspect, i.e. in our case models that hold the documentation definitions (jetbrains.mps.samples.customAspect.sampleLanguage.documentation).
The useful feature here is the concept switch construction, which allows you to ignore the concept implementation details. It simply loops through all documented concepts (the LOOP macro) and for each such concept creates a matching case (exactly ->$[ConceptDocumentation]) that returns a string value obtained from the associated ConceptDocumentation.
- So we have an interface and an implementation class. Now, we need to tie them together - we have to generate the part of the LanguageRuntime class, which will instantiate our concept, i.e. whenever the documentation aspect is required, it will return the DocumentationDescriptor class. To understand how the following works, look at how the class Language.java is generated (see Language class in model j.m.lang.descriptor.generator.template.main). The descriptor instantiation is done by a template switch called InstantiateAspectDescriptor, which we have to extend in our new aspect language so that it works with one more aspect model:
Note: as we have extended a template switch from another generator, we should also add an "extends" dependency to the documentation's language generator
Essentially, we're adding a check for the DocumentationAspectDescriptor interface to the generated Language class and return a fresh instance of the DocumentationDescriptor, if the requested aspectClass is our custom aspect interface.
- The only thing left is using our new aspect. For that purpose, an action needs to be created that will show documentation for a concept of a node under cursor on demand:
The jetbrains.mps.ide.actions@java_stub model must be imported in order to be able to specify the context parameters. The action must be created as part of a (newly created) plugin solution (more on plugin solutions at Plugin) with a StandalonePluginDescriptor and hooked into the menu through an ActionGroupDeclaration:
This way the IDE Code menu will be enhanced.
- Let's now try it out! Rebuild the project, create or open a node of the DocumentedConcept concept in the sandbox solution and invoke the Show Documentation action from the Code menu: