The chiseled statue presented in research was, just a few months prior, a heaping mess of granite. The process and challenges in a project are often the most interesting technical aspects, this section highlights specific design decisions and rough edges of the Argus internals. We finish with a discussion on known shortcomings and future plans.
Shown in
You Want What Again?
The first step in the Argus pipeline is to type-check the workspace modules and record obligations solved for during trait resolution. Post initial type-checking, Argus now has a large set of obligations and needs to determine which the developer would find useful. This set is comprised of all queries made by the trait solver, determining which are useful to debug an error is difficult for three reasons. First, we don’t want to expose overly complex compiler internals. The trait system has several variants of obligation, many of which, don’t have a source code representation. Second, after a failed obligation the trait solver may learn new information and then retry the query. There is no direct mechanism to distinguish a failed obligation from a failed obligation that succeeds on a subsequent solve attempt. Third, the trait solver can solve obligations that are not hard requirements, but rather, the answer informs type inference. I will refer to these as softline obligations, their failure does not always imply a type-check error.
Complex Obligations Until now, discussions of obligations centered around the form U: T, read as: “does type U implement trait T?” As of April 2024, Rust has eight kinds of predicates the trait solver can ask, and an additional seven “clause kinds” (a kind of predicate). Only three in total have an analogous source-code representation. By default Argus only shows these three clause kinds to users:
- trait predicates: U: T
- region outlives predicates: ‘a: ‘b
- type outlives predicates: U: ‘a
Other predicates are hidden by default and shown only if they are the direct cause of a type-check error. This includes queries such as “Is type U well-formed?” and “Can trait T be used as a trait object?” The goal is to avoid overwhelming developers with information they may not need. (Or may not have known existed!)
Subsequent Obligations The trait solver not only returns a yes/no/maybe answer for each obligation, but also a set of inference variable constraints. Added inference constraints may cause a previously ambiguous obligation to succeed. Consider again the simple ToString trait example. The type signature of the print_ln function is fn print_ln<T>(v: T) where T: ToString, this means that the caller needs to provide a type T that satisfies the bound ToString. In the calling context the first obligation given to the trait solver is 0?: ToString, where 0? is an inference variable. This obligation will result in an ambiguous response. However, the solver will add the inference constraint 0? = Vec<f32>. The subsequent iteration of this goal, Vec<f32>: ToString, is not ambiguous and fails. We do not want to show developers the first ambiguous obligation, only the second failure. Argus currently piggybacks on Rust compiler infrastructure to test whether a failed obligation \(O_F\) implies an ambiguous obligation \(O_A\). It should be noted that this strategy is unsafe due to inference variables potentially leaking out of context.
Softline Obligations Obligations received from the compiler come marked with a “cause code.” This encodes data about the origin of the obligation. For example, types returned from functions need to be sized. Consider the function fn to_string(&self) -> String from the ToString trait. The obligation String: Sized is solved for with a cause code SizedReturnType.
Consider again the ToString trait, we will modify the example main function from
struct A;
fn main() {
let v = vec![A];
format!("{}", v.to_string());
}The default behavior for Argus is to ignore miscellaneous obligations, unless their span can be tracked to a method call. For method calls, Argus will remove miscellaneous obligations and re-perform method selection, registering each obligation as required.
Moving Forward
In future releases of Argus we hope to improve the collect and filter steps in argus::analysis. In certain circumstances Argus filters too aggressively and important obligations are filtered from the default view. This can be improved by tracking the cause code within the Rust compiler more precisely, or a smarter analysis to distinguish truly miscellaneous obligations from important ones. Memory consumption during obligation collection can spike in even medium-sized codebases. Argus needs to maintain information about each obligation that it wishes to analyze further, especially ones for which a proof tree is generated. The current Argus release does little to throw away data it will not use and these extra obligations take up space. To throw away unnecessary obligations again requires the knowledge that an obligation will not be shown to the user, our hope is that the problems during collect and filter can both be mitigated by the extra provenance information coming from the compiler.