miniextendr_api/dataframe.rs
1//! R `data.frame` views, rooted Rust-built handles, and conversion traits.
2//!
3//! [`DataFrame`] is a cheap `Copy` view over a validated `data.frame` SEXP;
4//! [`BuiltDataFrame`] is the owned, GC-rooted handle returned by every
5//! Rust-side constructor. Together they serve every direction:
6//!
7//! - **build** (Rust → R): [`IntoDataFrame::into_dataframe`] / `into_dataframe_par` (`feature = "rayon"`),
8//! - **read** (R → Rust): [`DataFrame::column`] / [`FromDataFrame::from_dataframe`],
9//! - **edit** (post-assembly): [`DataFrame::rename`] / [`DataFrame::drop`] / [`DataFrame::select`] / …
10//! — the new-frame producers return an owned, GC-rooted [`BuiltDataFrame`] (#1247),
11//! so constructor → edit chains stay rooted at every link.
12//!
13//! The trait family mirrors the crate's existing [`IntoR`] /
14//! [`TryFromSexp`] pair, specialised to the data-frame SEXP:
15//!
16//! ```ignore
17//! use miniextendr_api::dataframe::{DataFrame, IntoDataFrame, FromDataFrame};
18//!
19//! // Rust → R (returns an owned, GC-rooted `BuiltDataFrame` that `Deref`s to `DataFrame`)
20//! let df = rows.into_dataframe()?; // sequential
21//! let df = rows.into_dataframe_par()?; // parallel (feature = "rayon")
22//!
23//! // R → Rust
24//! let rows: Vec<Row> = Vec::<Row>::from_dataframe(&df)?;
25//! ```
26//!
27//! `DataFrame` implements `TryFromSexp` and `IntoR`, so caller-rooted views slot
28//! into `#[miniextendr]` argument and return conversion. `BuiltDataFrame` also
29//! implements `IntoR`; return rooted Rust-side constructor results directly.
30//!
31//! # One error contract
32//!
33//! Every conversion failure surfaces as [`DataFrameError`]. The serde column assembler's
34//! internal `RSerdeError` is bridged via `From<RSerdeError>`; the parallel R→Rust reader
35//! reports through `DataFrameError` rather than a bare `String`.
36
37use crate::from_r::{SexpError, TryFromSexp};
38use crate::into_r::IntoR;
39use crate::list::{List, NamedList};
40use crate::typed_list::{TypedList, TypedListError, TypedListSpec, validate_list};
41use crate::{SEXP, SEXPTYPE, SexpExt};
42use std::ffi::CStr;
43
44pub mod group;
45pub use group::{GroupKey, GroupedDataFrame, group_rows};
46
47// region: Error type
48
49/// Error returned by any [`DataFrame`] construction, read, or conversion path.
50///
51/// This is the single data-frame error contract: the row-buffer build path, the serde
52/// columnar path, the parallel R→Rust reader, and validation all surface a `DataFrameError`.
53#[derive(Debug, Clone)]
54pub enum DataFrameError {
55 /// The SEXP is not a VECSXP.
56 NotList(String),
57 /// The object does not inherit from `data.frame`.
58 NotDataFrame,
59 /// The list has no `names` attribute (columns must be named).
60 NoNames,
61 /// Could not extract `nrow` from `row.names` attribute.
62 BadRowNames(String),
63 /// Columns have unequal lengths (when promoting from NamedList).
64 UnequalLengths {
65 /// First column length encountered.
66 expected: usize,
67 /// The column name that differs.
68 column: String,
69 /// The actual length of that column.
70 actual: usize,
71 },
72 /// A row could not be turned into named columns (e.g. unnamed list elements
73 /// in a `IntoList`-derived row). Replaces the old `panic!` on this path.
74 UnnamedColumns,
75 /// [`DataFrame::group_by`] referenced a column name that does not exist.
76 NoSuchColumn(String),
77 /// [`DataFrame::group_by_multi`] was called with an empty column slice.
78 EmptyGroupColumns,
79 /// [`DataFrame::group_by`] on a column type with no sane grouping
80 /// semantics (doubles, list-columns, …).
81 UnsupportedGroupColumn {
82 /// The offending column name.
83 column: String,
84 /// Its SEXPTYPE, rendered for the message.
85 type_of: String,
86 },
87 /// [`DataFrame::group_by_metadata`] was called on a frame carrying no
88 /// dplyr `groups` metadata (missing attribute, or its value is not a
89 /// `data.frame`) — i.e. not a `grouped_df`.
90 NotGroupedDataFrame,
91 /// The dplyr `groups` frame has no `.rows` list-column.
92 MissingGroupRows,
93 /// A `.rows` list element was not an integer / integerish index vector.
94 BadGroupRows {
95 /// The 0-based group (row of the `groups` frame) that carried it.
96 group: usize,
97 /// The offending element's SEXPTYPE (or `"non-integer double"`),
98 /// rendered for the message.
99 type_of: String,
100 },
101 /// A `.rows` index was `< 1` or `> nrow` of the source frame.
102 GroupIndexOutOfRange {
103 /// The 0-based group whose `.rows` carried the bad index.
104 group: usize,
105 /// The offending 1-based index value.
106 value: i64,
107 /// The source frame's row count (valid indices are `1..=nrow`).
108 nrow: usize,
109 },
110 /// A serde-driven schema/serialize/deserialize failure (the bridged
111 /// `RSerdeError` text) or another conversion failure carried as a message.
112 Conversion(String),
113}
114
115impl std::fmt::Display for DataFrameError {
116 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
117 match self {
118 DataFrameError::NotList(msg) => write!(f, "not a list: {}", msg),
119 DataFrameError::NotDataFrame => write!(f, "object does not inherit from data.frame"),
120 DataFrameError::NoNames => write!(f, "data.frame has no column names"),
121 DataFrameError::BadRowNames(msg) => {
122 write!(f, "could not extract nrow from row.names: {}", msg)
123 }
124 DataFrameError::UnequalLengths {
125 expected,
126 column,
127 actual,
128 } => write!(
129 f,
130 "column {:?} has length {} (expected {})",
131 column, actual, expected
132 ),
133 DataFrameError::UnnamedColumns => {
134 write!(f, "cannot create data frame from unnamed list elements")
135 }
136 DataFrameError::NoSuchColumn(name) => {
137 write!(f, "no such column: {:?}", name)
138 }
139 DataFrameError::EmptyGroupColumns => {
140 write!(f, "group_by_multi requires at least one column")
141 }
142 DataFrameError::UnsupportedGroupColumn { column, type_of } => write!(
143 f,
144 "cannot group by column {:?} ({}): supported key types are factor, \
145 character, integer, and logical — cut() or factor() the column first",
146 column, type_of
147 ),
148 DataFrameError::NotGroupedDataFrame => write!(
149 f,
150 "not a grouped_df: no `groups` metadata attribute (use group_by/group_by_multi \
151 to compute grouping instead)"
152 ),
153 DataFrameError::MissingGroupRows => {
154 write!(f, "grouped_df `groups` frame has no `.rows` list-column")
155 }
156 DataFrameError::BadGroupRows { group, type_of } => write!(
157 f,
158 "grouped_df `.rows` element for group {} is not an integer index vector ({})",
159 group, type_of
160 ),
161 DataFrameError::GroupIndexOutOfRange { group, value, nrow } => write!(
162 f,
163 "grouped_df `.rows` index {} for group {} is out of range (source frame has \
164 {} rows; valid indices are 1..={})",
165 value, group, nrow, nrow
166 ),
167 DataFrameError::Conversion(msg) => write!(f, "{}", msg),
168 }
169 }
170}
171
172impl std::error::Error for DataFrameError {}
173
174#[cfg(feature = "serde")]
175impl From<crate::serde::RSerdeError> for DataFrameError {
176 fn from(e: crate::serde::RSerdeError) -> Self {
177 DataFrameError::Conversion(e.to_string())
178 }
179}
180// endregion
181
182// region: DataFrame — cheap, unrooted data.frame view
183
184/// A cheap `Copy` view over a validated R `data.frame`.
185///
186/// The view carries no GC root. It is sound while an R `.Call` argument frame,
187/// a [`ProtectScope`](crate::ProtectScope), or an owning [`BuiltDataFrame`]
188/// keeps the SEXP reachable. Rust-side constructors never return this bare
189/// view; they return `BuiltDataFrame`.
190///
191/// # Building
192///
193/// Prefer the [`IntoDataFrame`] trait on your data (it returns an owned,
194/// GC-rooted [`BuiltDataFrame`] that `Deref`s to `DataFrame`):
195///
196/// ```ignore
197/// let df = rows.into_dataframe()?;
198/// ```
199///
200/// or the closure-fill `DataFrame::builder` for heterogeneous parallel column fill
201/// (`feature = "rayon"`).
202///
203/// # Reading
204///
205/// Wrap an incoming SEXP with [`DataFrame::from_sexp`] (or accept `DataFrame` directly as a
206/// `#[miniextendr]` argument), then pull typed columns with [`DataFrame::column`], or
207/// deserialize whole rows with [`FromDataFrame`].
208#[derive(Clone, Copy)]
209pub struct DataFrame {
210 sexp: SEXP,
211}
212
213impl DataFrame {
214 /// Wrap an already-built `data.frame` SEXP without re-validation.
215 ///
216 /// Used by the column assemblers, which produce a well-formed `data.frame` by
217 /// construction.
218 ///
219 /// # Safety
220 ///
221 /// `sexp` must be a VECSXP with the `data.frame` class and consistent `row.names`.
222 #[inline]
223 pub unsafe fn from_built_sexp(sexp: SEXP) -> Self {
224 Self { sexp }
225 }
226
227 /// Wrap an existing R `data.frame` SEXP, validating it.
228 ///
229 /// Validates that the object:
230 /// 1. Is a VECSXP (list)
231 /// 2. Inherits from `"data.frame"`
232 /// 3. Has a `names` attribute
233 /// 4. Has extractable `row.names` for nrow
234 ///
235 /// # Errors
236 ///
237 /// Returns [`DataFrameError`] if validation fails.
238 pub fn from_sexp(sexp: SEXP) -> Result<Self, DataFrameError> {
239 let stype = sexp.type_of();
240 if stype != SEXPTYPE::VECSXP {
241 return Err(DataFrameError::NotList(format!(
242 "expected VECSXP, got {:?}",
243 stype
244 )));
245 }
246 if !sexp.is_data_frame() {
247 return Err(DataFrameError::NotDataFrame);
248 }
249 // Require a names attribute (columns must be named).
250 let list = unsafe { List::from_raw(sexp) };
251 NamedList::new(list).ok_or(DataFrameError::NoNames)?;
252 // Confirm nrow is extractable.
253 extract_nrow(sexp)?;
254 Ok(Self { sexp })
255 }
256
257 // region: Read API (R → Rust column / row access)
258
259 /// Get a column by name, converting to type `T`.
260 ///
261 /// Returns `None` if the column name is not found or conversion fails.
262 ///
263 /// `T` may be a vector/collection target (`Vec<f64>`, `Vec<i32>`,
264 /// `Vec<String>`, …) — the natural shape for a column — or a scalar for a
265 /// length-1 column. The conversion error is discarded (that is what makes
266 /// this return `Option`), so `T::Error` is unconstrained; use
267 /// [`column_raw`](Self::column_raw) when you need the error.
268 #[inline]
269 pub fn column<T>(&self, name: &str) -> Option<T>
270 where
271 T: TryFromSexp,
272 {
273 self.named_list().get(name)
274 }
275
276 /// Get a column by 0-based index, converting to type `T`.
277 ///
278 /// As with [`column`](Self::column), `T` may be a vector/collection or a
279 /// scalar target type; the conversion error is discarded.
280 #[inline]
281 pub fn column_index<T>(&self, idx: usize) -> Option<T>
282 where
283 T: TryFromSexp,
284 {
285 let idx_isize: isize = idx.try_into().ok()?;
286 self.named_list().get_index(idx_isize)
287 }
288
289 /// Get the raw SEXP for a column by name.
290 #[inline]
291 pub fn column_raw(&self, name: &str) -> Option<SEXP> {
292 self.named_list().get_raw(name)
293 }
294
295 /// Number of rows.
296 #[inline]
297 pub fn nrow(&self) -> usize {
298 extract_nrow(self.sexp).unwrap_or(0)
299 }
300
301 /// Number of columns.
302 #[inline]
303 pub fn ncol(&self) -> usize {
304 self.sexp.len()
305 }
306
307 /// Collect column names in column order.
308 pub fn names(&self) -> Vec<String> {
309 let names_sexp = self.sexp.get_names();
310 if names_sexp.is_nil() {
311 return Vec::new();
312 }
313 let n = self.sexp.len() as isize;
314 (0..n)
315 .map(|i| names_sexp.string_elt_str(i).unwrap_or("").to_string())
316 .collect()
317 }
318
319 /// Check whether a column name exists.
320 #[inline]
321 pub fn contains_column(&self, name: &str) -> bool {
322 self.named_list().contains(name)
323 }
324
325 /// Validate the data frame's column types against a [`TypedListSpec`].
326 pub fn validate(&self, spec: &TypedListSpec) -> Result<TypedList, TypedListError> {
327 validate_list(unsafe { List::from_raw(self.sexp) }, spec)
328 }
329 // endregion
330
331 // region: Conversions
332
333 /// Get the underlying [`List`].
334 #[inline]
335 pub fn as_list(&self) -> List {
336 unsafe { List::from_raw(self.sexp) }
337 }
338
339 /// Get the underlying SEXP.
340 #[inline]
341 pub fn as_sexp(&self) -> SEXP {
342 self.sexp
343 }
344
345 /// Build the `NamedList` index for O(1) column-by-name access.
346 #[inline]
347 fn named_list(&self) -> NamedList {
348 NamedList::new(unsafe { List::from_raw(self.sexp) })
349 .expect("DataFrame always carries a names attribute")
350 }
351 // endregion
352
353 // region: Post-assembly editing (absorbed from the old serde columnar assembler)
354
355 /// Rename a column. No-op if `from` doesn't match any column name.
356 ///
357 /// In-place edit of the `names` attribute — returns the **same** frame (and
358 /// therefore inherits whatever root the input already had), unlike the
359 /// new-frame producers ([`drop`](Self::drop) and friends), which return a
360 /// rooted [`BuiltDataFrame`]. On a `BuiltDataFrame` receiver the inherent
361 /// forward ([`BuiltDataFrame::rename`]) keeps the handle instead.
362 pub fn rename(self, from: &str, to: &str) -> Self {
363 unsafe {
364 // Root `self.sexp` so its `names` attribute survives the
365 // `SEXP::charsxp` (Rf_mkCharLenCE) allocation below, which can GC.
366 let _guard = crate::OwnedProtect::new(self.sexp);
367 let names_sexp = self.sexp.get_names();
368 if names_sexp == SEXP::nil() {
369 return self;
370 }
371 let ncol = names_sexp.xlength();
372 for i in 0..ncol {
373 if col_name(names_sexp, i) == from {
374 names_sexp.set_string_elt(i, SEXP::charsxp(to));
375 break;
376 }
377 }
378 }
379 self
380 }
381
382 /// Strip a prefix from all column names that start with it.
383 ///
384 /// In-place edit of the `names` attribute — returns the **same** frame; see
385 /// [`rename`](Self::rename)'s rooting note.
386 pub fn strip_prefix(self, prefix: &str) -> Self {
387 unsafe {
388 // Root `self.sexp` so its `names` attribute survives the
389 // `SEXP::charsxp` (Rf_mkCharLenCE) allocation below, which can GC.
390 let _guard = crate::OwnedProtect::new(self.sexp);
391 let names_sexp = self.sexp.get_names();
392 if names_sexp == SEXP::nil() {
393 return self;
394 }
395 let ncol = names_sexp.xlength();
396 for i in 0..ncol {
397 let name = col_name(names_sexp, i);
398 if let Some(stripped) = name.strip_prefix(prefix) {
399 names_sexp.set_string_elt(i, SEXP::charsxp(stripped));
400 }
401 }
402 }
403 self
404 }
405
406 /// Remove a column by name. No-op if the column doesn't exist.
407 ///
408 /// # Rooting
409 ///
410 /// Returns an owned, GC-rooted [`BuiltDataFrame`] (#1247): the result frame
411 /// is rooted before this method returns, so holding it across R allocations
412 /// is safe by construction. The no-op path re-roots the input frame (each
413 /// `BuiltDataFrame` releases exactly its own root — `R_PreserveObject`
414 /// entries stack, so re-rooting an already-rooted SEXP is sound).
415 ///
416 /// The input view must be reachable when calling (the usual [`DataFrame`]
417 /// view contract: an R `.Call` argument frame, a
418 /// [`ProtectScope`](crate::ProtectScope), or a live [`BuiltDataFrame`]).
419 #[must_use]
420 pub fn drop(self, col: &str) -> BuiltDataFrame {
421 unsafe {
422 let names_sexp = self.sexp.get_names();
423 if names_sexp == SEXP::nil() {
424 return BuiltDataFrame::adopt(self);
425 }
426 let ncol = names_sexp.xlength();
427 let drop_idx = (0..ncol).find(|&i| col_name(names_sexp, i) == col);
428 let Some(drop_idx) = drop_idx else {
429 return BuiltDataFrame::adopt(self);
430 };
431
432 let new_ncol = ncol - 1;
433 let new_list = crate::OwnedProtect::new(SEXP::alloc_list(new_ncol));
434 let new_names = crate::OwnedProtect::new(SEXP::alloc_strsxp(new_ncol));
435
436 let mut j: isize = 0;
437 for i in 0..ncol {
438 if i == drop_idx {
439 continue;
440 }
441 new_list.set_vector_elt(j, self.sexp.vector_elt(i));
442 new_names.set_string_elt(j, names_sexp.string_elt(i));
443 j += 1;
444 }
445
446 new_list.set_names(*new_names);
447 copy_df_attrs(self.sexp, *new_list);
448
449 // Root the new frame before the OwnedProtect guards drop:
450 // `adopt_sexp` preserves first (protecting `new_list` across its own
451 // cons-cell allocation), the guards UNPROTECT after — no gap.
452 BuiltDataFrame::adopt_sexp(*new_list)
453 }
454 }
455
456 /// Keep only the named columns, in the order given. Unknown names are skipped.
457 ///
458 /// # Rooting
459 ///
460 /// Returns an owned, GC-rooted [`BuiltDataFrame`] — see
461 /// [`drop`](Self::drop)'s rooting note (#1247).
462 #[must_use]
463 pub fn select(self, cols: &[&str]) -> BuiltDataFrame {
464 unsafe {
465 let names_sexp = self.sexp.get_names();
466 if names_sexp == SEXP::nil() {
467 return BuiltDataFrame::adopt(self);
468 }
469 let ncol = names_sexp.xlength();
470
471 let indices: Vec<isize> = cols
472 .iter()
473 .filter_map(|&want| (0..ncol).find(|&i| col_name(names_sexp, i) == want))
474 .collect();
475
476 let new_ncol: isize = indices.len().try_into().expect("ncol overflow");
477 let new_list = crate::OwnedProtect::new(SEXP::alloc_list(new_ncol));
478 let new_names = crate::OwnedProtect::new(SEXP::alloc_strsxp(new_ncol));
479
480 for (j, &src_idx) in indices.iter().enumerate() {
481 let j_r: isize = j.try_into().expect("index overflow");
482 new_list.set_vector_elt(j_r, self.sexp.vector_elt(src_idx));
483 new_names.set_string_elt(j_r, names_sexp.string_elt(src_idx));
484 }
485
486 new_list.set_names(*new_names);
487 copy_df_attrs(self.sexp, *new_list);
488
489 // Root before the guards drop (see `drop` for the ordering argument).
490 BuiltDataFrame::adopt_sexp(*new_list)
491 }
492 }
493
494 /// Keep only the rows at the given 0-based indices, in order.
495 ///
496 /// Subsets every column (each a vector or list-column) to the specified rows
497 /// and rebuilds compact integer `row.names`. Used by the enum reader to
498 /// densify a flattened sub-frame before recursing into the inner type's reader.
499 ///
500 /// # PROTECT discipline
501 ///
502 /// Allocates one new column vector per column — `OwnedProtect`s the output list
503 /// across the loop so previously-built column SEXPs survive subsequent allocations.
504 ///
505 /// # Rooting
506 ///
507 /// Returns an owned, GC-rooted [`BuiltDataFrame`] — see
508 /// [`drop`](Self::drop)'s rooting note (#1247).
509 #[must_use]
510 pub fn select_rows(&self, idx: &[usize]) -> BuiltDataFrame {
511 use crate::SexpExt as _;
512
513 unsafe {
514 let names_sexp = self.sexp.get_names();
515 let ncol = self.sexp.xlength();
516 let new_nrow = idx.len();
517
518 let new_list = crate::OwnedProtect::new(SEXP::alloc_list(ncol));
519 let new_names = crate::OwnedProtect::new(SEXP::alloc_strsxp(ncol));
520
521 for col_j in 0..ncol {
522 let src_col = self.sexp.vector_elt(col_j);
523
524 // Gather the requested rows into a new dense column via the shared
525 // conversion helper (the row-selecting inverse of `scatter_column`).
526 // It returns an unprotected SEXP; we root it into the protected
527 // `new_list` immediately below, before any further allocation.
528 let new_col: SEXP = crate::convert::gather_column(src_col, idx);
529
530 // Root new_col in the protected output list BEFORE touching its
531 // attributes. `gather_column` returns an unprotected SEXP, and
532 // `set_class`/`set_levels` (Rf_setAttrib) allocate and can trigger
533 // GC. set_vector_elt does not allocate, so this ordering keeps
534 // new_col reachable (via new_list) across every allocating call.
535 new_list.set_vector_elt(col_j, new_col);
536 if names_sexp != SEXP::nil() {
537 new_names.set_string_elt(col_j, names_sexp.string_elt(col_j));
538 }
539
540 // Copy column attributes: class (for factor / Date / POSIXct) and
541 // levels (for factor columns). Safe now — new_col is rooted in the
542 // protected new_list, so GC during set_class/set_levels can't reap it.
543 let class_attr = src_col.get_class();
544 if class_attr != SEXP::nil() {
545 new_col.set_class(class_attr);
546 }
547 let levels_attr = src_col.get_levels();
548 if levels_attr != SEXP::nil() {
549 new_col.set_levels(levels_attr);
550 }
551 }
552
553 if names_sexp != SEXP::nil() {
554 new_list.set_names(*new_names);
555 }
556
557 // Set compact integer row.names (c(NA_integer_, -new_nrow)).
558 let (row_names, rn) = crate::into_r::alloc_r_vector::<i32>(2);
559 let _rn_guard = crate::OwnedProtect::new(row_names);
560 rn[0] = i32::MIN;
561 rn[1] = -(new_nrow as i32);
562 new_list.set_row_names(row_names);
563 // Copy the data.frame class attribute.
564 new_list.set_class(self.sexp.get_class());
565
566 // Root before the guards drop (see `drop` for the ordering argument).
567 BuiltDataFrame::adopt_sexp(*new_list)
568 }
569 }
570
571 /// Insert a column at index 0 (leftmost), removing any same-named column first.
572 ///
573 /// # Rooting
574 ///
575 /// Returns an owned, GC-rooted [`BuiltDataFrame`] — see
576 /// [`drop`](Self::drop)'s rooting note (#1247). `column` must be kept
577 /// reachable by the caller (e.g. under an
578 /// [`OwnedProtect`](crate::OwnedProtect)) across this call — the new frame
579 /// is allocated before `column` is stored into it.
580 #[must_use]
581 pub fn prepend_column(self, name: &str, column: SEXP) -> BuiltDataFrame {
582 // `drop` returns a rooted handle, so the intermediate frame survives the
583 // allocations below (pre-#1247 this was an unrooted view — a latent UAF
584 // window inside this very method).
585 let cleaned = self.drop(name);
586 unsafe {
587 let names_sexp = cleaned.as_sexp().get_names();
588 let ncol = if names_sexp == SEXP::nil() {
589 0
590 } else {
591 names_sexp.xlength()
592 };
593
594 let new_ncol = ncol + 1;
595 let new_list = crate::OwnedProtect::new(SEXP::alloc_list(new_ncol));
596 let new_names = crate::OwnedProtect::new(SEXP::alloc_strsxp(new_ncol));
597
598 new_list.set_vector_elt(0, column);
599 new_names.set_string_elt(0, SEXP::charsxp(name));
600
601 for i in 0..ncol {
602 new_list.set_vector_elt(i + 1, cleaned.as_sexp().vector_elt(i));
603 new_names.set_string_elt(i + 1, names_sexp.string_elt(i));
604 }
605
606 new_list.set_names(*new_names);
607 copy_df_attrs(cleaned.as_sexp(), *new_list);
608
609 // Root before the guards (and `cleaned`) drop — see `drop` for the
610 // ordering argument.
611 BuiltDataFrame::adopt_sexp(*new_list)
612 }
613 }
614
615 /// Upsert a column: replace the column named `name` if it exists, else append.
616 ///
617 /// # Rooting
618 ///
619 /// Both paths return an owned, GC-rooted [`BuiltDataFrame`] (#1247) — the
620 /// in-place replace path re-roots the same frame it received (sound:
621 /// `R_PreserveObject` entries stack; this handle releases exactly one).
622 /// `column` must be kept reachable by the caller across this call — the
623 /// append path allocates the new frame before `column` is stored into it.
624 #[must_use]
625 pub fn with_column(self, name: &str, column: SEXP) -> BuiltDataFrame {
626 unsafe {
627 let names_sexp = self.sexp.get_names();
628 if names_sexp == SEXP::nil() {
629 return BuiltDataFrame::adopt(self);
630 }
631 let ncol = names_sexp.xlength();
632 for i in 0..ncol {
633 if col_name(names_sexp, i) == name {
634 // set_vector_elt does not allocate; `column` is reachable
635 // from the (caller-rooted) frame before `adopt` allocates
636 // its cons cell.
637 self.sexp.set_vector_elt(i, column);
638 return BuiltDataFrame::adopt(self);
639 }
640 }
641
642 let new_ncol = ncol + 1;
643 let new_list = crate::OwnedProtect::new(SEXP::alloc_list(new_ncol));
644 let new_names = crate::OwnedProtect::new(SEXP::alloc_strsxp(new_ncol));
645
646 for i in 0..ncol {
647 new_list.set_vector_elt(i, self.sexp.vector_elt(i));
648 new_names.set_string_elt(i, names_sexp.string_elt(i));
649 }
650 new_list.set_vector_elt(ncol, column);
651 new_names.set_string_elt(ncol, SEXP::charsxp(name));
652
653 new_list.set_names(*new_names);
654 copy_df_attrs(self.sexp, *new_list);
655
656 // Root before the guards drop (see `drop` for the ordering argument).
657 BuiltDataFrame::adopt_sexp(*new_list)
658 }
659 }
660 // endregion
661
662 // region: builder (ex-RDataFrameBuilder, #768)
663
664 /// Start a closure-per-column builder yielding a rooted [`BuiltDataFrame`].
665 ///
666 /// The heterogeneous-column analogue of `with_r_matrix`: each column buffer is R memory
667 /// filled by a per-column closure. Available regardless of the `rayon` feature (#1055);
668 /// the columns are filled **in parallel** when `rayon` is enabled and **serially**
669 /// otherwise — the resulting `data.frame` is identical either way.
670 ///
671 /// ```ignore
672 /// let df = DataFrame::builder(1000)
673 /// .column::<f64>("x", |chunk, off| for (i, v) in chunk.iter_mut().enumerate() { *v = (off + i) as f64 })
674 /// .column_str("label", |i| Some(format!("row{i}")))
675 /// .build();
676 /// ```
677 #[inline]
678 pub fn builder(nrow: usize) -> crate::dataframe_builder::RDataFrameBuilder {
679 crate::dataframe_builder::RDataFrameBuilder::new(nrow)
680 }
681 // endregion
682}
683// endregion
684
685// region: column-order name helper + attr copy (absorbed from columnar)
686
687/// Read the i-th column name from a STRSXP names vector.
688///
689/// # Safety
690/// `names_sexp` must be a valid STRSXP with at least `i + 1` elements.
691unsafe fn col_name(names_sexp: SEXP, i: isize) -> &'static str {
692 unsafe {
693 let s = names_sexp.string_elt(i);
694 let p = s.r_char();
695 std::ffi::CStr::from_ptr(p).to_str().unwrap_or("")
696 }
697}
698
699/// Copy class and row.names attributes from one data.frame SEXP to another.
700///
701/// # Safety
702/// Both SEXPs must be valid VECSXPs.
703unsafe fn copy_df_attrs(from: SEXP, to: SEXP) {
704 to.set_class(from.get_class());
705 to.set_row_names(from.get_row_names());
706}
707// endregion
708
709// region: IntoR / TryFromSexp for DataFrame
710
711impl TryFromSexp for DataFrame {
712 type Error = SexpError;
713
714 fn try_from_sexp(sexp: SEXP) -> Result<Self, Self::Error> {
715 DataFrame::from_sexp(sexp).map_err(|e| SexpError::InvalidValue(e.to_string()))
716 }
717}
718
719impl IntoR for DataFrame {
720 type Error = std::convert::Infallible;
721 fn try_into_sexp(self) -> Result<SEXP, Self::Error> {
722 Ok(self.sexp)
723 }
724 unsafe fn try_into_sexp_unchecked(self) -> Result<SEXP, Self::Error> {
725 Ok(self.sexp)
726 }
727 #[inline]
728 fn into_sexp(self) -> SEXP {
729 self.sexp
730 }
731}
732// endregion
733
734// region: BuiltDataFrame — owned, GC-rooted, Rust-constructed data.frame
735
736/// An owned, GC-rooted `data.frame` **built on the Rust side**.
737///
738/// [`DataFrame`] is a cheap `Copy` *view* over a bare SEXP with no root of its
739/// own — sound only while something else keeps the SEXP reachable (an R `.Call`
740/// argument frame roots R-supplied frames; a surrounding
741/// [`ProtectScope`](crate::ProtectScope) roots transients). A frame that Rust
742/// *constructs* has no such external root, so holding the bare view across any R
743/// allocation is a latent use-after-free (issue #1128): the GC reclaims the
744/// frame, and a freed-but-intact read passes tests silently until the slot is
745/// reused.
746///
747/// `BuiltDataFrame` is the return type of every Rust-side constructor
748/// ([`IntoDataFrame::into_dataframe`], `SerdeRowBuilder::finish`,
749/// [`DataFrame::builder`]`().build()`, the serde `*_to_dataframe` verbs,
750/// [`NamedList::as_data_frame`]). It roots the frame with `R_PreserveObject` on
751/// construction and releases it with `R_ReleaseObject` on drop, so holding it
752/// across allocations is safe by construction. It [`Deref`](std::ops::Deref)s to
753/// [`DataFrame`], so every read/edit method keeps working unchanged; hand the
754/// frame back to R with [`IntoR::into_sexp`] — or just return it from a
755/// `#[miniextendr]` fn, which converts it through [`IntoR`] transparently.
756///
757/// Not `Copy`/`Clone` (each root is released exactly once) and not `Send`
758/// (R's precious list is R-main-thread state).
759///
760/// # Editing (chains stay rooted, #1247)
761///
762/// The new-frame editing methods ([`DataFrame::drop`], [`DataFrame::select`],
763/// [`DataFrame::select_rows`], [`DataFrame::prepend_column`],
764/// [`DataFrame::with_column`]) also return `BuiltDataFrame`, and this type
765/// carries inherent forwards for the whole editing set, so
766/// `rows.into_dataframe()?.drop("x").select(&["y"])` is rooted at every link.
767/// (The `drop` forward is load-bearing: without it, method resolution on a
768/// `BuiltDataFrame` receiver finds `Drop::drop` — E0040.)
769///
770/// # Residual
771///
772/// The cheap view can still be *smuggled* across allocations by hand:
773/// dereferencing (`*built`) copies out an unrooted `DataFrame` view that
774/// dangles once the handle drops. That is an opt-in footgun, not the
775/// constructor/editing path this type makes safe.
776pub struct BuiltDataFrame {
777 view: DataFrame,
778 /// `!Send + !Sync`: rooting/unrooting mutates R's global precious list,
779 /// which is R-main-thread state. Construction and drop only ever happen
780 /// inside framework code already on the R thread.
781 _not_send: std::marker::PhantomData<*mut ()>,
782}
783
784impl BuiltDataFrame {
785 /// Root an already-built `data.frame` SEXP and take ownership of the root.
786 ///
787 /// # Safety
788 ///
789 /// Must run on the R main thread. `sexp` must be a well-formed `data.frame`
790 /// VECSXP. Rooting is immediate — no allocation happens between entry and
791 /// `R_PreserveObject`, and `R_PreserveObject` protects `sexp` while it
792 /// allocates its cons cell — so a freshly-assembled unprotected SEXP is safe
793 /// to pass directly.
794 #[inline]
795 pub unsafe fn adopt_sexp(sexp: SEXP) -> Self {
796 // SAFETY: R main thread (caller contract); R_PreserveObject keeps `sexp`
797 // reachable across its own allocation.
798 unsafe { crate::sys::R_PreserveObject(sexp) };
799 Self {
800 // SAFETY: caller guarantees `sexp` is a well-formed data.frame VECSXP.
801 view: unsafe { DataFrame::from_built_sexp(sexp) },
802 _not_send: std::marker::PhantomData,
803 }
804 }
805
806 /// Root a [`DataFrame`] view, taking ownership of a fresh root.
807 ///
808 /// Rooting an already-rooted frame is sound: `R_PreserveObject` entries
809 /// stack (the precious list is a cons list, duplicates allowed), and each
810 /// `BuiltDataFrame` releases exactly the one entry it added.
811 ///
812 /// # Safety
813 ///
814 /// Must run on the R main thread. `df` must wrap a well-formed, still-live
815 /// `data.frame` VECSXP (reachable at the moment of the call).
816 #[inline]
817 pub unsafe fn adopt(df: DataFrame) -> Self {
818 // SAFETY: as documented on this fn.
819 unsafe { Self::adopt_sexp(df.as_sexp()) }
820 }
821
822 /// Detach the rooted SEXP, releasing the Rust-side root without running
823 /// `Drop`. Shared by the [`IntoR`] hand-off methods.
824 ///
825 /// Mirrors [`DataFrame::as_sexp`] / [`IntoR::into_sexp`]: the returned SEXP is
826 /// unprotected and the caller (typically the `.Call` return path) takes over
827 /// protection. Releasing the precious-list root then returning matches that
828 /// existing hand-off contract — `R_ReleaseObject` does not allocate, so no GC
829 /// can run between the release and the return. `into_sexp` is exposed only
830 /// through [`IntoR`] (like [`DataFrame`]), so there is no inherent method to
831 /// shadow the trait's.
832 #[inline]
833 fn release_into_sexp(self) -> SEXP {
834 let sexp = self.view.as_sexp();
835 // SAFETY: `!Send` guarantees we are on the constructing (R main) thread;
836 // release the exact root added in `adopt_sexp`. `mem::forget` prevents
837 // `Drop` from releasing it a second time.
838 unsafe { crate::sys::R_ReleaseObject(sexp) };
839 std::mem::forget(self);
840 sexp
841 }
842
843 // region: editing forwards (#1247) — keep chains rooted at every link
844 //
845 // These consume the handle (`self` stays alive until the tail expression
846 // has produced — and rooted — the result, so the input frame is reachable
847 // throughout the edit; its root is then released with no allocation in
848 // between). Plain `Deref` would instead copy out the view and, for `drop`,
849 // resolve to `Drop::drop` (E0040) — an inherent method shadows the trait.
850
851 /// Remove a column by name — see [`DataFrame::drop`].
852 ///
853 /// Inherent forward: consumes this handle and returns the rooted result.
854 /// (Also shadows `Drop::drop`, which method resolution would otherwise
855 /// select — E0040.)
856 #[must_use]
857 pub fn drop(self, col: &str) -> BuiltDataFrame {
858 (*self).drop(col)
859 }
860
861 /// Keep only the named columns — see [`DataFrame::select`].
862 #[must_use]
863 pub fn select(self, cols: &[&str]) -> BuiltDataFrame {
864 (*self).select(cols)
865 }
866
867 /// Keep only the given rows — see [`DataFrame::select_rows`].
868 #[must_use]
869 pub fn select_rows(&self, idx: &[usize]) -> BuiltDataFrame {
870 (**self).select_rows(idx)
871 }
872
873 /// Insert a column at index 0 — see [`DataFrame::prepend_column`].
874 ///
875 /// `column` must be kept reachable by the caller across this call.
876 #[must_use]
877 pub fn prepend_column(self, name: &str, column: SEXP) -> BuiltDataFrame {
878 (*self).prepend_column(name, column)
879 }
880
881 /// Upsert a column — see [`DataFrame::with_column`].
882 ///
883 /// `column` must be kept reachable by the caller across this call.
884 #[must_use]
885 pub fn with_column(self, name: &str, column: SEXP) -> BuiltDataFrame {
886 (*self).with_column(name, column)
887 }
888
889 /// Rename a column — see [`DataFrame::rename`].
890 ///
891 /// In-place edit of the same frame: this handle (and its root) carries
892 /// straight through.
893 #[must_use]
894 pub fn rename(self, from: &str, to: &str) -> BuiltDataFrame {
895 let _ = (*self).rename(from, to);
896 self
897 }
898
899 /// Strip a prefix from column names — see [`DataFrame::strip_prefix`].
900 ///
901 /// In-place edit of the same frame: this handle (and its root) carries
902 /// straight through.
903 #[must_use]
904 pub fn strip_prefix(self, prefix: &str) -> BuiltDataFrame {
905 let _ = (*self).strip_prefix(prefix);
906 self
907 }
908 // endregion
909}
910
911impl std::ops::Deref for BuiltDataFrame {
912 type Target = DataFrame;
913 #[inline]
914 fn deref(&self) -> &DataFrame {
915 &self.view
916 }
917}
918
919impl Drop for BuiltDataFrame {
920 fn drop(&mut self) {
921 // SAFETY: `!Send` guarantees drop runs on the R main thread; release the
922 // exact root added in `adopt_sexp`. `R_ReleaseObject` does not allocate.
923 unsafe { crate::sys::R_ReleaseObject(self.view.as_sexp()) };
924 }
925}
926
927impl IntoR for BuiltDataFrame {
928 type Error = std::convert::Infallible;
929 fn try_into_sexp(self) -> Result<SEXP, Self::Error> {
930 Ok(self.release_into_sexp())
931 }
932 unsafe fn try_into_sexp_unchecked(self) -> Result<SEXP, Self::Error> {
933 Ok(self.release_into_sexp())
934 }
935 #[inline]
936 fn into_sexp(self) -> SEXP {
937 self.release_into_sexp()
938 }
939}
940
941impl std::fmt::Debug for BuiltDataFrame {
942 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
943 f.debug_struct("BuiltDataFrame")
944 .field("nrow", &self.nrow())
945 .field("ncol", &self.ncol())
946 .finish()
947 }
948}
949// endregion
950
951// region: The conversion trait family (mirrors IntoR / TryFromSexp)
952
953/// Rust data → R `data.frame`. The data-frame analogue of [`IntoR`].
954///
955/// Implemented by `#[derive(DataFrameRow)]` on a row struct/enum (for `Vec<Row>`), by the
956/// blanket impl for any [`ColumnSource`] (`IntoList`-derived rows), and by the serde column
957/// path. Call it on your data: `rows.into_dataframe()?`.
958///
959/// # Parallel fast path
960///
961/// `into_dataframe_par` (present only with
962/// `feature = "rayon"`) produces the **same** [`BuiltDataFrame`] as
963/// [`into_dataframe`](IntoDataFrame::into_dataframe). It defaults to the sequential path, so
964/// every implementor gets a correct `_par` for free; `#[derive(DataFrameRow)]` row types
965/// override it with a genuinely parallel column fill (the #777 flattened `(column,row-range)`
966/// work-list). The verb is stable across feature sets — dropping `_par` degrades cleanly to
967/// the sequential call.
968pub trait IntoDataFrame: Sized {
969 /// Convert this value into an owned, GC-rooted [`BuiltDataFrame`].
970 fn into_dataframe(self) -> Result<BuiltDataFrame, DataFrameError>;
971
972 /// Parallel column fill (`feature = "rayon"`). Same result as `into_dataframe()`.
973 ///
974 /// Defaults to the sequential path; overridden by the derive for a real parallel fill.
975 #[cfg(feature = "rayon")]
976 fn into_dataframe_par(self) -> Result<BuiltDataFrame, DataFrameError> {
977 self.into_dataframe()
978 }
979}
980
981/// R `data.frame` → Rust data. The data-frame analogue of
982/// [`TryFromSexp`].
983///
984/// Implemented by `#[derive(DataFrameRow)]` for `Vec<Row>` and by the serde row path.
985///
986/// # Parallel fast path
987///
988/// `from_dataframe_par` (`feature = "rayon"`) reads the
989/// same rows as [`from_dataframe`](FromDataFrame::from_dataframe), defaulting to the
990/// sequential reader; the derive overrides it with the #765 off-main-thread row assembly.
991pub trait FromDataFrame: Sized {
992 /// Read rows back out of a [`DataFrame`].
993 fn from_dataframe(df: &DataFrame) -> Result<Self, DataFrameError>;
994
995 /// Parallel row read (`feature = "rayon"`). Same result as `from_dataframe()`.
996 #[cfg(feature = "rayon")]
997 fn from_dataframe_par(df: &DataFrame) -> Result<Self, DataFrameError> {
998 Self::from_dataframe(df)
999 }
1000}
1001
1002/// Enum rows → variant-partitioned R `data.frame`s. The split-representation
1003/// sibling of [`IntoDataFrame`].
1004///
1005/// Where [`IntoDataFrame`] produces one **aligned** `data.frame` (field-name
1006/// union, `NA` fill for columns a variant doesn't carry), this verb partitions
1007/// the rows by variant: each partition is a `data.frame` with only that
1008/// variant's own columns (non-optional types — no `NA` fill from sibling
1009/// variants), keyed by the snake_case variant name.
1010///
1011/// Implemented by `#[derive(DataFrameRow)]` on an **enum** row type (for
1012/// `Vec<Row>`, via the hidden [`DataFrameRowSplit`] bridge below). Struct rows
1013/// have no variants to partition, so the struct derive does not provide it.
1014/// Call it on your data: `rows.into_dataframe_split()`.
1015///
1016/// # Return shape
1017///
1018/// The returned [`List`] is a bare `data.frame` for a single-variant enum and a
1019/// named R list of `data.frame`s (one per variant) otherwise. Variants absent
1020/// from the input still appear as 0-row `data.frame`s with that variant's
1021/// column shape; unit variants produce a 0-column `data.frame` with the
1022/// correct row count. `List` is [`IntoR`], so a `#[miniextendr]` function
1023/// returns it directly.
1024pub trait IntoDataFrameSplit: Sized {
1025 /// Partition the rows by variant into one `data.frame` per variant.
1026 fn into_dataframe_split(self) -> List;
1027}
1028
1029/// Rust rows ↔ the pure-Rust columnar companion generated by `#[derive(DataFrameRow)]`.
1030///
1031/// `#[derive(DataFrameRow)]` builds a companion struct (`<Row>DataFrame`) whose fields are
1032/// `Vec`-columns — the in-memory, column-oriented form of `Vec<Row>`. This trait is the
1033/// documented verb surface for that companion: build it from rows (sequentially, or in parallel
1034/// with `feature = "rayon"`) and, for row-iterable companions, read the rows back.
1035///
1036/// It is the pure-Rust complement to [`IntoDataFrame`] / [`FromDataFrame`], which cross the R
1037/// boundary (`Vec<Row>` ↔ R `data.frame`). In particular `from_rows_par`
1038/// (`feature = "rayon"`) builds the **pure-Rust companion** in parallel —
1039/// `IntoDataFrame::into_dataframe_par` cannot, since it yields an R-backed
1040/// [`BuiltDataFrame`] rather than the companion.
1041///
1042/// The derive implements this on the companion type; call `CompanionType::from_rows(rows)`.
1043/// `Row` is a type parameter (not an associated type) so the derive can implement it for a
1044/// `pub` companion even when the row type is private — mirroring `From<Vec<Row>>`, and
1045/// avoiding the private-type-in-public-interface error an associated type would raise.
1046///
1047/// # Reading rows back
1048///
1049/// [`into_rows`](ColumnarFrame::into_rows) is available for **row-iterable** companions
1050/// (named-field structs without column expansion), where it defers to the companion's
1051/// [`IntoIterator`]. Enum and column-expansion companions are write-only in pure Rust; read
1052/// those back across the R boundary via [`FromDataFrame`] or the derive's one-call
1053/// `try_from_dataframe`.
1054pub trait ColumnarFrame<Row>: Sized {
1055 /// Transpose a row vector into the columnar companion (sequential).
1056 fn from_rows(rows: Vec<Row>) -> Self;
1057
1058 /// Parallel transposition (`feature = "rayon"`). Same result as
1059 /// [`from_rows`](ColumnarFrame::from_rows).
1060 ///
1061 /// Defaults to the sequential path; `#[derive(DataFrameRow)]` overrides it with a genuine
1062 /// parallel column fill for shapes that support one.
1063 #[cfg(feature = "rayon")]
1064 fn from_rows_par(rows: Vec<Row>) -> Self {
1065 Self::from_rows(rows)
1066 }
1067
1068 /// Read the rows back out of the companion.
1069 ///
1070 /// Available for row-iterable companions (named-field structs without column expansion);
1071 /// defers to the companion's [`IntoIterator`]. Enum and column-expansion companions are
1072 /// write-only in pure Rust — read those back via [`FromDataFrame`] / `try_from_dataframe`.
1073 fn into_rows(self) -> Vec<Row>
1074 where
1075 Self: IntoIterator<Item = Row>,
1076 {
1077 self.into_iter().collect()
1078 }
1079}
1080// endregion
1081
1082// region: ColumnSource — internal column-assembly engine (ex-public convert::IntoDataFrame)
1083
1084/// Internal engine that turns a value into a `data.frame`-shaped [`List`].
1085///
1086/// This was the historical public `convert::IntoDataFrame` (`-> List`). It is now an internal
1087/// engine: the public [`IntoDataFrame`] (`-> Result<BuiltDataFrame, _>`) and the enum-flatten
1088/// codegen both delegate to it. Not part of the public verb surface.
1089#[doc(hidden)]
1090pub trait ColumnSource {
1091 /// Convert into a `data.frame`-shaped [`List`] (named columns, `data.frame` class,
1092 /// `row.names`).
1093 fn into_column_list(self) -> List;
1094
1095 /// Extract named column SEXPs from this value.
1096 ///
1097 /// Returns `(name, raw SEXP)` per column. The SEXPs are owned by the produced
1098 /// data-frame SEXP and must be protected by the caller before it is released.
1099 ///
1100 /// # Safety
1101 ///
1102 /// Calls R API functions; must run on the R main thread.
1103 fn into_named_columns(self) -> Vec<(String, crate::SEXP)>
1104 where
1105 Self: Sized,
1106 {
1107 use crate::SexpExt as _;
1108 let list = self.into_column_list();
1109 let sexp = list.as_sexp();
1110 let n = sexp.len();
1111 let mut out = Vec::with_capacity(n);
1112 let names_sexp = sexp.get_names();
1113 let has_names = !names_sexp.is_nil();
1114 for i in 0..(n as isize) {
1115 let col_sexp = sexp.vector_elt(i);
1116 let col_name = if has_names {
1117 names_sexp.string_elt_str(i).unwrap_or("").to_string()
1118 } else {
1119 i.to_string()
1120 };
1121 out.push((col_name, col_sexp));
1122 }
1123 out
1124 }
1125
1126 /// Assemble this source into a validated [`DataFrame`].
1127 ///
1128 /// The column engine always sets the `data.frame` class (even for an empty frame); the one
1129 /// exception is the unnamed-row degradation, which returns a bare unclassed empty list — the
1130 /// old `panic!("unnamed list elements")` case, now a clean `Err(UnnamedColumns)`.
1131 ///
1132 /// This is the bridge from the internal column-assembly engine to the public [`DataFrame`].
1133 /// We deliberately do **not** offer a blanket `impl<T: ColumnSource> IntoDataFrame for T`:
1134 /// `#[derive(DataFrameRow)]` emits a *concrete* `impl IntoDataFrame for Vec<Row>` per row
1135 /// type (and serde uses the `SerdeRows<T>` newtype), so a generic `for T` blanket would
1136 /// coherence-conflict with every one of those (the compiler treats `Vec<Row>: ColumnSource`
1137 /// as possibly-true). The derive's `into_dataframe` glue calls this method instead.
1138 fn into_dataframe(self) -> Result<DataFrame, DataFrameError>
1139 where
1140 Self: Sized,
1141 {
1142 use crate::SexpExt as _;
1143 let sexp = self.into_column_list().as_sexp();
1144 if !sexp.is_data_frame() {
1145 return Err(DataFrameError::UnnamedColumns);
1146 }
1147 Ok(unsafe { DataFrame::from_built_sexp(sexp) })
1148 }
1149}
1150// endregion
1151
1152// region: rayon-gating shim for #[derive(DataFrameRow)] parallel methods
1153
1154/// Emit the wrapped items only when `miniextendr-api` itself was built with the
1155/// `rayon` feature.
1156///
1157/// `#[derive(DataFrameRow)]` uses this to gate its parallel `*_par` methods on
1158/// **this** crate's `rayon` feature instead of stamping a raw
1159/// `#[cfg(feature = "rayon")]` into the *consumer* crate. A `cfg` inside a
1160/// derive is evaluated against the destination crate, where `rayon` is usually
1161/// not a declared feature — so every downstream package that derives
1162/// `DataFrameRow` without a `rayon` feature of its own trips the
1163/// `unexpected_cfgs` lint (#1117). Routing through this macro moves the `#[cfg]`
1164/// decision into `miniextendr-api`, whose feature set always declares `rayon`,
1165/// so the consumer crate never sees the attribute. The parallel path now
1166/// activates purely on the API crate's `rayon` feature, independent of what the
1167/// consumer names its own.
1168#[doc(hidden)]
1169#[macro_export]
1170#[cfg(feature = "rayon")]
1171macro_rules! __dataframe_row_when_rayon {
1172 ($($item:tt)*) => { $($item)* };
1173}
1174
1175/// No-rayon build: the wrapped parallel methods vanish entirely. The trait's
1176/// `*_par` methods are themselves `#[cfg(feature = "rayon")]`, so there is
1177/// nothing to override.
1178#[doc(hidden)]
1179#[macro_export]
1180#[cfg(not(feature = "rayon"))]
1181macro_rules! __dataframe_row_when_rayon {
1182 ($($item:tt)*) => {};
1183}
1184
1185// endregion
1186
1187// region: DataFrameRowConvert — orphan-rule bridge for `Vec<Row>` conversions
1188
1189/// Row → DataFrame conversion glue emitted by `#[derive(DataFrameRow)]` on the **row type**.
1190///
1191/// The orphan rule forbids the derive from writing `impl IntoDataFrame for Vec<Row>` in the user
1192/// crate: both `IntoDataFrame` and `Vec` are foreign there, and `Row` only appears *covered*
1193/// inside `Vec<_>`, so there is no uncovered local type. Instead the derive implements this
1194/// `#[doc(hidden)]` trait on the local `Row` type (legal — `Row` is local), and `miniextendr_api`
1195/// carries the blanket [`IntoDataFrame`] / [`FromDataFrame`] impls for `Vec<T: DataFrameRowConvert>`
1196/// below (legal — `IntoDataFrame` is local *here*). Users still call the public
1197/// `rows.into_dataframe()?` / `Vec::<Row>::from_dataframe(&df)?` verbs.
1198#[doc(hidden)]
1199pub trait DataFrameRowConvert: Sized {
1200 /// Build a [`DataFrame`] from a row vector (sequential).
1201 fn rows_into_dataframe(rows: Vec<Self>) -> Result<DataFrame, DataFrameError>;
1202
1203 /// Build a [`DataFrame`] from a row vector (parallel; defaults to sequential).
1204 #[cfg(feature = "rayon")]
1205 fn rows_into_dataframe_par(rows: Vec<Self>) -> Result<DataFrame, DataFrameError> {
1206 Self::rows_into_dataframe(rows)
1207 }
1208
1209 /// Read a row vector out of a [`DataFrame`]. `None` means this row shape has no reader
1210 /// (scalar, column-expansion, and struct-flatten struct shapes do; tagged enum shapes with
1211 /// reader-capable fields do too; tagless/map-column/coerced/skip/`as_list` enum shapes and
1212 /// opaque-map shapes do not); the blanket surfaces that as a clear error.
1213 fn rows_from_dataframe(_df: &DataFrame) -> Option<Result<Vec<Self>, DataFrameError>> {
1214 None
1215 }
1216
1217 /// Parallel reader (defaults to the sequential reader).
1218 #[cfg(feature = "rayon")]
1219 fn rows_from_dataframe_par(df: &DataFrame) -> Option<Result<Vec<Self>, DataFrameError>> {
1220 Self::rows_from_dataframe(df)
1221 }
1222}
1223
1224/// Error returned by `Vec::<Row>::from_dataframe` when the row shape has no R→Rust reader.
1225fn no_reader_error() -> DataFrameError {
1226 DataFrameError::Conversion(
1227 "this DataFrameRow shape has no R→Rust reader (struct shapes with scalar/expansion/\
1228 struct-flatten fields do; tagged enum shapes with reader-capable fields do; \
1229 tagless/map-column/coerced/skip/as_list enum shapes and opaque-map shapes do not)"
1230 .to_string(),
1231 )
1232}
1233
1234impl<T: DataFrameRowConvert> IntoDataFrame for Vec<T> {
1235 fn into_dataframe(self) -> Result<BuiltDataFrame, DataFrameError> {
1236 // `rows_into_dataframe` returns the freshly-built frame as a bare view;
1237 // root it immediately (no allocation between the `?` and `adopt`) so the
1238 // returned handle owns its GC root.
1239 // SAFETY: builds SEXPs → runs on the R main thread.
1240 Ok(unsafe { BuiltDataFrame::adopt(T::rows_into_dataframe(self)?) })
1241 }
1242
1243 #[cfg(feature = "rayon")]
1244 fn into_dataframe_par(self) -> Result<BuiltDataFrame, DataFrameError> {
1245 // SAFETY: as in `into_dataframe` above.
1246 Ok(unsafe { BuiltDataFrame::adopt(T::rows_into_dataframe_par(self)?) })
1247 }
1248}
1249
1250impl<T: DataFrameRowConvert> FromDataFrame for Vec<T> {
1251 fn from_dataframe(df: &DataFrame) -> Result<Self, DataFrameError> {
1252 // No input root here: an R-supplied frame is rooted by R's `.Call`
1253 // argument frame, and a Rust-built frame now reaches the reader as a
1254 // borrowed `BuiltDataFrame` (rooted for the borrow) — the handle
1255 // supersedes the old `OwnedProtect` guard (#1128). The reader's own
1256 // sub-frame allocations stay protected by their internal guards.
1257 T::rows_from_dataframe(df).unwrap_or_else(|| Err(no_reader_error()))
1258 }
1259
1260 #[cfg(feature = "rayon")]
1261 fn from_dataframe_par(df: &DataFrame) -> Result<Self, DataFrameError> {
1262 T::rows_from_dataframe_par(df).unwrap_or_else(|| Err(no_reader_error()))
1263 }
1264}
1265
1266/// Row → split-representation glue emitted by `#[derive(DataFrameRow)]` on **enum** row types.
1267///
1268/// Same orphan-rule bridge as [`DataFrameRowConvert`]: the derive cannot write
1269/// `impl IntoDataFrameSplit for Vec<Row>` in the user crate (`Row` is covered by `Vec<_>`), so
1270/// it implements this `#[doc(hidden)]` trait on the local enum type and `miniextendr_api`
1271/// carries the blanket [`IntoDataFrameSplit`] impl for `Vec<T: DataFrameRowSplit>` below.
1272///
1273/// Deliberately a **separate** bridge from [`DataFrameRowConvert`]: the split representation
1274/// only exists for enums, and a method here (even defaulted) would grow the verb onto every
1275/// struct row type. Users call the public `rows.into_dataframe_split()` verb.
1276#[doc(hidden)]
1277pub trait DataFrameRowSplit: Sized {
1278 /// Partition a row vector by variant into one `data.frame` per variant.
1279 fn rows_into_dataframe_split(rows: Vec<Self>) -> List;
1280}
1281
1282impl<T: DataFrameRowSplit> IntoDataFrameSplit for Vec<T> {
1283 fn into_dataframe_split(self) -> List {
1284 T::rows_into_dataframe_split(self)
1285 }
1286}
1287// endregion
1288
1289// region: nrow extraction from row.names
1290
1291/// Extract `nrow` from R's `row.names` attribute.
1292fn extract_nrow(sexp: SEXP) -> Result<usize, DataFrameError> {
1293 let row_names = sexp.get_row_names();
1294
1295 if row_names.is_nil() {
1296 return nrow_from_first_column(sexp);
1297 }
1298
1299 let rn_type = row_names.type_of();
1300 let rn_len = row_names.xlength();
1301
1302 if rn_type == SEXPTYPE::INTSXP && rn_len == 2 {
1303 let rn: &[i32] = unsafe { row_names.as_slice() };
1304 if rn[0] == i32::MIN && rn[1] < 0 {
1305 return Ok((-rn[1]) as usize);
1306 }
1307 }
1308
1309 if let Ok(n) = usize::try_from(rn_len) {
1310 Ok(n)
1311 } else {
1312 Err(DataFrameError::BadRowNames(format!(
1313 "row.names has negative length: {}",
1314 rn_len
1315 )))
1316 }
1317}
1318
1319/// Fall back: extract nrow from the length of the first column.
1320fn nrow_from_first_column(sexp: SEXP) -> Result<usize, DataFrameError> {
1321 let ncol = sexp.xlength();
1322 if ncol == 0 {
1323 return Ok(0);
1324 }
1325 let first_col = sexp.vector_elt(0);
1326 if first_col == SEXP::nil() {
1327 return Ok(0);
1328 }
1329 let len = first_col.xlength();
1330 if let Ok(n) = usize::try_from(len) {
1331 Ok(n)
1332 } else {
1333 Err(DataFrameError::BadRowNames(
1334 "first column has negative length".to_string(),
1335 ))
1336 }
1337}
1338// endregion
1339
1340// region: NamedList / List → DataFrame promotion
1341
1342/// Validate that all columns in a NamedList have equal length, returning the common length.
1343fn validate_equal_lengths(named: &NamedList) -> Result<usize, DataFrameError> {
1344 let list = named.as_list();
1345 let n = list.len();
1346
1347 if n == 0 {
1348 return Ok(0);
1349 }
1350
1351 let first_col = list.as_sexp().vector_elt(0);
1352 let expected: usize = first_col.len();
1353
1354 let names_sexp = list.names();
1355 for i in 1..n {
1356 let col = list.as_sexp().vector_elt(i);
1357 let col_len: usize = col.len();
1358 if col_len != expected {
1359 let column = if let Some(names) = names_sexp {
1360 let name_sexp = names.string_elt(i);
1361 if name_sexp != SEXP::na_string() {
1362 let name_ptr = name_sexp.r_char();
1363 let name_cstr = unsafe { CStr::from_ptr(name_ptr) };
1364 name_cstr.to_str().unwrap_or("<invalid>").to_string()
1365 } else {
1366 format!("column {}", i)
1367 }
1368 } else {
1369 format!("column {}", i)
1370 };
1371
1372 return Err(DataFrameError::UnequalLengths {
1373 expected,
1374 column,
1375 actual: col_len,
1376 });
1377 }
1378 }
1379
1380 Ok(expected)
1381}
1382
1383impl NamedList {
1384 /// Promote this named list to a [`DataFrame`].
1385 ///
1386 /// Validates equal column lengths, sets the `data.frame` class, and adds compact integer
1387 /// `row.names`.
1388 ///
1389 /// # Errors
1390 ///
1391 /// Returns [`DataFrameError::UnequalLengths`] if columns differ in length.
1392 pub fn as_data_frame(&self) -> Result<BuiltDataFrame, DataFrameError> {
1393 let nrow = validate_equal_lengths(self)?;
1394 self.as_list().set_data_frame_class();
1395 self.as_list().set_row_names_int(nrow);
1396 // SAFETY: R main thread; the list is now a well-formed data.frame VECSXP.
1397 Ok(unsafe { BuiltDataFrame::adopt_sexp(self.as_list().as_sexp()) })
1398 }
1399}
1400
1401impl List {
1402 /// Promote this named list to a [`DataFrame`].
1403 ///
1404 /// # Errors
1405 ///
1406 /// Returns [`DataFrameError`] if the list has no names or columns differ in length.
1407 pub fn as_data_frame(&self) -> Result<BuiltDataFrame, DataFrameError> {
1408 let named = NamedList::new(*self).ok_or(DataFrameError::NoNames)?;
1409 named.as_data_frame()
1410 }
1411}
1412// endregion
1413
1414// region: NamedDataFrameListBuilder (moved from serde::columnar — no serde dependency)
1415
1416/// Assemble a named list whose elements are [`DataFrame`]s,
1417/// without per-result `OwnedProtect` bookkeeping.
1418///
1419/// # Why this is distinct from [`DataFrame::builder`]
1420///
1421/// [`DataFrame::builder`](crate::dataframe::DataFrame::builder) and the serde
1422/// `SerdeRowBuilder` both produce a single rooted [`BuiltDataFrame`]. This builder
1423/// produces a different shape — a named *list of* data.frames, e.g.
1424/// `list(results = df, error = df)` — so it deliberately keeps its own name
1425/// rather than folding into the `DataFrame::builder` vocabulary. Its inputs
1426/// are [`DataFrame`]s (from any producer: [`IntoDataFrame`], the serde
1427/// `vec_to_dataframe`, or [`GroupedDataFrame::frames`]); its output is a
1428/// [`List`].
1429///
1430/// Each [`push`](NamedDataFrameListBuilder::push) protects the input
1431/// data.frame's SEXP via an internal [`ProtectScope`](crate::ProtectScope);
1432/// [`build`](NamedDataFrameListBuilder::build) consumes the builder and emits
1433/// a named list via [`List::from_raw_pairs`](crate::list::List::from_raw_pairs).
1434/// The scope drops at the end of `build`, releasing the per-input protects —
1435/// by which point the children are reachable from the assembled list.
1436///
1437/// # Example
1438///
1439/// ```ignore
1440/// let result = NamedDataFrameListBuilder::new()
1441/// .push("results", *oks.into_dataframe()?) // deref the BuiltDataFrame to its view
1442/// .push("error", *errs.into_dataframe()?)
1443/// .build();
1444/// ```
1445pub struct NamedDataFrameListBuilder {
1446 scope: crate::ProtectScope,
1447 pairs: Vec<(String, SEXP)>,
1448}
1449
1450impl NamedDataFrameListBuilder {
1451 /// Create an empty builder.
1452 ///
1453 /// # Safety (caller)
1454 ///
1455 /// Must be called from the R main thread. The internal
1456 /// [`ProtectScope`](crate::ProtectScope) carries `!Send + !Sync`
1457 /// so the builder cannot be moved to another thread.
1458 pub fn new() -> Self {
1459 Self {
1460 // SAFETY: ProtectScope requires the R main thread. The builder is
1461 // constructible only on the R main thread; ProtectScope carries
1462 // NoSendSync so it cannot be moved off-thread.
1463 scope: unsafe { crate::ProtectScope::new() },
1464 pairs: Vec::new(),
1465 }
1466 }
1467
1468 /// Create a builder pre-allocated for `n` entries.
1469 ///
1470 /// Equivalent to [`new`](Self::new) but avoids repeated re-allocations
1471 /// when the number of partitions is known up front.
1472 pub fn with_capacity(n: usize) -> Self {
1473 Self {
1474 scope: unsafe { crate::ProtectScope::new() },
1475 pairs: Vec::with_capacity(n),
1476 }
1477 }
1478
1479 /// Append a named data.frame. The input's SEXP is protected
1480 /// internally for the lifetime of the builder.
1481 #[must_use]
1482 pub fn push<S: Into<String>>(mut self, name: S, df: DataFrame) -> Self {
1483 use crate::IntoR as _;
1484 let sexp = df.into_sexp();
1485 // SAFETY: R main thread (constructor invariant); sexp is a valid
1486 // VECSXP just produced by DataFrame::into_sexp.
1487 unsafe {
1488 self.scope.protect_raw(sexp);
1489 }
1490 self.pairs.push((name.into(), sexp));
1491 self
1492 }
1493
1494 /// Append an arbitrary SEXP under a name, protected like
1495 /// [`push`](Self::push). Used by the serde split-shape writer to carry
1496 /// the caller-supplied empty-`Ok` sentinel, which is deliberately not a
1497 /// `DataFrame`.
1498 ///
1499 /// # Safety
1500 ///
1501 /// `sexp` must be a valid R object; R main thread (constructor invariant).
1502 #[cfg(feature = "serde")]
1503 #[must_use]
1504 pub(crate) unsafe fn push_raw<S: Into<String>>(mut self, name: S, sexp: SEXP) -> Self {
1505 unsafe {
1506 self.scope.protect_raw(sexp);
1507 }
1508 self.pairs.push((name.into(), sexp));
1509 self
1510 }
1511
1512 /// Number of entries pushed so far.
1513 pub fn len(&self) -> usize {
1514 self.pairs.len()
1515 }
1516
1517 /// Whether no entries have been pushed yet.
1518 pub fn is_empty(&self) -> bool {
1519 self.pairs.is_empty()
1520 }
1521
1522 /// Consume the builder and return the assembled named [`List`].
1523 ///
1524 /// The returned `List`'s SEXP is *not* separately protected on return — the
1525 /// caller takes responsibility for protection (typically by immediately
1526 /// handing it back to R via the `.Call` return path). This matches the
1527 /// contract of [`List::from_raw_pairs`](crate::list::List::from_raw_pairs).
1528 pub fn build(self) -> crate::list::List {
1529 // pairs[i].1 is protected by self.scope; from_raw_pairs protects the
1530 // assembled VECSXP and STRSXP during construction. When self drops at
1531 // this function's exit, the input SEXPs are unprotected — but they are
1532 // now children of the returned list, so they remain reachable.
1533 crate::list::List::from_raw_pairs(self.pairs)
1534 }
1535}
1536
1537impl Default for NamedDataFrameListBuilder {
1538 fn default() -> Self {
1539 Self::new()
1540 }
1541}
1542// endregion
1543
1544// region: Debug impl
1545
1546impl std::fmt::Debug for DataFrame {
1547 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1548 f.debug_struct("DataFrame")
1549 .field("nrow", &self.nrow())
1550 .field("ncol", &self.ncol())
1551 .finish()
1552 }
1553}
1554
1555#[cfg(test)]
1556mod tests {
1557 use super::*;
1558
1559 #[test]
1560 fn test_data_frame_error_display() {
1561 let err = DataFrameError::NotDataFrame;
1562 assert_eq!(err.to_string(), "object does not inherit from data.frame");
1563
1564 let err = DataFrameError::NoNames;
1565 assert_eq!(err.to_string(), "data.frame has no column names");
1566
1567 let err = DataFrameError::UnequalLengths {
1568 expected: 3,
1569 column: "y".to_string(),
1570 actual: 5,
1571 };
1572 assert_eq!(err.to_string(), "column \"y\" has length 5 (expected 3)");
1573
1574 let err = DataFrameError::UnnamedColumns;
1575 assert_eq!(
1576 err.to_string(),
1577 "cannot create data frame from unnamed list elements"
1578 );
1579
1580 let err = DataFrameError::NoSuchColumn("g".to_string());
1581 assert_eq!(err.to_string(), "no such column: \"g\"");
1582 }
1583
1584 // region: NamedDataFrameListBuilder structural invariants
1585
1586 /// A new builder has zero length and reports is_empty().
1587 #[test]
1588 fn builder_new_is_empty() {
1589 let b = NamedDataFrameListBuilder::default();
1590 assert_eq!(b.len(), 0);
1591 assert!(b.is_empty());
1592 }
1593
1594 /// with_capacity reserves space but the builder is still empty.
1595 #[test]
1596 fn builder_with_capacity_starts_empty() {
1597 let b = NamedDataFrameListBuilder::with_capacity(8);
1598 assert_eq!(b.len(), 0);
1599 assert!(b.is_empty());
1600 }
1601
1602 /// The builder's scope count starts at zero (no protections yet).
1603 #[test]
1604 fn builder_scope_count_zero_before_push() {
1605 let b = NamedDataFrameListBuilder::new();
1606 assert_eq!(b.scope.count(), 0);
1607 }
1608 // endregion
1609}
1610// endregion