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.
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).
Kotlin code completion
The MPS implementation of Kotlin relies on the Coderules typesystem to calculate types and constrain scopes. If Coderules is not installed or enabled, Kotlin can still provide limited scoping support despite not being able to calculate the types fully.
Platform support
Source sets
In a regular Kotlin project, you can use source sets to separate code targetting different platforms. In MPS, we introduced this at the root level, with the option to specify the set of supported platforms for each Kotlin root node. These source sets can be configured at the root node level with the help of an intention action.
In practice, this means that:
Code under a given source set can only access declarations with compatible platforms. For example, code specific to the JVM may access only JVM-specific code and common code that targets the JVM.
Generated sources are structured under source-set-specific directories. If a directory is unspecified, it uses the default source set, which corresponds to the module’s default. Expected and actual declarations are now supported.
By default, Kotlin code without an explicit platform uses the JVM, maintaining backward compatibility.
Loading and compilation for stubs
Stubs have been improved to support new multiplatform use cases. In the past, MPS offered separate options for Kotlin and Kotlin/JVM stubs, which loaded common and JVM stubs, respectively.
These options have now been unified under the one for Kotlin stubs, which now automatically determines whether a provided artifact exposes common code, JVM code, or code for other platforms.
As declarations between common and platform-specific libraries are redundant (both artifacts contain all necessary declarations), a new mechanism for filtering duplication was introduced to keep stubs tidy. When platform-specific libraries are declared under the same module, they can access common declarations, so you don’t have to declare them again.
Dependency configuration is the same as before:
Both common and platform-specific libraries can be used as stubs. JVM libraries are required to compile common code to the JVM, and they should be declared in the Java facet.
For instance, writing common code requires you to use a common library for stubs, using the common source set, but you also have to declare the Java artifact in the Java facet.
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:
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:
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).
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.
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).