#macro-rules #procedural #inline #eval #macro

no-std crabtime

A powerful yet easy-to-use Rust macro that generates code by evaluating inline Rust logic at compile time

4 releases (stable)

Uses new Rust 2024

new 1.1.1 Mar 21, 2025
1.1.0 Mar 19, 2025
1.0.0 Mar 19, 2025
0.1.0 Mar 7, 2025

#410 in Rust patterns

Download history 113/week @ 2025-03-04 14/week @ 2025-03-11 543/week @ 2025-03-18

670 downloads per month
Used in smithy-cargo-macros

MIT/Apache

42KB
208 lines

banner

Click here to read the docs!


lib.rs:

🦀 Crabtime

banner

Crabtime offers a novel way to write Rust macros, inspired by Zig's comptime. It provides even more flexibility and power than procedural macros, while remaining easier and more natural to read and write than macro_rules!.



🆚 Comparison to Proc Macros and macro_rules!

Below is a comparison of key aspects of Rust's macro systems:

Input/Output
Crabtime Proc Macro macro_rules!
Input as Token Stream
Input as Macro Fragments
Input as Rust Code (String)
Output as Token Stream
Output as Macro Fragments Template
Output as Rust Code (String)
Functionalities
Crabtime Proc Macro macro_rules!
Advanced transformations
Space-aware interpolation
Can define fn-like macros
Can define derive macros 🚧
Can define attribute macros 🚧
Reusable across modules and crates
Comfort of life
Crabtime Proc Macro macro_rules!
Full expansion in IDEs[^supported_ides]
Full type hints in IDEs[^supported_ides]
Works with rustfmt
Easy to define (inline, the same crate)
Easy to read ⚠️
Hygienic


🎯 One-shot evaluation

The simplest and least exciting use of Crabtime is straightforward compile-time code evaluation. To evaluate an expression and paste its output as new code, just use crabtime::eval, as shown below:

const MY_NUM: usize = crabtime::eval! {
    (std::f32::consts::PI.sqrt() * 10.0).round() as usize
};


🤩 Function-like macros

Use the crabtime::function attribute to define a new function-like macro. Crabtime will remove the annotated function and replace it with a macro definition of the same name. You can then call the macro to compile and execute the function at build time, and use its output as the generated Rust code. You can also use the standard #[macro_export] attribute to export your macro. Let's start with a simple example, and let's refine it down the line. Let's generate the following Rust code:

enum Position1 { X }
enum Position2 { X, Y }
enum Position3 { X, Y, Z }
enum Position4 { X, Y, Z, W }

We can do it in this, not very exciting way:

// Replaces the function definition with a `gen_positions1!` macro.
#[crabtime::function]
#[macro_export] // <- This is how you export it!
fn gen_positions1() -> &str {
    "
    enum Position1 { X }
    enum Position2 { X, Y }
    enum Position3 { X, Y, Z }
    enum Position4 { X, Y, Z, W }
    "
}

// Compiles and evaluates the gen_positions1 function at build-time and
// uses its output as the new code source.
gen_positions1!();


🤩 Attribute and derive macros

Currently, generating attribute macros and derive macros is not supported, but there are several ways to achieve it. If you want to help, ping us on GitHub.



📤 Output

There are several ways to generate the output code from a Crabtime macro. We recommend you to use either crabtime::output! or crabtime::quote! macros, as they allow for the most concise, easy-to-understand, and maintainable implementations. Supported input types are described later, for now just ignore them.


Generating output by using crabtime::output!

The simplest and most recommended way to generate macro output is by using the crabtime::output! macro. It allows for space-aware variable interpolation. It's like the format! macro, but with inversed rules regarding curly braces – it preserves single braces and uses double braces for interpolation. Please note that it preserves spaces, so Position {{ix}} and Position{{ix}} mean different things, and the latter will generate Position1, Position2, etc.

#[crabtime::function]
fn gen_positions2(components: Vec<String>) {
    for dim in 1 ..= components.len() {
        let cons = components[0..dim].join(",");
        crabtime::output! {
            enum Position{{dim}} {
                {{cons}}
            }
        }
    }
}
gen_positions2!(["X", "Y", "Z", "W"]);

Generating output by using crabtime::quote!

The crabtime::quote! macro is just like crabtime::output!, but instead of outputting the code immediately, it returns it (as a String), so you can store it in a variable and re-use it across different subsequent calls to crabtime::quote! or crabtime::output!.

#[crabtime::function]
fn gen_positions3(components: Vec<String>) -> String {
    let structs = (1 ..= components.len()).map(|dim| {
        let cons = components[0..dim].join(",");
        crabtime::quote! {
            enum Position{{dim}} {
                {{cons}}
            }
        }
    }).collect::<Vec<String>>();
    structs.join("\n")
}
gen_positions3!(["X", "Y", "Z", "W"]);

Generating output by returning a string or number

You can simply return a string or number from the function. It will be used as the generated macro code.

#[crabtime::function]
fn gen_positions4(components: Vec<String>) -> String {
    (1 ..= components.len()).map(|dim| {
        let cons = components[0..dim].join(",");
        format!("enum Position{dim} {{ {cons} }}")
    }).collect::<Vec<_>>().join("\n")
}
gen_positions4!(["X", "Y", "Z", "W"]);

Generating output by using crabtime::output_str!

Alternatively, you can use the crabtime::output_str! macro to immediately write strings to the code output buffer:

#[crabtime::function]
fn gen_positions5(components: Vec<String>) {
    for dim in 1 ..= components.len() {
        let cons = components[0..dim].join(",");
        crabtime::output_str!("enum Position{dim} {{ {cons} }}")
    }
}
gen_positions5!(["X", "Y", "Z", "W"]);

Generating output by returning a TokenStream

Finally, you can output TokenStream from the macro. Please note that for brevity the below example uses inline dependency injection, which is described later. In real code you should use your Cargo.toml's [build-dependencies] section to include the necessary dependencies instead.

#[crabtime::function]
fn gen_positions6() -> proc_macro2::TokenStream {
    // Inline dependencies used for brevity.
    // You should use [build-dependencies] section in your Cargo.toml instead.
    #![dependency(proc-macro2 = "1")]
    #![dependency(syn = "2")]
    #![dependency(quote = "1")]
    use proc_macro2::Span;
    use quote::quote;

    let components = ["X", "Y", "Z", "W"];
    let defs = (1 ..= components.len()).map(|dim| {
        let cons = components[0..dim].iter().map(|t|
            syn::Ident::new(t, Span::call_site())
        );
        let ident = syn::Ident::new(&format!("Position{dim}"), Span::call_site());
        quote! {
            enum #ident {
                #(#cons),*
            }
        }
    }).collect::<Vec<_>>();
    quote! {
        #(#defs)*
    }
}
gen_positions6!();


📥 Input

Similarly to generating output, there are several ways to parametrize macros and provide them with input at their call site. We recommend you use the pattern parametrization, as it's the simplest and easiest to maintain.


Input by using supported arguments

Currently, you can use any combination of the following types as arguments to your macro and they will be automatically translated to patterns: Vec<...>, &str, String, and numbers. If the expected argument is a string, you can pass either a string literal or an identifier, which will automatically be converted to a string.

#[crabtime::function]
fn gen_positions7(name: String, components: Vec<String>) {
    for dim in 1 ..= components.len() {
        let cons = components[0..dim].join(",");
        crabtime::output! {
            enum {{name}}{{dim}} {
                {{cons}}
            }
        }
    }
}
gen_positions7!(Position, ["X", "Y", "Z", "W"]);
gen_positions7!(Color, ["R", "G", "B"]);

Input by using patterns

In case you want even more control, you can use the same patterns as macro_rules! by using a special pattern! macro, and you can expand any pattern using the expand! macro:

💡 Please note that the expand! macro simply passes its input along. It is used only to make the code within the function a valid Rust code block. Thus, you do not need to use it if you want to expand variables within other macros, like stringify!.
// Please note that we need to type the pattern argument as `_` to make the
// code a valid Rust code.
#[crabtime::function]
fn gen_positions8(pattern!($name:ident, $components:tt): _) {
    let components = expand!($components);
    for dim in 1 ..= components.len() {
        let cons = components[0..dim].join(",");
        // We don't need to use `expand!` here.
        let name = stringify!($name);
        crabtime::output! {
            enum {{name}}{{dim}} {
                {{cons}}
            }
        }
    }
}
gen_positions8!(Position, ["X", "Y", "Z", "W"]);
gen_positions8!(Color, ["R", "G", "B"]);

Input by using TokenStream

Alternatively, you can consume the provided input as a TokenStream:

#[crabtime::function]
fn gen_positions9(name: TokenStream) {
    #![dependency(proc-macro2 = "1")]
    let components = ["X", "Y", "Z", "W"];
    let name_str = name.to_string();
    for dim in 1 ..= components.len() {
        let cons = components[0..dim].join(",");
        crabtime::output! {
            enum {{name_str}}{{dim}} {
                {{cons}}
            }
        }
    }
}
gen_positions9!(Position);


🚀 Performance

The lifecycle of a Crabtime macro is similar to that of a procedural macro. It is compiled as a separate crate and then invoked to transform input tokens into output tokens. On the unstable Rust channel, Crabtime and procedural macros have the same performance. On the stable channel, Crabtime requires slightly more time than a procedural macro after you change your macro definition. In other words, Crabtime’s performance is similar to procedural macros. It has higher compilation overhead than macro_rules! but processes tokens and complex transformations faster.

Proc Macro
Crabtime
macro_rules!
First evaluation (incl. compilation) ⚠️ Relatively slow ⚠️ Relatively slow ✅ Fast
Next evaluation (on call-site change) ✅ Fast ✅ Fast on nightly
⚠️ Relatively slow on stable
❌ Slow for complex transformations
Cost after changing module code without changing macro-call site code ✅ Zero ✅ Zero ✅ Zero

Cache

When a Crabtime macro is called, it creates a new Rust project, compiles it, evaluates it, and interprets the results as the generated Rust code. When you call the macro again (for example, after changing the macro’s parameters or calling the same macro in a different place), Crabtime can reuse the previously generated project. This feature is called “caching.” It is enabled by default on the nightly channel and can be enabled on the stable channel by providing a module attribute, for example:

#[crabtime::function(cache_key=my_key)]
#[module(my_crate::my_module)]
fn my_macro() {
    // ...
}

The cache is always written to <project_dir>/target/debug/build/crabtime/<module>/<macro_name>. The defaults are presented below:

Rust Unstable Rust Stable
Cache enabled ❌ by default, ✅ when module used.
module default path to def-site module none

Please note that caching will be automatically enabled on the stable channel as soon as the proc_macro_span feature is stabilized. That feature allows Crabtime to read the path of the file where the macro was used, so it can build a unique cache key.


Performance Stats

Crabtime also generates runtime and performance statistics to help you understand how much time was spent evaluating your macros, where projects were generated, and which options were used. If you expand any usage of #[crabtime::function] (for example, in your IDE), you will see compilation stats like:

# Compilation Stats
Start: 13:17:09 (825)
Duration: 0.35 s
Cached: true
Output Dir: /Users/crabtime_user/my_project/target/debug/build/crabtime/macro_path
Macro Options: MacroOptions {
    cache: true,
    content_base_name: false,
}

Please note that you can be presented with the Cached: true result even after the first macro evaluation if your IDE or build system evaluated it earlier in the background.



🪲 Logging & Debugging

There are several ways to log from your Crabtime macros. Because proc_macro::Diagnostic is currently a nightly-only feature, Crabtime prints nicer warnings and errors if you are using nightly Rust channel. They look just like warnings and errors from the Rust compiler. Otherwise, your warnings and errors will be printed to the console with a [WARNING] or [ERROR] prefix.

Method Behavior on stable Behavior on nightly
println! Debug log in console Debug log in console
crabtime::warning! Debug log in console Warning in console
crabtime::error! Debug log in console Error in console

Stdout Protocol

Please note that Crabtime uses stdout for all communication between the code generation process and the host process. Depending on the prefix of each stdout line, it is interpreted according to the following table. In particular, instead of using the methods shown above, you can generate code from your macros by printing it to stdout (like println!("[OUTPUT] struct T {}")), but it's highly discouraged.

Prefix Meaning
(none) Debug log message (informational output).
[OUTPUT] A line of generated Rust code to be included in the final macro output.
[WARNING] A compilation warning.
[ERROR] A compilation error.

Stdout Protocol Utilities

Although you are not supposed to generate the Stdout Protocol messages manually, we believe that it is better to expose the underlying utilities so that in rare cases, you can use them to reduce the risk of malformed output. These functions allow you to transform multi-line strings by adding the appropriate prefixes:

mod crabtime {
    fn prefix_lines_with(prefix: &str, input: &str) -> String {
        // Adds the given prefix to each line of the input string.
        # panic!()
    }

    fn prefix_lines_with_output(input: &str) -> String {
        // Adds `[OUTPUT]` to each line of the input string.
        # panic!()
    }

    fn prefix_lines_with_warning(input: &str) -> String {
        // Adds `[WARNING]` to each line of the input string.
        # panic!()
    }

    fn prefix_lines_with_error(input: &str) -> String {
        // Adds `[ERROR]` to each line of the input string.
        # panic!()
    }
}

These macros allow you to directly print prefixed lines to stdout, following the protocol:

mod crabtime {
    macro_rules! output_str {
        // Outputs code by printing a line prefixed with `[OUTPUT]`.
        # () => {};
    }

    macro_rules! warning {
        // On the nightly channel prints a compilation warning.
        // On the stable channel prints a log prefixed with `[WARNING]`.
        # () => {};
    }

    macro_rules! error {
        // On the nightly channel prints a compilation error.
        // On the stable channel prints a log prefixed with `[ERROR]`.
        # () => {};
    }
}


⚙️ Macro Cargo Configuration

💡 On the Rust unstable channel, all configuration is automatically gathered from your Cargo.toml. It includes build-dependencies and code lints, including those defined in your workspace.

Every Crabtime macro is a separate Cargo project with its own configuration and dependencies. If you use nightly, Crabtime automatically uses your Cargo.toml configuration. On stable, due to lack of proc_macro_span stabilization, Crabtime cannot discover your Cargo.toml automatically. You must provide cargo configuration in your macro blocks, for example:

#[crabtime::function]
fn my_macro() {
    // Do this only on Rust stable channel. On the unstable channel
    // use your Cargo.toml's [build-dependencies] section instead.
    #![edition(2024)]
    #![resolver(3)]
    #![dependency(anyhow = "1.0")]

    type Result<T> = anyhow::Result<T>;
    // ...
}

Crabtime recognizes these Cargo configuration attributes. The attributes below override any configuration discovered in your Cargo.toml, even on nightly:


Supported Cargo Configuration Attributes
Attribute Default
#![edition(...)] 2024
#![resolver(...)] 3
#![dependency(...)] []


📚 Attributes

You can provide any set of global attributes (#![...]) on top of your Crabtime macro definition for them to be applied to the given generated Crabtime crate.



🗺️ Paths

Crabtime macros provide access to several path variables, allowing you to traverse your project's folder structure during macro evaluation. All paths are accessible within the crabtime:: namespace.

Path Availability Description
WORKSPACE_PATH Stable & Nightly Path to the root of your project. This is where the top-most Cargo.toml resides, whether it's a single-crate project or a Cargo workspace.
CRATE_CONFIG_PATH Nightly only Path to the Cargo.toml file of the current crate.
CALL_SITE_FILE_PATH Nightly only Path to the file where the macro was invoked.
#[crabtime::function]
fn check_paths() {
    println!("Workspace path: {}", crabtime::WORKSPACE_PATH);
}
check_paths!();


📖 How It Works Under The Hood

The content of a function annotated with crabtime::function is pasted into the main function of a temporary Rust project. This project is created, compiled, executed, and (if caching is disabled) removed at build time, and its stdout becomes the generated Rust code. The generated main function looks something like this:

const SOURCE_CODE: &str = "..."; // Your code as a string.

mod crabtime {
    // Various utils described in this documentation.
    # pub fn push_as_str(str: &mut String, result: &()) {}
    # pub fn prefix_lines_with_output(input: &str) -> String { String::new() }
}

fn main() {
    let mut __output_buffer__ = String::new();
    let result = {
        // Your code.
    };
    crabtime::push_as_str(&mut __output_buffer__, &result);
    println!("{}", crabtime::prefix_lines_with_output(&__output_buffer__));
}

The output! macro is essentially a shortcut for writing to output buffer using format!, so this:

#[crabtime::function]
fn my_macro_expansion1(components: Vec<String>) {
    for dim in 1 ..= components.len() {
        let cons = components[0..dim].join(",");
        crabtime::output! {
            enum Position{{dim}} {
                {{cons}}
            }
        }
    }
}
my_macro_expansion1!(["X", "Y", "Z", "W"]);

Is equivalent to:

#[crabtime::function]
fn my_macro_expansion2(pattern!([$($components_arg:expr),*$(,)?]): _) {
    let components: Vec<String> = expand!(
        [$(crabtime::stringify_if_needed!($components_arg).to_string()),*]
    ).into_iter().collect();
    for dim in 1 ..= components.len() {
        let cons = components[0..dim].join(",");
        crabtime::output_str! {"
            enum Position{dim} {{
                {cons}
            }}
        "}
    }
}
my_macro_expansion2!(["X", "Y", "Z", "W"]);

And that, in turn, is just the same as:

#[crabtime::function]
fn my_macro_expansion3() {
    let components = ["X", "Y", "Z", "W"];
    for dim in 1 ..= components.len() {
        let cons = components[0..dim].join(",");
        __output_buffer__.push_str(
            &format!("enum Position{dim} {{ {cons} }}\n")
        );
    }
}
my_macro_expansion3!();

Which, ultimately, is equivalent to:

#[crabtime::function]
fn my_macro_expansion4() {
    let components = ["X", "Y", "Z", "W"];
    for dim in 1 ..= components.len() {
        let cons = components[0..dim].join(",");
        println!("[OUTPUT] enum Position{dim} {{");
        println!("[OUTPUT]     {cons}");
        println!("[OUTPUT] }}");
    }
}
my_macro_expansion4!();


⚠️ Corner Cases

There are a few things you should be aware of when using Crabtime:

  • Caching is associated with the current file path. It means that if in a single file you have multiple Crabtime macros of the same name (e.g. by putting them in different modules within a single file), they will use the same Rust project under the hood, which effectively breaks the whole purpose of caching.
  • You can't use Crabtime functions to generate consts. Instead, use Crabtime::eval! as shown above. This is because when expanding constants, macros need to produce an additional pair of { and } around the expanded tokens. If anyone knows how to improve this, please contact us.
  • Error spans from the generated code are not mapped to your source code. It means that you will still get nice, colored error messages, but the line/column numbers will be pointing to the generated file, not to your source file. This is an area for improvement, and I'd be happy to accept a PR that fixes this.
  • Crabtime::eval! does not use caching, as there is no name we can associate the cache with.


⚠️ Troubleshooting

⚠️ Note: Rust IDEs differ in how they handle macro expansion. This macro is tuned for rustc and RustRover’s expansion engines.

If your IDE struggles to correctly expand crabtime::output!, you can switch to the crabtime::output_str! syntax described above. If you encounter this, please open an issue to let us know!

[^supported_ides]: This code was thoroughly tested in rustc, the IntelliJ/RustRover Rust expansion engine, and Rust Analyzer (VS Code, etc.).

Dependencies

~0.5–1MB
~24K SLoC