A cursed way to combine module paths in Rust’s declarative macros
16 May, 2025
Today, I ran into an interesting problem with a not-so-obvious solution: How do you concatenate path segments in Rust’s declarative macros?
TLDR:
Use repetition
$($segment:ident)::*
with::
separator. Each path segment is an identifier (ident
).// Macro: // Invoke: compose_paths!; compose_paths!; // Expansion: use A; use B; use A; use B;
Why I need to do this
In my project ferrishot I have two very similar enums:
Each Command
and KeymappableCommand
pair is created by a macro in their specific module.
KeymappableCommand
is almost the same as Command
, but it has additional fields representing the keystrokes required to invoke this command.
This enum is used to parse the KDL config file using knus:
keys
By parsing that we will have something like this:
use crate KeymappableCommand;
vec!
What we actually store is a HashMap<KEYS, Command>
where Command
is a version of KeymappableCommand
that excludes the key
and mods
field, where KEYS
is a tuple of said (key, mods)
.
This allows us to retrieve the correct Command
for any given key sequence in O(1)
time.
Each module has its own pair of (KeymappableCommand, Command)
, and then there are 2 separate enums at the root of the crate: crate::KeymappableCommand
and crate::Command
which compose all of the other KeymappableCommand
s and Command
s from other module.
The naive approach
In order to create a macro that lets me define the following:
As follows, for instance:
declare_commands!
I need to just write the path once, then concatenate it with the item I want to use. I initially tried this:
// surely, this will work right?
}
}
It feels like it should work. But specifically, this part is not valid Rust:
$path Command
$path KeymappableCommand
But an attempt to compile the above results in an error:
error: missing angle brackets in associated item path
-/main.rs:30:18
|
30 | $Variant
| ^^^^^
...
42 | / declare_commands!
| |_- in this macro invocation
|
= note: this error originates in the macro `declare_commands`
help: types that don't start with an identifier need to be surrounded with angle brackets in qualified paths
|
30 | $Variant
| + +
error: missing angle brackets in associated item path
-/main.rs:36:18
|
36 | $Variant
| ^^^^^
...
42 | / declare_commands!
| |_- in this macro invocation
The suggestion to add < >
is incorrect here, as that is not valid syntax for referencing items in modules.
If we try the advice, we get another error:
error: expected type, found module `crate action`
-/main.rs:43:16
|
43 | ImageUpload,
-/main.rs:44:8
|
44 | App,
-/main.rs:45:23
|
45 | Letters
| ^^^^^^^ not found in ` popup`
Some errors have detailed explanations: E0412, E0573.
For more information about an error, try `rustc --explain E0412`.
error: could not compile `macro_example` due to 3 previous errors
Composing path segments like this does not have an obvious solution. I initially tried the paste crate which lets you compose identifiers in this manner:
Tried paste!
}
}
But paste!
does not work for composing paths like that.
error: expected identifier after `:`
-/main.rs:44:22
|
44 | ImageUpload,
-/main.rs:45:11
|
45 | App,
-/main.rs:46:15
|
46 | Letters
| ^
error: could not compile `macro_example` due to 6 previous errors
The Solution
If we look at what a path is, it’s just identifiers (ident
) separated by double-colon ::
:
some_module
You can represent the above using the following macro specifier:
$::*
$( ... )*
denotes a repetition$( ... )SEPARATOR*
denotes a repetition, but we have to insert theSEPARATOR
in-between each repetition.
In path::to::some_module
, it would parse as follows:
$segment // `path`
:: // `SEPARATOR`
$segment // `to`
:: // `SEPARATOR`
$segment // `some_module`
Then, using $($segment)::*
in a similar fashion expands this repetition to the path:
path // `$segment`
:: // `SEPARATOR`
to // `$segment`
:: // `SEPARATOR`
some_module // `$segment`
And what’s interesting is that we can compose this with another double-colon ::
followed by an identifier (ident
) like this:
$::* some_identifier
The above (with path::to::some_module
as $($segment)::*
) will expand to this:
some_identifier
I have looked for a long time and haven’t found anyone talk about this. So the correct version of the macro I tried to make is this:
}
}