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 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}