Aquascope: Making Rust's Invisible Borrow Checker Visible
Hook
Rust's borrow checker rejects thousands of valid programs every day, but developers never see what it actually sees. Aquascope cracks open the compiler to visualize the invisible.
Context
Learning Rust's ownership model is notoriously difficult. Developers moving from garbage-collected languages must suddenly think about lifetimes, borrows, and moves—concepts that exist only in the compiler's analysis phase and leave no runtime trace. Traditional documentation relies on metaphors ("think of ownership like a library book") and static diagrams that quickly become inadequate when dealing with complex borrowing scenarios.
The Cognitive Engineering Lab at Brown University identified this as a fundamental pedagogical problem: how do you teach mental models for compiler behavior that students can't observe? Their answer is Aquascope, a tool that hooks directly into rustc's internal APIs and Miri (Rust's interpreter) to generate interactive visualizations showing both what the borrow checker sees during compilation and how data flows during execution. It's not syntax highlighting or superficial analysis—it's the actual compiler's perspective, rendered as explorable diagrams embedded directly in documentation.
Technical Insight
Aquascope's architecture is split into three cooperating components: a cargo plugin that orchestrates compiler analysis, an mdBook preprocessor that transforms annotated code blocks, and a JavaScript visualization layer that renders the results. The magic happens in how it integrates with Rust's nightly compiler infrastructure.
Unlike static analysis tools, Aquascope requires rustc-dev components because it literally calls the same APIs that rustc uses internally for borrow checking. When you annotate a code block in your mdBook documentation, the preprocessor extracts it, compiles it with instrumentation hooks, and captures the MIR (Mid-level Intermediate Representation) along with borrow checker state at various points in the program. Here's how you'd annotate a simple ownership example:
{{#aquascope}}
fn main() {
let s1 = String::from("hello");
let s2 = s1; // move occurs here
println!("{}", s1); // error: value borrowed after move
}
{{/aquascope}}
This generates two synchronized views: a permissions diagram showing how read/write/own permissions flow through variables, and a timeline showing when borrows begin and end. The permissions visualization is particularly revealing—it shows that after the move to s2, the variable s1 loses all permissions, which is exactly what the borrow checker sees but never explicitly tells you in error messages.
The runtime visualization mode uses a different approach, delegating to Miri to trace actual program execution. Miri is Rust's interpreter for MIR, and it can track every memory operation. Aquascope captures Miri's execution trace and renders it as a step-by-step animation of stack frames and heap allocations:
{{#aquascope_interpret}}
fn main() {
let mut v = vec![1, 2, 3];
v.push(4);
let first = &v[0];
println!("First: {}", first);
}
{{/aquascope_interpret}}
This produces an interactive stepper showing the vector's heap allocation, how push might trigger reallocation, and crucially, why the reference first remains valid (or wouldn't, if you tried to mutate v afterward). You can scrub through execution frames to see the exact moment allocations occur and references are created.
The technical challenge is maintaining compatibility with nightly Rust. The repository pins to a specific nightly version (currently 2024-12-15) because rustc's internal APIs have no stability guarantees. The team uses a Rust toolchain override file to ensure reproducibility:
# rust-toolchain.toml
[toolchain]
channel = "nightly-2024-12-15"
components = ["rustc-dev", "llvm-tools-preview", "miri"]
This tight coupling is both Aquascope's strength and weakness. Direct compiler integration provides unmatched accuracy—you're seeing exactly what rustc sees—but it means the tool breaks with every significant compiler refactoring. The maintainers occasionally have to rewrite entire integration layers when rustc internals change.
The visualization rendering itself happens browser-side using web components. The Rust analysis passes generate JSON describing ownership flows, borrow scopes, and execution traces. A TypeScript/JavaScript layer consumes this JSON and renders SVG diagrams with interactive hover states and stepping controls. This separation means you can inspect the raw JSON if you're debugging why a visualization looks wrong, or even write custom renderers for different output formats.
Gotcha
The biggest gotcha is the nightly dependency trap. Installing Aquascope requires not just any nightly Rust, but the specific nightly version it was built against, along with rustc-dev components that can be several gigabytes. If you're teaching a Rust course and have students install it, you'll field support requests about toolchain mismatches and missing components. The installation instructions require cargo-make and Depot (a build caching tool), which adds layers of potential failure.
More fundamentally, Aquascope is explicitly research software. The developers warn that it contains bugs, the interface isn't stable, and documentation is sparse. I discovered this firsthand when testing edge cases: complex generic code sometimes produces visualizations that don't render, and error messages often point to internal compiler panics rather than actionable fixes. You can't run this in CI to validate documentation examples—it's too fragile. This limits its use to curated educational content where an author can manually verify each visualization, not automated or large-scale documentation generation. If you're writing a Rust book, you'll budget extra time for diagrams mysteriously breaking after toolchain updates.
Verdict
Use if: You're creating focused educational materials about Rust ownership and borrowing (online courses, tutorial books, workshop materials) and can dedicate time to maintaining toolchain compatibility. The visualizations genuinely help learners build correct mental models faster than any alternative approach. It's worth the friction for high-impact teaching scenarios. Skip if: You need stability, automation, or production-grade documentation tooling. The nightly dependency and research-grade maturity make it unsuitable for anything in CI/CD pipelines, corporate documentation that must stay current, or projects where you can't tolerate occasional breakage. For those cases, invest time in carefully crafted static diagrams instead—they're more work upfront but require zero maintenance.