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

Matrix macro #886

Merged
merged 25 commits into from May 9, 2021
Merged
Show file tree
Hide file tree
Changes from 19 commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
e976922
Initial hacked together prototype without syn
Andlon Apr 11, 2021
ab95cf7
Initial impl using syn and quote
Andlon Apr 11, 2021
ed83350
Fix warnings, refactor code
Andlon Apr 11, 2021
1dccdb1
Exhaustive tests for small dimensions
Andlon Apr 12, 2021
e60136f
Update nalgebra-macros to nalgebra 0.26 and const generics
Andlon Apr 14, 2021
ec2a5a3
Construct ArrayStorage directly in matrix![]
Andlon Apr 17, 2021
7098a4f
Test that matrix![] can be used with const
Andlon Apr 17, 2021
9142dc8
Implement SMatrix::from_array_storage and use it in matriX! impl
Andlon Apr 17, 2021
d2c11ad
Impl DMatrix/DVector::from_vec_storage
Andlon Apr 17, 2021
5c84302
Implement dmatrix![] macro
Andlon Apr 29, 2021
d56db1a
Assert type in matrix/dmatrix tests
Andlon Apr 30, 2021
07d41e4
vector! and dvector! macros
Andlon Apr 30, 2021
da07749
Add trybuild tests to test error message reported when matrix dims mi…
Andlon Apr 30, 2021
b96c755
Document macros
Andlon Apr 30, 2021
eeab4db
Add nalgebra/macros feature and re-export matrix macros from nalgebra
Andlon Apr 30, 2021
0bde07f
Document that feature needs to be enabled, and require macros feature…
Andlon Apr 30, 2021
041b8c4
Add macro sanity tests to macros
Andlon Apr 30, 2021
08a77dd
Test nalgebra-macros on CI
Andlon Apr 30, 2021
8552fc8
Cargo fmt
Andlon May 3, 2021
f42ecf0
Improve nalgebra-macros/Cargo.toml metadata
Andlon May 5, 2021
57541aa
Add tests to ensure macros compile for all built-in types
Andlon May 5, 2021
6026a05
Test that matrix macros work with arbitrary expressions
Andlon May 5, 2021
39b275f
Formatting
Andlon May 5, 2021
3a3bc55
Move from_{}_storage impl blocks to matrix.rs
Andlon May 7, 2021
922393b
Enable from_{}_storage only when std/alloc available
Andlon May 7, 2021
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: 6 additions & 0 deletions .github/workflows/nalgebra-ci-build.yml
Expand Up @@ -69,6 +69,12 @@ jobs:
- name: test nalgebra-sparse (slow tests)
# Unfortunately, the "slow-tests" take so much time that we need to run them with --release
run: PROPTEST_CASES=10000 cargo test --release --manifest-path=nalgebra-sparse/Cargo.toml --features compare,proptest-support,slow-tests slow
test-nalgebra-macros:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: test nalgebra-macros
run: cargo test -p nalgebra-macros
build-wasm:
runs-on: ubuntu-latest
# env:
Expand Down
8 changes: 5 additions & 3 deletions Cargo.toml
Expand Up @@ -22,7 +22,7 @@ name = "nalgebra"
path = "src/lib.rs"

[features]
default = [ "std" ]
default = [ "std", "macros" ]
std = [ "matrixmultiply", "simba/std" ]
sparse = [ ]
debug = [ "approx/num-complex", "rand" ]
Expand All @@ -32,6 +32,7 @@ compare = [ "matrixcompare-core" ]
libm = [ "simba/libm" ]
libm-force = [ "simba/libm_force" ]
no_unsound_assume_init = [ ]
macros = [ "nalgebra-macros" ]

# Conversion
convert-mint = [ "mint" ]
Expand Down Expand Up @@ -60,6 +61,7 @@ proptest-support = [ "proptest" ]
slow-tests = []

[dependencies]
nalgebra-macros = { version = "0.1", path = "nalgebra-macros", optional = true }
typenum = "1.12"
rand-package = { package = "rand", version = "0.8", optional = true, default-features = false }
num-traits = { version = "0.2", default-features = false }
Expand Down Expand Up @@ -92,7 +94,7 @@ matrixcompare = "0.2.0"
itertools = "0.10"

[workspace]
members = [ "nalgebra-lapack", "nalgebra-glm", "nalgebra-sparse" ]
members = [ "nalgebra-lapack", "nalgebra-glm", "nalgebra-sparse", "nalgebra-macros" ]
resolver = "2"

[[example]]
Expand All @@ -109,4 +111,4 @@ lto = true

[package.metadata.docs.rs]
# Enable certain features when building docs for docs.rs
features = [ "proptest-support", "compare" ]
features = [ "proptest-support", "compare", "macros" ]
20 changes: 20 additions & 0 deletions nalgebra-macros/Cargo.toml
@@ -0,0 +1,20 @@
[package]
name = "nalgebra-macros"
version = "0.1.0"
authors = ["Andreas Longva <andreas.b.longva@gmail.com>"]
edition = "2018"

Andlon marked this conversation as resolved.
Show resolved Hide resolved
[lib]
proc-macro = true

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]
# TODO: Determine minimal features that we need
syn = { version="1.0", features = ["full"] }
quote = "1.0"
proc-macro2 = "1.0"

[dev-dependencies]
nalgebra = { version = "0.26.1", path = ".." }
trybuild = "1.0.42"
282 changes: 282 additions & 0 deletions nalgebra-macros/src/lib.rs
@@ -0,0 +1,282 @@
//! Macros for `nalgebra`.
//!
//! This crate is not intended for direct consumption. Instead, the macros are re-exported by
//! `nalgebra` if the `macros` feature is enabled (enabled by default).

extern crate proc_macro;

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

type MatrixRowSyntax = Punctuated<Expr, Token![,]>;

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),
})
}
}

/// Construct a fixed-size matrix directly from data.
///
/// **Note: Requires the `macro` feature to be enabled (enabled by default)**.
///
/// This macro facilitates easy construction of matrices when the entries of the matrix are known
/// (either as constants or expressions). This macro produces an instance of `SMatrix`. This means
/// that the data of the matrix is stored on the stack, and its dimensions are fixed at
/// compile-time. If you want to construct a dynamic matrix, use [`dmatrix!`] instead.
///
/// `matrix!` is intended to be both the simplest and most efficient way to construct (small)
/// matrices, and can also be used in *const fn* contexts.
///
/// The syntax is MATLAB-like. Column elements are separated by a comma (`,`), and a semi-colon
/// (`;`) designates that a new row begins.
///
/// # Examples
///
/// ```
/// use nalgebra::matrix;
///
/// // Produces a Matrix3<_> == SMatrix<_, 3, 3>
/// let a = matrix![1, 2, 3;
/// 4, 5, 6;
/// 7, 8, 9];
/// ```
///
/// You can construct matrices with arbitrary expressions for its elements:
///
/// ```
/// use nalgebra::{matrix, Matrix2};
/// let theta = 0.45f64;
///
/// let r = matrix![theta.cos(), - theta.sin();
/// theta.sin(), theta.cos()];
/// ```
#[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)
}

/// Construct a dynamic matrix directly from data.
///
/// **Note: Requires the `macro` feature to be enabled (enabled by default)**.
///
/// The syntax is exactly the same as for [`matrix!`], but instead of producing instances of
/// `SMatrix`, it produces instances of `DMatrix`. At the moment it is not usable
/// in `const fn` contexts.
///
/// ```
/// use nalgebra::dmatrix;
///
/// // Produces a DMatrix<_>
/// let a = dmatrix![1, 2, 3;
/// 4, 5, 6;
/// 7, 8, 9];
/// ```
#[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::Dynamic::new(#row_dim),
nalgebra::Dynamic::new(#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_separated_nonempty(input)?
.into_iter()
.collect();
Ok(Self { elements })
}
}
}

/// Construct a fixed-size column vector directly from data.
///
/// **Note: Requires the `macro` feature to be enabled (enabled by default)**.
///
/// Similarly to [`matrix!`], this macro facilitates easy construction of fixed-size vectors.
/// However, whereas the [`matrix!`] macro expects each row to be separated by a semi-colon,
/// the syntax of this macro is instead similar to `vec!`, in that the elements of the vector
/// are simply listed consecutively.
///
/// `vector!` is intended to be the most readable and performant way of constructing small,
/// fixed-size vectors, and it is usable in `const fn` contexts.
///
/// ## Examples
///
/// ```
/// use nalgebra::vector;
///
/// // Produces a Vector3<_> == SVector<_, 3>
/// let v = vector![1, 2, 3];
/// ```
#[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)
}

/// Construct a dynamic column vector directly from data.
///
/// **Note: Requires the `macro` feature to be enabled (enabled by default)**.
///
/// The syntax is exactly the same as for [`vector!`], but instead of producing instances of
/// `SVector`, it produces instances of `DVector`. At the moment it is not usable
/// in `const fn` contexts.
///
/// ```
/// use nalgebra::dvector;
///
/// // Produces a DVector<_>
/// let v = dvector![1, 2, 3];
/// ```
#[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::Dynamic::new(#len),
nalgebra::Const::<1>,
vec!#array_tokens))
};
proc_macro::TokenStream::from(output)
}