Is there a way to count with macros?

前端 未结 3 2009
后悔当初
后悔当初 2020-11-28 15:03

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\");`          


        
相关标签:
3条回答
  • 2020-11-28 15:36

    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.

    0 讨论(0)
  • 2020-11-28 15:44

    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).

    0 讨论(0)
  • 2020-11-28 15:53

    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.

    0 讨论(0)
提交回复
热议问题