MPS 2023.3 Help

MPS Kotlin language

Getting started

Working with MPS Kotlin requires installation of the type system plugin. You may find instructions on how to do that on the plugin page. This plugin is necessary to get adequate types for the code you will be writing.

Once installed, all you need to do to enable the type system is to toggle the Coderules widget on the bottom right side of your MPS window. Coderules is the framework on which the type system is based.

mps_kotlin_1.png

On a side note, once the code is written down in your model, the plugin (and coderules) are not always needed for generation/textgen/compilation (only if a sublanguage uses typesystem features in its generator).

Writing Kotlin code in MPS

In order to write Kotlin code in MPS, the first step is to import the language into your model. You can use the following devkits that bundle several useful languages for you:

  • jetbrains.mps.devkit.kotlin - a pure kotlin language

  • jetbrains.mps.devkit.kotlin.jvm - includes the kotlin language, baseLanguage and additional languages that provide interoperability from one to the other

Additionally, you can import jetbrains.mps.kotlin.smodel if you need to manipulate MPS nodes in your code.

You should then be able to write kotlin code by adding new kotlin files to your model. Please refer to the official kotlin documentation for a tour of what you can write with the language (the syntax is similar and the editing experience aims at giving you similar experience to a text-based IDE).

mps_kotlin_2.png

Importing libraries

Kotlin library support is currently limited to kotlin common libraries. It is currently not possible to import kotlin stubs from java classes. On the compilation/runtime, it is also not possible to add kotlin compiler plugins at the moment.

If you have a library that you want to use (eg. Ktor), you can import stubs from the “common” distribution of your library (usually a .jar containing .kotlin_metadata files instead of .class files). If you wish to run your code on the JVM, you can also attach the library’s JVM distribution into the classpath. As an example, we can try to set up Kotlinx Serialization Core for our project. There are two distributed jars for this library:

  • kotlinx-serialization-core - common distribution

  • kotlinx-serialization-core-jvm - jvm distribution

You can create a new solution for hosting this library, in this solution properties, you can add a “Kotlin Common” stub model root:

mps_kotlin_3.png

Select your common distribution jar (kotlinx-serialization-core.jar) to add it. You should now see a new entry with few kotlin_metadata detected. Then, mark the jar itself as “Sources”, to get the following result:

mps_kotlin_4.png

This alone would allow you to write code using this library. If you need to compile it to jvm, move to the java tab to add the jvm distribution (kotlinx-serialization-core-jvm.jar).

mps_kotlin_5.png

If your module does not already use kotlin, you might also need to add dependencies to the kotlin stdlib in “Dependencies” (so kotlin core types get resolved). Similarly, if your library also depends on other Kotlin common libraries, you would also need to depend on them (or import them using the same process if needed).

You should now be able to write code using this library. In this example (kotlin serialization), we would also need to set up a compiler plugin for it to work at runtime, but this is not supported by MPS at the moment.

Exporting your work / build scripts

While Kotlin code should be compiled and work along with java code in the editor, a small extra step is required to have that compilation reflected in the build script.

Each module requiring kotlin compilation in a build script needs to be marked so manually. You may do so from the inspector.

mps_kotlin_6.png

Note that this option is not compatible with the fork option of the java compiler.

Extending the language

Kotlin for MPS provides several facilities to make several aspects of the language extensible and compatible with existing concepts. Here is an overview of some of those features.

Types and type system

Typing your nodes (without using coderules)

In the pure kotlin language, almost all concepts fall into two categories: literal and function calls. Because of that, the typesystem offers by default two corresponding facilities to compute types. This section describes how to use the given API if the handled use cases fit your requirements, otherwise, feel free to dive into the coderules implementation of MPS Kotlin and integrate your own rules there.

For the simple cases, one can extend the IStaticType interface, and provide the type directly from the behavior method. This should be used only if the type is straightforward because it does not rely on type system operations (eg. otherNode.type, coerce, subtype checking…).

If that does not fit all your requirements, you may try to use your node as a function call. It should be used for any node whose type might depend on other nodes (children, receiver…) and would benefit from inference. For example, this facility is used for all kinds of function calls in kotlin (x.f(), f(), x f y…) but also all operators (+, -, []...) and some structure elements (for statement).

Everything starts from the IFunctionCall concept, it requires some behavior functions to be implemented.

First, function relative to the call itself:

  • Receiver - information about the type receiving this call, if any (eg. on x.f(), this should refer to the type of x)

  • Arguments

  • Type arguments

  • Null safe - will accept nullable receiver types if set to true

Then, a single function getFunctionDescriptor that will return an object describing the function declaration.

Finally, several methods provide a facility for function resolution, if that would be applicable to your use case (otherwise, they can all return null).

  • Function name - name to search for in the scope

  • Target link (might get removed) - in the case described above, link to use to assign the resolved function to the node.

  • Modifier filter - when searching, it filters out methods that do not have the same modifier

  • Function scope parts - list of scopes to search into for resolution of the function. As Kotlin uses its own Scope interface, this method will be called both from the constraints (wrapped into a regular Scope) and from the method resolution mechanism (eg. AutomaticResolutionHelper)

Please note that many functions return an abstraction of what we’re trying to describe (eg. FunctionDeclaration interface) instead of a node. This allows both not to enforce any concept in the function mechanism (any concept can be a function declaration, even if not initially meant to be, eg. baseLanguage methods used in kotlin) and to not enforce usage of actual nodes everywhere (arguments do not have to be expressions, and can just refer to a type directly).

Another interesting aspect is the TypeReference interface. Many abstractions use it to return types rather than using the IType concept (whose usage, unlike declarations, is enforced). This allows computation to differ if the reference is used outside of the typesystem (eg. type of a node will require to make a new call to the typesystem) or inside of the typesystem (type of the same node can be retrieved from internal typesystem features directly).

Creating new types

Kotlin class types alone are quite powerful, they support use and declaration site variance and the language provides its own way of defining DSLs. However, one might still need to add new types and new features.

There are two actions to adding a type to Kotlin: adding it to the structure and adding it to the coderules type system.

When it comes to adding types compatible with the kotlin structure, you can extend the IType interface, it contains a bunch of methods to add subtyping, generics, and scopes support to your type. You can find several examples in the kotlin.baseLanguageRef and kotlin.smodel languages.

On the coderules side, things are less strict. You may use any type you want, provided you add support for all mechanisms you wish to use (inference, subtyping…). However, as diving into the type system is quite complex, it would be recommended to use the well supported classType structure. It can be used for any type that refers to a classifier (no concept enforced) and has (optionally) some type parameters.

You can check out NodeType in kotlin.smodel for an example of that. The same language bundles an example of a type that does not rely on classType and implements all operations required (see conceptType).

Last modified: 07 March 2024