heucristic

An experimental language server for Crystal.

heucristic

An experimental language server for Crystal. It aims to be fast and responsive by making certain trade-offs, like not inferring types.

[!WARNING] Highly experimental. Also mostly experimenting with heavy use of LLMs while trying to solve a real-world problem I have.

Features

  • Hover shows the type of a call, variable, instance variable, constant or parameter. When the type is unknown, heucristic explains why instead.
  • Go to definition, including methods and types generated by macros, which jump to the macro call that created them.
  • Reports every type error in a file while you type, not just on save. Covers undefined methods and constants, wrong argument counts and types, non-exhaustive case ... in, unimplemented abstract methods, return type mismatches, private and protected calls, and instance variable mistakes.
    • Syntax errors only report the first error and the rest of the file isn't checked until it parses again.
  • Runs a Crystal compiler pass on save in a separate process.
  • Completion, including paths inside require strings.
  • Find references and rename.
  • Document and workspace symbols.

Install

No binaries are currently distributed.

You need Crystal 1.22 and LLVM (for llvm-config, used to build the macro helper). On macOS with Homebrew: brew install llvm.

git clone https://github.com/DanielGilchrist/heucristic.git
cd heucristic
script/install

script/install builds two binaries into bin/ and symlinks ~/.local/bin/heucristic to the server (set PREFIX to change where). The server finds heucristic-semantic next to its own binary, so to update:

git pull && script/install

If llvm-config isn't found, point at it with LLVM_CONFIG=/path/to/llvm-config script/install.

Editor support

Neovim

vim.lsp.config("heucristic", {
  cmd = { "heucristic" },
  filetypes = { "crystal" },
  root_markers = { "shard.yml", ".git" },
})

vim.lsp.enable("heucristic")

Optional init_options:

Option Meaning
defines Compile-time flags, e.g. { "tui" }, passed as -D to the compiler and the macro helper
stdlib false skips indexing the standard library
compiler Path to the crystal used for diagnostics on save
semantic Path to heucristic-semantic if it isn't next to the server

Trade-offs

  • No type inference. A method without a return type is treated as unknown. For good results you must add explicit type annotations.
    • When heucristic doesn't know a type, hover will show an explanation to make this more clear.
  • Hover shows a method's declared return type. Crystal's compiler uses what the method body actually returns, so a method declared : Int32? that only ever returns an Int32 shows as Int32?. This means hover can show a wider type than the compiler, and calls on the result can be unknown. Diagnostics account for this, so it never reports an error the compiler wouldn't.
  • Heucristic only reports a diagnostic when it knows every method a call could reach. If a type in the call is unknown, or methods could come from a macro it can't expand, method_missing, or a file the compiler didn't load, it skips the check. The compiler still catches those on save.
  • Compiler errors are only reported on save, and only the first one (this is a limitation of the Crystal compiler).
  • Macros other than getter, property, setter, record and delegate are expanded by a helper process on save. Until the next save, methods and types those macros define can be missing or out of date. If the project fails to compile, the results from the last successful run are kept.
  • Inside a generic method, calls on a value whose type is a type variable are unknown, because each caller defines the type.
  • Shard methods without a return type are unknown unless you add a shim for them (see below).
  • flag? checks in macros are evaluated for the platform heucristic was built on. Built on a Mac, code under {% if flag?(:linux) %} is skipped.

Keeping the editor responsive

Hover, go to definition and diagnostics should feel instant while you type, so nothing slow runs on that path.

  • No type inference, so a hover is a lookup instead of an analysis of the whole program.
  • The compiler and the macro helper run as separate processes, so they never block the server.
  • Each file is indexed on its own, in parallel.

Project shims

Heucristic fills gaps in the stdlib by "shimming" its APIs with types. Projects can provide their own shims too, for their own code or the libraries they use. These go in shims/ next to shard.yml. A shim re-opens the type and re-states the def with a return type and an empty body.

# shims/greeter.cr
class Greeter
  def greet(name : String) : String
  end
end

A shim matches a def by owner, name and parameters, and is only used when the real def has no return type. Otherwise it is ignored. Project shims are prioritised over the ones built into heucristic.

Development

crystal spec
crystal tool format
bin/ameba            # shards build ameba on a fresh clone
bin/score <project>  # resolve every call in a project and bucket the unknowns

See AGENTS.md for the architecture, principles and rules to follow.

License

MIT

Repository

heucristic

Owner
Statistic
  • 0
  • 0
  • 0
  • 0
  • 5
  • about 2 hours ago
  • September 8, 2026
License

MIT License

Links
Synced at

Mon, 09 Nov 2026 00:10:56 GMT

Languages