Borrow visualizer for the Rust Language Service

During RustConf, the small introduction to the Rust Language Service reminded me of an idea I had about a year ago. The idea was to show, in an IDE, the areas where a variable/expression was borrowed immutably, borrowed mutably or moved. Last week I decided to reach out on twitter to @jntrnr and @nrc (the guys who presented the Rust Language Service during RustConf). It turns out someone else, @evestera, saw my tweet and had thought of the exact same idea (down to the colors, except I had orange instead of red) and was already working on it for his master’s thesis http://erik.vestera.as/rustvis/specs.

I’d like to use this post as a way to organize the work around this as well as discuss improvements to the design. @eddyb helped me out on IRC by pointing out where in the compiler I could get the information necessary (src/librustc_borrowck/borrowck/gather_loans/mod.rs is where the important information for this is gathered by the compiler). I’ll add more details later on, but I wanted to put this out there before I got busy and forgot again.

One area that will be important but hasn’t been easy for me to figure out is how to deal with non-lexical lifetimes (which @jntrnr brought up on twitter). Using multiple colors can get confusing once the number of branches grows even a little. The colors have a meaning associated with them, and I think having too many colors would make it very confusing to follow.

The output could be something along the lines of:

[
    {type: "live", start: 5, end: 7},
    {type: "live", start: 12, end: 467},
    {type: "imm", start: 123, end: 456},
    {type: "mut", start: 234, end: 345},
    {type: "mov", start: 457, end: 467},
    ...
]

or

[
    "live": [{start: 5, end: 7}, {start:12, end:467}],
    ...
]

The reason for multiple live sections would be for function parameters. I don’t think it would make sense to highlight all parameters if the value of interest is the first param.

The only code I have at the moment is a very basic compiler plugin that isn’t usable yet. I’m not even sure if this is the right approach for something like the Rust Language Service. Rather than waste more time on my own I thought I’d reach out for help, and help out @evestera however I can.

Once this is in place, it opens up the possibility for an idea I have about editing Rust in a VR environment. I can add details if anyone would like, but I didn’t want to pollute the post with something nowhere near ready for dev.

18 Likes

As someone with mild color-blindness, which affects about 8% of males, I would like to point out that in GUI development it is important to use a non-color-based distinction (squiggly lines, bold, italic, …) which should be noticeable in greyscale, and then only enrich it with color-coded for the benefit of those who see colors properly.

10 Likes

Yeah, I've been thinking about that. The central focus though, should be to create the backend tooling (preferably as part of or connected to the Rust Language Service) providing the spans to mark. How to actually render those spans is up to the specific editor implementation. At least in Atom (my editor of choice at the moment) such spans would be easily customisable with user styles as well, even if the default implementation uses colors.

I should also emphasize that my "specs" linked above is only a generated page from files I would like to base tests on later. It's just something I made to be able to talk about the concept more easily. The exact ranges, presentation, etc is up for discussion, but won't necessarily be reflected in that page.

One thing I noticed was missing from your test cases was when the value is a function parameter. Is that something you’re considering as well?

I would wait for non-lexical lifetimes to do this. At that point there will probably be infra in the compiler to highlight what parts of the AST correspond to the some (MIR) CFG for errors, and you can piggy-back on that nicely.

4 Likes

Working on it now would just be a proof of concept to help flesh out visualization designs. I’m hesitant to delay it further since a year ago I was told it’d be better to wait until MIR was complete. That’s still not finished for all of the borrow checker portion of the compiler.

With a prototype we can still work on how it will feel to use and potentially make changes to the design. That way when non-lexical lifetimes is ready, it’s a matter of updating the code that interacts with the compiler, and we could provide this sooner.

Also, thanks for mentioning non-lexical lifetimes. I just got an idea how to display them.

Here's an idea for non-lexical borrows. I think the part that ties the state of the code after the if-else expression is the highlighting of the closing brace of the if and else blocks. If a branch is diverging then the spacing before the closing brace would be unhighlighted, but the closing brace would be highlighted the color of the state before the branch was entered.

Do these make sense? Does anyone think it's necessary to mark which branch the state came from?

While starting now might lead to some work having to be reworked later, I don't think it's necessary to wait until non-lexical lifetimes are complete to do this. There are several more or less separate problems that have to be solved (generating the data, transforming or making the data available via the RLS, and actually using the data in editors), and unless we base output on the MIR details (which I don't think is intended as a stable interface) only the compiler internal parts should have to be reworked when the transition is made.

Doesn't your example only show how the code would be visualized currently? (And I think the yellow span should be a bit shorter since the return value of restrict2 is not used.)

I've added a couple of examples based on the code in @nikomatsakis blog post series on non-lexical lifetimes showing the difference in visualizing the current and the proposed borrows/lifetimes: http://erik.vestera.as/rustvis/specs#nonlexical1-current.

I have to admit though, that even with the availability visible it's not easy to fully understand what is going on in the second example. I almost think you need a different visualization for that (like a full annotated/colorised control flow graph?).

1 Like

Sorry about that. I had a feeling I used an incorrect example a few minutes after I left the house.

What about using differently styled lines? Solid/dashed/dotted underlines could represent live, borrowed, mut borrowed. We could also use different end caps to represent return vs move vs drop. It would work for color blind users just as easily. Though I think editor support will be much harder. The last non-lexical example you had also worried me about how these background colors would interact with syntax highlighting. Theme owners would need to come up with colors that don’t make existing variables hard to see against the background.

Actually, thinking on it, the line style could be intuitive. A solid line represents all possible operations. A dashed line represents the lack of mutability with the spacing, but the longer dashes represent multiple readers. Dotted lines would represent single mutators with the dots.

Continuation to the next line could be represented with an arrow end cap, drop with a hollow circle, move with a solid circle, Return with… not sure. I think this area could use more work.

Playing around on gimp I think I found a nicer alternative. I still haven't thought of a way to visually explain the "why". Also, I apologize for the ugliness. I am by no means an artist. I did the initial mockup with just normal liveness and mut borrowed. With everything white it was difficult to scan at a glance. Once I added the color (it was a secondary thought, hence the terrible blending), it became much easier for me to read. I think maybe we should also consider using an overbar rather than underlines so we don't conflict with the existing pattern for warnings, errors and suggestions.

Using both color and shape at once actually helped a lot. Also worth checking out how others have solved the same issues. The pigments plugin for Atom essentially marks spans of code with colors, and actually has a few different marker types:

For the full highlights, note how the syntax highlighting is removed for the highlighted code, to assure readability. For our purposes I particularly like the possibility of using outlines. Outlines/borders provide ways of showing several things that are hard to show with simple highlights:

Though as mentioned earlier this will depend upon the actual editor displaying the data, as different editors have different drawing capabilities (for example I think there are several editors that would not allow lines to have different heights, as the bottom of the two examples above would require).

I like the outlines a lot. I’ll see if I can draw some of the existing examples with those looks. I also got started again on the prototype yesterday. @eddyb on irc pointed me to some really useful code he was working on. @jntrnr also mentioned that they’re going to make the RLS work public soon. If I can get the basic functionality in place, it should be very easy to port to RLS. Hopefully, I’ll even have it ready by the Rust Belt Rust conference to get really useful feedback :slight_smile: .

Until then:

  1. RLS https://github.com/jonathandturner/rustls/
  2. VSCode plugin for RLS https://github.com/jonathandturner/rustls_vscode
1 Like

After discussions on IRC, I found out that RLS consumes save-analysis data produced by the compiler, which does not include the data we need. I have to continue with the compiler plugin. Fortunately, I’m making some progress! It’s only crashing on my code now, and not on the compilation calls!

Edit: I’ll help update the compiler output and RLS once I figure out all the changes I need to make to the compiler. Currently the plugin prototype is an easier path. This would allow people to implement plugins for various ides while we reimplement.

Current output from the prototype (paths are for a Mac):

cargo run -- src/main.rs --crate-name borrow_bounds --crate-type bin -g -C metadata=872fa14f57985af3 --out-dir /Users/paul/programming/playground/rust/borrow_bounds/target/debug --emit=dep-info,link -L dependency=/Users/paul/programming/playground/rust/borrow_bounds/target/debug/deps --extern error_chain=/Users/paul/programming/playground/rust/borrow_bounds/target/debug/deps/liberror_chain-554f08ea2cb4f0f5.rlib --extern regex=/Users/paul/programming/playground/rust/borrow_bounds/target/debug/deps/libregex-a99351f81f55a22d.rlib  --sysroot=$(rustc --print sysroot)
    Finished debug [unoptimized + debuginfo] target(s) in 0.0 secs
     Running `target/debug/borrow_bounds src/main.rs --crate-name borrow_bounds --crate-type bin -g -C metadata=872fa14f57985af3 --out-dir /Users/paul/programming/playground/rust/borrow_bounds/target/debug --emit=dep-info,link -L dependency=/Users/paul/programming/playground/rust/borrow_bounds/target/debug/deps --extern error_chain=/Users/paul/programming/playground/rust/borrow_bounds/target/debug/deps/liberror_chain-554f08ea2cb4f0f5.rlib --extern regex=/Users/paul/programming/playground/rust/borrow_bounds/target/debug/deps/libregex-a99351f81f55a22d.rlib --sysroot=/Users/paul/.multirust/toolchains/nightly-x86_64-apple-darwin`
Looking at nodeid 98
lo: 1284, hi: 1287
found matching block: NodeLocal(pat(98: tcx))
Found 5 loans within fn identified by 98:
NodeLocal(pat(98: tcx))
NodeExpr(expr(145: move |compile_state: &mut driver::CompileState| {
let tcx =
    match compile_state.tcx {
        Some(tcx) => { tcx }
        _ => {
...

Followed by a lot more post-expanded code.

Currently, I’m writing the prototype with the rustc_driver::CompilerCalls trait and rustc_driver::run_compiler(...) function. At my current pace I’m hoping I’ll have a functioning demo with live highlighting and all by Rust Belt Rust next week. After that I’ll focus on porting my changes to RLS, which will require more involved changes to how the compiler writes its save-analysis files.

I’m also working on a VSCode extension while I wait for my poor 8 year old macbook to compile my changes to the compiler. I’ll give another update Friday night as I’ve got plans that will keep me busy tomorrow and Thursday. Hopefully I’ll have something nice to show by then :slight_smile:.

Nice! I had a similar idea about 11 months ago. Given that I don't have either the time or the required knowledge in Rust, the idea kept floating around my mind.

What I have so far is a couple of hand drawn sketches just to show how a frontend for the tool would look like. To be fair, this article about borrow and lifetimes in Rust helped me to grasp that concepts thanks to the “scope charts” to illustrate the scopes of owner, borrowers, etc. Also, I took some other graphical concepts from UML and its Sequence Diagram.

Basically, the references are:

  • Immutable binding: show a closed padlock
  • Mutable binding: don't show a padlock
  • Binding scope/lifetime: show a continuous line
  • Immutable borrowing: show a dotted/dashed line with a closed padlock
  • Mutable borrowing: show a dotted/dashed line (with no padlock)

One drawback I can think of now is the editor support. I don't know if code editors like Sublime Text, Atom, VS Code, etc, allows to implement these lines, arrows, icons and more in an easy way. If so, an alternative would be to show these graphs in an external browser window but that'd be cumbersome.

I hope these basic drawings shed some light on the design roadmap.




11 Likes

Exciting. I had not really thought about the possibility of doing this as a compiler plugin, so I've been looking mostly at doing the changes to the compiler code directly. My plan so far was to:

Modify check_crate in librustc_borrowck/borrowck/mod.rs to have it return the necessary analysis data and in phase_3_run_analysis_passes in librustc_driver/driver.rs store it in the ty::CrateAnalysis. Or perhaps it is better to store it in the TyCtxt, which avoids modifying the driver and ty::CrateAnalysis? Or is the necessary data already in TyCtxt and I'm just overlooking it?

Anyway, if the data is stored in one of those two ways it is accessible to librustc_save_analysis, and it should only be a case of modifying data.rs, external_data.rs, dump_visitor.rs and the dump implementations and it should be output. Not sure how it makes most sense to output though. Thinking maybe as additional data in VariableRefData?

Edit: By the way, I have some (rather rudimentary) code for drawing spans in Atom from my prototype with hardcoded data. Just let me know if that would be of any use to you for your prototype.

Thanks for sharing. Some interesting explorations in those notes.

The article you linked to was also quite helpful. Gave me another example of something that is quite hard to visualise with full spans. "Does not live long enough" becomes a bit clearer when you can see the borrow lasting longer than the availability of the borrowed variable:

(Would be even better if not for the fact that HTML does not support overlapping spans. Only nested. As is, the yellow has to be outside the green to go past).

Also tested the more complicated non-lexical lifetime example, but I think I'm missing something, as now I've got two mutable borrows at once in this example, which should not be allowed AFAIK.

(I've placed HTML versions of these experiments at http://erik.vestera.as/rustvis/presentation but no guarantees as to the longevity of that URL)

1 Like

I am so glad I made this post :smiley:. I wonder how many more people had the same idea around the same time.

@ssebastianj I think your examples should be possible within VS Code. It uses html to render, and extensions have control of the editing window for rendering. That being said, I haven’t attempted anything. I had actually gotten the idea somewhat from what one of the Rust resources had, where the lines were drawn on the side.

@evestera, I haven’t gotten a chance to push the changes to github. It’s not compiling at the moment due to some changes I was testing out, and I’d rather push it in a state people can actually use. The compiler_calls is definitely not the final design, just faster to get something working for now (can’t remember if I mentioned that in our earlier IRC chat).