My First PR for the Rust compiler
This is the first post for my new blog. I’ve somewhat recently started learning Rust and contributing to the community (not just Rust, but Servo as well). I’d like to document what went into making my first change to the compiler. It wasn’t my first commit to the project, I managed to accidentally score a spot on the Rust 1.0 contributors list for fixing typos in the documentation. I’ve also helped out on issue #22709.
It started off with me wanting to learn how the Rust compiler works. I came upon the conclusion that this would be easiest by fixing some of the issues tagged with E-Easy on their GitHub page. I found that a lot of the more recent ones had a lot of people commenting/working/mentoring already, so I decided to search for the oldest easy issues. I came across #2392, which was over 3 years old at the time. It had even changed over time as changes to the compiler were made.
The general idea at the time I started was to make this code:
output this error:
instead of this error:
I also wanted to make the note use `(s.x)(...)` if you meant to call the function stored in the `x` field
look more like use `(dog.x)(...)` if you meant to call the function stored in the `x` field
. (Note the s
=> dog
change). You can play around with this example here.
The code I needed to change was the below section of the report_error
function from the file src/librustc_typeck/check/method/suggest.rs
.
Let’s break this down a little. We’re in the suggest.rs
file under the parent directory of method
. The report_error
function is used to output suggestions when the compiler comes across some kind of error related to methods. On line A
:
We check if the receiver type is an error (this means there was an issue parsing the abc
in abc.xyz()
or the (a.x())
in (a.x()).bar()
. There’s no additional suggestions that can be made here since we can’t lookup the type to give better hints.
Moving on to line B
, we’re going to match on the error enum, which is of type MethodError
. For this issue, we’re only interested in the NoMatch
enumeration (on line C
). We also don’t care about the values that are pattern matched out (static_sources
, out_of_scope_traits
, and mode
), since they’re used for other purposes besides our issue at hand.
Here we’re using the function context to output an error. There are some issues with multilingual translations with this chunk of code, but I’ll get to that in another post. There’s nothing here I want to focus on.
This is where our focus is really needed. This is where we’re going to decide what suggestions to display after the error. We’re pattern matching against the definition id (shortened to did
in the code) of the receiver type (if the receiver type is a struct) and the existance of the receiver expression.
The call here is using the current context, cx
, along with the struct definition id to lookup all of the fields that belong to the struct with that definition id (The context stores a mapping of these ids to the different structs available from the current scope). Then, we’re going to iterate over all of the fields and see if their name matches the name of the method we attempted to call. If the compiler was on a line like (a.x()).y()
, and y
is what didn’t match, then did
would be the definition id of the struct that’s returned from the call to a.x()
, recvr_expr
would be Some
expression (a.x())
, and item_name
would be the string "y"
. If one such field is found, then a very generic suggestion is output as a note. It assumes that the user intended to call a function, without verifying if the field can be used as a function at all.
My fix for the issue (along with lots of help from eddyb on irc (#rust)) is below.
This is noticeably larger… the first change is here:
I’ve pattern matched the substs
and expr
. The expr
is explained previously (we’re just unwrapping the recvr_expr
), so we’re only going to go into what this new substs
thing is.
Let’s say we have the following method definition:
The Subst
for this corresponds to [A, B], Self=T, [X, Y]
, which, at the lowest level, is a vec![A, B, T, X, Y]
and a couple of indices splitting the vector into 3 pieces (again, thanks to eddyb).
So, now this makes it clear what a TyStruct
contains. A definition id that can be used to lookup some (potentially generic) struct, along with a list of types that can fill in all of those generics slots.
Moving on to the next if
:
Rather than just checking if a field with a matching name exists, I’m going to find one that exists and store it in the field
variable. If there’s none, then there’s no suggestion to be made, just like the code before the change.
Here I’m generating a string to print out the (a.x())
in (a.x()).y())
. The span in expr.span
represents a section of a string with two indices that represent the beginning and the end of the section. The string in question is the code that’s being parsed by the compiler. This string lives in the codemap of the context’s session. We can use the codemap’s span_to_snippet
method to convert the expression’s span into the string that represents the expression. If this fails for any reason, we’re going to default back to the old code’s beloved s
. (I really have no idea what the s
signifies, it was there before, so I decided to keep it for backup).
These are just closures to write the two span notes that can possible be output from here on. One of the two will be displayed at this point.
Here we’re looking up the field type. This will be used later on. It represents the concrete struct of the field.
Here we try and require the Rust provided FnOnce
trait language item. We’re going to try and use this to determine if the field implements the FnOnce
trait. If we’re able to obtain the trait definition id, then we can move forward with the more solid approach (this should occur most of the time).
The call to probe
, is necessary because the call to next_ty_var
at M
will modify the inferred context (infctx
). The probe
call will allow us to scope that change, and when the probe ends, the changes will be reverted.
This block is determining if the field type implements FnOnce
. If it does, then we’re going to display the stored function suggestion to the user. If not, we’re going to recommend removing the parenthesis.
Let’s roll back to line L
and consider the rare case that we can’t get the FnOnce
trait definition id. We’re going to try and match the field’s type. If it’s a closure or a bare function type, then we display the stored function message, otherwise we recommend removing the parenthesis.
Now, after this change, attempting to compile the above code will produce the below error.
You can play with this example here.
Note: After I started writing this post, but before it was published, I was mentioned in issue #26472. After reviewing that issue, I realized there’s a bug in my code, in that it will recommend private fields to a user. The fix for that is more complicated than you would think (I’d have to take the private scope into account, and currently, that’s in another module in the compiler, in a completely separate pass).
Let me know what you think of this article on twitter @Nashenas88 or leave a comment below!