This document describes the Coerce<R> trait system for converting Rust types to R’s native scalar types.

πŸ”—R’s Native Scalar Types

R has a fixed set of native scalar types that can appear in vectors:

R TypeRust TypeSEXPTYPE
integeri32INTSXP
numeric/doublef64REALSXP
logicalRLogicalLGLSXP
rawu8RAWSXP
complexRcomplexCPLXSXP

Note: RLogical is a newtype over i32 that safely represents R’s logical values (TRUE/FALSE/NA). The coercion traits also work with Rboolean (an enum for the TRUE/FALSE case without NA).

The RNativeType marker trait identifies these types:

pub trait RNativeType: Sized + Copy + 'static {
    const SEXP_TYPE: SEXPTYPE;
}

πŸ”—Core Traits

πŸ”—Coerce<R> - Infallible Coercion

For conversions that always succeed (identity, widening):

pub trait Coerce<R> {
    fn coerce(self) -> R;
}

Scalar implementations:

FromToNotes
i32i32Identity
f64f64Identity
RbooleanRbooleanIdentity
u8u8Identity
RcomplexRcomplexIdentity
i8, i16, u8, u16i32Widening to R integer
f32, i8..u32f64Widening to R real
u8u16, i16, u32Widening
i8i16Widening
u16u32Widening
i32i64, isizeWidening
u8i64, isize, u64, usize, f32Widening
i32f32Lossy (f32 has 24-bit mantissa)
f64f32Lossy narrowing
boolRbooleantrue β†’ TRUE, false β†’ FALSE
booli32true β†’ 1, false β†’ 0
boolf64true β†’ 1.0, false β†’ 0.0
Rbooleani32Direct cast
Option<f64>f64None β†’ NA_real_
Option<i32>i32None β†’ NA_integer_
Option<bool>i32None β†’ NA_LOGICAL
Option<Rboolean>i32None β†’ NA_LOGICAL

Slice/Vec implementations (element-wise):

FromToNotes
&[T]Vec<R>Where T: Copy + Coerce<R>
Vec<T>Vec<R>Where T: Coerce<R>
let slice: &[i8] = &[1, 2, 3];
let vec: Vec<i32> = slice.coerce();  // [1, 2, 3]

let v: Vec<i16> = vec![10, 20, 30];
let result: Vec<f64> = v.coerce();   // [10.0, 20.0, 30.0]

πŸ”—TryCoerce<R> - Fallible Coercion

For conversions that may fail (narrowing, overflow, precision loss):

pub trait TryCoerce<R> {
    type Error;
    fn try_coerce(self) -> Result<R, Self::Error>;
}

pub enum CoerceError {
    Overflow,       // Value out of range
    PrecisionLoss,  // Would lose significant digits
    NaN,            // NaN cannot be converted to integer
    Zero,           // Zero is not allowed (for NonZero* types)
}

Built-in implementations:

FromToFailure Condition
u32, u64, i64, usize, isizei32Value outside i32 range
f64, f32i32NaN, out of range, or has fractional part
i64, u64, isize, usizef64Value outside Β±2^53 (precision loss)
All integers except u8u8Value outside 0..255
i8, i16, i32, i64, u32, u64, usize, isizeu16Value outside 0..65535
i32, i64, u16, u32, u64, usize, isizei16Value outside i16 range
i16, i32, i64, u8, u16, u32, u64, usize, isizei8Value outside i8 range
f64u16, i16, i8NaN, out of range, or has fractional part
f64u32, u64, isize, usizeNaN, out of range, or has fractional part
i32u32, u64, usizeNegative value
i32, Rboolean, RLogicalboolNA or invalid value (LogicalCoerceError)

NonZero conversions (error: CoerceError::Zero or CoerceError::Overflow):

FromToFailure Condition
Same base typeNonZero{I8,I16,...,Usize}Value is zero
i32NonZeroI64, NonZeroIsizeValue is zero
i32NonZeroU32, NonZeroU64, NonZeroUsizeNegative or zero
i32NonZeroI8, NonZeroI16Out of range or zero
i32NonZeroU8, NonZeroU16Out of range or zero

Blanket impl: Coerce<R> automatically implements TryCoerce<R> with Error = Infallible.

Slice coercion: Slices/Vecs get TryCoerce automatically via the blanket impl when elements have Coerce. For fallible element-wise coercion, use manual iteration:

// R integer slice β†’ Rust u16 vec (common use case)
let r_ints: &[i32] = &[1, 100, 1000];
let result: Result<Vec<u16>, _> = r_ints
    .iter()
    .copied()
    .map(TryCoerce::try_coerce)
    .collect();
assert_eq!(result, Ok(vec![1u16, 100, 1000]));

// Failure case - negative values can't become u16
let bad: &[i32] = &[1, -5, 1000];
let result: Result<Vec<u16>, _> = bad
    .iter()
    .copied()
    .map(TryCoerce::try_coerce)
    .collect();
// Err(CoerceError::Overflow) - fails on -5

πŸ”—Trait Bounds

Use Coerce<R> directly in where clauses:

fn process_as_integer<T: Coerce<i32>>(value: T) -> i32 {
    value.coerce()
}

// Works with any type that can infallibly coerce to i32
process_as_integer(42i8);   // i8 β†’ i32
process_as_integer(true);   // bool β†’ i32
process_as_integer(100u16); // u16 β†’ i32

πŸ”—Usage with #[miniextendr]

Use #[miniextendr(coerce)] to enable automatic type coercion for non-R-native parameter types:

// Scalar coercion: R integer (i32) β†’ u16
#[miniextendr(coerce)]
fn process_u16(x: u16) -> i32 {
    x as i32
}

// Vec coercion: R integer vector (&[i32]) β†’ Vec<u16>
#[miniextendr(coerce)]
fn sum_u16_vec(x: Vec<u16>) -> i32 {
    x.iter().map(|&v| v as i32).sum()
}

// Float narrowing: R double (f64) β†’ f32
#[miniextendr(coerce)]
fn process_f32(x: f32) -> f64 {
    x as f64
}

Supported coercions:

Parameter TypeR TypeCoercion
u16, i16, i8integerTryCoerce (overflow β†’ panic)
u32, u64, i64integerTryCoerce (overflow β†’ panic)
f32numericCoerce (may lose precision)
Vec<u16>, Vec<i16>, etc.integer vectorelement-wise TryCoerce
Vec<f32>numeric vectorelement-wise Coerce

Example in R:

# Works - value fits in u16
process_u16(100L)  # Returns 100

# Errors - value doesn't fit in u16
process_u16(-1L)   # Error: coercion to u16 failed: Overflow
process_u16(70000L)  # Error: coercion to u16 failed: Overflow

# Vec coercion
sum_u16_vec(c(1L, 2L, 3L))  # Returns 6
sum_u16_vec(c(1L, -1L, 3L)) # Error: coercion to Vec<u16> failed: Overflow

Combining with other attributes:

#[miniextendr(coerce, invisible)]
fn process_silently(x: u16) -> i32 {
    x as i32  // Returns invisibly
}

πŸ”—Per-Parameter Coercion with #[miniextendr(coerce)]

For selective coercion, add #[miniextendr(coerce)] to individual parameters:

// Only coerce the first parameter
#[miniextendr]
fn process_mixed(#[miniextendr(coerce)] x: u16, y: i32) -> i32 {
    x as i32 + y  // x is coerced from R integer, y is used directly
}

// Coerce multiple specific parameters
#[miniextendr]
fn process_both(#[miniextendr(coerce)] x: u16, #[miniextendr(coerce)] y: i16, z: i32) -> i32 {
    x as i32 + y as i32 + z  // x and y coerced, z is direct R integer
}

// Coerce Vec parameter
#[miniextendr]
fn sum_u16(#[miniextendr(coerce)] values: Vec<u16>, offset: i32) -> i32 {
    values.iter().map(|&v| v as i32).sum::<i32>() + offset
}

Example in R:

# x is coerced to u16, y is used as-is
process_mixed(100L, 5L)  # Returns 105

# Overflow only affects coerced parameter
process_mixed(-1L, 5L)   # Error: coercion to u16 failed

This is useful when you have a mix of R-native types and types that need coercion.

πŸ”—Manual Coercion (Alternative)

For more control, accept R native types and coerce manually:

#[miniextendr]
fn widen_to_real(x: i32) -> f64 {
    x.coerce()  // i32 β†’ f64, always succeeds
}

#[miniextendr]
fn try_narrow(x: f64) -> i32 {
    match TryCoerce::<i32>::try_coerce(x) {
        Ok(v) => v,
        Err(_) => i32::MIN,  // Return NA on failure
    }
}

Helper functions with generic bounds:

fn internal_helper<T: Coerce<i32>>(x: T) -> i32 {
    x.coerce()
}

#[miniextendr]
fn from_i8(x: i8) -> i32 {
    internal_helper(x)  // Concrete type at call site
}

πŸ”—What Doesn’t Work

Generic #[miniextendr] functions:

// THIS DOES NOT COMPILE
#[miniextendr]
fn generic_coerce<T: Coerce<i32>>(x: T) -> i32 {
    x.coerce()
}

Why: The macro generates TryFromSexp::try_from_sexp(arg) which requires knowing the concrete type T at compile time. A trait bound alone doesn’t tell the macro what R type to expect.

πŸ”—No Automatic R-Side Coercion

miniextendr does NOT automatically insert as.integer(), as.numeric(), etc. in generated R wrappers.

πŸ”—Why Not?

R has no scalars - everything is a vector (length-1 slice).

Consider a function that modifies data in place:

#[miniextendr]
fn double_first(x: &mut [i32]) {
    x[0] *= 2;
}
# Without coercion - works correctly
x <- c(1L, 2L, 3L)
double_first(x)
x[1]  # 2L - modified in place βœ“

# With automatic coercion - BROKEN
x <- c(1.0, 2.0, 3.0)  # numeric, not integer
double_first(x)  # If wrapper did as.integer(x), it would create a COPY
x[1]  # Still 1.0 - user's data unchanged! βœ—

Automatic coercion creates copies, silently breaking β€œmodify in place” semantics.

πŸ”—The Correct Approach

  1. Type mismatch = error - Let users see the error and decide how to handle it
  2. Explicit coercion in R - Users call as.integer(x) when they understand the copy implications
  3. Rust-side Coerce - Use the trait for internal conversions and return values
# User handles coercion explicitly
x <- c(1.0, 2.0, 3.0)
x_int <- as.integer(x)  # User knows this is a copy
double_first(x_int)
x_int[1]  # 2L - the copy was modified

πŸ”—Newtype Wrappers with #[derive(RNativeType)]

For newtype wrappers around R native types, use the RNativeType derive macro.

πŸ”—Supported Struct Forms

Both tuple structs and single-field named structs are supported:

use miniextendr_api::RNativeType;

// Tuple struct (most common)
#[derive(Clone, Copy, RNativeType)]
struct UserId(i32);

#[derive(Clone, Copy, RNativeType)]
struct Score(f64);

// Named single-field struct
#[derive(Clone, Copy, RNativeType)]
struct Temperature { celsius: f64 }

πŸ”—Using with Coerce

The derive forwards the inner type’s SEXP_TYPE and dataptr_mut. The newtype can then participate in coercion as a target type:

impl Coerce<UserId> for i32 {
    fn coerce(self) -> UserId {
        UserId(self)
    }
}

let id: UserId = 42.coerce();

πŸ”—Requirements

  • Must be a newtype struct (exactly one field, tuple or named)
  • The inner type must implement RNativeType (i32, f64, RLogical, u8, Rcomplex, or another derived type)
  • Should also derive Copy (required by RNativeType: Copy)

πŸ”—Implementing Coerce for Custom Types

use miniextendr_api::{Coerce, TryCoerce, CoerceError, RNativeType};

// Infallible coercion
impl Coerce<i32> for MyType {
    fn coerce(self) -> i32 {
        self.value as i32
    }
}

// Fallible coercion
impl TryCoerce<i32> for MyOtherType {
    type Error = CoerceError;

    fn try_coerce(self) -> Result<i32, CoerceError> {
        if self.value > i32::MAX as i64 {
            Err(CoerceError::Overflow)
        } else {
            Ok(self.value as i32)
        }
    }
}

πŸ”—Comparison with R’s Coercion

miniextendr’s TryCoerce is stricter than R’s coerceVector(). This is intentional - Rust-idiomatic explicit failure over silent data loss.

ConversionR Behaviorminiextendr Behavior
42.7 β†’ integerTruncates to 42Err(PrecisionLoss)
1e20 β†’ integerNA with warningErr(Overflow)
NaN β†’ integerNAErr(NaN)
300 β†’ raw0 with warningErr(Overflow)
-5 β†’ raw0 with warningErr(Overflow)
NA β†’ raw0 with warningErr(Overflow)

R source reference (src/main/coerce.c):

// IntegerFromReal - just truncates, no fractional check
int IntegerFromReal(double x, int *warn) {
    if (ISNAN(x)) return NA_INTEGER;
    if (x >= INT_MAX+1. || x <= INT_MIN) {
        *warn |= WARN_INT_NA;
        return NA_INTEGER;
    }
    return (int) x;  // Truncates!
}

// coerceToRaw - out of range becomes 0
if (tmp == NA_INTEGER || tmp < 0 || tmp > 255) {
    tmp = 0;
    warn |= WARN_RAW;
}

To match R’s truncation behavior, use as cast after bounds check:

fn r_style_to_int(x: f64) -> i32 {
    if x.is_nan() { return i32::MIN; }  // NA
    if x >= (i32::MAX as f64 + 1.0) || x <= i32::MIN as f64 {
        return i32::MIN;  // NA
    }
    x as i32  // Truncates like R
}

πŸ”—Summary

Use CaseSolution
Convert Rust types internallyCoerce<R> / TryCoerce<R>
Generic helper functionsTrait bounds (Coerce<i32>, Coerce<f64>, etc.)
R β†’ Rust at boundaryExplicit types, no auto-coercion
Rust β†’ R return valuesCoerce<R> works fine
R i32 slice β†’ Rust u16 vecslice.iter().copied().map(TryCoerce::try_coerce).collect()
Mutable slice parametersNever auto-coerce - breaks semantics
Match R’s truncationUse as cast after bounds check

The Coerce<R> trait system provides type-safe conversions within Rust while respecting R’s copy-on-coerce semantics at the language boundary.

πŸ”—Feature Module Coercion Policies

Each optional feature module has its own coercion behavior. This section documents how R values are converted to feature-specific types.

πŸ”—Float-Centric Types

πŸ”—ordered-float Feature

Target TypeAcceptsBehavior
OrderedFloat<f64>R numeric (REALSXP)Direct conversion via TryFromSexp for f64
OrderedFloat<f32>R numeric (REALSXP)Converts f64 β†’ f32 (may lose precision)
Vec<OrderedFloat<T>>R numeric vectorElement-wise conversion

Integer input behavior: R integers are coerced by R’s standard rules when passed to a function expecting numeric. The Coerce trait provides i32 β†’ OrderedFloat<f64> (infallible widening) and i32 β†’ OrderedFloat<f32> as TryCoerce (may fail for large values due to f32 precision limits).

Precision loss: f64 β†’ f32 narrowing uses TryCoerce with PrecisionLoss error when round-trip fails.

πŸ”—rust-decimal Feature

Target TypeAcceptsBehavior
DecimalR numeric (REALSXP) or character (STRSXP)Numeric: fast but may lose precision. String: exact parsing.
Option<Decimal>Same + NANA β†’ None
Vec<Decimal>Numeric/character vectorElement-wise, NA values error

Integer input behavior: R integers are coerced to numeric by R before reaching Rust. The Decimal::from_f64_retain() is used, which may not exactly represent all float values.

Recommended for precision: Use character input for exact decimal values:

# Exact decimal from string
precise <- rust_decimal_from_str("123.456789012345")

# May have floating-point artifacts
approx <- rust_decimal_from_numeric(123.456789012345)

πŸ”—String-Based Types

πŸ”—num-bigint Feature

Target TypeAcceptsBehavior
BigIntR character (STRSXP)Parses string, supports hex (0x), octal (0o), binary (0b)
BigUintR character (STRSXP)Same, but rejects negative values
Vec<BigInt>Character vectorElement-wise, NA values error

Why string-only: R’s numeric types cannot represent arbitrary-precision integers without loss. Even i32 input would lose information for values outside [-2^31, 2^31).

Usage:

# Correct - string input preserves full precision
big <- bigint_from_str("123456789012345678901234567890")

# Also supported
hex <- bigint_from_str("0xDEADBEEF")

πŸ”—uuid Feature

Target TypeAcceptsBehavior
UuidR character (STRSXP)Parses standard UUID formats
Option<Uuid>Same + NANA β†’ None

Accepted formats:

  • Hyphenated: 550e8400-e29b-41d4-a716-446655440000
  • Simple: 550e8400e29b41d4a716446655440000
  • URN: urn:uuid:550e8400-e29b-41d4-a716-446655440000
  • Braced: {550e8400-e29b-41d4-a716-446655440000}

πŸ”—Container Types with Coerced<T, R>

πŸ”—tinyvec Feature

Target TypeAcceptsBehavior
TinyVec<[T; N]> where T: TryFromSexpMatching R vectorDirect element conversion
TinyVec<[Coerced<T, R>; N]>R vector of type RElement-wise coercion via TryCoerce
ArrayVec<T, N>Same patternsFixed-capacity variant

Coerced<T, R> pattern: Wraps each element to apply TryCoerce during conversion:

// Accepts R integer, coerces each element to u16
fn process(values: TinyVec<[Coerced<u16, i32>; 8]>) -> i32 {
    values.iter().map(|c| c.0 as i32).sum()
}

πŸ”—nalgebra Feature

Target TypeAcceptsBehavior
DVector<T>R vectorElement-wise conversion
DVector<Coerced<T, R>>R vector of type RElement-wise coercion
DMatrix<T>R matrixBy-column conversion
DMatrix<Coerced<T, R>>R matrix of type RElement-wise coercion

Matrix coercion example:

// Accepts R integer matrix, coerces to f32 elements
fn process_matrix(m: DMatrix<Coerced<f32, f64>>) -> f64 {
    m.iter().map(|c| c.0 as f64).sum()
}

πŸ”—Time Types

πŸ”—time Feature

Target TypeAcceptsBehavior
DateR Date (numeric with class)Days since 1970-01-01
OffsetDateTimeR POSIXct (numeric with class)Seconds since epoch + timezone
PrimitiveDateTimeR POSIXlt listComponents: year, month, day, etc.
TimeR character (STRSXP)Parses time string
DurationR numeric (REALSXP)Seconds as f64

Note: R Date/POSIXct are stored as numeric internally. The conversion respects R’s epoch (1970-01-01) and timezone handling.

πŸ”—Summary Table: Input Type by Feature

FeaturePrimary R InputAlternativeNotes
ordered-floatnumeric-Wraps f64/f32
rust-decimalnumericcharacterString for exact values
num-bigintcharacter-String only (precision)
uuidcharacter-UUID string formats
timeDate/POSIXct/numericcharacterDepends on target type
tinyvecAny via CoercedDirectFlexible with wrapper
nalgebraAny via CoercedDirectFlexible with wrapper

πŸ”—Error Handling Patterns

Strict (default): Most features reject invalid input with errors:

// Fails for negative values
fn positive_only(x: BigUint) -> String { ... }

Lossy (explicit): Some features provide both strict and lossy paths:

// rust_decimal: exact vs approximate
let exact = Decimal::from_str("1.1")?;           // Exact
let approx = Decimal::from_f64_retain(1.1)?;    // May have artifacts

With Coerced<T, R>: Coercion errors become function errors:

// Returns Err if any element overflows u16
fn coerced_sum(values: Vec<Coerced<u16, i32>>) -> Result<u32, CoerceError> { ... }