Skip to content

Commit

Permalink
Support complex (#122)
Browse files Browse the repository at this point in the history
  • Loading branch information
yutannihilation authored Mar 27, 2024
1 parent aa1ae87 commit 0f75024
Show file tree
Hide file tree
Showing 23 changed files with 457 additions and 16 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/R-CMD-check.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ jobs:
crate-type = ["staticlib"]
[dependencies]
savvy = { path = "../savvy" }
savvy = { path = "../savvy", features = ["complex"] }
savvy-ffi = { path = "../savvy/savvy-ffi" }
[workspace.package]
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/docs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ jobs:
- uses: dtolnay/rust-toolchain@nightly

- name: Build API Reference
run: cargo doc --no-deps
run: cargo doc --no-deps --features complex
env:
RUSTDOCFLAGS: "--enable-index-page -Zunstable-options"

Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/wasm_and_arm64.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ jobs:
crate-type = ["staticlib"]
[dependencies]
savvy = { path = "../savvy" }
savvy = { path = "../savvy", features = ["complex"] }
savvy-ffi = { path = "../savvy/savvy-ffi" }
[workspace.package]
Expand Down
3 changes: 2 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,8 @@ impl Person {
user-defined struct can be used more flexibly than before. Please refer to
[the "Struct" section of the
guide](https://yutannihilation.github.io/savvy/guide/10_struct.html)

* An experimental support on complex is added under `compex` feature flag.
`ComplexSexp` and `OwnedComplexSexp` are the corresponding Rust types.
* `OwnedIntegerSexp` and etc now have `set_na(i)` method for shorthand of
`set_elt(i, T::na())`. This is particularly useful for `OwnedLogicalSexp`
because its setter interface `set_elt()` only accepts `bool` and no missing
Expand Down
10 changes: 10 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,20 @@ rust-version = "1.64.0"
savvy-ffi = { version = "0.3.0", path = "./savvy-ffi" }
savvy-macro = { version = "0.3.0", path = "./savvy-macro" }
once_cell = "1"
num-complex = { version = "0.4.5", optional = true }

[features]
default = []

# Support complex
complex = ["num-complex", "savvy-ffi/complex"]

[build-dependencies]
cc = "1"

[package.metadata.docs.rs]
features = ["complex"]

[workspace.metadata.release]
tag = false # do not create tags for individual crates (e.g. "savvy-cli-v0.2.5")

Expand Down
15 changes: 15 additions & 0 deletions R-package/R/wrappers.R
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,21 @@ set_attr_int <- function(attr, value) {
}


new_complex <- function(size) {
.Call(new_complex__impl, size)
}


first_complex <- function(x) {
.Call(first_complex__impl, x)
}


abs_complex <- function(x) {
.Call(abs_complex__impl, x)
}


scalar_input_int <- function(x) {
invisible(.Call(scalar_input_int__impl, x))
}
Expand Down
18 changes: 18 additions & 0 deletions R-package/src/init.c
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,21 @@ SEXP set_attr_int__impl(SEXP attr, SEXP value) {
return handle_result(res);
}

SEXP new_complex__impl(SEXP size) {
SEXP res = new_complex(size);
return handle_result(res);
}

SEXP first_complex__impl(SEXP x) {
SEXP res = first_complex(x);
return handle_result(res);
}

SEXP abs_complex__impl(SEXP x) {
SEXP res = abs_complex(x);
return handle_result(res);
}

SEXP scalar_input_int__impl(SEXP x) {
SEXP res = scalar_input_int(x);
return handle_result(res);
Expand Down Expand Up @@ -339,6 +354,9 @@ static const R_CallMethodDef CallEntries[] = {
{"set_names_int__impl", (DL_FUNC) &set_names_int__impl, 0},
{"set_dim_int__impl", (DL_FUNC) &set_dim_int__impl, 0},
{"set_attr_int__impl", (DL_FUNC) &set_attr_int__impl, 2},
{"new_complex__impl", (DL_FUNC) &new_complex__impl, 1},
{"first_complex__impl", (DL_FUNC) &first_complex__impl, 1},
{"abs_complex__impl", (DL_FUNC) &abs_complex__impl, 1},
{"scalar_input_int__impl", (DL_FUNC) &scalar_input_int__impl, 1},
{"scalar_input_usize__impl", (DL_FUNC) &scalar_input_usize__impl, 1},
{"scalar_input_real__impl", (DL_FUNC) &scalar_input_real__impl, 1},
Expand Down
2 changes: 1 addition & 1 deletion R-package/src/rust/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ edition = "2021"
crate-type = ["staticlib"]

[dependencies]
savvy = { path = "../../../" }
savvy = { path = "../../../", features = ["complex"] }
savvy-ffi = { path = "../../../savvy-ffi" }

[workspace]
3 changes: 3 additions & 0 deletions R-package/src/rust/api.h
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@ SEXP set_class_int(void);
SEXP set_names_int(void);
SEXP set_dim_int(void);
SEXP set_attr_int(SEXP attr, SEXP value);
SEXP new_complex(SEXP size);
SEXP first_complex(SEXP x);
SEXP abs_complex(SEXP x);
SEXP scalar_input_int(SEXP x);
SEXP scalar_input_usize(SEXP x);
SEXP scalar_input_real(SEXP x);
Expand Down
28 changes: 28 additions & 0 deletions R-package/src/rust/src/complex.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
use savvy::savvy;
use savvy::NotAvailableValue;

#[savvy]
fn new_complex(size: usize) -> savvy::Result<savvy::Sexp> {
savvy::OwnedComplexSexp::new(size)?.into()
}

#[savvy]
fn first_complex(x: savvy::ComplexSexp) -> savvy::Result<savvy::Sexp> {
let x_first = x.as_slice()[0];
x_first.try_into()
}

#[savvy]
fn abs_complex(x: savvy::ComplexSexp) -> savvy::Result<savvy::Sexp> {
let mut out = savvy::OwnedRealSexp::new(x.len())?;

for (i, c) in x.iter().enumerate() {
if !c.is_na() {
out[i] = (c.re * c.re + c.im * c.im).sqrt();
} else {
out.set_na(i)?;
}
}

out.into()
}
18 changes: 15 additions & 3 deletions R-package/src/rust/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@ pub use init_vectors::*;
mod function;
pub use function::*;

mod complex;
pub use complex::*;

use savvy::{r_print, savvy, OwnedListSexp};

use savvy::{
Expand Down Expand Up @@ -229,8 +232,14 @@ fn print_list(x: ListSexp) -> savvy::Result<()> {
.join(", ")
)
}
TypedSexp::String(x) => {
format!("character [{}]", x.iter().collect::<Vec<&str>>().join(", "))
TypedSexp::Complex(x) => {
format!(
"complex [{}]",
x.iter()
.map(|r| format!("{}+{}i", r.re, r.im))
.collect::<Vec<String>>()
.join(", ")
)
}
TypedSexp::Logical(x) => {
format!(
Expand All @@ -241,11 +250,14 @@ fn print_list(x: ListSexp) -> savvy::Result<()> {
.join(", ")
)
}
TypedSexp::String(x) => {
format!("character [{}]", x.iter().collect::<Vec<&str>>().join(", "))
}
TypedSexp::List(_) => "list".to_string(),
TypedSexp::Null(_) => "NULL".to_string(),
TypedSexp::ExternalPointer(_) => "external pointer".to_string(),
TypedSexp::Function(_) => "function".to_string(),
TypedSexp::Other(_) => "Unsupported".to_string(),
_ => "Unsupported".to_string(),
};

let name = if k.is_empty() { "(no name)" } else { k };
Expand Down
5 changes: 5 additions & 0 deletions R-package/tests/testthat/test-complex.R
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
test_that("complex works", {
expect_equal(new_complex(3L), rep(0+0i, 3))
expect_equal(first_complex(1:3 + 1i * (3:1)), 1+3i)
expect_equal(abs_complex(c(3+4i, NA, 1+1i)), c(5, NA, sqrt(2)))
})
3 changes: 3 additions & 0 deletions book/src/02_key_ideas.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@ and the latter, owned SEXP, is writable. Here's the list:
| `STRSXP` (character) | `StringSexp` | `OwnedStringSexp` |
| `VECSXP` (list) | `ListSexp` | `OwnedListSexp` |
| `EXTPTRSXP` (external pointer) | `ExternalPointerSexp` | n/a |
| `CPLXSXP` (complex)[^1] | `ComplexSexp` | `OwnedComplexSexp` |

[^1]: Complex is optionally supported under feature flag `complex`

You might wonder why this is needed when we can just use `mut` to distinguish
the difference of mutability. I mainly had two motivations for this:
Expand Down
25 changes: 24 additions & 1 deletion book/src/08_atomic_types.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# Integer, Real, String, And Bool
# Integer, Real, String, Bool, And Complex

## Integer and real

Expand Down Expand Up @@ -149,3 +149,26 @@ very unsafe.
[Rf_translateCharUTF8]: https://github.com/wch/r-source/blob/c3423d28830acbbbf7b38daa58f436fb06d91381/src/main/sysutils.c#L1284-L1296


## Complex

Complex is optionally supported under feature flag `complex`. If it's enabled,
you can use `ComplexSexp` and `OwnedComplexSexp` to use a complex vector for
input or output, and you can extract the slice of `num_complex::Complex64` from
it.

```rust
#[savvy]
fn abs_complex(x: savvy::ComplexSexp) -> savvy::Result<savvy::Sexp> {
let mut out = savvy::OwnedRealSexp::new(x.len())?;

for (i, c) in x.iter().enumerate() {
if !c.is_na() {
out[i] = (c.re * c.re + c.im * c.im).sqrt();
} else {
out.set_na(i)?;
}
}

out.into()
}
```
2 changes: 1 addition & 1 deletion book/src/SUMMARY.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
- [Handling Vector Output](./05_output.md)
- [Handling Scalars](./06_scalar.md)
- [Type-specific Topics]()
- [Integer, Real, String, And Bool](./08_atomic_types.md)
- [Integer, Real, String, Bool, And Complex](./08_atomic_types.md)
- [List](./09_list.md)
- [Struct](./10_struct.md)
- [Error-handling](./11_error.md)
Expand Down
4 changes: 2 additions & 2 deletions savvy-bindgen/src/savvy_fn.rs
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ impl SavvyInputType {
let ty_str = type_ident.to_string();
match ty_str.as_str() {
// Owned-types are not allowed for the input
"OwnedIntegerSexp" | "OwnedRealSexp" | "OwnedLogicalSexp"
"OwnedIntegerSexp" | "OwnedRealSexp" | "OwnedComplexSexp" | "OwnedLogicalSexp"
| "OwnedStringSexp" | "OwnedListSexp" => {
let msg = format!(
"`Owned-` types are not allowed here. Did you mean `{}`?",
Expand All @@ -66,7 +66,7 @@ impl SavvyInputType {
}

// Read-only types
"Sexp" | "IntegerSexp" | "RealSexp" | "LogicalSexp" | "StringSexp"
"Sexp" | "IntegerSexp" | "RealSexp" | "ComplexSexp" | "LogicalSexp" | "StringSexp"
| "ListSexp" | "FunctionSexp" => Ok(Self {
category: SavvyInputTypeCategory::SexpWrapper,
ty_orig: ty.clone(),
Expand Down
7 changes: 7 additions & 0 deletions savvy-ffi/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,10 @@ repository.workspace = true

[package.metadata.dist]
dist = false

[dependencies]
num-complex = { version = "0.4.5", optional = true }

[features]
default = []
complex = ["num-complex"]
25 changes: 25 additions & 0 deletions savvy-ffi/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,31 @@ extern "C" {
pub fn Rf_isReal(s: SEXP) -> Rboolean;
}

// Complex
//
// Since the representation of Rcomplex matches num_complex's Compplex64, use it
// directly. Note that num-complex's docment warns as following and this seems
// the case of passing as a value.
//
// Note that `Complex<F>` where `F` is a floating point type is **only**
// memory layout compatible with C’s complex types, **not** necessarily
// calling convention compatible. This means that for FFI you can only pass
// `Complex<F>` behind a pointer, not as a value.
// (https://docs.rs/num-complex/latest/num_complex/struct.Complex.html#representation-and-foreign-function-interface-compatibility)
//
// While it's true it's not guaranteed to be safe, in actual, no problem has
// benn found so far, and it's a common attitude to ignore the unsafety.
//
// cf. https://gitlab.com/petsc/petsc-rs/-/issues/1
#[cfg(feature = "complex")]
extern "C" {
pub fn COMPLEX(x: SEXP) -> *mut num_complex::Complex64;
pub fn COMPLEX_ELT(x: SEXP, i: R_xlen_t) -> num_complex::Complex64;
pub fn SET_COMPLEX_ELT(x: SEXP, i: R_xlen_t, v: num_complex::Complex64);
pub fn Rf_ScalarComplex(arg1: num_complex::Complex64) -> SEXP;
pub fn Rf_isComplex(s: SEXP) -> Rboolean;
}

// Logical
extern "C" {
pub fn LOGICAL(x: SEXP) -> *mut ::std::os::raw::c_int;
Expand Down
3 changes: 3 additions & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,9 @@ pub use sexp::real::{OwnedRealSexp, RealSexp};
pub use sexp::string::{OwnedStringSexp, StringSexp};
pub use sexp::{Sexp, TypedSexp};

#[cfg(feature = "complex")]
pub use sexp::complex::{ComplexSexp, OwnedComplexSexp};

pub use unwind_protect::unwind_protect;

// re-export
Expand Down
Loading

0 comments on commit 0f75024

Please sign in to comment.