matrixrustmacros

Rust Macro count repetitions of nested match


To declare a matrix, I'm using a vector of length w*h, and implementing the Index trait, which does the necessary maths to find the correct value in the matrix. Now I'd like to define a macro which makes declaring and using matrices easier.

Below is the solution I came up with:

macro_rules! _count {
    () => (0usize);
    ( $x:tt $($xs:tt)* ) => (1usize + _count!($($xs)*));
}

macro_rules! _count_semicolon {
    ($([$($n:tt),+]);+) => {};
    ($($($n:tt),+);+) => { _count!(_count_semicolon!($([$($n),+]);+)) - 1 }
}

macro_rules! matrix {
    ($($($n:expr) +);+) => {{
        use std::rc::Rc;
        use crate::Matrix;
        
        let len = _count!($($($n)+)+);
        let lines = _count_semicolon!($($($n),+);+);
        
        Matrix {
            width: len / lines,
            height: lines,
            data: Rc::new(vec![$($($n as f64),+),+].into_boxed_slice()),
            transpose: false
        }
    }};
}

The intent is simple: Declare a matrix by writing a grid of numbers separated by whitespaces, and rows by semicolons between braces. The height is the number of semicolons + 1, and the width is the total number of elements (ie the length of the vector) / height. The total length is determined by placing all elements as arguments into the count macro. A similar approach is used to count the semicolons, except the elements delimited by semicolons are placed in square brackets, to be treated as a single argument. Since this doesn't work, I use another macro to rewrite this into an empty argument, and pass it to the count macro instead.

So far my implementation seems to work for the following cases:

matrix! [
  1 2 3 4 5;
  6 7 8 9 10
];
matrix! [
  1 2
  3 4
];
matrix! [
  5 6
  7 8
];

However not for the following. The widths and heights are 1 and 2, respectively. It correctly counts the length (3), but yields a height of 2, and since 3/2.floor() == 1,

matrix![ 
  1 2 3
];
matrix![ 
  4 5 6
];

An explanation and/or fix would be highly appreciated

Edit:

I received two very good answers while I was asleep. Both of which do exactly as advertised, and answer the question perfectly, however, since @BallpointBen's answer is just slightly more descriptive, will be the one I accept.

Thank you both for your assistance <3


Solution

  • The issue is that the $x:tt in ( $x:tt $($xs:tt)* ) in _count! matches any token tree — including _count_semicolon!! In fact, _count treats it as two token trees, _count_semicolon and !. The expansion of _count_semicolon! happens after, not before, _count reads its tokens, and _count! simply... counts the (two) tokens in _count_semicolon ! instead of leaving them in place to expand. (You can see this if you compile with Rust nightly and #![feature(trace_macros)].)

    Instead, the following works:

    macro_rules! _count {
        () => (0usize);
        ( $x:tt $($xs:tt)* ) => (1usize + _count!($($xs)*));
    }
    
    macro_rules! _count_semicolon {
        ($($($n:expr),+);+) => { _count_semicolon_bracketed!($([$($n),+]);+) }
    }
    
    macro_rules! _count_semicolon_bracketed {
         ($($n:tt);+) => {_count!( $($n)+ )};
    }
    

    We've split _count_semicolon into two macros (to avoid recursion) and have gotten rid of the -1.

    Now, we have the following expansions:

    matrix!();
    // ... becomes ...
    {
        use std::rc::Rc;
    
        use crate::Matrix;
        let len = 0usize;
        let lines = 0usize;
        Matrix {
            width: len / lines,
            height: lines,
            data: Rc::new(($crate::vec::Vec::new()).into_boxed_slice()),
            transpose: false,
        }
    }
    
    matrix!(1 2 3);
    // ... becomes ...
    {
        use std::rc::Rc;
    
        use crate::Matrix;
        let len = 1usize + 1usize + 1usize + 0usize;
        let lines = 1usize + 0usize;
        Matrix {
            width: len / lines,
            height: lines,
            data: Rc::new(
                (<[_]>::into_vec(
                    #[rustc_box]
                    $crate::boxed::Box::new([(1 as f64), (2 as f64), (3 as f64)]),
                ))
                .into_boxed_slice(),
            ),
            transpose: false,
        }
    }
    
    matrix!(1 2 3; 4 5 6);
    // ... becomes ...
    {
        use std::rc::Rc;
    
        use crate::Matrix;
        let len = 1usize + 1usize + 1usize + 1usize + 1usize + 1usize + 0usize;
        let lines = 1usize + 1usize + 0usize;
        Matrix {
            width: len / lines,
            height: lines,
            data: Rc::new(
                (<[_]>::into_vec(
                    #[rustc_box]
                    $crate::boxed::Box::new([
                        (1 as f64),
                        (2 as f64),
                        (3 as f64),
                        (4 as f64),
                        (5 as f64),
                        (6 as f64),
                    ]),
                ))
                .into_boxed_slice(),
            ),
            transpose: false,
        }
    }