Skip to main content

Resolvers

A resolver answers "what is defined at this position". This is the part that usually forces you to install and configure a language server; here two real type checkers are compiled into the module, two more use the toolchain the project already has, and three languages get a symbol table built from their own sources.

LanguageEngineWhere it runsExternal requirementBest status
Pythontyin-process, as a librarynoneok
TypeScripttypescript-goin-process, via a Go c-archivenoneok
Gogo/packages + go/typesin-process, same c-archiveGo toolchainok
Rustrust-analyzer scipsubprocess, index decoded nativelyrust-analyzer, Cargook
PHPa symbol table over the parsed sourcesin-processnonedegraded
C / C++a symbol table over the parsed sourcesin-processnonedegraded
YAMLok, with zero queries

The last column is what the graph can report at best, and it is what to read before trusting an edge. A type checker knows which overload a call selected and which class's method it landed on; a symbol table knows only which declarations were visible from that file. See PHP and C and C++ for what each one cannot answer.

YAML's ok is not a rounding of unavailable: nothing failed and nothing is missing, because a YAML document declares no symbols to resolve.

Why two are linked in and two are not

Python and TypeScript ship their standard-library type information as data — typeshed and lib.d.ts — so both can be bundled and the whole checker linked into the module. Nothing is spawned, and there is no JSON-RPC.

Go and Rust cannot work that way. Type checking Go reads the standard library's sources from GOROOT and shells out to go list; resolving Rust needs a real Cargo workspace load. Neither can be frozen into a wheel, so both rely on the toolchain the project already has.

Why PHP and the C family get a symbol table instead

Neither has a checker in a form callix could link, and neither has an indexer common enough to shell out to. For C and C++ the obstacle is sharper: every precise option — scip-clang, clangd's index, libclang — hard-requires a compile_commands.json, which is a by-product of running the build and which a survey of twelve prominent C/C++ repositories found checked into none of them. A resolver built on it would report unavailable on very nearly every folder someone points callix at.

So both build a table from the sources already parsed and report degraded. That is not a placeholder — PHP resolves by autoload map and namespace, C and C++ by declaration visibility through #include, and a table encodes exactly that — but it is not type inference, and the status says so.

Both accept resolver= if you have something better; see Custom resolvers and parsers.

Cost

The engine dominates the runtime, and it dominates differently per language:

  • Python / TypeScript — resolution is a function call. On a 400k-line Python project the whole analysis lands around 9 seconds.
  • Gopackages.Load type-checks the project up front; that is where the time and the memory go.
  • Rustrust-analyzer scip is a subprocess that indexes the entire workspace before a single question is asked. On ruff that is about 96 seconds of the ~100 the analysis takes. The lookups afterwards cost well under a second.
  • PHP, C, C++ — the table is built from the parse callix already did, so resolution adds a fraction of a second rather than a phase.

The batch SCIP index is a deliberate choice over an interactive rust-analyzer server: the server keeps the whole workspace's analysis state resident and balloons to tens of gigabytes on large projects, while the index is written once and read statically. callix decodes it without a protobuf runtime — only the symbol, the roles bitfield and the range start are needed.

When the toolchain is missing

Nothing fails. The adapter returns a structural graph and says so in graph.metadata["resolver_status"]. Pass strict=True to analyze() if you would rather have an AdapterError.

strict=True therefore always raises for PHP, C and C++, whose best status is degraded. That is the intended reading: strict means "refuse a graph you cannot vouch for", and there callix cannot.

For Rust there is one more wrinkle worth knowing: rust-analyzer on PATH is usually a rustup proxy that honours a project's rust-toolchain.toml. If that pinned toolchain lacks the component — ruff pins one that does — the proxy exits with Unknown binary and resolution would silently yield nothing. callix asks rustup which from the project root first and falls back to the default toolchain rather than accepting the empty answer.