Is there a way to count with macros?
Asked Answered
R

4

18

I want to create a macro that prints "Hello" a specified number of times. It's used like:

many_greetings!(3);  // expands to three `println!("Hello");` statements

The naive way to create that macro is:

macro_rules! many_greetings {
    ($times:expr) => {{
        println!("Hello");
        many_greetings!($times - 1);
    }};
    (0) => ();
}

However, this doesn't work because the compiler does not evaluate expressions; $times - 1 isn't calculated, but fed as a new expression into the macro.

Recency answered 17/11, 2015 at 7:58 Comment(5)
The closest you can get is with recursion, taking advantage of the fact matches are reevaluated: is.gd/3QfTr9 It is very ugly, though.Powered
Would be good if you could state why using a for loop in the macro isn't a good solution (since it seems like an obvious answer).Wager
@Wager This is an artificial example. I am not really interested in this specific use case; the question is about the general case of "counting with macros".Recency
OK, in that case it makes it hard to know what is a good answer, since in the example you give its quite obvious you would use iteration. Note that this questions title is quite similar to another question: stackoverflow.com/questions/30152800 (what I was searching for and why I stumbled on this page). AFAICS it's about macro expansion, not counting.Wager
@Wager building an array initializer of non-Copy elements (for example, String) or for n > 32 elements of any type is a use case for this for which for is unsuitable. For example: static FOO: [AtomicUsize; 100] = arr_init![AtomicUsize::new(0); 100];. The arr_init! macro should emit [AtomicUsize::new(0), AtomicUsize::new(0), ... , AtomicUsize::new(0) ] (n = 100 elements) at compile-time.Norbert
C
9

While the ordinary macro system does not enable you to repeat the macro expansion many times, there is no problem with using a for loop in the macro:

macro_rules! many_greetings {
    ($times:expr) => {{
        for _ in 0..$times {
            println!("Hello");
        }
    }};
}

If you really need to repeat the macro, you have to look into procedural macros/compiler plugins (which as of 1.4 are unstable, and a bit harder to write).

Edit: There are probably better ways of implementing this, but I've spent long enough on this for today, so here goes. repeat!, a macro that actually duplicates a block of code a number of times:

main.rs

#![feature(plugin)]
#![plugin(repeat)]

fn main() {
    let mut n = 0;
    repeat!{ 4 {
        println!("hello {}", n);
        n += 1;
    }};
}

lib.rs

#![feature(plugin_registrar, rustc_private)]

extern crate syntax;
extern crate rustc;

use syntax::codemap::Span;
use syntax::ast::TokenTree;
use syntax::ext::base::{ExtCtxt, MacResult, MacEager, DummyResult};
use rustc::plugin::Registry;
use syntax::util::small_vector::SmallVector;
use syntax::ast::Lit_;
use std::error::Error;

fn expand_repeat(cx: &mut ExtCtxt, sp: Span, tts: &[TokenTree]) -> Box<MacResult + 'static> {
    let mut parser = cx.new_parser_from_tts(tts);
    let times = match parser.parse_lit() {
        Ok(lit) => match lit.node {
            Lit_::LitInt(n, _) => n,
            _ => {
                cx.span_err(lit.span, "Expected literal integer");
                return DummyResult::any(sp);
            }
        },
        Err(e) => {
            cx.span_err(sp, e.description());
            return DummyResult::any(sp);
        }
    };
    let res = parser.parse_block();

    match res {
        Ok(block) => {
            let mut stmts = SmallVector::many(block.stmts.clone());
            for _ in 1..times {
                let rep_stmts = SmallVector::many(block.stmts.clone());
                stmts.push_all(rep_stmts);
            }
            MacEager::stmts(stmts)
        }
        Err(e) => {
            cx.span_err(sp, e.description());
            DummyResult::any(sp)
        }
    }
}

#[plugin_registrar]
pub fn plugin_registrar(reg: &mut Registry) {
    reg.register_macro("repeat", expand_repeat);
}

added to Cargo.toml

[lib]
name = "repeat"
plugin = true

Note that if we really don't want to do looping, but expanding at compile-time, we have to do things like requiring literal numbers. After all, we are not able to evaluate variables and function calls that reference other parts of the program at compile time.

Commeasure answered 17/11, 2015 at 18:30 Comment(3)
Sure, a loop always works, but then it doesn't need to be a macro. My example was stupid anyway. Could you post some code, how one would do it with compiler plugins? If you have time for that... that would be awesome :)Recency
I've added an example of a procedural macro which can repeat arbitrary code a number of times. I can't think of any good reason to use this exact macro, but as you can see, it is possible.Commeasure
Is this answer still valid? Might Rust's recursive macro be able to handle this?Wager
R
8

As the other answers already said: no, you can't count like this with declarative macros (macro_rules!).


But you can implement the many_greetings! example as a procedural macro. procedural macros were stabilized a while ago, so the definition works on stable. However, we can't yet expand macros into statements on stable -- that's what the #![feature(proc_macro_hygiene)] is for.

This looks like a lot of code, but most code is just error handling, so it's not that complicated!

examples/main.rs

#![feature(proc_macro_hygiene)]

use count_proc_macro::many_greetings;

fn main() {
    many_greetings!(3);
}

Cargo.toml

[package]
name = "count-proc-macro"
version = "0.1.0"
authors = ["me"]
edition = "2018"

[lib]
proc-macro = true

[dependencies]
quote = "0.6"

src/lib.rs

extern crate proc_macro;

use std::iter;
use proc_macro::{Span, TokenStream, TokenTree};
use quote::{quote, quote_spanned};


/// Expands into multiple `println!("Hello");` statements. E.g.
/// `many_greetings!(3);` will expand into three `println`s.
#[proc_macro]
pub fn many_greetings(input: TokenStream) -> TokenStream {
    let tokens = input.into_iter().collect::<Vec<_>>();

    // Make sure at least one token is provided.
    if tokens.is_empty() {
        return err(Span::call_site(), "expected integer, found no input");
    }

    // Make sure we don't have too many tokens.
    if tokens.len() > 1 {
        return err(tokens[1].span(), "unexpected second token");
    }

    // Get the number from our token.
    let count = match &tokens[0] {
        TokenTree::Literal(lit) => {
            // Unfortunately, `Literal` doesn't have nice methods right now, so
            // the easiest way for us to get an integer out of it is to convert
            // it into string and parse it again.
            if let Ok(count) = lit.to_string().parse::<usize>() {
                count
            } else {
                let msg = format!("expected unsigned integer, found `{}`", lit);
                return err(lit.span(), msg);
            }
        }
        other => {
            let msg = format!("expected integer literal, found `{}`", other);
            return err(other.span(), msg);
        }
    };

    // Return multiple `println` statements.
    iter::repeat(quote! { println!("Hello"); })
        .map(TokenStream::from)
        .take(count)
        .collect()
}

/// Report an error with the given `span` and message.
fn err(span: Span, msg: impl Into<String>) -> TokenStream {
    let msg = msg.into();
    quote_spanned!(span.into()=> {
        compile_error!(#msg);
    }).into()
}

Running cargo run --example main prints three "Hello"s.

Recency answered 24/1, 2019 at 16:19 Comment(0)
C
5

For those looking for a way to do this, there is also the seq_macro crate.

It is fairly easy to use and works out of the box with stable Rust.

use seq_macro::seq;

macro_rules! many_greetings {
    ($times:literal) => {
        seq!{ N in 0..$times {
            println!("Hello");
        }}
    };
}

fn main() {
    many_greetings!(3);
    many_greetings!(12);
}
Chatterjee answered 15/7, 2021 at 12:57 Comment(2)
What if the code requires const values? in this case would it be possible to obtain N as a const instead of a regular variable?Puce
@purple_turtle I'm pretty sure that if you use N in the scope passed to seq!, it will be expanded as a literal, which is a const value.Chatterjee
G
4

As far as I know, no. The macro language is based on pattern matching and variable substitution, and only evaluates macros.

Now, you can implement counting with evaluation: it just is boring... see the playpen

macro_rules! many_greetings {
    (3) => {{
        println!("Hello");
        many_greetings!(2);
    }};
    (2) => {{
        println!("Hello");
        many_greetings!(1);
    }};
    (1) => {{
        println!("Hello");
        many_greetings!(0);
    }};
    (0) => ();
}

Based on this, I am pretty sure one could invent a set of macro to "count" and invoke various operations at each step (with the count).

Garver answered 17/11, 2015 at 8:22 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.