Skip to main content

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(#[automatically_derived]
impl ::core::fmt::Debug for DataFrameError {
    #[inline]
    fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result {
        match self {
            Self::NotList(__self_0) =>
                ::core::fmt::Formatter::debug_tuple_field1_finish(f,
                    "NotList", &__self_0),
            Self::NotDataFrame =>
                ::core::fmt::Formatter::write_str(f, "NotDataFrame"),
            Self::NoNames => ::core::fmt::Formatter::write_str(f, "NoNames"),
            Self::BadRowNames(__self_0) =>
                ::core::fmt::Formatter::debug_tuple_field1_finish(f,
                    "BadRowNames", &__self_0),
            Self::UnequalLengths { expected: __self_0, columns: __self_1 } =>
                ::core::fmt::Formatter::debug_struct_field2_finish(f,
                    "UnequalLengths", "expected", __self_0, "columns",
                    &__self_1),
            Self::UnnamedColumns =>
                ::core::fmt::Formatter::write_str(f, "UnnamedColumns"),
            Self::NoSuchColumn(__self_0) =>
                ::core::fmt::Formatter::debug_tuple_field1_finish(f,
                    "NoSuchColumn", &__self_0),
            Self::EmptyGroupColumns =>
                ::core::fmt::Formatter::write_str(f, "EmptyGroupColumns"),
            Self::UnsupportedGroupColumn { column: __self_0, type_of: __self_1
                } =>
                ::core::fmt::Formatter::debug_struct_field2_finish(f,
                    "UnsupportedGroupColumn", "column", __self_0, "type_of",
                    &__self_1),
            Self::NotGroupedDataFrame =>
                ::core::fmt::Formatter::write_str(f, "NotGroupedDataFrame"),
            Self::GroupsNotDataFrame =>
                ::core::fmt::Formatter::write_str(f, "GroupsNotDataFrame"),
            Self::MissingGroupRows =>
                ::core::fmt::Formatter::write_str(f, "MissingGroupRows"),
            Self::BadGroupRows { group: __self_0, type_of: __self_1 } =>
                ::core::fmt::Formatter::debug_struct_field2_finish(f,
                    "BadGroupRows", "group", __self_0, "type_of", &__self_1),
            Self::GroupIndexOutOfRange {
                group: __self_0, value: __self_1, nrow: __self_2 } =>
                ::core::fmt::Formatter::debug_struct_field3_finish(f,
                    "GroupIndexOutOfRange", "group", __self_0, "value",
                    __self_1, "nrow", &__self_2),
            Self::GroupRowUncovered { row: __self_0, nrow: __self_1 } =>
                ::core::fmt::Formatter::debug_struct_field2_finish(f,
                    "GroupRowUncovered", "row", __self_0, "nrow", &__self_1),
            Self::GroupRowDuplicated {
                row: __self_0, first_group: __self_1, second_group: __self_2 }
                =>
                ::core::fmt::Formatter::debug_struct_field3_finish(f,
                    "GroupRowDuplicated", "row", __self_0, "first_group",
                    __self_1, "second_group", &__self_2),
            Self::Conversion(__self_0) =>
                ::core::fmt::Formatter::debug_tuple_field1_finish(f,
                    "Conversion", &__self_0),
        }
    }
}Debug, #[automatically_derived]
impl ::core::clone::Clone for DataFrameError {
    #[inline]
    fn clone(&self) -> Self {
        match self {
            Self::NotList(__self_0) =>
                DataFrameError::NotList(::core::clone::Clone::clone(__self_0)),
            Self::NotDataFrame => DataFrameError::NotDataFrame,
            Self::NoNames => DataFrameError::NoNames,
            Self::BadRowNames(__self_0) =>
                DataFrameError::BadRowNames(::core::clone::Clone::clone(__self_0)),
            Self::UnequalLengths { expected: __self_0, columns: __self_1 } =>
                DataFrameError::UnequalLengths {
                    expected: ::core::clone::Clone::clone(__self_0),
                    columns: ::core::clone::Clone::clone(__self_1),
                },
            Self::UnnamedColumns => DataFrameError::UnnamedColumns,
            Self::NoSuchColumn(__self_0) =>
                DataFrameError::NoSuchColumn(::core::clone::Clone::clone(__self_0)),
            Self::EmptyGroupColumns => DataFrameError::EmptyGroupColumns,
            Self::UnsupportedGroupColumn { column: __self_0, type_of: __self_1
                } =>
                DataFrameError::UnsupportedGroupColumn {
                    column: ::core::clone::Clone::clone(__self_0),
                    type_of: ::core::clone::Clone::clone(__self_1),
                },
            Self::NotGroupedDataFrame => DataFrameError::NotGroupedDataFrame,
            Self::GroupsNotDataFrame => DataFrameError::GroupsNotDataFrame,
            Self::MissingGroupRows => DataFrameError::MissingGroupRows,
            Self::BadGroupRows { group: __self_0, type_of: __self_1 } =>
                DataFrameError::BadGroupRows {
                    group: ::core::clone::Clone::clone(__self_0),
                    type_of: ::core::clone::Clone::clone(__self_1),
                },
            Self::GroupIndexOutOfRange {
                group: __self_0, value: __self_1, nrow: __self_2 } =>
                DataFrameError::GroupIndexOutOfRange {
                    group: ::core::clone::Clone::clone(__self_0),
                    value: ::core::clone::Clone::clone(__self_1),
                    nrow: ::core::clone::Clone::clone(__self_2),
                },
            Self::GroupRowUncovered { row: __self_0, nrow: __self_1 } =>
                DataFrameError::GroupRowUncovered {
                    row: ::core::clone::Clone::clone(__self_0),
                    nrow: ::core::clone::Clone::clone(__self_1),
                },
            Self::GroupRowDuplicated {
                row: __self_0, first_group: __self_1, second_group: __self_2 }
                =>
                DataFrameError::GroupRowDuplicated {
                    row: ::core::clone::Clone::clone(__self_0),
                    first_group: ::core::clone::Clone::clone(__self_1),
                    second_group: ::core::clone::Clone::clone(__self_2),
                },
            Self::Conversion(__self_0) =>
                DataFrameError::Conversion(::core::clone::Clone::clone(__self_0)),
        }
    }
}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) => f.write_fmt(format_args!("expected a data frame, got {0}",
        crate::typed_list::sexptype_name(*actual)))write!(
153                f,
154                "expected a data frame, got {}",
155                crate::typed_list::sexptype_name(*actual)
156            ),
157            DataFrameError::NotDataFrame => f.write_fmt(format_args!("object does not inherit from data.frame"))write!(f, "object does not inherit from data.frame"),
158            DataFrameError::NoNames => f.write_fmt(format_args!("data.frame has no column names"))write!(f, "data.frame has no column names"),
159            DataFrameError::BadRowNames(msg) => {
160                f.write_fmt(format_args!("could not extract nrow from row.names: {0}", msg))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                        f.write_fmt(format_args!("; "))write!(f, "; ")?;
166                    }
167                    f.write_fmt(format_args!("column {0:?} has length {1} (expected {2})", column,
        actual, expected))write!(
168                        f,
169                        "column {column:?} has length {actual} (expected {expected})"
170                    )?;
171                }
172                Ok(())
173            }
174            DataFrameError::UnnamedColumns => {
175                f.write_fmt(format_args!("cannot create data frame from unnamed list elements"))write!(f, "cannot create data frame from unnamed list elements")
176            }
177            DataFrameError::NoSuchColumn(name) => {
178                f.write_fmt(format_args!("no such column: {0:?}", name))write!(f, "no such column: {:?}", name)
179            }
180            DataFrameError::EmptyGroupColumns => {
181                f.write_fmt(format_args!("group_by_multi requires at least one column"))write!(f, "group_by_multi requires at least one column")
182            }
183            DataFrameError::UnsupportedGroupColumn { column, type_of } => f.write_fmt(format_args!("cannot group by column {0:?} ({1}): supported key types are factor, character, integer, logical, and double (including Date and POSIXct)",
        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 => f.write_fmt(format_args!("not a grouped_df: the frame does not inherit from grouped_df or rowwise_df; use group_by/group_by_multi to compute grouping instead"))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 => f.write_fmt(format_args!("not a valid grouped_df or rowwise_df: the `groups` attribute must be a data frame. {0}",
        CORRUPT_GROUPS_HINT))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 => f.write_fmt(format_args!("not a valid grouped_df or rowwise_df: the last column of the `groups` attribute must be called `.rows`. {0}",
        CORRUPT_GROUPS_HINT))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            } => f.write_fmt(format_args!("not a valid grouped_df or rowwise_df: the `.rows` column must be a list of one-based integer vectors, not {0}. {1}",
        type_of, CORRUPT_GROUPS_HINT))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            } => f.write_fmt(format_args!("not a valid grouped_df or rowwise_df: the `.rows` column must be a list of one-based integer vectors, but the element for group {0} is {1}. {2}",
        group + 1, type_of, CORRUPT_GROUPS_HINT))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 } => f.write_fmt(format_args!("grouped_df `.rows` index {0} for group {1} is out of range (the frame has {2} rows; valid indices are 1..={3}). {4}",
        value, group + 1, nrow, nrow, STALE_GROUPS_HINT))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 } => f.write_fmt(format_args!("grouped_df `.rows` do not cover row {0} (the frame has {1} rows; each row belongs to exactly one group). {2}",
        row + 1, nrow, STALE_GROUPS_HINT))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 => f.write_fmt(format_args!("grouped_df `.rows` list row {0} twice in group {1} (each row belongs to exactly one group). {2}",
        row + 1, first_group + 1, STALE_GROUPS_HINT))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            } => f.write_fmt(format_args!("grouped_df `.rows` put row {0} in both group {1} and group {2} (each row belongs to exactly one group). {3}",
        row + 1, first_group + 1, second_group + 1, STALE_GROUPS_HINT))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) => f.write_fmt(format_args!("{0}", 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(#[automatically_derived]
#[doc(hidden)]
unsafe impl ::core::clone::TrivialClone for DataFrame { }
#[automatically_derived]
impl ::core::clone::Clone for DataFrame {
    #[inline]
    fn clone(&self) -> Self {
        let _: ::core::clone::AssertParamIsClone<SEXP>;
        *self
    }
}Clone, #[automatically_derived]
impl ::core::marker::Copy for DataFrame { }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| !#[allow(non_exhaustive_omitted_patterns)] match col_name(class, i) {
    "grouped_df" | "rowwise_df" => true,
    _ => false,
}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(::alloc::__export::must_use({
        ::alloc::fmt::format(format_args!("row.names has negative length: {0}",
                rn_len))
    })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                    ::alloc::__export::must_use({
        ::alloc::fmt::format(format_args!("column {0}", i))
    })format!("column {}", i)
1882                }
1883            } else {
1884                ::alloc::__export::must_use({
        ::alloc::fmt::format(format_args!("column {0}", i))
    })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