
If we need more than one generator for the same language, things can become complicated. With help of generation plans, we can sort generators apart cleanly, and let the user decide which ones to engage.
This article follows Sergej Koščejev’s excellent idea on language design patterns. |
The example used in this article is available on GitHub. We refer to concrete names of our example in parenthesis. |
Let’s assume we have a nice domain-specific language, our users enjoy working with it. However, at some point the fruits of their labor needs to be available outside of MPS — and often times in more than one format. For example, we might want to generate some executable Java code, some HTML documentation, and JSON representation from the same model. Moreover, some users might want only a subset of these outputs.
Each of these targets can easily be implemented as MPS generator. As soon as we tried to handle all outputs in the same generator, we run into issues:
We need to copy the input once for each output.
Generator rules to different target languages are intermixed.
We cannot use $COPY_SRC$
or $COPY_SRCL$
macros any more.
It’s hard to control which outputs are actually generated.
To solve this issue, we propose clean separation of our language and generators.
We design our actual DSL (Entities
) as usual; however, the DSL language does not contain a generator.
For each output format, we create a separate language (Entities.gen.baselanguage
and Entities.gen.xml
), dubbed generator language.
A generator language defines only one concept called generator enabler (GenerateXml
).
Other than that, the generator language contains a generator from our actual DSL to one target language.
In the generator’s is applicable
section, we make sure the whole generator gets toggled by presence of a generator enabler.
We create one genplan solution (Entities.genplan
).
It contains one model with one genplan for each generator language.
In our example, this means two models: Entities.genplan.baselanguage
and Entities.genplan.xml
.
One additional model (Entities.genplan.genplan
) contains the main generator plan.
The main generator plan forks to each of the generator language genplans, i.e. creates a copy of the current input model for each fork and processes them independently.
Finally, we create a devkit (Entities.devkit
).
It contains our actual DSL, all generator languages, and the genplan solution.
Make sure to attach the main generator plan to the devkit.
In a user model (Entities.sandbox
), our user can create DSL concepts as they please (my.special.entities
).
As long as they don’t create any of the generator enablers, generating the model will yield to no outcome.
As soon as our user adds an instance of a generator enabler (Generate BaseLanguage
, Generate XML
), the corresponding generator will execute on the next model generation.
This design solves all original issues:
MPS' genplans take care of copying the input.
Each generator language is cleanly separated from other generators, and focuses on one target language.
All macros work as if there’s only one generator.
We control a generator’s activation at one central place.
In real-world applications, users are often confused by generator enablers as root nodes. They prefer to have one place to configure all generation concerns; that might include additional aspects like an output directory.
We can accommodate for that by an additional language:
Create a generator config language, e.g. Entities.gen
.
Create an interface IGeneratorEnabler
.
Create a rootable concept GeneratorConfig
with a list of IGeneratorEnabler
as children.
Make each generator enabler implement IGeneratorEnabler
.
The generator enablers themselves stay in their own generator language.
Adjust the generator’s is applicable
sections to look inside the GeneratorConfig
for their enablers.
Instead of several root nodes, the user creates one GeneratorConfig
in their model, and adjust its contents to their needs.
Both the GeneratorConfig
and each generator enabler might define additional members to configure the generation process.
Delete generator from actual DSL. (If the actual DSL already contains a generator, create one generator language first, then move the generator’s contents there.)
For each target language, create a new language, dubbed generator language.
Add generator enabler concept (e.g. GenerateXml
): If an instance of this concept is present in a model, this generator will execute.
Set instance can be root
to true
.
Set alias
to e.g. Generate YourTargetLanguage
.
Add #alias#
editor component.
Add an is applicable
constraint to the generator language's mapping configuration
:
is applicable:
(genContext)->boolean {
genContext.inputModel.roots(GenerateXml).isNotEmpty;
}
GenerateXml
refers to the previously added generation enabler.
Implement the generator for one target language.
Create a new solution, dubbed genplan solution.
For each generation language, add one model to the genplan solution.
Add jetbrains.mps.lang.generator.plan
to Used Languages.
Create a new Plan
instance.
Name it actual DSL name - target language name
.
Add an ApplyGenerators
step.
Refer to the generator in generation language.
Entities - XML
apply
Generator Entities.gen.xml.generator
Add a model for the main generation plan to the genplan solution.
Add jetbrains.mps.lang.generator.plan
to Used Languages.
Create a new Plan
instance inside the main generation plan model.
Name it actual DSL name default
.
For each generator language, add one fork
step to the main generation plan.
Add a dependency to the corresponding genplan model.
Example: model Entities.genplan.genplan
depends on Entities.genplan.baselanguage
and Entities.genplan.xml
.
In the fork
step, refer to the corresponding genplan.
Entities default
fork with Entities - BaseLanguage Entities.genplan.baselanguage
fork with Entities - XML Entities.genplan.xml
Create a new devkit.
Add the actual DSL, all generator languages, and the genplan solution to the devkit.
Press Apply button.
Assign the main generation plan to the devkit.
In any user model’s Used Languages, replace the actual DSL by the devkit.