Skip to main content

otter/types/terms/
integer.rs

1//! [`Integer`] — arbitrary-precision Erlang integers, with `i64`/`u64`
2//! accessors and, under the `bigint` feature, full `BigInt` conversion for
3//! magnitudes the fixed-width accessors cannot reach.
4
5use core::marker::PhantomData;
6
7#[cfg(feature = "bigint")]
8use num_bigint::{BigInt, Sign};
9
10use crate::types::sealed::Sealed;
11use crate::types::{Env, Invariant, RawTerm, Term};
12
13/// An Erlang integer. Arbitrary precision — small integers are tagged
14/// immediates, large integers (bignums) are heap-allocated on the env.
15///
16/// Carries only its env's brand `'id`, not the env itself. An integer can be
17/// read back only with an env of the same brand (the 1:1 identity guarantee),
18/// so the accessors take the env explicitly rather than the term carrying it.
19#[derive(Clone, Copy)]
20pub struct Integer<'id> {
21    raw_term: RawTerm,
22    _id: Invariant<'id>,
23}
24
25impl<'id> Integer<'id> {
26    #[crate::raw]
27    pub(crate) fn from_raw(raw_term: RawTerm) -> Self {
28        Self { raw_term, _id: PhantomData }
29    }
30
31    /// Construct an integer term from an `i64` (`enif_make_int64`).
32    pub fn from_i64(env: impl Env<'id>, val: i64) -> Self {
33        let raw_term = unsafe { enif_ffi::make_int64(env.raw_env(), val) };
34        Integer { raw_term, _id: PhantomData }
35    }
36
37    /// Construct an integer term from a `u64` (`enif_make_uint64`).
38    pub fn from_u64(env: impl Env<'id>, val: u64) -> Self {
39        let raw_term = unsafe { enif_ffi::make_uint64(env.raw_env(), val) };
40        Integer { raw_term, _id: PhantomData }
41    }
42
43    /// Read back an `i64` (`enif_get_int64`). `None` if the term does not fit in
44    /// `i64`. `env` must carry the same brand as this term.
45    pub fn to_i64(self, env: impl Env<'id>) -> Option<i64> {
46        let mut val: i64 = 0;
47        (unsafe { enif_ffi::get_int64(env.raw_env(), self.raw_term, &mut val) } != 0).then_some(val)
48    }
49
50    /// Read back a `u64` (`enif_get_uint64`). `None` if the term does not fit in
51    /// `u64` (including negatives).
52    pub fn to_u64(self, env: impl Env<'id>) -> Option<u64> {
53        let mut val: u64 = 0;
54        (unsafe { enif_ffi::get_uint64(env.raw_env(), self.raw_term, &mut val) } != 0).then_some(val)
55    }
56
57    /// Read this integer as an arbitrary-precision [`BigInt`], including bignums
58    /// that exceed `i64`/`u64` — which [`to_i64`](Self::to_i64) /
59    /// [`to_u64`](Self::to_u64) cannot reach, since the NIF API has no accessor
60    /// beyond `enif_get_int64`/`enif_get_uint64`.
61    ///
62    /// Total over every integer term: it serializes the term to the external
63    /// term format (`enif_term_to_binary`) and reconstructs the value from the
64    /// ETF integer encoding. Infallible — the term is already a validated
65    /// integer, so it always serializes to one of the four ETF integer tags.
66    ///
67    /// Requires the `bigint` feature.
68    #[cfg(feature = "bigint")]
69    #[cfg_attr(docsrs, doc(cfg(feature = "bigint")))]
70    pub fn to_bigint(self, env: impl Env<'id>) -> BigInt {
71        let buf = crate::types::serialize(env, self)
72            .expect("enif_term_to_binary failed on an integer term");
73        etf_integer_to_bigint(buf.as_bytes())
74    }
75
76    /// Construct an integer term from an arbitrary-precision [`BigInt`],
77    /// including values beyond `i64`/`u64`.
78    ///
79    /// Values within range go straight through `enif_make_int64` /
80    /// `enif_make_uint64`; larger magnitudes are emitted as an ETF bignum
81    /// (`SMALL_BIG_EXT`/`LARGE_BIG_EXT`) and parsed back with
82    /// `enif_binary_to_term`. Infallible.
83    ///
84    /// Requires the `bigint` feature.
85    #[cfg(feature = "bigint")]
86    #[cfg_attr(docsrs, doc(cfg(feature = "bigint")))]
87    pub fn from_bigint(env: impl Env<'id>, val: &BigInt) -> Self {
88        if let Ok(i) = i64::try_from(val) {
89            return Self::from_i64(env, i);
90        }
91        if let Ok(u) = u64::try_from(val) {
92            return Self::from_u64(env, u);
93        }
94        let etf = bigint_to_etf(val);
95        let term = crate::types::deserialize(env, &etf, false)
96            .expect("enif_binary_to_term failed on otter-authored bignum ETF");
97        Integer::from_raw(term.raw_term())
98    }
99}
100
101// ETF integer tags (external term format §). The version byte 131 prefixes
102// every term `enif_term_to_binary` produces.
103#[cfg(feature = "bigint")]
104const ETF_VERSION: u8 = 131;
105#[cfg(feature = "bigint")]
106const SMALL_INTEGER_EXT: u8 = 97; // 1 unsigned byte
107#[cfg(feature = "bigint")]
108const INTEGER_EXT: u8 = 98; // 4 bytes, big-endian, signed
109#[cfg(feature = "bigint")]
110const SMALL_BIG_EXT: u8 = 110; // n:u8, sign:u8, n bytes little-endian magnitude
111#[cfg(feature = "bigint")]
112const LARGE_BIG_EXT: u8 = 111; // n:u32 big-endian, sign:u8, n bytes LE magnitude
113
114/// Parse a `BigInt` out of the ETF bytes `enif_term_to_binary` produced for an
115/// integer term. The term is a validated integer, so the body is exactly one of
116/// the four integer tags; any other shape is a contract violation and panics.
117#[cfg(feature = "bigint")]
118fn etf_integer_to_bigint(bytes: &[u8]) -> BigInt {
119    assert!(
120        bytes.len() >= 2 && bytes[0] == ETF_VERSION,
121        "malformed ETF from enif_term_to_binary"
122    );
123    let body = &bytes[1..];
124    match body[0] {
125        SMALL_INTEGER_EXT => BigInt::from(body[1]),
126        INTEGER_EXT => BigInt::from(i32::from_be_bytes([body[1], body[2], body[3], body[4]])),
127        SMALL_BIG_EXT => {
128            let n = body[1] as usize;
129            BigInt::from_bytes_le(etf_sign(body[2]), &body[3..3 + n])
130        }
131        LARGE_BIG_EXT => {
132            let n = u32::from_be_bytes([body[1], body[2], body[3], body[4]]) as usize;
133            BigInt::from_bytes_le(etf_sign(body[5]), &body[6..6 + n])
134        }
135        tag => panic!("enif_term_to_binary of an integer produced unexpected ETF tag {tag}"),
136    }
137}
138
139/// ETF big sign byte: 0 = non-negative, anything else = negative.
140#[cfg(feature = "bigint")]
141fn etf_sign(byte: u8) -> Sign {
142    if byte == 0 { Sign::Plus } else { Sign::Minus }
143}
144
145/// Encode a `BigInt` as a version-prefixed ETF bignum. Caller has already ruled
146/// out the i64/u64 fast paths, so the value never fits a smaller integer tag.
147#[cfg(feature = "bigint")]
148fn bigint_to_etf(val: &BigInt) -> Vec<u8> {
149    let (sign, mag) = val.to_bytes_le();
150    let sign_byte = if sign == Sign::Minus { 1 } else { 0 };
151    let mut etf = Vec::with_capacity(mag.len() + 7);
152    etf.push(ETF_VERSION);
153    if mag.len() <= u8::MAX as usize {
154        etf.push(SMALL_BIG_EXT);
155        etf.push(mag.len() as u8);
156        etf.push(sign_byte);
157    } else {
158        etf.push(LARGE_BIG_EXT);
159        etf.extend_from_slice(&(mag.len() as u32).to_be_bytes());
160        etf.push(sign_byte);
161    }
162    etf.extend_from_slice(&mag);
163    etf
164}
165
166impl<'id> Sealed for Integer<'id> {}
167
168impl<'id> Term<'id> for Integer<'id> {
169    fn raw_term(self) -> RawTerm {
170        self.raw_term
171    }
172}
173
174impl PartialEq for Integer<'_> {
175    fn eq(&self, other: &Self) -> bool {
176        unsafe { enif_ffi::is_identical(self.raw_term, other.raw_term) != 0 }
177    }
178}
179
180impl Eq for Integer<'_> {}
181
182impl PartialOrd for Integer<'_> {
183    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
184        Some(self.cmp(other))
185    }
186}
187
188impl Ord for Integer<'_> {
189    fn cmp(&self, other: &Self) -> std::cmp::Ordering {
190        let c = unsafe { enif_ffi::compare(self.raw_term, other.raw_term) };
191        c.cmp(&0)
192    }
193}
194
195impl std::fmt::Debug for Integer<'_> {
196    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
197        write!(f, "Integer")
198    }
199}