MPS 2023.3 Help

Lightweight DSL

Years of evolving the MPS core languages led us to recognizing recurring patterns in our language-descriptive DSLs for IDE integration. Conceptually they looked and behaved like higher-level constructions expressed by plain classes that implement some higher-level interfaces. In the good tradition of language-oriented programming we decided to reflect these patterns in the languages and re-implement them as a thin abstraction on top of plain BaseLanguage classes. The newLightweight DSL language has been created to enable such abstractions.

The jetbrains.mps.baselanguage.lightweightdsl language enables internal DSLs to be embedded inside BaseLanguage classes. Internal DSLs in general are easier and faster to develop than full-blown external DSLs, they typically reuse the syntax of the host language and tightly integrate with the surrounding non-DSL code. Similarly, lightweight DSLs in MPS can be created by defining a single node and then weaving the node into a BaseLanguage ClassConcept or its subconcepts.

MPS itself leverages this mechanism in several places:

  • MigrationScript concept, which is a mere BaseLanguage class, is enhanced by the Migration DSLDescriptor that adds a few extra properties, members and custom members

  • Find Usages

  • Intentions

  • Custom language aspects

You can have your lightweight DSL weaved into a plain BaseLanguage class or into your own concept extending ClassConcept.

Enhancing plain classes

The core idea behind the Lightweight DSL language is to allow the DSL designer to define interfaces with constraints, such as optional methods and properties, types depending on constraints, optional method parameters, custom class members and others. The DSLDescriptor concept represents nodes that codify such constrained interfaces that should be enforced on implementing Classes. For example, the following DSLDescriptor instance will weave a numberOfFrames property into classes as well as a calculateFoo() method and a custom BuilderMember node:

dslclass SwingBuilder for concept ClassConcept { property numberOfFrames : integer ; placeholder<one frame> method calculateFoo(int value): int ; required custom member BuilderMember ; <modifiers> initializer: {node<ClassConcept> node, model model => } }

In order to have a particular DSLDescriptor take effect on a class, it needs to be annotated with a DSLAnnotation, which is done through intentions:

ldsl1.png
ldsl2.png

As soon as the annotation is added, the required elements defined in the DSLDescriptor or the ones with placeholders specified are added to the class. Deleting required elements will result in an error indicated on the class:

ldsl3.png

An intention can be used to re-add the required elements as well as the placeholders in one step:

ldsl4.png

The standard Implement method action Ctrl+I will also work for required methods.

Defining a DSLDescriptor

There are four types of elements a DSLDescriptor can add to classes:

  • properties

  • methods

  • custom members

  • an initializer

Each of these can be required, can have a placeholder defined to represent a missing member and custom members can be marked as multiple indicating that more than one node of this kind can be a member of the class.

Properties

Properties have a name and can be of type string, int or boolean. Properties become full blown members of the weaved class and can be accessed from other members, including the ones weaved in through this or other DSLDescriptor.

Methods

Methods provide a very convenient way to let users of your lightweight DSL inject code - the DSLDescriptor specifies method signatures of methods that the enhanced classes can or must implement. Unlike for normal methods, the return type as well as the types of any parameters of these weaved methods can be decided based on the actual usage in the host (enhanced) class.

Dependent types

The dependent type, when specified for a method's return type or a parameter type, specifies way to calculate the actual type of the method for a given class.

ldsl5.png

Conditional method parameters

Methods can have some parameters marked as conditional, so that they only become visible in user code when the provided condition is satisfied. To mark a parameter as conditional you can use the corresponding intention:

ldsl19.png

The actual condition is specified in the Inspector:

ldsl20.png

Custom members

If neither properties nor methods provide the required level of abstraction, custom members can be used to weave in any arbitrary concept with just a little bit of extra work.

ldsl6.png

The CustomMemberDescriptor points to a concept (BuilderMember in our case) that will be weaved into the class as a new member. For this to be possible the concept has to implement the MemberInstance and ClassifierMember concept interfaces and point back to the CustomMemberDescriptor from the overriding getDeclaration() method.

ldsl7.png
ldsl8.png

A node (or multiple nodes, if the CustomMemberDescriptor is defined as multiple) of the specified concept can now be instantiated as a member of the host class and edited right inside it:

ldsl9.png

Initializer

The initializer function gets a chance to enhance the host class programmatically as soon as it is being created or annotated with the DSLAnnotation. The node (the host class) itself may not have been added to the model when the initializer is called, thus the model parameter is provided as well as the node. If you import the smodel language you can manipulate the class and the model. Typically the initializer would set the imports and used languages or provide default values and implementations for the weaved in members, if needed.

ldsl10.png

Enhancing custom ClassConcept sub-concepts

There are some specifics if you're enhancing not BaseLanguage classes directly, but specific sub-concepts of the ClassConcept class. This allows you to combine the benefits of external and internal DSLs. You typically create ClassConcept sub-concepts instead of using plain BaseLanguage classes once your intended DSL requires customized editing experience, dedicated generator or specific type-system rules. So you create a sub-concept of ClassConcept and make it implement the AutoInitDSLClass.

ldsl11.png

Implementing AutoInitDSLClass ensures that whenever a node of your concept gets created, the weaved in DSLs get properly initialized by calling the initializer defined in their DSLDescriptor s.

ldsl12.png

The AutoInitDSLClass interface also mandates the implementing concepts to override the getDescriptor() method that should return the particular DSLDescriptor instance to weave in.

Samples

Samples illustrating basic usages of the Lightweight DSL can be found in the lightweightDSL sample project bundled with MPS distributions.

Last modified: 07 March 2024