Skip to main content

miniextendr_api/list/
named_builder.rs

1//! Owned construction of heterogeneous named lists and data frames.
2
3use crate::dataframe::{BuiltDataFrame, DataFrameError};
4use crate::{IntoR, Preserved, SEXP, SexpExt};
5
6/// A safe, owned builder for a named R list of mixed [`IntoR`] values.
7///
8/// One growing `VECSXP` holds the values and is rooted by a [`Preserved`] handle.
9/// Moving or dropping builders in any order is safe: no protect-stack entries
10/// are held by the builder, regardless of the number of values pushed.
11/// Construction, use and drop must take place on R's main thread (`!Send + !Sync`).
12///
13/// Return the builder from a `#[miniextendr]` function to return a named list.
14/// To keep working with the finished value while R allocates (set a class,
15/// pass it to R code, read it back), consume it with
16/// [`into_list`](Self::into_list) for a rooted [`BuiltList`], or with
17/// [`into_data_frame`](Self::into_data_frame) for a rooted data frame. Empty
18/// results have `names = character(0)`. There is no bare [`List`](super::List)
19/// finish.
20///
21/// ```ignore
22/// let mut columns = NamedListBuilder::with_capacity(2);
23/// columns.push("id", vec![1, 2, 3]);
24/// columns.push("value", vec![0.5, 1.5, 2.5]);
25/// columns.rename(1, "estimate");
26/// let frame = columns.into_data_frame()?;
27/// ```
28pub struct NamedListBuilder {
29    values: Preserved,
30    names: Vec<String>,
31}
32
33impl NamedListBuilder {
34    /// Create an empty builder on R's main thread.
35    pub fn new() -> Self {
36        Self::with_capacity(0)
37    }
38
39    /// Reserve space for `n` entries on R's main thread.
40    pub fn with_capacity(n: usize) -> Self {
41        let capacity = isize::try_from(n).expect("list capacity exceeds isize::MAX");
42        Self {
43            // SAFETY: constructor runs on R main thread; the list is rooted
44            // before anything else allocates.
45            values: Preserved::new(unsafe { SEXP::alloc_list(capacity) }),
46            names: Vec::new(),
47        }
48    }
49
50    /// Convert and append a value, rooting it before any later allocation.
51    pub fn push(&mut self, name: impl Into<String>, value: impl IntoR) {
52        self.push_sexp(name, value.into_sexp());
53    }
54
55    /// Append an existing R value, retaining its attributes and identity.
56    ///
57    /// The value must be live when passed in. A freshly allocated, unrooted
58    /// value can be passed directly: even when growth is needed it is rooted
59    /// before the allocation. The builder then keeps it alive independently
60    /// of the caller's roots.
61    pub fn push_sexp(&mut self, name: impl Into<String>, sexp: SEXP) {
62        let index = isize::try_from(self.len()).expect("list length exceeds isize::MAX");
63        if index == self.values.xlength() {
64            // Growth allocates before the value can enter the container.
65            // This temporary precious-list root uses no protect-stack slot.
66            let pending = Preserved::new(sexp);
67            let capacity = self.len().saturating_mul(2).max(4);
68            self.resize(capacity);
69            self.values.set_vector_elt(index, pending.as_sexp());
70        } else {
71            // SET_VECTOR_ELT on this ordinary VECSXP does not allocate.
72            self.values.set_vector_elt(index, sexp);
73        }
74        // Even a user-defined name conversion may allocate in R. Store the
75        // value first so it stays rooted throughout that conversion too.
76        self.names.push(name.into());
77    }
78
79    /// Rename the entry at the zero-based `index`.
80    ///
81    /// Panics if the index is outside `0..self.len()`.
82    pub fn rename(&mut self, index: usize, name: impl Into<String>) {
83        self.names[index] = name.into();
84    }
85
86    /// Entry names in insertion order.
87    pub fn names(&self) -> &[String] {
88        &self.names
89    }
90
91    /// Edit names without changing the number or order of values.
92    pub fn names_mut(&mut self) -> &mut [String] {
93        &mut self.names
94    }
95
96    /// Number of entries pushed so far.
97    pub fn len(&self) -> usize {
98        self.names.len()
99    }
100
101    /// Whether no entries have been pushed.
102    pub fn is_empty(&self) -> bool {
103        self.names.is_empty()
104    }
105
106    /// Finish as an owned, GC-rooted named list.
107    ///
108    /// The [`BuiltList`] keeps the list reachable while it lives, so its
109    /// attributes can be set and it can be handed to R code before it is
110    /// returned.
111    pub fn into_list(mut self) -> BuiltList {
112        self.finish();
113        // SAFETY: `self.values` roots a VECSXP; the handle takes that root over.
114        unsafe { BuiltList::from_root(self.values) }
115    }
116
117    /// Finish as an owned, GC-rooted data frame with compact `row.names`.
118    ///
119    /// Returns [`DataFrameError::UnequalLengths`] naming every column whose
120    /// length differs from the first. No recycling is performed. Zero columns
121    /// produce a zero-row frame with `names = character(0)`.
122    pub fn into_data_frame(mut self) -> Result<BuiltDataFrame, DataFrameError> {
123        self.finish();
124        // SAFETY: self keeps the list rooted throughout validation and all
125        // attribute allocations, until BuiltDataFrame has its own root.
126        let list = unsafe { super::List::from_raw(*self.values) };
127        let named = super::NamedList::new(list).expect("builder always sets names");
128        let nrow = crate::dataframe::validate_equal_lengths(&named)?;
129        let nrow = i32::try_from(nrow)
130            .map_err(|_| DataFrameError::BadRowNames("row count exceeds i32::MAX".into()))?;
131        let row_names = if nrow == 0 {
132            Vec::<i32>::new()
133        } else {
134            ::alloc::boxed::box_assume_init_into_vec_unsafe(::alloc::intrinsics::write_box_via_move(::alloc::boxed::Box::new_uninit(),
        [i32::MIN, -nrow]))vec![i32::MIN, -nrow]
135        };
136        let row_names = Preserved::new(row_names);
137        self.values.set_row_names(row_names.as_sexp());
138        let class = Preserved::new("data.frame");
139        self.values.set_class(class.as_sexp());
140        // SAFETY: equal lengths, names and valid row.names/class set above;
141        // the frame takes the list's root over.
142        Ok(unsafe { BuiltDataFrame::from_root(self.values) })
143    }
144
145    /// Reallocate, keeping both containers rooted until copying is complete.
146    fn resize(&mut self, capacity: usize) {
147        let mut replacement = Self::with_capacity(capacity);
148        for i in 0..self.len() {
149            let i = isize::try_from(i).expect("list index exceeds isize::MAX");
150            replacement
151                .values
152                .set_vector_elt(i, self.values.vector_elt(i));
153        }
154        std::mem::swap(&mut self.values, &mut replacement.values);
155        // replacement drops the old container's root, after the new one
156        // owns every child. Its names Vec is empty.
157    }
158
159    fn finish(&mut self) {
160        if self.values.len() != self.len() {
161            self.resize(self.len());
162        }
163        let names = Preserved::new(self.names.as_slice());
164        self.values.set_names(names.as_sexp());
165    }
166}
167
168impl Default for NamedListBuilder {
169    fn default() -> Self {
170        Self::new()
171    }
172}
173
174impl IntoR for NamedListBuilder {
175    type Error = std::convert::Infallible;
176
177    fn try_into_sexp(mut self) -> Result<SEXP, Self::Error> {
178        self.finish();
179        // Releasing the root does not allocate. As with BuiltDataFrame, the
180        // IntoR caller takes responsibility for the returned SEXP.
181        Ok(self.values.into_sexp())
182    }
183
184    unsafe fn try_into_sexp_unchecked(self) -> Result<SEXP, Self::Error> {
185        self.try_into_sexp()
186    }
187}
188
189/// An owned, GC-rooted R list: the plain-list finish of [`NamedListBuilder`].
190///
191/// The list analogue of [`BuiltDataFrame`]: it holds a [`Preserved`] root for
192/// as long as it lives (`!Send + !Sync`), dereferences to the
193/// [`List`](super::List) view for reads, and returns to R through [`IntoR`].
194/// [`set_class`](Self::set_class) and [`set_attr`](Self::set_attr) allocate
195/// while the list stays rooted, and return the handle, so a chain is rooted at
196/// every link:
197///
198/// ```ignore
199/// let out = columns.into_list().set_class(&["my_result", "list"]);
200/// call_r_helper(out.as_sexp()); // `out` keeps the list alive across the call
201/// out
202/// ```
203///
204/// A `List` copied out of the handle (`*out`) is an unrooted view: it stays
205/// valid only while the handle lives.
206pub struct BuiltList {
207    view: super::List,
208    /// Roots `view`'s list for as long as the handle lives.
209    root: Preserved,
210}
211
212impl BuiltList {
213    /// Take over an existing root of a list.
214    ///
215    /// # Safety
216    ///
217    /// `root` holds a VECSXP.
218    unsafe fn from_root(root: Preserved) -> Self {
219        Self {
220            // SAFETY: caller guarantees a VECSXP.
221            view: unsafe { super::List::from_raw(root.as_sexp()) },
222            root,
223        }
224    }
225
226    /// Set the `class` attribute.
227    pub fn set_class(self, classes: &[&str]) -> Self {
228        let class = Preserved::new(classes);
229        self.view.as_sexp().set_class(class.as_sexp());
230        self
231    }
232
233    /// Set the attribute `name` to `value`.
234    pub fn set_attr(self, name: &str, value: impl IntoR) -> Self {
235        let value = Preserved::new(value);
236        // Symbols are never collected.
237        let symbol = SEXP::symbol(name);
238        self.view.as_sexp().set_attr(symbol, value.as_sexp());
239        self
240    }
241}
242
243impl std::ops::Deref for BuiltList {
244    type Target = super::List;
245    #[inline]
246    fn deref(&self) -> &super::List {
247        &self.view
248    }
249}
250
251impl IntoR for BuiltList {
252    type Error = std::convert::Infallible;
253
254    fn try_into_sexp(self) -> Result<SEXP, Self::Error> {
255        // As with BuiltDataFrame, the IntoR caller takes responsibility for
256        // the returned SEXP.
257        Ok(self.root.into_sexp())
258    }
259
260    unsafe fn try_into_sexp_unchecked(self) -> Result<SEXP, Self::Error> {
261        Ok(self.root.into_sexp())
262    }
263
264    #[inline]
265    fn into_sexp(self) -> SEXP {
266        self.root.into_sexp()
267    }
268}
269
270impl std::fmt::Debug for BuiltList {
271    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
272        f.debug_struct("BuiltList")
273            .field("len", &self.view.as_sexp().xlength())
274            .finish()
275    }
276}