on
MPS language design patterns: Multiple generators for same language
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. |
Rationale
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.
Overview
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.
Alternative for generator enablers
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 ofIGeneratorEnabler
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 theGeneratorConfig
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.
Multiple generators: a checklist
-
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.- Structure
-
-
Set
instance can be root
totrue
. -
Set
alias
to e.g.Generate YourTargetLanguage
.
-
- Editor
-
Add
#alias#
editor component.
-
Add an
is applicable
constraint to the generator language'smapping 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.
Addjetbrains.mps.lang.generator.plan
to Used Languages.-
Create a new
Plan
instance.
Name itactual 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.
Addjetbrains.mps.lang.generator.plan
to Used Languages. -
Create a new
Plan
instance inside the main generation plan model.
Name itactual 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 onEntities.genplan.baselanguage
andEntities.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.
About Niko Stotz
I head the Model Driven Engineering team at F1RE, and support our colleagues and customers with language engineering and DSL expertise.
You can contact me at niko@f1re.io.