Skip to main content

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