MPS 2020.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 new Lightweight 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

Ldsl2

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

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

Ldsl4

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

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

The actual condition is specified in the Inspector:

 

Ldsl20

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

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

Ldsl8

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

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

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

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

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: 26 February 2021