Adamas: experimental LSP-first compiler work, current status, and feedback request

Hi everyone,

I want to share an experimental Crystal project I have been working on:

The short version: it started as an attempt to build a better Crystal LSP, and

over time grew into a larger compiler experiment with a new frontend, HIR/MIR

pipeline, LLVM backend, and bootstrap work.

This is not an official Crystal replacement, and the compiler/codegen side is

still beta. The LSP server is currently the most usable and stable part.

Why I started with LSP

My original motivation was developer experience. Crystal is a great language,

but for larger projects the editor experience can still be rough: slow first

responses, incomplete navigation, hover/definition gaps, semantic coloring

issues, and expensive re-analysis after small changes.

So I started with the LSP problem:

  • fast hover and go-to-definition;

  • better source-backed signatures;

  • semantic tokens that work on real Crystal/stdlib code;

  • completion/signature help without loading too much of the dependency graph;

  • document symbols, folding, formatting, rename, references, inlay hints, and

call hierarchy;

  • VS Code integration that can run side by side with the existing crystal

compiler, using crystal2 tool lsp or a configured server path.

The LSP now has a focused regression suite. The latest local gate is:


spec/lsp: 266 examples, 0 failures

It has also been tested on real projects, including Crystal V2 itself and

DiamondDB.

What is different from existing Crystal LSPs?

The main goal is not just “another LSP server”, but a compiler-backed language

server with aggressive caching and fast foreground paths.

Some design points:

  • Lazy foreground work. Opening a file should not eagerly build every index

if the first user action only needs hover, definition, or document symbols.

  • AST/project/prelude caches. The server keeps reusable summaries and avoids

redoing unchanged work where possible.

  • Fast navigation paths. Many hover/definition requests avoid dependency

graph loading and use source-backed method/type information directly.

  • Semantic token optimization. Large semantic-token responses are cached,

persisted for unchanged disk-backed files, and support LSP full/delta

responses.

  • Real-world edge cases. Recent fixes cover Crystal operator methods,

generated numeric conversions, macro calls, lib fun receivers, scoped aliases,

qualified receivers, callable parameters, and semantic coloring inside

case/when branches.

  • VS Code configurability. The extension no longer hardcodes a repo-local

server path. It can discover crystal2, crystal_v2, or crystal_v2_lsp,

and the path/args can be overridden in settings.

This is still young software, but the current LSP is usable enough that I would

like other Crystal users to try it and report concrete failures.

Why did this turn into code generation?

While building a good LSP, I kept running into the same underlying need: the

language server wants compiler-quality parsing, name resolution, type

information, macro awareness, and stable source mapping.

At that point it became natural to ask: if the frontend and semantic model are

being rebuilt anyway, can we also experiment with a compiler architecture that

is easier to cache, inspect, test, and eventually bootstrap?

That led to the current compiler pipeline:


Crystal source

-> parser / arena AST

-> HIR

-> MIR

-> LLVM IR

-> native binary

The bigger long-term idea is a compiler architecture with:

  • better incremental and cached analysis;

  • explicit IR boundaries for debugging;

  • source/HIR/MIR/LLVM equivalence checks across bootstrap stages;

  • method-level reachability analysis;

  • room for memory-management experiments such as stack/slab/ARC/GC strategy

selection;

  • a shared compiler core that can power both codegen and the LSP.

Current compiler/codegen status

The honest status: the compiler is not ready as a production replacement.

What works:

  • the V2 compiler can be built from the current Crystal compiler;

  • stage1 can build a generated stage2 compiler;

  • many focused no-prelude/codegen/bootstrap regressions pass;

  • the repository has HIR/MIR/LLVM infrastructure and many regression guards;

  • there is a spec-first contract layer under docs/specs/ for bootstrap

invariants.

What does not work yet:

  • generated stage2 is still unstable on broader full-prelude compilation;

  • clean stage1 -> stage2 -> stage3 bootstrap is still in progress;

  • codegen has known bugs and should be treated as experimental;

  • it is not a drop-in replacement for the official Crystal compiler.

So my recommendation today is:

  • try the LSP if you want editor tooling;

  • look at the compiler/codegen side if you are interested in compiler

internals, experiments, or helping with bootstrap bugs.

Things I would like Crystal V2 to eventually enable

Some of these are already partly implemented; some are still design goals:

  • very fast editor feedback on large Crystal files;

  • reliable source-backed hover/definition/signature help;

  • better semantic coloring for Crystal-specific constructs and operators;

  • incremental compilation-style dependency tracking;

  • inspectable HIR/MIR/LLVM outputs for debugging compiler bugs;

  • a bootstrap corridor with normalized semantic equivalence checks:

original -> stage1 -> stage2 -> stage3 -> ...;

  • room to experiment with memory strategies beyond today’s default model;

  • better tooling hooks around formatting, symbols, call hierarchy, and later

debugging support.

How to try it

Repository:

Build the LSP server:


git clone https://github.com/skuznetsov/crystal_lsp

cd crystal_lsp

./build_lsp.sh

The VS Code extension is in:


vscode-extension/

It can use:


crystal2 tool lsp

or a manually configured server path:


{

"crystalv2.lsp.serverPath": "/path/to/crystal2",

"crystalv2.lsp.serverArgs": ["tool", "lsp"]

}

Useful docs:

Feedback wanted

The most useful feedback would be:

  • LSP bugs on real Crystal projects, especially hover/definition/completion

misses;

  • slow files or slow request traces;

  • semantic coloring gaps;

  • crashes with a small reproducer;

  • codegen/bootstrap contributors who enjoy reducing compiler edge cases.

If you try it, please include:

  • OS and Crystal version;

  • how you launched the server;

  • the smallest .cr snippet or repo/file where it fails;

  • what the current Crystal tooling does versus Crystal V2.

Again: this is experimental, and the compiler side is not production-ready.

But the LSP is now stable enough that I think it is worth broader testing.

I congratulate you on the huge effort @ComputerMage! I will try the LSP with Zed and inform how it goes!

But I hope that the rest of the compiler could bring valuable insights to the main compiler, as well.

You did an immense work here!

This is great, thanks a lot @ComputerMage :heart:

I tried the instructions to run this on WSL2 with Crystal 1.20.1. Unfortunately it just crashes with a cryptic error.

░▒▓   …/crystal_lsp/bin   main ?   13:48 
❯ ./crystal_v2_lsp
Invalid memory access (signal 11) at address 0x7ba0fffffed2
[0x5d56619caa39] ?? +102625586227769 in ./crystal_v2_lsp
[0x5d56619ca3a4] ?? +102625586226084 in ./crystal_v2_lsp
[0x7ba088445330] ?? +135929411162928 in /lib/x86_64-linux-gnu/libc.so.6
[0x5d5663bd729d] GC_malloc_kind +221 in ./crystal_v2_lsp
[0x5d5661b1f096] ?? +102625587622038 in ./crystal_v2_lsp
[0x5d5661a8f2ad] ?? +102625587032749 in ./crystal_v2_lsp
[0x5d5661c24a2d] ?? +102625588693549 in ./crystal_v2_lsp
[0x5d566239678e] ?? +102625596499854 in ./crystal_v2_lsp
[0x5d5662362e42] ?? +102625596288578 in ./crystal_v2_lsp
[0x5d5661a8fd8e] ?? +102625587035534 in ./crystal_v2_lsp
[0x5d566226774a] ?? +102625595258698 in ./crystal_v2_lsp
[0x5d5661dc0199] ?? +102625590378905 in ./crystal_v2_lsp
[0x5d5661da2da2] ?? +102625590259106 in ./crystal_v2_lsp
[0x5d5661a8f2ad] ?? +102625587032749 in ./crystal_v2_lsp
[0x5d5661c24a2d] ?? +102625588693549 in ./crystal_v2_lsp
[0x5d5661dc00c2] ?? +102625590378690 in ./crystal_v2_lsp
[0x5d5661da2da2] ?? +102625590259106 in ./crystal_v2_lsp
[0x5d5661a8f2ad] ?? +102625587032749 in ./crystal_v2_lsp
[0x5d5661c24a2d] ?? +102625588693549 in ./crystal_v2_lsp
[0x5d5661dc00c2] ?? +102625590378690 in ./crystal_v2_lsp
[0x5d5661da2da2] ?? +102625590259106 in ./crystal_v2_lsp
[0x5d5661a8f2ad] ?? +102625587032749 in ./crystal_v2_lsp
[0x5d5661c24a2d] ?? +102625588693549 in ./crystal_v2_lsp
[0x5d5661dc00c2] ?? +102625590378690 in ./crystal_v2_lsp
[0x5d5661da2da2] ?? +102625590259106 in ./crystal_v2_lsp
[0x5d5661a8f2ad] ?? +102625587032749 in ./crystal_v2_lsp
[0x5d5661c24a2d] ?? +102625588693549 in ./crystal_v2_lsp
[0x5d5661dc00c2] ?? +102625590378690 in ./crystal_v2_lsp
[0x5d5661db501f] ?? +102625590333471 in ./crystal_v2_lsp
[0x5d5661a8f2ad] ?? +102625587032749 in ./crystal_v2_lsp
[0x5d5661c24a2d] ?? +102625588693549 in ./crystal_v2_lsp
[0x5d5661dc00c2] ?? +102625590378690 in ./crystal_v2_lsp
[0x5d5661da2da2] ?? +102625590259106 in ./crystal_v2_lsp
[0x5d5661a8f2ad] ?? +102625587032749 in ./crystal_v2_lsp
[0x5d5661c24a2d] ?? +102625588693549 in ./crystal_v2_lsp
[0x5d5661dc00c2] ?? +102625590378690 in ./crystal_v2_lsp
[0x5d5661da2da2] ?? +102625590259106 in ./crystal_v2_lsp
[0x5d5661a8f2ad] ?? +102625587032749 in ./crystal_v2_lsp
[0x5d5661c24a2d] ?? +102625588693549 in ./crystal_v2_lsp
[0x5d5661dc00c2] ?? +102625590378690 in ./crystal_v2_lsp
[0x5d5661da2da2] ?? +102625590259106 in ./crystal_v2_lsp
[0x5d5661a8f2ad] ?? +102625587032749 in ./crystal_v2_lsp
[0x5d5661c24a2d] ?? +102625588693549 in ./crystal_v2_lsp
[0x5d5661dc00c2] ?? +102625590378690 in ./crystal_v2_lsp
[0x5d5661da2da2] ?? +102625590259106 in ./crystal_v2_lsp
[0x5d5661a8f2ad] ?? +102625587032749 in ./crystal_v2_lsp
[0x5d5661c24a2d] ?? +102625588693549 in ./crystal_v2_lsp
[0x5d5661dc00c2] ?? +102625590378690 in ./crystal_v2_lsp
[0x5d5661da2da2] ?? +102625590259106 in ./crystal_v2_lsp
[0x5d5661a8f2ad] ?? +102625587032749 in ./crystal_v2_lsp
[0x5d5661c24a2d] ?? +102625588693549 in ./crystal_v2_lsp
[0x5d5661dc00c2] ?? +102625590378690 in ./crystal_v2_lsp
[0x5d5661db501f] ?? +102625590333471 in ./crystal_v2_lsp
[0x5d5661a8f2ad] ?? +102625587032749 in ./crystal_v2_lsp
[0x5d5661c24a2d] ?? +102625588693549 in ./crystal_v2_lsp
[0x5d5661dc00c2] ?? +102625590378690 in ./crystal_v2_lsp
[0x5d5661da2da2] ?? +102625590259106 in ./crystal_v2_lsp
[0x5d5661a8f2ad] ?? +102625587032749 in ./crystal_v2_lsp
[0x5d5661c24a2d] ?? +102625588693549 in ./crystal_v2_lsp
[0x5d5661dc00c2] ?? +102625590378690 in ./crystal_v2_lsp
[0x5d5661db501f] ?? +102625590333471 in ./crystal_v2_lsp
[0x5d5661a8f2ad] ?? +102625587032749 in ./crystal_v2_lsp
[0x5d5661c24a2d] ?? +102625588693549 in ./crystal_v2_lsp
[0x5d5661dc00c2] ?? +102625590378690 in ./crystal_v2_lsp
[0x5d5661da2da2] ?? +102625590259106 in ./crystal_v2_lsp
[0x5d5661a8f2ad] ?? +102625587032749 in ./crystal_v2_lsp
[0x5d56622674f9] ?? +102625595258105 in ./crystal_v2_lsp
[0x5d5661a8f2ad] ?? +102625587032749 in ./crystal_v2_lsp
[0x5d5661b8398e] ?? +102625588033934 in ./crystal_v2_lsp
[0x5d5661a8f2ad] ?? +102625587032749 in ./crystal_v2_lsp
[0x5d5661ab9e05] ?? +102625587207685 in ./crystal_v2_lsp
[0x5d5661c24a2d] ?? +102625588693549 in ./crystal_v2_lsp
[0x5d5661dc00c2] ?? +102625590378690 in ./crystal_v2_lsp
[0x5d5661da2da2] ?? +102625590259106 in ./crystal_v2_lsp
[0x5d5661a8f2ad] ?? +102625587032749 in ./crystal_v2_lsp
[0x5d5661c24a2d] ?? +102625588693549 in ./crystal_v2_lsp
[0x5d5661dc00c2] ?? +102625590378690 in ./crystal_v2_lsp
[0x5d5661da2da2] ?? +102625590259106 in ./crystal_v2_lsp
[0x5d5661a8f2ad] ?? +102625587032749 in ./crystal_v2_lsp
[0x5d5661c24a2d] ?? +102625588693549 in ./crystal_v2_lsp
[0x5d5661dc00c2] ?? +102625590378690 in ./crystal_v2_lsp
[0x5d5661da2da2] ?? +102625590259106 in ./crystal_v2_lsp
[0x5d5661a8f2ad] ?? +102625587032749 in ./crystal_v2_lsp
[0x5d5661c24a2d] ?? +102625588693549 in ./crystal_v2_lsp
[0x5d5661dc00c2] ?? +102625590378690 in ./crystal_v2_lsp
[0x5d5661da2da2] ?? +102625590259106 in ./crystal_v2_lsp
[0x5d5661a8f2ad] ?? +102625587032749 in ./crystal_v2_lsp
[0x5d5661c24a2d] ?? +102625588693549 in ./crystal_v2_lsp
[0x5d5661dc00c2] ?? +102625590378690 in ./crystal_v2_lsp
[0x5d5661da2da2] ?? +102625590259106 in ./crystal_v2_lsp
[0x5d5661a8f2ad] ?? +102625587032749 in ./crystal_v2_lsp
[0x5d5661c24a2d] ?? +102625588693549 in ./crystal_v2_lsp
[0x5d5661dc00c2] ?? +102625590378690 in ./crystal_v2_lsp
[0x5d5661db501f] ?? +102625590333471 in ./crystal_v2_lsp
[0x5d5661a8f2ad] ?? +102625587032749 in ./crystal_v2_lsp
[0x5d5661c24a2d] ?? +102625588693549 in ./crystal_v2_lsp
[0x5d5661dc00c2] ?? +102625590378690 in ./crystal_v2_lsp
[0x5d5661da2da2] ?? +102625590259106 in ./crystal_v2_lsp
[0x5d5661a8f2ad] ?? +102625587032749 in ./crystal_v2_lsp
[0x5d5661c24a2d] ?? +102625588693549 in ./crystal_v2_lsp
[0x5d5661dc00c2] ?? +102625590378690 in ./crystal_v2_lsp
[0x5d5661da2da2] ?? +102625590259106 in ./crystal_v2_lsp
[0x5d5661a8f2ad] ?? +102625587032749 in ./crystal_v2_lsp
[0x5d5661c24a2d] ?? +102625588693549 in ./crystal_v2_lsp
[0x5d5661dc00c2] ?? +102625590378690 in ./crystal_v2_lsp
[0x5d5661da2da2] ?? +102625590259106 in ./crystal_v2_lsp
[0x5d5661a8f2ad] ?? +102625587032749 in ./crystal_v2_lsp
[0x5d5661c24a2d] ?? +102625588693549 in ./crystal_v2_lsp
[0x5d5661dc00c2] ?? +102625590378690 in ./crystal_v2_lsp
[0x5d5661da2da2] ?? +102625590259106 in ./crystal_v2_lsp
[0x5d5661a8f2ad] ?? +102625587032749 in ./crystal_v2_lsp
[0x5d5661c24a2d] ?? +102625588693549 in ./crystal_v2_lsp
[0x5d5661dc00c2] ?? +102625590378690 in ./crystal_v2_lsp
[0x5d5661da2da2] ?? +102625590259106 in ./crystal_v2_lsp
[0x5d5661a8f2ad] ?? +102625587032749 in ./crystal_v2_lsp
[0x5d5661c24a2d] ?? +102625588693549 in ./crystal_v2_lsp
[0x5d5661dc00c2] ?? +102625590378690 in ./crystal_v2_lsp
[0x5d5661da2da2] ?? +102625590259106 in ./crystal_v2_lsp
[0x5d5661a8f2ad] ?? +102625587032749 in ./crystal_v2_lsp
[0x5d5661c24a2d] ?? +102625588693549 in ./crystal_v2_lsp
[0x5d5661dc00c2] ?? +102625590378690 in ./crystal_v2_lsp
[0x5d5661db501f] ?? +102625590333471 in ./crystal_v2_lsp
[0x5d5661a8f2ad] ?? +102625587032749 in ./crystal_v2_lsp
[0x5d5661c24a2d] ?? +102625588693549 in ./crystal_v2_lsp
[0x5d5661dc00c2] ?? +102625590378690 in ./crystal_v2_lsp
[0x5d5661da2da2] ?? +102625590259106 in ./crystal_v2_lsp
[0x5d5661a8f2ad] ?? +102625587032749 in ./crystal_v2_lsp
[0x5d5661c24a2d] ?? +102625588693549 in ./crystal_v2_lsp
[0x5d5661dc00c2] ?? +102625590378690 in ./crystal_v2_lsp
[0x5d5661da2da2] ?? +102625590259106 in ./crystal_v2_lsp
[0x5d5661a8f2ad] ?? +102625587032749 in ./crystal_v2_lsp
[0x5d5661c24a2d] ?? +102625588693549 in ./crystal_v2_lsp
[0x5d5661dc00c2] ?? +102625590378690 in ./crystal_v2_lsp
[0x5d5661a222bf] ?? +102625586586303 in ./crystal_v2_lsp
[0x5d5662f7d9e8] ?? +102625608980968 in ./crystal_v2_lsp
[0x5d5662f7d69a] ?? +102625608980122 in ./crystal_v2_lsp
[0x5d5662f7bec7] ?? +102625608974023 in ./crystal_v2_lsp
[0x5d5662f7be44] ?? +102625608973892 in ./crystal_v2_lsp (3 times)
[0x5d5662f7afd0] ?? +102625608970192 in ./crystal_v2_lsp
[0x5d5662f6c8c7] ?? +102625608911047 in ./crystal_v2_lsp
[0x5d56619c89ad] ?? +102625586219437 in ./crystal_v2_lsp
[0x0] ???

Paul,

Thank you very much for your kind words!
There is still some polish and bugfixing that needs to be done, but I decided to publish it anyway, because otherwise I will privately polish it forever :slight_smile:

And I hope that the community will start testing it and helping me to fix those pesky bugs.

Serdar,

Can you build and run it in a debug mode? It is very fast that way as well, but it will give me more info in a trace log on what went wrong.

Run with crystal build src/crystal_v2.cr -o bin/crystal_v2 --error-trace

░▒▓   …/crystal_lsp   main ?   14:50 
❯ crystal build src/crystal_v2.cr -o bin/crystal_v2 --error-trace
In src/crystal_v2.cr:23:19

 23 | exit_code = cli.run
                      ^--
Error: instantiating 'CrystalV2::Compiler::CLI#run()'


In src/compiler/cli.cr:1195:18

 1195 | return compile(input_file, options, out_io, err_io)
               ^------
Error: instantiating 'compile(String, CrystalV2::Compiler::CLI::Options, IO::FileDescriptor, IO::FileDescriptor)'


In src/compiler/cli.cr:1359:13

 1359 | parse_file_recursive(prelude_path, all_arenas, loaded_files, input_file, input_base_dir, options, out_io)
        ^-------------------
Error: instantiating 'parse_file_recursive(String, Array(CrystalV2::Compiler::CLI::ParsedUnit), Set(String), String, String, CrystalV2::Compiler::CLI::Options, IO::FileDescriptor)'


In src/compiler/cli.cr:3509:9

 3509 | {% unless flag?(:bootstrap_fast) %}
        ^
Error: expanding macro


There was a problem expanding macro 'macro_137462847589040'

Called macro defined in src/compiler/cli.cr:3509:9

 3509 | {% unless flag?(:bootstrap_fast) %}

Which expanded to:

 >  1 |
 >  2 |         # V2 BOOTSTRAP: Disable AST cache — File operations call check_no_null_byte
 >  3 |         # which is broken in stage2 (V2's String#byte_index falsely finds null bytes).
 >  4 |         if false && options.ast_cache
 >  5 |           source_mtime_ns = begin
 >  6 |             stat2 = uninitialized LibC::Stat
 >  7 |             if LibC.stat(abs_path.to_unsafe, pointerof(stat2)) == 0
 >  8 |               stat2.st_mtimespec.tv_sec.to_i64 * 1_000_000_000_i64 + stat2.st_mtimespec.tv_nsec.to_i64
 >  9 |             else
 > 10 |               nil
 > 11 |             end
 > 12 |           end
 > 13 |           if cached = LSP::AstCache.load(abs_path, source_mtime_ns)
 > 14 |             @ast_cache_hits += 1
 > 15 |             arena = cached.arena
 > 16 |             # Self-hosted release builds can mis-handle Array(ExprId) carriers
 > 17 |             # returned from parser/cache paths. Duplicate roots immediately so
 > 18 |             # downstream require scanning and lowering walk fresh GC-managed
 > 19 |             # storage instead of reusing the original backing buffer.
 > 20 |             exprs = cached.roots.dup
 > 21 |             base_dir = safe_dirname(abs_path)
 > 22 |             if cached_requires = load_require_cache(abs_path)
 > 23 |               if source_has_glob_require?(source) || cached_requires.any? { |path| !File.exists?(path) }
 > 24 |                 cached_requires = nil
 > 25 |               end
 > 26 |             end
 > 27 |             if cached_requires
 > 28 |               log(options, out_io, "  Require cache hit (#{cached_requires.size}): #{abs_path}") if options.verbose
 > 29 |               cached_requires.each do |req_path|
 > 30 |                 parse_file_recursive(req_path, results, loaded, input_file, input_base_dir, options, out_io)
 > 31 |               end
 > 32 |             else
 > 33 |               log(options, out_io, "  Require cache miss: #{abs_path}") if options.verbose
 > 34 |               requires = [] of String
 > 35 |               scan_requires_from_exprs(arena, exprs, source, base_dir, input_base_dir, options, out_io, requires)
 > 36 |               parse_required_files(requires, 0, results, loaded, input_file, input_base_dir, options, out_io)
 > 37 |               needs_source_fallback = source_requires_fallback?(source, requires, loaded)
 > 38 |               if needs_source_fallback
 > 39 |                 if options.verbose && !requires.empty?
 > 40 |                   missing = 0
 > 41 |                   requires.each do |req|
 > 42 |                     unless loaded.includes?(req)
 > 43 |                       missing += 1
 > 44 |                     end
 > 45 |                   end
 > 46 |                   if missing > 0
 > 47 |                     log(options, out_io, "  Require AST/source mismatch: #{missing} unresolved after AST scan, using source fallback")
 > 48 |                   end
 > 49 |                 end
 > 50 |                 fallback_requires = extract_require_literals_from_source(source)
 > 51 |                 if options.verbose
 > 52 |                   log(options, out_io, "  Source require fallback entries=#{fallback_requires.size}")
 > 53 |                 end
 > 54 |                 fallback_resolved = 0
 > 55 |                 fallback_unresolved = 0
 > 56 |                 fallback_start = requires.size
 > 57 |                 fallback_requires.each do |req_path|
 > 58 |                   resolved = resolve_require_path(req_path, base_dir, input_base_dir)
 > 59 |                   if resolved
 > 60 |                     case resolved
 > 61 |                     when String
 > 62 |                       fallback_resolved += 1
 > 63 |                       requires << resolved
 > 64 |                     when Array
 > 65 |                       fallback_resolved += resolved.size
 > 66 |                       resolved.each do |file|
 > 67 |                         requires << file
 > 68 |                       end
 > 69 |                     end
 > 70 |                   elsif req_path.includes?('*')
 > 71 |                     Dir.glob(path_join(base_dir, req_path)).sort.each do |file|
 > 72 |                       fallback_resolved += 1
 > 73 |                       requires << file
 > 74 |                     end
 > 75 |                   else
 > 76 |                     fallback_unresolved += 1
 > 77 |                     if options.verbose && fallback_unresolved <= 5
 > 78 |                       log(options, out_io, "  Source fallback unresolved require: #{req_path}")
 > 79 |                     end
 > 80 |                   end
 > 81 |                 end
 > 82 |                 parse_required_files(requires, fallback_start, results, loaded, input_file, input_base_dir, options, out_io)
 > 83 |                 if options.verbose
 > 84 |                   log(options, out_io, "  Source fallback resolved=#{fallback_resolved} unresolved=#{fallback_unresolved}")
 > 85 |                 end
 > 86 |               end
 > 87 |               save_require_cache(abs_path, requires)
 > 88 |             end
 > 89 |             results << ParsedUnit.new(arena, exprs, abs_path, source, [] of Frontend::Diagnostic)
 > 90 |             log(options, out_io, "  AST cache hit: #{abs_path}") if options.verbose
 > 91 |             return
 > 92 |           end
 > 93 |           @ast_cache_misses += 1
 > 94 |         end
 > 95 |
Error: undefined method 'st_mtimespec' for LibC::Stat

LibC::Stat trace:

  macro macro_137462847589040 (in /home/uzumaki/playground/crystal_lsp/src/compiler/cli.cr:3509):8

                  stat2.st_mtimespec.tv_sec.to_i64 * 1_000_000_000_i64 + stat2.st_mtimespec.tv_nsec.to_i64


  macro macro_137462847589040 (in /home/uzumaki/playground/crystal_lsp/src/compiler/cli.cr:3509):6

                stat2 = uninitialized LibC::Stat

This is strange. What version of crystal dd you use?

I just tested it on crystal 1.20.2:

(base) sergey@MacBook-Pro-2 crystal_v2_repo % which crystal
/opt/homebrew/bin/crystal
(base) sergey@MacBook-Pro-2 crystal_v2_repo % crystal build src/crystal_v2.cr -o bin/crystal_v2_tst --error-trace
ld64.lld: warning: Option `-stack_size' is not yet implemented. Stay tuned...
(base) sergey@MacBook-Pro-2 crystal_v2_repo % crystal version
Crystal 1.20.2 (2026-05-15)

LLVM: 22.1.5
Default target: aarch64-apple-darwin25.5.0

It’s Ubuntu 24.04 on WSL2 using Crystal 1.20.1

Got it. Let me compile on my AWS Linux instance with exact version.

I don’t have a Windows though, to it will be not really apples-to-apples, but close enough.

I found what the issue was. I was using Darwin-only LibC::Stat#st_mtimespec inside a disabled AST-cache branch.

I fixed that and pushed a new commit. Please try it and let me know if it helps.

It works :tada:

I successfully run the crystal_lspvia Cursor.

  • Hover does not work :cross_mark:
  • Go to definition works :white_check_mark:
  • Completion works :white_check_mark:
  • Format works :white_check_mark:
  • Rename works :white_check_mark:
  • Outline does not work :cross_mark:

Do you mind sharing what code hover and outline were not working?

Here is example of hover and outline on crystal_lsp code itself:

I just used your Calculator code, however please be aware that I use Cursor and not VS Code

class Calculator
  def add(x : Int32, y : Int32) : Int32
    x + y
  end

  def multiply(x : Int32, y : Int32) : Int32
    x * y
  end
end

calc = Calculator.new
result = calc.add(5, 3)
puts result

I’ll give better comments when I find the time, but let me quickly ask for a change of name. crystal2 is very inconvenient for an unofficial tool, borderline legally problematic :folded_hands:

EDIT: But of course, thanks in ton for working on improving Crystal DX!

Hey Beta!

No problem. I will rename the project.

This is great!

However, I think reimplementing the compiler might not be good… or, put another way, there’s a big chance that it will diverge from how the current compiler works, and then it will turn into a lot of work. I can also imagine reviewing such large code base will be hard.

That said, I still think the LSP part might be good to implement separately. Today I started wondering whether implementing something like what I mention here could be easily implemented with Claude Code. The idea would be what I describe there, so to quickly type-check an open file to then provide some LSP features. The ones that come to my mind are: autocompletion, go-to-definition, hover, inlay hints for local variables and arguments (this would be awesome to have, if you know how it works in Rust), signature help…

The algorithm of type-checking Crystal in this way is probably pretty long, I don’t know if complex, but that’s why using Claude Code might be a good fit for this. And, because this LSP would be a best-effort, and not critical for compiling correct code, it’s fine if it’s not totally correct (unlike reimplementing the compiler).

I might eventually try it, though I don’t have a Claude Code personal subscription, and I’d have to switch between that one and the one in my workspace, which is a bit cumbersome…

Just throwing out this idea in case someone wants to play with it.

Thank you, Ary!

It’s better to use ChatGPT and Codex, as they make far fewer mistakes. The best way is to use both subscriptions: GPT assigns tasks and conducts adversary reviews, while Claude implements them.

LSP is great for very fast editing as it keeps each file/AST in a separate virtual arena, which is then combined. Plus, it has a recoverable parser. Because of that, editing a single file only parses that AST, not the whole project.

In the compiler, I used some new techniques I invented with agents and wanted to try: Crystal-specific optimizations, HIR/MIR/LL, a Hybrid memory model (Stack/Slab/ARC/AtomicARC/GC), and other features.

PS: By the way, as Beta said, I cannot name it Crystal, so I already renamed it to Adamas, where Crystal is a subset, so both languages can benefit from its LSP.

And I started this project just to see if agents are smart enough to build everything from scratch, given my ideas and guidelines.

So they actually are, if you know how to control them.

GitHub - skuznetsov/cogni-ml: Crystal ML library: Autograd, Tensors, Neural Networks, Optimizers · GitHub was built with agents as well as GitHub - skuznetsov/pg_sorted_heap: Sorted heap table AM for PostgreSQL with zone map scan pruning · GitHub and my many others projects.

This may be a naive question, but how do you all use LSP?

When using Crystal, I often see people say that “Crystal has no LSP.”
I mostly program alone, as a hobby, so I have not really experienced situations where LSP is necessary. Because of that, I feel that I do not deeply understand why people want LSP so much.

My understanding is that LSP is useful when reading or modifying a large codebase written by someone else. In other words, it is like a smarter shortcut than plain search, letting you jump across files to definitions and references. Is this roughly correct?

In that sense, LSP also feels like a simple kind of AI that understands code. However, I do not see much emotional resistance to LSP, unlike the resistance often seen toward LLMs. Is that because LSP is mainly used for code understanding and navigation, not code generation?

Seeing the Adamas project, where the author is willing to modify the compiler itself to achieve better LSP support, made me feel that the motivation behind LSP is much stronger than I had imagined.