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}