Argus is a tool for interactive exploration of Rust’s trait solving process, made possible by exposing the internal proof trees constructed by the Rust trait solver. The Argus interface creates a lens into trait resolution and does not reduce away important information like compiler diagnostics. This section introduces the Argus interface with the two running examples, and shows how Argus can provide more specific error information for our Axum web server.
A Good Diagnostic, Preserved
Compiler diagnostics are reductive, explained in
trait ToString {
fn to_string(&self) -> String;
}
impl ToString for i32 {
fn to_string(&self) -> String {
format!("{}", self)
}
}
impl ToString for char {
fn to_string(&self) -> String {
format!("{}", self)
}
}
impl<T> ToString for Vec<T>
where
T: ToString,
{
fn to_string(&self) -> String {
format!(
"[{}]",
self.iter()
.map(ToString::to_string)
.collect::<Vec<_>>()
.join(", ")
)
}
}
fn print_ln<T>(v: T)
where
T: ToString,
{
println!("{}", v.to_string());
}
fn main() {
let v = vec![1.618, 3.14] as Vec<f32>;
print_ln(v);
}Shown in
For this simple trait error, the bottom-up view shows us the same information as provided by the compiler diagnostic: an unsatisfied trait bound f32: ToString. That is our root cause. Expanding the bottom-up view shows the provenance of each bound. The top-down view of this example is uninteresting because the trait solver did no branching, and the proof tree is really a “proof stick.” Therefore, it is simply the mirror of the bottom-up view as shown in


The top-down view starts with the failed root goal, and interactively allows for exploration down towards failures. Encoded in the left border is the relationship between parent and child (And/Or). A dashed border represents the Or relationship, meaning one child needs to hold. This is used when reducing a goal via a trait implementation rule. The solid border represents the And relationship, meaning all children need to hold to satisfy the parent. This conjunctive relationship is most commonly introduced by goal subconditions introduced by the where clause of a trait implementation block.
A poor diagnostic, pinpointed
In
use axum::{body::Bytes, routing::post, Router};
use tokio::net::TcpListener;
async fn is_valid_user(name: &str, pwd: &Bytes) -> bool {
true
}
async fn login(name: String, pwd: Bytes) -> String {
if is_valid_user(&name, &pwd).await {
format!("Welcome, {name}")
} else {
"Invalid credentials".to_string()
}
}
#[tokio::main]
async fn main() {
let app = Router::new().route("/login", post(login));
let listener = TcpListener::bind("0.0.0.0:3000").await.unwrap();
axum::serve(listener, app).await.unwrap();
}Seen in
The problem of proof tree exploration is that of an interface problem. The compiler has large data, Rust types are large, and yet we need to make this information consumable. Argus isn’t tied down to reporting textual static diagnostics, opening the room to interface design experimentation. Some of the ways in which Argus deals with large data is path shortening and trimming trait implementation headers.
Path shortening The definition path of an item is the absolute, qualified path uniquely identifying its definition. Common paths such as std::vec::Vec don’t seem too harmless. Library paths, especially those involving traits, are a bit harder to read at times. Consider a simple social media application whose implementation uses the popular Diesel library to handle relational mapping and query building.
fn users_with_equiv_post_id(
conn: &mut PgConnection
) {
users::table
// .inner_join(posts::table)
.filter(users::id.eq(posts::id))
.select((users::id, users::name))
.load::<(i32, String)>(conn);
}use diesel::prelude::*;
table! {
users(id) {
id -> Integer,
name -> Text,
}
}
table! {
posts(id) {
id -> Integer,
name -> Text,
user_id -> Integer,
}
}<QuerySource as
diesel::query_source::AppearsInFromClause<posts::table>>::Count
== diesel::query_source::peano_numbers::OnceTo further beat a dead horse Rust reports “the full type name has been written to ‘file.long-type.txt’” where all types involved in the error are written. Temporary debug files have several hashes in their names making the real filename much longer too. Ponder this statement. The type was too long to be displayed and the developer needs to dig through output files if they want to see it.
Instead of shying away from these large qualified paths, Argus can shorten them by default, and provide the fully qualified version on hover, demonstrated in

Trimming implementation headers Documentation is a crucial resource to help developers, especially when using new libraries. Rust documentation is generally good, but it could do better especially when it comes to traits and their implementors. Consider again our running example with Axum. We have a function login that fails to satisfy the criteria to implement the Handler trait. The reported error is like “trait bound login: Handler is not satisfied.” The intuition for many developers is to visit the documentation page for handlers to see what types do implement the trait. Unfortunately, the verbosity of Rust is a little jarring when sifting through dozens of implementation types in documentation.

Shown in
Looking through this documentation can be confusing and it isn’t initially apparent which block one should start with. Ideally, Rust would have variadic generics, a long sought after feature,
A small sample of user feedback suggests that the Argus view is more readable than online documentation. This isn’t to say it couldn’t be improved, future work will include hyperlinks to library documentation and improved rendering of Fn trait output types. Using the notation -> Ty instead of Fn::Output == Ty to indicate return types.
