diff --git a/extensions/positron-r/amalthea/crates/harp/src/object.rs b/extensions/positron-r/amalthea/crates/harp/src/object.rs index 4c8a18139368..60b7d12cb079 100644 --- a/extensions/positron-r/amalthea/crates/harp/src/object.rs +++ b/extensions/positron-r/amalthea/crates/harp/src/object.rs @@ -101,6 +101,7 @@ unsafe fn unprotect(cell: SEXP) { } +#[derive(Debug)] pub struct RObject { pub sexp: SEXP, pub cell: SEXP, @@ -214,15 +215,44 @@ impl From for RObject { } } -impl From> for RObject { - fn from(value: Vec) -> Self { +pub trait ToCharSxp { + fn to_charsxp(&self) -> SEXP; +} + +impl ToCharSxp for &str { + fn to_charsxp(&self) -> SEXP { + unsafe { + /* + Rf_mkCharLenCE() will take care of allocating a nul terminated + string on the C side, so we don't need to worry about this here + + c = allocCharsxp(len); + memcpy(CHAR_RW(c), name, len); + + The only caveat is that this will error() if self embeds a nul + + rust strings being utf8, we can use cetype_t_CE_UTF8 and skip + worrying about various problems in Rf_mkCharLenCE() + */ + Rf_mkCharLenCE(self.as_ptr() as *mut c_char, self.len() as i32, cetype_t_CE_UTF8) + } + } +} + +impl ToCharSxp for String { + fn to_charsxp(&self) -> SEXP { + self.as_str().to_charsxp() + } +} + +impl From<&[S]> for RObject where S : ToCharSxp { + fn from(value: &[S]) -> Self { unsafe { let n = value.len() as isize; let vector = Rf_protect(Rf_allocVector(STRSXP, n)); for i in 0..n { let string = value.get_unchecked(i as usize); - let element = Rf_mkCharLenCE(string.as_ptr() as *mut c_char, n as i32, cetype_t_CE_UTF8); - SET_STRING_ELT(vector, i as R_xlen_t, element); + SET_STRING_ELT(vector, i as R_xlen_t, string.to_charsxp()); } Rf_unprotect(1); return RObject::new(vector); @@ -230,6 +260,17 @@ impl From> for RObject { } } +impl From<&[S; N]> for RObject where S : ToCharSxp { + fn from(value: &[S; N]) -> Self { + RObject::from(&value[..]) + } +} + +impl From> for RObject where S : ToCharSxp { + fn from(value: Vec) -> Self { + RObject::from(&value[..]) + } +} /// Convert RObject into other types. @@ -347,3 +388,74 @@ impl TryFrom for HashMap { } } } + +#[cfg(test)] +mod tests { + use libR_sys::{STRING_ELT, R_NaString}; + + use crate::{r_test, r_string, protect, utils::CharSxpEq}; + + use super::RObject; + + #[test] + #[allow(non_snake_case)] + fn test_eq_charsxp() {r_test! { + let mut protect = protect::RProtect::new(); + let r_string = protect.add(r_string!("Apple")); + let apple = STRING_ELT(r_string, 0); + + assert!("Apple".eq_charsxp(apple)); + assert!(String::from("Apple").eq_charsxp(apple)); + + assert!(!"Apple".eq_charsxp(R_NaString)); + assert!(!String::from("Apple").eq_charsxp(R_NaString)); + }} + + #[test] + #[allow(non_snake_case)] + fn test_RObject_from_Vec_str() { r_test! { + let expected = ["Apple", "Orange", "한"]; + + // RObject from &[&str; 3] + let r_strings = RObject::from(&expected); + assert_eq!(r_strings, expected); // [&str] + assert_eq!(r_strings, expected[..]); // [&str; const N] + assert_eq!(r_strings, expected.to_vec()); // Vec<&str> + + // RObject from &[&str] + let r_strings = RObject::from(&expected[..]); + assert_eq!(r_strings, expected); // [&str] + assert_eq!(r_strings, expected[..]); // [&str; const N] + assert_eq!(r_strings, expected.to_vec()); // Vec<&str> + + // RObject from Vec<&str> + let r_strings = RObject::from(expected.to_vec()); + assert_eq!(r_strings, expected); // [&str] + assert_eq!(r_strings, expected[..]); // [&str; const N] + assert_eq!(r_strings, expected.to_vec()); // Vec<&str> + }} + + #[test] + #[allow(non_snake_case)] + fn test_RObject_from_Vec_String() { r_test! { + let expected = [String::from("Apple"), String::from("Orange"), String::from("한")]; + + // RObject from &[String; 3] + let r_strings = RObject::from(&expected); + assert_eq!(r_strings, expected[..]); // [String] + assert_eq!(r_strings, expected); // [String; const N] + assert_eq!(r_strings, expected.to_vec()); // Vec + + // RObject from &[String; 3] + let r_strings = RObject::from(&expected[..]); + assert_eq!(r_strings, expected[..]); // [String] + assert_eq!(r_strings, expected); // [String; const N] + assert_eq!(r_strings, expected.to_vec()); // Vec + + // RObject from Vec + let r_strings = RObject::from(expected.to_vec()); + assert_eq!(r_strings, expected[..]); // [String] + assert_eq!(r_strings, expected); // [String; const N] + assert_eq!(r_strings, expected.to_vec()); // Vec + }} +} diff --git a/extensions/positron-r/amalthea/crates/harp/src/utils.rs b/extensions/positron-r/amalthea/crates/harp/src/utils.rs index 3eec9dc501ad..a32622e90b8e 100644 --- a/extensions/positron-r/amalthea/crates/harp/src/utils.rs +++ b/extensions/positron-r/amalthea/crates/harp/src/utils.rs @@ -48,6 +48,58 @@ pub unsafe fn r_assert_length(object: SEXP, expected: u32) -> Result { Ok(actual) } +pub trait CharSxpEq { + fn eq_charsxp(&self, s: SEXP) -> bool; +} + +impl CharSxpEq for &str { + fn eq_charsxp(&self, s: SEXP) -> bool { + unsafe { + s != R_NaString && (*self).eq(CStr::from_ptr(R_CHAR(s)).to_str().unwrap()) + } + } +} + +impl CharSxpEq for String { + fn eq_charsxp(&self, s: SEXP) -> bool { + (&self[..]).eq_charsxp(s) + } +} + +impl PartialEq<[S]> for RObject where S : CharSxpEq { + fn eq(&self, other: &[S]) -> bool { + unsafe { + let object = self.sexp; + if r_typeof(object) != STRSXP { + return false; + } + + let n = Rf_xlength(object) as isize; + if n != other.len() as isize { + return false; + } + for i in 0..n { + if !other.get_unchecked(i as usize).eq_charsxp(STRING_ELT(object, i)) { + return false; + } + } + true + } + } +} + +impl PartialEq<[S; N]> for RObject where S : CharSxpEq { + fn eq(&self, other: &[S; N]) -> bool { + self.eq(&other[..]) + } +} + +impl PartialEq> for RObject where S : CharSxpEq { + fn eq(&self, other: &Vec) -> bool { + self.eq(&other[..]) + } +} + pub unsafe fn r_typeof(object: SEXP) -> u32 { TYPEOF(object) as u32 }