This repository contains the JetBrains MPS submission for "The State of the Art in Language Workbenches: 12 years later" paper. It holds a simple implementation (excluding satisfiability checking) of the Questionnaire Language (QL) as described in the original QL assignment.
Authors: Klemens Schindler (@klemensschindler) and Eugen Schindler (@eugenschindler)
The original JetBrains MPS QL implementation is in the repository https://github.com/DSLFoundry/mps-lwc14, which in turn is based on https://github.com/DSLFoundry/mps-lwc13.
The documentation for input to the paper can be found here:
- Short summary including brief history of JetBrains MPS
- To open and run the implementation, see further below in this README file. Statistics are at the bottom of this README file.
- QL feature table for JetBrains MPS
- LWB feature table for JetBrains MPS
- MW feature table for JetBrains MPS
- Install JetBrains MPS 2025.1 from the list on https://www.jetbrains.com/mps/download/previous.html for your platform
- Run
./gradlew(the used MPS and gradle 7 needs java 21) after cloning this repository in order to download all dependencies and build the language - Open the root directory of this repository in MPS
- To play with an example QL model, open the root node QuestionnaireLanguage → Sandbox → Sandbox → QLBasic → HouseOwning
This section sketches the language implementation highlights and especially the differences in effort and amount of code needed to implement the language in MPS 2025.1 (and included batteries) as compared to the original last implementation of QL from 2014 in MPS 2.5.
Since version 2.5, MPS and its ecosystem of commercial and community-based libraries has evolved quite much. Most notably, for the new implementation of QL, we have heavily used the KernelF extensible "funclarative" language (contained within the IETS3.opensource project). In practice, the most frequent use case of KernelF we have encountered, is to just "plug-in" and tailor (advanced) expressions instead of having to develop an expression hierarchy and various useful functionality for it every time from scratch in a language.
Another useful extension is the plaintextgen plugin which helps to easily generate all types of custom text from MPS models. Since we used HTML as generation target, we could also have used the built-in jetbrains.mps.lang.html language to generate the code, but we chose plaintextgen to illustrate a quick and easy method of getting text out of models.
- Each of the concepts mentioned in the QL assignment has been implemented 1:1 as an MPS concept.
- Similar as in the original implementation in 2013, we have used the IQuestionBlockContents (now renamed to IFormContents) interface concept to allow for free creation and movements of lines in the structural projectional editor of MPS.
- The Empty concept represents empty lines. In addition, we have the DerivedValueReference and QuestionReference concepts in order to model references in the language.
- Since the de.slisson.mps.richtext language already existed to model rich text, we have done this again in the new implementation, since richtext suffices for the text of a question. If we would have had a need for more advanced text modeling (à la LaTeX, including references to other part of the model, tagging, document structure, etc.), we could have used the mbeddr.doc language (with various out-of-the-box generation targets like HTML, Markdown, and LaTeX/PDF).
- In order to model the type of a question, we chose to simply hook into the KernelF language, using KernelF's Type concept. This enables us to leverage the KernelF infrastructure of typing. What this does in practice, is that we can simply import the KernelF simpleTypes language (org.iets3.core.expr.simpleTypes) on model-level in order to get a whole set of standard types, such as Boolean, NumberType, and String. The only effort it takes to leverage all this infrastructure, is to simply make the
typechild of theQuestionconcept of typeType (org.iets3.core.expr.base). - Like a
Question, aDerivedValuealso has a KernelFType, but in order to derive a value, we also need an expression. Here, we simply make a child calledvaluewhich is of typeExpression (org.iets3.core.expr.base). This gives us access to the full set of expressions possible in KernelF (ranging from simple binary expressions like addition or multiplication, to dot expressions, option types, logic expressions, (in) equalities, etc.). In order to make sure that we connect to the KernelF typesystem, we have to add a type inference rule that makes sure that the type of aDerivedValue.valueis equal to the expected type. This makes sure that whatever type computation happens via the KernelF infrastructure, we will get feedback from the editor if the resulting type of the expression invaluedoes not fit withexpectedType. - In the same way, in order to have advanced conditions for the if-blocks in QL, we just need to make the
conditionchild of theConditionalBlockconcept of typeBooleanExpression (org.iets3.core.expr.base). - In order to allow for using references to question and derived value definitions in expressions like derived values or conditions of a conditional block, we simply hook these references into KernelF as custom expressions, so they can compose with the KernelF expression machanism. This is accomplished by simply making the
QuestionReferenceandDerivedValueReferenceconcepts extendExpression (org.iets3.core.expr.base). - In MPS 2.5 there was no plaintextgen plugin, so we would have been forced to either make a textgen aspect for each concept directly (which is really not fun to do and a lot of work) or to define an intermediate language for HTML (which also didn't yet exist out of the box in MPS 2.5). Using plaintextgen, however, making a code generator quickly for a specific case is as simple as copy-pasting the reference implementation into a plaintextgen model (which models lines, words, and general layouts of plain text) and then simply add generator macros to the relevant parts of the reference implementation in order to templatize the static reference implementation text, which is pretty similar to typical template-driven code generation.
- Using KernelF's expression infrastructure adds quite some advanced functionality to a language with almost zero effort. If the language is relatively simple, however, one may want to restrict the plethora of possible expressions of KernelF to only a subset (e.g. only additions, subtractions, multiplications, and divisions). Though it doesn't cost much effort, it needs some considerations. Details are beyond the scope of this text.
- kotlin base language instead of java base language
- constraint-based typesystem
- shadow models or Dclare4MPS (for QL, this can enable a live programming workflow, transforming to generated HTML code incrementally and continuously while editing the model)
- see the MPS Extensions documentation for an overview of community extensions
- see MPS Platform Docs for a nearly exhaustive overview of the most used non-commercial third-party MPS extensions
- the build language itself is a generation target, which makes it possible to build extensions to the MPS build mechanism itself
In the last 12 years, MPS itself has done steps to further engineer and stabilize the tool. Many bugs have been fixed, existing features streamlined, and performance highly optimized. The basic meta-interfaces of the MPS structure aspect have stayed stable, while many other aspects and additional extension capabilities and features (such as custom aspects, testing and debugging infrastructure for various aspects, mass node manipulation, many productivity features in the IDE, generation plans, ...) have been added.
In addition, commercial and community extensions have been developed that add productivity and power to the toolbox of MPS language developers. This enables a quite concise and powerful way to write and maintain ecosystems of languages and language stacks for people who are knowledgeable with MPS language development, while keeping basic language creation still relatively accessible to new MPS language developers.
-
Total code written: 1023 nodes
- Language: 256 nodes
- Generator: 621 nodes
- Build solution: 106 nodes
- Example model: 40 nodes
-
Total implementation effort (excluding documentation and build setup, including design): 5 person hours
-
Total documentation effort (README, Summary, Excel sheets): 10 person hours
-
Total coffee consumed: 7 cups
-
Total cookies/candies eaten: 11
-
In the last (mps-lwc14) implementation, the amount of total nodes is a factor of 43 more than the current implementation (mps-lwc25). Let's be generous and say that the SAT checking made the code base twice as complex, then we still come to more than a factor of 21 in size reduction!
-
The last implementation took 5 people several evenings, while the current implementation was 2 people (excluding one cosmetics commit from JetBrains) for 2 short evenings (excluding documentation). We don't have exact statistics to factor out the exact implementation time from the design time, but the significant reduction in amount of total implementation code (measured in nodes), should give a pretty good idea of how much less the implementation work itself was.
In this specific QL implementation, we attribute the high reduction in code (and hence implementation effort) to KernelF (which saves having to make from scratch entire expression concept hierarchies) and plaintextgen (which saves textgens or intermediate languages and drastically reduces the effort of templatizing reference implementations from plain text).
With this data, we can conclude that the QL implementation may not be a benchmark to extensively measure the power of language implementation in MPS. It may be time to update the benchmark to a level that is more challenging for MPS and its opensource ecosystem of "batteries included" :-)