mystral
Mystral
A Crystal language server that stays out of your way.
Mystral is an experiment in how far the parser alone can take an editor. Hover, go-to, completion and friends answer from a parser-built index — no compiler on the request path, so they're instant (sub-millisecond). Types are AST-shaped: exact where the syntax spells them out (x = Foo.new, a param's : T, a declared ivar), best-effort otherwise, and nothing rather than a guess. The compiler runs only in the background — for diagnostics and to reap richer facts — never on a keystroke.
How fast
Measured on real projects (release build, warm index — what a hover actually hits):
| project | files | lines | indexes in | hover p50 / p99 |
|---|---|---|---|---|
| kemal | 72 | 7.2k | 12 ms | 0.10 / 0.30 ms |
| lune | 166 | 19.5k | 45 ms | 0.03 / 0.10 ms |
| mint | 754 | 34k | 62 ms | 0.06 / 0.90 ms |
The one slow thing — a full crystal build --no-codegen (~3 s) — runs debounced in a subprocess and is skipped when reachable content hasn't changed. Memory is ~60–70 MB resident, almost all the symbol index (workspace + stdlib + shards), bounded — light for an LSP. (Developed on a fanless 2020 M1 Air, 8 GB.)
What works
- Hover, go-to-definition, completion, references, signature help, document/workspace symbols, document highlight, formatting.
- Hover shows doc comments, param types, instance/class vars, block args, locals, and annotations (
@[JSON::Field], …). - Indexes your workspace plus everything on
crystal env CRYSTAL_PATH(stdlib,lib/shards) at startup — hovering into a dependency is as fast as your own code. - Platform-split files: the require-graph walk evaluates
flag?(:darwin)against the host, so stdlib's 23 per-platformLibC.opensignatures collapse to yours.
Not yet: rename, inlay hints, code actions.
Diagnostics
- Syntax — instant, on every keystroke, no shellout.
- Semantic — on a settled edit (and at startup), a debounced
crystal build --no-codegensubprocess catches undefined methods, type mismatches, wrong arity. Content-hash cached (an unchanged program is never recompiled); one compile refreshes every open file. Never a stale or false squiggle — if requires don't resolve (deps missing), you get a red line on the offendingshard.ymldependency and a "runshards install?" toast, not noise on require lines.
A different bet
The established tools — crystalline and vscode-crystal-lang — drive navigation through the real compiler. That's the exact, correct path; Mystral isn't trying to replace it. Mystral bets that most editing doesn't need type inference, and keeps the compiler off the request path for parser-speed answers.
The cost, named honestly — what genuinely needs the type system, Mystral approximates or sits out:
- generic instantiation and
is_a?union narrowing, - cross-include / cross-module dispatch by type,
- macro expansion across files.
Shrinking that list is the work. A background side-index reaps crystal tool output off the hot path — content-hash keyed, served on the next hover at parser speed. Two slices are in: inheritance through a generic superclass (class B < A(Int32)) resolves via reaped type hierarchy, and an untyped local (arr = [Foo.new]) fills in from an on-demand crystal tool context run.
| Mystral | compiler-backed | |
|---|---|---|
| Approach | AST + parser index | real compiler, per request |
| Type accuracy | AST-shaped | exact |
| Hover (warm) | sub-millisecond | seconds |
| Hover detail | + docs, params, ivars, block args, locals, annotations | compiler types |
Install
make deploy # specs + release build + install to /usr/local/bin/mystral
(macOS: the Makefile rm -fs before cp — adhoc-signed binaries get SIGKILLed if overwritten in place; don't swap it for a plain cp.)
- VSCode —
cd editors/vscode && npm install && npx vsce package && code --install-extension mystral-vscode-*.vsix. Prefers/usr/local/bin/mystral, then$PATH. Logs at/tmp/mystral.log(MYSTRAL_DEBUG=1for verbose). - Helix / Neovim — merge editors/helix/languages.toml or drop in editors/neovim/mystral.lua. Any editor with a generic LSP client works —
mystralwith no subcommand serves over stdio. - CLI —
mystral check FILE.crprints every symbol the indexer sees.
Tests: make test (unit) and make test-integration (shells out to crystal).
Acknowledgements
The VSCode extension ships the Crystal TextMate grammar from vscode-crystal-lang (license). Thanks to it and crystalline for charting the compiler-backed path.
License
MIT — see LICENSE.
mystral
- 0
- 0
- 0
- 0
- 1
- about 2 hours ago
- May 28, 2026
MIT License
Thu, 28 May 2026 19:22:33 GMT