Language Server

Installing

cargo build --all will build the language server. Installing it is as easy as putting the binary somewhere on the path. My preferred way to do this is to create a symlink to the binary.

cd ~/.local/bin
ln -s $CALYX_REPO/target/debug/calyx-lsp calyx-lsp

Editor LSP clients will know how to find and use this binary.

Developing the Language Server

The language server protocol is general communication protocol defined by Microsoft so that external programs can provide intelligent language features to a variety of editors.

The Calyx language server is built on top of tower-lsp, a library that makes it straightforward to define various LSP endpoints. The core of the implemention is an implementation of the LanguageServer trait.

Server Initialization

The initialize method is called when the server is first started, and defines what the server supports. Currently, we support:

  • definition_provider: This handles jump to definition.
  • completion_provider: Handles completing at point.

Here we also define how we want the client to send us changes when the document changes. Currently, we use TextDocumentSyncKind::Full which specifies that the client should resent the entire text document, everytime it changes. This simplifies the implementation, at the cost of efficiency. At some point, we should change this to support incremental updates.

Server Configuration

The Calyx language-server supports two methods of configuration: the newer "server-pull" model of configuraton where the server requests specific configuration keys from the client, and the workspace/didChangeConfiguration method where the client notifies the server of any configuration changes.

The only configuration that the server supports is specifying where to find Calyx libraries. Details for how to specify this configuration is found here.

Architecture

The Backend struct stores the server configuration, and all of the open documents. This is the struct that we implement LanguageServer for. There is a lock around the map holding the documents, and a lock around the config.

For certain things, like jumping to definitions out of file, and finding completions for cells defined out of file, computations that start from one document, need to search through other documents. This is handled through the QueryResult trait in query_result.rs. This is documented in more detail in the source code, but provides a mechanism for searching through multiple documents.

Tree-sitter Parsing

We use tree-sitter to maintain a parse tree of open documents. We could theorectically use the Calyx parser itself for this, but tree-sitter provides incremental and error-tolerant parsing and a powerful query language that make it convenient to use.

The grammar is defined in calyx-lsp/tree-sitter-calyx and is automatically built when calyx-lsp is built.

Debugging the Server

Most clients launch the server in subprocess which makes it annoying to see the stdout of the server process. If you build the server with the log feature enabled, the server will log messages to /tmp/calyx-lsp-debug.log and will write the tree-sitter parse tree to /tmp/calyx-lsp-debug-tree.log. Use the log::stdout!() and log::update!() macros to write to these files.

[tree-sitter]: