Skip to main content

r/interpreter/builtins/
collections.rs

1//! Rust `std::collections` exposed as R data structures.
2//!
3//! Each collection is stored on the `Interpreter` struct in a `Vec<CollectionObject>`.
4//! R sees a collection as an integer ID with a class attribute (e.g. `"hashmap"`,
5//! `"btreemap"`, `"hashset"`, `"heap"`, `"deque"`). Functions like `hashmap_set()`,
6//! `hashset_add()`, `heap_push()`, etc. mutate the collection in place through the ID.
7//!
8//! This gives R users O(1) hash lookups, ordered maps, priority queues, and deques —
9//! data structures that have no native equivalent in base R.
10
11use std::collections::{BTreeMap, BinaryHeap, HashMap, HashSet, VecDeque};
12
13use super::CallArgs;
14use crate::interpreter::value::*;
15use crate::interpreter::BuiltinContext;
16use crate::interpreter::Interpreter;
17use minir_macros::interpreter_builtin;
18
19// region: CollectionObject
20
21/// A single collection object stored on the interpreter.
22#[derive(Debug, Clone)]
23pub enum CollectionObject {
24    /// Unordered key-value store (`HashMap<String, RValue>`).
25    HashMap(HashMap<String, RValue>),
26    /// Ordered key-value store (`BTreeMap<String, RValue>`).
27    BTreeMap(BTreeMap<String, RValue>),
28    /// Unordered unique-element set (`HashSet<String>`).
29    HashSet(HashSet<String>),
30    /// Max-heap priority queue of `f64` values.
31    BinaryHeap(BinaryHeap<OrdF64>),
32    /// Double-ended queue of `RValue`.
33    VecDeque(VecDeque<RValue>),
34}
35
36/// Wrapper around `f64` that implements `Ord` for use in `BinaryHeap`.
37///
38/// Uses `total_cmp` so NaN values have a deterministic position rather than
39/// causing undefined ordering behavior.
40#[derive(Debug, Clone, Copy, PartialEq)]
41pub struct OrdF64(pub f64);
42
43impl Eq for OrdF64 {}
44
45impl PartialOrd for OrdF64 {
46    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
47        Some(self.cmp(other))
48    }
49}
50
51impl Ord for OrdF64 {
52    fn cmp(&self, other: &Self) -> std::cmp::Ordering {
53        self.0.total_cmp(&other.0)
54    }
55}
56
57// endregion
58
59// region: Interpreter collection helpers
60
61impl Interpreter {
62    /// Allocate a new collection, returning its integer ID.
63    pub(crate) fn add_collection(&self, obj: CollectionObject) -> usize {
64        let mut collections = self.collections.borrow_mut();
65        let id = collections.len();
66        collections.push(obj);
67        id
68    }
69}
70
71// endregion
72
73// region: Helpers
74
75/// Build an integer scalar with a class attribute representing a collection.
76fn collection_value(id: usize, class: &str) -> RValue {
77    let mut rv = RVector::from(Vector::Integer(
78        vec![Some(i64::try_from(id).unwrap_or(0))].into(),
79    ));
80    rv.set_attr(
81        "class".to_string(),
82        RValue::vec(Vector::Character(vec![Some(class.to_string())].into())),
83    );
84    RValue::Vector(rv)
85}
86
87/// Extract a collection ID from an RValue (integer scalar, possibly with a class attribute).
88fn collection_id(val: &RValue) -> Result<usize, RError> {
89    val.as_vector()
90        .and_then(|v| v.as_integer_scalar())
91        .and_then(|i| usize::try_from(i).ok())
92        .ok_or_else(|| {
93            RError::new(
94                RErrorKind::Argument,
95                "invalid collection handle — expected an integer ID returned by a collection constructor".to_string(),
96            )
97        })
98}
99
100/// Extract a string key from an argument at the given position.
101fn require_string(args: &CallArgs, name: &str, pos: usize) -> Result<String, RError> {
102    args.string(name, pos)
103}
104
105// endregion
106
107// region: HashMap builtins
108
109/// Create an empty HashMap (unordered key-value store).
110///
111/// Returns an integer ID with class "hashmap". Use `hashmap_set()`,
112/// `hashmap_get()`, etc. to manipulate it.
113///
114/// @return integer scalar with class "hashmap"
115#[interpreter_builtin(
116    name = "hashmap",
117    min_args = 0,
118    max_args = 0,
119    namespace = "collections"
120)]
121fn interp_hashmap(
122    _args: &[RValue],
123    _named: &[(String, RValue)],
124    context: &BuiltinContext,
125) -> Result<RValue, RError> {
126    let interp = context.interpreter();
127    let id = interp.add_collection(CollectionObject::HashMap(HashMap::new()));
128    Ok(collection_value(id, "hashmap"))
129}
130
131/// Insert or update a key-value pair in a HashMap.
132///
133/// @param h integer scalar: hashmap ID
134/// @param key character scalar: the key to set
135/// @param value any R value to store
136/// @return the previous value for the key, or NULL if the key was new
137#[interpreter_builtin(
138    name = "hashmap_set",
139    min_args = 3,
140    max_args = 3,
141    namespace = "collections"
142)]
143fn interp_hashmap_set(
144    args: &[RValue],
145    named: &[(String, RValue)],
146    context: &BuiltinContext,
147) -> Result<RValue, RError> {
148    let call_args = CallArgs::new(args, named);
149    let id = collection_id(call_args.value("h", 0).unwrap_or(&RValue::Null))?;
150    let key = require_string(&call_args, "key", 1)?;
151    let value = call_args.value("value", 2).cloned().unwrap_or(RValue::Null);
152
153    let interp = context.interpreter();
154    let mut collections = interp.collections.borrow_mut();
155    match collections.get_mut(id) {
156        Some(CollectionObject::HashMap(map)) => {
157            let old = map.insert(key, value);
158            Ok(old.unwrap_or(RValue::Null))
159        }
160        _ => Err(RError::new(
161            RErrorKind::Argument,
162            format!("collection {id} is not a hashmap"),
163        )),
164    }
165}
166
167/// Look up a key in a HashMap, with an optional default.
168///
169/// @param h integer scalar: hashmap ID
170/// @param key character scalar: the key to look up
171/// @param default value to return if the key is not found (default NULL)
172/// @return the stored value, or `default` if the key does not exist
173#[interpreter_builtin(
174    name = "hashmap_get",
175    min_args = 2,
176    max_args = 3,
177    namespace = "collections"
178)]
179fn interp_hashmap_get(
180    args: &[RValue],
181    named: &[(String, RValue)],
182    context: &BuiltinContext,
183) -> Result<RValue, RError> {
184    let call_args = CallArgs::new(args, named);
185    let id = collection_id(call_args.value("h", 0).unwrap_or(&RValue::Null))?;
186    let key = require_string(&call_args, "key", 1)?;
187    let default = call_args
188        .value("default", 2)
189        .cloned()
190        .unwrap_or(RValue::Null);
191
192    let interp = context.interpreter();
193    let collections = interp.collections.borrow();
194    match collections.get(id) {
195        Some(CollectionObject::HashMap(map)) => Ok(map.get(&key).cloned().unwrap_or(default)),
196        _ => Err(RError::new(
197            RErrorKind::Argument,
198            format!("collection {id} is not a hashmap"),
199        )),
200    }
201}
202
203/// Check whether a key exists in a HashMap.
204///
205/// @param h integer scalar: hashmap ID
206/// @param key character scalar: the key to check
207/// @return logical scalar: TRUE if the key exists, FALSE otherwise
208#[interpreter_builtin(
209    name = "hashmap_has",
210    min_args = 2,
211    max_args = 2,
212    namespace = "collections"
213)]
214fn interp_hashmap_has(
215    args: &[RValue],
216    named: &[(String, RValue)],
217    context: &BuiltinContext,
218) -> Result<RValue, RError> {
219    let call_args = CallArgs::new(args, named);
220    let id = collection_id(call_args.value("h", 0).unwrap_or(&RValue::Null))?;
221    let key = require_string(&call_args, "key", 1)?;
222
223    let interp = context.interpreter();
224    let collections = interp.collections.borrow();
225    match collections.get(id) {
226        Some(CollectionObject::HashMap(map)) => Ok(RValue::vec(Vector::Logical(
227            vec![Some(map.contains_key(&key))].into(),
228        ))),
229        _ => Err(RError::new(
230            RErrorKind::Argument,
231            format!("collection {id} is not a hashmap"),
232        )),
233    }
234}
235
236/// Remove a key from a HashMap, returning the old value.
237///
238/// @param h integer scalar: hashmap ID
239/// @param key character scalar: the key to remove
240/// @return the removed value, or NULL if the key did not exist
241#[interpreter_builtin(
242    name = "hashmap_remove",
243    min_args = 2,
244    max_args = 2,
245    namespace = "collections"
246)]
247fn interp_hashmap_remove(
248    args: &[RValue],
249    named: &[(String, RValue)],
250    context: &BuiltinContext,
251) -> Result<RValue, RError> {
252    let call_args = CallArgs::new(args, named);
253    let id = collection_id(call_args.value("h", 0).unwrap_or(&RValue::Null))?;
254    let key = require_string(&call_args, "key", 1)?;
255
256    let interp = context.interpreter();
257    let mut collections = interp.collections.borrow_mut();
258    match collections.get_mut(id) {
259        Some(CollectionObject::HashMap(map)) => Ok(map.remove(&key).unwrap_or(RValue::Null)),
260        _ => Err(RError::new(
261            RErrorKind::Argument,
262            format!("collection {id} is not a hashmap"),
263        )),
264    }
265}
266
267/// Return all keys of a HashMap as a character vector.
268///
269/// The order of keys is not guaranteed (HashMap is unordered).
270///
271/// @param h integer scalar: hashmap ID
272/// @return character vector of keys
273#[interpreter_builtin(
274    name = "hashmap_keys",
275    min_args = 1,
276    max_args = 1,
277    namespace = "collections"
278)]
279fn interp_hashmap_keys(
280    args: &[RValue],
281    named: &[(String, RValue)],
282    context: &BuiltinContext,
283) -> Result<RValue, RError> {
284    let call_args = CallArgs::new(args, named);
285    let id = collection_id(call_args.value("h", 0).unwrap_or(&RValue::Null))?;
286
287    let interp = context.interpreter();
288    let collections = interp.collections.borrow();
289    match collections.get(id) {
290        Some(CollectionObject::HashMap(map)) => {
291            let keys: Vec<Option<String>> = map.keys().map(|k| Some(k.clone())).collect();
292            Ok(RValue::vec(Vector::Character(keys.into())))
293        }
294        _ => Err(RError::new(
295            RErrorKind::Argument,
296            format!("collection {id} is not a hashmap"),
297        )),
298    }
299}
300
301/// Return all values of a HashMap as a list.
302///
303/// The order of values corresponds to the (unordered) key iteration order.
304///
305/// @param h integer scalar: hashmap ID
306/// @return list of values
307#[interpreter_builtin(
308    name = "hashmap_values",
309    min_args = 1,
310    max_args = 1,
311    namespace = "collections"
312)]
313fn interp_hashmap_values(
314    args: &[RValue],
315    named: &[(String, RValue)],
316    context: &BuiltinContext,
317) -> Result<RValue, RError> {
318    let call_args = CallArgs::new(args, named);
319    let id = collection_id(call_args.value("h", 0).unwrap_or(&RValue::Null))?;
320
321    let interp = context.interpreter();
322    let collections = interp.collections.borrow();
323    match collections.get(id) {
324        Some(CollectionObject::HashMap(map)) => {
325            let values: Vec<(Option<String>, RValue)> =
326                map.values().map(|v| (None, v.clone())).collect();
327            Ok(RValue::List(RList::new(values)))
328        }
329        _ => Err(RError::new(
330            RErrorKind::Argument,
331            format!("collection {id} is not a hashmap"),
332        )),
333    }
334}
335
336/// Return the number of key-value pairs in a HashMap.
337///
338/// @param h integer scalar: hashmap ID
339/// @return integer scalar: the number of entries
340#[interpreter_builtin(
341    name = "hashmap_size",
342    min_args = 1,
343    max_args = 1,
344    namespace = "collections"
345)]
346fn interp_hashmap_size(
347    args: &[RValue],
348    named: &[(String, RValue)],
349    context: &BuiltinContext,
350) -> Result<RValue, RError> {
351    let call_args = CallArgs::new(args, named);
352    let id = collection_id(call_args.value("h", 0).unwrap_or(&RValue::Null))?;
353
354    let interp = context.interpreter();
355    let collections = interp.collections.borrow();
356    match collections.get(id) {
357        Some(CollectionObject::HashMap(map)) => Ok(RValue::vec(Vector::Integer(
358            vec![Some(i64::try_from(map.len()).unwrap_or(i64::MAX))].into(),
359        ))),
360        _ => Err(RError::new(
361            RErrorKind::Argument,
362            format!("collection {id} is not a hashmap"),
363        )),
364    }
365}
366
367/// Convert a HashMap to a named R list.
368///
369/// Each key becomes a name in the list, each value becomes the corresponding element.
370///
371/// @param h integer scalar: hashmap ID
372/// @return named list
373#[interpreter_builtin(
374    name = "hashmap_to_list",
375    min_args = 1,
376    max_args = 1,
377    namespace = "collections"
378)]
379fn interp_hashmap_to_list(
380    args: &[RValue],
381    named: &[(String, RValue)],
382    context: &BuiltinContext,
383) -> Result<RValue, RError> {
384    let call_args = CallArgs::new(args, named);
385    let id = collection_id(call_args.value("h", 0).unwrap_or(&RValue::Null))?;
386
387    let interp = context.interpreter();
388    let collections = interp.collections.borrow();
389    match collections.get(id) {
390        Some(CollectionObject::HashMap(map)) => {
391            let entries: Vec<(Option<String>, RValue)> = map
392                .iter()
393                .map(|(k, v)| (Some(k.clone()), v.clone()))
394                .collect();
395            Ok(RValue::List(RList::new(entries)))
396        }
397        _ => Err(RError::new(
398            RErrorKind::Argument,
399            format!("collection {id} is not a hashmap"),
400        )),
401    }
402}
403
404// endregion
405
406// region: BTreeMap builtins
407
408/// Create an empty BTreeMap (ordered key-value store).
409///
410/// Keys are always maintained in sorted order. Returns an integer ID with
411/// class "btreemap".
412///
413/// @return integer scalar with class "btreemap"
414#[interpreter_builtin(
415    name = "btreemap",
416    min_args = 0,
417    max_args = 0,
418    namespace = "collections"
419)]
420fn interp_btreemap(
421    _args: &[RValue],
422    _named: &[(String, RValue)],
423    context: &BuiltinContext,
424) -> Result<RValue, RError> {
425    let interp = context.interpreter();
426    let id = interp.add_collection(CollectionObject::BTreeMap(BTreeMap::new()));
427    Ok(collection_value(id, "btreemap"))
428}
429
430/// Insert or update a key-value pair in a BTreeMap.
431///
432/// @param h integer scalar: btreemap ID
433/// @param key character scalar: the key to set
434/// @param value any R value to store
435/// @return the previous value for the key, or NULL if the key was new
436#[interpreter_builtin(
437    name = "btreemap_set",
438    min_args = 3,
439    max_args = 3,
440    namespace = "collections"
441)]
442fn interp_btreemap_set(
443    args: &[RValue],
444    named: &[(String, RValue)],
445    context: &BuiltinContext,
446) -> Result<RValue, RError> {
447    let call_args = CallArgs::new(args, named);
448    let id = collection_id(call_args.value("h", 0).unwrap_or(&RValue::Null))?;
449    let key = require_string(&call_args, "key", 1)?;
450    let value = call_args.value("value", 2).cloned().unwrap_or(RValue::Null);
451
452    let interp = context.interpreter();
453    let mut collections = interp.collections.borrow_mut();
454    match collections.get_mut(id) {
455        Some(CollectionObject::BTreeMap(map)) => {
456            let old = map.insert(key, value);
457            Ok(old.unwrap_or(RValue::Null))
458        }
459        _ => Err(RError::new(
460            RErrorKind::Argument,
461            format!("collection {id} is not a btreemap"),
462        )),
463    }
464}
465
466/// Look up a key in a BTreeMap, with an optional default.
467///
468/// @param h integer scalar: btreemap ID
469/// @param key character scalar: the key to look up
470/// @param default value to return if the key is not found (default NULL)
471/// @return the stored value, or `default` if the key does not exist
472#[interpreter_builtin(
473    name = "btreemap_get",
474    min_args = 2,
475    max_args = 3,
476    namespace = "collections"
477)]
478fn interp_btreemap_get(
479    args: &[RValue],
480    named: &[(String, RValue)],
481    context: &BuiltinContext,
482) -> Result<RValue, RError> {
483    let call_args = CallArgs::new(args, named);
484    let id = collection_id(call_args.value("h", 0).unwrap_or(&RValue::Null))?;
485    let key = require_string(&call_args, "key", 1)?;
486    let default = call_args
487        .value("default", 2)
488        .cloned()
489        .unwrap_or(RValue::Null);
490
491    let interp = context.interpreter();
492    let collections = interp.collections.borrow();
493    match collections.get(id) {
494        Some(CollectionObject::BTreeMap(map)) => Ok(map.get(&key).cloned().unwrap_or(default)),
495        _ => Err(RError::new(
496            RErrorKind::Argument,
497            format!("collection {id} is not a btreemap"),
498        )),
499    }
500}
501
502/// Check whether a key exists in a BTreeMap.
503///
504/// @param h integer scalar: btreemap ID
505/// @param key character scalar: the key to check
506/// @return logical scalar: TRUE if the key exists, FALSE otherwise
507#[interpreter_builtin(
508    name = "btreemap_has",
509    min_args = 2,
510    max_args = 2,
511    namespace = "collections"
512)]
513fn interp_btreemap_has(
514    args: &[RValue],
515    named: &[(String, RValue)],
516    context: &BuiltinContext,
517) -> Result<RValue, RError> {
518    let call_args = CallArgs::new(args, named);
519    let id = collection_id(call_args.value("h", 0).unwrap_or(&RValue::Null))?;
520    let key = require_string(&call_args, "key", 1)?;
521
522    let interp = context.interpreter();
523    let collections = interp.collections.borrow();
524    match collections.get(id) {
525        Some(CollectionObject::BTreeMap(map)) => Ok(RValue::vec(Vector::Logical(
526            vec![Some(map.contains_key(&key))].into(),
527        ))),
528        _ => Err(RError::new(
529            RErrorKind::Argument,
530            format!("collection {id} is not a btreemap"),
531        )),
532    }
533}
534
535/// Remove a key from a BTreeMap, returning the old value.
536///
537/// @param h integer scalar: btreemap ID
538/// @param key character scalar: the key to remove
539/// @return the removed value, or NULL if the key did not exist
540#[interpreter_builtin(
541    name = "btreemap_remove",
542    min_args = 2,
543    max_args = 2,
544    namespace = "collections"
545)]
546fn interp_btreemap_remove(
547    args: &[RValue],
548    named: &[(String, RValue)],
549    context: &BuiltinContext,
550) -> Result<RValue, RError> {
551    let call_args = CallArgs::new(args, named);
552    let id = collection_id(call_args.value("h", 0).unwrap_or(&RValue::Null))?;
553    let key = require_string(&call_args, "key", 1)?;
554
555    let interp = context.interpreter();
556    let mut collections = interp.collections.borrow_mut();
557    match collections.get_mut(id) {
558        Some(CollectionObject::BTreeMap(map)) => Ok(map.remove(&key).unwrap_or(RValue::Null)),
559        _ => Err(RError::new(
560            RErrorKind::Argument,
561            format!("collection {id} is not a btreemap"),
562        )),
563    }
564}
565
566/// Return all keys of a BTreeMap as a sorted character vector.
567///
568/// @param h integer scalar: btreemap ID
569/// @return character vector of keys in sorted order
570#[interpreter_builtin(
571    name = "btreemap_keys",
572    min_args = 1,
573    max_args = 1,
574    namespace = "collections"
575)]
576fn interp_btreemap_keys(
577    args: &[RValue],
578    named: &[(String, RValue)],
579    context: &BuiltinContext,
580) -> Result<RValue, RError> {
581    let call_args = CallArgs::new(args, named);
582    let id = collection_id(call_args.value("h", 0).unwrap_or(&RValue::Null))?;
583
584    let interp = context.interpreter();
585    let collections = interp.collections.borrow();
586    match collections.get(id) {
587        Some(CollectionObject::BTreeMap(map)) => {
588            let keys: Vec<Option<String>> = map.keys().map(|k| Some(k.clone())).collect();
589            Ok(RValue::vec(Vector::Character(keys.into())))
590        }
591        _ => Err(RError::new(
592            RErrorKind::Argument,
593            format!("collection {id} is not a btreemap"),
594        )),
595    }
596}
597
598/// Return all values of a BTreeMap as a list (in key-sorted order).
599///
600/// @param h integer scalar: btreemap ID
601/// @return list of values in key-sorted order
602#[interpreter_builtin(
603    name = "btreemap_values",
604    min_args = 1,
605    max_args = 1,
606    namespace = "collections"
607)]
608fn interp_btreemap_values(
609    args: &[RValue],
610    named: &[(String, RValue)],
611    context: &BuiltinContext,
612) -> Result<RValue, RError> {
613    let call_args = CallArgs::new(args, named);
614    let id = collection_id(call_args.value("h", 0).unwrap_or(&RValue::Null))?;
615
616    let interp = context.interpreter();
617    let collections = interp.collections.borrow();
618    match collections.get(id) {
619        Some(CollectionObject::BTreeMap(map)) => {
620            let values: Vec<(Option<String>, RValue)> =
621                map.values().map(|v| (None, v.clone())).collect();
622            Ok(RValue::List(RList::new(values)))
623        }
624        _ => Err(RError::new(
625            RErrorKind::Argument,
626            format!("collection {id} is not a btreemap"),
627        )),
628    }
629}
630
631/// Return the number of key-value pairs in a BTreeMap.
632///
633/// @param h integer scalar: btreemap ID
634/// @return integer scalar: the number of entries
635#[interpreter_builtin(
636    name = "btreemap_size",
637    min_args = 1,
638    max_args = 1,
639    namespace = "collections"
640)]
641fn interp_btreemap_size(
642    args: &[RValue],
643    named: &[(String, RValue)],
644    context: &BuiltinContext,
645) -> Result<RValue, RError> {
646    let call_args = CallArgs::new(args, named);
647    let id = collection_id(call_args.value("h", 0).unwrap_or(&RValue::Null))?;
648
649    let interp = context.interpreter();
650    let collections = interp.collections.borrow();
651    match collections.get(id) {
652        Some(CollectionObject::BTreeMap(map)) => Ok(RValue::vec(Vector::Integer(
653            vec![Some(i64::try_from(map.len()).unwrap_or(i64::MAX))].into(),
654        ))),
655        _ => Err(RError::new(
656            RErrorKind::Argument,
657            format!("collection {id} is not a btreemap"),
658        )),
659    }
660}
661
662/// Convert a BTreeMap to a named R list (in key-sorted order).
663///
664/// @param h integer scalar: btreemap ID
665/// @return named list with keys in sorted order
666#[interpreter_builtin(
667    name = "btreemap_to_list",
668    min_args = 1,
669    max_args = 1,
670    namespace = "collections"
671)]
672fn interp_btreemap_to_list(
673    args: &[RValue],
674    named: &[(String, RValue)],
675    context: &BuiltinContext,
676) -> Result<RValue, RError> {
677    let call_args = CallArgs::new(args, named);
678    let id = collection_id(call_args.value("h", 0).unwrap_or(&RValue::Null))?;
679
680    let interp = context.interpreter();
681    let collections = interp.collections.borrow();
682    match collections.get(id) {
683        Some(CollectionObject::BTreeMap(map)) => {
684            let entries: Vec<(Option<String>, RValue)> = map
685                .iter()
686                .map(|(k, v)| (Some(k.clone()), v.clone()))
687                .collect();
688            Ok(RValue::List(RList::new(entries)))
689        }
690        _ => Err(RError::new(
691            RErrorKind::Argument,
692            format!("collection {id} is not a btreemap"),
693        )),
694    }
695}
696
697// endregion
698
699// region: HashSet builtins
700
701/// Create an empty HashSet (unordered unique-element set).
702///
703/// Returns an integer ID with class "hashset". Use `hashset_add()`,
704/// `hashset_has()`, etc. to manipulate it.
705///
706/// @return integer scalar with class "hashset"
707#[interpreter_builtin(
708    name = "hashset",
709    min_args = 0,
710    max_args = 0,
711    namespace = "collections"
712)]
713fn interp_hashset(
714    _args: &[RValue],
715    _named: &[(String, RValue)],
716    context: &BuiltinContext,
717) -> Result<RValue, RError> {
718    let interp = context.interpreter();
719    let id = interp.add_collection(CollectionObject::HashSet(HashSet::new()));
720    Ok(collection_value(id, "hashset"))
721}
722
723/// Add a string element to a HashSet.
724///
725/// @param s integer scalar: hashset ID
726/// @param value character scalar: the element to add
727/// @return logical scalar: TRUE if the element was new, FALSE if already present
728#[interpreter_builtin(
729    name = "hashset_add",
730    min_args = 2,
731    max_args = 2,
732    namespace = "collections"
733)]
734fn interp_hashset_add(
735    args: &[RValue],
736    named: &[(String, RValue)],
737    context: &BuiltinContext,
738) -> Result<RValue, RError> {
739    let call_args = CallArgs::new(args, named);
740    let id = collection_id(call_args.value("s", 0).unwrap_or(&RValue::Null))?;
741    let value = require_string(&call_args, "value", 1)?;
742
743    let interp = context.interpreter();
744    let mut collections = interp.collections.borrow_mut();
745    match collections.get_mut(id) {
746        Some(CollectionObject::HashSet(set)) => {
747            let was_new = set.insert(value);
748            Ok(RValue::vec(Vector::Logical(vec![Some(was_new)].into())))
749        }
750        _ => Err(RError::new(
751            RErrorKind::Argument,
752            format!("collection {id} is not a hashset"),
753        )),
754    }
755}
756
757/// Check whether a string element is in a HashSet.
758///
759/// @param s integer scalar: hashset ID
760/// @param value character scalar: the element to check
761/// @return logical scalar: TRUE if present, FALSE otherwise
762#[interpreter_builtin(
763    name = "hashset_has",
764    min_args = 2,
765    max_args = 2,
766    namespace = "collections"
767)]
768fn interp_hashset_has(
769    args: &[RValue],
770    named: &[(String, RValue)],
771    context: &BuiltinContext,
772) -> Result<RValue, RError> {
773    let call_args = CallArgs::new(args, named);
774    let id = collection_id(call_args.value("s", 0).unwrap_or(&RValue::Null))?;
775    let value = require_string(&call_args, "value", 1)?;
776
777    let interp = context.interpreter();
778    let collections = interp.collections.borrow();
779    match collections.get(id) {
780        Some(CollectionObject::HashSet(set)) => Ok(RValue::vec(Vector::Logical(
781            vec![Some(set.contains(&value))].into(),
782        ))),
783        _ => Err(RError::new(
784            RErrorKind::Argument,
785            format!("collection {id} is not a hashset"),
786        )),
787    }
788}
789
790/// Remove a string element from a HashSet.
791///
792/// @param s integer scalar: hashset ID
793/// @param value character scalar: the element to remove
794/// @return logical scalar: TRUE if the element was present and removed, FALSE otherwise
795#[interpreter_builtin(
796    name = "hashset_remove",
797    min_args = 2,
798    max_args = 2,
799    namespace = "collections"
800)]
801fn interp_hashset_remove(
802    args: &[RValue],
803    named: &[(String, RValue)],
804    context: &BuiltinContext,
805) -> Result<RValue, RError> {
806    let call_args = CallArgs::new(args, named);
807    let id = collection_id(call_args.value("s", 0).unwrap_or(&RValue::Null))?;
808    let value = require_string(&call_args, "value", 1)?;
809
810    let interp = context.interpreter();
811    let mut collections = interp.collections.borrow_mut();
812    match collections.get_mut(id) {
813        Some(CollectionObject::HashSet(set)) => {
814            let was_present = set.remove(&value);
815            Ok(RValue::vec(Vector::Logical(vec![Some(was_present)].into())))
816        }
817        _ => Err(RError::new(
818            RErrorKind::Argument,
819            format!("collection {id} is not a hashset"),
820        )),
821    }
822}
823
824/// Return the number of elements in a HashSet.
825///
826/// @param s integer scalar: hashset ID
827/// @return integer scalar: the number of elements
828#[interpreter_builtin(
829    name = "hashset_size",
830    min_args = 1,
831    max_args = 1,
832    namespace = "collections"
833)]
834fn interp_hashset_size(
835    args: &[RValue],
836    named: &[(String, RValue)],
837    context: &BuiltinContext,
838) -> Result<RValue, RError> {
839    let call_args = CallArgs::new(args, named);
840    let id = collection_id(call_args.value("s", 0).unwrap_or(&RValue::Null))?;
841
842    let interp = context.interpreter();
843    let collections = interp.collections.borrow();
844    match collections.get(id) {
845        Some(CollectionObject::HashSet(set)) => Ok(RValue::vec(Vector::Integer(
846            vec![Some(i64::try_from(set.len()).unwrap_or(i64::MAX))].into(),
847        ))),
848        _ => Err(RError::new(
849            RErrorKind::Argument,
850            format!("collection {id} is not a hashset"),
851        )),
852    }
853}
854
855/// Convert a HashSet to a character vector.
856///
857/// The order of elements is not guaranteed (HashSet is unordered).
858///
859/// @param s integer scalar: hashset ID
860/// @return character vector of the set's elements
861#[interpreter_builtin(
862    name = "hashset_to_vector",
863    min_args = 1,
864    max_args = 1,
865    namespace = "collections"
866)]
867fn interp_hashset_to_vector(
868    args: &[RValue],
869    named: &[(String, RValue)],
870    context: &BuiltinContext,
871) -> Result<RValue, RError> {
872    let call_args = CallArgs::new(args, named);
873    let id = collection_id(call_args.value("s", 0).unwrap_or(&RValue::Null))?;
874
875    let interp = context.interpreter();
876    let collections = interp.collections.borrow();
877    match collections.get(id) {
878        Some(CollectionObject::HashSet(set)) => {
879            let elems: Vec<Option<String>> = set.iter().map(|e| Some(e.clone())).collect();
880            Ok(RValue::vec(Vector::Character(elems.into())))
881        }
882        _ => Err(RError::new(
883            RErrorKind::Argument,
884            format!("collection {id} is not a hashset"),
885        )),
886    }
887}
888
889/// Compute the union of two HashSets, returning a new HashSet.
890///
891/// @param s1 integer scalar: first hashset ID
892/// @param s2 integer scalar: second hashset ID
893/// @return integer scalar with class "hashset": the union
894#[interpreter_builtin(
895    name = "hashset_union",
896    min_args = 2,
897    max_args = 2,
898    namespace = "collections"
899)]
900fn interp_hashset_union(
901    args: &[RValue],
902    named: &[(String, RValue)],
903    context: &BuiltinContext,
904) -> Result<RValue, RError> {
905    let call_args = CallArgs::new(args, named);
906    let id1 = collection_id(call_args.value("s1", 0).unwrap_or(&RValue::Null))?;
907    let id2 = collection_id(call_args.value("s2", 1).unwrap_or(&RValue::Null))?;
908
909    let interp = context.interpreter();
910    let collections = interp.collections.borrow();
911    let set1 = match collections.get(id1) {
912        Some(CollectionObject::HashSet(s)) => s,
913        _ => {
914            return Err(RError::new(
915                RErrorKind::Argument,
916                format!("collection {id1} is not a hashset"),
917            ))
918        }
919    };
920    let set2 = match collections.get(id2) {
921        Some(CollectionObject::HashSet(s)) => s,
922        _ => {
923            return Err(RError::new(
924                RErrorKind::Argument,
925                format!("collection {id2} is not a hashset"),
926            ))
927        }
928    };
929    let result: HashSet<String> = set1.union(set2).cloned().collect();
930    drop(collections);
931    let id = interp.add_collection(CollectionObject::HashSet(result));
932    Ok(collection_value(id, "hashset"))
933}
934
935/// Compute the intersection of two HashSets, returning a new HashSet.
936///
937/// @param s1 integer scalar: first hashset ID
938/// @param s2 integer scalar: second hashset ID
939/// @return integer scalar with class "hashset": the intersection
940#[interpreter_builtin(
941    name = "hashset_intersect",
942    min_args = 2,
943    max_args = 2,
944    namespace = "collections"
945)]
946fn interp_hashset_intersect(
947    args: &[RValue],
948    named: &[(String, RValue)],
949    context: &BuiltinContext,
950) -> Result<RValue, RError> {
951    let call_args = CallArgs::new(args, named);
952    let id1 = collection_id(call_args.value("s1", 0).unwrap_or(&RValue::Null))?;
953    let id2 = collection_id(call_args.value("s2", 1).unwrap_or(&RValue::Null))?;
954
955    let interp = context.interpreter();
956    let collections = interp.collections.borrow();
957    let set1 = match collections.get(id1) {
958        Some(CollectionObject::HashSet(s)) => s,
959        _ => {
960            return Err(RError::new(
961                RErrorKind::Argument,
962                format!("collection {id1} is not a hashset"),
963            ))
964        }
965    };
966    let set2 = match collections.get(id2) {
967        Some(CollectionObject::HashSet(s)) => s,
968        _ => {
969            return Err(RError::new(
970                RErrorKind::Argument,
971                format!("collection {id2} is not a hashset"),
972            ))
973        }
974    };
975    let result: HashSet<String> = set1.intersection(set2).cloned().collect();
976    drop(collections);
977    let id = interp.add_collection(CollectionObject::HashSet(result));
978    Ok(collection_value(id, "hashset"))
979}
980
981/// Compute the difference of two HashSets (s1 minus s2), returning a new HashSet.
982///
983/// @param s1 integer scalar: first hashset ID
984/// @param s2 integer scalar: second hashset ID
985/// @return integer scalar with class "hashset": elements in s1 but not in s2
986#[interpreter_builtin(
987    name = "hashset_diff",
988    min_args = 2,
989    max_args = 2,
990    namespace = "collections"
991)]
992fn interp_hashset_diff(
993    args: &[RValue],
994    named: &[(String, RValue)],
995    context: &BuiltinContext,
996) -> Result<RValue, RError> {
997    let call_args = CallArgs::new(args, named);
998    let id1 = collection_id(call_args.value("s1", 0).unwrap_or(&RValue::Null))?;
999    let id2 = collection_id(call_args.value("s2", 1).unwrap_or(&RValue::Null))?;
1000
1001    let interp = context.interpreter();
1002    let collections = interp.collections.borrow();
1003    let set1 = match collections.get(id1) {
1004        Some(CollectionObject::HashSet(s)) => s,
1005        _ => {
1006            return Err(RError::new(
1007                RErrorKind::Argument,
1008                format!("collection {id1} is not a hashset"),
1009            ))
1010        }
1011    };
1012    let set2 = match collections.get(id2) {
1013        Some(CollectionObject::HashSet(s)) => s,
1014        _ => {
1015            return Err(RError::new(
1016                RErrorKind::Argument,
1017                format!("collection {id2} is not a hashset"),
1018            ))
1019        }
1020    };
1021    let result: HashSet<String> = set1.difference(set2).cloned().collect();
1022    drop(collections);
1023    let id = interp.add_collection(CollectionObject::HashSet(result));
1024    Ok(collection_value(id, "hashset"))
1025}
1026
1027// endregion
1028
1029// region: BinaryHeap builtins
1030
1031/// Create an empty max-heap (priority queue of numeric values).
1032///
1033/// Returns an integer ID with class "heap". Use `heap_push()`, `heap_pop()`,
1034/// etc. to manipulate it. The largest value is always at the top.
1035///
1036/// @return integer scalar with class "heap"
1037#[interpreter_builtin(name = "heap", min_args = 0, max_args = 0, namespace = "collections")]
1038fn interp_heap(
1039    _args: &[RValue],
1040    _named: &[(String, RValue)],
1041    context: &BuiltinContext,
1042) -> Result<RValue, RError> {
1043    let interp = context.interpreter();
1044    let id = interp.add_collection(CollectionObject::BinaryHeap(BinaryHeap::new()));
1045    Ok(collection_value(id, "heap"))
1046}
1047
1048/// Push a numeric value onto a max-heap.
1049///
1050/// @param h integer scalar: heap ID
1051/// @param value numeric scalar: the value to push
1052/// @return NULL (invisibly)
1053#[interpreter_builtin(
1054    name = "heap_push",
1055    min_args = 2,
1056    max_args = 2,
1057    namespace = "collections"
1058)]
1059fn interp_heap_push(
1060    args: &[RValue],
1061    named: &[(String, RValue)],
1062    context: &BuiltinContext,
1063) -> Result<RValue, RError> {
1064    let call_args = CallArgs::new(args, named);
1065    let id = collection_id(call_args.value("h", 0).unwrap_or(&RValue::Null))?;
1066    let val = call_args
1067        .value("value", 1)
1068        .and_then(|v| v.as_vector())
1069        .and_then(|v| v.as_double_scalar())
1070        .ok_or_else(|| {
1071            RError::new(
1072                RErrorKind::Argument,
1073                "heap_push() requires a numeric scalar value".to_string(),
1074            )
1075        })?;
1076
1077    let interp = context.interpreter();
1078    let mut collections = interp.collections.borrow_mut();
1079    match collections.get_mut(id) {
1080        Some(CollectionObject::BinaryHeap(heap)) => {
1081            heap.push(OrdF64(val));
1082            Ok(RValue::Null)
1083        }
1084        _ => Err(RError::new(
1085            RErrorKind::Argument,
1086            format!("collection {id} is not a heap"),
1087        )),
1088    }
1089}
1090
1091/// Pop and return the maximum value from a max-heap.
1092///
1093/// Returns NULL if the heap is empty.
1094///
1095/// @param h integer scalar: heap ID
1096/// @return numeric scalar (the max value), or NULL if empty
1097#[interpreter_builtin(
1098    name = "heap_pop",
1099    min_args = 1,
1100    max_args = 1,
1101    namespace = "collections"
1102)]
1103fn interp_heap_pop(
1104    args: &[RValue],
1105    named: &[(String, RValue)],
1106    context: &BuiltinContext,
1107) -> Result<RValue, RError> {
1108    let call_args = CallArgs::new(args, named);
1109    let id = collection_id(call_args.value("h", 0).unwrap_or(&RValue::Null))?;
1110
1111    let interp = context.interpreter();
1112    let mut collections = interp.collections.borrow_mut();
1113    match collections.get_mut(id) {
1114        Some(CollectionObject::BinaryHeap(heap)) => match heap.pop() {
1115            Some(OrdF64(val)) => Ok(RValue::vec(Vector::Double(vec![Some(val)].into()))),
1116            None => Ok(RValue::Null),
1117        },
1118        _ => Err(RError::new(
1119            RErrorKind::Argument,
1120            format!("collection {id} is not a heap"),
1121        )),
1122    }
1123}
1124
1125/// Peek at the maximum value in a max-heap without removing it.
1126///
1127/// Returns NULL if the heap is empty.
1128///
1129/// @param h integer scalar: heap ID
1130/// @return numeric scalar (the max value), or NULL if empty
1131#[interpreter_builtin(
1132    name = "heap_peek",
1133    min_args = 1,
1134    max_args = 1,
1135    namespace = "collections"
1136)]
1137fn interp_heap_peek(
1138    args: &[RValue],
1139    named: &[(String, RValue)],
1140    context: &BuiltinContext,
1141) -> Result<RValue, RError> {
1142    let call_args = CallArgs::new(args, named);
1143    let id = collection_id(call_args.value("h", 0).unwrap_or(&RValue::Null))?;
1144
1145    let interp = context.interpreter();
1146    let collections = interp.collections.borrow();
1147    match collections.get(id) {
1148        Some(CollectionObject::BinaryHeap(heap)) => match heap.peek() {
1149            Some(OrdF64(val)) => Ok(RValue::vec(Vector::Double(vec![Some(*val)].into()))),
1150            None => Ok(RValue::Null),
1151        },
1152        _ => Err(RError::new(
1153            RErrorKind::Argument,
1154            format!("collection {id} is not a heap"),
1155        )),
1156    }
1157}
1158
1159/// Return the number of elements in a max-heap.
1160///
1161/// @param h integer scalar: heap ID
1162/// @return integer scalar: the number of elements
1163#[interpreter_builtin(
1164    name = "heap_size",
1165    min_args = 1,
1166    max_args = 1,
1167    namespace = "collections"
1168)]
1169fn interp_heap_size(
1170    args: &[RValue],
1171    named: &[(String, RValue)],
1172    context: &BuiltinContext,
1173) -> Result<RValue, RError> {
1174    let call_args = CallArgs::new(args, named);
1175    let id = collection_id(call_args.value("h", 0).unwrap_or(&RValue::Null))?;
1176
1177    let interp = context.interpreter();
1178    let collections = interp.collections.borrow();
1179    match collections.get(id) {
1180        Some(CollectionObject::BinaryHeap(heap)) => Ok(RValue::vec(Vector::Integer(
1181            vec![Some(i64::try_from(heap.len()).unwrap_or(i64::MAX))].into(),
1182        ))),
1183        _ => Err(RError::new(
1184            RErrorKind::Argument,
1185            format!("collection {id} is not a heap"),
1186        )),
1187    }
1188}
1189
1190// endregion
1191
1192// region: VecDeque builtins
1193
1194/// Create an empty deque (double-ended queue of R values).
1195///
1196/// Returns an integer ID with class "deque". Use `deque_push_back()`,
1197/// `deque_push_front()`, `deque_pop_back()`, `deque_pop_front()` to manipulate it.
1198///
1199/// @return integer scalar with class "deque"
1200#[interpreter_builtin(name = "deque", min_args = 0, max_args = 0, namespace = "collections")]
1201fn interp_deque(
1202    _args: &[RValue],
1203    _named: &[(String, RValue)],
1204    context: &BuiltinContext,
1205) -> Result<RValue, RError> {
1206    let interp = context.interpreter();
1207    let id = interp.add_collection(CollectionObject::VecDeque(VecDeque::new()));
1208    Ok(collection_value(id, "deque"))
1209}
1210
1211/// Append a value to the back of a deque.
1212///
1213/// @param d integer scalar: deque ID
1214/// @param value any R value to append
1215/// @return NULL (invisibly)
1216#[interpreter_builtin(
1217    name = "deque_push_back",
1218    min_args = 2,
1219    max_args = 2,
1220    namespace = "collections"
1221)]
1222fn interp_deque_push_back(
1223    args: &[RValue],
1224    named: &[(String, RValue)],
1225    context: &BuiltinContext,
1226) -> Result<RValue, RError> {
1227    let call_args = CallArgs::new(args, named);
1228    let id = collection_id(call_args.value("d", 0).unwrap_or(&RValue::Null))?;
1229    let value = call_args.value("value", 1).cloned().unwrap_or(RValue::Null);
1230
1231    let interp = context.interpreter();
1232    let mut collections = interp.collections.borrow_mut();
1233    match collections.get_mut(id) {
1234        Some(CollectionObject::VecDeque(deque)) => {
1235            deque.push_back(value);
1236            Ok(RValue::Null)
1237        }
1238        _ => Err(RError::new(
1239            RErrorKind::Argument,
1240            format!("collection {id} is not a deque"),
1241        )),
1242    }
1243}
1244
1245/// Prepend a value to the front of a deque.
1246///
1247/// @param d integer scalar: deque ID
1248/// @param value any R value to prepend
1249/// @return NULL (invisibly)
1250#[interpreter_builtin(
1251    name = "deque_push_front",
1252    min_args = 2,
1253    max_args = 2,
1254    namespace = "collections"
1255)]
1256fn interp_deque_push_front(
1257    args: &[RValue],
1258    named: &[(String, RValue)],
1259    context: &BuiltinContext,
1260) -> Result<RValue, RError> {
1261    let call_args = CallArgs::new(args, named);
1262    let id = collection_id(call_args.value("d", 0).unwrap_or(&RValue::Null))?;
1263    let value = call_args.value("value", 1).cloned().unwrap_or(RValue::Null);
1264
1265    let interp = context.interpreter();
1266    let mut collections = interp.collections.borrow_mut();
1267    match collections.get_mut(id) {
1268        Some(CollectionObject::VecDeque(deque)) => {
1269            deque.push_front(value);
1270            Ok(RValue::Null)
1271        }
1272        _ => Err(RError::new(
1273            RErrorKind::Argument,
1274            format!("collection {id} is not a deque"),
1275        )),
1276    }
1277}
1278
1279/// Remove and return the last element of a deque.
1280///
1281/// Returns NULL if the deque is empty.
1282///
1283/// @param d integer scalar: deque ID
1284/// @return the removed value, or NULL if empty
1285#[interpreter_builtin(
1286    name = "deque_pop_back",
1287    min_args = 1,
1288    max_args = 1,
1289    namespace = "collections"
1290)]
1291fn interp_deque_pop_back(
1292    args: &[RValue],
1293    named: &[(String, RValue)],
1294    context: &BuiltinContext,
1295) -> Result<RValue, RError> {
1296    let call_args = CallArgs::new(args, named);
1297    let id = collection_id(call_args.value("d", 0).unwrap_or(&RValue::Null))?;
1298
1299    let interp = context.interpreter();
1300    let mut collections = interp.collections.borrow_mut();
1301    match collections.get_mut(id) {
1302        Some(CollectionObject::VecDeque(deque)) => Ok(deque.pop_back().unwrap_or(RValue::Null)),
1303        _ => Err(RError::new(
1304            RErrorKind::Argument,
1305            format!("collection {id} is not a deque"),
1306        )),
1307    }
1308}
1309
1310/// Remove and return the first element of a deque.
1311///
1312/// Returns NULL if the deque is empty.
1313///
1314/// @param d integer scalar: deque ID
1315/// @return the removed value, or NULL if empty
1316#[interpreter_builtin(
1317    name = "deque_pop_front",
1318    min_args = 1,
1319    max_args = 1,
1320    namespace = "collections"
1321)]
1322fn interp_deque_pop_front(
1323    args: &[RValue],
1324    named: &[(String, RValue)],
1325    context: &BuiltinContext,
1326) -> Result<RValue, RError> {
1327    let call_args = CallArgs::new(args, named);
1328    let id = collection_id(call_args.value("d", 0).unwrap_or(&RValue::Null))?;
1329
1330    let interp = context.interpreter();
1331    let mut collections = interp.collections.borrow_mut();
1332    match collections.get_mut(id) {
1333        Some(CollectionObject::VecDeque(deque)) => Ok(deque.pop_front().unwrap_or(RValue::Null)),
1334        _ => Err(RError::new(
1335            RErrorKind::Argument,
1336            format!("collection {id} is not a deque"),
1337        )),
1338    }
1339}
1340
1341/// Return the number of elements in a deque.
1342///
1343/// @param d integer scalar: deque ID
1344/// @return integer scalar: the number of elements
1345#[interpreter_builtin(
1346    name = "deque_size",
1347    min_args = 1,
1348    max_args = 1,
1349    namespace = "collections"
1350)]
1351fn interp_deque_size(
1352    args: &[RValue],
1353    named: &[(String, RValue)],
1354    context: &BuiltinContext,
1355) -> Result<RValue, RError> {
1356    let call_args = CallArgs::new(args, named);
1357    let id = collection_id(call_args.value("d", 0).unwrap_or(&RValue::Null))?;
1358
1359    let interp = context.interpreter();
1360    let collections = interp.collections.borrow();
1361    match collections.get(id) {
1362        Some(CollectionObject::VecDeque(deque)) => Ok(RValue::vec(Vector::Integer(
1363            vec![Some(i64::try_from(deque.len()).unwrap_or(i64::MAX))].into(),
1364        ))),
1365        _ => Err(RError::new(
1366            RErrorKind::Argument,
1367            format!("collection {id} is not a deque"),
1368        )),
1369    }
1370}
1371
1372/// Convert a deque to an R list.
1373///
1374/// @param d integer scalar: deque ID
1375/// @return list of the deque's elements (front to back)
1376#[interpreter_builtin(
1377    name = "deque_to_list",
1378    min_args = 1,
1379    max_args = 1,
1380    namespace = "collections"
1381)]
1382fn interp_deque_to_list(
1383    args: &[RValue],
1384    named: &[(String, RValue)],
1385    context: &BuiltinContext,
1386) -> Result<RValue, RError> {
1387    let call_args = CallArgs::new(args, named);
1388    let id = collection_id(call_args.value("d", 0).unwrap_or(&RValue::Null))?;
1389
1390    let interp = context.interpreter();
1391    let collections = interp.collections.borrow();
1392    match collections.get(id) {
1393        Some(CollectionObject::VecDeque(deque)) => {
1394            let values: Vec<(Option<String>, RValue)> =
1395                deque.iter().map(|v| (None, v.clone())).collect();
1396            Ok(RValue::List(RList::new(values)))
1397        }
1398        _ => Err(RError::new(
1399            RErrorKind::Argument,
1400            format!("collection {id} is not a deque"),
1401        )),
1402    }
1403}
1404
1405// endregion