Skip to main content

otter/types/terms/
tuple.rs

1//! [`Tuple`] — fixed-arity Erlang tuples — and [`TupleView`], the resolved form
2//! whose elements can be indexed and iterated. The split keeps a pass-through
3//! tuple from paying for the element fetch.
4
5use core::marker::PhantomData;
6use std::ffi::{c_int, c_uint};
7
8use crate::types::sealed::Sealed;
9use crate::types::{AnyTerm, Env, Invariant, RawTerm, Term};
10
11/// An Erlang tuple.
12///
13/// The lean form: a one-word term handle, like every other otter term. Reading
14/// its elements is an explicit step — [`with_elements`](Tuple::with_elements)
15/// performs the single `enif_get_tuple` and yields a [`TupleView`], the
16/// collection-like form. Keeping the two separate means a tuple that is only
17/// passed through (matched in `TypedTerm`, re-encoded, compared) never pays for
18/// the element fetch.
19#[derive(Clone, Copy)]
20pub struct Tuple<'id> {
21    raw_term: RawTerm,
22    _id: Invariant<'id>,
23}
24
25impl<'id> Tuple<'id> {
26    #[crate::raw]
27    pub(crate) fn from_raw(raw_term: RawTerm) -> Self {
28        Self { raw_term, _id: PhantomData }
29    }
30
31    /// Resolve the tuple's elements (one `enif_get_tuple`) into a [`TupleView`].
32    ///
33    /// A `Tuple` is only ever built from a term already confirmed to be a tuple,
34    /// so `enif_get_tuple` cannot fail — a zero return is an internal contract
35    /// violation, not a user-input case, hence the assert.
36    pub fn with_elements(self, env: impl Env<'id>) -> TupleView<'id> {
37        let mut arity: c_int = 0;
38        let mut array: *const RawTerm = std::ptr::null();
39        let ok = unsafe { enif_ffi::get_tuple(env.raw_env(), self.raw_term, &mut arity, &mut array) };
40        assert!(ok != 0, "enif_get_tuple failed on a validated Tuple");
41        // enif_get_tuple may leave `array` null for the empty tuple; never hand
42        // a null pointer to from_raw_parts.
43        let raw_elements = if arity == 0 {
44            &[][..]
45        } else {
46            unsafe { std::slice::from_raw_parts(array, arity as usize) }
47        };
48        TupleView { raw_term: self.raw_term, raw_elements, _id: PhantomData }
49    }
50
51    /// Construct a tuple from any iterable of terms of this brand
52    /// (`enif_make_tuple_from_array`).
53    pub fn from_terms<I, T>(env: impl Env<'id>, terms: I) -> Tuple<'id>
54    where
55        I: IntoIterator<Item = T>,
56        T: Term<'id>,
57    {
58        let raw: Vec<RawTerm> = terms.into_iter().map(|t| t.raw_term()).collect();
59        let raw_term = unsafe {
60            enif_ffi::make_tuple_from_array(env.raw_env(), raw.as_ptr(), raw.len() as c_uint)
61        };
62        Tuple { raw_term, _id: PhantomData }
63    }
64
65    /// Returns `true` if `term` is a tuple (`enif_is_tuple`).
66    pub fn is_tuple(env: impl Env<'id>, term: impl Term<'id>) -> bool {
67        unsafe { enif_ffi::is_tuple(env.raw_env(), term.raw_term()) != 0 }
68    }
69}
70
71impl PartialEq for Tuple<'_> {
72    fn eq(&self, other: &Self) -> bool {
73        unsafe { enif_ffi::is_identical(self.raw_term, other.raw_term) != 0 }
74    }
75}
76
77impl Eq for Tuple<'_> {}
78
79impl PartialOrd for Tuple<'_> {
80    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
81        Some(self.cmp(other))
82    }
83}
84
85impl Ord for Tuple<'_> {
86    fn cmp(&self, other: &Self) -> std::cmp::Ordering {
87        let c = unsafe { enif_ffi::compare(self.raw_term, other.raw_term) };
88        c.cmp(&0)
89    }
90}
91
92impl std::fmt::Debug for Tuple<'_> {
93    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
94        write!(f, "Tuple")
95    }
96}
97
98impl<'id> Sealed for Tuple<'id> {}
99
100impl<'id> Term<'id> for Tuple<'id> {
101    fn raw_term(self) -> RawTerm {
102        self.raw_term
103    }
104}
105
106// ---------------------------------------------------------------------------
107// TupleView — a tuple with its elements resolved
108// ---------------------------------------------------------------------------
109
110/// A [`Tuple`] whose elements have been resolved, produced by
111/// [`Tuple::with_elements`].
112///
113/// It is still a tuple [`Term`] (it carries the original term word, so it
114/// encodes and compares like one), and additionally behaves as a fixed-size
115/// collection of its elements: [`len`](Self::len)/[`is_empty`](Self::is_empty),
116/// `tuple[i]` indexing, and iteration, all yielding [`AnyTerm<'id>`] — the
117/// unresolved element terms. `enif_get_tuple` returns a pointer into the boxed
118/// tuple's own heap storage (it hands back `tuple_val(t)+1`, confirmed in the
119/// ERTS source); the process heap holds that at a fixed address for the
120/// duration of a NIF call (no GC mid-NIF), so the cached slice is valid for
121/// this view's brand `'id` and the type stays `Copy`.
122#[derive(Clone, Copy)]
123pub struct TupleView<'id> {
124    raw_term: RawTerm,
125    raw_elements: &'id [RawTerm],
126    _id: Invariant<'id>,
127}
128
129impl<'id> TupleView<'id> {
130    /// The elements as a slice of [`AnyTerm`]. Zero-cost: `AnyTerm` is
131    /// `#[repr(transparent)]` over `RawTerm`, so the stored raw-word slice is
132    /// reinterpreted in place under this view's brand.
133    fn elements(self) -> &'id [AnyTerm<'id>] {
134        // SAFETY: AnyTerm<'id> is repr(transparent) over RawTerm and carries
135        // only a ZST brand marker, so [RawTerm] and [AnyTerm<'id>] are layout-
136        // identical; every raw word is a valid term of this tuple's brand.
137        unsafe { std::mem::transmute::<&'id [RawTerm], &'id [AnyTerm<'id>]>(self.raw_elements) }
138    }
139
140    /// Number of elements (arity) of the tuple.
141    pub fn len(self) -> usize {
142        self.raw_elements.len()
143    }
144
145    /// Returns `true` if the tuple has zero elements.
146    pub fn is_empty(self) -> bool {
147        self.raw_elements.is_empty()
148    }
149}
150
151impl<'id> std::ops::Index<usize> for TupleView<'id> {
152    type Output = AnyTerm<'id>;
153
154    /// The element at zero-based index `i`. Panics if `i >= self.len()`.
155    fn index(&self, i: usize) -> &AnyTerm<'id> {
156        &self.elements()[i]
157    }
158}
159
160impl<'id> IntoIterator for TupleView<'id> {
161    type Item = AnyTerm<'id>;
162    type IntoIter = std::iter::Copied<std::slice::Iter<'id, AnyTerm<'id>>>;
163
164    fn into_iter(self) -> Self::IntoIter {
165        self.elements().iter().copied()
166    }
167}
168
169impl std::fmt::Debug for TupleView<'_> {
170    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
171        write!(f, "TupleView")
172    }
173}
174
175impl<'id> Sealed for TupleView<'id> {}
176
177impl<'id> Term<'id> for TupleView<'id> {
178    fn raw_term(self) -> RawTerm {
179        self.raw_term
180    }
181}