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::{SexpClassError, SexpError, TryFromSexp};
38use crate::into_r::IntoR;
39use crate::list::{List, NamedList};
40use crate::typed_list::{TypedList, TypedListError, TypedListSpec, validate_list};
41use crate::{IsNA, SEXP, SEXPTYPE, SexpExt};
42use std::ffi::CStr;
43
44pub mod group;
45pub use group::{GroupDeclaration, GroupKey, GroupedDataFrame, RealKey, group_rows};
46
47mod shape;
48pub use shape::{DataFrameShape, SplitResults};
49
50// region: Error type
51
52/// Error returned by any [`DataFrame`] construction, read, or conversion path.
53///
54/// This is the single data-frame error contract: the row-buffer build path, the serde
55/// columnar path, the parallel R→Rust reader, and validation all surface a `DataFrameError`.
56#[derive(Debug, Clone)]
57pub enum DataFrameError {
58 /// The SEXP is not a VECSXP; carries the SEXPTYPE it was.
59 NotList(SEXPTYPE),
60 /// The object does not inherit from `data.frame`.
61 NotDataFrame,
62 /// The list has no `names` attribute (columns must be named).
63 NoNames,
64 /// Could not extract `nrow` from `row.names` attribute.
65 BadRowNames(String),
66 /// Columns have unequal lengths (when promoting from a named list).
67 UnequalLengths {
68 /// First column length encountered.
69 expected: usize,
70 /// Every mismatched column, in order, with its actual length.
71 columns: Vec<(String, usize)>,
72 },
73 /// A row could not be turned into named columns (e.g. unnamed list elements
74 /// in a `IntoList`-derived row). Replaces the old `panic!` on this path.
75 UnnamedColumns,
76 /// [`DataFrame::group_by`] referenced a column name that does not exist.
77 NoSuchColumn(String),
78 /// [`DataFrame::group_by_multi`] was called with an empty column slice.
79 EmptyGroupColumns,
80 /// [`DataFrame::group_by`] on a column type with no grouping semantics
81 /// (list-columns, complex, raw, bit64 `integer64`, …).
82 UnsupportedGroupColumn {
83 /// The offending column name.
84 column: String,
85 /// Its SEXPTYPE (or `"integer64"`), rendered for the message.
86 type_of: String,
87 },
88 /// [`DataFrame::group_by_metadata`] was called on a frame that does not
89 /// inherit from `grouped_df` or `rowwise_df`.
90 NotGroupedDataFrame,
91 /// A frame inherits from `grouped_df` or `rowwise_df`, but its `groups`
92 /// attribute is missing or is not a data frame with at least one column.
93 /// dplyr treats such a frame as corrupt, not as ungrouped ("The `groups`
94 /// attribute must be a data frame.").
95 GroupsNotDataFrame,
96 /// The last column of a grouped frame's `groups` attribute is not called
97 /// `.rows` (dplyr: "The last column of the `groups` attribute must be
98 /// called `.rows`.").
99 MissingGroupRows,
100 /// The `.rows` column of a grouped frame's `groups` attribute is not a
101 /// list of integer vectors (dplyr: "The `.rows` column must be list of
102 /// one-based integer vectors."). Doubles are rejected, as in dplyr.
103 BadGroupRows {
104 /// The 0-based group (row of the `groups` frame) whose element is not
105 /// an integer vector, or `None` when the `.rows` column itself is not
106 /// a list. The message numbers groups from 1, as R does.
107 group: Option<usize>,
108 /// R's `typeof()` of the offending element (or of the `.rows`
109 /// column), rendered for the message.
110 type_of: String,
111 },
112 /// A `.rows` index was `< 1` or `> nrow` of the source frame: the
113 /// grouping metadata is stale. The message names the usual cause (the
114 /// frame was subset without dplyr loaded) and the fix.
115 GroupIndexOutOfRange {
116 /// The 0-based group whose `.rows` carried the bad index; the message
117 /// numbers it from 1, as R does.
118 group: usize,
119 /// The offending 1-based index value.
120 value: i64,
121 /// The source frame's row count (valid indices are `1..=nrow`).
122 nrow: usize,
123 },
124 /// No group's `.rows` holds this row of the source frame: the grouping
125 /// metadata is stale (the message names the cause and the fix).
126 GroupRowUncovered {
127 /// The first uncovered 0-based row; the message numbers it from 1.
128 row: usize,
129 /// The source frame's row count.
130 nrow: usize,
131 },
132 /// Two `.rows` entries hold the same row of the source frame (in two
133 /// groups, or twice in one): the grouping metadata is stale (the message
134 /// names the cause and the fix).
135 GroupRowDuplicated {
136 /// The 0-based row; the message numbers it from 1.
137 row: usize,
138 /// The 0-based group that held the row first.
139 first_group: usize,
140 /// The 0-based group that held it again (equal to `first_group` for a
141 /// row listed twice in one group).
142 second_group: usize,
143 },
144 /// A serde-driven schema/serialize/deserialize failure (the bridged
145 /// `RSerdeError` text) or another conversion failure carried as a message.
146 Conversion(String),
147}
148
149impl std::fmt::Display for DataFrameError {
150 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
151 match self {
152 DataFrameError::NotList(actual) => write!(
153 f,
154 "expected a data frame, got {}",
155 crate::typed_list::sexptype_name(*actual)
156 ),
157 DataFrameError::NotDataFrame => write!(f, "object does not inherit from data.frame"),
158 DataFrameError::NoNames => write!(f, "data.frame has no column names"),
159 DataFrameError::BadRowNames(msg) => {
160 write!(f, "could not extract nrow from row.names: {}", msg)
161 }
162 DataFrameError::UnequalLengths { expected, columns } => {
163 for (i, (column, actual)) in columns.iter().enumerate() {
164 if i > 0 {
165 write!(f, "; ")?;
166 }
167 write!(
168 f,
169 "column {column:?} has length {actual} (expected {expected})"
170 )?;
171 }
172 Ok(())
173 }
174 DataFrameError::UnnamedColumns => {
175 write!(f, "cannot create data frame from unnamed list elements")
176 }
177 DataFrameError::NoSuchColumn(name) => {
178 write!(f, "no such column: {:?}", name)
179 }
180 DataFrameError::EmptyGroupColumns => {
181 write!(f, "group_by_multi requires at least one column")
182 }
183 DataFrameError::UnsupportedGroupColumn { column, type_of } => write!(
184 f,
185 "cannot group by column {:?} ({}): supported key types are factor, \
186 character, integer, logical, and double (including Date and POSIXct)",
187 column, type_of
188 ),
189 DataFrameError::NotGroupedDataFrame => write!(
190 f,
191 "not a grouped_df: the frame does not inherit from grouped_df or rowwise_df; \
192 use group_by/group_by_multi to compute grouping instead"
193 ),
194 DataFrameError::GroupsNotDataFrame => write!(
195 f,
196 "not a valid grouped_df or rowwise_df: the `groups` attribute must be a data \
197 frame. {}",
198 CORRUPT_GROUPS_HINT
199 ),
200 DataFrameError::MissingGroupRows => write!(
201 f,
202 "not a valid grouped_df or rowwise_df: the last column of the `groups` \
203 attribute must be called `.rows`. {}",
204 CORRUPT_GROUPS_HINT
205 ),
206 DataFrameError::BadGroupRows {
207 group: None,
208 type_of,
209 } => write!(
210 f,
211 "not a valid grouped_df or rowwise_df: the `.rows` column must be a list of \
212 one-based integer vectors, not {}. {}",
213 type_of, CORRUPT_GROUPS_HINT
214 ),
215 DataFrameError::BadGroupRows {
216 group: Some(group),
217 type_of,
218 } => write!(
219 f,
220 "not a valid grouped_df or rowwise_df: the `.rows` column must be a list of \
221 one-based integer vectors, but the element for group {} is {}. {}",
222 group + 1,
223 type_of,
224 CORRUPT_GROUPS_HINT
225 ),
226 DataFrameError::GroupIndexOutOfRange { group, value, nrow } => write!(
227 f,
228 "grouped_df `.rows` index {} for group {} is out of range (the frame has \
229 {} rows; valid indices are 1..={}). {}",
230 value,
231 group + 1,
232 nrow,
233 nrow,
234 STALE_GROUPS_HINT
235 ),
236 DataFrameError::GroupRowUncovered { row, nrow } => write!(
237 f,
238 "grouped_df `.rows` do not cover row {} (the frame has {} rows; each row \
239 belongs to exactly one group). {}",
240 row + 1,
241 nrow,
242 STALE_GROUPS_HINT
243 ),
244 DataFrameError::GroupRowDuplicated {
245 row,
246 first_group,
247 second_group,
248 } if first_group == second_group => write!(
249 f,
250 "grouped_df `.rows` list row {} twice in group {} (each row belongs to \
251 exactly one group). {}",
252 row + 1,
253 first_group + 1,
254 STALE_GROUPS_HINT
255 ),
256 DataFrameError::GroupRowDuplicated {
257 row,
258 first_group,
259 second_group,
260 } => write!(
261 f,
262 "grouped_df `.rows` put row {} in both group {} and group {} (each row \
263 belongs to exactly one group). {}",
264 row + 1,
265 first_group + 1,
266 second_group + 1,
267 STALE_GROUPS_HINT
268 ),
269 DataFrameError::Conversion(msg) => write!(f, "{}", msg),
270 }
271 }
272}
273
274impl std::error::Error for DataFrameError {}
275
276/// The cause and the fix that the stale-`.rows` errors
277/// ([`DataFrameError::GroupIndexOutOfRange`],
278/// [`DataFrameError::GroupRowUncovered`],
279/// [`DataFrameError::GroupRowDuplicated`]) append to their message.
280const STALE_GROUPS_HINT: &str = "The `groups` metadata is stale: the grouped frame was \
281 likely subset or reordered without dplyr loaded, which keeps the old `groups` attribute. \
282 Regroup it with dplyr::group_by() (with dplyr loaded, `[` regroups by itself), or read just \
283 the grouping declaration (DataFrame::group_declaration) and group the current rows.";
284
285/// The fix that the corrupt-`groups` errors ([`DataFrameError::GroupsNotDataFrame`],
286/// [`DataFrameError::MissingGroupRows`], [`DataFrameError::BadGroupRows`])
287/// append to their message.
288const CORRUPT_GROUPS_HINT: &str = "dplyr's validators reject such a frame too. Strip the \
289 grouping with dplyr::ungroup(), then regroup it with dplyr::group_by().";
290
291#[cfg(feature = "serde")]
292impl From<crate::serde::RSerdeError> for DataFrameError {
293 fn from(e: crate::serde::RSerdeError) -> Self {
294 DataFrameError::Conversion(e.to_string())
295 }
296}
297// endregion
298
299// region: DataFrame — cheap, unrooted data.frame view
300
301/// A cheap `Copy` view over a validated R `data.frame`.
302///
303/// The view carries no GC root. It is sound while an R `.Call` argument frame,
304/// a [`ProtectScope`](crate::ProtectScope), or an owning [`BuiltDataFrame`]
305/// keeps the SEXP reachable. Rust-side constructors never return this bare
306/// view; they return `BuiltDataFrame`.
307///
308/// # Building
309///
310/// Prefer the [`IntoDataFrame`] trait on your data (it returns an owned,
311/// GC-rooted [`BuiltDataFrame`] that `Deref`s to `DataFrame`):
312///
313/// ```ignore
314/// let df = rows.into_dataframe()?;
315/// ```
316///
317/// or the closure-fill `DataFrame::builder` for heterogeneous parallel column fill
318/// (`feature = "rayon"`).
319///
320/// # Reading
321///
322/// Wrap an incoming SEXP with [`DataFrame::from_sexp`] (or accept `DataFrame` directly as a
323/// `#[miniextendr]` argument), then pull typed columns with [`DataFrame::column`], or
324/// deserialize whole rows with [`FromDataFrame`].
325#[derive(Clone, Copy)]
326pub struct DataFrame {
327 sexp: SEXP,
328}
329
330impl DataFrame {
331 /// Wrap an already-built `data.frame` SEXP without re-validation.
332 ///
333 /// Used by the column assemblers, which produce a well-formed `data.frame` by
334 /// construction.
335 ///
336 /// # Safety
337 ///
338 /// `sexp` must be a VECSXP with the `data.frame` class and consistent `row.names`.
339 #[inline]
340 pub unsafe fn from_built_sexp(sexp: SEXP) -> Self {
341 Self { sexp }
342 }
343
344 /// Wrap an existing R `data.frame` SEXP, validating it.
345 ///
346 /// Validates that the object:
347 /// 1. Is a VECSXP (list)
348 /// 2. Inherits from `"data.frame"`
349 /// 3. Has a `names` attribute
350 /// 4. Has extractable `row.names` for nrow
351 ///
352 /// # Errors
353 ///
354 /// Returns [`DataFrameError`] if validation fails.
355 pub fn from_sexp(sexp: SEXP) -> Result<Self, DataFrameError> {
356 let stype = sexp.type_of();
357 if stype != SEXPTYPE::VECSXP {
358 return Err(DataFrameError::NotList(stype));
359 }
360 if !sexp.is_data_frame() {
361 return Err(DataFrameError::NotDataFrame);
362 }
363 // Require a names attribute (columns must be named).
364 let list = unsafe { List::from_raw(sexp) };
365 NamedList::new(list).ok_or(DataFrameError::NoNames)?;
366 // Confirm nrow is extractable.
367 extract_nrow(sexp)?;
368 Ok(Self { sexp })
369 }
370
371 // region: Read API (R → Rust column / row access)
372
373 /// Get a column by name, converting to type `T`.
374 ///
375 /// Returns `None` if the column name is not found or conversion fails.
376 ///
377 /// `T` may be a vector/collection target (`Vec<f64>`, `Vec<i32>`,
378 /// `Vec<String>`, …) — the natural shape for a column — or a scalar for a
379 /// length-1 column. The conversion error is discarded (that is what makes
380 /// this return `Option`), so `T::Error` is unconstrained; use
381 /// [`column_raw`](Self::column_raw) when you need the error.
382 #[inline]
383 pub fn column<T>(&self, name: &str) -> Option<T>
384 where
385 T: TryFromSexp,
386 {
387 self.named_list().get(name)
388 }
389
390 /// Get a column by 0-based index, converting to type `T`.
391 ///
392 /// As with [`column`](Self::column), `T` may be a vector/collection or a
393 /// scalar target type; the conversion error is discarded.
394 #[inline]
395 pub fn column_index<T>(&self, idx: usize) -> Option<T>
396 where
397 T: TryFromSexp,
398 {
399 let idx_isize: isize = idx.try_into().ok()?;
400 self.named_list().get_index(idx_isize)
401 }
402
403 /// Get the raw SEXP for a column by name.
404 #[inline]
405 pub fn column_raw(&self, name: &str) -> Option<SEXP> {
406 self.named_list().get_raw(name)
407 }
408
409 /// Number of rows.
410 #[inline]
411 pub fn nrow(&self) -> usize {
412 extract_nrow(self.sexp).unwrap_or(0)
413 }
414
415 /// Number of columns.
416 #[inline]
417 pub fn ncol(&self) -> usize {
418 self.sexp.len()
419 }
420
421 /// Collect column names in column order.
422 pub fn names(&self) -> Vec<String> {
423 let names_sexp = self.sexp.get_names();
424 if names_sexp.is_nil() {
425 return Vec::new();
426 }
427 let n = self.sexp.len() as isize;
428 (0..n)
429 .map(|i| names_sexp.string_elt_str(i).unwrap_or("").to_string())
430 .collect()
431 }
432
433 /// Check whether a column name exists.
434 #[inline]
435 pub fn contains_column(&self, name: &str) -> bool {
436 self.named_list().contains(name)
437 }
438
439 /// Validate the data frame's column types against a [`TypedListSpec`].
440 pub fn validate(&self, spec: &TypedListSpec) -> Result<TypedList, TypedListError> {
441 validate_list(unsafe { List::from_raw(self.sexp) }, spec)
442 }
443 // endregion
444
445 // region: Conversions
446
447 /// Get the underlying [`List`].
448 #[inline]
449 pub fn as_list(&self) -> List {
450 unsafe { List::from_raw(self.sexp) }
451 }
452
453 /// Get the underlying SEXP.
454 #[inline]
455 pub fn as_sexp(&self) -> SEXP {
456 self.sexp
457 }
458
459 /// Build the `NamedList` index for O(1) column-by-name access.
460 #[inline]
461 fn named_list(&self) -> NamedList {
462 NamedList::new(unsafe { List::from_raw(self.sexp) })
463 .expect("DataFrame always carries a names attribute")
464 }
465 // endregion
466
467 // region: Post-assembly editing (absorbed from the old serde columnar assembler)
468
469 /// Rename a column. No-op if `from` doesn't match any column name.
470 ///
471 /// In-place edit of the `names` attribute — returns the **same** frame (and
472 /// therefore inherits whatever root the input already had), unlike the
473 /// new-frame producers ([`drop`](Self::drop) and friends), which return a
474 /// rooted [`BuiltDataFrame`]. On a `BuiltDataFrame` receiver the inherent
475 /// forward ([`BuiltDataFrame::rename`]) keeps the handle instead.
476 ///
477 /// Other attributes are untouched, so renaming a grouping column of a
478 /// dplyr `grouped_df` leaves its `groups` naming the old column (#1702).
479 pub fn rename(self, from: &str, to: &str) -> Self {
480 unsafe {
481 // Root `self.sexp` so its `names` attribute survives the
482 // `SEXP::charsxp` (Rf_mkCharLenCE) allocation below, which can GC.
483 let _guard = crate::OwnedProtect::new(self.sexp);
484 let names_sexp = self.sexp.get_names();
485 if names_sexp == SEXP::nil() {
486 return self;
487 }
488 let ncol = names_sexp.xlength();
489 for i in 0..ncol {
490 if col_name(names_sexp, i) == from {
491 names_sexp.set_string_elt(i, SEXP::charsxp(to));
492 break;
493 }
494 }
495 }
496 self
497 }
498
499 /// Strip a prefix from all column names that start with it.
500 ///
501 /// In-place edit of the `names` attribute — returns the **same** frame; see
502 /// [`rename`](Self::rename)'s rooting note.
503 pub fn strip_prefix(self, prefix: &str) -> Self {
504 unsafe {
505 // Root `self.sexp` so its `names` attribute survives the
506 // `SEXP::charsxp` (Rf_mkCharLenCE) allocation below, which can GC.
507 let _guard = crate::OwnedProtect::new(self.sexp);
508 let names_sexp = self.sexp.get_names();
509 if names_sexp == SEXP::nil() {
510 return self;
511 }
512 let ncol = names_sexp.xlength();
513 for i in 0..ncol {
514 let name = col_name(names_sexp, i);
515 if let Some(stripped) = name.strip_prefix(prefix) {
516 names_sexp.set_string_elt(i, SEXP::charsxp(stripped));
517 }
518 }
519 }
520 self
521 }
522
523 /// Remove a column by name. No-op if the column doesn't exist.
524 ///
525 /// # Frame attributes
526 ///
527 /// The new frame keeps every attribute of the input except `names`: its
528 /// `class`, `row.names`, and anything else on the frame (a `units` map, a
529 /// package's metadata), as `dplyr::select()` and tibble's `[` do. This
530 /// holds for every column producer ([`select`](Self::select),
531 /// [`prepend_column`](Self::prepend_column),
532 /// [`with_column`](Self::with_column)).
533 ///
534 /// A dplyr `grouped_df` / `rowwise_df` keeps its grouping while every
535 /// grouping column is still there, unchanged. Removing or replacing one
536 /// returns the ungrouped frame instead (no `groups` attribute, no
537 /// `grouped_df` / `rowwise_df` class), where dplyr's `[` would regroup by
538 /// the grouping columns that remain.
539 ///
540 /// # Rooting
541 ///
542 /// Returns an owned, GC-rooted [`BuiltDataFrame`] (#1247): the result frame
543 /// is rooted before this method returns, so holding it across R allocations
544 /// is safe by construction. The no-op path re-roots the input frame (each
545 /// `BuiltDataFrame` releases exactly its own root — `R_PreserveObject`
546 /// entries stack, so re-rooting an already-rooted SEXP is sound).
547 ///
548 /// The input view must be reachable when calling (the usual [`DataFrame`]
549 /// view contract: an R `.Call` argument frame, a
550 /// [`ProtectScope`](crate::ProtectScope), or a live [`BuiltDataFrame`]).
551 #[must_use]
552 pub fn drop(self, col: &str) -> BuiltDataFrame {
553 unsafe {
554 let names_sexp = self.sexp.get_names();
555 if names_sexp == SEXP::nil() {
556 return BuiltDataFrame::adopt(self);
557 }
558 let ncol = names_sexp.xlength();
559 let drop_idx = (0..ncol).find(|&i| col_name(names_sexp, i) == col);
560 let Some(drop_idx) = drop_idx else {
561 return BuiltDataFrame::adopt(self);
562 };
563
564 let new_ncol = ncol - 1;
565 let new_list = crate::OwnedProtect::new(SEXP::alloc_list(new_ncol));
566 let new_names = crate::OwnedProtect::new(SEXP::alloc_strsxp(new_ncol));
567
568 let mut j: isize = 0;
569 for i in 0..ncol {
570 if i == drop_idx {
571 continue;
572 }
573 new_list.set_vector_elt(j, self.sexp.vector_elt(i));
574 new_names.set_string_elt(j, names_sexp.string_elt(i));
575 j += 1;
576 }
577
578 new_list.set_names(*new_names);
579 copy_frame_attrs(self.sexp, *new_list, true);
580
581 // Root the new frame before the OwnedProtect guards drop:
582 // `adopt_sexp` preserves first (protecting `new_list` across its own
583 // cons-cell allocation), the guards UNPROTECT after — no gap.
584 BuiltDataFrame::adopt_sexp(*new_list)
585 }
586 }
587
588 /// Keep only the named columns, in the order given. Unknown names are skipped.
589 ///
590 /// Keeps the frame's attributes and, while every grouping column is
591 /// selected, its dplyr grouping; see [`drop`](Self::drop)'s frame-attribute
592 /// note.
593 ///
594 /// # Rooting
595 ///
596 /// Returns an owned, GC-rooted [`BuiltDataFrame`] — see
597 /// [`drop`](Self::drop)'s rooting note (#1247).
598 #[must_use]
599 pub fn select(self, cols: &[&str]) -> BuiltDataFrame {
600 unsafe {
601 let names_sexp = self.sexp.get_names();
602 if names_sexp == SEXP::nil() {
603 return BuiltDataFrame::adopt(self);
604 }
605 let ncol = names_sexp.xlength();
606
607 let indices: Vec<isize> = cols
608 .iter()
609 .filter_map(|&want| (0..ncol).find(|&i| col_name(names_sexp, i) == want))
610 .collect();
611
612 let new_ncol: isize = indices.len().try_into().expect("ncol overflow");
613 let new_list = crate::OwnedProtect::new(SEXP::alloc_list(new_ncol));
614 let new_names = crate::OwnedProtect::new(SEXP::alloc_strsxp(new_ncol));
615
616 for (j, &src_idx) in indices.iter().enumerate() {
617 let j_r: isize = j.try_into().expect("index overflow");
618 new_list.set_vector_elt(j_r, self.sexp.vector_elt(src_idx));
619 new_names.set_string_elt(j_r, names_sexp.string_elt(src_idx));
620 }
621
622 new_list.set_names(*new_names);
623 copy_frame_attrs(self.sexp, *new_list, true);
624
625 // Root before the guards drop (see `drop` for the ordering argument).
626 BuiltDataFrame::adopt_sexp(*new_list)
627 }
628 }
629
630 /// Keep only the rows at the given 0-based indices, in order.
631 ///
632 /// Subsets every column to the specified rows and rebuilds compact integer
633 /// `row.names`. Used by the enum reader to densify a flattened sub-frame
634 /// before recursing into the inner type's reader, and by group iteration.
635 ///
636 /// A row is a row of every column, as in `df[idx, , drop = FALSE]`:
637 ///
638 /// - A vector or list column is gathered by element, and its element
639 /// `names`, if any, are subset with the values.
640 /// - A column with a `dim` (an `I(matrix)` column, or an array) is gathered
641 /// along its first dimension: `dim` becomes `c(length(idx), dim[-1])` and
642 /// the first `dimnames` component (the row names) is subset, the rest kept.
643 /// Arrays of three or more dimensions follow `vctrs::vec_slice()` here;
644 /// base `[.data.frame` subsets those by element.
645 /// - A data.frame column (a packed column) is subset the same way,
646 /// recursively.
647 ///
648 /// Each column keeps all of its other attributes ([`SexpExt::copy_most_attrib`], the
649 /// `vctrs::vec_slice()` rule), so a `POSIXct` column keeps its `tzone`, a
650 /// `difftime` its `units` and an `I()` column its `AsIs` class.
651 ///
652 /// # Frame attributes
653 ///
654 /// The new frame keeps every attribute of the input frame except `names`
655 /// (the columns' names, as before) and `row.names` (fresh, `1:length(idx)`):
656 /// its `class`, and anything else on the frame, such as a `units` map
657 /// describing some columns or a package's metadata. This is what
658 /// `dplyr::slice()` (`dplyr_row_slice()` / `dplyr_reconstruct()`),
659 /// `vctrs::vec_slice()`, tibble's `[` and base `[.data.frame` keep on a row
660 /// subset. A packed data.frame column keeps its own the same way.
661 ///
662 /// dplyr's grouping does not survive. A `grouped_df` or `rowwise_df` caches
663 /// each group's row indices in its `groups` attribute, which no longer
664 /// matches the selected rows, so the result has no `groups` attribute and
665 /// no `grouped_df` / `rowwise_df` class: it is the ungrouped frame (a
666 /// tibble, for a dplyr input). `dplyr::slice()` recomputes the groups
667 /// instead. To keep the grouping, read it from the input with
668 /// [`group_declaration`](Self::group_declaration) and regroup the result,
669 /// in Rust with [`group_by_multi`](Self::group_by_multi) over its
670 /// [`vars`](crate::dataframe::GroupDeclaration::vars), or in R with
671 /// `dplyr::grouped_df(out, dplyr::group_vars(df))`.
672 ///
673 /// Any other attribute indexed by row is copied as it is. A package that
674 /// keeps one must rebuild it after a row subset, as dplyr's extension guide
675 /// (`?dplyr_row_slice`) asks of `dplyr_row_slice()` methods.
676 ///
677 /// # PROTECT discipline
678 ///
679 /// Allocates new column vectors (and `dim` / `dimnames` for matrix columns)
680 /// while the output list is `OwnedProtect`ed, rooting each new column in it
681 /// before the next allocation.
682 ///
683 /// # Rooting
684 ///
685 /// Returns an owned, GC-rooted [`BuiltDataFrame`] — see
686 /// [`drop`](Self::drop)'s rooting note (#1247).
687 #[must_use]
688 pub fn select_rows(&self, idx: &[usize]) -> BuiltDataFrame {
689 unsafe {
690 let out = select_frame_rows(self.sexp, idx);
691 // Root before the guard drops (see `drop` for the ordering argument).
692 BuiltDataFrame::adopt_sexp(*out)
693 }
694 }
695
696 /// Insert a column at index 0 (leftmost), removing any same-named column first.
697 ///
698 /// Keeps the frame's attributes and its dplyr grouping, unless `name`
699 /// replaces a grouping column; see [`drop`](Self::drop)'s frame-attribute
700 /// note.
701 ///
702 /// # Rooting
703 ///
704 /// Returns an owned, GC-rooted [`BuiltDataFrame`] — see
705 /// [`drop`](Self::drop)'s rooting note (#1247). `column` must be kept
706 /// reachable by the caller (e.g. under an
707 /// [`OwnedProtect`](crate::OwnedProtect)) across this call — the new frame
708 /// is allocated before `column` is stored into it.
709 #[must_use]
710 pub fn prepend_column(self, name: &str, column: SEXP) -> BuiltDataFrame {
711 // `drop` returns a rooted handle, so the intermediate frame survives the
712 // allocations below (pre-#1247 this was an unrooted view — a latent UAF
713 // window inside this very method).
714 let cleaned = self.drop(name);
715 unsafe {
716 let names_sexp = cleaned.as_sexp().get_names();
717 let ncol = if names_sexp == SEXP::nil() {
718 0
719 } else {
720 names_sexp.xlength()
721 };
722
723 let new_ncol = ncol + 1;
724 let new_list = crate::OwnedProtect::new(SEXP::alloc_list(new_ncol));
725 let new_names = crate::OwnedProtect::new(SEXP::alloc_strsxp(new_ncol));
726
727 new_list.set_vector_elt(0, column);
728 new_names.set_string_elt(0, SEXP::charsxp(name));
729
730 for i in 0..ncol {
731 new_list.set_vector_elt(i + 1, cleaned.as_sexp().vector_elt(i));
732 new_names.set_string_elt(i + 1, names_sexp.string_elt(i));
733 }
734
735 new_list.set_names(*new_names);
736 copy_frame_attrs(cleaned.as_sexp(), *new_list, true);
737
738 // Root before the guards (and `cleaned`) drop — see `drop` for the
739 // ordering argument.
740 BuiltDataFrame::adopt_sexp(*new_list)
741 }
742 }
743
744 /// Upsert a column: replace the column named `name` if it exists, else append.
745 ///
746 /// The append path builds a new frame that keeps the input's attributes
747 /// and dplyr grouping (see [`drop`](Self::drop)'s frame-attribute note).
748 /// The replace path stores `column` into the input frame itself and
749 /// returns that frame, attributes untouched, so replacing a grouping
750 /// column leaves dplyr's `groups` stale (#1702).
751 ///
752 /// # Rooting
753 ///
754 /// Both paths return an owned, GC-rooted [`BuiltDataFrame`] (#1247) — the
755 /// in-place replace path re-roots the same frame it received (sound:
756 /// `R_PreserveObject` entries stack; this handle releases exactly one).
757 /// `column` must be kept reachable by the caller across this call — the
758 /// append path allocates the new frame before `column` is stored into it.
759 #[must_use]
760 pub fn with_column(self, name: &str, column: SEXP) -> BuiltDataFrame {
761 unsafe {
762 let names_sexp = self.sexp.get_names();
763 if names_sexp == SEXP::nil() {
764 return BuiltDataFrame::adopt(self);
765 }
766 let ncol = names_sexp.xlength();
767 for i in 0..ncol {
768 if col_name(names_sexp, i) == name {
769 // set_vector_elt does not allocate; `column` is reachable
770 // from the (caller-rooted) frame before `adopt` allocates
771 // its cons cell.
772 self.sexp.set_vector_elt(i, column);
773 return BuiltDataFrame::adopt(self);
774 }
775 }
776
777 let new_ncol = ncol + 1;
778 let new_list = crate::OwnedProtect::new(SEXP::alloc_list(new_ncol));
779 let new_names = crate::OwnedProtect::new(SEXP::alloc_strsxp(new_ncol));
780
781 for i in 0..ncol {
782 new_list.set_vector_elt(i, self.sexp.vector_elt(i));
783 new_names.set_string_elt(i, names_sexp.string_elt(i));
784 }
785 new_list.set_vector_elt(ncol, column);
786 new_names.set_string_elt(ncol, SEXP::charsxp(name));
787
788 new_list.set_names(*new_names);
789 copy_frame_attrs(self.sexp, *new_list, true);
790
791 // Root before the guards drop (see `drop` for the ordering argument).
792 BuiltDataFrame::adopt_sexp(*new_list)
793 }
794 }
795 // endregion
796
797 // region: builder (ex-RDataFrameBuilder, #768)
798
799 /// Start a closure-per-column builder yielding a rooted [`BuiltDataFrame`].
800 ///
801 /// The heterogeneous-column analogue of `with_r_matrix`: each column buffer is R memory
802 /// filled by a per-column closure. Available regardless of the `rayon` feature (#1055);
803 /// the columns are filled **in parallel** when `rayon` is enabled and **serially**
804 /// otherwise — the resulting `data.frame` is identical either way.
805 ///
806 /// ```ignore
807 /// let df = DataFrame::builder(1000)
808 /// .column::<f64>("x", |chunk, off| for (i, v) in chunk.iter_mut().enumerate() { *v = (off + i) as f64 })
809 /// .column_str("label", |i| Some(format!("row{i}")))
810 /// .build();
811 /// ```
812 #[inline]
813 pub fn builder(nrow: usize) -> crate::dataframe_builder::RDataFrameBuilder {
814 crate::dataframe_builder::RDataFrameBuilder::new(nrow)
815 }
816 // endregion
817}
818// endregion
819
820// region: column-name helper + frame-attribute copy
821
822/// Read the i-th column name from a STRSXP names vector.
823///
824/// # Safety
825/// `names_sexp` must be a valid STRSXP with at least `i + 1` elements.
826unsafe fn col_name(names_sexp: SEXP, i: isize) -> &'static str {
827 unsafe {
828 let s = names_sexp.string_elt(i);
829 let p = s.r_char();
830 std::ffi::CStr::from_ptr(p).to_str().unwrap_or("")
831 }
832}
833
834/// The body of [`DataFrame::select_rows`]: row-subset the data.frame VECSXP
835/// `frame` at the 0-based rows `idx`. Shared with packed data.frame columns.
836///
837/// Returns the new frame still protected; the caller roots it before the guard
838/// drops.
839///
840/// # Safety
841///
842/// R main thread; `frame` is a data.frame list whose columns all have at least
843/// `max(idx) + 1` rows.
844unsafe fn select_frame_rows(frame: SEXP, idx: &[usize]) -> crate::OwnedProtect {
845 use crate::SexpExt as _;
846
847 unsafe {
848 let names_sexp = frame.get_names();
849 let ncol = frame.xlength();
850 let new_nrow = i32::try_from(idx.len()).expect("row count exceeds i32::MAX");
851
852 let new_list = crate::OwnedProtect::new(SEXP::alloc_list(ncol));
853 {
854 let new_names = crate::OwnedProtect::new(SEXP::alloc_strsxp(ncol));
855
856 for col_j in 0..ncol {
857 let src_col = frame.vector_elt(col_j);
858 // Protected on return; rooted in the protected `new_list` before
859 // its guard (the top of the protect stack) drops.
860 let new_col = select_column_rows(src_col, idx);
861 new_list.set_vector_elt(col_j, *new_col);
862 drop(new_col);
863 if names_sexp != SEXP::nil() {
864 new_names.set_string_elt(col_j, names_sexp.string_elt(col_j));
865 }
866 }
867
868 if names_sexp != SEXP::nil() {
869 new_list.set_names(*new_names);
870 }
871 }
872
873 // Every frame attribute of the source (`class` included), with dplyr's
874 // row-indexed `groups` removed. This copies the source's `row.names`
875 // too, so it runs before the fresh ones are set below.
876 copy_frame_attrs(frame, *new_list, false);
877
878 // Set compact integer row.names (c(NA_integer_, -new_nrow)).
879 let (row_names, rn) = crate::into_r::alloc_r_vector::<i32>(2);
880 {
881 let _rn_guard = crate::OwnedProtect::new(row_names);
882 rn[0] = i32::MIN;
883 rn[1] = -new_nrow;
884 new_list.set_row_names(row_names);
885 }
886
887 new_list
888 }
889}
890
891/// Row-subset one data.frame column at the 0-based rows `idx` (see
892/// [`DataFrame::select_rows`] for the per-kind rules). Returns it protected.
893///
894/// # Safety
895///
896/// R main thread; `col` is a valid column with at least `max(idx) + 1` rows.
897unsafe fn select_column_rows(col: SEXP, idx: &[usize]) -> crate::OwnedProtect {
898 use crate::SexpExt as _;
899
900 unsafe {
901 if col.type_of() == SEXPTYPE::VECSXP && col.is_data_frame() {
902 return select_frame_rows(col, idx);
903 }
904
905 let dim = col.get_dim();
906 if dim.type_of() != SEXPTYPE::INTSXP || dim.xlength() == 0 {
907 // A plain vector or list column: a row is an element.
908 let out = crate::OwnedProtect::new(crate::convert::gather_column(col, idx));
909 // Every attribute except names/dim/dimnames, as `vctrs::vec_slice()`
910 // does: `class` and `levels` (factor, Date), `tzone` (POSIXct), `units`
911 // (difftime), labels, and anything else a package keeps on a column.
912 // Base `[` would drop the last kind on an unclassed column; a row
913 // subset should not change a column's meaning.
914 out.copy_most_attrib(col);
915 // Element names are per row, so they are subset with the values.
916 let col_names = col.get_names();
917 if col_names != SEXP::nil() {
918 let new_names =
919 crate::OwnedProtect::new(crate::convert::gather_column(col_names, idx));
920 out.set_names(*new_names);
921 }
922 return out;
923 }
924
925 // A matrix or array column: a row is one index of the first dimension,
926 // taken at every position of the trailing dimensions (column-major).
927 let dims: Vec<usize> = dim
928 .as_slice::<i32>()
929 .iter()
930 .map(|&d| usize::try_from(d).expect("dim entries are non-negative"))
931 .collect();
932 let nrow = dims[0];
933 let trailing: usize = dims[1..].iter().product();
934 let flat: Vec<usize> = (0..trailing)
935 .flat_map(|k| idx.iter().map(move |&row| row + k * nrow))
936 .collect();
937
938 let out = crate::OwnedProtect::new(crate::convert::gather_column(col, &flat));
939 out.copy_most_attrib(col);
940
941 {
942 let (new_dim, dim_vals) = crate::into_r::alloc_r_vector::<i32>(dims.len());
943 let _dim_guard = crate::OwnedProtect::new(new_dim);
944 dim_vals[0] = i32::try_from(idx.len()).expect("row count exceeds i32::MAX");
945 dim_vals[1..].copy_from_slice(&dim.as_slice::<i32>()[1..]);
946 out.set_dim(new_dim);
947 }
948
949 let dimnames = col.get_dimnames();
950 if dimnames != SEXP::nil() {
951 let n = dimnames.xlength();
952 let new_dimnames = crate::OwnedProtect::new(SEXP::alloc_list(n));
953 let row_names = dimnames.vector_elt(0);
954 if row_names != SEXP::nil() {
955 // Unprotected until the `set_vector_elt` right after, which does
956 // not allocate.
957 new_dimnames.set_vector_elt(0, crate::convert::gather_column(row_names, idx));
958 }
959 for i in 1..n {
960 new_dimnames.set_vector_elt(i, dimnames.vector_elt(i));
961 }
962 let dimnames_names = dimnames.get_names();
963 if dimnames_names != SEXP::nil() {
964 new_dimnames.set_names(dimnames_names);
965 }
966 out.set_dimnames(*new_dimnames);
967 }
968
969 out
970 }
971}
972
973/// Copy the frame attributes of `from` onto the new frame `to`.
974///
975/// Every attribute except `names` is copied ([`SexpExt::copy_most_attrib`]), the rule
976/// `vctrs::vec_slice()`, tibble's `[` and `dplyr_reconstruct()` follow:
977/// `class`, `row.names` (verbatim; a producer that changes the rows sets its
978/// own afterwards) and anything else a package keeps on the frame, such as a
979/// `units` map or its metadata.
980///
981/// dplyr's grouping is the exception. A `grouped_df` / `rowwise_df` caches
982/// its groups in a `groups` attribute that names the key columns and holds
983/// each group's row indices, so it is copied only while it still describes
984/// `to`: `same_rows`, and every grouping variable is the very column it was
985/// in `from` (moved, not rebuilt or replaced). Otherwise `to` loses `groups`
986/// and the `grouped_df` / `rowwise_df` class and is the ungrouped frame (a
987/// tibble, for a dplyr input), never a grouped frame whose groups are stale.
988///
989/// # Safety
990///
991/// R main thread; `from` is a data.frame list and `to` a list whose `names`
992/// are set, protected by the caller.
993unsafe fn copy_frame_attrs(from: SEXP, to: SEXP, same_rows: bool) {
994 unsafe {
995 to.copy_most_attrib(from);
996 let grouped = from.inherits_class(c"grouped_df") || from.inherits_class(c"rowwise_df");
997 if grouped && !(same_rows && grouping_columns_kept(from, to)) {
998 ungroup_frame(to);
999 }
1000 }
1001}
1002
1003/// Whether every grouping variable of the grouped frame `from` (the columns
1004/// its `groups` attribute names, `.rows` aside) is a column of `to` that is
1005/// the same SEXP as in `from`. `false` when `groups` is not a data frame.
1006///
1007/// # Safety
1008///
1009/// R main thread; `from` and `to` are data.frame lists.
1010unsafe fn grouping_columns_kept(from: SEXP, to: SEXP) -> bool {
1011 unsafe {
1012 let groups = from.get_attr(crate::sys::Rf_install(c"groups".as_ptr()));
1013 if groups.type_of() != SEXPTYPE::VECSXP || !groups.is_data_frame() {
1014 return false;
1015 }
1016 let vars = groups.get_names();
1017 if vars == SEXP::nil() {
1018 return false;
1019 }
1020 (0..vars.xlength())
1021 .map(|i| col_name(vars, i))
1022 .filter(|&var| var != ".rows")
1023 .all(
1024 |var| match (column_named(from, var), column_named(to, var)) {
1025 (Some(before), Some(after)) => before == after,
1026 _ => false,
1027 },
1028 )
1029 }
1030}
1031
1032/// The first column of the data.frame list `frame` named `name`.
1033///
1034/// # Safety
1035///
1036/// R main thread; `frame` is a list.
1037unsafe fn column_named(frame: SEXP, name: &str) -> Option<SEXP> {
1038 unsafe {
1039 let names = frame.get_names();
1040 if names == SEXP::nil() {
1041 return None;
1042 }
1043 (0..names.xlength())
1044 .find(|&i| col_name(names, i) == name)
1045 .map(|i| frame.vector_elt(i))
1046 }
1047}
1048
1049/// Remove dplyr's grouping from the frame `to`: its `groups` attribute and
1050/// the `grouped_df` / `rowwise_df` entries of its class.
1051///
1052/// # Safety
1053///
1054/// R main thread; `to` is a data.frame list protected by the caller.
1055unsafe fn ungroup_frame(to: SEXP) {
1056 unsafe {
1057 to.set_attr(crate::sys::Rf_install(c"groups".as_ptr()), SEXP::nil());
1058 // Still attached to (and so rooted by) `to` until `set_class` below.
1059 let class = to.get_class();
1060 let kept: Vec<isize> = (0..class.xlength())
1061 .filter(|&i| !matches!(col_name(class, i), "grouped_df" | "rowwise_df"))
1062 .collect();
1063 let new_class = crate::OwnedProtect::new(SEXP::alloc_strsxp(
1064 isize::try_from(kept.len()).expect("class length fits isize"),
1065 ));
1066 for (j, &i) in kept.iter().enumerate() {
1067 new_class.set_string_elt(
1068 isize::try_from(j).expect("class length fits isize"),
1069 class.string_elt(i),
1070 );
1071 }
1072 to.set_class(*new_class);
1073 }
1074}
1075// endregion
1076
1077// region: IntoR / TryFromSexp for DataFrame
1078
1079impl TryFromSexp for DataFrame {
1080 type Error = SexpError;
1081
1082 // As an `Either` arm, a data frame refuses nothing under `no_na`: its
1083 // cells may be missing, and reading them is the function's own business.
1084 // (A `DataFrame` parameter keeps the R guard, which refuses any NA cell.)
1085 #[inline]
1086 fn __mx_input_has_na(&self, _input: SEXP) -> bool {
1087 false
1088 }
1089
1090 fn try_from_sexp(sexp: SEXP) -> Result<Self, Self::Error> {
1091 // Not a data frame at all (a non-list, or a list without the class) is
1092 // a class error, worded `got integer` / `got list` by the argument
1093 // error; a data frame that fails later keeps its own message.
1094 let not_a_data_frame = |actual| {
1095 SexpClassError {
1096 expected: "a data frame",
1097 actual,
1098 }
1099 .into()
1100 };
1101 DataFrame::from_sexp(sexp).map_err(|e| match e {
1102 DataFrameError::NotList(actual) => not_a_data_frame(actual),
1103 DataFrameError::NotDataFrame => not_a_data_frame(SEXPTYPE::VECSXP),
1104 other => SexpError::InvalidValue(other.to_string()),
1105 })
1106 }
1107}
1108
1109/// `NULL` → `None`; any other value converts as a [`DataFrame`] (same view,
1110/// attributes and `NA` cells, same error) and becomes `Some`.
1111///
1112/// Concrete rather than left to the newtype blanket `Option<T>` impl
1113/// (`newtype.rs`), which `DataFrame` does not match: without this impl,
1114/// `Option<DataFrame>: TryFromSexp` fails with E0275 (overflow) instead of
1115/// converting.
1116impl TryFromSexp for Option<DataFrame> {
1117 type Error = <DataFrame as TryFromSexp>::Error;
1118 const NATIVE_BORROW: Option<crate::from_r::NativeBorrow> =
1119 <DataFrame as TryFromSexp>::NATIVE_BORROW;
1120 const CHARACTER_ONLY: bool = <DataFrame as TryFromSexp>::CHARACTER_ONLY;
1121 // `__MX_EXPECTATION` keeps its `None`: the inner text without `NULL or`
1122 // would be wrong, and `#[miniextendr]` words an `Option<_>` parameter or
1123 // arm itself (`NULL or <T>`).
1124
1125 // `NULL` (`None`) is "not given" and passes `no_na`.
1126 #[inline]
1127 fn __mx_has_na(&self) -> bool {
1128 self.as_ref()
1129 .is_some_and(<DataFrame as TryFromSexp>::__mx_has_na)
1130 }
1131
1132 // As an `Either` arm, a given frame is read by `DataFrame` (it keeps its
1133 // `NA` cells); `NULL` holds no `NA`.
1134 #[inline]
1135 fn __mx_input_has_na(&self, input: SEXP) -> bool {
1136 match self {
1137 Some(df) => <DataFrame as TryFromSexp>::__mx_input_has_na(df, input),
1138 None => false,
1139 }
1140 }
1141
1142 #[inline]
1143 fn try_from_sexp(sexp: SEXP) -> Result<Self, Self::Error> {
1144 if sexp.type_of() == SEXPTYPE::NILSXP {
1145 Ok(None)
1146 } else {
1147 <DataFrame as TryFromSexp>::try_from_sexp(sexp).map(Some)
1148 }
1149 }
1150
1151 #[inline]
1152 unsafe fn try_from_sexp_unchecked(sexp: SEXP) -> Result<Self, Self::Error> {
1153 if sexp.type_of() == SEXPTYPE::NILSXP {
1154 Ok(None)
1155 } else {
1156 unsafe { <DataFrame as TryFromSexp>::try_from_sexp_unchecked(sexp) }.map(Some)
1157 }
1158 }
1159}
1160
1161impl IntoR for DataFrame {
1162 type Error = std::convert::Infallible;
1163 fn try_into_sexp(self) -> Result<SEXP, Self::Error> {
1164 Ok(self.sexp)
1165 }
1166 unsafe fn try_into_sexp_unchecked(self) -> Result<SEXP, Self::Error> {
1167 Ok(self.sexp)
1168 }
1169 #[inline]
1170 fn into_sexp(self) -> SEXP {
1171 self.sexp
1172 }
1173}
1174// endregion
1175
1176// region: BuiltDataFrame — owned, GC-rooted, Rust-constructed data.frame
1177
1178/// An owned, GC-rooted `data.frame` **built on the Rust side**.
1179///
1180/// [`DataFrame`] is a cheap `Copy` *view* over a bare SEXP with no root of its
1181/// own — sound only while something else keeps the SEXP reachable (an R `.Call`
1182/// argument frame roots R-supplied frames; a surrounding
1183/// [`ProtectScope`](crate::ProtectScope) roots transients). A frame that Rust
1184/// *constructs* has no such external root, so holding the bare view across any R
1185/// allocation is a latent use-after-free (issue #1128): the GC reclaims the
1186/// frame, and a freed-but-intact read passes tests silently until the slot is
1187/// reused.
1188///
1189/// `BuiltDataFrame` is the return type of every Rust-side constructor
1190/// ([`IntoDataFrame::into_dataframe`], `SerdeRowBuilder::finish`,
1191/// [`DataFrame::builder`]`().build()`, the serde `*_to_dataframe` verbs,
1192/// [`NamedList::as_data_frame`]), and the partition type inside the split verbs'
1193/// [`DataFrameShape`] ([`IntoDataFrameSplit::into_dataframe_split`], #1750; the
1194/// serde `vec_to_dataframe_split` / `result_to_dataframe`, #1265). It roots the
1195/// frame with a [`Preserved`](crate::Preserved) handle (`R_PreserveObject` on
1196/// construction, `R_ReleaseObject` on drop), so holding it across allocations
1197/// is safe by construction. It [`Deref`](std::ops::Deref)s to
1198/// [`DataFrame`], so every read/edit method keeps working unchanged; hand the
1199/// frame back to R with [`IntoR::into_sexp`] — or just return it from a
1200/// `#[miniextendr]` fn, which converts it through [`IntoR`] transparently.
1201///
1202/// Not `Copy`/`Clone` (each root is released exactly once) and not `Send`
1203/// (R's precious list is R-main-thread state).
1204///
1205/// # Editing (chains stay rooted, #1247)
1206///
1207/// The new-frame editing methods ([`DataFrame::drop`], [`DataFrame::select`],
1208/// [`DataFrame::select_rows`], [`DataFrame::prepend_column`],
1209/// [`DataFrame::with_column`]) also return `BuiltDataFrame`, and this type
1210/// carries inherent forwards for the whole editing set, so
1211/// `rows.into_dataframe()?.drop("x").select(&["y"])` is rooted at every link.
1212///
1213/// # Residual
1214///
1215/// The cheap view can still be *smuggled* across allocations by hand:
1216/// dereferencing (`*built`) copies out an unrooted `DataFrame` view that
1217/// dangles once the handle drops. That is an opt-in footgun, not the
1218/// constructor/editing path this type makes safe.
1219pub struct BuiltDataFrame {
1220 view: DataFrame,
1221 /// Roots `view`'s frame for as long as the handle lives.
1222 root: crate::Preserved,
1223}
1224
1225impl BuiltDataFrame {
1226 /// Root an already-built `data.frame` SEXP and take ownership of the root.
1227 ///
1228 /// # Safety
1229 ///
1230 /// Must run on the R main thread. `sexp` must be a well-formed `data.frame`
1231 /// VECSXP. Rooting is immediate (see [`Preserved::new`](crate::Preserved::new)),
1232 /// so a freshly-assembled unprotected SEXP is safe to pass directly.
1233 #[inline]
1234 pub unsafe fn adopt_sexp(sexp: SEXP) -> Self {
1235 // SAFETY: as documented on this fn.
1236 unsafe { Self::from_root(crate::Preserved::new(sexp)) }
1237 }
1238
1239 /// Root a [`DataFrame`] view, taking ownership of a fresh root.
1240 ///
1241 /// Rooting an already-rooted frame is sound: `R_PreserveObject` entries
1242 /// stack (the precious list is a cons list, duplicates allowed), and each
1243 /// `BuiltDataFrame` releases exactly the one entry it added.
1244 ///
1245 /// # Safety
1246 ///
1247 /// Must run on the R main thread. `df` must wrap a well-formed, still-live
1248 /// `data.frame` VECSXP (reachable at the moment of the call).
1249 #[inline]
1250 pub unsafe fn adopt(df: DataFrame) -> Self {
1251 // SAFETY: as documented on this fn.
1252 unsafe { Self::adopt_sexp(df.as_sexp()) }
1253 }
1254
1255 /// Take over an existing root of a frame.
1256 ///
1257 /// # Safety
1258 ///
1259 /// `root` holds a well-formed `data.frame` VECSXP.
1260 #[inline]
1261 pub(crate) unsafe fn from_root(root: crate::Preserved) -> Self {
1262 Self {
1263 // SAFETY: caller guarantees `root` holds a well-formed data.frame VECSXP.
1264 view: unsafe { DataFrame::from_built_sexp(root.as_sexp()) },
1265 root,
1266 }
1267 }
1268
1269 // region: editing forwards (#1247) — keep chains rooted at every link
1270 //
1271 // These consume the handle (`self` stays alive until the tail expression
1272 // has produced — and rooted — the result, so the input frame is reachable
1273 // throughout the edit; its root is then released with no allocation in
1274 // between). Plain `Deref` would instead copy out the view and return the
1275 // view method's result, rooted or not.
1276
1277 /// Remove a column by name — see [`DataFrame::drop`].
1278 ///
1279 /// Inherent forward: consumes this handle and returns the rooted result.
1280 #[must_use]
1281 pub fn drop(self, col: &str) -> BuiltDataFrame {
1282 (*self).drop(col)
1283 }
1284
1285 /// Keep only the named columns — see [`DataFrame::select`].
1286 #[must_use]
1287 pub fn select(self, cols: &[&str]) -> BuiltDataFrame {
1288 (*self).select(cols)
1289 }
1290
1291 /// Keep only the given rows — see [`DataFrame::select_rows`].
1292 #[must_use]
1293 pub fn select_rows(&self, idx: &[usize]) -> BuiltDataFrame {
1294 (**self).select_rows(idx)
1295 }
1296
1297 /// Insert a column at index 0 — see [`DataFrame::prepend_column`].
1298 ///
1299 /// `column` must be kept reachable by the caller across this call.
1300 #[must_use]
1301 pub fn prepend_column(self, name: &str, column: SEXP) -> BuiltDataFrame {
1302 (*self).prepend_column(name, column)
1303 }
1304
1305 /// Upsert a column — see [`DataFrame::with_column`].
1306 ///
1307 /// `column` must be kept reachable by the caller across this call.
1308 #[must_use]
1309 pub fn with_column(self, name: &str, column: SEXP) -> BuiltDataFrame {
1310 (*self).with_column(name, column)
1311 }
1312
1313 /// Rename a column — see [`DataFrame::rename`].
1314 ///
1315 /// In-place edit of the same frame: this handle (and its root) carries
1316 /// straight through.
1317 #[must_use]
1318 pub fn rename(self, from: &str, to: &str) -> BuiltDataFrame {
1319 let _ = (*self).rename(from, to);
1320 self
1321 }
1322
1323 /// Strip a prefix from column names — see [`DataFrame::strip_prefix`].
1324 ///
1325 /// In-place edit of the same frame: this handle (and its root) carries
1326 /// straight through.
1327 #[must_use]
1328 pub fn strip_prefix(self, prefix: &str) -> BuiltDataFrame {
1329 let _ = (*self).strip_prefix(prefix);
1330 self
1331 }
1332 // endregion
1333}
1334
1335impl std::ops::Deref for BuiltDataFrame {
1336 type Target = DataFrame;
1337 #[inline]
1338 fn deref(&self) -> &DataFrame {
1339 &self.view
1340 }
1341}
1342
1343/// Releases the root and hands the frame over, unprotected, as
1344/// [`DataFrame::as_sexp`] / [`IntoR::into_sexp`] do: the caller (typically the
1345/// `.Call` return path) takes over protection. `R_ReleaseObject` does not
1346/// allocate, so no GC can run between the release and the return.
1347impl IntoR for BuiltDataFrame {
1348 type Error = std::convert::Infallible;
1349 fn try_into_sexp(self) -> Result<SEXP, Self::Error> {
1350 Ok(self.root.into_sexp())
1351 }
1352 unsafe fn try_into_sexp_unchecked(self) -> Result<SEXP, Self::Error> {
1353 Ok(self.root.into_sexp())
1354 }
1355 #[inline]
1356 fn into_sexp(self) -> SEXP {
1357 self.root.into_sexp()
1358 }
1359}
1360
1361impl std::fmt::Debug for BuiltDataFrame {
1362 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1363 f.debug_struct("BuiltDataFrame")
1364 .field("nrow", &self.nrow())
1365 .field("ncol", &self.ncol())
1366 .finish()
1367 }
1368}
1369// endregion
1370
1371// region: The conversion trait family (mirrors IntoR / TryFromSexp)
1372
1373/// Rust data → R `data.frame`. The data-frame analogue of [`IntoR`].
1374///
1375/// Implemented for `Vec<Row>` when `Row` derives `DataFrameRow` (struct or enum), and by the
1376/// serde column path. Call it on your data: `rows.into_dataframe()?`.
1377///
1378/// # Parallel fast path
1379///
1380/// `into_dataframe_par` (present only with
1381/// `feature = "rayon"`) produces the **same** [`BuiltDataFrame`] as
1382/// [`into_dataframe`](IntoDataFrame::into_dataframe). It defaults to the sequential path, so
1383/// every implementor gets a correct `_par` for free; `#[derive(DataFrameRow)]` row types
1384/// override it with a genuinely parallel column fill (the #777 flattened `(column,row-range)`
1385/// work-list). The verb is stable across feature sets — dropping `_par` degrades cleanly to
1386/// the sequential call.
1387pub trait IntoDataFrame: Sized {
1388 /// Convert this value into an owned, GC-rooted [`BuiltDataFrame`].
1389 fn into_dataframe(self) -> Result<BuiltDataFrame, DataFrameError>;
1390
1391 /// Parallel column fill (`feature = "rayon"`). Same result as `into_dataframe()`.
1392 ///
1393 /// Defaults to the sequential path; overridden by the derive for a real parallel fill.
1394 #[cfg(feature = "rayon")]
1395 fn into_dataframe_par(self) -> Result<BuiltDataFrame, DataFrameError> {
1396 self.into_dataframe()
1397 }
1398}
1399
1400/// R `data.frame` → Rust data. The data-frame analogue of
1401/// [`TryFromSexp`].
1402///
1403/// Implemented by `#[derive(DataFrameRow)]` for `Vec<Row>` and by the serde row path.
1404///
1405/// # Parallel fast path
1406///
1407/// `from_dataframe_par` (`feature = "rayon"`) reads the
1408/// same rows as [`from_dataframe`](FromDataFrame::from_dataframe), defaulting to the
1409/// sequential reader; the derive overrides it with the #765 off-main-thread row assembly.
1410pub trait FromDataFrame: Sized {
1411 /// Read rows back out of a [`DataFrame`].
1412 fn from_dataframe(df: &DataFrame) -> Result<Self, DataFrameError>;
1413
1414 /// Parallel row read (`feature = "rayon"`). Same result as `from_dataframe()`.
1415 #[cfg(feature = "rayon")]
1416 fn from_dataframe_par(df: &DataFrame) -> Result<Self, DataFrameError> {
1417 Self::from_dataframe(df)
1418 }
1419}
1420
1421/// Enum rows → variant-partitioned R `data.frame`s. The split-representation
1422/// sibling of [`IntoDataFrame`].
1423///
1424/// Where [`IntoDataFrame`] produces one **aligned** `data.frame` (field-name
1425/// union, `NA` fill for columns a variant doesn't carry), this verb partitions
1426/// the rows by variant: each partition is a `data.frame` with only that
1427/// variant's own columns (non-optional types — no `NA` fill from sibling
1428/// variants), keyed by the snake_case variant name.
1429///
1430/// Implemented by `#[derive(DataFrameRow)]` on an **enum** row type (for
1431/// `Vec<Row>`, via the hidden [`DataFrameRowSplit`] bridge below). Struct rows
1432/// have no variants to partition, so the struct derive does not provide it.
1433/// Call it on your data: `rows.into_dataframe_split()`.
1434///
1435/// # Return shape
1436///
1437/// The returned [`DataFrameShape`] says which R value the split produced:
1438///
1439/// - [`DataFrameShape::Bare`] — a single-variant enum: one `data.frame`.
1440/// - [`DataFrameShape::PerVariantList`] — a multi-variant enum: one
1441/// `(snake_case_name, data.frame)` entry per declared variant, in
1442/// declaration order, which converts to a named R list of `data.frame`s.
1443///
1444/// The derive never produces [`DataFrameShape::Split`] (that variant belongs to
1445/// the serde `result_to_dataframe` helper). Variants absent from the input
1446/// still appear as 0-row `data.frame`s with that variant's column shape; unit
1447/// variants produce a 0-column `data.frame` with the correct row count.
1448/// `DataFrameShape` is [`IntoR`], so a `#[miniextendr]` function returns it
1449/// directly.
1450///
1451/// # GC discipline (rooted by construction, #1750)
1452///
1453/// Every partition is an owned, GC-rooted [`BuiltDataFrame`], so the result can
1454/// be held across R allocations (building another frame, calling back into R)
1455/// before it is returned or converted; there is no convert-immediately
1456/// contract. The [`IntoR`] conversion assembles the named list and releases
1457/// the per-partition roots.
1458pub trait IntoDataFrameSplit: Sized {
1459 /// Partition the rows by variant into one `data.frame` per variant.
1460 fn into_dataframe_split(self) -> DataFrameShape;
1461}
1462
1463/// Rust rows ↔ the pure-Rust columnar companion generated by `#[derive(DataFrameRow)]`.
1464///
1465/// `#[derive(DataFrameRow)]` builds a companion struct (`<Row>DataFrame`) whose fields are
1466/// `Vec`-columns — the in-memory, column-oriented form of `Vec<Row>`. This trait is the
1467/// documented verb surface for that companion: build it from rows (sequentially, or in parallel
1468/// with `feature = "rayon"`) and, for row-iterable companions, read the rows back.
1469///
1470/// It is the pure-Rust complement to [`IntoDataFrame`] / [`FromDataFrame`], which cross the R
1471/// boundary (`Vec<Row>` ↔ R `data.frame`). In particular `from_rows_par`
1472/// (`feature = "rayon"`) builds the **pure-Rust companion** in parallel —
1473/// `IntoDataFrame::into_dataframe_par` cannot, since it yields an R-backed
1474/// [`BuiltDataFrame`] rather than the companion.
1475///
1476/// The derive implements this on the companion type; call `CompanionType::from_rows(rows)`.
1477/// `Row` is a type parameter (not an associated type) so the derive can implement it for a
1478/// `pub` companion even when the row type is private — mirroring `From<Vec<Row>>`, and
1479/// avoiding the private-type-in-public-interface error an associated type would raise.
1480///
1481/// # Reading rows back
1482///
1483/// [`into_rows`](ColumnarFrame::into_rows) is available for **row-iterable** companions
1484/// (named-field structs without column expansion), where it defers to the companion's
1485/// [`IntoIterator`]. Enum and column-expansion companions are write-only in pure Rust; read
1486/// those back across the R boundary via [`FromDataFrame`] or the derive's one-call
1487/// `try_from_dataframe`.
1488pub trait ColumnarFrame<Row>: Sized {
1489 /// Transpose a row vector into the columnar companion (sequential).
1490 fn from_rows(rows: Vec<Row>) -> Self;
1491
1492 /// Parallel transposition (`feature = "rayon"`). Same result as
1493 /// [`from_rows`](ColumnarFrame::from_rows).
1494 ///
1495 /// Defaults to the sequential path; `#[derive(DataFrameRow)]` overrides it with a genuine
1496 /// parallel column fill wherever the row shape allows one: the rows and column values are
1497 /// `Send`, placeholder-filled columns are `Default`, and struct-flattened / `as_list`
1498 /// values are `Clone`. A row shape without these still compiles and gets the sequential
1499 /// path.
1500 #[cfg(feature = "rayon")]
1501 fn from_rows_par(rows: Vec<Row>) -> Self {
1502 Self::from_rows(rows)
1503 }
1504
1505 /// Read the rows back out of the companion.
1506 ///
1507 /// Available for row-iterable companions (named-field structs without column expansion);
1508 /// defers to the companion's [`IntoIterator`]. Enum and column-expansion companions are
1509 /// write-only in pure Rust — read those back via [`FromDataFrame`] / `try_from_dataframe`.
1510 fn into_rows(self) -> Vec<Row>
1511 where
1512 Self: IntoIterator<Item = Row>,
1513 {
1514 self.into_iter().collect()
1515 }
1516}
1517
1518/// The parallel column fill `#[derive(DataFrameRow)]` writes for a companion.
1519///
1520/// The derive implements it only where the row shape allows a parallel fill:
1521/// the rows and every column value can move to another thread (`Send`), and
1522/// what the fill needs of its columns holds (`Default` for a placeholder,
1523/// `Clone` for a value copied out before the rows move). Each of those bounds
1524/// sits behind a higher-ranked binder, so the impl simply does not apply when
1525/// one fails. [`ColumnarFrame::from_rows_par`] then falls back to `from_rows`
1526/// through [`FromRowsParProbe`] instead of failing to compile, so turning on
1527/// `rayon` anywhere in a crate graph never breaks a row type.
1528#[cfg(feature = "rayon")]
1529#[doc(hidden)]
1530pub trait ParallelColumns<Row>: Sized {
1531 /// Fill the companion's columns from the rows in parallel.
1532 fn from_rows_parallel(rows: Vec<Row>) -> Self;
1533}
1534
1535/// Selects the derive's `from_rows_par`: [`ParallelColumns`] when the
1536/// companion implements it, `from_rows` otherwise.
1537///
1538/// The derived override calls
1539/// `(&FromRowsParProbe::<Self, Row>::new()).fill_columns(rows)` with
1540/// [`FromRowsParParallel`] and [`FromRowsParSequential`] in scope. Method
1541/// lookup tries the receiver type `&FromRowsParProbe` first, where only the
1542/// parallel trait's method fits (and only if its impl applies), then
1543/// `&&FromRowsParProbe`, where the sequential one does.
1544#[cfg(feature = "rayon")]
1545#[doc(hidden)]
1546pub struct FromRowsParProbe<Df, Row>(std::marker::PhantomData<fn() -> (Df, Row)>);
1547
1548#[cfg(feature = "rayon")]
1549impl<Df, Row> FromRowsParProbe<Df, Row> {
1550 /// The probe for companion `Df` over `Row`.
1551 #[allow(clippy::new_without_default)]
1552 pub fn new() -> Self {
1553 FromRowsParProbe(std::marker::PhantomData)
1554 }
1555}
1556
1557/// The parallel arm of [`FromRowsParProbe`].
1558#[cfg(feature = "rayon")]
1559#[doc(hidden)]
1560pub trait FromRowsParParallel<Df, Row> {
1561 /// Fill `Df` with [`ParallelColumns::from_rows_parallel`].
1562 fn fill_columns(&self, rows: Vec<Row>) -> Df;
1563}
1564
1565#[cfg(feature = "rayon")]
1566impl<Df: ParallelColumns<Row>, Row> FromRowsParParallel<Df, Row> for FromRowsParProbe<Df, Row> {
1567 fn fill_columns(&self, rows: Vec<Row>) -> Df {
1568 Df::from_rows_parallel(rows)
1569 }
1570}
1571
1572/// The sequential arm of [`FromRowsParProbe`].
1573#[cfg(feature = "rayon")]
1574#[doc(hidden)]
1575pub trait FromRowsParSequential<Df, Row> {
1576 /// Fill `Df` with [`ColumnarFrame::from_rows`].
1577 fn fill_columns(&self, rows: Vec<Row>) -> Df;
1578}
1579
1580#[cfg(feature = "rayon")]
1581impl<Df: ColumnarFrame<Row>, Row> FromRowsParSequential<Df, Row> for &FromRowsParProbe<Df, Row> {
1582 fn fill_columns(&self, rows: Vec<Row>) -> Df {
1583 Df::from_rows(rows)
1584 }
1585}
1586// endregion
1587
1588// region: ColumnSource — internal column-assembly engine (ex-public convert::IntoDataFrame)
1589
1590/// Internal engine that turns a value into a `data.frame`-shaped [`List`].
1591///
1592/// This was the historical public `convert::IntoDataFrame` (`-> List`). It is now an internal
1593/// engine: the public [`IntoDataFrame`] (`-> Result<BuiltDataFrame, _>`) and the enum-flatten
1594/// codegen both delegate to it. Not part of the public verb surface.
1595#[doc(hidden)]
1596pub trait ColumnSource {
1597 /// Convert into a `data.frame`-shaped [`List`] (named columns, `data.frame` class,
1598 /// `row.names`).
1599 fn into_column_list(self) -> List;
1600
1601 /// Extract named column SEXPs from this value.
1602 ///
1603 /// Returns `(name, raw SEXP)` per column. The assembled column list is
1604 /// protected in `scope` before anything is read from it, so every returned
1605 /// SEXP stays reachable (as an element of that list) for as long as `scope`
1606 /// lives, across any number of later allocations (#1748).
1607 ///
1608 /// # Safety
1609 ///
1610 /// Must run on the R main thread, and `scope` must be the innermost live
1611 /// [`ProtectScope`](crate::gc_protect::ProtectScope): its `Drop` unprotects
1612 /// from the top of R's protect stack.
1613 unsafe fn into_named_columns(
1614 self,
1615 scope: &crate::gc_protect::ProtectScope,
1616 ) -> Vec<(String, crate::SEXP)>
1617 where
1618 Self: Sized,
1619 {
1620 use crate::SexpExt as _;
1621 // SAFETY: R main thread and innermost scope (caller contract). Nothing
1622 // allocates between building the list and protecting it.
1623 let sexp = unsafe { scope.protect_raw(self.into_column_list().as_sexp()) };
1624 let n = sexp.len();
1625 let mut out = Vec::with_capacity(n);
1626 let names_sexp = sexp.get_names();
1627 let has_names = !names_sexp.is_nil();
1628 for i in 0..(n as isize) {
1629 let col_sexp = sexp.vector_elt(i);
1630 let col_name = if has_names {
1631 names_sexp.string_elt_str(i).unwrap_or("").to_string()
1632 } else {
1633 i.to_string()
1634 };
1635 out.push((col_name, col_sexp));
1636 }
1637 out
1638 }
1639
1640 /// Assemble this source into a validated [`DataFrame`].
1641 ///
1642 /// The column engine always sets the `data.frame` class (even for an empty frame); the one
1643 /// exception is the unnamed-row degradation, which returns a bare unclassed empty list — the
1644 /// old `panic!("unnamed list elements")` case, now a clean `Err(UnnamedColumns)`.
1645 ///
1646 /// This is the bridge from the internal column-assembly engine to the public [`DataFrame`].
1647 /// We deliberately do **not** offer a blanket `impl<T: ColumnSource> IntoDataFrame for T`:
1648 /// `#[derive(DataFrameRow)]` emits a *concrete* `impl IntoDataFrame for Vec<Row>` per row
1649 /// type (and serde uses the `SerdeRows<T>` newtype), so a generic `for T` blanket would
1650 /// coherence-conflict with every one of those (the compiler treats `Vec<Row>: ColumnSource`
1651 /// as possibly-true). The derive's `into_dataframe` glue calls this method instead.
1652 fn into_dataframe(self) -> Result<DataFrame, DataFrameError>
1653 where
1654 Self: Sized,
1655 {
1656 use crate::SexpExt as _;
1657 let sexp = self.into_column_list().as_sexp();
1658 if !sexp.is_data_frame() {
1659 return Err(DataFrameError::UnnamedColumns);
1660 }
1661 Ok(unsafe { DataFrame::from_built_sexp(sexp) })
1662 }
1663}
1664// endregion
1665
1666// region: rayon-gating shim for #[derive(DataFrameRow)] parallel methods
1667
1668/// Emit the wrapped items only when `miniextendr-api` itself was built with the
1669/// `rayon` feature.
1670///
1671/// `#[derive(DataFrameRow)]` uses this to gate its parallel `*_par` methods on
1672/// **this** crate's `rayon` feature instead of stamping a raw
1673/// `#[cfg(feature = "rayon")]` into the *consumer* crate. A `cfg` inside a
1674/// derive is evaluated against the destination crate, where `rayon` is usually
1675/// not a declared feature — so every downstream package that derives
1676/// `DataFrameRow` without a `rayon` feature of its own trips the
1677/// `unexpected_cfgs` lint (#1117). Routing through this macro moves the `#[cfg]`
1678/// decision into `miniextendr-api`, whose feature set always declares `rayon`,
1679/// so the consumer crate never sees the attribute. The parallel path now
1680/// activates purely on the API crate's `rayon` feature, independent of what the
1681/// consumer names its own.
1682#[doc(hidden)]
1683#[macro_export]
1684#[cfg(feature = "rayon")]
1685macro_rules! __dataframe_row_when_rayon {
1686 ($($item:tt)*) => { $($item)* };
1687}
1688
1689/// No-rayon build: the wrapped parallel methods vanish entirely. The trait's
1690/// `*_par` methods are themselves `#[cfg(feature = "rayon")]`, so there is
1691/// nothing to override.
1692#[doc(hidden)]
1693#[macro_export]
1694#[cfg(not(feature = "rayon"))]
1695macro_rules! __dataframe_row_when_rayon {
1696 ($($item:tt)*) => {};
1697}
1698
1699// endregion
1700
1701// region: DataFrameRowConvert — orphan-rule bridge for `Vec<Row>` conversions
1702
1703/// Row → DataFrame conversion glue emitted by `#[derive(DataFrameRow)]` on the **row type**.
1704///
1705/// The orphan rule forbids the derive from writing `impl IntoDataFrame for Vec<Row>` in the user
1706/// crate: both `IntoDataFrame` and `Vec` are foreign there, and `Row` only appears *covered*
1707/// inside `Vec<_>`, so there is no uncovered local type. Instead the derive implements this
1708/// `#[doc(hidden)]` trait on the local `Row` type (legal — `Row` is local), and `miniextendr_api`
1709/// carries the blanket [`IntoDataFrame`] / [`FromDataFrame`] impls for `Vec<T: DataFrameRowConvert>`
1710/// below (legal — `IntoDataFrame` is local *here*). Users still call the public
1711/// `rows.into_dataframe()?` / `Vec::<Row>::from_dataframe(&df)?` verbs.
1712#[doc(hidden)]
1713pub trait DataFrameRowConvert: Sized {
1714 /// Build a [`DataFrame`] from a row vector (sequential).
1715 fn rows_into_dataframe(rows: Vec<Self>) -> Result<DataFrame, DataFrameError>;
1716
1717 /// Build a [`DataFrame`] from a row vector (parallel; defaults to sequential).
1718 #[cfg(feature = "rayon")]
1719 fn rows_into_dataframe_par(rows: Vec<Self>) -> Result<DataFrame, DataFrameError> {
1720 Self::rows_into_dataframe(rows)
1721 }
1722
1723 /// Read a row vector out of a [`DataFrame`]. `None` means this row shape has no reader
1724 /// (scalar, column-expansion, and struct-flatten struct shapes do; tagged enum shapes with
1725 /// reader-capable fields do too; tagless/map-column/coerced/skip/`as_list` enum shapes and
1726 /// opaque-map shapes do not); the blanket surfaces that as a clear error.
1727 fn rows_from_dataframe(_df: &DataFrame) -> Option<Result<Vec<Self>, DataFrameError>> {
1728 None
1729 }
1730
1731 /// Parallel reader (defaults to the sequential reader).
1732 #[cfg(feature = "rayon")]
1733 fn rows_from_dataframe_par(df: &DataFrame) -> Option<Result<Vec<Self>, DataFrameError>> {
1734 Self::rows_from_dataframe(df)
1735 }
1736}
1737
1738/// Error returned by `Vec::<Row>::from_dataframe` when the row shape has no R→Rust reader.
1739fn no_reader_error() -> DataFrameError {
1740 DataFrameError::Conversion(
1741 "this DataFrameRow shape has no R→Rust reader (struct shapes with scalar/expansion/\
1742 struct-flatten fields do; tagged enum shapes with reader-capable fields do; \
1743 tagless/map-column/coerced/skip/as_list enum shapes and opaque-map shapes do not)"
1744 .to_string(),
1745 )
1746}
1747
1748impl<T: DataFrameRowConvert> IntoDataFrame for Vec<T> {
1749 fn into_dataframe(self) -> Result<BuiltDataFrame, DataFrameError> {
1750 // `rows_into_dataframe` returns the freshly-built frame as a bare view;
1751 // root it immediately (no allocation between the `?` and `adopt`) so the
1752 // returned handle owns its GC root.
1753 // SAFETY: builds SEXPs → runs on the R main thread.
1754 Ok(unsafe { BuiltDataFrame::adopt(T::rows_into_dataframe(self)?) })
1755 }
1756
1757 #[cfg(feature = "rayon")]
1758 fn into_dataframe_par(self) -> Result<BuiltDataFrame, DataFrameError> {
1759 // SAFETY: as in `into_dataframe` above.
1760 Ok(unsafe { BuiltDataFrame::adopt(T::rows_into_dataframe_par(self)?) })
1761 }
1762}
1763
1764impl<T: DataFrameRowConvert> FromDataFrame for Vec<T> {
1765 fn from_dataframe(df: &DataFrame) -> Result<Self, DataFrameError> {
1766 // No input root here: an R-supplied frame is rooted by R's `.Call`
1767 // argument frame, and a Rust-built frame now reaches the reader as a
1768 // borrowed `BuiltDataFrame` (rooted for the borrow) — the handle
1769 // supersedes the old `OwnedProtect` guard (#1128). The reader's own
1770 // sub-frame allocations stay protected by their internal guards.
1771 T::rows_from_dataframe(df).unwrap_or_else(|| Err(no_reader_error()))
1772 }
1773
1774 #[cfg(feature = "rayon")]
1775 fn from_dataframe_par(df: &DataFrame) -> Result<Self, DataFrameError> {
1776 T::rows_from_dataframe_par(df).unwrap_or_else(|| Err(no_reader_error()))
1777 }
1778}
1779
1780/// Row → split-representation glue emitted by `#[derive(DataFrameRow)]` on **enum** row types.
1781///
1782/// Same orphan-rule bridge as [`DataFrameRowConvert`]: the derive cannot write
1783/// `impl IntoDataFrameSplit for Vec<Row>` in the user crate (`Row` is covered by `Vec<_>`), so
1784/// it implements this `#[doc(hidden)]` trait on the local enum type and `miniextendr_api`
1785/// carries the blanket [`IntoDataFrameSplit`] impl for `Vec<T: DataFrameRowSplit>` below.
1786///
1787/// Deliberately a **separate** bridge from [`DataFrameRowConvert`]: the split representation
1788/// only exists for enums, and a method here (even defaulted) would grow the verb onto every
1789/// struct row type. Users call the public `rows.into_dataframe_split()` verb.
1790#[doc(hidden)]
1791pub trait DataFrameRowSplit: Sized {
1792 /// Partition a row vector by variant into one `data.frame` per variant.
1793 fn rows_into_dataframe_split(rows: Vec<Self>) -> DataFrameShape;
1794}
1795
1796impl<T: DataFrameRowSplit> IntoDataFrameSplit for Vec<T> {
1797 fn into_dataframe_split(self) -> DataFrameShape {
1798 T::rows_into_dataframe_split(self)
1799 }
1800}
1801// endregion
1802
1803// region: nrow extraction from row.names
1804
1805/// Extract `nrow` from R's `row.names` attribute.
1806fn extract_nrow(sexp: SEXP) -> Result<usize, DataFrameError> {
1807 let row_names = sexp.get_row_names();
1808
1809 if row_names.is_nil() {
1810 return nrow_from_first_column(sexp);
1811 }
1812
1813 let rn_type = row_names.type_of();
1814 let rn_len = row_names.xlength();
1815
1816 if rn_type == SEXPTYPE::INTSXP && rn_len == 2 {
1817 let rn: &[i32] = unsafe { row_names.as_slice() };
1818 if rn[0].is_na() && rn[1] < 0 {
1819 return Ok((-rn[1]) as usize);
1820 }
1821 }
1822
1823 if let Ok(n) = usize::try_from(rn_len) {
1824 Ok(n)
1825 } else {
1826 Err(DataFrameError::BadRowNames(format!(
1827 "row.names has negative length: {}",
1828 rn_len
1829 )))
1830 }
1831}
1832
1833/// Fall back: extract nrow from the length of the first column.
1834fn nrow_from_first_column(sexp: SEXP) -> Result<usize, DataFrameError> {
1835 let ncol = sexp.xlength();
1836 if ncol == 0 {
1837 return Ok(0);
1838 }
1839 let first_col = sexp.vector_elt(0);
1840 if first_col == SEXP::nil() {
1841 return Ok(0);
1842 }
1843 let len = first_col.xlength();
1844 if let Ok(n) = usize::try_from(len) {
1845 Ok(n)
1846 } else {
1847 Err(DataFrameError::BadRowNames(
1848 "first column has negative length".to_string(),
1849 ))
1850 }
1851}
1852// endregion
1853
1854// region: NamedList / List → DataFrame promotion
1855
1856/// Validate that all columns in a NamedList have equal length, returning the common length.
1857pub(crate) fn validate_equal_lengths(named: &NamedList) -> Result<usize, DataFrameError> {
1858 let list = named.as_list();
1859 let n = list.len();
1860
1861 if n == 0 {
1862 return Ok(0);
1863 }
1864
1865 let first_col = list.as_sexp().vector_elt(0);
1866 let expected: usize = first_col.len();
1867
1868 let names_sexp = list.names();
1869 let mut columns = Vec::new();
1870 for i in 1..n {
1871 let col = list.as_sexp().vector_elt(i);
1872 let col_len: usize = col.len();
1873 if col_len != expected {
1874 let column = if let Some(names) = names_sexp {
1875 let name_sexp = names.string_elt(i);
1876 if name_sexp != SEXP::na_string() {
1877 let name_ptr = name_sexp.r_char();
1878 let name_cstr = unsafe { CStr::from_ptr(name_ptr) };
1879 name_cstr.to_str().unwrap_or("<invalid>").to_string()
1880 } else {
1881 format!("column {}", i)
1882 }
1883 } else {
1884 format!("column {}", i)
1885 };
1886
1887 columns.push((column, col_len));
1888 }
1889 }
1890
1891 if columns.is_empty() {
1892 Ok(expected)
1893 } else {
1894 Err(DataFrameError::UnequalLengths { expected, columns })
1895 }
1896}
1897
1898impl NamedList {
1899 /// Promote this named list to a [`DataFrame`].
1900 ///
1901 /// Validates equal column lengths, sets the `data.frame` class, and adds compact integer
1902 /// `row.names`.
1903 ///
1904 /// # Errors
1905 ///
1906 /// Returns [`DataFrameError::UnequalLengths`] if columns differ in length.
1907 pub fn as_data_frame(&self) -> Result<BuiltDataFrame, DataFrameError> {
1908 let nrow = validate_equal_lengths(self)?;
1909 self.as_list().set_data_frame_class();
1910 self.as_list().set_row_names_int(nrow);
1911 // SAFETY: R main thread; the list is now a well-formed data.frame VECSXP.
1912 Ok(unsafe { BuiltDataFrame::adopt_sexp(self.as_list().as_sexp()) })
1913 }
1914}
1915
1916impl List {
1917 /// Promote this named list to a [`DataFrame`].
1918 ///
1919 /// # Errors
1920 ///
1921 /// Returns [`DataFrameError`] if the list has no names or columns differ in length.
1922 pub fn as_data_frame(&self) -> Result<BuiltDataFrame, DataFrameError> {
1923 let named = NamedList::new(*self).ok_or(DataFrameError::NoNames)?;
1924 named.as_data_frame()
1925 }
1926}
1927// endregion
1928
1929// region: Debug impl
1930
1931impl std::fmt::Debug for DataFrame {
1932 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1933 f.debug_struct("DataFrame")
1934 .field("nrow", &self.nrow())
1935 .field("ncol", &self.ncol())
1936 .finish()
1937 }
1938}
1939
1940#[cfg(test)]
1941mod tests {
1942 use super::*;
1943
1944 #[test]
1945 fn test_data_frame_error_display() {
1946 let err = DataFrameError::NotDataFrame;
1947 assert_eq!(err.to_string(), "object does not inherit from data.frame");
1948
1949 let err = DataFrameError::NoNames;
1950 assert_eq!(err.to_string(), "data.frame has no column names");
1951
1952 let err = DataFrameError::UnequalLengths {
1953 expected: 3,
1954 columns: vec![("y".to_string(), 5)],
1955 };
1956 assert_eq!(err.to_string(), "column \"y\" has length 5 (expected 3)");
1957
1958 let err = DataFrameError::UnnamedColumns;
1959 assert_eq!(
1960 err.to_string(),
1961 "cannot create data frame from unnamed list elements"
1962 );
1963
1964 let err = DataFrameError::NoSuchColumn("g".to_string());
1965 assert_eq!(err.to_string(), "no such column: \"g\"");
1966 }
1967}
1968// endregion