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