Skip to main content

otter/types/terms/
map.rs

1//! [`Map`] — immutable Erlang maps: lookup, functional update (each mutation
2//! returns a new map), and key-value [iteration](MapIterator).
3
4use core::marker::PhantomData;
5
6use crate::types::sealed::Sealed;
7use crate::types::{AnyEnv, AnyTerm, Env, Invariant, RawTerm, Term};
8
9/// An Erlang map. Immutable — all mutations return a new map.
10#[derive(Clone, Copy)]
11pub struct Map<'id> {
12    raw_term: RawTerm,
13    _id: Invariant<'id>,
14}
15
16impl<'id> Map<'id> {
17    #[crate::raw]
18    pub(crate) fn from_raw(raw_term: RawTerm) -> Self {
19        Self { raw_term, _id: PhantomData }
20    }
21
22    /// Create an empty map (`enif_make_new_map`).
23    pub fn new(env: impl Env<'id>) -> Map<'id> {
24        let raw_term = unsafe { enif_ffi::make_new_map(env.raw_env()) };
25        Map { raw_term, _id: PhantomData }
26    }
27
28    /// Number of key-value pairs (`enif_get_map_size`).
29    pub fn size(self, env: impl Env<'id>) -> usize {
30        let mut size: usize = 0;
31        // get_map_size fails only on a non-map; a Map is always a validated map
32        // term, so this cannot fail (and 0 is a real size — never fake it).
33        let ok = unsafe { enif_ffi::get_map_size(env.raw_env(), self.raw_term, &mut size) };
34        assert!(ok != 0, "enif_get_map_size failed on a validated Map");
35        size
36    }
37
38    /// Look up `key` (`enif_get_map_value`). `None` if absent.
39    pub fn get(self, env: impl Env<'id>, key: impl Term<'id>) -> Option<AnyTerm<'id>> {
40        let mut value: RawTerm = 0;
41        (unsafe {
42            enif_ffi::get_map_value(env.raw_env(), self.raw_term, key.raw_term(), &mut value)
43        } != 0)
44            .then(|| AnyTerm::wrap(value, env))
45    }
46
47    /// Return a new map with `key` set to `value` (`enif_make_map_put`, insert
48    /// or replace).
49    pub fn put(self, env: impl Env<'id>, key: impl Term<'id>, value: impl Term<'id>) -> Map<'id> {
50        let mut out: RawTerm = 0;
51        let ok = unsafe {
52            enif_ffi::make_map_put(env.raw_env(), self.raw_term, key.raw_term(), value.raw_term(), &mut out)
53        };
54        assert!(ok != 0, "make_map_put on a valid map failed");
55        Map { raw_term: out, _id: PhantomData }
56    }
57
58    /// Return a new map with `key` updated to `value` (`enif_make_map_update`).
59    /// `None` if the key is absent.
60    pub fn update(
61        self,
62        env: impl Env<'id>,
63        key: impl Term<'id>,
64        value: impl Term<'id>,
65    ) -> Option<Map<'id>> {
66        let mut out: RawTerm = 0;
67        (unsafe {
68            enif_ffi::make_map_update(env.raw_env(), self.raw_term, key.raw_term(), value.raw_term(), &mut out)
69        } != 0)
70            .then_some(Map { raw_term: out, _id: PhantomData })
71    }
72
73    /// Return a new map with `key` removed (`enif_make_map_remove`). Removing a
74    /// key the map does not contain returns it unchanged.
75    pub fn remove(self, env: impl Env<'id>, key: impl Term<'id>) -> Map<'id> {
76        let mut out: RawTerm = 0;
77        // make_map_remove fails only on a non-map (an absent key yields the map
78        // unchanged, not a failure), so this cannot fail on a validated Map.
79        let ok = unsafe {
80            enif_ffi::make_map_remove(env.raw_env(), self.raw_term, key.raw_term(), &mut out)
81        };
82        assert!(ok != 0, "make_map_remove on a valid map failed");
83        Map { raw_term: out, _id: PhantomData }
84    }
85
86    /// Returns `true` if `term` is a map (`enif_is_map`).
87    pub fn is_map(env: impl Env<'id>, term: impl Term<'id>) -> bool {
88        unsafe { enif_ffi::is_map(env.raw_env(), term.raw_term()) != 0 }
89    }
90
91    /// Iterate `(key, value)` pairs in unspecified order.
92    pub fn iter(self, env: impl Env<'id>) -> MapIterator<'id> {
93        let mut iter: Box<enif_ffi::MapIterator> = Box::new(unsafe { std::mem::zeroed() });
94        // create fails only on a non-map; on a validated Map it cannot fail.
95        // Proceeding on a failed create would leave the iterator zeroed and the
96        // first get_pair would read an uninitialized cursor.
97        let ok = unsafe {
98            enif_ffi::map_iterator_create(
99                env.raw_env(),
100                self.raw_term,
101                &mut *iter,
102                enif_ffi::MapIteratorEntry::First,
103            )
104        };
105        assert!(ok != 0, "enif_map_iterator_create failed on a validated Map");
106        MapIterator { iter, env: env.as_any_env(), exhausted: false }
107    }
108}
109
110/// Iterator over the key-value pairs of a [`Map`].
111///
112/// Ownership/mutation model (verified against ERTS, `erl_nif.c`
113/// `enif_map_iterator_*`, OTP 26 and 27):
114///
115/// - A flatmap iterator owns nothing — `ks`/`vs` are interior pointers into the
116///   map term's heap arrays; `create` is pure setup and `destroy` is a no-op.
117/// - A hashmap iterator owns one heap block: `create` does
118///   `erts_alloc(ErtsDynamicWStack)` and `destroy` frees it (plus a second,
119///   grown buffer if the traversal stack outgrew its inline default).
120/// - `next`/`prev` mutate the cursor and the shared work-stack **in place**.
121///
122/// The load-bearing invariant is therefore **exactly one owner ever calls
123/// `destroy`**: a bitwise copy would share the `wstack` pointer and double-free
124/// on the second drop (and corrupt the shared stack via independent `next`).
125/// That is why `MapIterator` is non-`Copy` and owns the single `Drop`.
126///
127/// The struct holds **no** self-references (every pointer targets the
128/// separately-allocated wstack or the map's own heap, never `&iter`), so moving
129/// it is sound on the targeted releases and the `Box` is not strictly required
130/// today. It is kept as a forward-compat pin: the struct is documented "all
131/// fields internal and may change" with reserved `__spare__[2]` slots, so a
132/// future ERTS could introduce a self-referential field that would make moves
133/// unsound. See issue robust-12 for the full investigation.
134pub struct MapIterator<'id> {
135    iter: Box<enif_ffi::MapIterator>,
136    env: AnyEnv<'id>,
137    exhausted: bool,
138}
139
140impl<'id> Iterator for MapIterator<'id> {
141    type Item = (AnyTerm<'id>, AnyTerm<'id>);
142
143    fn next(&mut self) -> Option<Self::Item> {
144        if self.exhausted {
145            return None;
146        }
147        let mut key: RawTerm = 0;
148        let mut value: RawTerm = 0;
149        if unsafe {
150            enif_ffi::map_iterator_get_pair(self.env.raw_env(), &mut *self.iter, &mut key, &mut value)
151        } != 0
152        {
153            // Advance for the next call; exhaustion is detected by get_pair.
154            unsafe { enif_ffi::map_iterator_next(self.env.raw_env(), &mut *self.iter) };
155            Some((AnyTerm::wrap(key, self.env), AnyTerm::wrap(value, self.env)))
156        } else {
157            self.exhausted = true;
158            None
159        }
160    }
161}
162
163impl Drop for MapIterator<'_> {
164    fn drop(&mut self) {
165        unsafe { enif_ffi::map_iterator_destroy(self.env.raw_env(), &mut *self.iter) };
166    }
167}
168
169impl PartialEq for Map<'_> {
170    fn eq(&self, other: &Self) -> bool {
171        unsafe { enif_ffi::is_identical(self.raw_term, other.raw_term) != 0 }
172    }
173}
174
175impl Eq for Map<'_> {}
176
177impl PartialOrd for Map<'_> {
178    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
179        Some(self.cmp(other))
180    }
181}
182
183impl Ord for Map<'_> {
184    fn cmp(&self, other: &Self) -> std::cmp::Ordering {
185        let c = unsafe { enif_ffi::compare(self.raw_term, other.raw_term) };
186        c.cmp(&0)
187    }
188}
189
190impl std::fmt::Debug for Map<'_> {
191    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
192        write!(f, "Map")
193    }
194}
195
196impl<'id> Sealed for Map<'id> {}
197
198impl<'id> Term<'id> for Map<'id> {
199    fn raw_term(self) -> RawTerm {
200        self.raw_term
201    }
202}