Repository: extendr/extendr
Pull request #116 (2021-01-07 13:03:16)

Add a special "NA" string that tests as a NA.
To make constructing string vectors with nulls more intuitive, I've added
a special null string with value "NA" but guaranteed to have a different address from
the regular "NA" string.

This means we can keep `StrIter` returning strings and still test for null strings.

The converse works, to, with `na_str()` mixable with regular string references.

```
    let character_with_nulls = r!(["a", na_str(), "b"]);
```

You can use `is_na()` to test for NAs.


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/extendr-api/src/functions.rs b/extendr-api/src/functions.rs
index d24f3d327..b8962609c 100644
--- a/extendr-api/src/functions.rs
+++ b/extendr-api/src/functions.rs
@@ -364,6 +364,20 @@ pub fn blank_scalar_string() -> Robj {
     unsafe { new_sys(R_BlankScalarString) }
 }
 
+/// Special "NA" string that represents null strings.
+/// ```
+/// use extendr_api::*;
+/// test! {
+///     assert!(na_str().as_ptr() != "NA".as_ptr());
+///     assert_eq!(na_str(), "NA");
+///     assert_eq!("NA".is_na(), false);
+///     assert_eq!(na_str().is_na(), true);
+/// }
+/// ```
+pub fn na_str() -> &'static str {
+    unsafe { std::str::from_utf8_unchecked(&[b'N', b'A']) }
+}
+
 /// Extendr test harness.
 ///
 /// See also [test!]
diff --git a/extendr-api/src/lib.rs b/extendr-api/src/lib.rs
index e6ee9d012..aec65618d 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::*;
@@ -288,6 +290,13 @@ impl IsNA for Bool {
     }
 }
 
+impl IsNA for &str {
+    /// Check for NA in a string by address.
+    fn is_na(&self) -> bool {
+        self.as_ptr() == na_str().as_ptr()
+    }
+}
+
 #[doc(hidden)]
 pub fn print_r_output<T: Into<Vec<u8>>>(s: T) {
     let cs = CString::new(s).expect("NulError");
@@ -499,4 +508,12 @@ mod tests {
         let result = R!(test_con).unwrap();
         assert_eq!(result, r!("Hello world"));
     }
+
+    #[test]
+    fn test_na_str() {
+        assert!(na_str().as_ptr() != "NA".as_ptr());
+        assert_eq!(na_str(), "NA");
+        assert_eq!("NA".is_na(), false);
+        assert_eq!(na_str().is_na(), true);
+    }
 }
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/into_robj.rs b/extendr-api/src/robj/into_robj.rs
index 7c864d65d..15fa1d214 100644
--- a/extendr-api/src/robj/into_robj.rs
+++ b/extendr-api/src/robj/into_robj.rs
@@ -2,7 +2,13 @@ use super::*;
 use crate::single_threaded;
 
 fn str_to_character(s: &str) -> SEXP {
-    unsafe { Rf_mkCharLen(s.as_ptr() as *const raw::c_char, s.len() as i32) }
+    unsafe {
+        if s.is_na() {
+            R_NaString
+        } else {
+            Rf_mkCharLen(s.as_ptr() as *const raw::c_char, s.len() as i32)
+        }
+    }
 }
 
 /// Convert a null to an Robj.
diff --git a/extendr-api/src/robj/iter.rs b/extendr-api/src/robj/iter.rs
index bd95f0a5f..46b575974 100644
--- a/extendr-api/src/robj/iter.rs
+++ b/extendr-api/src/robj/iter.rs
@@ -215,15 +215,17 @@ impl StrIter {
 fn str_from_strsxp<'a>(sexp: SEXP, index: isize) -> &'a str {
     unsafe {
         if index < 0 || index >= Rf_xlength(sexp) {
-            ""
+            na_str()
         } else {
             let charsxp = STRING_ELT(sexp, index);
-            if TYPEOF(charsxp) == CHARSXP as i32 {
+            if charsxp == R_NaString {
+                na_str()
+            } else if TYPEOF(charsxp) == CHARSXP as i32 {
                 let ptr = R_CHAR(charsxp) as *const u8;
                 let slice = std::slice::from_raw_parts(ptr, Rf_xlength(charsxp) as usize);
                 std::str::from_utf8_unchecked(slice)
             } else {
-                "error: not a char"
+                na_str()
             }
         }
     }
@@ -353,6 +355,15 @@ impl Robj {
     /// assert_eq!(factor.as_integer_vector().unwrap(), vec![1, 2, 3, 3]);
     /// assert_eq!(factor.as_str_iter().unwrap().collect::<Vec<_>>(), vec!["abcd", "def", "fg", "fg"]);
     /// assert_eq!(factor.as_str_iter().unwrap().collect::<Vec<_>>(), vec!["abcd", "def", "fg", "fg"]);
+    ///
+    /// let obj = Robj::from(vec![Some("a"), Some("b"), None]);
+    /// assert_eq!(obj.as_str_iter().unwrap().map(|s| s.is_na()).collect::<Vec<_>>(), vec![false, false, true]);
+    ///
+    /// let obj = Robj::from(vec!["a", "b", na_str()]);
+    /// assert_eq!(obj.as_str_iter().unwrap().map(|s| s.is_na()).collect::<Vec<_>>(), vec![false, false, true]);
+    ///
+    /// let obj = Robj::from(vec!["a", "b", "NA"]);
+    /// assert_eq!(obj.as_str_iter().unwrap().map(|s| s.is_na()).collect::<Vec<_>>(), vec![false, false, false]);
     /// ```
     pub fn as_str_iter(&self) -> Option<StrIter> {
         let i = 0;
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"
+            }
           }
-