Disclaimer: This article was created using AI-based writing and communication companions. With their help, the core topics of this rich and nuanced livestream were distilled into a compact blog post format.
How do Rust IDEs understand code? That was the central question explored in a recent RustRover livestream featuring Lukas Wirth, Rust engineer at Zed and team lead for rust-analyzer, and Vlad Beskrovny, engineer on RustRover at JetBrains. Rather than comparing editors or debating preferences, the discussion focused on what actually happens under the hood when an IDE analyzes Rust code.
If you missed the livestream, you can watch the full recording on JetBrains TV. Below is a structured recap of the key questions and insights from the session.
Q1. How did Lukas and Vlad get started with Rust?
Before diving into compiler frontends and IDE architecture, the livestream started with a more personal question: how did they first get into programming? Interestingly, both Lukas and Vlad mentioned Minecraft modding in Java as one of their earliest programming experiences. Lukas started writing Java mods for Minecraft while still in school, eventually teaching himself Rust when entering university.
“I taught myself Rust when I entered university and basically stopped using any other language at that point.”
Vlad discovered Rust around 2014, but did not seriously start writing it until joining JetBrains and working on the IntelliJ Rust plugin, the predecessor to RustRover.
Q2. Why do Rust IDEs reimplement parts of the compiler?
To provide features like completion, go to declaration, semantic highlighting, and refactorings, Rust IDEs effectively need to understand the language almost as deeply as the compiler itself.
“To provide smart features such as completion and go to declaration, we have to reimplement half of the compiler, basically the whole compiler frontend.
”
So why not simply reuse the compiler directly? Compilers optimize for throughput:
how efficiently they can transform source code into binaries.
IDEs optimize for latency:
how quickly they can answer small interactive questions while the developer is typing.
“I typed a dot, and how quickly can I see the completion variants? At this point, I don’t care about other function bodies, about the rest of the files, about any other files in the project. I just want my completion to appear instantly.”
That difference fundamentally changes the architecture. Compilers tend to process code eagerly and sequentially: parse everything, resolve everything, expand everything, infer everything. IDEs instead try to compute only the minimum information necessary for the current interaction.
Q3. How did Rust tooling evolve from RLS to rust-analyzer and RustRover?
The livestream also revisited the history of Rust tooling. Before rust-analyzer, Rust’s primary language server was RLS, the Rust Language Server.
RLS attempted to build IDE functionality directly on top of the compiler using “save analysis”. The compiler produced large JSON outputs containing semantic information, which the language server later queried. In practice, this approach struggled with latency and incomplete code.
“It was nearly impossible to implement completion this way because rustc barely works with incomplete code, which is almost always the case when a user needs completion.”
”
RLS was eventually replaced by rust-analyzer, which adopted a more incremental architecture focused specifically on IDE responsiveness.
The discussion also touched on the origins of IntelliJ Rust, the project that eventually evolved into RustRover. Interestingly, both rust-analyzer and IntelliJ Rust originated from work started by Alex Kladov, although the projects later evolved in very different architectural directions.
Q4. Why is name resolution in Rust so difficult?
Rust’s module graph is cyclic, which means IDEs cannot resolve names incrementally in the same simple way many other languages can.
Lukas demonstrated this using a chain of nested reexports where resolving a single symbol required tracing through several modules, aliases, and glob imports before reaching the original declaration. To support this workflow, IDEs repeatedly:
• collect modules
• resolve imports
• expand macros
• collect newly generated items
• and repeat the process until no unresolved symbols remain
This repeated process is often described as “fix point iteration”. And unfortunately for tooling authors, macros make this process even more complicated.
Q5. Why are procedural macros such a challenge for IDEs?
In theory, a procedural macro is simply a function that transforms tokens into other tokens. In practice, procedural macros are dynamically loaded libraries that can:
• access the filesystem
• read environment variables
• execute arbitrary code
• crash processes
• or terminate execution entirely
“Proc macros are kind of more than that. They are dynamic linked libraries. They can do whatever they want on the host system.”
That creates major challenges for IDEs. If a procedural macro crashes inside the IDE process itself, it could terminate the entire IDE session. To avoid that, both rust-analyzer and RustRover isolate procedural macro execution into separate processes and communicate through custom protocols.
“If the proc macro actually hard crashes or exits the process, in the worst case we just lose a proc macro server that we can spin up again. But at least the IDE keeps running.”
Q6. Why is Rust type inference difficult to replicate?
Rust’s type system introduces another layer of complexity. The good news for tooling authors is that Rust type inference is mostly local to function bodies, which makes incremental analysis possible. The bad news is that Rust contains countless special inference rules and edge cases that IDEs must replicate precisely.
“The issue with Rust type inference is that it has way too many arbitrary rules. Literally thousands of arbitrary rules we have to meticulously replicate.”
During the livestream, he demonstrated several examples where tiny structural changes completely changed whether code compiled successfully.
Some of these behaviors even depend on the internal order in which expressions are processed during inference. These details directly affect editor features like:
• completion
• diagnostics
• navigation
• inspections
• and inlay hints
And unlike compilers, IDEs must provide useful semantic results even while the code is incomplete.
Q7. How does RustRover analyze large Rust projects?
RustRover begins by building a project model from Cargo metadata and crate dependencies. It then indexes project files using PSI, or Program Structure Interface, an abstraction layer used throughout JetBrains IDEs. Vlad said that PSI can be backed by either:
• full syntax trees
• or lightweight “stubs” containing only declarations and signatures
This allows RustRover to avoid fully parsing every file eagerly, significantly reducing memory usage and improving responsiveness. The indexing system itself uses a MapReduce-style architecture where files are processed independently and incrementally.
One especially interesting detail was that during indexing, RustRover can skip parsing function bodies in some phases because stubs only require declarations and signatures.
“During indexing we don’t parse function bodies at all.”
Instead, RustRover can move through the file structure efficiently by lexing and counting braces, which significantly speeds up indexing. The broader point was that modern IDEs cannot be purely lazy. At some point, they still need eager analysis.
“The true art of an IDE design is to draw this line in the right place”
Q8. How does rust-analyzer approach the same problem differently?
While RustRover relies heavily on indexing infrastructure, rust-analyzer uses a query-driven architecture inspired by the Rust compiler itself. Semantic operations are modeled as memoized dependency-tracked queries using the Salsa framework.
“All the semantically interesting bits in rust-analyzer are put behind so-called queries.”
This allows rust-analyzer to invalidate and recompute only the precise semantic information affected by an edit. Unnecessary dependencies can accidentally invalidate computations on every keystroke, making performance optimization surprisingly subtle.
Lukas explained several layers of garbage collection and memory optimization used inside rust-analyzer, including:
• LRU query caches
• symbol interning
• and custom mark-and-sweep tracing collectors for type internals
Q9. How does IDE analysis connect to debugging?
Vlad demonstrated how RustRover integrates semantic analysis directly into debugging workflows. RustRover’s debugger uses a customized LLDB integration together with IDE-generated MIR representations for evaluated expressions.
When developers evaluate expressions during debugging sessions, RustRover generates MIR for the relevant expression graph, serializes it, and interprets it through the debugger backend.
It was a strong example of how modern IDEs increasingly behave less like text editors and more like full semantic environments built around the language itself. The livestream ended with a quick audience question about debugging asynchronous Rust workflows and whether RustRover could eventually visualize Tokio async tasks similarly to Rider.
Q10. Do Rust tooling authors secretly hate Rust?
“Usually a feature that makes the language more pleasant to use tends to introduce a lot more complexity on the implementation side.
”
Vlad also added:
“I love Rust. How can you otherwise explain why I spent like nine years of my life going through all these complexities?
”
At the same time, both acknowledged that some Rust features arrived early in the language’s history before the ecosystem fully understood their long-term tooling implications, especially around procedural macros.
If you are interested in Rust tooling, compiler internals, IDE architecture, or language design tradeoffs, the full discussion between Lukas Wirth and Vlad Beskrovny is worth watching.

