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    /// Temporary slot for element conversion and list growth.
56    temp: ReprotectSlot<'a>,
57    /// Number of elements currently in the list.
58    len: usize,
59    /// Current capacity of the list.
60    cap: usize,
61    /// Reference to the scope for creating the final Root.
62    scope: &'a ProtectScope,
63    /// Per-element names (None = unnamed, Some = named).
64    names: Vec<Option<String>>,
65}
66
67impl<'a> ListAccumulator<'a> {
68    /// Create a new accumulator with the given initial capacity.
69    ///
70    /// A capacity of 0 is valid; the list will grow on first push.
71    ///
72    /// # Safety
73    ///
74    /// Must be called from the R main thread.
75    pub unsafe fn new(scope: &'a ProtectScope, initial_cap: usize) -> Self {
76        let cap = initial_cap.max(1); // At least 1 to avoid edge cases
77        let cap_isize: isize = cap.try_into().expect("capacity exceeds isize::MAX");
78        let list_sexp = unsafe { sys::Rf_allocVector(VECSXP, cap_isize) };
79        let list = unsafe { scope.protect_with_index(list_sexp) };
80        let temp = unsafe { scope.protect_with_index(SEXP::nil()) };
81
82        Self {
83            list,
84            temp,
85            len: 0,
86            cap,
87            scope,
88            names: Vec::new(),
89        }
90    }
91
92    /// Push a value onto the accumulator.
93    ///
94    /// The value is converted to a SEXP via [`IntoR`] and inserted.
95    /// If the internal list is full, it grows automatically.
96    ///
97    /// # Safety
98    ///
99    /// Must be called from the R main thread.
100    pub unsafe fn push<T: IntoR>(&mut self, value: T) {
101        // Grow if needed
102        if self.len >= self.cap {
103            unsafe { self.grow() };
104        }
105
106        // Convert value using temp slot for protection during conversion
107        let sexp = unsafe { self.temp.set_with(|| value.into_sexp()) };
108
109        // Insert into list (list and temp are both protected)
110        let len_isize: isize = self.len.try_into().expect("list length exceeds isize::MAX");
111        self.list.get().set_vector_elt(len_isize, sexp);
112
113        self.names.push(None);
114        self.len += 1;
115    }
116
117    /// Push a raw SEXP onto the accumulator.
118    ///
119    /// # Safety
120    ///
121    /// - Must be called from the R main thread
122    /// - `sexp` must be a valid SEXP (it will be temporarily protected)
123    pub unsafe fn push_sexp(&mut self, sexp: SEXP) {
124        // Grow if needed
125        if self.len >= self.cap {
126            unsafe { self.grow() };
127        }
128
129        // Protect the sexp during insertion using temp slot
130        let len_isize: isize = self.len.try_into().expect("list length exceeds isize::MAX");
131        unsafe {
132            self.temp.set(sexp);
133            self.list.get().set_vector_elt(len_isize, sexp);
134        }
135
136        self.names.push(None);
137        self.len += 1;
138    }
139
140    /// Push a named value onto the accumulator.
141    ///
142    /// # Safety
143    ///
144    /// Must be called from the R main thread.
145    pub unsafe fn push_named<T: IntoR>(&mut self, name: &str, value: T) {
146        // Grow if needed
147        if self.len >= self.cap {
148            unsafe { self.grow() };
149        }
150
151        let sexp = unsafe { self.temp.set_with(|| value.into_sexp()) };
152
153        let len_isize: isize = self.len.try_into().expect("list length exceeds isize::MAX");
154        self.list.get().set_vector_elt(len_isize, sexp);
155
156        self.names.push(Some(name.to_string()));
157        self.len += 1;
158    }
159
160    /// Push a value only if the condition is true.
161    ///
162    /// # Safety
163    ///
164    /// Must be called from the R main thread.
165    pub unsafe fn push_if<T: IntoR>(&mut self, condition: bool, value: T) {
166        if condition {
167            unsafe { self.push(value) };
168        }
169    }
170
171    /// Push a lazily-evaluated value only if the condition is true.
172    ///
173    /// The closure is only called if `condition` is true.
174    ///
175    /// # Safety
176    ///
177    /// Must be called from the R main thread.
178    pub unsafe fn push_if_with<T: IntoR>(&mut self, condition: bool, f: impl FnOnce() -> T) {
179        if condition {
180            unsafe { self.push(f()) };
181        }
182    }
183
184    /// Push all items from an iterator.
185    ///
186    /// # Safety
187    ///
188    /// Must be called from the R main thread.
189    pub unsafe fn extend_from<I, T>(&mut self, iter: I)
190    where
191        I: IntoIterator<Item = T>,
192        T: IntoR,
193    {
194        for item in iter {
195            unsafe { self.push(item) };
196        }
197    }
198
199    /// Grow the internal list by 2x.
200    ///
201    /// # Safety
202    ///
203    /// Must be called from the R main thread.
204    unsafe fn grow(&mut self) {
205        let new_cap = self.cap.saturating_mul(2).max(4);
206        let new_cap_isize: isize = new_cap.try_into().expect("new capacity exceeds isize::MAX");
207
208        // Allocate new list via temp slot (safe pattern)
209        let old_list = self.list.get();
210        unsafe {
211            self.temp
212                .set_with(|| sys::Rf_allocVector(VECSXP, new_cap_isize));
213        }
214        let new_list = self.temp.get();
215
216        // Copy existing elements
217        for i in 0..self.len {
218            let idx: isize = i.try_into().expect("index exceeds isize::MAX");
219            let elem = old_list.vector_elt(idx);
220            new_list.set_vector_elt(idx, elem);
221        }
222
223        // Replace list slot with new list
224        unsafe { self.list.set(new_list) };
225        self.cap = new_cap;
226    }
227
228    /// Get the current number of elements.
229    #[inline]
230    pub fn len(&self) -> usize {
231        self.len
232    }
233
234    /// Check if the accumulator is empty.
235    #[inline]
236    pub fn is_empty(&self) -> bool {
237        self.len == 0
238    }
239
240    /// Get the current capacity.
241    #[inline]
242    pub fn capacity(&self) -> usize {
243        self.cap
244    }
245
246    /// Finalize the accumulator and return a `Root` pointing to the list.
247    ///
248    /// The returned list is truncated to the actual length (if smaller than capacity).
249    ///
250    /// # Safety
251    ///
252    /// Must be called from the R main thread.
253    pub unsafe fn into_root(self) -> Root<'a> {
254        let has_names = self.names.iter().any(|n| n.is_some());
255
256        // If len < cap, we need to shrink the list
257        let len_isize: isize = self.len.try_into().expect("list length exceeds isize::MAX");
258        let root = if self.len < self.cap {
259            unsafe {
260                let shrunk = self.list.get().resize(len_isize);
261                // The shrunk list might be the same or a new allocation
262                // Either way, we protect it via the scope
263                self.scope.protect(shrunk)
264            }
265        } else {
266            // List is already the right size, create a Root without extra protection
267            unsafe { self.scope.rooted(self.list.get()) }
268        };
269
270        if has_names {
271            unsafe {
272                // OwnedProtect handles Rf_protect/Rf_unprotect automatically.
273                // Rf_mkCharLenCE can allocate, so names_sexp must be protected.
274                let names_sexp = OwnedProtect::new(sys::Rf_allocVector(STRSXP, len_isize));
275                for (i, name) in self.names.iter().enumerate() {
276                    let idx: isize = i.try_into().expect("index exceeds isize::MAX");
277                    if let Some(n) = name {
278                        let _n_len: i32 = n.len().try_into().expect("name exceeds i32::MAX bytes");
279                        let charsxp = SEXP::charsxp(n);
280                        names_sexp.get().set_string_elt(idx, charsxp);
281                    } else {
282                        names_sexp.get().set_string_elt(idx, SEXP::blank_string());
283                    }
284                }
285                root.get().set_names(names_sexp.get());
286            }
287        }
288
289        root
290    }
291
292    /// Finalize and return the raw SEXP.
293    ///
294    /// # Safety
295    ///
296    /// Must be called from the R main thread.
297    pub unsafe fn into_sexp(self) -> SEXP {
298        unsafe { self.into_root().get() }
299    }
300}
301
302/// Collect an iterator into an R list with bounded protect stack usage.
303///
304/// This is a convenience wrapper around [`ListAccumulator`] for iterator-based
305/// collection. Each element is converted via [`IntoR`].
306///
307/// # Safety
308///
309/// Must be called from the R main thread.
310///
311/// # Example
312///
313/// ```ignore
314/// unsafe fn squares(n: usize) -> SEXP {
315///     let scope = ProtectScope::new();
316///     collect_list(&scope, (0..n).map(|i| (i * i) as i32)).get()
317/// }
318/// ```
319pub unsafe fn collect_list<'a, I, T>(scope: &'a ProtectScope, iter: I) -> Root<'a>
320where
321    I: IntoIterator<Item = T>,
322    T: IntoR,
323{
324    let iter = iter.into_iter();
325    let (lower, upper) = iter.size_hint();
326    let initial_cap = upper.unwrap_or(lower).max(4);
327
328    let mut acc = unsafe { ListAccumulator::new(scope, initial_cap) };
329
330    for item in iter {
331        unsafe { acc.push(item) };
332    }
333
334    unsafe { acc.into_root() }
335}
336
337impl ListMut {
338    /// Wrap an existing `VECSXP` without additional checks.
339    ///
340    /// # Safety
341    ///
342    /// Caller must ensure `sexp` is a valid `VECSXP` and remains managed by R.
343    #[inline]
344    pub const unsafe fn from_raw(sexp: SEXP) -> Self {
345        ListMut(sexp)
346    }
347
348    /// Get the underlying `SEXP`.
349    #[inline]
350    pub const fn as_sexp(&self) -> SEXP {
351        self.0
352    }
353
354    /// Length of the list (number of elements).
355    #[inline]
356    pub fn len(&self) -> isize {
357        self.0.xlength()
358    }
359
360    /// Returns true if the list is empty.
361    #[inline]
362    pub fn is_empty(&self) -> bool {
363        self.len() == 0
364    }
365
366    /// Get raw SEXP element at 0-based index. Returns `None` if out of bounds.
367    #[inline]
368    pub fn get(&self, idx: isize) -> Option<SEXP> {
369        if idx < 0 || idx >= self.len() {
370            return None;
371        }
372        Some(self.0.vector_elt(idx))
373    }
374
375    /// Set raw SEXP element at 0-based index.
376    #[inline]
377    pub fn set(&mut self, idx: isize, value: SEXP) -> Result<(), SexpError> {
378        if idx < 0 || idx >= self.len() {
379            return Err(SexpError::InvalidValue("index out of bounds".into()));
380        }
381        self.0.set_vector_elt(idx, value);
382        Ok(())
383    }
384}
385// endregion