This document describes how miniextendr converts between R types and Rust types. Conversions are governed by three modes (normal, coerce, strict) and apply to both directions: R-to-Rust (TryFromSexp) and Rust-to-R (IntoR).

See also: miniextendr-api/src/from_r.rs, miniextendr-api/src/into_r.rs, miniextendr-api/src/strict.rs, miniextendr-api/src/coerce.rs


πŸ”—Conversion Modes

πŸ”—Normal Mode (default)

Each Rust type accepts exactly one R type. For example, i32 only accepts INTSXP, f64 only accepts REALSXP. A type mismatch produces an error.

πŸ”—Coerce Mode

Coerced types (like i64, u64, isize, usize, and sub-integer types i8, i16, u16, u32, f32) accept multiple R types: INTSXP, REALSXP, RAWSXP, and LGLSXP. The value is extracted as the R native type, then converted to the target Rust type via TryCoerce. This is the default for these types – no attribute is needed.

πŸ”—Strict Mode (#[miniextendr(strict)])

Only INTSXP and REALSXP are accepted. RAWSXP and LGLSXP are rejected. Additionally, output values that don’t fit in R’s integer range (i32) cause a panic (R error) instead of silently widening to REALSXP (f64).


πŸ”—R-to-Rust Conversions (Input: TryFromSexp)

πŸ”—Native Scalar Types (Normal Mode)

These types require an exact R type match. Length must be 1.

Rust TypeAccepted R TypeOn NAOn Type Mismatch
i32INTSXPError (SexpError::Na): use Option<i32> for NAError
f64REALSXPReturns NA_real_ (specific NaN)Error
u8RAWSXPNo NA concept in rawError
RcomplexCPLXSXPReturns Rcomplex { r: NA_real_, i: NA_real_ }Error
boolLGLSXPError (NA is not true/false)Error
RbooleanLGLSXPError (NA not representable)Error
RLogicalLGLSXPReturns RLogical::NaError
StringSTRSXPError (NA_character_)Error
&strSTRSXPError (NA_character_)Error

πŸ”—Option Wrappers (Normal Mode)

Option<T> maps NA to None and NULL to None:

Rust TypeAccepted R TypeOn NAOn NULL
Option<i32>INTSXPNoneNone
Option<f64>REALSXPNoneNone
Option<u8>RAWSXPSome(val) (raw has no NA)None
Option<Rcomplex>CPLXSXPNoneNone
Option<bool>LGLSXPNoneNone
Option<Rboolean>LGLSXPNoneNone
Option<String>STRSXPNoneNone

πŸ”—Coerced Scalar Types (Multi-Source)

These types accept INTSXP, REALSXP, RAWSXP, and LGLSXP:

Rust TypeINTSXPREALSXPRAWSXPLGLSXPSTRSXP
i8Narrow i32->i8f64->i8 (reject frac/NaN)u8->i8logical->i32->i8Error
i16Narrow i32->i16f64->i16 (reject frac/NaN)u8->i16logical->i32->i16Error
u16i32->u16 (reject neg)f64->u16 (reject frac/neg/NaN)u8->u16logical->i32->u16Error
u32i32->u32 (reject neg)f64->u32 (reject frac/neg/NaN)u8->u32logical->i32->u32Error
f32i32 as f32f64 as f32u8 as f32logical as f32Error
i64Widen i32->i64f64->i64 (reject frac/NaN/Inf)u8->i64logical->i32->i64Error
u64i32->u64 (reject neg)f64->u64 (reject frac/neg/NaN)u8->u64logical->i32->u64Error
isizeWiden i32->isizef64->i64->isize (reject frac)u8->isizelogical->isizeError
usizei32->usize (reject neg)f64->u64->usize (reject frac/neg)u8->usizelogical->i32->usizeError

Notes on coercion checks:

  • Fractional check: f64 values with a non-zero fractional part are rejected (e.g., 3.14 fails)
  • NaN/Inf: Both are rejected when converting f64 to integer types
  • Range check: Values outside the target type’s range are rejected (e.g., 300 fails for i8)
  • NA propagation: NA_integer_ and NA_real_ produce errors for non-Option types; Option<i64> etc. map NA to None

πŸ”—Strict Mode Scalar Types

Only INTSXP and REALSXP accepted; RAWSXP and LGLSXP are rejected:

Rust TypeINTSXPREALSXPRAWSXPLGLSXP
i64 (strict)Widen i32->i64f64->i64 (reject frac/NaN)PanicPanic
u64 (strict)i32->u64 (reject neg)f64->u64 (reject frac/neg)PanicPanic
isize (strict)Delegates to i64Delegates to i64PanicPanic
usize (strict)Delegates to u64Delegates to u64PanicPanic

πŸ”—Vector Types

Vector conversions (Vec<T>) follow the same source-type rules as scalars:

Rust TypeAccepted R Type(s)Element Behavior
Vec<i32> / &[i32]INTSXP onlyDirect memcpy
Vec<f64> / &[f64]REALSXP onlyDirect memcpy
Vec<u8> / &[u8]RAWSXP onlyDirect memcpy
Vec<bool>LGLSXP onlyEach logical->bool; NA causes error
Vec<String>STRSXP onlyEach CHARSXP->String; NA causes error
Vec<Option<i32>>INTSXP onlyNA_integer_ -> None
Vec<Option<f64>>REALSXP onlyNA_real_ -> None
Vec<Option<bool>>LGLSXP onlyNA_logical -> None
Vec<Option<String>>STRSXP onlyNA_character_ -> None
Vec<i64> (strict)INTSXP or REALSXPPer-element checked coercion; RAWSXP/LGLSXP rejected
Vec<u64> (strict)INTSXP or REALSXPPer-element checked coercion; RAWSXP/LGLSXP rejected

πŸ”—Rust-to-R Conversions (Output: IntoR)

πŸ”—Scalar Types

Rust TypeR Output TypeNotes
i32INTSXPDirect via Rf_ScalarInteger
f64REALSXPDirect via Rf_ScalarReal
u8RAWSXPDirect via Rf_ScalarRaw
boolLGLSXPtrue->1, false->0
RbooleanLGLSXPDirect
RLogicalLGLSXPIncludes NA support
String / &strSTRSXPUTF-8 encoding via Rf_mkCharLenCE
charSTRSXPSingle UTF-8 character as string
()NILSXPReturns R NULL

πŸ”—Widening Scalar Types

Rust TypeR Output TypeNotes
i8, i16, u16INTSXPInfallible widening to i32
f32, u32REALSXPInfallible widening to f64

πŸ”—Smart Scalar Conversion (i64, u64, isize, usize)

These types use a smart conversion strategy: fit in i32 -> INTSXP, otherwise -> REALSXP.

Rust TypeConditionR Output TypeNotes
i64i32::MIN < val <= i32::MAXINTSXPExact representation
i64Otherwise (incl. i32::MIN)REALSXPMay lose precision >2^53
u64val <= i32::MAXINTSXPExact representation
u64val > i32::MAXREALSXPMay lose precision >2^53
isizeDelegates to i64INTSXP or REALSXPSame rules as i64
usizeDelegates to u64INTSXP or REALSXPSame rules as u64

Why i32::MIN is excluded from INTSXP: In R, i32::MIN (-2147483648) is NA_integer_. Returning it as INTSXP would create an unintended NA value.

πŸ”—Strict Output Conversion

With #[miniextendr(strict)], large integer types panic instead of falling back to REALSXP:

Rust TypeConditionStrict Behavior
i64Fits in (i32::MIN, i32::MAX]INTSXP (same as normal)
i64Outside rangePanic (R error)
u64val <= i32::MAXINTSXP (same as normal)
u64val > i32::MAXPanic (R error)
Vec<i64>All elements fitINTSXP vector
Vec<i64>Any element outside rangePanic (R error)

πŸ”—Option Types (NA Mapping)

Rust TypeSome(val)None
Option<i32>INTSXP scalarNA_integer_
Option<f64>REALSXP scalarNA_real_
Option<bool>LGLSXP scalarNA_logical
Option<Rboolean>LGLSXP scalarNA_logical
Option<String>STRSXP scalarNA_character_
Option<&str>STRSXP scalarNA_character_
Option<Vec<T>>R vectorNULL (R_NilValue)
Option<HashMap<...>>Named listNULL (R_NilValue)

πŸ”—Vector Types

Rust TypeR Output TypeNotes
Vec<i32> / &[i32]INTSXPBulk memcpy
Vec<f64> / &[f64]REALSXPBulk memcpy
Vec<u8> / &[u8]RAWSXPBulk memcpy
Vec<bool> / &[bool]LGLSXPElement-wise bool as i32
Vec<String>STRSXPElement-wise CHARSXP creation
Vec<Option<i32>>INTSXPNone -> NA_integer_
Vec<Option<f64>>REALSXPNone -> NA_real_
Vec<Option<bool>>LGLSXPNone -> NA_logical
Vec<Option<String>>STRSXPNone -> NA_character_
Vec<Option<&str>>STRSXPNone -> NA_character_ (borrowed strings, mirrors Vec<Option<String>>)

Vec<Option<scalar>> lands as a typed R vector with NA sentinels. Vec<Option<C>> for collection element types lands as a list-column with NULL for None (see Collection Types below).

πŸ”—Smart Vector Conversion (Vec of large integers)

Vec<i64>, Vec<u64>, Vec<isize>, Vec<usize> check whether all elements fit in i32. If yes, the entire vector is INTSXP; otherwise, the entire vector is REALSXP.

Rust TypeAll Fit in i32?R Output Type
Vec<i64>Yes (all in (i32::MIN, i32::MAX])INTSXP
Vec<i64>No (any element outside)REALSXP
Vec<u64>Yes (all <= i32::MAX)INTSXP
Vec<u64>NoREALSXP

πŸ”—Collection Types

Rust TypeR Output Type
HashMap<String, V>Named list (VECSXP)
BTreeMap<String, V>Named list (VECSXP)
HashSet<T> / BTreeSet<T>Vector (order may vary for HashSet)
VecDeque<T>Vector (converted to Vec first)
BinaryHeap<T>Vector (arbitrary order)
Vec<Vec<T>>List of vectors (VECSXP)
Vec<&[T]> / Vec<&[String]>List of vectors (VECSXP), borrowed slices
(A, B, ...)Unnamed list (VECSXP), up to 8 elements (IntoR only, no TryFromSexp)

πŸ”—Vec<Option<C>> for collection element types

Vec<Option<C>> where C is a collection lands as a VECSXP list-column. Some(c) becomes the element’s normal IntoR output; None becomes R_NilValue (NULL). The wrap is required by enum DataFrameRow align codegen, which represents every column as Vec<Option<T>> so non-payload variants can NA-fill.

Rust TypeR Output TypeNone Behavior
Vec<Option<Vec<T>>> (T: RNativeType)VECSXP of typed vectorsNULL
Vec<Option<Vec<String>>>VECSXP of character vectorsNULL
Vec<Option<HashSet<T>>> (T: RNativeType + Eq + Hash)VECSXP of typed vectorsNULL
Vec<Option<HashSet<String>>>VECSXP of character vectorsNULL
Vec<Option<BTreeSet<T>>> (T: RNativeType + Ord)VECSXP of sorted typed vectorsNULL
Vec<Option<BTreeSet<String>>>VECSXP of sorted character vectorsNULL
Vec<Option<HashMap<String, V>>> (V: IntoR)VECSXP of named listsNULL
Vec<Option<BTreeMap<String, V>>> (V: IntoR)VECSXP of named listsNULL
Vec<Option<&[T]>> (T: RNativeType)VECSXP of typed vectorsNULL
Vec<Option<&[String]>>VECSXP of character vectorsNULL
Vec<Option<Vec<K>>> (K: RNativeType, keys column)VECSXP of typed vectorsNULL
Vec<Option<Vec<V>>> (V: IntoR, values column)VECSXPNULL
PathBufSTRSXP (lossy UTF-8 conversion)
OsStringSTRSXP (lossy UTF-8 conversion)

The Vec<Option<Vec<K>>> and Vec<Option<Vec<V>>> types appear as the companion-struct column types for HashMap<K,V> / BTreeMap<K,V> enum variant fields, expanded to <field>_keys / <field>_values columns by DataFrameRow derive. No new IntoR impls are required beyond those already present.

πŸ”—Nested DataFrameRow enum fields

When an enum variant field is itself a DataFrameRow enum, it flattens into prefixed columns at into_data_frame() time. The companion struct holds Vec<Option<Inner>> and calls Inner::to_dataframe(dense_rows) β†’ into_named_columns() β†’ scatter to full-length column via scatter_column.

ModeRust field annotationCompanion struct typeR column(s)NA behavior
Flatten (default)(none) β€” inner must impl DataFrameRowVec<Option<Inner>><field>_variant (STRSXP) + all of Inner’s other columns, each prefixed with <field>_NA in all prefixed columns for absent-variant rows
as_factor#[dataframe(as_factor)] β€” inner must be unit-only (impl UnitEnumFactor)Vec<Option<Inner>><field> (INTSXP factor)NA_integer_ for absent-variant rows
as_list#[dataframe(as_list)]Vec<Option<Inner>><field> (VECSXP list-column)NULL for absent-variant rows

Notes:

  • Factor levels: emitted in enum variant declaration order; levels(df$field) returns all variants regardless of which appear in the data.
  • Inner tag: use #[dataframe(tag = "variant")] on the inner enum so the discriminant column is <outer_field>_variant (single underscore). A leading underscore on the inner tag (e.g. tag = "_variant") produces a double underscore in the outer column name.
  • Auto-emit UnitEnumFactor: #[derive(DataFrameRow)] on a unit-only enum also emits UnitEnumFactor + IntoR (factor SEXP) automatically. Generic unit enums (with type parameters) are excluded from auto-emission; implement UnitEnumFactor manually if needed.
  • Struct fields: the same flatten / as_factor / as_list modes apply to struct-typed variant fields that implement DataFrameRow.

πŸ”—Result and Error Types

Rust TypeOk(val)Err(e)
Result<T, E: Debug> (default)T::into_sexp()Panic -> R error
Result<T, E: Display> (unwrap_in_r)T::into_sexp()list(error = msg)
Result<T, ()>T::into_sexp()NULL (R_NilValue)

πŸ”—Date / Time Conversions (Feature-Gated)

πŸ”—time feature

Enabled with features = ["time"].

Rust TypeR TypeNotes
OffsetDateTimePOSIXctUTC only; tzone attr set to "UTC"
DateDateDays since 1970-01-01
DurationdifftimeSeconds unit

πŸ”—jiff feature

Enabled with features = ["jiff"]. Bundles IANA timezone database (tzdb-bundle-always).

Rust TypeR TypeNotes
TimestampPOSIXct (UTC)Nanosecond precision; floor-based fractional-second split for correctness on negative timestamps
ZonedPOSIXct + tzone attrIANA timezone name preserved; unknown tz on input β†’ error (no UTC fallback)
civil::DateDateDays since 1970-01-01 via Span::try_days
SignedDurationdifftime (secs)Nanosecond-precision duration stored as f64 seconds
Vec<Timestamp> (ALTREP)POSIXctJiffTimestampVec; elements materialized on access via Arc<Vec<Timestamp>>
Vec<Zoned> (ALTREP, single-tz strict)POSIXct + tzone attrJiffZonedVec; construction-time check rejects heterogeneous timezones

Adapter traits (wrapping via ExternalPtr): RTimestamp, RDate, RZoned, RSignedDuration, RSpan, RDateTime, RTime

vctrs rcrd constructors (requires vctrs feature): span_vec_to_rcrd, zoned_vec_to_rcrd, datetime_vec_to_rcrd, time_vec_to_rcrd


πŸ”—Raw/Bytemuck Conversions (Feature-Gated)

Enabled with features = ["raw_conversions"]. Uses R’s RAWSXP for binary POD data.

WrapperDirectionFormatType Tag
Raw<T>BothHeaderless bytesNo
RawSlice<T>BothHeaderless byte sequenceNo
RawTagged<T>Both16-byte header + bytesYes (mx_raw_type attr)
RawSliceTagged<T>Both16-byte header + byte sequenceYes (mx_raw_type attr)

Safety checks: length validation, alignment (copy if misaligned), magic/version validation (tagged only), type name matching (tagged only).


πŸ”—Special Values Quick Reference

R ValueRust RepresentationNotes
NA_integer_i32::MIN (-2147483648)Excluded from valid i32 range; inbound NA produces SexpError::Na on i32 (use Option<i32> to receive NA)
NA_real_0x7FF0000000000007A2 (specific NaN bit pattern)Distinguished from ordinary f64::NAN by bit-exact comparison; ALTREP no_na/sum/min/max treat only this bit pattern as NA
NA_logical_i32::MINSame sentinel as NA_integer_
NA_character_R_NaString CHARSXPMapped to None in Option<String>
NaNf64::NANNot the same as NA_real_; passes through as valid f64
Inf / -Inff64::INFINITY / f64::NEG_INFINITYValid f64 values; rejected when coercing to integers
NULLR_NilValueMapped to None in Option<T>; () produces NULL

πŸ”—Cookbook: Common Conversion Recipes

πŸ”—Vec<Option<i64>>: how it converts to R

Each element uses the smart i64 conversion. If all Some values fit in i32, the whole vector is INTSXP; otherwise REALSXP. None values become NA_integer_ or NA_real_ accordingly.

#[miniextendr]
fn make_nullable_ids() -> Vec<Option<i64>> {
    vec![Some(1), None, Some(42), Some(i64::MAX)]
    // -> REALSXP because i64::MAX doesn't fit in i32
}

πŸ”—β€œI want to accept either integer or numeric from R”

Use a coerced type (i64, u64, f32), which accepts INTSXP, REALSXP, RAWSXP, and LGLSXP automatically:

#[miniextendr]
fn flexible_input(x: i64) -> i64 {
    x * 2  // works with integer(1) or numeric(1) from R
}

Or use #[miniextendr(strict)] to only accept INTSXP and REALSXP (no raw/logical):

#[miniextendr(strict)]
fn strict_input(x: i64) -> i64 { x * 2 }

πŸ”—β€œI want a named list from R as a HashMap”

use std::collections::HashMap;

#[miniextendr]
fn process_config(config: HashMap<String, f64>) -> f64 {
    config.get("threshold").copied().unwrap_or(0.5)
}

In R: process_config(list(threshold = 0.9, alpha = 0.05))

πŸ”—β€œI want to return NA for missing values”

Wrap in Option. None becomes the appropriate NA:

#[miniextendr]
fn safe_divide(a: f64, b: f64) -> Option<f64> {
    if b == 0.0 { None } else { Some(a / b) }
}

πŸ”—β€œI want to return NULL on failure, not an error”

Use Result<T, ()>:

#[miniextendr]
fn try_parse(s: String) -> Result<i32, ()> {
    s.parse::<i32>().map_err(|_| ())
    // Ok(42) -> 42L in R; Err(()) -> NULL in R
}

πŸ”—β€œI have a struct and want to pass it to R and back”

Use #[miniextendr] on an impl block. The struct is wrapped in an ExternalPtr:

struct Counter { n: i32 }

#[miniextendr]
impl Counter {
    fn new() -> Self { Counter { n: 0 } }
    fn increment(&mut self) { self.n += 1; }
    fn get(&self) -> i32 { self.n }
}

πŸ”—β€œI want to accept R’s ... (dots)”

Use _dots: &Dots as the last parameter:

#[miniextendr]
fn sum_all(x: f64, _dots: &Dots) -> f64 {
    // x is the first argument; _dots captures the rest
    x  // dots are validated but not directly accessible as Rust values
}

For typed dots validation, see DOTS_TYPED_LIST.md.