BorIs - An Ownership and Borrowing Visualizer

Hello all :wave:
as part of my master's thesis, I am building a tool for generating interactive ownership and lifetime visualizations for rust programs.
The main goal of this project is to make these concepts easier to learn for beginners and people switching from other languages to rust.

It could also be useful for detecting and understanding borrow checker errors, however error visualizations are currently very limited.
As the analysis does not depend on the rustc borrow checker/ polonius, it does not have access to the 'official' borrow checker output.
The ownership and reference analysis is fully hand-written utilizing the RustAnalyzer crate, thus expect bugs and by far not full language support (a list of known limitations can be found in the GitHub page).
However, for most cases (even more complex ones) the tool was able to produce at least a meaningful output in my tests so far.

As this is part of a research project, you would help me a lot if you spend two minutes on a survey about such visualizations (only 7 short questions).
The survey even contains interactive visualizations generated by this tool, which you can play around with in your browser!

https://opnform.com/forms/visualizing-ownership-and-borrowing-in-rust-programs-nseo4z

I would be happy to hear your feedback and comments!

Best regards
Christian

14 Likes

Here are some comments about introducing Rust ownership and borrowing concepts generally, which are only tangentially related to your diagrams. (I ran out of space in the comment form and they're tangential anyway, I think.)

Primitive Types

  • The distinction is any type that's Copy, which is not limited to primitive types

Mutability

  • mut bindings prevent overwriting and taking &mut _ to a given variable, but not actual mutation in the face of interior mutability
  • So depending on how pedantic you want to be, I can write to a non-mut binding
  • Example
    • Pedant retort: I didn't overwrite the Cell itself

Borrowing

  • "Exclusive reference" or "unique reference" are better terms than "mutable reference" (despite the spelling) due to the above reasons

All the mut/borrowing terminology notes are basically forward-looking defenses around newcomers having to go back, throw out various things they were told, and relearn about what things can actually be mutated once they learn about shared mutability (various Cells, atomics, Mutex, etc).

1 Like

Hi, thank you for your feedback! I definitely agree with all of your points.

Regarding the Copy-trait: I have not mentioned this in the survey, as I was not sure who would be answering.. Someone who is completely new to Rust would not know about traits yet, and I was afraid that the survey would become too long. Similar reason for interior mutability..
Calling &mut exclusive/unique would probably have been a good idea looking back.

Personally, I am still pretty new to Rust, this is my first project, which I have basically used to learn these concepts myself..

I have already received some quite a lot of interesting comments/suggestions in the survey, so thank you to everyone who has answered so far!

1 Like

The idea looks good, though I probably would not use this tool (compiler messages were good enough for me so far), though I think loops need more work (I written details in the survey comment). Note that “another example to play with” has

bottom-aligned to closure on the right

which would look better if is_odd was either aligned to the middle or the top of the block on the right, and said block did not end with an empty line.

Expansion of macros on click is nice, I am going to check whether I can somehow get something similar in Neovim.

1 Like

Hello, thank you for the comments. I have not read all the survey results yet, but I can imagine what you are referring to..

I do agree that alignment is a bit odd/unintuitive in this case.. this could probably be fixed in the future, but this is hard to generalize for all cases.
In general, the renderer tries to lay out the expression from top to bottom in the order they are evaluated, such that the 'lifetime' spans on the left side make sense.
Otherwise there could be a case like:

let mut x = 42;
x = {
  println!("do sth..");
  x + 1  
};

where the 'old' value of x is used 'after' it has been reassigned.
In case of the closure, this should not really apply, but currently all cases are handled equally for simplicity..

The renderer is working on the HIR level, so the macro expansion was basically for free (thanks to RustAnalyzer :slight_smile: ).
Another neat side effect of this is that you can also expand for, while let, and ?.
At least that is what I have implemented so far, by default everything is expanded and I am just re-sugaring it for rendering.

1 Like

I did not realize that it is based on evaluation order, makes sense now. I would say that doing something like this:

is less confusing (note faint arrows: for is_odd I also removed highlighting of the extra empty line and did not do that for sum_of_squared_odd_numbers to check what looks better: I think highlighted empty line is better, and it is not like you can get rid of it in general case, though you probably can special-case moving everything down one line if last line is just }).

I should say though that for me existing presentation has become more fine with understanding of the logic behind the placement. Still maybe at least something like that:

would be good, or maybe even better than my arrows (they kinda look silly).

1 Like

I have just hacked this in, and I do think it makes it look a bit more cohesive..

I am also just testing if removing the vertical control-flow lines in branches, add much to the readability, or just add clutter..
Not sure about this yet, but there is definitely still much room for improvement.

1 Like