-
Notifications
You must be signed in to change notification settings - Fork 2.1k
Add initial version of macros course #3265
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Large diffs are not rendered by default.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,38 @@ | ||
| --- | ||
| minutes: 5 | ||
| --- | ||
|
|
||
| <!-- | ||
| Copyright 2026 Google LLC | ||
| SPDX-License-Identifier: CC-BY-4.0 | ||
| --> | ||
|
|
||
| # Brackets and Token Trees | ||
|
|
||
| A **token tree** can represent either a single token, or a **group** of tokens | ||
| enclosed by matching delimiters (brackets): | ||
|
|
||
| - **Single tokens (leaf nodes):** `foo`, `+`, `,`, `123`. | ||
| - **Grouped tokens (internal nodes):** Enclosed by parentheses `()`, braces | ||
| `{}`, or square brackets `[]`. | ||
|
|
||
| For example, the token stream: `foo + (bar * baz)` | ||
|
|
||
| Is parsed into **3 separate token trees**: | ||
|
|
||
| 1. `foo` (a single token) | ||
| 2. `+` (a single token) | ||
| 3. `(bar * baz)` (a token group containing three child token trees: `bar`, `*`, | ||
| and `baz`) | ||
|
|
||
| Because grouping happens _during_ lexical analysis, **unbalanced groups are | ||
| strictly disallowed**. You cannot pass unbalanced parentheses or braces into or | ||
| out of a macro! | ||
|
|
||
| <details> | ||
|
|
||
| - Note that this means you cannot use a macro to generate half a block, like | ||
| `let x = {` and close it with another macro or tokens outside the macro. The | ||
| entire block must be passed or returned as a single, well-formed token group. | ||
|
|
||
| </details> |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,31 @@ | ||
| --- | ||
| minutes: 5 | ||
| --- | ||
|
|
||
| <!-- | ||
| Copyright 2026 Google LLC | ||
| SPDX-License-Identifier: CC-BY-4.0 | ||
| --> | ||
|
|
||
| # Ways To Define Macros | ||
|
|
||
| There are two separate ways of implementing macros in Rust. Each has distinct | ||
| advantages and trade-offs: | ||
|
|
||
| | Feature | Declarative Macros | Procedural Macros | | ||
| | ------------ | -------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------ | | ||
| | **Usage** | Function-like macros only | All three kinds (Derive, Attr, Function-like) | | ||
| | **Location** | Implemented within your normal crate | Must be defined in a separate `proc-macro` crate | | ||
| | **Pros** | - Low boilerplate<br>- No extra compilation step<br>- Easy to write and reuse | - Extremely powerful and expressive<br>- Written in standard Rust<br>- Full programmatic control | | ||
| | **Cons** | - Bespoke pattern-matching syntax<br>- Cannot inspect arbitrary token structures | - Can slow down build time<br>- Substantial boilerplate required | | ||
| | **Hygiene** | Partially/mixed hygienic by default | Configurable / custom hygiene | | ||
|
|
||
| <details> | ||
|
|
||
| - Explain that procedural macros are literally compiled as libraries and | ||
| executed _inside_ the compiler while compiling the consuming code. This is why | ||
| they require a separate crate. | ||
| - Emphasize that you should always prefer declarative macros for simple code | ||
| generation due to their much smaller impact on build times. | ||
|
|
||
| </details> |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,42 @@ | ||
| --- | ||
| minutes: 5 | ||
| --- | ||
|
|
||
| <!-- | ||
| Copyright 2026 Google LLC | ||
| SPDX-License-Identifier: CC-BY-4.0 | ||
| --> | ||
|
|
||
| # Macros In Rust | ||
|
|
||
| Rust macros provide structured code generation. There are **three kinds** of | ||
| macros, and **two ways** they are implemented. | ||
|
|
||
| ### Kinds of Macros | ||
|
|
||
| - **Derive Macros:** Added to type definitions (structs, enums, unions) to | ||
| auto-implement traits (e.g., `#[derive(Default)]`). | ||
| - **Function-Like Macros:** Invoked with an exclamation mark in | ||
| item/statement/expr context (e.g., `println!("Hello!")`, `vec![1, 2, 3]`, or | ||
| `include_bytes!("manifest.bin")`). | ||
| - **Attribute Macros:** Attached as custom attributes to any item, like | ||
| functions or modules (e.g., `#[tokio::main]`). | ||
|
|
||
| ### Implementation Forms | ||
|
|
||
| - **Declarative Macros (AKA "Macros By Example"):** Part of the language itself, | ||
| based on pattern matching; these can only be used to define function-like | ||
| macros. | ||
| - **Procedural Macros:** Rust functions running as compiler plugins that | ||
| transform token streams. These can perform arbitrary operations at compile | ||
| time, including I/O if desired. | ||
|
|
||
| <details> | ||
|
|
||
| - Highlight that students have already used function-like macros (like | ||
| `println!`, `vec!`, `format!`) and derive macros (like | ||
| `#[derive(Clone, Debug)]`). | ||
| - Explain that attribute macros are very popular in libraries like Tokio or | ||
| Axum, often transforming functions similar to Python decorators. | ||
|
|
||
| </details> |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,38 @@ | ||
| --- | ||
| minutes: 5 | ||
| --- | ||
|
|
||
| <!-- | ||
| Copyright 2026 Google LLC | ||
| SPDX-License-Identifier: CC-BY-4.0 | ||
| --> | ||
|
|
||
| # Hygiene | ||
|
|
||
| A subtle aspect of macro systems is their degree of **hygiene**, or independence | ||
| from the lexical environment of their expansion. | ||
|
|
||
| Macro hygiene enables macros to avoid accidentally being influenced by or | ||
| polluting the scope of the code surrounding their call sites. | ||
|
|
||
| In this section, we will cover: | ||
|
|
||
| - What macro hygiene is and why it is important. | ||
| - How unhygienic macros can result in bugs or impede understanding code. | ||
| - The extent to which Rust macros are hygienic and the how partial hygiene in | ||
| Rust macros works. | ||
|
|
||
| <details> | ||
|
|
||
| - Explain that in many preprocessor-based languages (such as C), macros are | ||
| completely unhygienic, operating solely on raw tokens and potentially | ||
| interacting with the lexical environment differently at each expansion. This | ||
| means that the ability to reason about macros agnostic of the context in which | ||
| they will expand is extremely limited. This can lead to bugs when names used | ||
| in macros coincide with names used at their call sites. | ||
| - The notion of hygiene may be familiar to students who know LISP, as it | ||
| famously exhibits fully hygienic macros. | ||
| - This slide serves as a transition into the detailed discussion of macro | ||
| hygiene. | ||
|
|
||
| </details> |
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
| @@ -0,0 +1,85 @@ | ||||||
| --- | ||||||
| minutes: 5 | ||||||
| --- | ||||||
|
|
||||||
| <!-- | ||||||
| Copyright 2026 Google LLC | ||||||
| SPDX-License-Identifier: CC-BY-4.0 | ||||||
| --> | ||||||
|
|
||||||
| # Hygiene In Rust Macros | ||||||
|
|
||||||
| Declarative macros in Rust are partially hygieninic. | ||||||
|
|
||||||
| - They are hygienic with respect to: local variables, parameters, loop labels, | ||||||
| and the special `$crate` variable. | ||||||
| - They are **not** hygienic with respect to: items, types, methods, and traits. | ||||||
|
|
||||||
| ## Rationale | ||||||
|
|
||||||
| Frequently, rust macros are used as shorthand to refer to existing types and | ||||||
| traits, e.g. when defining `impl`s. In this situation, hygienic macros would | ||||||
| always need to accept all relevant items as arguments, imposing a floor beneath | ||||||
| which we could not decrease lexical boilerplate. | ||||||
|
Comment on lines
+20
to
+23
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This could benefit from an example showing how macros that interact with with items get more verbose when you have to explicitly pass in everything. Maybe something that implements a trait for a bunch of types, and would have to have the trait and all of the method names passed in explicitly? |
||||||
|
|
||||||
| On the other hand, hygiene helps us write reliable code, so it is desirable for | ||||||
| any internal operations that a macro may want to perform. Luckily for us, Rust | ||||||
| does provide a solution for hygienic references to items. | ||||||
|
|
||||||
| ### The `$crate` variable | ||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This section on |
||||||
|
|
||||||
| Item and crate paths are unhygienic, so item paths within a macro definition | ||||||
| could will refer to a different item than intended if their leading module or | ||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||
| crate name is defined differently at the call site than the macro author | ||||||
| expected. | ||||||
|
Comment on lines
+31
to
+34
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This could use a dedicated example showing how a path in a macro gets interpreted relative to where the macro is expanded, maybe also splitting this into its own slide. |
||||||
|
|
||||||
| In general, declarative macros themselves cannot carry along crate dependencies | ||||||
| in a hygienic way. However, there is a way out: to unambiguously refer to the | ||||||
| macro's **defining crate** only, the `$crate` metavariable may be used. | ||||||
|
|
||||||
| `$crate` expands to the root path of the crate that defined the macro. This can | ||||||
| be used to refer to local helper items without fear of interference, regardless | ||||||
| of the macro call site. These local helpers may call or re-exports items from | ||||||
| the standard library or other dependencies. | ||||||
|
|
||||||
| ```rust,compile_fail | ||||||
| // Macro-defining crate `my_macros` | ||||||
| pub fn my_macro_helper(s: &str) { | ||||||
| std::io::print(s) | ||||||
| } | ||||||
|
|
||||||
| macro_rules! print_something { | ||||||
| ($args:tt) => { | ||||||
| // Safe from shadowing of the standard library or any other crate, | ||||||
| // because items from this crate accessed with $crate are hygienic! | ||||||
| $crate::my_macro_helper(stringify!($args)) | ||||||
| }; | ||||||
| } | ||||||
|
|
||||||
| // Macro-consuming crate that alters meaning of the `std` crate name | ||||||
| #![no_std] | ||||||
|
|
||||||
| // libcore exports many similar APIs to libstd, but not `io::print` | ||||||
| extern crate core as std; | ||||||
|
|
||||||
| fn main() { | ||||||
| my_macros::print_something!() | ||||||
| } | ||||||
| ``` | ||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This example is very confusing to me:
I think the better example would be to have two crates that define the same function, and show that using Alternative example codeCrate 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 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
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. My motivation here was to produce a somewhat realistic situation where this would occur--the most likely way macros might rely on ambiently-available names is via the standard library, but some consumers may violate that expectation. For some reason I thought there was a non-macro But given that And yeah, I don't think we can provide a working interactive example here because what's really happening is a cross-crate interaction. But I'll change the example to something closer to what you suggest.
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Having thought about this more, I think it might be best to split this slide in half. The first can demonstrate how the lack of hygiene for paths can make a macro refer to different items when invoked from different locations, and show how we might use absolute paths to try to avoid that situation. This can be done with multiple modules in a single crate. The second can show that even absolute paths do not solve this problem completely in the presence of multiple crates, and motivate/explain the $crate keyword. We kind of try to do this already, but the existing "what is hygiene" slide is really just introducing the concept without demonstrating how the semantics of Rust item paths interact with it. |
||||||
|
|
||||||
| <details> | ||||||
|
|
||||||
| - Carefully delineate dependencies in the example: the program as a whole | ||||||
| depends on libstd, but in the top-level crate it is not a direct dependency, | ||||||
| and libcore is imported with its name instead. libcore does not export | ||||||
| `io::print`, so a straightforward reference to `std::io::print` in the macro | ||||||
| would expand to a non-existing path. But because the macro crate does depend | ||||||
| on libstd, and the macro only accesses its own local helper through the | ||||||
| `$crate` metavariable, it is able to reliably refer to the stdlib (or another | ||||||
| crate) indirectly. | ||||||
| - Explain that to enforce hygiene on local variables, the compiler keeps track | ||||||
| of "syntax contexts." A local variable defined inside the macro has a | ||||||
| different syntax context than a variable of the same name defined outside, | ||||||
| which prevents collisions. | ||||||
|
|
||||||
| </details> | ||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The two examples from this slide could probably be broken into two separate slides. Maybe three: One for the "What Is Macro Hiegiene" section at the top, and then two more slides for the two examples. |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,56 @@ | ||
| --- | ||
| minutes: 5 | ||
| --- | ||
|
|
||
| <!-- | ||
| Copyright 2026 Google LLC | ||
| SPDX-License-Identifier: CC-BY-4.0 | ||
| --> | ||
|
|
||
| # What Is Macro Hygiene | ||
|
|
||
| A macro system is **unhygienic** if a macro can: | ||
|
|
||
| 1. Implicitly access identifiers in the surrounding callsite scope. | ||
| 2. Define a new local identifier that bleeds out and is implicitly accessible by | ||
| the surrounding callsite. | ||
|
|
||
| ### Example 1: Implicitly Accessing Callsite State (Unhygienic) | ||
|
|
||
| ```rust,ignore | ||
| macro_rules! use_local { | ||
| () => { | ||
| // Unhygienic: attempts to implicitly read `local` from callsite | ||
| println!("{}", local); | ||
| }; | ||
| } | ||
|
|
||
| fn main() { | ||
| let local = "Hello, Macros!".to_string(); | ||
| use_local!(); // In an unhygienic system, this would compile! | ||
| } | ||
| ``` | ||
|
|
||
| ### Example 2: Leaking Local Variables (Unhygienic) | ||
|
|
||
| ```rust,ignore | ||
| macro_rules! make_local { | ||
| () => { | ||
| // Unhygienic: attempts to leak `local` to callsite | ||
| let local = "Hello, Macros!".to_string(); | ||
| }; | ||
| } | ||
|
|
||
| fn main() { | ||
| make_local!(); | ||
| println!("{}", local); // In an unhygienic system, this would compile! | ||
| } | ||
| ``` | ||
|
|
||
| In Rust, **neither of these examples compile**. Both produce the error: | ||
| `error[E0425]: cannot find value 'local' in this scope`. | ||
|
|
||
| Rust's macro system treats variables hygienically, protecting from silent | ||
| namespace pollution. | ||
|
|
||
| However, declarative macros in Rust are not fully hygienic! | ||
|
Comment on lines
+50
to
+56
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. These lines could be pulled into speaker notes to make the slide a bit more concise. |
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The nesting is semantically helpful to understand the structure of the information, but I agree and have a similar workflow when teaching--I often want to see how many slides I need to cover and which are coming up next. What if we made it possible (or the default) to expand all children?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe a conservative compromise for now is to just unindent the individual syn and quote slides.