Repository: extendr/extendr
Pull request #112 (2021-01-07 01:07:50)

Generate ndarray-related documentations
As suggested by https://github.com/extendr/rextendr/issues/10#issuecomment-755374666, this PR adds `--all-features`, though this won't compile on my local (#111)... Can I just merge this and see what will happen?


Patch:
diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml
index 4671c1407..452027629 100644
--- a/.github/workflows/docs.yml
+++ b/.github/workflows/docs.yml
@@ -16,7 +16,7 @@ jobs:
         with:
           toolchain: stable
       - name: Docs
-        run: cargo doc --workspace --no-deps
+        run: cargo doc --workspace --no-deps --all-features
       - name: Deploy
         uses: peaceiris/actions-gh-pages@v3
         with:
diff --git a/MAINTAINERS_GUIDE.md b/MAINTAINERS_GUIDE.md
index aa66e2f41..e2c89a69a 100644
--- a/MAINTAINERS_GUIDE.md
+++ b/MAINTAINERS_GUIDE.md
@@ -16,20 +16,24 @@ We use `cargo ws version` for this. This command does:
 
 1. Bump all the versions of the crates in this workspace.
 2. Commit.
-3. Tag with the bumped version.
+3. Tag with the bumped version. (This step will be skipped this time as we specify `--no-git-tag`)
 4. Push to the repo immediately. (Be careful! If you want to review the change manually before pushing, add `--no-git-push` option as well.)
 
 ``` sh
-cargo ws version --force='*' --no-individual-tags --pre-id alpha prerelease
+cargo ws version --force='*' --no-git-tag --pre-id alpha prerelease
 ```
 
 The meanings of the options and arguments are
 
 * `--force='*'`: By default, `cargo ws version` skips the crates unchange since the last version. This option makes them included in the targets. 
-* `--no-individual-tags`: By default, `cargo ws version` creates a tag for each crates (e.g. `crateA@v0.0.1`) in addition to the usual version tag (e.g. `v0.0.1`). This option skips the individual tags.
+* `--no-git-tag`: By default, `cargo ws version` creates a tag for the new commit. This option skips the step.
 * `--pre-id alpha`: Specify the identifier prepended to the version.
 * `prerelease`: Increase the version with prerelease identifier (e.g. `v0.1.10 -> v0.1.10-alpha.0`, `v0.1.10-alpha.0 -> v0.1.10-alpha.1`). We can also specify `patch`, `minor`, or `major` to increment the corresponding part of the version. Alternatively, we can omit this and choose the version interactively.
 
+When we want to create a tag at the same time, remove `--no-git-tag` and specify `--no-individual-tags`.
+
+* `--no-individual-tags`: By default, `cargo ws version` creates a tag for each crate (e.g. `crateA@v0.0.1`) in addition to the usual version tag (e.g. `v0.0.1`). This option skips the individual tags.
+
 <details>
 
 <summary>console output</summary>
@@ -71,11 +75,7 @@ When we publish, we use `cargo ws publish`. This command does:
 2. Publish all the crates within the workspace.
 
 ``` sh
-# publish
 cargo ws publish --force='*' --no-individual-tags patch
-
-# change the version for further development
-cargo ws version --force='*' --no-individual-tags --pre-id alpha prerelease
 ```
 
 This command will publish to Crates.io immediately.
@@ -86,7 +86,7 @@ If we want to review the change manually before publishing, we can do it step by
 cargo ws version --force='*' --no-individual-tags --no-git-push patch
 
 # review the changes
-git show HEAD^
+git show HEAD
 
 # push all the commits and tags
 git push
@@ -94,7 +94,6 @@ git push --tags
 
 # publish without modifying the current version
 cargo ws publish --force='*' --from-git
-
-# change the version for further development
-cargo ws version --force='*' --no-individual-tags --pre-id alpha prerelease
 ```
+
+We might also want to bump version to the development version. In the case, follow "Bump version without publish" section above.
\ No newline at end of file
diff --git a/extendr-api/src/lib.rs b/extendr-api/src/lib.rs
index e6ee9d012..1b399ce55 100644
--- a/extendr-api/src/lib.rs
+++ b/extendr-api/src/lib.rs
@@ -198,6 +198,7 @@ mod rmacros;
 mod robj;
 mod thread_safety;
 mod wrapper;
+mod matrix;
 
 #[cfg(feature = "ndarray")]
 mod robj_ndarray;
@@ -210,6 +211,7 @@ pub use rmacros::*;
 pub use robj::*;
 pub use thread_safety::{handle_panic, single_threaded};
 pub use wrapper::*;
+pub use matrix::*;
 
 #[cfg(feature = "ndarray")]
 pub use robj_ndarray::*;
diff --git a/extendr-api/src/matrix.rs b/extendr-api/src/matrix.rs
new file mode 100644
index 000000000..ceff8c137
--- /dev/null
+++ b/extendr-api/src/matrix.rs
@@ -0,0 +1,314 @@
+//! Wrappers for matrices with deferred arithmetic.
+
+use crate::*;
+use std::ops::{Index, IndexMut};
+
+/// Wrapper for creating and using matrices and arrays.
+///
+/// ```
+/// use extendr_api::*;
+/// test! {
+///     let matrix = Matrix::new([
+///          1., 2., 3.,
+///          4., 5., 6.], 3, 2);
+///     let robj = r!(matrix);
+///     assert_eq!(robj.is_matrix(), true);
+///     assert_eq!(robj.nrows(), 3);
+///     assert_eq!(robj.ncols(), 2);
+///
+///     let matrix2 : Matrix<&[f64]> = robj.as_matrix().ok_or("error")?;
+///     assert_eq!(matrix2.data().len(), 6);
+///     assert_eq!(matrix2.nrows(), 3);
+///     assert_eq!(matrix2.ncols(), 2);
+/// }
+/// ```
+#[derive(Debug, PartialEq)]
+pub struct Array<T, D> {
+    data: T,
+    dim: D,
+}
+
+pub type Vector<T> = Array<T, [usize; 1]>;
+pub type Matrix<T> = Array<T, [usize; 2]>;
+pub type Matrix3D<T> = Array<T, [usize; 3]>;
+
+const BASE: usize = 0;
+
+trait Offset<D> {
+    /// Get the offset into the array for a given index.
+    fn offset(&self, idx: D) -> usize;
+}
+
+impl<T> Offset<[usize; 1]> for Array<T, [usize; 1]> {
+    /// Get the offset into the array for a given index.
+    fn offset(&self, index: [usize; 1]) -> usize {
+        if index[0] - BASE > self.dim[0] {
+            panic!("array index: row overflow");
+        }
+        index[0] - BASE
+    }
+}
+
+impl<T> Offset<[usize; 2]> for Array<T, [usize; 2]> {
+    /// Get the offset into the array for a given index.
+    fn offset(&self, index: [usize; 2]) -> usize {
+        if index[0] - BASE > self.dim[0] {
+            panic!("matrix index: row overflow");
+        }
+        if index[1] - BASE > self.dim[1] {
+            panic!("matrix index: column overflow");
+        }
+        (index[0] - BASE) + self.dim[0] * (index[1] - BASE)
+    }
+}
+
+impl<T> Offset<[usize; 3]> for Array<T, [usize; 3]> {
+    /// Get the offset into the array for a given index.
+    fn offset(&self, index: [usize; 3]) -> usize {
+        if index[0] - BASE > self.dim[0] {
+            panic!("Matrix3D index: row overflow");
+        }
+        if index[1] - BASE > self.dim[1] {
+            panic!("Matrix3D index: column overflow");
+        }
+        if index[2] - BASE > self.dim[2] {
+            panic!("Matrix3D index: submatrix overflow");
+        }
+        (index[0] - BASE) + self.dim[0] * (index[1] - BASE + self.dim[1] * (index[2] - BASE))
+    }
+}
+
+impl<T, D> Array<T, D> {
+    /// Get the underlying data fro this array.
+    pub fn data(&self) -> &T {
+        &self.data
+    }
+
+    /// Get the dimensions for this array.
+    pub fn dim(&self) -> &D {
+        &self.dim
+    }
+}
+
+impl<T> Vector<T> {
+    /// Make a new vector type.
+    pub fn new(data: T, nrows: usize) -> Self {
+        let dim = [nrows];
+        Self { data, dim }
+    }
+
+    /// Get the number of rows.
+    pub fn nrows(&self) -> usize {
+        self.dim[0]
+    }
+}
+
+impl<T> Matrix<T> {
+    /// Create a new matrix wrapper.
+    pub fn new(data: T, nrows: usize, ncols: usize) -> Self {
+        let dim = [nrows, ncols];
+        Self { data, dim }
+    }
+
+    /// Get the number of rows.
+    pub fn nrows(&self) -> usize {
+        self.dim[0]
+    }
+
+    /// Get the number of columns.
+    pub fn ncols(&self) -> usize {
+        self.dim[1]
+    }
+}
+
+impl<T> Matrix3D<T> {
+    /// Create a new matrix wrapper.
+    pub fn new(data: T, nrows: usize, ncols: usize, nsub: usize) -> Self {
+        let dim = [nrows, ncols, nsub];
+        Self { data, dim }
+    }
+
+    /// Get the number of rows.
+    pub fn nrows(&self) -> usize {
+        self.dim[0]
+    }
+
+    /// Get the number of columns.
+    pub fn ncols(&self) -> usize {
+        self.dim[1]
+    }
+
+    /// Get the number of submatrices.
+    pub fn nsub(&self) -> usize {
+        self.dim[2]
+    }
+}
+
+impl<T> From<Vector<T>> for Robj
+where
+    T: Into<Robj>,
+{
+    fn from(array: Vector<T>) -> Self {
+        let res = array.data.into();
+        res
+    }
+}
+
+impl<T> From<Array<T, [usize; 2]>> for Robj
+where
+    T: Into<Robj>,
+{
+    fn from(array: Array<T, [usize; 2]>) -> Self {
+        let res = array.data.into();
+        let dim = [array.dim[0] as i32, array.dim[1] as i32];
+        res.set_attrib(Symbol("dim"), dim);
+        res.set_attrib(Symbol("class"), "matrix");
+        res
+    }
+}
+
+impl<T> From<Array<T, [usize; 3]>> for Robj
+where
+    T: Into<Robj>,
+{
+    fn from(array: Array<T, [usize; 3]>) -> Self {
+        let res = array.data.into();
+        let dim = [
+            array.dim[0] as i32,
+            array.dim[1] as i32,
+            array.dim[2] as i32,
+        ];
+        res.set_attrib(Symbol("dim"), dim);
+        res.set_attrib(Symbol("class"), "array");
+        res
+    }
+}
+
+impl Robj {
+    pub fn as_vector<'a, E>(&self) -> Option<Vector<&'a [E]>>
+    where
+        Self: AsTypedSlice<'a, E>,
+    {
+        if let Some(data) = self.as_typed_slice() {
+            let dim = [self.nrows() as usize];
+            return Some(Array { data, dim });
+        }
+        None
+    }
+
+    pub fn as_matrix<'a, E>(&self) -> Option<Matrix<&'a [E]>>
+    where
+        Self: AsTypedSlice<'a, E>,
+    {
+        if self.is_matrix() {
+            if let Some(data) = self.as_typed_slice() {
+                let dim = [self.nrows() as usize, self.ncols() as usize];
+                return Some(Array { data, dim });
+            }
+        }
+        None
+    }
+
+    pub fn as_matrix3d<'a, E>(&self) -> Option<Matrix3D<&'a [E]>>
+    where
+        Self: AsTypedSlice<'a, E>,
+    {
+        if self.is_array() {
+            if let Some(data) = self.as_typed_slice() {
+                if let Some(dim) = self.dim() {
+                    let dim: Vec<_> = dim.cloned().collect();
+                    let dim = [dim[0] as usize, dim[1] as usize, dim[2] as usize];
+                    return Some(Array { data, dim });
+                }
+            }
+        }
+        None
+    }
+}
+
+impl<T> Index<[usize; 2]> for Array<T, [usize; 2]>
+where
+    T: Index<usize>,
+{
+    type Output = <T as Index<usize>>::Output;
+
+    /// Zero-based indexing in row, column order.
+    ///
+    /// Panics if out of bounds.
+    /// ```
+    /// use extendr_api::*;
+    /// test! {
+    ///     let matrix = Matrix::new(vec![
+    ///          1., 2., 3.,
+    ///          4., 5., 6.], 3, 2);
+    ///     assert_eq!(matrix[[0, 0]], 1.);
+    ///     assert_eq!(matrix[[1, 0]], 2.);
+    ///     assert_eq!(matrix[[2, 1]], 6.);
+    /// }
+    /// ```
+    fn index(&self, index: [usize; 2]) -> &Self::Output {
+        &self.data[self.offset(index)]
+    }
+}
+
+impl<T> IndexMut<[usize; 2]> for Array<T, [usize; 2]>
+where
+    T: IndexMut<usize>,
+{
+    /// Zero-based mutable indexing in row, column order.
+    ///
+    /// Panics if out of bounds.
+    /// ```
+    /// use extendr_api::*;
+    /// test! {
+    ///     let mut matrix = Matrix::new(vec![0.; 6], 3, 2);
+    ///     matrix[[0, 0]] = 1.;
+    ///     matrix[[1, 0]] = 2.;
+    ///     matrix[[2, 0]] = 3.;
+    ///     assert_eq!(matrix, Matrix::new(vec![1., 2., 3., 0., 0., 0.], 3, 2));
+    /// }
+    /// ```
+    fn index_mut(&mut self, index: [usize; 2]) -> &mut Self::Output {
+        let offset = self.offset(index);
+        &mut self.data[offset]
+    }
+}
+
+#[test]
+fn matrix_ops() {
+    test! {
+        let vector = Vector::new([1., 2., 3.], 3);
+        let robj = r!(vector);
+        assert_eq!(robj.is_vector(), true);
+        assert_eq!(robj.nrows(), 3);
+
+        let vector2 : Vector<&[f64]> = robj.as_vector().ok_or("expected array")?;
+        assert_eq!(vector2.data().len(), 3);
+        assert_eq!(vector2.nrows(), 3);
+
+        let matrix = Matrix::new([
+            1., 2., 3.,
+            4., 5., 6.], 3, 2);
+        let robj = r!(matrix);
+        assert_eq!(robj.is_matrix(), true);
+        assert_eq!(robj.nrows(), 3);
+        assert_eq!(robj.ncols(), 2);
+        let matrix2 : Matrix<&[f64]> = robj.as_matrix().ok_or("expected matrix")?;
+        assert_eq!(matrix2.data().len(), 6);
+        assert_eq!(matrix2.nrows(), 3);
+        assert_eq!(matrix2.ncols(), 2);
+
+        let array = Matrix3D::new([
+            1., 2.,  3., 4.,
+            5.,  6., 7., 8.], 2, 2, 2);
+        let robj = r!(array);
+        assert_eq!(robj.is_array(), true);
+        assert_eq!(robj.nrows(), 2);
+        assert_eq!(robj.ncols(), 2);
+        let array2 : Matrix3D<&[f64]> = robj.as_matrix3d().ok_or("expected matrix3d")?;
+        assert_eq!(array2.data().len(), 8);
+        assert_eq!(array2.nrows(), 2);
+        assert_eq!(array2.ncols(), 2);
+        assert_eq!(array2.nsub(), 2);
+    }
+}
diff --git a/extendr-api/src/robj/mod.rs b/extendr-api/src/robj/mod.rs
index fae7690dc..0669c5235 100644
--- a/extendr-api/src/robj/mod.rs
+++ b/extendr-api/src/robj/mod.rs
@@ -859,13 +859,13 @@ impl Robj {
     ///    let value = robj.set_attrib(Symbol("xyz"), 1);
     ///    assert_eq!(robj.get_attrib(Symbol("xyz")), Some(value));
     /// ```
-    pub fn set_attrib<N, V>(&mut self, name: N, value: V) -> Robj
+    pub fn set_attrib<N, V>(&self, name: N, value: V) -> Robj
     where
-        Robj: From<N>,
-        Robj: From<V>,
+        N: Into<Robj>,
+        V: Into<Robj>,
     {
-        let name = Robj::from(name);
-        let value = Robj::from(value);
+        let name = name.into();
+        let value = value.into();
         single_threaded(|| unsafe {
             new_borrowed(Rf_setAttrib(self.get(), name.get(), value.get()))
         })
diff --git a/extendr-api/src/robj_ndarray.rs b/extendr-api/src/robj_ndarray.rs
index 1b3f7cf94..67954f28b 100644
--- a/extendr-api/src/robj_ndarray.rs
+++ b/extendr-api/src/robj_ndarray.rs
@@ -1,87 +1,129 @@
 #[doc(hidden)]
-use libR_sys::*;
-#[doc(hidden)]
 use ndarray::prelude::*;
 
-use crate::logical::Bool;
-use crate::robj::{AsTypedSlice, FromRobj, Robj};
+use crate::*;
 
-/// Input Numeric vector parameter.
-/// Note we don't accept mutable R objects as parameters
-/// but you can make this behaviour using unsafe code.
 impl<'a, T> FromRobj<'a> for ArrayView1<'a, T>
 where
-    Robj: AsTypedSlice<T>,
+    Robj: AsTypedSlice<'a, T>,
 {
+    /// Convert an R object to a `ndarray` ArrayView1.
     fn from_robj(robj: &'a Robj) -> std::result::Result<Self, &'static str> {
         if let Some(v) = robj.as_typed_slice() {
             Ok(ArrayView1::<'a, T>::from(v))
         } else {
-            Err("not a floating point vector")
+            Err("Not a vector of the correct type.")
         }
     }
 }
 
 macro_rules! make_array_view_2 {
-    ($type: ty, $fn: tt, $error_str: tt, $($sexp: tt),* ) => {
+    ($type: ty, $error_str: expr) => {
         impl<'a> FromRobj<'a> for ArrayView2<'a, $type> {
+            /// Convert an R object to a `ndarray` ArrayView2.
             fn from_robj(robj: &'a Robj) -> std::result::Result<Self, &'static str> {
-                match robj.sexptype() {
-                    $( $sexp )|* => unsafe {
-                        let ptr = $fn(robj.get()) as *const $type;
-                        let ncols = Rf_ncols(robj.get()) as usize;
-                        let nrows = Rf_nrows(robj.get()) as usize;
-
-                        Ok(ArrayView2::from_shape_ptr((nrows, ncols).f(), ptr))
-                    },
-                    _ => Err($error_str),
+                if robj.is_matrix() {
+                    let nrows = robj.nrows();
+                    let ncols = robj.ncols();
+                    if let Some(v) = robj.as_typed_slice() {
+                        // use fortran order.
+                        let shape = (nrows, ncols).into_shape().f();
+                        if let Ok(res) = ArrayView2::from_shape(shape, v) {
+                            return Ok(res);
+                        }
+                    }
                 }
+                return Err($error_str);
             }
         }
     }
 }
 
-make_array_view_2!(Bool, INTEGER, "not a logical matrix", LGLSXP);
-make_array_view_2!(i32, INTEGER, "not a integer matrix", INTSXP);
-make_array_view_2!(f64, REAL, "not a floating point matrix", REALSXP);
-make_array_view_2!(u8, RAW, "not a raw matrix", RAWSXP);
+make_array_view_2!(Bool, "Not a logical matrix.");
+make_array_view_2!(i32, "Not an integer matrix.");
+make_array_view_2!(f64, "Not a floating point matrix.");
+//make_array_view_2!(u8, "Not a raw matrix.");
+
+// impl<'a, T> From<ArrayView2<'a, T>> for Robj
+// where
+//     T : ToVectorValue
+// {
+//     fn from(array: ArrayView2<T>) -> Self {
+//         let dims = array.dim();
+//         let slice : &[T] = array.as_slice().unwrap();
+//         let mx = Matrix::new(slice, dims.0, dims.1);
+//         r!(mx)
+//     }
+// }
 
 #[test]
 fn test_from_robj() {
-    assert_eq!(
-        <ArrayView1<f64>>::from_robj(&Robj::from(1.)),
-        Ok(ArrayView1::<f64>::from(&[1.][..]))
-    );
-    assert_eq!(
-        <ArrayView1<i32>>::from_robj(&Robj::from(1)),
-        Ok(ArrayView1::<i32>::from(&[1][..]))
-    );
-    assert_eq!(
-        <ArrayView1<Bool>>::from_robj(&Robj::from(true)),
-        Ok(ArrayView1::<Bool>::from(&[Bool(1)][..]))
-    );
-    assert_eq!(
-        <ArrayView2<f64>>::from_robj(&Robj::from(1.)),
-        Ok(ArrayView2::<f64>::from_shape((1, 1), &[1.][..]).unwrap())
-    );
-    assert_eq!(
-        <ArrayView2<i32>>::from_robj(&Robj::from(1)),
-        Ok(ArrayView2::<i32>::from_shape((1, 1), &[1][..]).unwrap())
-    );
-    assert_eq!(
-        <ArrayView2<Bool>>::from_robj(&Robj::from(true)),
-        Ok(ArrayView2::<Bool>::from_shape((1, 1), &[Bool(1)][..]).unwrap())
-    );
+    test! {
+        assert_eq!(
+            <ArrayView1<f64>>::from_robj(&Robj::from(1.)),
+            Ok(ArrayView1::<f64>::from(&[1.][..]))
+        );
+        assert_eq!(
+            <ArrayView1<i32>>::from_robj(&Robj::from(1)),
+            Ok(ArrayView1::<i32>::from(&[1][..]))
+        );
+        assert_eq!(
+            <ArrayView1<Bool>>::from_robj(&Robj::from(true)),
+            Ok(ArrayView1::<Bool>::from(&[Bool(1)][..]))
+        );
+
+        let robj = R!(matrix(c(1, 2, 3, 4, 5, 6, 7, 8), ncol=2, nrow=4))?;
+        let mx = <ArrayView2<f64>>::from_robj(&robj)?;
+        assert_eq!(mx[[0, 0]], 1.0);
+        assert_eq!(mx[[1, 0]], 2.0);
+        assert_eq!(mx[[2, 0]], 3.0);
+        assert_eq!(mx[[3, 0]], 4.0);
+        assert_eq!(mx[[0, 1]], 5.0);
+        assert_eq!(mx[[1, 1]], 6.0);
+        assert_eq!(mx[[2, 1]], 7.0);
+        assert_eq!(mx[[3, 1]], 8.0);
 
-    assert_eq!(
-        <ArrayView2<f64>>::from_robj(
-            &Robj::eval_string("matrix(c(1, 2, 3, 4, 5, 6, 7, 8), ncol=2, nrow=4, byrow=T)")
-                .unwrap()
-        ),
-        Ok(ArrayView2::<f64>::from_shape(
-            (4, 2),
-            &[1f64, 2f64, 3f64, 4f64, 5f64, 6f64, 7f64, 8f64][..]
-        )
-        .unwrap())
-    );
+        // check basic logic of fortran-order matrices.
+        let col0 = mx.column(0);
+        assert_eq!(col0[0], 1.0);
+        assert_eq!(col0[1], 2.0);
+        assert_eq!(col0[2], 3.0);
+        assert_eq!(col0[3], 4.0);
+
+        // check integer matrices
+        let robj = R!(matrix(c(1L, 2L, 3L, 4L, 5L, 6L, 7L, 8L), ncol=2, nrow=4))?;
+        let mx = <ArrayView2<i32>>::from_robj(&robj)?;
+        assert_eq!(mx[[0, 0]], 1);
+        assert_eq!(mx[[1, 0]], 2);
+        assert_eq!(mx[[2, 0]], 3);
+        assert_eq!(mx[[3, 0]], 4);
+        assert_eq!(mx[[0, 1]], 5);
+        assert_eq!(mx[[1, 1]], 6);
+        assert_eq!(mx[[2, 1]], 7);
+        assert_eq!(mx[[3, 1]], 8);
+
+        // check logical matrices
+        let robj = R!(matrix(c(T, T, T, T, F, F, F, F), ncol=2, nrow=4))?;
+        let mx = <ArrayView2<Bool>>::from_robj(&robj)?;
+        assert_eq!(mx[[0, 0]], TRUE);
+        assert_eq!(mx[[1, 0]], TRUE);
+        assert_eq!(mx[[2, 0]], TRUE);
+        assert_eq!(mx[[3, 0]], TRUE);
+        assert_eq!(mx[[0, 1]], FALSE);
+        assert_eq!(mx[[1, 1]], FALSE);
+        assert_eq!(mx[[2, 1]], FALSE);
+        assert_eq!(mx[[3, 1]], FALSE);
+
+        // check raw matrices
+        // let robj = r!(Matrix::new(vec![1_u8, 2, 3, 4, 5, 6, 7, 8], 4, 2));
+        // let mx = <ArrayView2<u8>>::from_robj(&robj)?;
+        // assert_eq!(mx[[0, 0]], 1);
+        // assert_eq!(mx[[1, 0]], 2);
+        // assert_eq!(mx[[2, 0]], 3);
+        // assert_eq!(mx[[3, 0]], 4);
+        // assert_eq!(mx[[0, 1]], 5);
+        // assert_eq!(mx[[1, 1]], 6);
+        // assert_eq!(mx[[2, 1]], 7);
+        // assert_eq!(mx[[3, 1]], 8);
+    }
 }


Test patch:
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index c9c75c7e3..a42c5fe34 100644
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test.yml
@@ -245,11 +245,19 @@ jobs:
         run: |
           foreach($target in ($env:BUILD_TARGETS).Split(',')) {
             echo $target
-            cargo build --manifest-path extendr-api/Cargo.toml --features tests-all $(if($target -ne 'default') {"--target=$target"} ) 
+            cargo build --manifest-path extendr-api/Cargo.toml --features tests-all $(if($target -ne 'default') {"--target=$target"} )
+            if (!$?) { 
+              echo "::error::$target" ;
+              throw "Last exit code $LASTEXITCODE"
+            }
           }
           foreach($target in ($env:BUILD_TARGETS).Split(',')) {
             echo $target
             cargo build --manifest-path extendr-engine/Cargo.toml --features tests-all $(if($target -ne 'default') {"--target=$target"} ) 
+            if (!$?) { 
+              echo "::error::$target" ;
+              throw "Last exit code $LASTEXITCODE"
+            }
           }
 
       - name: Run tests
@@ -257,17 +265,32 @@ jobs:
           foreach($target in ($env:BUILD_TARGETS).Split(',')) {
             echo $target
             cargo test --manifest-path extendr-engine/Cargo.toml --features tests-all $(if($target -ne 'default') {"--target=$target"} ) -- --nocapture 
+            if (!$?) { 
+              echo "::error::$target" ;
+              throw "Last exit code $LASTEXITCODE"
+            }
           }
           foreach($target in ($env:BUILD_TARGETS).Split(',')) {
             echo $target
             cargo test --manifest-path extendr-api/Cargo.toml --features tests-all $(if($target -ne 'default') {"--target=$target"} ) -- --nocapture 
+            if (!$?) { 
+              echo "::error::$target" ;
+              throw "Last exit code $LASTEXITCODE"
+            }
           }
           foreach($target in ($env:BUILD_TARGETS).Split(',')) {
             echo $target
             cargo test --manifest-path extendr-api/Cargo.toml --features tests-minimal $(if($target -ne 'default') {"--target=$target"} ) -- --nocapture
+            if (!$?) { 
+              echo "::error::$target" ;
+              throw "Last exit code $LASTEXITCODE"
+            }
           }
           foreach($target in ($env:BUILD_TARGETS).Split(',')) {
             echo $target
             cargo test --manifest-path extendr-macros/Cargo.toml  $(if($target -ne 'default') {"--target=$target"} ) -- --nocapture
+            if (!$?) { 
+              echo "::error::$target" ;
+              throw "Last exit code $LASTEXITCODE"
+            }
           }
-