Skip to main content

miniextendr_api/
dataframe.rs

1//! The unified owned R `data.frame` type and its conversion traits.
2//!
3//! [`DataFrame`] is **the** data-frame type: a single owned wrapper around a built
4//! `data.frame` SEXP that serves every direction —
5//!
6//! - **build** (Rust → R): [`IntoDataFrame::into_dataframe`] / `into_dataframe_par` (`feature = "rayon"`),
7//! - **read** (R → Rust): [`DataFrame::column`] / [`FromDataFrame::from_dataframe`],
8//! - **edit** (post-assembly): [`DataFrame::rename`] / [`DataFrame::drop`] / [`DataFrame::select`] / …
9//!
10//! The trait family mirrors the crate's existing [`IntoR`] /
11//! [`TryFromSexp`] pair, specialised to the data-frame SEXP:
12//!
13//! ```ignore
14//! use miniextendr_api::dataframe::{DataFrame, IntoDataFrame, FromDataFrame};
15//!
16//! // Rust → R
17//! let df: DataFrame = rows.into_dataframe()?;          // sequential
18//! let df: DataFrame = rows.into_dataframe_par()?;      // parallel (feature = "rayon")
19//!
20//! // R → Rust
21//! let rows: Vec<Row> = Vec::<Row>::from_dataframe(&df)?;
22//! ```
23//!
24//! `DataFrame` implements both `IntoR` and `TryFromSexp`, so it slots into
25//! `#[miniextendr]` function codegen with no special-casing — return it directly or accept
26//! it as an argument.
27//!
28//! # One error contract
29//!
30//! Every conversion failure surfaces as [`DataFrameError`]. The serde column assembler's
31//! internal `RSerdeError` is bridged via `From<RSerdeError>`; the parallel R→Rust reader
32//! reports through `DataFrameError` rather than a bare `String`.
33
34use crate::from_r::{SexpError, TryFromSexp};
35use crate::into_r::IntoR;
36use crate::list::{List, NamedList};
37use crate::typed_list::{TypedList, TypedListError, TypedListSpec, validate_list};
38use crate::{SEXP, SEXPTYPE, SexpExt};
39use std::ffi::CStr;
40
41pub mod group;
42pub use group::{GroupKey, GroupedDataFrame, group_rows};
43
44// region: Error type
45
46/// Error returned by any [`DataFrame`] construction, read, or conversion path.
47///
48/// This is the single data-frame error contract: the row-buffer build path, the serde
49/// columnar path, the parallel R→Rust reader, and validation all surface a `DataFrameError`.
50#[derive(Debug, Clone)]
51pub enum DataFrameError {
52    /// The SEXP is not a VECSXP.
53    NotList(String),
54    /// The object does not inherit from `data.frame`.
55    NotDataFrame,
56    /// The list has no `names` attribute (columns must be named).
57    NoNames,
58    /// Could not extract `nrow` from `row.names` attribute.
59    BadRowNames(String),
60    /// Columns have unequal lengths (when promoting from NamedList).
61    UnequalLengths {
62        /// First column length encountered.
63        expected: usize,
64        /// The column name that differs.
65        column: String,
66        /// The actual length of that column.
67        actual: usize,
68    },
69    /// A row could not be turned into named columns (e.g. unnamed list elements
70    /// in a `IntoList`-derived row). Replaces the old `panic!` on this path.
71    UnnamedColumns,
72    /// [`DataFrame::group_by`] referenced a column name that does not exist.
73    NoSuchColumn(String),
74    /// [`DataFrame::group_by`] on a column type with no sane grouping
75    /// semantics (doubles, list-columns, …).
76    UnsupportedGroupColumn {
77        /// The offending column name.
78        column: String,
79        /// Its SEXPTYPE, rendered for the message.
80        type_of: String,
81    },
82    /// A serde-driven schema/serialize/deserialize failure (the bridged
83    /// `RSerdeError` text) or another conversion failure carried as a message.
84    Conversion(String),
85}
86
87impl std::fmt::Display for DataFrameError {
88    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
89        match self {
90            DataFrameError::NotList(msg) => write!(f, "not a list: {}", msg),
91            DataFrameError::NotDataFrame => write!(f, "object does not inherit from data.frame"),
92            DataFrameError::NoNames => write!(f, "data.frame has no column names"),
93            DataFrameError::BadRowNames(msg) => {
94                write!(f, "could not extract nrow from row.names: {}", msg)
95            }
96            DataFrameError::UnequalLengths {
97                expected,
98                column,
99                actual,
100            } => write!(
101                f,
102                "column {:?} has length {} (expected {})",
103                column, actual, expected
104            ),
105            DataFrameError::UnnamedColumns => {
106                write!(f, "cannot create data frame from unnamed list elements")
107            }
108            DataFrameError::NoSuchColumn(name) => {
109                write!(f, "no such column: {:?}", name)
110            }
111            DataFrameError::UnsupportedGroupColumn { column, type_of } => write!(
112                f,
113                "cannot group by column {:?} ({}): supported key types are factor, \
114                 character, integer, and logical — cut() or factor() the column first",
115                column, type_of
116            ),
117            DataFrameError::Conversion(msg) => write!(f, "{}", msg),
118        }
119    }
120}
121
122impl std::error::Error for DataFrameError {}
123
124#[cfg(feature = "serde")]
125impl From<crate::serde::RSerdeError> for DataFrameError {
126    fn from(e: crate::serde::RSerdeError) -> Self {
127        DataFrameError::Conversion(e.to_string())
128    }
129}
130// endregion
131
132// region: DataFrame — the unified owned data.frame
133
134/// An owned, validated R `data.frame`. **The** data-frame type.
135///
136/// Wraps a built VECSXP carrying the `data.frame` class + `row.names`. A single coherent
137/// type for building (Rust → R), reading (R → Rust), and post-assembly editing — replacing
138/// the historical row-buffer / built-SEXP / read-wrapper trio with one coherent type.
139///
140/// # Building
141///
142/// Prefer the [`IntoDataFrame`] trait on your data:
143///
144/// ```ignore
145/// let df: DataFrame = rows.into_dataframe()?;
146/// ```
147///
148/// or the closure-fill `DataFrame::builder` for heterogeneous parallel column fill
149/// (`feature = "rayon"`).
150///
151/// # Reading
152///
153/// Wrap an incoming SEXP with [`DataFrame::from_sexp`] (or accept `DataFrame` directly as a
154/// `#[miniextendr]` argument), then pull typed columns with [`DataFrame::column`], or
155/// deserialize whole rows with [`FromDataFrame`].
156#[derive(Clone, Copy)]
157pub struct DataFrame {
158    sexp: SEXP,
159}
160
161impl DataFrame {
162    /// Wrap an already-built `data.frame` SEXP without re-validation.
163    ///
164    /// Used by the column assemblers, which produce a well-formed `data.frame` by
165    /// construction.
166    ///
167    /// # Safety
168    ///
169    /// `sexp` must be a VECSXP with the `data.frame` class and consistent `row.names`.
170    #[inline]
171    pub unsafe fn from_built_sexp(sexp: SEXP) -> Self {
172        Self { sexp }
173    }
174
175    /// Wrap an existing R `data.frame` SEXP, validating it.
176    ///
177    /// Validates that the object:
178    /// 1. Is a VECSXP (list)
179    /// 2. Inherits from `"data.frame"`
180    /// 3. Has a `names` attribute
181    /// 4. Has extractable `row.names` for nrow
182    ///
183    /// # Errors
184    ///
185    /// Returns [`DataFrameError`] if validation fails.
186    pub fn from_sexp(sexp: SEXP) -> Result<Self, DataFrameError> {
187        let stype = sexp.type_of();
188        if stype != SEXPTYPE::VECSXP {
189            return Err(DataFrameError::NotList(format!(
190                "expected VECSXP, got {:?}",
191                stype
192            )));
193        }
194        if !sexp.is_data_frame() {
195            return Err(DataFrameError::NotDataFrame);
196        }
197        // Require a names attribute (columns must be named).
198        let list = unsafe { List::from_raw(sexp) };
199        NamedList::new(list).ok_or(DataFrameError::NoNames)?;
200        // Confirm nrow is extractable.
201        extract_nrow(sexp)?;
202        Ok(Self { sexp })
203    }
204
205    // region: Read API (R → Rust column / row access)
206
207    /// Get a column by name, converting each element to type `T`.
208    ///
209    /// Returns `None` if the column name is not found or conversion fails.
210    #[inline]
211    pub fn column<T>(&self, name: &str) -> Option<T>
212    where
213        T: TryFromSexp<Error = SexpError>,
214    {
215        self.named_list().get(name)
216    }
217
218    /// Get a column by 0-based index, converting to type `T`.
219    #[inline]
220    pub fn column_index<T>(&self, idx: usize) -> Option<T>
221    where
222        T: TryFromSexp<Error = SexpError>,
223    {
224        let idx_isize: isize = idx.try_into().ok()?;
225        self.named_list().get_index(idx_isize)
226    }
227
228    /// Get the raw SEXP for a column by name.
229    #[inline]
230    pub fn column_raw(&self, name: &str) -> Option<SEXP> {
231        self.named_list().get_raw(name)
232    }
233
234    /// Number of rows.
235    #[inline]
236    pub fn nrow(&self) -> usize {
237        extract_nrow(self.sexp).unwrap_or(0)
238    }
239
240    /// Number of columns.
241    #[inline]
242    pub fn ncol(&self) -> usize {
243        self.sexp.len()
244    }
245
246    /// Collect column names in column order.
247    pub fn names(&self) -> Vec<String> {
248        let names_sexp = self.sexp.get_names();
249        if names_sexp.is_nil() {
250            return Vec::new();
251        }
252        let n = self.sexp.len() as isize;
253        (0..n)
254            .map(|i| names_sexp.string_elt_str(i).unwrap_or("").to_string())
255            .collect()
256    }
257
258    /// Check whether a column name exists.
259    #[inline]
260    pub fn contains_column(&self, name: &str) -> bool {
261        self.named_list().contains(name)
262    }
263
264    /// Validate the data frame's column types against a [`TypedListSpec`].
265    pub fn validate(&self, spec: &TypedListSpec) -> Result<TypedList, TypedListError> {
266        validate_list(unsafe { List::from_raw(self.sexp) }, spec)
267    }
268    // endregion
269
270    // region: Conversions
271
272    /// Get the underlying [`List`].
273    #[inline]
274    pub fn as_list(&self) -> List {
275        unsafe { List::from_raw(self.sexp) }
276    }
277
278    /// Get the underlying SEXP.
279    #[inline]
280    pub fn as_sexp(&self) -> SEXP {
281        self.sexp
282    }
283
284    /// Build the `NamedList` index for O(1) column-by-name access.
285    #[inline]
286    fn named_list(&self) -> NamedList {
287        NamedList::new(unsafe { List::from_raw(self.sexp) })
288            .expect("DataFrame always carries a names attribute")
289    }
290    // endregion
291
292    // region: Post-assembly editing (absorbed from the old serde columnar assembler)
293
294    /// Rename a column. No-op if `from` doesn't match any column name.
295    pub fn rename(self, from: &str, to: &str) -> Self {
296        unsafe {
297            // Root `self.sexp` so its `names` attribute survives the
298            // `SEXP::charsxp` (Rf_mkCharLenCE) allocation below, which can GC.
299            let _guard = crate::OwnedProtect::new(self.sexp);
300            let names_sexp = self.sexp.get_names();
301            if names_sexp == SEXP::nil() {
302                return self;
303            }
304            let ncol = names_sexp.xlength();
305            for i in 0..ncol {
306                if col_name(names_sexp, i) == from {
307                    names_sexp.set_string_elt(i, SEXP::charsxp(to));
308                    break;
309                }
310            }
311        }
312        self
313    }
314
315    /// Strip a prefix from all column names that start with it.
316    pub fn strip_prefix(self, prefix: &str) -> Self {
317        unsafe {
318            // Root `self.sexp` so its `names` attribute survives the
319            // `SEXP::charsxp` (Rf_mkCharLenCE) allocation below, which can GC.
320            let _guard = crate::OwnedProtect::new(self.sexp);
321            let names_sexp = self.sexp.get_names();
322            if names_sexp == SEXP::nil() {
323                return self;
324            }
325            let ncol = names_sexp.xlength();
326            for i in 0..ncol {
327                let name = col_name(names_sexp, i);
328                if let Some(stripped) = name.strip_prefix(prefix) {
329                    names_sexp.set_string_elt(i, SEXP::charsxp(stripped));
330                }
331            }
332        }
333        self
334    }
335
336    /// Remove a column by name. No-op if the column doesn't exist.
337    pub fn drop(self, col: &str) -> Self {
338        unsafe {
339            let names_sexp = self.sexp.get_names();
340            if names_sexp == SEXP::nil() {
341                return self;
342            }
343            let ncol = names_sexp.xlength();
344            let drop_idx = (0..ncol).find(|&i| col_name(names_sexp, i) == col);
345            let Some(drop_idx) = drop_idx else {
346                return self;
347            };
348
349            let new_ncol = ncol - 1;
350            let new_list = crate::OwnedProtect::new(SEXP::alloc_list(new_ncol));
351            let new_names = crate::OwnedProtect::new(SEXP::alloc_strsxp(new_ncol));
352
353            let mut j: isize = 0;
354            for i in 0..ncol {
355                if i == drop_idx {
356                    continue;
357                }
358                new_list.set_vector_elt(j, self.sexp.vector_elt(i));
359                new_names.set_string_elt(j, names_sexp.string_elt(i));
360                j += 1;
361            }
362
363            new_list.set_names(*new_names);
364            copy_df_attrs(self.sexp, *new_list);
365
366            DataFrame { sexp: *new_list }
367        }
368    }
369
370    /// Keep only the named columns, in the order given. Unknown names are skipped.
371    pub fn select(self, cols: &[&str]) -> Self {
372        unsafe {
373            let names_sexp = self.sexp.get_names();
374            if names_sexp == SEXP::nil() {
375                return self;
376            }
377            let ncol = names_sexp.xlength();
378
379            let indices: Vec<isize> = cols
380                .iter()
381                .filter_map(|&want| (0..ncol).find(|&i| col_name(names_sexp, i) == want))
382                .collect();
383
384            let new_ncol: isize = indices.len().try_into().expect("ncol overflow");
385            let new_list = crate::OwnedProtect::new(SEXP::alloc_list(new_ncol));
386            let new_names = crate::OwnedProtect::new(SEXP::alloc_strsxp(new_ncol));
387
388            for (j, &src_idx) in indices.iter().enumerate() {
389                let j_r: isize = j.try_into().expect("index overflow");
390                new_list.set_vector_elt(j_r, self.sexp.vector_elt(src_idx));
391                new_names.set_string_elt(j_r, names_sexp.string_elt(src_idx));
392            }
393
394            new_list.set_names(*new_names);
395            copy_df_attrs(self.sexp, *new_list);
396
397            DataFrame { sexp: *new_list }
398        }
399    }
400
401    /// Keep only the rows at the given 0-based indices, in order.
402    ///
403    /// Subsets every column (each a vector or list-column) to the specified rows
404    /// and rebuilds compact integer `row.names`. Used by the enum reader to
405    /// densify a flattened sub-frame before recursing into the inner type's reader.
406    ///
407    /// # PROTECT discipline
408    ///
409    /// Allocates one new column vector per column — `OwnedProtect`s the output list
410    /// across the loop so previously-built column SEXPs survive subsequent allocations.
411    pub fn select_rows(&self, idx: &[usize]) -> Self {
412        use crate::SexpExt as _;
413
414        unsafe {
415            let names_sexp = self.sexp.get_names();
416            let ncol = self.sexp.xlength();
417            let new_nrow = idx.len();
418
419            let new_list = crate::OwnedProtect::new(SEXP::alloc_list(ncol));
420            let new_names = crate::OwnedProtect::new(SEXP::alloc_strsxp(ncol));
421
422            for col_j in 0..ncol {
423                let src_col = self.sexp.vector_elt(col_j);
424
425                // Gather the requested rows into a new dense column via the shared
426                // conversion helper (the row-selecting inverse of `scatter_column`).
427                // It returns an unprotected SEXP; we root it into the protected
428                // `new_list` immediately below, before any further allocation.
429                let new_col: SEXP = crate::convert::gather_column(src_col, idx);
430
431                // Root new_col in the protected output list BEFORE touching its
432                // attributes. `gather_column` returns an unprotected SEXP, and
433                // `set_class`/`set_levels` (Rf_setAttrib) allocate and can trigger
434                // GC. set_vector_elt does not allocate, so this ordering keeps
435                // new_col reachable (via new_list) across every allocating call.
436                new_list.set_vector_elt(col_j, new_col);
437                if names_sexp != SEXP::nil() {
438                    new_names.set_string_elt(col_j, names_sexp.string_elt(col_j));
439                }
440
441                // Copy column attributes: class (for factor / Date / POSIXct) and
442                // levels (for factor columns). Safe now — new_col is rooted in the
443                // protected new_list, so GC during set_class/set_levels can't reap it.
444                let class_attr = src_col.get_class();
445                if class_attr != SEXP::nil() {
446                    new_col.set_class(class_attr);
447                }
448                let levels_attr = src_col.get_levels();
449                if levels_attr != SEXP::nil() {
450                    new_col.set_levels(levels_attr);
451                }
452            }
453
454            if names_sexp != SEXP::nil() {
455                new_list.set_names(*new_names);
456            }
457
458            // Set compact integer row.names (c(NA_integer_, -new_nrow)).
459            let (row_names, rn) = crate::into_r::alloc_r_vector::<i32>(2);
460            let _rn_guard = crate::OwnedProtect::new(row_names);
461            rn[0] = i32::MIN;
462            rn[1] = -(new_nrow as i32);
463            new_list.set_row_names(row_names);
464            // Copy the data.frame class attribute.
465            new_list.set_class(self.sexp.get_class());
466
467            DataFrame { sexp: *new_list }
468        }
469    }
470
471    /// Insert a column at index 0 (leftmost), removing any same-named column first.
472    pub fn prepend_column(self, name: &str, column: SEXP) -> Self {
473        let cleaned = self.drop(name);
474        unsafe {
475            let names_sexp = cleaned.sexp.get_names();
476            let ncol = if names_sexp == SEXP::nil() {
477                0
478            } else {
479                names_sexp.xlength()
480            };
481
482            let new_ncol = ncol + 1;
483            let new_list = crate::OwnedProtect::new(SEXP::alloc_list(new_ncol));
484            let new_names = crate::OwnedProtect::new(SEXP::alloc_strsxp(new_ncol));
485
486            new_list.set_vector_elt(0, column);
487            new_names.set_string_elt(0, SEXP::charsxp(name));
488
489            for i in 0..ncol {
490                new_list.set_vector_elt(i + 1, cleaned.sexp.vector_elt(i));
491                new_names.set_string_elt(i + 1, names_sexp.string_elt(i));
492            }
493
494            new_list.set_names(*new_names);
495            copy_df_attrs(cleaned.sexp, *new_list);
496
497            DataFrame { sexp: *new_list }
498        }
499    }
500
501    /// Upsert a column: replace the column named `name` if it exists, else append.
502    pub fn with_column(self, name: &str, column: SEXP) -> Self {
503        unsafe {
504            let names_sexp = self.sexp.get_names();
505            if names_sexp == SEXP::nil() {
506                return self;
507            }
508            let ncol = names_sexp.xlength();
509            for i in 0..ncol {
510                if col_name(names_sexp, i) == name {
511                    self.sexp.set_vector_elt(i, column);
512                    return self;
513                }
514            }
515
516            let new_ncol = ncol + 1;
517            let new_list = crate::OwnedProtect::new(SEXP::alloc_list(new_ncol));
518            let new_names = crate::OwnedProtect::new(SEXP::alloc_strsxp(new_ncol));
519
520            for i in 0..ncol {
521                new_list.set_vector_elt(i, self.sexp.vector_elt(i));
522                new_names.set_string_elt(i, names_sexp.string_elt(i));
523            }
524            new_list.set_vector_elt(ncol, column);
525            new_names.set_string_elt(ncol, SEXP::charsxp(name));
526
527            new_list.set_names(*new_names);
528            copy_df_attrs(self.sexp, *new_list);
529
530            DataFrame { sexp: *new_list }
531        }
532    }
533    // endregion
534
535    // region: builder (ex-RDataFrameBuilder, #768)
536
537    /// Start a closure-per-column builder yielding a [`DataFrame`].
538    ///
539    /// The heterogeneous-column analogue of `with_r_matrix`: each column buffer is R memory
540    /// filled by a per-column closure. Available regardless of the `rayon` feature (#1055);
541    /// the columns are filled **in parallel** when `rayon` is enabled and **serially**
542    /// otherwise — the resulting `data.frame` is identical either way.
543    ///
544    /// ```ignore
545    /// let df = DataFrame::builder(1000)
546    ///     .column::<f64>("x", |chunk, off| for (i, v) in chunk.iter_mut().enumerate() { *v = (off + i) as f64 })
547    ///     .column_str("label", |i| Some(format!("row{i}")))
548    ///     .build();
549    /// ```
550    #[inline]
551    pub fn builder(nrow: usize) -> crate::dataframe_builder::RDataFrameBuilder {
552        crate::dataframe_builder::RDataFrameBuilder::new(nrow)
553    }
554    // endregion
555}
556// endregion
557
558// region: column-order name helper + attr copy (absorbed from columnar)
559
560/// Read the i-th column name from a STRSXP names vector.
561///
562/// # Safety
563/// `names_sexp` must be a valid STRSXP with at least `i + 1` elements.
564unsafe fn col_name(names_sexp: SEXP, i: isize) -> &'static str {
565    unsafe {
566        let s = names_sexp.string_elt(i);
567        let p = s.r_char();
568        std::ffi::CStr::from_ptr(p).to_str().unwrap_or("")
569    }
570}
571
572/// Copy class and row.names attributes from one data.frame SEXP to another.
573///
574/// # Safety
575/// Both SEXPs must be valid VECSXPs.
576unsafe fn copy_df_attrs(from: SEXP, to: SEXP) {
577    to.set_class(from.get_class());
578    to.set_row_names(from.get_row_names());
579}
580// endregion
581
582// region: IntoR / TryFromSexp for DataFrame
583
584impl TryFromSexp for DataFrame {
585    type Error = SexpError;
586
587    fn try_from_sexp(sexp: SEXP) -> Result<Self, Self::Error> {
588        DataFrame::from_sexp(sexp).map_err(|e| SexpError::InvalidValue(e.to_string()))
589    }
590}
591
592impl IntoR for DataFrame {
593    type Error = std::convert::Infallible;
594    fn try_into_sexp(self) -> Result<SEXP, Self::Error> {
595        Ok(self.sexp)
596    }
597    unsafe fn try_into_sexp_unchecked(self) -> Result<SEXP, Self::Error> {
598        Ok(self.sexp)
599    }
600    #[inline]
601    fn into_sexp(self) -> SEXP {
602        self.sexp
603    }
604}
605// endregion
606
607// region: The conversion trait family (mirrors IntoR / TryFromSexp)
608
609/// Rust data → R `data.frame`. The data-frame analogue of [`IntoR`].
610///
611/// Implemented by `#[derive(DataFrameRow)]` on a row struct/enum (for `Vec<Row>`), by the
612/// blanket impl for any [`ColumnSource`] (`IntoList`-derived rows), and by the serde column
613/// path. Call it on your data: `rows.into_dataframe()?`.
614///
615/// # Parallel fast path
616///
617/// `into_dataframe_par` (present only with
618/// `feature = "rayon"`) produces the **same** [`DataFrame`] as
619/// [`into_dataframe`](IntoDataFrame::into_dataframe). It defaults to the sequential path, so
620/// every implementor gets a correct `_par` for free; `#[derive(DataFrameRow)]` row types
621/// override it with a genuinely parallel column fill (the #777 flattened `(column,row-range)`
622/// work-list). The verb is stable across feature sets — dropping `_par` degrades cleanly to
623/// the sequential call.
624pub trait IntoDataFrame: Sized {
625    /// Convert this value into a [`DataFrame`].
626    fn into_dataframe(self) -> Result<DataFrame, DataFrameError>;
627
628    /// Parallel column fill (`feature = "rayon"`). Same result as `into_dataframe()`.
629    ///
630    /// Defaults to the sequential path; overridden by the derive for a real parallel fill.
631    #[cfg(feature = "rayon")]
632    fn into_dataframe_par(self) -> Result<DataFrame, DataFrameError> {
633        self.into_dataframe()
634    }
635}
636
637/// R `data.frame` → Rust data. The data-frame analogue of
638/// [`TryFromSexp`].
639///
640/// Implemented by `#[derive(DataFrameRow)]` for `Vec<Row>` and by the serde row path.
641///
642/// # Parallel fast path
643///
644/// `from_dataframe_par` (`feature = "rayon"`) reads the
645/// same rows as [`from_dataframe`](FromDataFrame::from_dataframe), defaulting to the
646/// sequential reader; the derive overrides it with the #765 off-main-thread row assembly.
647pub trait FromDataFrame: Sized {
648    /// Read rows back out of a [`DataFrame`].
649    fn from_dataframe(df: &DataFrame) -> Result<Self, DataFrameError>;
650
651    /// Parallel row read (`feature = "rayon"`). Same result as `from_dataframe()`.
652    #[cfg(feature = "rayon")]
653    fn from_dataframe_par(df: &DataFrame) -> Result<Self, DataFrameError> {
654        Self::from_dataframe(df)
655    }
656}
657// endregion
658
659// region: ColumnSource — internal column-assembly engine (ex-public convert::IntoDataFrame)
660
661/// Internal engine that turns a value into a `data.frame`-shaped [`List`].
662///
663/// This was the historical public `convert::IntoDataFrame` (`-> List`). It is now an internal
664/// engine: the public [`IntoDataFrame`] (`-> Result<DataFrame, _>`) and the enum-flatten
665/// codegen both delegate to it. Not part of the public verb surface.
666#[doc(hidden)]
667pub trait ColumnSource {
668    /// Convert into a `data.frame`-shaped [`List`] (named columns, `data.frame` class,
669    /// `row.names`).
670    fn into_column_list(self) -> List;
671
672    /// Extract named column SEXPs from this value.
673    ///
674    /// Returns `(name, raw SEXP)` per column. The SEXPs are owned by the produced
675    /// data-frame SEXP and must be protected by the caller before it is released.
676    ///
677    /// # Safety
678    ///
679    /// Calls R API functions; must run on the R main thread.
680    fn into_named_columns(self) -> Vec<(String, crate::SEXP)>
681    where
682        Self: Sized,
683    {
684        use crate::SexpExt as _;
685        let list = self.into_column_list();
686        let sexp = list.as_sexp();
687        let n = sexp.len();
688        let mut out = Vec::with_capacity(n);
689        let names_sexp = sexp.get_names();
690        let has_names = !names_sexp.is_nil();
691        for i in 0..(n as isize) {
692            let col_sexp = sexp.vector_elt(i);
693            let col_name = if has_names {
694                names_sexp.string_elt_str(i).unwrap_or("").to_string()
695            } else {
696                i.to_string()
697            };
698            out.push((col_name, col_sexp));
699        }
700        out
701    }
702
703    /// Assemble this source into a validated [`DataFrame`].
704    ///
705    /// The column engine always sets the `data.frame` class (even for an empty frame); the one
706    /// exception is the unnamed-row degradation, which returns a bare unclassed empty list — the
707    /// old `panic!("unnamed list elements")` case, now a clean `Err(UnnamedColumns)`.
708    ///
709    /// This is the bridge from the internal column-assembly engine to the public [`DataFrame`].
710    /// We deliberately do **not** offer a blanket `impl<T: ColumnSource> IntoDataFrame for T`:
711    /// `#[derive(DataFrameRow)]` emits a *concrete* `impl IntoDataFrame for Vec<Row>` per row
712    /// type (and serde uses the `SerdeRows<T>` newtype), so a generic `for T` blanket would
713    /// coherence-conflict with every one of those (the compiler treats `Vec<Row>: ColumnSource`
714    /// as possibly-true). The derive's `into_dataframe` glue calls this method instead.
715    fn into_dataframe(self) -> Result<DataFrame, DataFrameError>
716    where
717        Self: Sized,
718    {
719        use crate::SexpExt as _;
720        let sexp = self.into_column_list().as_sexp();
721        if !sexp.is_data_frame() {
722            return Err(DataFrameError::UnnamedColumns);
723        }
724        Ok(unsafe { DataFrame::from_built_sexp(sexp) })
725    }
726}
727// endregion
728
729// region: DataFrameRowConvert — orphan-rule bridge for `Vec<Row>` conversions
730
731/// Row → DataFrame conversion glue emitted by `#[derive(DataFrameRow)]` on the **row type**.
732///
733/// The orphan rule forbids the derive from writing `impl IntoDataFrame for Vec<Row>` in the user
734/// crate: both `IntoDataFrame` and `Vec` are foreign there, and `Row` only appears *covered*
735/// inside `Vec<_>`, so there is no uncovered local type. Instead the derive implements this
736/// `#[doc(hidden)]` trait on the local `Row` type (legal — `Row` is local), and `miniextendr_api`
737/// carries the blanket [`IntoDataFrame`] / [`FromDataFrame`] impls for `Vec<T: DataFrameRowConvert>`
738/// below (legal — `IntoDataFrame` is local *here*). Users still call the public
739/// `rows.into_dataframe()?` / `Vec::<Row>::from_dataframe(&df)?` verbs.
740#[doc(hidden)]
741pub trait DataFrameRowConvert: Sized {
742    /// Build a [`DataFrame`] from a row vector (sequential).
743    fn rows_into_dataframe(rows: Vec<Self>) -> Result<DataFrame, DataFrameError>;
744
745    /// Build a [`DataFrame`] from a row vector (parallel; defaults to sequential).
746    #[cfg(feature = "rayon")]
747    fn rows_into_dataframe_par(rows: Vec<Self>) -> Result<DataFrame, DataFrameError> {
748        Self::rows_into_dataframe(rows)
749    }
750
751    /// Read a row vector out of a [`DataFrame`]. `None` means this row shape has no reader
752    /// (scalar, column-expansion, and struct-flatten struct shapes do; tagged enum shapes with
753    /// reader-capable fields do too; tagless/map-column/coerced/skip/`as_list` enum shapes and
754    /// opaque-map shapes do not); the blanket surfaces that as a clear error.
755    fn rows_from_dataframe(_df: &DataFrame) -> Option<Result<Vec<Self>, DataFrameError>> {
756        None
757    }
758
759    /// Parallel reader (defaults to the sequential reader).
760    #[cfg(feature = "rayon")]
761    fn rows_from_dataframe_par(df: &DataFrame) -> Option<Result<Vec<Self>, DataFrameError>> {
762        Self::rows_from_dataframe(df)
763    }
764}
765
766/// Error returned by `Vec::<Row>::from_dataframe` when the row shape has no R→Rust reader.
767fn no_reader_error() -> DataFrameError {
768    DataFrameError::Conversion(
769        "this DataFrameRow shape has no R→Rust reader (struct shapes with scalar/expansion/\
770         struct-flatten fields do; tagged enum shapes with reader-capable fields do; \
771         tagless/map-column/coerced/skip/as_list enum shapes and opaque-map shapes do not)"
772            .to_string(),
773    )
774}
775
776impl<T: DataFrameRowConvert> IntoDataFrame for Vec<T> {
777    fn into_dataframe(self) -> Result<DataFrame, DataFrameError> {
778        T::rows_into_dataframe(self)
779    }
780
781    #[cfg(feature = "rayon")]
782    fn into_dataframe_par(self) -> Result<DataFrame, DataFrameError> {
783        T::rows_into_dataframe_par(self)
784    }
785}
786
787impl<T: DataFrameRowConvert> FromDataFrame for Vec<T> {
788    fn from_dataframe(df: &DataFrame) -> Result<Self, DataFrameError> {
789        // Root the input across the read. A `.Call` caller gets this from R's
790        // argument frame, but a Rust caller may hand in a freshly-built,
791        // unprotected frame (`into_dataframe` returns an unrooted SEXP wrapper);
792        // reader-internal allocations would reclaim it mid-read under
793        // `gctorture(TRUE)` (caught by gc_stress_reader_nested_flatten).
794        // Mirrors the guard in serde's `dataframe_to_vec` — see
795        // reviews/2026-05-29-serde-deserialize-fixture-gctorture-input-protect.md.
796        // SAFETY: reader entry runs on the R main thread; `df` wraps a valid SEXP.
797        let _input = unsafe { crate::OwnedProtect::new(df.as_sexp()) };
798        T::rows_from_dataframe(df).unwrap_or_else(|| Err(no_reader_error()))
799    }
800
801    #[cfg(feature = "rayon")]
802    fn from_dataframe_par(df: &DataFrame) -> Result<Self, DataFrameError> {
803        // SAFETY: as in `from_dataframe` above.
804        let _input = unsafe { crate::OwnedProtect::new(df.as_sexp()) };
805        T::rows_from_dataframe_par(df).unwrap_or_else(|| Err(no_reader_error()))
806    }
807}
808// endregion
809
810// region: nrow extraction from row.names
811
812/// Extract `nrow` from R's `row.names` attribute.
813fn extract_nrow(sexp: SEXP) -> Result<usize, DataFrameError> {
814    let row_names = sexp.get_row_names();
815
816    if row_names.is_nil() {
817        return nrow_from_first_column(sexp);
818    }
819
820    let rn_type = row_names.type_of();
821    let rn_len = row_names.xlength();
822
823    if rn_type == SEXPTYPE::INTSXP && rn_len == 2 {
824        let rn: &[i32] = unsafe { row_names.as_slice() };
825        if rn[0] == i32::MIN && rn[1] < 0 {
826            return Ok((-rn[1]) as usize);
827        }
828    }
829
830    if let Ok(n) = usize::try_from(rn_len) {
831        Ok(n)
832    } else {
833        Err(DataFrameError::BadRowNames(format!(
834            "row.names has negative length: {}",
835            rn_len
836        )))
837    }
838}
839
840/// Fall back: extract nrow from the length of the first column.
841fn nrow_from_first_column(sexp: SEXP) -> Result<usize, DataFrameError> {
842    let ncol = sexp.xlength();
843    if ncol == 0 {
844        return Ok(0);
845    }
846    let first_col = sexp.vector_elt(0);
847    if first_col == SEXP::nil() {
848        return Ok(0);
849    }
850    let len = first_col.xlength();
851    if let Ok(n) = usize::try_from(len) {
852        Ok(n)
853    } else {
854        Err(DataFrameError::BadRowNames(
855            "first column has negative length".to_string(),
856        ))
857    }
858}
859// endregion
860
861// region: NamedList / List → DataFrame promotion
862
863/// Validate that all columns in a NamedList have equal length, returning the common length.
864fn validate_equal_lengths(named: &NamedList) -> Result<usize, DataFrameError> {
865    let list = named.as_list();
866    let n = list.len();
867
868    if n == 0 {
869        return Ok(0);
870    }
871
872    let first_col = list.as_sexp().vector_elt(0);
873    let expected: usize = first_col.len();
874
875    let names_sexp = list.names();
876    for i in 1..n {
877        let col = list.as_sexp().vector_elt(i);
878        let col_len: usize = col.len();
879        if col_len != expected {
880            let column = if let Some(names) = names_sexp {
881                let name_sexp = names.string_elt(i);
882                if name_sexp != SEXP::na_string() {
883                    let name_ptr = name_sexp.r_char();
884                    let name_cstr = unsafe { CStr::from_ptr(name_ptr) };
885                    name_cstr.to_str().unwrap_or("<invalid>").to_string()
886                } else {
887                    format!("column {}", i)
888                }
889            } else {
890                format!("column {}", i)
891            };
892
893            return Err(DataFrameError::UnequalLengths {
894                expected,
895                column,
896                actual: col_len,
897            });
898        }
899    }
900
901    Ok(expected)
902}
903
904impl NamedList {
905    /// Promote this named list to a [`DataFrame`].
906    ///
907    /// Validates equal column lengths, sets the `data.frame` class, and adds compact integer
908    /// `row.names`.
909    ///
910    /// # Errors
911    ///
912    /// Returns [`DataFrameError::UnequalLengths`] if columns differ in length.
913    pub fn as_data_frame(&self) -> Result<DataFrame, DataFrameError> {
914        let nrow = validate_equal_lengths(self)?;
915        self.as_list().set_data_frame_class();
916        self.as_list().set_row_names_int(nrow);
917        Ok(DataFrame {
918            sexp: self.as_list().as_sexp(),
919        })
920    }
921}
922
923impl List {
924    /// Promote this named list to a [`DataFrame`].
925    ///
926    /// # Errors
927    ///
928    /// Returns [`DataFrameError`] if the list has no names or columns differ in length.
929    pub fn as_data_frame(&self) -> Result<DataFrame, DataFrameError> {
930        let named = NamedList::new(*self).ok_or(DataFrameError::NoNames)?;
931        named.as_data_frame()
932    }
933}
934// endregion
935
936// region: NamedDataFrameListBuilder (moved from serde::columnar — no serde dependency)
937
938/// Assemble a named list whose elements are [`DataFrame`]s,
939/// without per-result `OwnedProtect` bookkeeping.
940///
941/// # Why this is distinct from [`DataFrame::builder`]
942///
943/// [`DataFrame::builder`](crate::dataframe::DataFrame::builder) and the serde
944/// `SerdeRowBuilder` both produce a *single* [`DataFrame`]. This builder
945/// produces a different shape — a named *list of* data.frames, e.g.
946/// `list(results = df, error = df)` — so it deliberately keeps its own name
947/// rather than folding into the `DataFrame::builder` vocabulary. Its inputs
948/// are [`DataFrame`]s (from any producer: [`IntoDataFrame`], the serde
949/// `vec_to_dataframe`, or [`GroupedDataFrame::frames`]); its output is a
950/// [`List`].
951///
952/// Each [`push`](NamedDataFrameListBuilder::push) protects the input
953/// data.frame's SEXP via an internal [`ProtectScope`](crate::ProtectScope);
954/// [`build`](NamedDataFrameListBuilder::build) consumes the builder and emits
955/// a named list via [`List::from_raw_pairs`](crate::list::List::from_raw_pairs).
956/// The scope drops at the end of `build`, releasing the per-input protects —
957/// by which point the children are reachable from the assembled list.
958///
959/// # Example
960///
961/// ```ignore
962/// let result = NamedDataFrameListBuilder::new()
963///     .push("results", oks.into_dataframe()?)
964///     .push("error",   errs.into_dataframe()?)
965///     .build();
966/// ```
967pub struct NamedDataFrameListBuilder {
968    scope: crate::ProtectScope,
969    pairs: Vec<(String, SEXP)>,
970}
971
972impl NamedDataFrameListBuilder {
973    /// Create an empty builder.
974    ///
975    /// # Safety (caller)
976    ///
977    /// Must be called from the R main thread. The internal
978    /// [`ProtectScope`](crate::ProtectScope) carries `!Send + !Sync`
979    /// so the builder cannot be moved to another thread.
980    pub fn new() -> Self {
981        Self {
982            // SAFETY: ProtectScope requires the R main thread. The builder is
983            // constructible only on the R main thread; ProtectScope carries
984            // NoSendSync so it cannot be moved off-thread.
985            scope: unsafe { crate::ProtectScope::new() },
986            pairs: Vec::new(),
987        }
988    }
989
990    /// Create a builder pre-allocated for `n` entries.
991    ///
992    /// Equivalent to [`new`](Self::new) but avoids repeated re-allocations
993    /// when the number of partitions is known up front.
994    pub fn with_capacity(n: usize) -> Self {
995        Self {
996            scope: unsafe { crate::ProtectScope::new() },
997            pairs: Vec::with_capacity(n),
998        }
999    }
1000
1001    /// Append a named data.frame. The input's SEXP is protected
1002    /// internally for the lifetime of the builder.
1003    #[must_use]
1004    pub fn push<S: Into<String>>(mut self, name: S, df: DataFrame) -> Self {
1005        use crate::IntoR as _;
1006        let sexp = df.into_sexp();
1007        // SAFETY: R main thread (constructor invariant); sexp is a valid
1008        // VECSXP just produced by DataFrame::into_sexp.
1009        unsafe {
1010            self.scope.protect_raw(sexp);
1011        }
1012        self.pairs.push((name.into(), sexp));
1013        self
1014    }
1015
1016    /// Append an arbitrary SEXP under a name, protected like
1017    /// [`push`](Self::push). Used by the serde split-shape writer to carry
1018    /// the caller-supplied empty-`Ok` sentinel, which is deliberately not a
1019    /// `DataFrame`.
1020    ///
1021    /// # Safety
1022    ///
1023    /// `sexp` must be a valid R object; R main thread (constructor invariant).
1024    #[cfg(feature = "serde")]
1025    #[must_use]
1026    pub(crate) unsafe fn push_raw<S: Into<String>>(mut self, name: S, sexp: SEXP) -> Self {
1027        unsafe {
1028            self.scope.protect_raw(sexp);
1029        }
1030        self.pairs.push((name.into(), sexp));
1031        self
1032    }
1033
1034    /// Number of entries pushed so far.
1035    pub fn len(&self) -> usize {
1036        self.pairs.len()
1037    }
1038
1039    /// Whether no entries have been pushed yet.
1040    pub fn is_empty(&self) -> bool {
1041        self.pairs.is_empty()
1042    }
1043
1044    /// Consume the builder and return the assembled named [`List`].
1045    ///
1046    /// The returned `List`'s SEXP is *not* separately protected on return — the
1047    /// caller takes responsibility for protection (typically by immediately
1048    /// handing it back to R via the `.Call` return path). This matches the
1049    /// contract of [`List::from_raw_pairs`](crate::list::List::from_raw_pairs).
1050    pub fn build(self) -> crate::list::List {
1051        // pairs[i].1 is protected by self.scope; from_raw_pairs protects the
1052        // assembled VECSXP and STRSXP during construction. When self drops at
1053        // this function's exit, the input SEXPs are unprotected — but they are
1054        // now children of the returned list, so they remain reachable.
1055        crate::list::List::from_raw_pairs(self.pairs)
1056    }
1057}
1058
1059impl Default for NamedDataFrameListBuilder {
1060    fn default() -> Self {
1061        Self::new()
1062    }
1063}
1064// endregion
1065
1066// region: Debug impl
1067
1068impl std::fmt::Debug for DataFrame {
1069    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1070        f.debug_struct("DataFrame")
1071            .field("nrow", &self.nrow())
1072            .field("ncol", &self.ncol())
1073            .finish()
1074    }
1075}
1076
1077#[cfg(test)]
1078mod tests {
1079    use super::*;
1080
1081    #[test]
1082    fn test_data_frame_error_display() {
1083        let err = DataFrameError::NotDataFrame;
1084        assert_eq!(err.to_string(), "object does not inherit from data.frame");
1085
1086        let err = DataFrameError::NoNames;
1087        assert_eq!(err.to_string(), "data.frame has no column names");
1088
1089        let err = DataFrameError::UnequalLengths {
1090            expected: 3,
1091            column: "y".to_string(),
1092            actual: 5,
1093        };
1094        assert_eq!(err.to_string(), "column \"y\" has length 5 (expected 3)");
1095
1096        let err = DataFrameError::UnnamedColumns;
1097        assert_eq!(
1098            err.to_string(),
1099            "cannot create data frame from unnamed list elements"
1100        );
1101
1102        let err = DataFrameError::NoSuchColumn("g".to_string());
1103        assert_eq!(err.to_string(), "no such column: \"g\"");
1104    }
1105
1106    // region: NamedDataFrameListBuilder structural invariants
1107
1108    /// A new builder has zero length and reports is_empty().
1109    #[test]
1110    fn builder_new_is_empty() {
1111        let b = NamedDataFrameListBuilder::default();
1112        assert_eq!(b.len(), 0);
1113        assert!(b.is_empty());
1114    }
1115
1116    /// with_capacity reserves space but the builder is still empty.
1117    #[test]
1118    fn builder_with_capacity_starts_empty() {
1119        let b = NamedDataFrameListBuilder::with_capacity(8);
1120        assert_eq!(b.len(), 0);
1121        assert!(b.is_empty());
1122    }
1123
1124    /// The builder's scope count starts at zero (no protections yet).
1125    #[test]
1126    fn builder_scope_count_zero_before_push() {
1127        let b = NamedDataFrameListBuilder::new();
1128        assert_eq!(b.scope.count(), 0);
1129    }
1130    // endregion
1131}
1132// endregion