Skip to content

Add initial version of macros course - #3265

Open
fw-immunant wants to merge 2 commits into
google:mainfrom
fw-immunant:fw/macros
Open

fw-immunant wants to merge 2 commits into
google:mainfrom
fw-immunant:fw/macros

Conversation

@fw-immunant

Copy link
Copy Markdown
Collaborator

This covers declarative and procedural macros, and should be a good complement to the fundamentals course

@fw-immunant
fw-immunant force-pushed the fw/macros branch 8 times, most recently from c0c3d7c to 3636813 Compare August 25, 2026 01:00
@fw-immunant
fw-immunant force-pushed the fw/macros branch 2 times, most recently from 19049a2 to de5f466 Compare August 25, 2026 21:37
This covers declarative and procedural macros, and should be a good complement to the fundamentals course
@djmitche

Copy link
Copy Markdown
Collaborator

Wow, this is exciting! I am pretty far from the course now, and will not have the time to review. Sorry!

@djmitche
djmitche removed their request for review August 25, 2026 23:48

@randomPoison randomPoison left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is off to a good start! I have various suggestions, but most of them are pretty minor. I think there's room to further flesh out the proc-macro section, especially with more exercises, but I don't think we need to iterate on that in this PR.

For the new exercises, it would be good to split the code into their own files and then pull sections into the slides with #include, that way the exercise and solution code stay in sync. At least that's how we do it for the Fundamentals exercises (and you did this already for the proc macro exercise).

- Explain that `#[derive(Debug)]` generates an implementation of
`std::fmt::Debug` for the struct.
- Note that derive macros can also support "helper attributes" (attributes
placed on struct fields, like `#[serde(rename = "name")]`), which configure

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A better example might be the #[default] attribute from the Default derive, which is used to specify the default variant for an enum. The example code could probably also be changed to demonstrate and enum with that attribute.

Comment thread src/macros/streams.md
}
```

- Because macro output is inserted as an AST subtree, we don't need to wrap

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
- Because macro output is inserted as an AST subtree, we don't need to wrap
Because macro output is inserted as an AST subtree, we don't need to wrap

`,`, `:`, etc.) can be sequenced to design readable and mnemonic input syntax
for your macros.

Note, this macro invokes itself recursively in its transcriber body.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This line would be better as a speaker note, mostly just to keep the slide a bit more compact.

Comment on lines +37 to +41
There is overlap between a number of these specifiers, e.g. we could write our
own pattern for blocks using `stmt` instead of using `block` directly. In
general, try to reach for the most semantically appropriate fragment specifier
where possible, which will catch more syntactic edge cases and integrate more
seamlessly into the language.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this bit could be a speaker note.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you think it'd be useful here to talk about making the trailing separator optional? The way the example is written you can't have a trailing ,. If the matcher were changed to $( $val:expr, )+ then a trailing separator would be mandatory. You can make the trailing comma optional by making the matcher $( $val:expr ),+ $(,)?, which is a little cumbersome but makes the macro syntax more flexible.

If this isn't the right place to call that out, maybe we could have a slide for this in the "declarative macro techniques" section?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is worth a speaker note on the repetition slide. Will add.

fn main() {
my_macros::print_something!()
}
```

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This example is very confusing to me:

  • It references std::io::print, which doesn't appear to be a thing.
  • Uses #[no_std], which is something students may not understand but would be awkward to explain here.
  • It tries to illustrate that using #[no_std] in a downstream crate would change what std means in the macro, except the macro only references my_macro_helper. The speaker note somewhat explains what's going on here, but I think the code example needs to illustrate that better.

I think the better example would be to have two crates that define the same function, and show that using $crate allows the macro to always reference the one from the crate where the macro is defined.

Alternative example code

Crate A, which defines a function and a macro that references the function:

pub fn print() {
    println!("Hello from crate A");
}

macro_rules! print_something {
    () => {
        // With $crate this will always reference `print` in this
        // same crate, without it the macro will call whatever
        // `print` is in scope where the macro is invoked.
        $crate::print()
    };
}

Crate B, which defines its own print and uses the macro from crate A:

use crate_a::print_something;

pub fn print() {
    println!("Hello from crate B");
}

fn main() {
    print_something!();
}

We'd then show that the program prints "Hello from crate A", and if $crate is removed then it'll print "Hello from crate B". Not sure if we can actually demonstrate that in the slides though, since it involves multiple crates interacting :/

Comment thread src/SUMMARY.md
Comment on lines +297 to +303
- [Writing Procedural Macros](macros/proc-macros/writing.md)
- [Dependencies](macros/proc-macros/writing/deps.md)
- [The `proc_macro` Crate](macros/proc-macros/writing/deps/proc_macro.md)
- [The `proc_macro2` Crate](macros/proc-macros/writing/deps/proc_macro2.md)
- [The `syn` and `quote` Crates](macros/proc-macros/writing/deps/syn-quote.md)
- [The `syn` AST](macros/proc-macros/writing/deps/syn-ast.md)
- [The `quote!` macro](macros/proc-macros/writing/deps/quote-macro.md)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
- [Writing Procedural Macros](macros/proc-macros/writing.md)
- [Dependencies](macros/proc-macros/writing/deps.md)
- [The `proc_macro` Crate](macros/proc-macros/writing/deps/proc_macro.md)
- [The `proc_macro2` Crate](macros/proc-macros/writing/deps/proc_macro2.md)
- [The `syn` and `quote` Crates](macros/proc-macros/writing/deps/syn-quote.md)
- [The `syn` AST](macros/proc-macros/writing/deps/syn-ast.md)
- [The `quote!` macro](macros/proc-macros/writing/deps/quote-macro.md)
- [Writing Procedural Macros](macros/proc-macros/writing.md)
- [Dependencies](macros/proc-macros/writing/deps.md)
- [The `proc_macro` Crate](macros/proc-macros/writing/deps/proc_macro.md)
- [The `proc_macro2` Crate](macros/proc-macros/writing/deps/proc_macro2.md)
- [The `syn` and `quote` Crates](macros/proc-macros/writing/deps/syn-quote.md)
- [The `syn` AST](macros/proc-macros/writing/deps/syn-ast.md)
- [The `quote!` macro](macros/proc-macros/writing/deps/quote-macro.md)

I prefer to avoid deep nesting of slides like this. While teaching I often look at the table of contents to remind myself how many slides I have left, which helps with time management. Nesting slides like this somewhat interferes with my ability to do that quickly.

If you think it's worth de-nesting this a bit, then I think the morning slides could be structured a bit more flatly as well.

use syn::{DeriveInput, parse2};

fn derive_display_impl(input: TokenStream) -> TokenStream {
// ANCHOR-END: Derive

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
// ANCHOR-END: Derive
// ANCHOR_END: Derive

Comment on lines +19 to +26
// Generate the Display implementation using quote!
quote! {
impl std::fmt::Display for #name {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", #name_str)
}
}
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This example solution doesn't do anything with the fields of the struct, is that intentional? I would think it would be better to have students actually handle the fields, since that'd also require them to handle repetitions in quote!.

Comment on lines +15 to +19
Since we are running this in a single-file environment without a separate
`proc-macro = true` crate setup, we will write a normal Rust function that takes
simulated token streams (using `proc_macro2` and `quote!`), parses them with
`syn`, generates the output with `quote!`, and asserts that the generated code
is correct.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't love this setup :/ I think it'd be better to have students actually setup a local workspace with a separate crate for the proc macro, and then test the proc macro's implementation by using the derive in actual code.

That said, I'm not sure there's a good way give students that setup, since we're somewhat limited by the fact that we give students the starting code in the slides. Maybe we just give students the test code and have them setup the local crate structure? e.g. we give them something like:

#[derive(Display)]
struct MyAwesomeType {
    field: i32,
}

fn main() {
    let my_type = MyAwesomeType { field: 123 };
    let string = my_type.to_string(); // Goes through `Display`.
    assert_eq!(my_string, "MyAwesomeType: 123"); // Or whatever we want the output string to be.
}

And then we have them setup the crates and proc macro to get that to compile and run.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants