Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improved stack! implementation, tests #1375

Open
wants to merge 35 commits into
base: dev
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
e0c917f
Add macro for concatenating matrices
birktj Feb 15, 2022
abc34c0
Replace DimUnify with DimEq::representative
birktj Feb 22, 2022
9efbb3b
Add some simple cat macro output generation tests
birktj Mar 3, 2022
ae37ee3
Fix formatting in cat macro code
birktj Feb 2, 2023
0dc7f5c
Add random prefix to cat macro output
birktj Feb 2, 2023
704ba34
Add simple quote_spanned for cat macro
birktj Feb 2, 2023
86a6ffe
Use `generic_view_mut` in cat macro
birktj Feb 2, 2023
a291911
Fix clippy lints in cat macro
birktj Feb 2, 2023
b374dca
Clean up documentation for cat macro
birktj Feb 2, 2023
5191892
Remove identity literal from cat macro
birktj Apr 22, 2023
87c0310
Allow references in input to cat macro
birktj Apr 22, 2023
58a93f0
Rename cat macro to stack
birktj Apr 22, 2023
ae296a8
Add more stack macro tests
birktj Apr 22, 2023
8b80ccf
Add comment to explain reason for prefix in stack! macro
Andlon Mar 18, 2024
8656a25
Refactor matrix!, stack! macros into separate modules
Andlon Mar 19, 2024
afdd0b8
Take all blocks by reference in stack! macro
Andlon Mar 20, 2024
e249433
Make empty stack![] invocation well-defined
Andlon Mar 20, 2024
fcfed5e
Fix stack! macro incorrect reference to data
Andlon Mar 25, 2024
9774d7f
More extensive tests for stack! macro
Andlon Mar 26, 2024
526c3b6
Move nalgebra-macros tests to nalgebra tests
Andlon Mar 29, 2024
3c941ce
Fix stack! code generation tests
Andlon Mar 29, 2024
26bd8ff
Add back nalgebra as dev-dependency of nalgebra-macros
Andlon Mar 29, 2024
5272edd
Fix accidental wrong matrix! macro references in docs
Andlon Mar 29, 2024
91c3c35
Rewrite stack! documentation for clarity
Andlon Mar 30, 2024
d690284
Formatting
Andlon Mar 30, 2024
557ef33
Skip formatting of macro, rustfmt messes it up
Andlon Mar 31, 2024
2523d2f
Rewrite stack! impl for improved clarity, Span behavior
Andlon Apr 1, 2024
b443d15
Use SameNumberOfRows/Columns instead of DimEq in stack! macro
Andlon Apr 1, 2024
5a1b42f
Check that stack! panics at runtime for basic dimension mismatch
Andlon Apr 1, 2024
015ac7d
Add suggested edge cases from initial PR to tests
Andlon Apr 1, 2024
c3749bc
stack! impl: use fixed prefix everywhere
Andlon Apr 1, 2024
7940bd5
nalgebra-macros: Remove clippy pedantic, fix clippy complaints
Andlon Apr 1, 2024
f378ab9
Add stack! sanity tests for built-ins and Complex
Andlon Apr 3, 2024
9d1071c
Fix formatting in test
Andlon May 1, 2024
45cad66
Improve readability of format_ident! calls in stack! impl
Andlon May 1, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
6 changes: 5 additions & 1 deletion Cargo.toml
Expand Up @@ -33,7 +33,6 @@ libm = [ "simba/libm" ]
libm-force = [ "simba/libm_force" ]
macros = [ "nalgebra-macros" ]


# Conversion
convert-mint = [ "mint" ]
convert-bytemuck = [ "bytemuck" ]
Expand Down Expand Up @@ -118,6 +117,11 @@ nalgebra = { path = ".", features = ["debug", "compare", "rand", "macros"]}
matrixcompare = "0.3.0"
itertools = "0.10"

# For macro testing
trybuild = "1.0.90"

cool_asserts = "2.0.3"

[workspace]
members = [ "nalgebra-lapack", "nalgebra-glm", "nalgebra-sparse", "nalgebra-macros" ]
resolver = "2"
Expand Down
3 changes: 1 addition & 2 deletions nalgebra-macros/Cargo.toml
Expand Up @@ -21,5 +21,4 @@ quote = "1.0"
proc-macro2 = "1.0"

[dev-dependencies]
nalgebra = { version = "0.32.0", path = ".." }
trybuild = "1.0.42"
nalgebra = { version = "0.32.1", path = ".." }
284 changes: 110 additions & 174 deletions nalgebra-macros/src/lib.rs
Expand Up @@ -12,102 +12,19 @@
future_incompatible,
missing_copy_implementations,
missing_debug_implementations,
clippy::all,
clippy::pedantic
clippy::all
)]

use proc_macro::TokenStream;
use quote::{quote, ToTokens, TokenStreamExt};
use syn::parse::{Error, Parse, ParseStream, Result};
use syn::punctuated::Punctuated;
use syn::Expr;
use syn::{parse_macro_input, Token};

use proc_macro2::{Delimiter, Spacing, TokenStream as TokenStream2, TokenTree};
use proc_macro2::{Group, Punct};

struct Matrix {
// Represent the matrix as a row-major vector of vectors of expressions
rows: Vec<Vec<Expr>>,
ncols: usize,
}

impl Matrix {
fn nrows(&self) -> usize {
self.rows.len()
}

fn ncols(&self) -> usize {
self.ncols
}

/// Produces a stream of tokens representing this matrix as a column-major nested array.
fn to_col_major_nested_array_tokens(&self) -> TokenStream2 {
let mut result = TokenStream2::new();
for j in 0..self.ncols() {
let mut col = TokenStream2::new();
let col_iter = (0..self.nrows()).map(move |i| &self.rows[i][j]);
col.append_separated(col_iter, Punct::new(',', Spacing::Alone));
result.append(Group::new(Delimiter::Bracket, col));
result.append(Punct::new(',', Spacing::Alone));
}
TokenStream2::from(TokenTree::Group(Group::new(Delimiter::Bracket, result)))
}

/// Produces a stream of tokens representing this matrix as a column-major flat array
/// (suitable for representing e.g. a `DMatrix`).
fn to_col_major_flat_array_tokens(&self) -> TokenStream2 {
let mut data = TokenStream2::new();
for j in 0..self.ncols() {
for i in 0..self.nrows() {
self.rows[i][j].to_tokens(&mut data);
data.append(Punct::new(',', Spacing::Alone));
}
}
TokenStream2::from(TokenTree::Group(Group::new(Delimiter::Bracket, data)))
}
}
mod matrix_vector_impl;
mod stack_impl;

type MatrixRowSyntax = Punctuated<Expr, Token![,]>;
use matrix_vector_impl::{Matrix, Vector};

impl Parse for Matrix {
fn parse(input: ParseStream<'_>) -> Result<Self> {
let mut rows = Vec::new();
let mut ncols = None;

while !input.is_empty() {
let row_span = input.span();
let row = MatrixRowSyntax::parse_separated_nonempty(input)?;

if let Some(ncols) = ncols {
if row.len() != ncols {
let row_idx = rows.len();
let error_msg = format!(
"Unexpected number of entries in row {}. Expected {}, found {} entries.",
row_idx,
ncols,
row.len()
);
return Err(Error::new(row_span, error_msg));
}
} else {
ncols = Some(row.len());
}
rows.push(row.into_iter().collect());

// We've just read a row, so if there are more tokens, there must be a semi-colon,
// otherwise the input is malformed
if !input.is_empty() {
input.parse::<Token![;]>()?;
}
}

Ok(Self {
rows,
ncols: ncols.unwrap_or(0),
})
}
}
use crate::matrix_vector_impl::{dmatrix_impl, dvector_impl, matrix_impl, vector_impl};
use proc_macro::TokenStream;
use quote::quote;
use stack_impl::stack_impl;
use syn::parse_macro_input;

/// Construct a fixed-size matrix directly from data.
///
Expand Down Expand Up @@ -145,20 +62,7 @@ impl Parse for Matrix {
/// ```
#[proc_macro]
pub fn matrix(stream: TokenStream) -> TokenStream {
let matrix = parse_macro_input!(stream as Matrix);

let row_dim = matrix.nrows();
let col_dim = matrix.ncols();

let array_tokens = matrix.to_col_major_nested_array_tokens();

// TODO: Use quote_spanned instead??
let output = quote! {
nalgebra::SMatrix::<_, #row_dim, #col_dim>
::from_array_storage(nalgebra::ArrayStorage(#array_tokens))
};

proc_macro::TokenStream::from(output)
matrix_impl(stream)
}

/// Construct a dynamic matrix directly from data.
Expand All @@ -180,55 +84,7 @@ pub fn matrix(stream: TokenStream) -> TokenStream {
/// ```
#[proc_macro]
pub fn dmatrix(stream: TokenStream) -> TokenStream {
let matrix = parse_macro_input!(stream as Matrix);

let row_dim = matrix.nrows();
let col_dim = matrix.ncols();

let array_tokens = matrix.to_col_major_flat_array_tokens();

// TODO: Use quote_spanned instead??
let output = quote! {
nalgebra::DMatrix::<_>
::from_vec_storage(nalgebra::VecStorage::new(
nalgebra::Dyn(#row_dim),
nalgebra::Dyn(#col_dim),
vec!#array_tokens))
};

proc_macro::TokenStream::from(output)
}

struct Vector {
elements: Vec<Expr>,
}

impl Vector {
fn to_array_tokens(&self) -> TokenStream2 {
let mut data = TokenStream2::new();
data.append_separated(&self.elements, Punct::new(',', Spacing::Alone));
TokenStream2::from(TokenTree::Group(Group::new(Delimiter::Bracket, data)))
}

fn len(&self) -> usize {
self.elements.len()
}
}

impl Parse for Vector {
fn parse(input: ParseStream<'_>) -> Result<Self> {
// The syntax of a vector is just the syntax of a single matrix row
if input.is_empty() {
Ok(Self {
elements: Vec::new(),
})
} else {
let elements = MatrixRowSyntax::parse_terminated(input)?
.into_iter()
.collect();
Ok(Self { elements })
}
}
dmatrix_impl(stream)
}

/// Construct a fixed-size column vector directly from data.
Expand All @@ -252,14 +108,7 @@ impl Parse for Vector {
/// ```
#[proc_macro]
pub fn vector(stream: TokenStream) -> TokenStream {
let vector = parse_macro_input!(stream as Vector);
let len = vector.len();
let array_tokens = vector.to_array_tokens();
let output = quote! {
nalgebra::SVector::<_, #len>
::from_array_storage(nalgebra::ArrayStorage([#array_tokens]))
};
proc_macro::TokenStream::from(output)
vector_impl(stream)
}

/// Construct a dynamic column vector directly from data.
Expand All @@ -279,17 +128,7 @@ pub fn vector(stream: TokenStream) -> TokenStream {
/// ```
#[proc_macro]
pub fn dvector(stream: TokenStream) -> TokenStream {
let vector = parse_macro_input!(stream as Vector);
let len = vector.len();
let array_tokens = vector.to_array_tokens();
let output = quote! {
nalgebra::DVector::<_>
::from_vec_storage(nalgebra::VecStorage::new(
nalgebra::Dyn(#len),
nalgebra::Const::<1>,
vec!#array_tokens))
};
proc_macro::TokenStream::from(output)
dvector_impl(stream)
}

/// Construct a fixed-size point directly from data.
Expand Down Expand Up @@ -321,3 +160,100 @@ pub fn point(stream: TokenStream) -> TokenStream {
};
proc_macro::TokenStream::from(output)
}

/// Construct a new matrix by stacking matrices in a block matrix.
///
/// **Note: Requires the `macros` feature to be enabled (enabled by default)**.
///
/// This macro facilitates the construction of
/// [block matrices](https://en.wikipedia.org/wiki/Block_matrix)
/// by stacking blocks (matrices) using the same MATLAB-like syntax as the [`matrix!`] and
/// [`dmatrix!`] macros:
///
/// ```rust
/// # use nalgebra::stack;
/// #
/// # fn main() {
/// # let [a, b, c, d] = std::array::from_fn(|_| nalgebra::Matrix1::new(0));
/// // a, b, c and d are matrices
/// let block_matrix = stack![ a, b;
/// c, d ];
/// # }
/// ```
///
/// The resulting matrix is stack-allocated if the dimension of each block row and column
/// can be determined at compile-time, otherwise it is heap-allocated.
/// This is the case if, for every row, there is at least one matrix with a fixed number of rows,
/// and, for every column, there is at least one matrix with a fixed number of columns.
///
/// [`stack!`] also supports special syntax to indicate zero blocks in a matrix:
///
/// ```rust
/// # use nalgebra::stack;
/// #
/// # fn main() {
/// # let [a, b, c, d] = std::array::from_fn(|_| nalgebra::Matrix1::new(0));
/// // a and d are matrices
/// let block_matrix = stack![ a, 0;
/// 0, d ];
/// # }
/// ```
/// Here, the `0` literal indicates a zero matrix of implicitly defined size.
/// In order to infer the size of the zero blocks, there must be at least one matrix
/// in every row and column of the matrix.
/// In other words, no row or column can consist entirely of implicit zero blocks.
///
/// # Panics
///
/// Panics if dimensions are inconsistent and it cannot be determined at compile-time.
///
/// # Examples
///
/// ```
/// use nalgebra::{matrix, SMatrix, stack};
///
/// let a = matrix![1, 2;
/// 3, 4];
/// let b = matrix![5, 6;
/// 7, 8];
/// let c = matrix![9, 10];
///
/// let block_matrix = stack![ a, b;
/// c, 0 ];
///
/// assert_eq!(block_matrix, matrix![1, 2, 5, 6;
/// 3, 4, 7, 8;
/// 9, 10, 0, 0]);
///
/// // Verify that the resulting block matrix is stack-allocated
/// let _: SMatrix<_, 3, 4> = block_matrix;
/// ```
///
/// The example above shows how stacking stack-allocated matrices results in a stack-allocated
/// block matrix. If all row and column dimensions can not be determined at compile-time,
/// the result is instead a dynamically allocated matrix:
///
/// ```
/// use nalgebra::{dmatrix, DMatrix, Dyn, matrix, OMatrix, SMatrix, stack, U3};
///
/// # let a = matrix![1, 2; 3, 4]; let c = matrix![9, 10];
/// // a and c as before, but b is a dynamic matrix this time
/// let b = dmatrix![5, 6;
/// 7, 8];
///
/// // In this case, the number of rows can be statically inferred to be 3 (U3),
/// // but the number of columns cannot, hence it is dynamic
/// let block_matrix: OMatrix<_, U3, Dyn> = stack![ a, b;
/// c, 0 ];
///
/// // If necessary, a fully dynamic matrix (DMatrix) can be obtained by reshaping
/// let dyn_block_matrix: DMatrix<_> = block_matrix.reshape_generic(Dyn(3), Dyn(4));
/// ```
/// Note that explicitly annotating the types of `block_matrix` and `dyn_block_matrix` is
/// only made for illustrative purposes, and is not generally necessary.
///
#[proc_macro]
pub fn stack(stream: TokenStream) -> TokenStream {
let matrix = parse_macro_input!(stream as Matrix);
proc_macro::TokenStream::from(stack_impl(matrix).unwrap_or_else(syn::Error::into_compile_error))
}