Skip to content

← Macros, FFI and Type-Driven Design step 10 of 28

Hard Primitives

Macro callbacks and token-tree bundles

A macro can generate a call to another macro. Pass the callback's name as an argument, then use that name in the generated invocation. This lets one macro prepare arguments while another decides what to do with them.

Your task

Fix with_list! and call_with!. Keep the supplied sum_all!, max_all!, dispatch and bundled definitions.

Invocation Result
with_list!(sum_all, [1, 2, 3, 4]) 10
with_list!(max_all, [3, -7, 9, 2]) 9
call_with!(sum_all, (10, 20, 30)) 60
call_with!(max_all, (-1, -5, -3)) -1

dispatch() returns (10, 9) and bundled() returns (60, -1). Both adapters take a callback name without !. The starter's first adapter emits a function call instead of a macro invocation; the second ignores its bundle.

Why nesting does not splice in an argument list

Suppose make_list!() expands to the tokens 1, 2, 3. This does not make sum_all!(make_list!()) equivalent to sum_all!(1, 2, 3).

The sum_all! matcher sees the unexpanded invocation make_list!() as one expression. If it forwards that expression into its sum, the nested macro must eventually produce a valid expression in that position. Three bare, comma-separated expressions do not satisfy that requirement.

A nested macro that expands to a single expression can work perfectly well. The problem is trying to have the inner expansion change how the outer macro has already matched its arguments.

Generate the callback invocation

The matcher for with_list! captures a name and a bracketed expression list:

($cb:ident, [$($x:expr),* $(,)?])

$cb:ident accepts a single identifier, such as sum_all, rather than a qualified path such as helpers::sum_all. The repeated expr capture accepts comma-separated expressions, with an optional trailing comma.

Generate a macro invocation with those expressions:

$cb!($($x),*)

The ! matters: $cb(...) would call a function. Here, the generated invocation is expanded as another macro call. No runtime callback or function pointer is involved.

Forwarded expr fragments remain parsed expressions. A receiving macro can capture them as expressions, as the supplied callbacks do; it generally cannot inspect their original spelling with literal-token matchers.

Forward a delimited bundle

A token tree, or tt, can be a single token or an entire delimited group. In this exercise, pass a group containing the callback's arguments:

($cb:ident, $args:tt) => {
    $cb! $args
};

For call_with!(sum_all, (10, 20, 30)), $args contains the parentheses as well as the tokens inside. The generated call is sum_all!(10, 20, 30). call_with! does not need to parse the individual expressions.

Although tt also accepts a bare token, the emitted macro call needs a delimited group. Use parentheses, brackets or braces around the bundle. The callback still determines which contents are valid: sum_all! accepts an empty list, but max_all! requires a first expression.

What about built-in macros?

Some compiler built-ins expand nested arguments before processing them. For example, concat!("a", stringify!(b)) produces "ab". That is a feature of concat!, not evidence that stringify! eagerly expands its input. stringify!(make_list!()) produces text describing the invocation, rather than the list it would generate.

Do not assume that your own declarative or procedural macro receives an already expanded argument list. A callback is useful when a token-producing macro needs to hand its output to a consumer explicitly.