With the 2.0 release of Chapel, many areas of the language have seen significant improvement. Several of these improvements have changed Chapel’s compiler and ecosystem, allowing the team to create a fully-featured Language Server for Chapel. This brings tons of interactive features to your text editor, making writing and reading Chapel code easier than ever.
In this article, we show off many of the exciting features of this new tool, Chapel Language Server (CLS). Although demonstrations throughout the article are made using VSCode, CLS itself is editor-agnostic. This means that you can configure [note: The Chapel team currently actively supports VSCode and NeoVim, but any editor that supports the Language Server Protocol plugins can use these same features. If you need support getting CLS in your favorite editor please let us know! ] to do many of the same things.
Feature Highlights
Let’s walk through one of our example codes,
examples/primers/fileIO.chpl
and see the Chapel Language Server in action!
Go-to-Definition
Reading through this file, we see a procedure named writeSquareArray
being
called. While we could search through the file manually for anything named
writeSquareArray
, it is faster to go straight to its definition using
the editor’s features:
Hover for Documentation
Inside of writeSquareArray
, we see a file being opened with an ioMode
argument. This is a great opportunity to show off more features; we can hover
over it and see the documentation for ioMode
right inside the editor:
Of course, we could also jump straight to its definition in the standard modules and inspect the implementation directly.
Refactoring Code
The next feature we show off is applying code modifications. We can use the
Rename Symbol feature to change all occurrences of a symbol to a new name.
For example, here, we rename writer
to myWriter
:
Diagnostics
With CLS running in an editor, we can see warnings and errors in real time. The following code produces both a warning and an error:
|
|
Without running the compiler, we can see that the editor is already showing us this information:
Resolving Diagnostics
The last of the core features we want to show is a more complex code modification. Not only can we see diagnostics right in the editor, but in some cases we can automatically resolve them. Currently, we have a number of deprecations wired up to support this. Let’s see this in action:
All of these features mean you spend less effort on the mechanics of writing code and more time thinking about the problem you’re trying to solve.
Using CLS in your application
So far, we have shown the language server working on a single file that is only
using the Chapel standard library. The CLS features we’ve shown off so far can
also be used with large projects that have complex build systems. To support that,
we provide chpl-shim
, a utility that inspects the build process to collect
all the information the language server needs. All that is required is to prefix
the compilation command; for example, instead of chpl ...
we use chpl-shim chpl ...
.
Let’s take Arkouda, a Python
data analytics package written in Chapel, as an example. After installing all
of the dependencies, Arkouda is built by calling make
. To gather build
information for CLS, we can invoke:
$CHPL_HOME/tools/chpl-language-server/chpl-shim make
This creates a .cls-commands.json
file that contains all of the information CLS needs. Now when we open the Arkouda project in our editor, we can use all of the features we’ve shown off so far.
One thing to watch for in this demo is how fast it is. Arkouda is a large
project at nearly 40,000 lines of Chapel code. You can see this when we jump to
the definition of NumPyDType
. The first time,
[note:
These demos were done using a debug build of the compiler, causing this slight delay. When using a release build, this initial delay is greatly reduced.
]
The second time, it is instantaneous. This is because the language server is
using the Dyno compiler library, which uses a query system to quickly respond
to incremental changes in files by only recomputing what has actually changed.
(Tell me more about Dyno)
The Chapel language is currently in the middle of a compiler revamp, a project
we have been calling Dyno. Dyno is a compiler library that is used in the
chpl
compiler while also serving as a resource for other programs like the language
server. This allows tool writers to use the same parsing and resolution logic
as the compiler. For example, chpldoc
, our documentation generation tool, is built using
the Dyno library. We can also use it to build tools like the Chapel linter—chplcheck
—and
of course the language server.
Having the compiler as a library is great, but there’s much more to Dyno than that! The Dyno query system enables rapid responses to incremental changes in files by only recomputing what has actually changed. If we modify one local variable in a subroutine, we will only re-resolve aspects of that routine’s body that are sensitive to the change. This means that when using tools built on Dyno, you get real-time feedback as you write code.
Experimental Features
With Dyno, features based on type resolution are becoming available to tools, including the Chapel Language Server. Although type resolution is still a work in progress, we’d like to showcase some advanced features that rely on it.
Type Inlays
The first feature we want to show off is type inlays. These are hints that display the (inferred) type of a variable declaration if one is not explicitly provided. For example, you might have a declaration like this:
|
|
What’s the type of ‘result’? Type inlays make the answer evident at a glance:
Dead Code
We can show off another feature of CLS using if
-statements whose
branches are known at compile-time. Since the compiler discards branches that
it knows cannot be taken, the code in those branches never runs. When CLS
detects such code, it displays the dead code as a comment. For
example, consider the following excerpt:
|
|
Since we declared firstParam
to be 1
above, the else
branch will never
be taken. Because of this, CLS displays that branch using the editor’s
comment color, indicating that thirdParam
is set to "hello"
.
Generics
Let’s move on to another fun feature: displaying instantiations. Chapel
supports generic procedures. Instead of accepting only one type of expression
for each argument, these procedures allow different types at different callsites. For example, here’s
a toy procedure, assignOneToAnother
, that takes a reference to a variable and
sets that variable to a new value:
|
|
Note that we haven’t specified the type of changeMe
, and therefore, any type
can serve as an argument. We did, however, constrain the type of changeTo
.
Thus, the procedure has to be called with two integers, or two strings, but
not an integer and a string. Having defined our procedure, let’s call it with
two different sets of arguments:
|
|
Now there are two instantiations of assignOneToAnother
: one with integer
arguments, and one with string arguments. We can view both of them, and
have the type hints inform us of the types of various intermediate variables
in the procedure’s body. Here’s an animated demo:
Chapel allows compile-time inspection of types too. Generic procedures often make use of this, by checking if an argument is of a certain type and changing behavior accordingly. For example, one might see a pattern like the following:
|
|
Here, we have a compile-time procedure that checks if some type allows for
executing an operation more efficiently. In our case, we pretend that
there’s an efficient implementation of our operation (computing pi),
called doEfficientOperation
, that works only with integer arguments. For
all other types of arguments, we fall back to the imaginary “slow” version,
doSlowOperation
. In this case, to represent the fact that it’s “slow”, we
wrote it with an addition. We stress that this is a toy example, only meant
to illustrate a common pattern in Chapel code.
Finally, genericFunction
accepts any argument x
, and decides whether
it should perform the operation efficiently or slowly. It relies on the
result of calling typeSupportsEfficientOperation
.
When we view instantiations of this procedure, we see that CLS is smart enough
to figure out the value of the if
-statement in each version of the procedure,
and it highlights the branch that’s taken: doEfficientOperation
for the int
argument, and doSlowOperation
for the string
argument:
Call Graphs
The final feature we want to show off is the call graph. This feature allows
you to view the incoming and outgoing calls for particular procedures. For
example, perhaps you have found a procedure and want to determine where it
is called from. In the animated example below, we use the call graph
to find where doEfficientOperation
is used in our test file:
Notice that in this example, although genericFunction
is called twice,
it only shows up once in the list of callers for doEfficientOperation
, and
only the int
-based call to it is shown. This is because CLS detects that
only one of the instantiations of genericFunction
(namely the one with
integers) calls genericFunction
, and thus, only the genericFunction(42)
results in a call to doEfficientOperation
.
When developing Chapel code, one might be interested in figuring out why exactly a procedure has certain instantiations. The instantiations feature can be used in tandem with the call graph to individually inspect where each instantiation is called from.
You can view the file used for these examples here:
somecomplexfunction.chpl
|
|
Conclusion
With the Chapel Language Server, Chapel users have gained a powerful new tool to read and write Chapel code. We have shown off many of the features that CLS provides, with a glimpse of what’s on the horizon. We hope that you will give it a try and let us know what you think!
To try CLS, check out our list of supported editors here. If you have any questions or feedback, please feel free to reach out to the Chapel team!