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