Skip to main content

otter/codec/
mod.rs

1//! Conversions between Erlang terms and native Rust types — [`Encoder`],
2//! [`Decoder`], and [`CodecError`].
3//!
4//! Both directions are fallible and symmetric. A failed [`Decoder`] on a NIF
5//! argument becomes a `badarg` exception on the way in; a failed [`Encoder`] on
6//! a NIF return becomes a `badret` on the way out — the `#[otter::nif]` macro
7//! performs both conversions, so [`CodecError`] never appears in a NIF signature.
8//!
9//! otter's own term types ([`Integer`], [`Binary`], [`Map`], …) implement both
10//! traits and **never fail**: encoding wraps the term's machine word for free,
11//! decoding pays exactly one type check. The fallible impls are the native-Rust
12//! conversions, split across submodules by concern:
13//!
14//! - **integers** — `i8`…`i64`, `u8`…`u64`, `isize`/`usize`
15//!   ([`IntegerOverflow`](CodecError::IntegerOverflow) on a value that does not
16//!   fit)
17//! - **floats** — `f32`/`f64` ([`NotFinite`](CodecError::NotFinite) on encode,
18//!   [`FloatRange`](CodecError::FloatRange) on a too-wide `f32` decode)
19//! - **bool** — the atoms `true`/`false`
20//! - **`str`/`String`** — encode to a binary, decode from a binary or charlist
21//!   ([`NotUtf8`](CodecError::NotUtf8))
22//! - **tuples** — arities 1–12 ([`WrongArity`](CodecError::WrongArity))
23//! - **`Vec<T>`/`[T]`** — proper Erlang lists
24//! - **`HashMap<K, V>`** — Erlang maps
25//! - **[`num_bigint::BigInt`]** — arbitrary-precision integers, under the
26//!   `bigint` feature
27//!
28//! # No blanket `TryFrom`
29//!
30//! A blanket `impl<'a, T: Decoder<'a>> TryFrom<TypedTerm<'a>> for T` cannot be
31//! provided: it violates Rust's orphan rules (E0210), because neither `TryFrom`
32//! nor the type parameter `T` is local to this crate. Call
33//! [`T::decode(term, env)`](Decoder::decode) directly instead.
34//!
35//! [`num_bigint::BigInt`]: https://docs.rs/num-bigint
36//! [`Integer`]: crate::types::Integer
37//! [`Binary`]: crate::types::Binary
38//! [`Map`]: crate::types::Map
39
40#[cfg(feature = "bigint")]
41#[cfg_attr(docsrs, doc(cfg(feature = "bigint")))]
42mod bigint;
43mod bool;
44mod float;
45mod integer;
46mod list;
47mod map;
48mod string;
49mod tuple;
50
51use crate::types::{
52    AnyTerm, Atom, Binary, Bitstring, Env, Float, Fun, Integer, List, LocalPid, LocalPort, Map, Pid,
53    Port, Raised, Reference, Term, Tuple, TupleView, TypedTerm, THE_NON_VALUE,
54};
55
56/// Error returned by term type conversion operations.
57///
58/// otter's internal error type — it never appears in user NIF signatures. The
59/// `#[otter::nif]` macro converts codec failures to `badarg` automatically.
60#[derive(Debug, Clone, Copy, PartialEq, Eq)]
61pub enum CodecError {
62    /// The term was not the expected type.
63    WrongType,
64    /// An integer term did not fit the requested Rust integer type.
65    IntegerOverflow,
66    /// A float value could not be represented as an Erlang float because it is
67    /// not finite (NaN or infinity) — an encode-side failure.
68    NotFinite,
69    /// A finite float term fell outside the finite range of the requested Rust
70    /// float type (only `f32`, on decode).
71    FloatRange,
72    /// A binary's bytes were not valid UTF-8, or a list was not a valid string,
73    /// when decoding to a Rust `String`.
74    NotUtf8,
75    /// An Erlang tuple's arity did not match the arity of the Rust tuple type it
76    /// was being decoded into.
77    WrongArity,
78    /// The term's type code is one this otter build does not recognize — a term
79    /// type added by a newer OTP than otter knows about.
80    UnknownTermType,
81}
82
83impl std::fmt::Display for CodecError {
84    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
85        match self {
86            CodecError::WrongType => write!(f, "wrong term type"),
87            CodecError::IntegerOverflow => write!(f, "integer overflow"),
88            CodecError::NotFinite => write!(f, "float is not finite"),
89            CodecError::FloatRange => write!(f, "float out of range"),
90            CodecError::NotUtf8 => write!(f, "not valid UTF-8"),
91            CodecError::WrongArity => write!(f, "wrong tuple arity"),
92            CodecError::UnknownTermType => write!(f, "unknown term type"),
93        }
94    }
95}
96
97impl std::error::Error for CodecError {}
98
99// ---------------------------------------------------------------------------
100// Encoder
101// ---------------------------------------------------------------------------
102
103/// Convert a value into an Erlang term of brand `'id`.
104///
105/// Fallible, mirroring [`Decoder`]: a value outside the Erlang term domain
106/// (e.g. a non-finite `f64`) returns `Err(CodecError)`. The `#[otter::nif]`
107/// return path turns an `Err` into a `badret` exception — symmetric to the
108/// `badarg` a failed [`Decoder`] raises on the way in. Impls that cannot fail —
109/// every otter term type encodes by wrapping its word for free — return `Ok`.
110///
111/// A same-brand term encodes for free; cross-env terms are not encoded — copy
112/// them first with [`Term::copy_to`].
113pub trait Encoder<'id> {
114    fn encode(&self, env: impl Env<'id>) -> Result<AnyTerm<'id>, CodecError>;
115}
116
117/// Encode a `Result<T, Raised>` in **return position only**.
118///
119/// `Ok(v)` encodes `v`. `Err(raised)` carries proof that an exception is already
120/// pending on the env, so the non-value marker is returned and the BEAM raises
121/// the pending exception on NIF return. Never encode an `Err` mid-term (inside a
122/// tuple/list/map) — that diverts the marker into a value position. Always
123/// propagate a `Result<T, Raised>` with `?` or `return`.
124impl<'id, T: Encoder<'id>> Encoder<'id> for Result<T, Raised<'id>> {
125    fn encode(&self, env: impl Env<'id>) -> Result<AnyTerm<'id>, CodecError> {
126        match self {
127            Ok(v) => v.encode(env),
128            Err(_) => Ok(AnyTerm::wrap(THE_NON_VALUE, env)),
129        }
130    }
131}
132
133// ---------------------------------------------------------------------------
134// Decoder
135// ---------------------------------------------------------------------------
136
137/// Extract a value from an Erlang term of brand `'id`.
138///
139/// Implemented by otter term types. Takes the raw [`AnyTerm`] plus an env (the
140/// term carries only its brand, not its env), so each impl pays exactly the type
141/// check it needs. Returns `Err(CodecError)` if the term is not the expected
142/// type or the value does not fit.
143pub trait Decoder<'id>: Sized {
144    fn decode(term: AnyTerm<'id>, env: impl Env<'id>) -> Result<Self, CodecError>;
145}
146
147/// The identity decode — any term decodes to itself.
148impl<'id> Decoder<'id> for AnyTerm<'id> {
149    fn decode(term: AnyTerm<'id>, _env: impl Env<'id>) -> Result<Self, CodecError> {
150        Ok(term)
151    }
152}
153
154// ---------------------------------------------------------------------------
155// Built-in type impls
156//
157// Centralized here because each impl uses only the noun's public surface
158// (`Term::raw_term`, the `is_*`/`term_type` predicates, `from_raw`): `encode`
159// wraps the same-brand word for free, `decode` checks the type then rewraps.
160//
161// Two decode idioms appear below, chosen by what the NIF API offers, not by
162// taste: types with a dedicated `enif_is_*` predicate (atom, binary, fun, pid,
163// port, ref, list, map, tuple) check via `Type::is_*(env, term)`; the three
164// with no such predicate (integer, float, bitstring) fall back to comparing
165// `env.term_type(term)` against the expected `TermType`.
166// ---------------------------------------------------------------------------
167
168macro_rules! encode_by_wrap {
169    ($($t:ty),+ $(,)?) => { $(
170        /// Encodes for free by rewrapping the term's own machine word — an otter
171        /// term is already an Erlang term. Never fails.
172        impl<'id> Encoder<'id> for $t {
173            fn encode(&self, env: impl Env<'id>) -> Result<AnyTerm<'id>, CodecError> {
174                Ok(AnyTerm::wrap(Term::raw_term(*self), env))
175            }
176        }
177    )+ };
178}
179
180encode_by_wrap!(
181    AnyTerm<'id>, Integer<'id>, Float<'id>, Reference<'id>, Fun<'id>, Tuple<'id>, List<'id>,
182    Map<'id>, Binary<'id>, Bitstring<'id>, Pid<'id>, Port<'id>, TupleView<'id>, Atom, LocalPid,
183    LocalPort,
184);
185
186/// Encodes the already-resolved term by rewrapping its word. Never fails.
187impl<'id> Encoder<'id> for TypedTerm<'id> {
188    fn encode(&self, env: impl Env<'id>) -> Result<AnyTerm<'id>, CodecError> {
189        Ok(AnyTerm::wrap((*self).raw_term(), env))
190    }
191}
192
193// --- Decoders ---
194//
195// Each checks the term's type, then rewraps the word into the concrete type;
196// the only failure is [`CodecError::WrongType`] (or [`UnknownTermType`] for the
197// `TypedTerm` resolve). No data is read off the BEAM heap.
198
199/// Accepts an integer term (`enif_term_type == Integer`); rejects anything else
200/// as [`WrongType`](CodecError::WrongType).
201impl<'id> Decoder<'id> for Integer<'id> {
202    fn decode(term: AnyTerm<'id>, env: impl Env<'id>) -> Result<Self, CodecError> {
203        if env.term_type(term) == Some(enif_ffi::TermType::Integer) {
204            Ok(Integer::from_raw(term.raw_term()))
205        } else {
206            Err(CodecError::WrongType)
207        }
208    }
209}
210
211/// Accepts a float term (`enif_term_type == Float`); [`WrongType`](CodecError::WrongType)
212/// otherwise.
213impl<'id> Decoder<'id> for Float<'id> {
214    fn decode(term: AnyTerm<'id>, env: impl Env<'id>) -> Result<Self, CodecError> {
215        if env.term_type(term) == Some(enif_ffi::TermType::Float) {
216            Ok(Float::from_raw(term.raw_term()))
217        } else {
218            Err(CodecError::WrongType)
219        }
220    }
221}
222
223/// Accepts any bitstring (`enif_term_type == Bitstring`) — both byte-aligned
224/// binaries and sub-byte bitstrings. Refine to [`Binary`] for the byte-aligned
225/// case. [`WrongType`](CodecError::WrongType) otherwise.
226impl<'id> Decoder<'id> for Bitstring<'id> {
227    fn decode(term: AnyTerm<'id>, env: impl Env<'id>) -> Result<Self, CodecError> {
228        // Every binary is a bitstring, so this accepts both byte-aligned and
229        // sub-byte; use `Binary` for the byte-aligned refinement.
230        if env.term_type(term) == Some(enif_ffi::TermType::Bitstring) {
231            Ok(Bitstring::from_raw(term.raw_term()))
232        } else {
233            Err(CodecError::WrongType)
234        }
235    }
236}
237
238/// Accepts a reference (`enif_is_ref`); [`WrongType`](CodecError::WrongType)
239/// otherwise.
240impl<'id> Decoder<'id> for Reference<'id> {
241    fn decode(term: AnyTerm<'id>, env: impl Env<'id>) -> Result<Self, CodecError> {
242        if Reference::is_ref(env, term) {
243            Ok(Reference::from_raw(term.raw_term()))
244        } else {
245            Err(CodecError::WrongType)
246        }
247    }
248}
249
250/// Accepts a fun (`enif_is_fun`); [`WrongType`](CodecError::WrongType) otherwise.
251impl<'id> Decoder<'id> for Fun<'id> {
252    fn decode(term: AnyTerm<'id>, env: impl Env<'id>) -> Result<Self, CodecError> {
253        if Fun::is_fun(env, term) {
254            Ok(Fun::from_raw(term.raw_term()))
255        } else {
256            Err(CodecError::WrongType)
257        }
258    }
259}
260
261/// Accepts a tuple of any arity (`enif_is_tuple`);
262/// [`WrongType`](CodecError::WrongType) otherwise.
263impl<'id> Decoder<'id> for Tuple<'id> {
264    fn decode(term: AnyTerm<'id>, env: impl Env<'id>) -> Result<Self, CodecError> {
265        if Tuple::is_tuple(env, term) {
266            Ok(Tuple::from_raw(term.raw_term()))
267        } else {
268            Err(CodecError::WrongType)
269        }
270    }
271}
272
273/// Accepts a list — including the empty list, and improper lists (`enif_is_list`).
274/// [`WrongType`](CodecError::WrongType) otherwise.
275impl<'id> Decoder<'id> for List<'id> {
276    fn decode(term: AnyTerm<'id>, env: impl Env<'id>) -> Result<Self, CodecError> {
277        if List::is_list(env, term) {
278            Ok(List::from_raw(term.raw_term()))
279        } else {
280            Err(CodecError::WrongType)
281        }
282    }
283}
284
285/// Accepts a map (`enif_is_map`); [`WrongType`](CodecError::WrongType) otherwise.
286impl<'id> Decoder<'id> for Map<'id> {
287    fn decode(term: AnyTerm<'id>, env: impl Env<'id>) -> Result<Self, CodecError> {
288        if Map::is_map(env, term) {
289            Ok(Map::from_raw(term.raw_term()))
290        } else {
291            Err(CodecError::WrongType)
292        }
293    }
294}
295
296/// Accepts a byte-aligned binary (`enif_is_binary`); a sub-byte bitstring is
297/// rejected as [`WrongType`](CodecError::WrongType) — decode to [`Bitstring`]
298/// for those.
299impl<'id> Decoder<'id> for Binary<'id> {
300    fn decode(term: AnyTerm<'id>, env: impl Env<'id>) -> Result<Self, CodecError> {
301        if Binary::is_binary(env, term) {
302            Ok(Binary::from_raw(term.raw_term()))
303        } else {
304            Err(CodecError::WrongType)
305        }
306    }
307}
308
309/// Accepts a pid of any locality (`enif_is_pid`); to require a node-local pid,
310/// decode to [`LocalPid`]. [`WrongType`](CodecError::WrongType) otherwise.
311impl<'id> Decoder<'id> for Pid<'id> {
312    fn decode(term: AnyTerm<'id>, env: impl Env<'id>) -> Result<Self, CodecError> {
313        if Pid::is_pid(env, term) {
314            Ok(Pid::from_raw(term.raw_term()))
315        } else {
316            Err(CodecError::WrongType)
317        }
318    }
319}
320
321/// Accepts a port of any locality (`enif_is_port`); to require a node-local
322/// port, decode to [`LocalPort`]. [`WrongType`](CodecError::WrongType) otherwise.
323impl<'id> Decoder<'id> for Port<'id> {
324    fn decode(term: AnyTerm<'id>, env: impl Env<'id>) -> Result<Self, CodecError> {
325        if Port::is_port(env, term) {
326            Ok(Port::from_raw(term.raw_term()))
327        } else {
328            Err(CodecError::WrongType)
329        }
330    }
331}
332
333/// Accepts an atom (`enif_is_atom`); [`WrongType`](CodecError::WrongType)
334/// otherwise.
335impl<'id> Decoder<'id> for Atom {
336    fn decode(term: AnyTerm<'id>, env: impl Env<'id>) -> Result<Self, CodecError> {
337        if Atom::is_atom(env, term) {
338            Ok(Atom::from_raw(term.raw_term()))
339        } else {
340            Err(CodecError::WrongType)
341        }
342    }
343}
344
345/// Accepts a node-local pid (`enif_get_local_pid`); an external (remote-node)
346/// pid is rejected as [`WrongType`](CodecError::WrongType).
347impl<'id> Decoder<'id> for LocalPid {
348    fn decode(term: AnyTerm<'id>, env: impl Env<'id>) -> Result<Self, CodecError> {
349        // An external pid passes is_pid but is not local; get_local_pid rejects.
350        Pid::from_raw(term.raw_term()).to_local(env).ok_or(CodecError::WrongType)
351    }
352}
353
354/// Accepts a node-local port; an external port is rejected as
355/// [`WrongType`](CodecError::WrongType).
356impl<'id> Decoder<'id> for LocalPort {
357    fn decode(term: AnyTerm<'id>, env: impl Env<'id>) -> Result<Self, CodecError> {
358        Port::from_raw(term.raw_term()).to_local(env).ok_or(CodecError::WrongType)
359    }
360}
361
362/// Resolves the term to its [`TypedTerm`] variant (one `enif_term_type` call).
363/// A type code this otter build does not recognize — a term type from a newer
364/// OTP — is [`UnknownTermType`](CodecError::UnknownTermType).
365impl<'id> Decoder<'id> for TypedTerm<'id> {
366    fn decode(term: AnyTerm<'id>, env: impl Env<'id>) -> Result<Self, CodecError> {
367        term.resolve(env).ok_or(CodecError::UnknownTermType)
368    }
369}