Token Goblin — munches your tokens, forge out charms

token-goblin is a proc-macro library for defining inline proc-macro, directly inside your crate, without separate proc-macro target.
It is inspired by crates like crabtime and inline-proc, but aims to provide a more polished, flexible, and ergonomic API.
Getting started
Add token-goblin to your crate:
[]
= "0.1.0"
Then try:
This generates a new macro, or charm, named foo!:
foo!; // will expand to `bar baz`
In other words, #[munch] turns the function into a new macro.
Note: beacause token-goblin::munch are macros that generate macros, charm term would be used for generated macros in docs for clarity (and a little bit of lore).
Usecases
A well-fed goblin is a productive goblin. Here is what it does once it has chewed through your tokens.
Simple string based API like in crabtime
Some users don't want to mess with proc-macro API, they found it foreign and confusing.
crabtime showed another way to write macro - a simple string based API, that allows to use String and Vec<String> dirrectly as input of macro.
Example adopted from crabtime docs:
generate_enums!;
which will expand to:
// ... up to
Note: while it is inspired by crabtime, and token-goblin adopted this approach, instead of hardcoding String, Vec<String> type handling, input is expected to implement syn::parse::Parse trait.
So CommaSeparated<Token> is just two wrappers in token-goblin-runtime crate, that provides required syn::parse::Parse implementation.
Inline proc-macro
String based API is simple, but it's looses span information, and reduces IDE/diagnostics quality.
If you don't want to lose span informations, but it stills annoys you, that to implement a simple
proc-macro you need to create a separate crate.
token-goblin provides a classic proc-macro2 API as well:
And even better, it's support syn based types as input params:
Or, you can define multiple charms in one module, and extend input param
generate_enums!;
generate_structs!;
Probes, and evals
Sometimes the goblin just sits by the fire and counts things in its head, so you don't have to at runtime.
The other common cases for macros is to precomupte some data.
crabtime provides eval macro for this purpose.
But with token-goblin, you can implement it by yourself:
// prints:
// x: 18
Note: that any expression is embedded into charm as code, and cannot use external variables or call functions from your crate.
But you are not limited to simple expressions, in fact you can do any compile-time execution, like evaluating bytecodes, or even downloading something from the internet (using external states in macro is not recommended though).
e.g. from example_readme/examples/brainfuck.rs
let result = request_and_execute!;
println!;
// downloads: ">,[>,]<[.<]" program that reverses input
// prints:
// result: Hello World!
While executing brainfuck program, is pure-functional and therefore fits well to proc-macro purposes, using system API and requesting external data is clearly misuse. But the whole crate is experiments around proc-macro, so i think it's fun to
showcase it as well.
Note: While token-goblin itself doesn't cache the output of charms, the rust itself might cache them, especially when -Zcache-proc-macros is enabled.
Note: I there is a plan to implement wasm as feature that will enforce sandboxing of charms.
Reflection?
In computer science, reflective programming or reflection is the ability of a process to examine, introspect, and modify its own structure and behavior.
Reflection is a powerful feature, that allows to dynamically generate code, without knowing the exact types, by observing their structure.
The zig has comptime keyword, that allows to execute code at compile time, and observe the structure of the code.
In Rust we only have derives, They could replace some kind of reflections, e.g. by providing a way to generate some traits based on the struct fields. The one missing problem, is they not extendable.
E.g. the one who write struct Foo define the list of derived traits, and this list is not extendable.
So if you want to extend some type with your custom trait, you need to duplicate the Foo definition somewhere in some form. Reflection could solve this problem, by providing shape of the type, and then generate the trait based on it.
token-goblin have similar feature called Snif, that allows to collect information about some type, and pass it to another macro.
snif!;
generate_getters!() will receive input in format:
[Foo => { struct Foo { x : i32, } }] [ extra args]
and can generate code based on the information about types (in this example generate getters for Foo).
This example can be found in example_readme/examples/generate_getters.rs
More future-ful example that convert array of structs into struct of arrays can be found in token-goblin/examples/struct_of_arrays.rs
Multiple of small derives
Sometimes in big projects, you need to define multiple small derives, e.g. parsing/emitting/printing functional are distinct, and should be separated. Placing them in one "macro" crate might be not the better choice.
As opposite, token-goblin allows you to split the logic into multiple "macro" crates, and use them as dependencies.
snif!;
snif!;
This aproach is partially shown in token-goblin/examples/struct_of_arrays.rs. But i use it in real project, where i want to extend my type with additional meta-data, but want to keep derive logic separated, it looks like this:
snif!;
// ..
Do i need to rewrite declarative macros to proc-macro API?
While proc-macro API is more Rust-like and powerful, one might want to rewrite all declarative macros to proc-macro API. But working with TokenStream introduce some boilerplate, and some macros should be kept as declarative.
TTs muncher is a technique of writing recursive declarative macros, to parse complex input.
If we took example from link above (slightly modified):
It expects input in format:
let a = 10;
trace!
expands to something like:
and produces output into console:
x = 5
y = 50
x = 5
y = 50
Rewritting it as to proc-macro TokenStream API, will increase amount of code, and contain a lot of boilerplate:
Using syn with syn-derive might help with main logic:
It still requires defining TraceInput and TraceStmt structs, and syn::parse::Parse implementation for them.
See example_readme/examples/ttmunch-replace.rs for more details.
With token-goblin you don't need to chose, since it allows you to combine both approaches.
e.g. writing declarative macro as facade that will check patterns, and compute results in proc-macro API.
Uncommenting non ident expansions will fail at compile time:

There still old but good crate proc-macro-rules that allows you to use declarative macros patterns directly in proc-macro API.
Questions
Why it's named Token Goblin?
During thinkering about name, the ChatGPT 5.5 suggested this variant among others:

Which i found ridiculous, especially after i saw OpenAI post how their fighting "goblin" overuse by ChatGPT.
Also the idea of "some magical entity that eats tokens" looks like a good metaphor for macros.
Why entrypoint macros named munch and spit?
- Because
munchandspitfit well in "goblin" lore. - I think that
#[munch] fnwould be a good replacement for existing TTs muncher - technique of writing recursive declarative macros, to parse complex input.
Why not use crabtime or inline-proc?
They both looks notmaintained.
inline-proc uses syn 1.0 and no updates for ~5-6 years. And doesn't compile anymore on modern rust versions.
I have tried to contribute to crabtime https://github.com/wdanilo/crabtime/issues?q=author%3Avldm
But looks like author is not interested in maintaining it anymore. There still issues related to build-cache.
token-goblin combines all the features from both crabtime and inline-proc, like:
- using dylib to load proc-macro definition
- support for workspace dependencies
- support for attributes and derive macros helpers
- mod and fn entrypoints
and adds some extra:
- Emit ide helper for Rust-Analyzer completion ide-helper
- Allow span information to be preserved in output span_recovery
- Convert any panic to compile error panic
- Extendable interface for input and output ux
And planned more:
- Mapping panics/compile errors to
compile_error!should show any error in right source location. - Support for
wasmas feature that will enforce sandboxing ofcharms. - "reflection" like macro, to store tokens of some items, and use them as input to another macro.
Testing
Most of tests are implemented as regular integration tests, or doctests dirrectly in macro library. Fixtures represents tests that need to be run with different environment (currently only toolchain, or cargo config).
Fixtures can be run with:
Usage recommendations
Some hints and recommendation for using token-goblin in your projects.
IDE support
As with offline build, it is recommended to add token-goblin-runtime to [dev-dependencies] in your Cargo.toml. This will help rust-analyzer to find needed crate, and provide important semantic information for your macros.
Lazieness
token-goblin::munch provides lazy attribute, that allows enforcing lazieness of charm compilation.
By default all charms generated by token-goblin::munch are eager. This means, that charm is compiled during expansion of #[munch] attribute, the users of charm only use compiled dylib.
This setup is faster, since charm is compiled only once, and every users (expansion of charm itself) should skip compiliation step.
But, during development, flycheck could call cargo check on broken code, and spam with errors, in vscode + lens this can slowdown IDE performance.
Therefore, you can set lazy to true in #[token_goblin::munch] attributes.
// or #[token_goblin::munch(lazy = true)]
with this setup, #[munch] will not compile the charm, instead during foo expansion, compiliation will be triggered.
Note: that for same code, compiliation is only triggered once, since token-goblin caches the compiled dylib.
Debugging
Also, you can set TOKEN_GOBLIN_PRINT_LEVEL environment variable to 1-4 to enable debug prints.
Also, you can use println / eprintln / dbg and other macros to debug your charms.
Share cache or not?
By default all charms generated by token-goblin::munch will share same build-cache directory.
Sharing cache, enforce cargo to lock directory, and therefore one "slow" charm can slow down all compilition process.
To avoid this, you can set split_cache to true in #[token_goblin::munch] attributes.
// or #[token_goblin::munch(split_cache = true)]
This will generate force charm to use separate build-cache directory, and therefore will not be affected by other charms.
I recommend use split_cache for "big" charms only, that requires a lot of dependencies, or takes a lot of time to build. This is because charm with separated cache can be compiled in parallel with other charms.
Ceveats:
Even a helpful goblin has its quirks. Mind these before you let it loose.
- only
proc-macro2::fallbackis used (noproc-macroapi is available) in generated crates (which introduce some limitations) - mixed_site - is not supported by
proc_macro2::fallback - we use
dev-dependenciesforcharmsdependencies, which cannot be optional (by design of cargo resolver), so one small macro may increase compile time by rebuilding alldev-dependencies. namein#[munch] fn nameshould not be proc-macro generated, and is expected to have local source file.- on macos
dylibs(newly generated chamrs) loading may took more time than compile itself (~300ms). This is known issue related to XProtect. See the link above for workaround. - Rust-Analyzer will not analyze "optional" dependencies, and emit "unresolved external crate" errors on charms.
To disable IDE support for charms, use
no_ide_helperattribute#[token_goblin::munch(dependencies = [..],no_ide_helper)]
Offline build
Note: token-goblin-runtime is hardcoded dependency of generated crates, and might be not downloaded using cargo fetch or cargo vendor, in order to build offline, add token-goblin-runtime to [dev-dependencies] in your Cargo.toml.