Repository: extendr/extendr
Pull request #115 (2021-01-07 11:57:14)

Let CI fail when the build/test fails
As discussed on https://github.com/extendr/extendr/pull/112#issuecomment-755836852, we need to check the status (`$?`) and throw an error manually here.


Patch:
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()))
         })


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"
+            }
           }
-