1

Let's assume, we want a bunch of constants, associating each square of a chess board with its coordinates, so we can use those constants in our Rust code.

One such definition could be:

#[allow(dead_code)]
const A1: (usize,usize) = (0, 0);

and there would be 64 of them.

Now, as a emacs user, I could generate the source code easily, for example with:

(dolist (col '(?A ?B ?C ?D ?E ?F ?G ?H))
  (dolist (row '(?1 ?2 ?3 ?4 ?5 ?6 ?7 ?8))
    (insert "#[allow(dead_code)]")
    (end-of-line)
    (newline-and-indent)
    (insert "const " col row ": (usize,usize) = ("
        (format "%d" (- col ?A))
        ", "
        (format "%d" (- row ?1))
        ");")
    (end-of-line)
    (newline-and-indent)))

With the drawback, that now my file just grew by 128 exceptionally boring lines.

In Common Lisp, I would solve this aspect, by defining myself a macro, for example:

(defmacro defconst-square-names ()
       (labels ((square-name (row col)
              (intern 
               (format nil "+~C~D+" 
                   (code-char (+ (char-code #\A) col))
                   (+ row 1))))
            (one-square (row col)
              `(defconstant ,(square-name row col)
             (cons ,row ,col))))
         `(eval-when (:compile-toplevel :load-toplevel :execute)
        ,@(loop
           for col below 8
           appending 
           (loop for row below 8
             collecting (one-square row col))))))
(defconst-square-names) ;; nicer packaging of those 64 boring lines...

Now, the question arises, of course,

  • if Rust macro system is able to accomplish this?
  • can someone show such a macro?
  • I read, you need to put such Rust macro into a separate crate or whatnot?!

UPDATE

@aedm pointed me with the comment about seq-macro crate to my first attempt to get it done. But unfortunately, from skimming over various Rust documents about macros, I still don't know how to define and call compile time functions from within such a macro:

fn const_name(index:usize) -> String {
  format!("{}{}",
      char::from_u32('A' as u32
             + (index as u32 % 8)).unwrap()
      , index / 8)
}

seq!(index in 0..64 {
  #[allow(dead_code)]
  const $crate::const_name(index) : (usize,usize) = ($(index / 8), $(index %8));
});

In my Common Lisp solution, I just defined local functions within the macro to get such things done. What is the Rust way?

BitTickler
  • 10,905
  • 5
  • 32
  • 53
  • What have you tried so far? Here's the rust reference on [macros](https://doc.rust-lang.org/reference/introduction.html) And a similar question on stackoverflow https://stackoverflow.com/questions/54552847/build-all-pairs-of-elements-quadratic-set-in-declarative-macro regarding: "I read, you need to put such Rust macro into a separate crate or whatnot?!" That's only for procedural macros. Declarative macros can be used in the crate in which they're declared – pantalohnes Mar 11 '22 at 19:02
  • [seq-macro](https://crates.io/crates/seq-macro) might be worth a look. – aedm Mar 11 '22 at 19:03
  • @pantalohnes The difference eluding me from what I read, I stopped trying after reading this extra crate stuff and deciding to ask here first, if it is even possible, instead of tinkering for a day or 2 to find out myself. The "Ask the expert first" approach... – BitTickler Mar 11 '22 at 19:03
  • @aedm This looks interesting. Is it possible to write that macro outside a function (their example uses it in `main()`)? – BitTickler Mar 11 '22 at 19:05
  • Yes, you can use it to define global constants or even structs with repeating fields. – aedm Mar 11 '22 at 19:07
  • Rust procedural macros are 100% able to accomplish this since they can transform the AST in any way they want (that makes sense to the compiler, anyway). – cdhowie Mar 11 '22 at 19:26
  • Maybe, instead of using `seq-macro`, a LISP guy like me should consider using [quote](https://github.com/dtolnay/quote)? – BitTickler Mar 11 '22 at 20:16
  • I don't think `seq!` actually evaluates calls to other functions. Trying to call `const_name()` from inside of the macro isn't going to work like that. – cdhowie Mar 11 '22 at 20:21
  • 2
    I think this may be an XY problem. Having 64 identifiers in the code seems like it honestly wouldn't be very useful. For example, you could probably _much_ easier write a macro like `square!(A1)` that evaluates to `(0, 0)`. – cdhowie Mar 11 '22 at 20:47

1 Answers1

1

Here's one way to do it only with macro_rules! ("macros by example") and the paste crate (to construct the identifiers). It's not especially elegant, but it is fairly short and doesn't require you to write a proc-macro crate.

It needs to be invoked with all of the involved symbols since macro_rules! can't do arithmetic. (Maybe seq-macro would help some with that, but I'm not familiar with it.)

use paste::paste;

macro_rules! board {
    // For each column, call column!() passing the details of that column
    // and all of the rows. (This can't be done in one macro because macro
    // repetition works like "zip", not like "cartesian product".)
    ( ($($cols:ident $colnos:literal),*), $rows:tt ) => {
        $( column!($cols, $colnos, $rows); )*
    };
}

/// Helper for board!
macro_rules! column {
    ( $col:ident, $colno:literal, ($($rows:literal),*) ) => {
        $(
            paste! {
                // [< >] are special brackets that tell the `paste!` macro to
                // paste together all the pieces appearing within them into
                // a single identifier.
                #[allow(dead_code)]
                const [< $col $rows >]: (usize, usize) = ($colno, $rows - 1);
            }
        )*
    };
}

board!((A 0, B 1, C 2, D 3, E 4, F 5, G 6, H 7), (1, 2, 3, 4, 5, 6, 7, 8));

fn main() {
    dbg!(A1, A8, H1, H8);
}
Kevin Reid
  • 37,492
  • 13
  • 80
  • 108
  • From this answer, I conclude, that in Rust, once you are in the "macro-domain", there is no way to call ordinary functions and as such, "compile time functions" are a hidden non-feature. – BitTickler Mar 15 '22 at 16:46
  • @BitTickler That's a simplification. `macro_rules!` cannot run any Rust code (it is only a special macro pattern-matching language), but procedural macros get to run ordinary Rust code (as long as it's part of the macro's defining crate or its dependencies, not the crate the macro is called in). Also, `const fn` enables compile-time function calls but those happen in a later stage of compilation, _after_ macro expansion and type checking. – Kevin Reid Mar 15 '22 at 18:09