Millet, a language server for Standard ML (SML), is now available.

Millet logo

I’d like to:

  1. Introduce some of the main features of the project.
  2. Note some caveats and potential areas of improvement.
  3. Talk a bit about its development.
  4. Close with some thanks.

Features: an overview


The extension provides syntax highlighting, as well as bracket and comment configuration.


There are snippets for common language constructs.

Millet suggesting a snippet for a case expression

Inline errors

Parse errors, type errors, and more show up directly in the editor.

Millet showing an error that a function is not exhaustive

Every error has an error code. In the screenshot, that’s the blue number 5011 next to the the error message. Click it for a more detailed explanation.

Hover for type/documentation

Hover over an expression, pattern, type, or keyword to see more information about it.

Millet showing information about the type of an expression when hovering

When something has a polymorphic type, Millet displays both:

Millet showing first the most general type of a function, then the specific type induced by the types of the function arguments

Most items from the standard basis library also have documentation available on hover.

Millet showing showing the general type, specific type, and documentation for List.foldl, a standard basis library function

Go to definition/type definition

Jump to (or peek) the definition of a value, type, structure, signature, or functor.

Millet peeking the definition of a structure member

Additionally, if the item has a type, it’s possible to jump to (or peek) the definition of the type of that item.

Since types can be composed of other types, there may be many options to jump to. In that case, all available options are shown.

Millet peeking all the involved type definitions of an expression


Millet supports the full grammar of Standard ML. In addition, Millet parses various “holes”, like _ and ....

Though these “holes” are rejected in later stages of analysis, a programmer can use them as placeholders. Millet even reports the inferred type of hole expressions in the error.

Millet showing an error for an expression hole, noting its inferred type

Code actions

Millet provides code actions to reduce manual typing of boilerplate code.

The “fill case” quick fix automatically fills a case expression with all the variants of a datatype.

Multiple files

To allow for large projects with many files, Millet has support for some common SML “group” file types:

These file types tell Millet what files in the project to analyze, and in what order.

Limitations: a caveat

At time of writing, there are some major limitations of Millet:

See also this doc discussing some of the limitations CMU 15-150 students may run into, and how to try to fix them.

With enough spare time on my (or others’) part, these limitations might be addressed in the future.

Development: a narrative


It was the early months of 2020, the last semester of my undergraduate career at CMU. I was a teaching assistant for 15-150, our introductory-level functional programming class, taught in Standard ML.

This being the fifth time I’d TA’d that class, I saw once again a pattern I had become familiar with. I would see students grapple with not just the “deep ideas” of the course, but “surface level” issues.

In my view, the whole point of any course is to provoke thought about the “deep ideas”. So the first category of struggling seems to me more acceptable, or at least, less avoidable.

But these “surface level” issues were not inherent to functional programming itself. Rather, they were about things like the quality of the error messages reported by the implementation of Standard ML that we chose to use.

To me, these issues seemed more solvable. I thought that with improved tooling for SML, students would be able to focus on thinking about the fundamentals of FP. That way, they could minimize time spent doing things like deciphering inscrutable error messages.

In particular, I desired good editor integration. With this, the code editing experience is elevated above the common tedious loop of:

  1. Write some code in an editor.
  2. Switch to the terminal to compile it.
  3. Compile it, and inspect any errors.
  4. Switch back to the editor to make modifications.
  5. Return to step 1.

Instead, in many cases, the programmer enters the much tighter loop of:

  1. Write some code in an editor.
  2. There is no step 2, because errors appear in the editor.

Now, while 15-150 is the class I was most involved in as a TA at CMU, I found that I particularly enjoyed other classes involved with programming languages. In addition to 15-150, classes like:

rank among my most favorite at CMU.

So, with the combination of:

I set to work implementing a suite of tools for Standard ML.

First attempt

Rather early along, I realized the most important of these tools would be a language server. This is the core of the “in-editor” experience. So I focused on that.

However, the requirements for a language server are rather different from that of a traditional compiler. The major difference is this:

It wasn’t until I was basically done with the MVP that I realized this. Thus, in this first iteration, Millet would immediately halt when there was even one error in the program. This is basically unacceptable for a language server.

This, combined with:

meant that I ended up putting the project on hold indefinitely.


Somewhere in there, I wrote another language server, this time for C0, CMU’s own teaching language used in 15-122, the intro-level imperative programming course. This time, I wrote the language server more like a language server and less like a compiler.

However, it was only after I got it to MVP status that I realized one already existed, and it:

So I stopped work on my own.


The project remained dormant until May 2022, when a friend shared a screenshot with me. It was a discussion about Millet in a CMU student Discord.

I could hardly believe it. Even in its unfinished state (which was noted in the discussion), Millet was still attracting some interest.

This alone was enough to get me back in action. First I modernized the old code and tests:

Then, alongside the old code, I wrote a completely new implementation, using what I had learned about the requirements of language servers.

After reaching parity with the old implementation, I deleted the old to continue with the new. It’s since become better than the old implementation ever was.

It’s faster

I profiled it, and made copying of some key data structures more efficient. This sped it up by 15x on one particularly large 15-150 homework handout.

It handles more language constructs

For instance, it now supports:

In addition, it handles other language constructs ostensibly supported in the old implementation with much fewer bugs.

It’s built like a language server

This means it’s able to:

It has more features

In addition to showing errors inline (or, well, the old implementation really just showed “error” inline), it now has:

It supports multiple files

The old implementation would analyze each SML file in the workspace in isolation. This meant files could not import or export things from one another.

Now, Millet uses its (limited) support for MLB and CM files to process many files.

Thanks: a recognition

I’d like to give thanks to some folks that helped me along the way: