Come On Nut…
56 Days Until I Can Walk
Ok, so, I thought today was going to run much smoother than it ultimately did. I’m on the final few tasks of this builder workshop, and the remaining problems seem like they should be very straightforward:
- Use attributes to generate a setter for collection fields that allow users to add one item at a time
- Generate a compiler error message if there is a typo in the attribute
- Handle redefined preludes
I have spent more or less all of today working on the first two. Let’s start by considering the first challenge above.
If a field in a struct is annotated with the #[builder(each = "...")]
attribute, then I can assume that this field is a Vec
type. I must therefore generate a setter which allows the user to insert items into this collection one at a time. The name of the setter is the string value given in quotes. See an example below:
The problem really is more of a conceptual one than anything to do with how challenging the task is. When inspecting a field in a DerivedInput
struct, each field has an attr
property containing attributes. Extracting this information is trivially simple. Parsing it, however, is not. builder
is itself an attribute, and each
is an attribute nested under it. Therefore, what we seemingly want to use is the parse_nested_meta
method on the Attribute
struct. This method takes a closure as input and lets us cycle through and inspect the attributes nested under the parent. The below code will work to solve the first challenge:
Given a field as input, we iterate over the field’s attributes. If we encounter an attribute with the identity “builder”, then we inspect the attributes nested beneath it. If we find the key/value pair “each”, then we parse the value as a string literal and store it in a result
variable defined in the outside the scope of the closure. We return an empty Ok
result to signal that all went well.
As mentioned, this works! And should extend easily for the case where we need to handle an erroneous key/value pair, right? Seemingly not so. If we extend this just a little bit to solve the second challenge, I arrive at the below solution:
I would expect the above to work, once the issue of throwing the compiler error is resolved, but it does not. In fact, what I found is that parse_nested_meta
is returning an error here for each
, indicating that the equals sign used is a syntax error, and a comma was expected instead. This is very strange, considering syn
can parse the attribute and correctly pass its value back to me. So even though I can parse and use this attribute correctly, the compiler error is still throwing, even though the attribute is correctly formatted by my definition. What’s going on here? I’m not sure yet.
I am aware that there is another method—parse_args
—wherein I believe it is up to me to manually parse the contents of the attribute. This seems overly laborious. Doable! But laborious. So I’m a little stumped here.
However, it was interesting to see how to inject compiler errors into the AST using syn::Error
. Instead of calling the compile_error!
macro, triggering something like a panic (I believe), what we actually want to do is insert the compiler error directly into the AST and let the compiler itself trigger the panic. So we can’t just throw a compile error from anywhere. Instead we still need our derive
function to return a TokenStream. But this should contain the tokens for a compile error instead of the tokens we were planning to produce.
This might look something akin to the below:
Ultimately I was able to generate a compiler error message, but my message doesn’t exactly match the expected one (I’m underlining a little bit too much code in the output). I’ve learned a lot about macros from this exercise, and I’m going to keep chipping away at this tomorrow. But at this stage, this feels like diminishing returns. Last week I had no concept of how macros work. This week, I actually feel like I have a foundational understanding, although I’m clearly no expert.
I think the best thing to do will be to get as much done tomorrow as I can, and then jump back into the ray caster on Monday. At the very least, I am happy that I know how to use declarative macros to clean my code a little, and avoid unnecessary function calls.