At the time of writing (Rascal LSP 2.22.4 & VS Code 0.13.4), the situation is as follows.
The Rascal VS Code extension is shipped with specific versions of Rascal and Rascal-LSP. Language projects separately define dependencies on specific versions of Rascal and Rascal-LSP in their pom.xml (POM). When registering a language in the extension with registerLanguage, its contributions (IDE features) will run with the Rascal and Rascal-LSP versions from the extension.
This has several drawbacks:
- Differences between the versions in the contributions with other contexts, that consider the versions in the POM, like the VS Code REPL and Maven (
compile, rascal:console, etc.). This leads to confusing discrepancies between the different contexts and unpredictable results.
- Users are forced to update to a new Rascal-LSP version when the extension is updated, possibly requiring changes to their projects.
Solution: run the contributions with the versions as supplied in the POM. This document outlines the required changes to enable this.
Existing implementation
In the following examples, for simplicity, we only show the most important classes related to parametric languages. We consider a case with two registered languages: Language 1
and Language 2
.
Currently, the components of the parametric language server related to the language contributions run within a single process. This schematic shows how the classes are related to each other.
Since everything lives in the same process, they share the same Java classloader by default, and therefore the same versions of classes. However, we want different versions for different instances of ILanguageContributions.
Design
To separate the dependency versions of the language contributions from the versions of the VS Code extension, we propose to run the contributions in separate processes (1 per language). For inter-process communication, we forward LSP requests to the right process using JSON-RPC (which is already in use in many places in the Rascal/LSP infrastructure).
We use a parametric language router that manages language processes (and dependency versions/classpaths) and dispatches LSP calls to the right process over JSON-RPC. On the receiving end, we use a single language server that routes the incoming requests to the responsible service (document or workspace). This allows us to re-use many existing components without many changes.
Since this requires certain compatibility between the versions running in the main and delegate processes, LSP will put constraints on the minimal versions of DSL dependencies (Rascal and Rascal LSP).
Pros
- Relatively simple
- LSP requests can be forwarded verbatim
- Not many extra moving parts are needed
- Isolation of languages; a crash in one language does not influence others
Cons
- Memory usage grows linearly with number of registered languages
Tests
To test this properly, we should consider adding some more UI tests. Some example scenarios:
- Register/unregister multiple languages and check that IDE features for both work at the same time.
- Register multiple languages and check their dependency versions from the language contributions.
- Register a language, check its versions, change the pom, unregister, re-register, and check that the versions changed.
Alternatives considered
Demultiplex by version
The above, but with one process per version combo and multiple languages per process.
Pros
- Less memory overhead/more sharing in the case of multiple languages that use the same versions.
Cons
- More complexity
- Every request going out of the demultiplexer needs the language name.
- The version-specific process needs another demultiplexer to differentiate between languages.
- Interference of languages; a crash in one language might influence another
Custom classloader
Pros
- Less processes, more memory sharing
- No hard-to-debug JSON-RPC bridge
Cons
- Hard to implement the class loader
- Tricky to get right:
- Object equality between instances from different class loaders/versions
- Backwards compatibility between different versions
- Interference of languages; a crash in one language might influence another
OSGI
Many of the same properties as using a custom class loader.
At the time of writing (Rascal LSP
2.22.4& VS Code0.13.4), the situation is as follows.The Rascal VS Code extension is shipped with specific versions of Rascal and Rascal-LSP. Language projects separately define dependencies on specific versions of Rascal and Rascal-LSP in their
pom.xml(POM). When registering a language in the extension withregisterLanguage, its contributions (IDE features) will run with the Rascal and Rascal-LSP versions from the extension.This has several drawbacks:
compile,rascal:console, etc.). This leads to confusing discrepancies between the different contexts and unpredictable results.Solution: run the contributions with the versions as supplied in the POM. This document outlines the required changes to enable this.
Existing implementation
In the following examples, for simplicity, we only show the most important classes related to parametric languages. We consider a case with two registered languages: Language 1
and Language 2
.
Currently, the components of the parametric language server related to the language contributions run within a single process. This schematic shows how the classes are related to each other.
Since everything lives in the same process, they share the same Java classloader by default, and therefore the same versions of classes. However, we want different versions for different instances of
ILanguageContributions.Design
To separate the dependency versions of the language contributions from the versions of the VS Code extension, we propose to run the contributions in separate processes (1 per language). For inter-process communication, we forward LSP requests to the right process using JSON-RPC (which is already in use in many places in the Rascal/LSP infrastructure).
We use a parametric language router that manages language processes (and dependency versions/classpaths) and dispatches LSP calls to the right process over JSON-RPC. On the receiving end, we use a single language server that routes the incoming requests to the responsible service (document or workspace). This allows us to re-use many existing components without many changes.
Since this requires certain compatibility between the versions running in the main and delegate processes, LSP will put constraints on the minimal versions of DSL dependencies (Rascal and Rascal LSP).
Pros
Cons
Tests
To test this properly, we should consider adding some more UI tests. Some example scenarios:
Alternatives considered
Demultiplex by version
The above, but with one process per version combo and multiple languages per process.
Pros
Cons
Custom classloader
Pros
Cons
OSGI
Many of the same properties as using a custom class loader.