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