miniextendr_api/list/accumulator.rs
1//! `ListAccumulator` — unknown-length list construction with bounded stack usage.
2//!
3//! Unlike [`ListBuilder`](super::ListBuilder) which requires knowing the size at construction,
4//! `ListAccumulator` supports dynamic growth via `push`. It uses
5//! `ReprotectSlot` internally to maintain O(1) protect stack usage.
6
7use crate::SEXPTYPE::{STRSXP, VECSXP};
8use crate::from_r::SexpError;
9use crate::gc_protect::{OwnedProtect, ProtectScope, ReprotectSlot, Root};
10use crate::into_r::IntoR;
11use crate::sys::{self};
12use crate::{SEXP, SexpExt};
13
14use super::ListMut;
15
16/// Accumulator for building lists when the length is unknown upfront.
17///
18/// Unlike [`super::ListBuilder`] which requires knowing the size at construction,
19/// `ListAccumulator` supports dynamic growth via [`push`](Self::push). It uses
20/// [`ReprotectSlot`] internally to maintain **O(1) protect stack usage** regardless
21/// of how many elements are pushed.
22///
23/// # When to Use
24///
25/// | Scenario | Recommended Type |
26/// |----------|-----------------|
27/// | Known size | [`super::ListBuilder`] - more efficient, no reallocation |
28/// | Unknown size | `ListAccumulator` - bounded stack, dynamic growth |
29/// | Streaming/iterators | `ListAccumulator` or [`collect_list`] |
30///
31/// # Growth Strategy
32///
33/// The internal list grows exponentially (2x) when capacity is exceeded,
34/// achieving amortized O(1) push. Elements are copied during growth.
35///
36/// # Example
37///
38/// ```ignore
39/// unsafe fn collect_filtered(items: &[i32]) -> SEXP {
40/// let scope = ProtectScope::new();
41/// let mut acc = ListAccumulator::new(&scope, 4); // initial capacity hint
42///
43/// for &item in items {
44/// if item > 0 {
45/// acc.push(item); // auto-converts via IntoR
46/// }
47/// }
48///
49/// acc.into_root().get()
50/// }
51/// ```
52pub struct ListAccumulator<'a> {
53 /// The current list container (protected via ReprotectSlot).
54 list: ReprotectSlot<'a>,
55 /// The element being pushed, rooted from its conversion until it is in
56 /// `list` (#1907): across [`grow`](Self::grow), which allocates.
57 temp: ReprotectSlot<'a>,
58 /// Number of elements currently in the list.
59 len: usize,
60 /// Current capacity of the list.
61 cap: usize,
62 /// Reference to the scope for creating the final Root.
63 scope: &'a ProtectScope,
64 /// Per-element names (None = unnamed, Some = named).
65 names: Vec<Option<String>>,
66 /// Whether the result always carries `names`, even when empty
67 /// ([`named`](Self::named)).
68 named: bool,
69}
70
71impl<'a> ListAccumulator<'a> {
72 /// Create a new accumulator with the given initial capacity.
73 ///
74 /// A capacity of 0 is valid; the list will grow on first push.
75 ///
76 /// # Safety
77 ///
78 /// Must be called from the R main thread.
79 pub unsafe fn new(scope: &'a ProtectScope, initial_cap: usize) -> Self {
80 unsafe { Self::with_names(scope, initial_cap, false) }
81 }
82
83 /// Create an accumulator for a named list: the result always has
84 /// `names`, `""` for an element pushed without one, and `character(0)`
85 /// when nothing was pushed, as `List::from_raw_pairs(vec![])` gives
86 /// (#1907). So an empty named list can still become a zero-column data
87 /// frame. An accumulator from [`new`](Self::new) has `names` only once
88 /// something was pushed with [`push_named`](Self::push_named).
89 ///
90 /// # Safety
91 ///
92 /// Must be called from the R main thread.
93 pub unsafe fn named(scope: &'a ProtectScope, initial_cap: usize) -> Self {
94 unsafe { Self::with_names(scope, initial_cap, true) }
95 }
96
97 unsafe fn with_names(scope: &'a ProtectScope, initial_cap: usize, named: bool) -> Self {
98 let cap = initial_cap.max(1); // At least 1 to avoid edge cases
99 let cap_isize: isize = cap.try_into().expect("capacity exceeds isize::MAX");
100 let list_sexp = unsafe { sys::Rf_allocVector(VECSXP, cap_isize) };
101 let list = unsafe { scope.protect_with_index(list_sexp) };
102 let temp = unsafe { scope.protect_with_index(SEXP::nil()) };
103
104 Self {
105 list,
106 temp,
107 len: 0,
108 cap,
109 scope,
110 names: Vec::new(),
111 named,
112 }
113 }
114
115 /// Push a value onto the accumulator.
116 ///
117 /// The value is converted to a SEXP via [`IntoR`] and inserted.
118 /// If the internal list is full, it grows automatically.
119 ///
120 /// # Safety
121 ///
122 /// Must be called from the R main thread.
123 pub unsafe fn push<T: IntoR>(&mut self, value: T) {
124 // Convert into the temp slot first: a `SEXP` value converts to
125 // itself, and the growth below allocates.
126 let sexp = unsafe { self.temp.set_with(|| value.into_sexp()) };
127 unsafe { self.insert(sexp, None) };
128 }
129
130 /// Push a raw SEXP onto the accumulator.
131 ///
132 /// # Safety
133 ///
134 /// - Must be called from the R main thread
135 /// - `sexp` must be a valid SEXP. It need not be rooted: the accumulator
136 /// roots it before anything allocates, so a freshly allocated value
137 /// (`acc.push_sexp(Rf_allocVector(...))`) is safe.
138 pub unsafe fn push_sexp(&mut self, sexp: SEXP) {
139 unsafe {
140 self.temp.set(sexp);
141 self.insert(sexp, None);
142 }
143 }
144
145 /// Push a named value onto the accumulator.
146 ///
147 /// # Safety
148 ///
149 /// Must be called from the R main thread.
150 pub unsafe fn push_named<T: IntoR>(&mut self, name: &str, value: T) {
151 let sexp = unsafe { self.temp.set_with(|| value.into_sexp()) };
152 unsafe { self.insert(sexp, Some(name.to_string())) };
153 }
154
155 /// Store `sexp`, already rooted in the temp slot, as the next element,
156 /// growing the list first when it is full.
157 ///
158 /// # Safety
159 ///
160 /// Must be called from the R main thread, with `sexp` in `self.temp`.
161 unsafe fn insert(&mut self, sexp: SEXP, name: Option<String>) {
162 if self.len >= self.cap {
163 unsafe { self.grow() };
164 }
165 let len_isize: isize = self.len.try_into().expect("list length exceeds isize::MAX");
166 self.list.get().set_vector_elt(len_isize, sexp);
167 self.names.push(name);
168 self.len += 1;
169 }
170
171 /// Push a value only if the condition is true.
172 ///
173 /// # Safety
174 ///
175 /// Must be called from the R main thread.
176 pub unsafe fn push_if<T: IntoR>(&mut self, condition: bool, value: T) {
177 if condition {
178 unsafe { self.push(value) };
179 }
180 }
181
182 /// Push a lazily-evaluated value only if the condition is true.
183 ///
184 /// The closure is only called if `condition` is true.
185 ///
186 /// # Safety
187 ///
188 /// Must be called from the R main thread.
189 pub unsafe fn push_if_with<T: IntoR>(&mut self, condition: bool, f: impl FnOnce() -> T) {
190 if condition {
191 unsafe { self.push(f()) };
192 }
193 }
194
195 /// Push all items from an iterator.
196 ///
197 /// # Safety
198 ///
199 /// Must be called from the R main thread.
200 pub unsafe fn extend_from<I, T>(&mut self, iter: I)
201 where
202 I: IntoIterator<Item = T>,
203 T: IntoR,
204 {
205 for item in iter {
206 unsafe { self.push(item) };
207 }
208 }
209
210 /// Grow the internal list by 2x.
211 ///
212 /// The new list is rooted by a local [`OwnedProtect`] until it replaces
213 /// the old one, so the temp slot keeps the element being pushed (#1907).
214 /// The protect is balanced here, so the stack use stays O(1).
215 ///
216 /// # Safety
217 ///
218 /// Must be called from the R main thread.
219 unsafe fn grow(&mut self) {
220 let new_cap = self.cap.saturating_mul(2).max(4);
221 let new_cap_isize: isize = new_cap.try_into().expect("new capacity exceeds isize::MAX");
222
223 let old_list = self.list.get();
224 let rooted = unsafe { OwnedProtect::new(sys::Rf_allocVector(VECSXP, new_cap_isize)) };
225 let new_list = rooted.get();
226
227 // Copy existing elements
228 for i in 0..self.len {
229 let idx: isize = i.try_into().expect("index exceeds isize::MAX");
230 let elem = old_list.vector_elt(idx);
231 new_list.set_vector_elt(idx, elem);
232 }
233
234 // Replace list slot with new list
235 unsafe { self.list.set(new_list) };
236 self.cap = new_cap;
237 }
238
239 /// Get the current number of elements.
240 #[inline]
241 pub fn len(&self) -> usize {
242 self.len
243 }
244
245 /// Check if the accumulator is empty.
246 #[inline]
247 pub fn is_empty(&self) -> bool {
248 self.len == 0
249 }
250
251 /// Get the current capacity.
252 #[inline]
253 pub fn capacity(&self) -> usize {
254 self.cap
255 }
256
257 /// Finalize the accumulator and return a `Root` pointing to the list.
258 ///
259 /// The returned list is truncated to the actual length (if smaller than capacity).
260 ///
261 /// # Safety
262 ///
263 /// Must be called from the R main thread.
264 pub unsafe fn into_root(self) -> Root<'a> {
265 let has_names = self.named || self.names.iter().any(|n| n.is_some());
266
267 // If len < cap, we need to shrink the list
268 let len_isize: isize = self.len.try_into().expect("list length exceeds isize::MAX");
269 let root = if self.len < self.cap {
270 unsafe {
271 let shrunk = self.list.get().resize(len_isize);
272 // The shrunk list might be the same or a new allocation
273 // Either way, we protect it via the scope
274 self.scope.protect(shrunk)
275 }
276 } else {
277 // List is already the right size, create a Root without extra protection
278 unsafe { self.scope.rooted(self.list.get()) }
279 };
280
281 if has_names {
282 unsafe {
283 // OwnedProtect handles Rf_protect/Rf_unprotect automatically.
284 // Rf_mkCharLenCE can allocate, so names_sexp must be protected.
285 let names_sexp = OwnedProtect::new(sys::Rf_allocVector(STRSXP, len_isize));
286 for (i, name) in self.names.iter().enumerate() {
287 let idx: isize = i.try_into().expect("index exceeds isize::MAX");
288 if let Some(n) = name {
289 let _n_len: i32 = n.len().try_into().expect("name exceeds i32::MAX bytes");
290 let charsxp = SEXP::charsxp(n);
291 names_sexp.get().set_string_elt(idx, charsxp);
292 } else {
293 names_sexp.get().set_string_elt(idx, SEXP::blank_string());
294 }
295 }
296 root.get().set_names(names_sexp.get());
297 }
298 }
299
300 root
301 }
302
303 /// Finalize and return the raw SEXP.
304 ///
305 /// # Safety
306 ///
307 /// Must be called from the R main thread.
308 pub unsafe fn into_sexp(self) -> SEXP {
309 unsafe { self.into_root().get() }
310 }
311}
312
313/// Collect an iterator into an R list with bounded protect stack usage.
314///
315/// This is a convenience wrapper around [`ListAccumulator`] for iterator-based
316/// collection. Each element is converted via [`IntoR`].
317///
318/// # Safety
319///
320/// Must be called from the R main thread.
321///
322/// # Example
323///
324/// ```ignore
325/// unsafe fn squares(n: usize) -> SEXP {
326/// let scope = ProtectScope::new();
327/// collect_list(&scope, (0..n).map(|i| (i * i) as i32)).get()
328/// }
329/// ```
330pub unsafe fn collect_list<'a, I, T>(scope: &'a ProtectScope, iter: I) -> Root<'a>
331where
332 I: IntoIterator<Item = T>,
333 T: IntoR,
334{
335 let iter = iter.into_iter();
336 let (lower, upper) = iter.size_hint();
337 let initial_cap = upper.unwrap_or(lower).max(4);
338
339 let mut acc = unsafe { ListAccumulator::new(scope, initial_cap) };
340
341 for item in iter {
342 unsafe { acc.push(item) };
343 }
344
345 unsafe { acc.into_root() }
346}
347
348impl ListMut {
349 /// Wrap an existing `VECSXP` without additional checks.
350 ///
351 /// # Safety
352 ///
353 /// Caller must ensure `sexp` is a valid `VECSXP` and remains managed by R.
354 #[inline]
355 pub const unsafe fn from_raw(sexp: SEXP) -> Self {
356 ListMut(sexp)
357 }
358
359 /// Get the underlying `SEXP`.
360 #[inline]
361 pub const fn as_sexp(&self) -> SEXP {
362 self.0
363 }
364
365 /// Length of the list (number of elements).
366 #[inline]
367 pub fn len(&self) -> isize {
368 self.0.xlength()
369 }
370
371 /// Returns true if the list is empty.
372 #[inline]
373 pub fn is_empty(&self) -> bool {
374 self.len() == 0
375 }
376
377 /// Get raw SEXP element at 0-based index. Returns `None` if out of bounds.
378 #[inline]
379 pub fn get(&self, idx: isize) -> Option<SEXP> {
380 if idx < 0 || idx >= self.len() {
381 return None;
382 }
383 Some(self.0.vector_elt(idx))
384 }
385
386 /// Set raw SEXP element at 0-based index.
387 #[inline]
388 pub fn set(&mut self, idx: isize, value: SEXP) -> Result<(), SexpError> {
389 if idx < 0 || idx >= self.len() {
390 return Err(SexpError::InvalidValue("index out of bounds".into()));
391 }
392 self.0.set_vector_elt(idx, value);
393 Ok(())
394 }
395}
396// endregion