Work in progress
The Program Left Behind
Rust Conversation R.3 — Nested Expansion and Residual Rust
Now let's add an equality check to our arithmetic:
fn matches(x: i64) -> bool {
eval_and_compare!(add(2, multiply(3, 4)) == x)
}
After every macro invocation has expanded, what ordinary Rust expression do you expect inside matches?
R.2 tells us that I can fold the entire arithmetic expression on the left into a constant. However, x is a Rust variable whose value can be known only when the program runs. So I expect:
14i64 == x
What AST struct would you construct for this input?
One question first. In R.2, we stored the parentheses in the AST. Here we also need to store the == token. How can I represent it?
Syn represents it as:
Token![==]
:::
Then I would construct:
#[derive(syn_derive::Parse, syn_derive::ToTokens)]
struct EvalAndCompare {
known_expression: IntegerExpr,
eq_eq_token: Token![==],
outside_expression: Expr,
}
Great. How would you write the logic that walks over this value and generates the expanded code?
We already wrote the evaluate function. I can call it and then quote the result:
pub(crate) fn expand_compare(input: TokenStream) -> syn::Result<TokenStream> {
let comparison: EvalAndCompare = syn::parse2(input)?;
let known_value = evaluate(&comparison.known_expression)?;
let eq_eq_token = comparison.eq_eq_token;
let outside_expression = comparison.outside_expression;
Ok(quote! {
#known_value #eq_eq_token #outside_expression
})
}
That is a good observation. Reusing a function is one of the most important ways to avoid reinventing the same wheel.
However, functions are not the only things we can reuse in macro code.
I see. We can also reuse a macro. I have seen this pattern before, but how can we apply it here?
One way to structure generated code is to place the part whose behavior is already settled behind a boundary and leave the remaining computation as a hole.
Here, eval_and_compare! is a slightly misleading name for this stage, because its own work is only to construct the comparison. It can leave the arithmetic intact inside quote! for another macro to expand. What would that version look like?
Perhaps like this:
pub(crate) fn expand_compare(input: TokenStream) -> syn::Result<TokenStream> {
let comparison: EvalAndCompare = syn::parse2(input)?;
let known_expression = comparison.known_expression;
let eq_eq_token = comparison.eq_eq_token;
let outside_expression = comparison.outside_expression;
let residual: Expr = syn::parse2(quote! {
todo!{} #eq_eq_token #outside_expression
})?;
Ok(quote! { #residual })
}
Good, very very close, your todo!{} is a macro call, you should use another macro call.
I see.
eval_integer!{#known_expression} == x
Let's double check if this expand to the same thing as using normal function reuse.
The nested invocation expands first:
eval_integer!(add(2, multiply(3, 4)))
// becomes
14i64
Therefore the complete residual expression is:
14i64 == x
Same. So why we use macro composition instead of just call evaluate function? Macro in macro is something try to overflow my brain stack.
examples/rust-02-compile-time/compile-time-macros/src/integer.rs
Open lines 47–58
pub(crate) fn expand_compare(input: TokenStream) -> syn::Result<TokenStream> { let comparison: EvalAndCompare = syn::parse2(input)?; let known_expression = comparison.known_expression; let eq_eq_token = comparison.eq_eq_token; let outside_expression = comparison.outside_expression; let residual: Expr = syn::parse2(quote! { eval_integer!(#known_expression) #eq_eq_token #outside_expression })?; Ok(quote! { #residual })}
The caller imports both macros:
use compile_time_macros::{eval_and_compare, eval_integer};
But its source explicitly invokes only eval_and_compare!. Which import appears unnecessary?
May I remove eval_integers?
Remove it and check the example again.
Compilation fails:
error: cannot find macro `eval_integer` in this scope
note: this error originates in the macro `eval_and_compare`
So eval_and_compare! expanded successfully, but its returned program still needed the consumer to bind eval_integer!.
Would the direct function-reuse version have needed that import?
No. Calling evaluate would bind the evaluator inside the upper pass and emit 14i64 == x immediately.
Macro reuse instead emits the unqualified name eval_integer!, so the consumer selects the lower pass.
Then what happens if the consumer imports another compatible macro under that name?
use another_evaluator::fold_integer as eval_integer;
expand_compare remains unchanged. Its returned syntax names only eval_integer!; the consumer decides which independently compiled lower pass that name denotes.
Run the complete caller:
cargo run --manifest-path examples/rust-02-compile-time/Cargo.toml --package compile-time-demo --example compare
true
false
Why do the two calls return different results after both compile from the same residual expression?
Both calls execute:
14i64 == x
The arithmetic was fixed during expansion, but x was not. matches(14) makes the residual comparison true; matches(15) makes it false.
examples/rust-02-compile-time/demo/examples/compare.rs
Open lines 1–10
use compile_time_macros::{eval_and_compare, eval_integer};fn matches(x: i64) -> bool { eval_and_compare!(add(2, multiply(3, 4)) == x)}fn main() { println!("{}", matches(14)); println!("{}", matches(15));}
Reuse across expansion stages
We produced the same residual program in two ways.
With function reuse, expand_compare calls evaluate while it is running. The known arithmetic becomes 14i64 before the outer expansion returns:
14i64 == x
With macro reuse, the outer stage does not compute the arithmetic. It returns syntax containing another macro invocation:
eval_integer!(add(2, multiply(3, 4))) == x
That invocation is a typed local hole: it occupies a Rust expression position, so its later expansion must produce an expression. Rustc expands it to 14i64, leaving the same residual program:
14i64 == x
No value passes from one macro to another at the first boundary. The outer macro hands rustc syntax. eval_and_compare! owns the comparison shape, eval_integer! owns arithmetic meaning, and ordinary Rust later supplies x and performs ==.
Macro composition is the mechanism: one stage emits another stage's invocation. Partial evaluation is the effect: known arithmetic disappears while the comparison depending on x remains.