arena_terms/
term.rs

1//! Defines the core [`Term`] type and related constructors.
2//!
3//! Provides a compact representation for Prolog-like terms
4//! and basic utilities for creating and inspecting them.
5
6use crate::{Arena, EpochID, TermError};
7use core::fmt;
8use smartstring::alias::String;
9use std::borrow::Cow;
10
11// The following type definitions describe the internal representation
12// of a term.  Rather than packing data into a single integer we use
13// a tagged enum to store the various kinds of terms.  Each variant
14// carries its associated data directly, for example a 64 bit integer
15// for numeric types or a small inline buffer for short atoms and
16// variables.  Long names or sequences store an index and length into
17// the appropriate arena.
18
19#[derive(Debug, Copy, Clone, PartialEq, PartialOrd)]
20pub(crate) struct TinyArray {
21    pub(crate) bytes: [u8; 14],
22    pub(crate) len: u8,
23}
24
25#[derive(Debug, Copy, Clone, PartialEq, PartialOrd)]
26pub(crate) struct Slice {
27    pub(crate) epoch_id: EpochID,
28    pub(crate) index: u32,
29    pub(crate) len: u32,
30}
31
32/// Internal handle describing the kind of a term and storing its data.
33///
34/// Each variant stores the associated value directly.  The `repr(u8)`
35/// attribute ensures the discriminant occupies a single byte, which
36/// together with the payloads yields a `Term` size of 16 bytes on
37/// 64‑bit targets.
38#[derive(Debug, Copy, Clone, PartialEq, PartialOrd)]
39#[repr(u8)]
40pub(crate) enum Handle {
41    Int(i64),
42    Real(f64),
43    Date(i64),
44    Var(TinyArray),
45    VarRef(Slice),
46    Atom(TinyArray),
47    AtomRef(Slice),
48    Str(TinyArray),
49    StrRef(Slice),
50    Bin(TinyArray),
51    BinRef(Slice),
52    FuncRef(Slice),
53    ListRef(Slice),
54    ListCRef(Slice),
55    TupleRef(Slice),
56}
57
58/// A compact, copyable handle referencing a term stored in a [`Arena`].
59///
60/// Internally a `Term` stores a single [`Handle`] enum variant.
61/// On 64‑bit targets the discriminant and associated payload occupy
62/// 16 bytes in total.  Users should never construct `Term` values
63/// directly; instead use the associated constructors or the
64/// convenience macros in the [`term`] module.
65/// Instances of `Term` are cheap to copy (`Copy` and `Clone`).
66
67// TODO: Consider implementing Hash, Eq, and Ord. Verify whether it is valid
68//       to provide PartialEq, Eq, PartialOrd, Ord, and Hash when:
69//       - Two different Term handles may point to the same term value, or
70//       - Two identical Term handles obtained from different arenas may
71//         represent distinct term values.
72#[derive(Copy, Clone, PartialEq, PartialOrd)]
73pub struct Term(pub(crate) Handle);
74
75impl AsRef<Term> for Term {
76    fn as_ref(&self) -> &Self {
77        self
78    }
79}
80
81macro_rules! impl_from_integers_for_term {
82    ($($t:ty),* $(,)?) => {$(
83        impl From<$t> for Term {
84            #[inline]
85            fn from(v: $t) -> Self { Term::int(v as i64) }
86        }
87    )*};
88}
89impl_from_integers_for_term!(i8, i16, i32, i64, u8, u16, u32);
90
91macro_rules! impl_from_floats_for_term {
92    ($($t:ty),* $(,)?) => {$(
93        impl From<$t> for Term {
94            #[inline]
95            fn from(v: $t) -> Self { Term::real(v as f64) }
96        }
97    )*};
98}
99impl_from_floats_for_term!(f32, f64);
100
101pub trait IntoTerm {
102    fn into_term(self, arena: &mut Arena) -> Term;
103}
104
105macro_rules! impl_intoterm_for_integers {
106    ($($t:ty),* $(,)?) => {$(
107        impl IntoTerm for $t {
108            #[inline]
109            fn into_term(self, _arena: &mut Arena) -> Term { Term::int(self as i64) }
110        }
111    )*};
112}
113impl_intoterm_for_integers!(i8, i16, i32, i64, u8, u16, u32);
114
115macro_rules! impl_intoterm_for_floats {
116    ($($t:ty),* $(,)?) => {$(
117        impl IntoTerm for $t {
118            #[inline]
119            fn into_term(self, _arena: &mut Arena) -> Term { Term::real(self as f64) }
120        }
121    )*};
122}
123impl_intoterm_for_floats!(f32, f64);
124
125impl<'a> IntoTerm for &'a str {
126    #[inline]
127    fn into_term(self, arena: &mut Arena) -> Term {
128        Term::str(arena, self)
129    }
130}
131
132impl<'a> IntoTerm for &'a [u8] {
133    #[inline]
134    fn into_term(self, arena: &mut Arena) -> Term {
135        Term::bin(arena, self)
136    }
137}
138
139impl<'a> IntoTerm for Cow<'a, str> {
140    #[inline]
141    fn into_term(self, arena: &mut Arena) -> Term {
142        match self {
143            Cow::Borrowed(s) => Term::str(arena, s),
144            Cow::Owned(s) => Term::str(arena, s),
145        }
146    }
147}
148
149impl<'a> IntoTerm for Cow<'a, [u8]> {
150    #[inline]
151    fn into_term(self, arena: &mut Arena) -> Term {
152        match self {
153            Cow::Borrowed(s) => Term::bin(arena, s),
154            Cow::Owned(s) => Term::bin(arena, s),
155        }
156    }
157}
158
159impl IntoTerm for String {
160    #[inline]
161    fn into_term(self, arena: &mut Arena) -> Term {
162        Term::str(arena, &self)
163    }
164}
165
166impl IntoTerm for std::string::String {
167    #[inline]
168    fn into_term(self, arena: &mut Arena) -> Term {
169        Term::str(arena, &self)
170    }
171}
172
173impl IntoTerm for Vec<u8> {
174    #[inline]
175    fn into_term(self, arena: &mut Arena) -> Term {
176        Term::bin(arena, &self)
177    }
178}
179
180impl IntoTerm for Term {
181    #[inline]
182    fn into_term(self, _arena: &mut Arena) -> Term {
183        self
184    }
185}
186
187impl IntoTerm for &Term {
188    #[inline]
189    fn into_term(self, _arena: &mut Arena) -> Term {
190        *self
191    }
192}
193
194impl<F> IntoTerm for F
195where
196    F: FnOnce(&mut Arena) -> Term,
197{
198    #[inline]
199    fn into_term(self, arena: &mut Arena) -> Term {
200        self(arena)
201    }
202}
203
204impl Term {
205    /// Construct a new integer term.  The full 64 bit two's complement
206    /// representation of `i` is stored in the payload.  No truncation
207    /// occurs.
208    #[inline]
209    pub fn int(i: impl Into<i64>) -> Self {
210        Self(Handle::Int(i.into()))
211    }
212
213    /// Construct a new floating point term.  The full 64 bit IEEE‑754
214    /// bit pattern is stored in the payload without truncation.
215    #[inline]
216    pub fn real(f: impl Into<f64>) -> Self {
217        Self(Handle::Real(f.into()))
218    }
219
220    /// Construct a new date term representing a Unix epoch in
221    /// milliseconds.  Dates share the same underlying storage as
222    /// integers but use a distinct tag so they do not compare equal
223    /// with integer terms.
224    #[inline]
225    pub fn date(ms: impl Into<i64>) -> Self {
226        Self(Handle::Date(ms.into()))
227    }
228
229    /// Construct or intern an atom into the arena and produce a term
230    /// referencing it.  Small atom names (≤14 bytes of UTF‑8) are
231    /// inlined directly into the handle; longer names are interned
232    /// into the arena and referenced by index and length.
233    #[inline]
234    pub fn atom(arena: &mut Arena, name: impl AsRef<str>) -> Self {
235        let name = name.as_ref();
236        let bytes = name.as_bytes();
237        if bytes.len() <= 14 {
238            let mut buf = [0u8; 14];
239            buf[..bytes.len()].copy_from_slice(bytes);
240            Self(Handle::Atom(TinyArray {
241                bytes: buf,
242                len: bytes.len() as u8,
243            }))
244        } else {
245            Self(Handle::AtomRef(arena.intern_str(name)))
246        }
247    }
248
249    /// Construct or intern a variable into the arena and produce a
250    /// term referencing it.  Small variable names (≤14 bytes) are
251    /// inlined directly into the handle; longer names are interned in
252    /// the arena and referenced by index.
253    #[inline]
254    pub fn var(arena: &mut Arena, name: impl AsRef<str>) -> Self {
255        let name = name.as_ref();
256        let bytes = name.as_bytes();
257        if bytes.len() <= 14 {
258            let mut buf = [0u8; 14];
259            buf[..bytes.len()].copy_from_slice(bytes);
260            Self(Handle::Var(TinyArray {
261                bytes: buf,
262                len: bytes.len() as u8,
263            }))
264        } else {
265            Self(Handle::VarRef(arena.intern_str(name)))
266        }
267    }
268
269    /// Construct or intern a UTF‑8 string into the arena and produce a
270    /// term referencing it.  Strings longer than 14 bytes are interned
271    /// in the arena; shorter strings are inlined.  Invalid UTF‑8 will
272    /// result in an error.
273    #[inline]
274    pub fn str(arena: &mut Arena, s: impl AsRef<str>) -> Self {
275        let s = s.as_ref();
276        let bytes = s.as_bytes();
277        if bytes.len() <= 14 {
278            let mut buf = [0u8; 14];
279            buf[..bytes.len()].copy_from_slice(bytes);
280            Self(Handle::Str(TinyArray {
281                bytes: buf,
282                len: bytes.len() as u8,
283            }))
284        } else {
285            Self(Handle::StrRef(arena.intern_str(s)))
286        }
287    }
288
289    /// Construct or intern a binary blob into the arena and produce a
290    /// term referencing it.  Blobs longer than 14 bytes are interned
291    /// in the arena; shorter blobs are inlined.
292    #[inline]
293    pub fn bin(arena: &mut Arena, bytes: impl AsRef<[u8]>) -> Self {
294        let bytes = bytes.as_ref();
295        if bytes.len() <= 14 {
296            let mut buf = [0u8; 14];
297            buf[..bytes.len()].copy_from_slice(bytes);
298            Self(Handle::Bin(TinyArray {
299                bytes: buf,
300                len: bytes.len() as u8,
301            }))
302        } else {
303            Self(Handle::BinRef(arena.intern_bytes(bytes)))
304        }
305    }
306
307    /// Construct a new compound term by interning the functor and
308    /// arguments in the arena.  The returned term references a slice
309    /// in the arena's term storage consisting of the functor atom as
310    /// the first entry followed by the argument handles.  A functor of
311    /// arity zero results in an atom.
312    #[inline]
313    pub fn func(
314        arena: &mut Arena,
315        functor: impl AsRef<str>,
316        args: impl IntoIterator<Item = impl IntoTerm>,
317    ) -> Self {
318        let functor_atom = Self::atom(arena, functor);
319        let mut args = args.into_iter();
320        let Some(first) = args.next() else {
321            return functor_atom;
322        };
323        Self(Handle::FuncRef(arena.intern_func(
324            functor_atom,
325            std::iter::once(first).chain(args),
326        )))
327    }
328
329    /// Construct a new compound term by interning the functor and its arguments
330    /// into the arena as a sequence of terms (functor first, then arguments).
331    /// A functor with no arguments yields the atom itself.  Errors if
332    /// no functor is provided or if the first term is not an atom.
333    #[inline]
334    pub fn funcv(
335        arena: &mut Arena,
336        terms: impl IntoIterator<Item = impl IntoTerm>,
337    ) -> Result<Self, TermError> {
338        let mut terms = terms.into_iter();
339        let Some(functor_atom) = terms.next() else {
340            return Err(TermError::MissingFunctor);
341        };
342        let functor_atom = functor_atom.into_term(arena);
343        if !functor_atom.is_atom() {
344            return Err(TermError::InvalidFunctor(functor_atom));
345        }
346        let Some(first) = terms.next() else {
347            return Ok(functor_atom);
348        };
349        Ok(Self(Handle::FuncRef(arena.intern_func(
350            functor_atom,
351            std::iter::once(first).chain(terms),
352        ))))
353    }
354
355    /// Constructs a new list. A list is represented internally as an
356    /// array of terms. If `terms` is empty, returns `nil`.
357    #[inline]
358    pub fn list(arena: &mut Arena, terms: impl IntoIterator<Item = impl IntoTerm>) -> Self {
359        let mut terms = terms.into_iter();
360        let Some(first) = terms.next() else {
361            return Self::NIL;
362        };
363        Self(Handle::ListRef(
364            arena.intern_seq(std::iter::once(first).chain(terms)),
365        ))
366    }
367
368    /// Constructs a new improper list. An improper list is represented as
369    /// a list and additional argument. If `terms` is empty, returns `nil`.
370    #[inline]
371    pub fn listc(
372        arena: &mut Arena,
373        terms: impl IntoIterator<Item = impl IntoTerm>,
374        tail: impl IntoTerm,
375    ) -> Self {
376        let mut terms = terms.into_iter();
377        let Some(first) = terms.next() else {
378            return Self::NIL;
379        };
380        let tail = tail.into_term(arena);
381        if tail != Term::NIL {
382            Self(Handle::ListCRef(arena.intern_seq_plus_one(
383                std::iter::once(first).chain(terms),
384                tail,
385            )))
386        } else {
387            Self(Handle::ListRef(
388                arena.intern_seq(std::iter::once(first).chain(terms)),
389            ))
390        }
391    }
392
393    /// Constructs a new tuple. A tuple is represented internally as an array
394    /// of terms.
395    #[inline]
396    pub fn tuple(arena: &mut Arena, terms: impl IntoIterator<Item = impl IntoTerm>) -> Self {
397        let mut terms = terms.into_iter();
398        let Some(first) = terms.next() else {
399            return Self::UNIT;
400        };
401        Self(Handle::TupleRef(
402            arena.intern_seq(std::iter::once(first).chain(terms)),
403        ))
404    }
405
406    /// Constant representing the zero‑arity tuple (unit).  Internally
407    /// this is the atom `"unit"` encoded as a small atom.  It may
408    /// be copied freely and does not depend on any arena.
409    pub const UNIT: Self = {
410        let buf: [u8; 14] = [b'u', b'n', b'i', b't', 0, 0, 0, 0, 0, 0, 0, 0, 0, 0];
411        Self(Handle::Atom(TinyArray { bytes: buf, len: 4 }))
412    };
413
414    /// Constant representing the empty list (nil).  Internally this is
415    /// the atom `"nil"` encoded as a small atom.  It may be copied
416    /// freely and does not depend on any arena.
417    pub const NIL: Self = {
418        let buf: [u8; 14] = [b'n', b'i', b'l', 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0];
419        Self(Handle::Atom(TinyArray { bytes: buf, len: 3 }))
420    };
421
422    /// Returns the value if `term` is an integer, otherwise an error.
423    #[inline]
424    pub fn unpack_int(&self, arena: &Arena) -> Result<i64, TermError> {
425        arena.unpack_int(self)
426    }
427
428    /// Returns the value if `term` is a real, otherwise an error.
429    #[inline]
430    pub fn unpack_real(&self, arena: &Arena) -> Result<f64, TermError> {
431        arena.unpack_real(self)
432    }
433
434    /// Returns the value if `term` is a date, otherwise an error.
435    #[inline]
436    pub fn unpack_date(&self, arena: &Arena) -> Result<i64, TermError> {
437        arena.unpack_date(self)
438    }
439
440    /// Returns the string slice if `term` is a string, otherwise an error.
441    #[inline]
442    pub fn unpack_str<'a>(&'a self, arena: &'a Arena) -> Result<&'a str, TermError> {
443        arena.unpack_str(self)
444    }
445
446    /// Returns the slice if `term` is a binary blob, otherwise an error.
447    #[inline]
448    pub fn unpack_bin<'a>(&'a self, arena: &'a Arena) -> Result<&'a [u8], TermError> {
449        arena.unpack_bin(self)
450    }
451
452    /// Returns the name if `term` is an atom, otherwise an error.
453    #[inline]
454    pub fn unpack_atom<'a>(
455        &'a self,
456        arena: &'a Arena,
457        allowed_names: &[&str],
458    ) -> Result<&'a str, TermError> {
459        arena.unpack_atom(self, allowed_names)
460    }
461
462    /// Returns the name if `term` is a variable, otherwise an error.
463    #[inline]
464    pub fn unpack_var<'a>(
465        &'a self,
466        arena: &'a Arena,
467        allowed_names: &[&str],
468    ) -> Result<&'a str, TermError> {
469        arena.unpack_var(self, allowed_names)
470    }
471
472    /// Returns the name and arguments if `term` is a compound term of any arity
473    /// or an atom and its name is in `allowed_names` (or if `allowed_names` is empty),
474    /// otherwise returns an error.
475    #[inline]
476    pub fn unpack_func_any<'a>(
477        &'a self,
478        arena: &'a Arena,
479        allowed_names: &[&str],
480    ) -> Result<(&'a Term, &'a [Term]), TermError> {
481        arena.unpack_func_any(self, allowed_names)
482    }
483
484    /// Returns the name and arguments if `term` is a compound term of arity `ARITY`
485    /// (or an atom if `ARITY == 0`) and its name is in `allowed_names` (or if `allowed_names` is empty),
486    /// otherwise returns an error.
487    #[inline]
488    pub fn unpack_func<'a, const ARITY: usize>(
489        &'a self,
490        arena: &'a Arena,
491        allowed_names: &[&str],
492    ) -> Result<(&'a Term, [Term; ARITY]), TermError> {
493        arena.unpack_func(self, allowed_names)
494    }
495
496    /// Returns the slice with list elements and the tail if `term` is a list,
497    /// otherwise returns an error.
498    #[inline]
499    pub fn unpack_list<'a>(
500        &'a self,
501        arena: &'a Arena,
502    ) -> Result<(&'a [Term], &'a Term), TermError> {
503        arena.unpack_list(self)
504    }
505
506    /// Returns the slice with tuple elements if `term` is a tuple of any arity,
507    /// otherwise returns an error.
508    #[inline]
509    pub fn unpack_tuple_any<'a>(&'a self, arena: &'a Arena) -> Result<&'a [Term], TermError> {
510        arena.unpack_tuple_any(self)
511    }
512
513    /// Returns the tuple elements if `term` is a tuple of arity `ARITY`,
514    /// otherwise returns an error.
515    #[inline]
516    pub fn unpack_tuple<const ARITY: usize>(
517        &self,
518        arena: &Arena,
519    ) -> Result<[Term; ARITY], TermError> {
520        arena.unpack_tuple(self)
521    }
522
523    /// Returns `true` if the value fits directly in `Term` without arena storage,
524    /// i.e. `int`, `real`, `date`, or a small `atom`, `var`, `str`, or `bin`.
525    #[inline]
526    pub fn is_inline(&self) -> bool {
527        match &self.0 {
528            Handle::Int(_)
529            | Handle::Real(_)
530            | Handle::Date(_)
531            | Handle::Atom(_)
532            | Handle::Var(_)
533            | Handle::Str(_)
534            | Handle::Bin(_) => true,
535            Handle::AtomRef(_)
536            | Handle::VarRef(_)
537            | Handle::StrRef(_)
538            | Handle::BinRef(_)
539            | Handle::FuncRef(_)
540            | Handle::ListRef(_)
541            | Handle::ListCRef(_)
542            | Handle::TupleRef(_) => false,
543        }
544    }
545
546    /// Returns `true` if the term is a compound term.
547    #[inline]
548    pub fn is_func(&self) -> bool {
549        matches!(self.0, Handle::FuncRef(_))
550    }
551
552    /// Returns `true` if the term is a list.
553    #[inline]
554    pub fn is_list(&self) -> bool {
555        matches!(self.0, Handle::ListRef(_) | Handle::ListCRef(_)) || *self == Self::NIL
556    }
557
558    /// Returns `true` if the term is a tuple.
559    #[inline]
560    pub fn is_tuple(&self) -> bool {
561        matches!(self.0, Handle::TupleRef(_)) || *self == Self::UNIT
562    }
563
564    /// Returns `true` if the term is an integer.
565    #[inline]
566    pub fn is_int(&self) -> bool {
567        matches!(self.0, Handle::Int(_))
568    }
569
570    /// Returns `true` if the term is a real (floating-point) number.
571    #[inline]
572    pub fn is_real(&self) -> bool {
573        matches!(self.0, Handle::Real(_))
574    }
575
576    /// Returns `true` if the term is a date.
577    #[inline]
578    pub fn is_date(&self) -> bool {
579        matches!(self.0, Handle::Date(_))
580    }
581
582    /// Returns `true` if the term is an atom.
583    #[inline]
584    pub fn is_atom(&self) -> bool {
585        matches!(self.0, Handle::Atom(_) | Handle::AtomRef(_))
586    }
587
588    /// Returns `true` if the term is a variable.
589    #[inline]
590    pub fn is_var(&self) -> bool {
591        matches!(self.0, Handle::Var(_) | Handle::VarRef(_))
592    }
593
594    /// Returns `true` if the term is a number (`int`, `real`, or `date`).
595    #[inline]
596    pub fn is_number(&self) -> bool {
597        matches!(self.0, Handle::Int(_) | Handle::Real(_) | Handle::Date(_))
598    }
599
600    /// Returns `true` if the term is a string.
601    #[inline]
602    pub fn is_str(&self) -> bool {
603        matches!(self.0, Handle::Str(_) | Handle::StrRef(_))
604    }
605
606    /// Returns `true` if the term is a binary blob.
607    #[inline]
608    pub fn is_bin(&self) -> bool {
609        matches!(self.0, Handle::Bin(_) | Handle::BinRef(_))
610    }
611
612    /// Returns the arity of the term. Currently the arity of lists and variables is 0.
613    #[inline]
614    pub fn arity(&self) -> usize {
615        match &self.0 {
616            Handle::Atom(_)
617            | Handle::AtomRef(_)
618            | Handle::Int(_)
619            | Handle::Real(_)
620            | Handle::Date(_)
621            | Handle::Str(_)
622            | Handle::StrRef(_)
623            | Handle::Bin(_)
624            | Handle::BinRef(_) => 0,
625            Handle::FuncRef(Slice { len: n, .. }) => (n - 1) as usize,
626            Handle::TupleRef(Slice { len: n, .. }) => *n as usize,
627            Handle::ListRef(_) | Handle::ListCRef(_) | Handle::Var(_) | Handle::VarRef(_) => 0,
628        }
629    }
630
631    /// Returns the name of a compound term, atom, or variable.
632    /// Use [`atom_name`], [`func_name`], or [`var_name`]
633    /// to ensure the term is of a specific kind.
634    #[inline]
635    pub fn name<'a>(&'a self, arena: &'a Arena) -> Result<&'a str, TermError> {
636        arena.name(self)
637    }
638
639    /// Returns the name of an atom,
640    #[inline]
641    pub fn atom_name<'a>(&'a self, arena: &'a Arena) -> Result<&'a str, TermError> {
642        arena.unpack_atom(self, &[])
643    }
644
645    /// Returns the name of a variable.
646    #[inline]
647    pub fn var_name<'a>(&'a self, arena: &'a Arena) -> Result<&'a str, TermError> {
648        arena.unpack_var(self, &[])
649    }
650
651    /// Returns the name of a compund term.
652    #[inline]
653    pub fn func_name<'a>(&'a self, arena: &'a Arena) -> Result<&'a str, TermError> {
654        let (functor, _) = arena.unpack_func_any(self, &[])?;
655        arena.atom_name(functor)
656    }
657
658    /// Returns a string describing the kind of this term.
659    #[inline]
660    pub fn kind_name(&self) -> &'static str {
661        match &self.0 {
662            Handle::Int(_) => "int",
663            Handle::Real(_) => "real",
664            Handle::Date(_) => "date",
665            Handle::Var(_) | Handle::VarRef(_) => "var",
666            Handle::Atom(_) | Handle::AtomRef(_) => "atom",
667            Handle::Str(_) | Handle::StrRef(_) => "str",
668            Handle::Bin(_) | Handle::BinRef(_) => "bin",
669            Handle::FuncRef(_) => "func",
670            Handle::ListRef(_) | Handle::ListCRef(_) => "list",
671            Handle::TupleRef(_) => "tuple",
672        }
673    }
674}
675
676/// Implements the standard [`Debug`] formatter for [`Term`].
677///
678/// This prints a developer-friendly representation of the term,
679/// showing its kind (e.g. `int`, `atom`, `list`, `tuple`) and
680/// its internal value in a form useful for debugging.  
681///
682/// The output is not guaranteed to be stable across versions and
683/// should not be parsed; it is intended purely for diagnostics
684/// and logging.
685///
686/// # Example
687/// ```rust
688/// # use arena_terms::Term;
689/// let t = Term::int(42);
690/// println!("{:?}", t); // e.g. prints `Int(42)`
691/// ```
692impl fmt::Debug for Term {
693    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
694        match &self.0 {
695            Handle::Int(i) => f.debug_tuple("Int").field(i).finish(),
696            Handle::Real(r) => f.debug_tuple("Real").field(r).finish(),
697            Handle::Date(d) => f.debug_tuple("Date").field(d).finish(),
698            Handle::Var(v) => {
699                let name =
700                    core::str::from_utf8(&v.bytes[..v.len as usize]).unwrap_or("<invalid utf8>");
701                f.debug_struct("Var").field("name", &name).finish()
702            }
703            Handle::VarRef(v) => f
704                .debug_struct("VarRef")
705                .field("epoch_id", &v.epoch_id)
706                .field("index", &v.index)
707                .field("len", &v.len)
708                .finish(),
709            Handle::Atom(a) => {
710                let name =
711                    core::str::from_utf8(&a.bytes[..a.len as usize]).unwrap_or("<invalid utf8>");
712                f.debug_struct("Atom").field("name", &name).finish()
713            }
714            Handle::AtomRef(v) => f
715                .debug_struct("AtomRef")
716                .field("epoch_id", &v.epoch_id)
717                .field("index", &v.index)
718                .field("len", &v.len)
719                .finish(),
720            Handle::Str(s) => {
721                let value =
722                    core::str::from_utf8(&s.bytes[..s.len as usize]).unwrap_or("<invalid utf8>");
723                f.debug_struct("Str").field("value", &value).finish()
724            }
725            Handle::StrRef(v) => f
726                .debug_struct("StrRef")
727                .field("epoch_id", &v.epoch_id)
728                .field("index", &v.index)
729                .field("len", &v.len)
730                .finish(),
731            Handle::Bin(b) => {
732                let slice = &b.bytes[..b.len as usize];
733                f.debug_struct("Bin").field("bytes", &slice).finish()
734            }
735            Handle::BinRef(v) => f
736                .debug_struct("BinRef")
737                .field("epoch_id", &v.epoch_id)
738                .field("index", &v.index)
739                .field("len", &v.len)
740                .finish(),
741            Handle::FuncRef(v) => f
742                .debug_struct("Func")
743                .field("epoch_id", &v.epoch_id)
744                .field("index", &v.index)
745                .field("len", &v.len)
746                .finish(),
747            Handle::ListRef(v) => f
748                .debug_struct("List")
749                .field("epoch_id", &v.epoch_id)
750                .field("index", &v.index)
751                .field("len", &v.len)
752                .finish(),
753            Handle::ListCRef(v) => f
754                .debug_struct("ListC")
755                .field("epoch_id", &v.epoch_id)
756                .field("index", &v.index)
757                .field("len", &v.len)
758                .finish(),
759            Handle::TupleRef(v) => f
760                .debug_struct("Tuple")
761                .field("epoch_id", &v.epoch_id)
762                .field("index", &v.index)
763                .field("len", &v.len)
764                .finish(),
765        }
766    }
767}
768
769/// Convenience macros to construct func, list and tuple.
770#[macro_export]
771macro_rules! list {
772    // with tail, explicit arena
773    ($($arg:expr),* $(,)?; $tail:expr => $arena:expr) => {
774        $crate::list!($($arg),* ; $tail)($arena)
775    };
776    // without tail, explicit arena
777    ($($arg:expr),* $(,)? => $arena:expr) => {
778        $crate::list!($($arg),*)($arena)
779    };
780    // with tail, implicit arena
781    ($($arg:expr),* $(,)?; $tail:expr) => { (|__arena: &mut $crate::Arena| {
782        let __args: &[$crate::Term] = &[$($arg.into_term(__arena)),*];
783        let __tail: Term = $tail.into_term(__arena);
784        __arena.listc(__args, __tail)
785    })};
786    // without tail, implicit arena
787    ($($arg:expr),* $(,)?) => { (|__arena: &mut $crate::Arena| {
788        let __args: &[$crate::Term] = &[$($arg.into_term(__arena)),*];
789        __arena.list(__args)
790    })};
791}
792
793#[macro_export]
794macro_rules! tuple {
795    // explicit arena
796    ($($arg:expr),* $(,)? => $arena:expr) => {
797        $crate::tuple!($($arg),*)($arena)
798    };
799    // implicit arena
800    ($($arg:expr),* $(,)?) => { (|__arena: &mut $crate::Arena| {
801        let __args: &[$crate::Term] = &[$($arg.into_term(__arena)),*];
802        __arena.tuple(__args)
803    })};
804}
805
806#[macro_export]
807macro_rules! func {
808    // explicit arena
809    ($functor:expr; $($arg:expr),+ $(,)? => $arena:expr) => {
810        $crate::func!($functor; $($arg),+)($arena)
811    };
812    // implicit arena
813    ($functor:expr; $($arg:expr),+ $(,)?) => { (|__arena: &mut $crate::Arena| {
814        let __args: &[$crate::Term] = &[$($arg.into_term(__arena)),+];
815        __arena.func($functor, __args)
816    })};
817}
818
819#[macro_export]
820macro_rules! atom {
821    // explicit arena
822    ($functor:expr => $arena:expr) => {
823        $crate::atom!($functor)($arena)
824    };
825    // implicit arena
826    ($functor:expr) => {
827        (|__arena: &mut $crate::Arena| __arena.atom($functor))
828    };
829}
830
831#[macro_export]
832macro_rules! var {
833    // explicit arena
834    ($name:expr => $arena:expr) => {
835        $crate::var!($name)($arena)
836    };
837    // implicit arena
838    ($name:expr) => {
839        (|__arena: &mut $crate::Arena| __arena.var($name))
840    };
841}
842
843#[macro_export]
844macro_rules! date {
845    ($value:expr) => {
846        $crate::Term::date($value)
847    };
848}
849
850#[macro_export]
851macro_rules! unit {
852    () => {
853        $crate::Term::UNIT
854    };
855}
856
857#[macro_export]
858macro_rules! nil {
859    () => {
860        $crate::Term::NIL
861    };
862}
863
864#[cfg(test)]
865mod tests {
866    use super::*;
867    use crate::View;
868    use std::fmt::Write;
869
870    #[test]
871    fn term_size_is_16_bytes() {
872        assert_eq!(core::mem::size_of::<Term>(), 16);
873    }
874
875    #[test]
876    fn option_term_size_is_16_bytes() {
877        assert_eq!(core::mem::size_of::<Option<Term>>(), 16);
878    }
879
880    #[test]
881    fn small_atom_interning() {
882        let mut arena = Arena::new();
883        let a1 = Term::atom(&mut arena, "foo");
884        let a2 = Term::atom(&mut arena, "foo");
885        assert_eq!(a1, a2);
886        if let Ok(View::Atom(name)) = a1.view(&arena) {
887            assert_eq!(name, "foo");
888        } else {
889            panic!("wrong view");
890        }
891    }
892
893    #[test]
894    fn compound_construction_and_formatting() {
895        let mut arena = Arena::new();
896        let a = Term::int(1);
897        let b = Term::real(2.0);
898        let c = Term::date(1000);
899        let d = Term::atom(&mut arena, "hello");
900        let e = Term::var(&mut arena, "Hello");
901        let f = Term::str(&mut arena, "A str\ning. Longer string.");
902        let g = list![d, e, f => &mut arena];
903        let h = tuple!(f, f => &mut arena);
904        let p = Term::func(&mut arena, "point", &[a, b, c, d, e, f, g, h]);
905        let p = func![
906            "foo";
907            Term::NIL,
908            Term::UNIT,
909            p,
910            p,
911            list![],
912            list![a, b; c],
913            => &mut arena
914        ];
915        dbg!(&p);
916        dbg!(p.view(&arena).unwrap());
917        dbg!(arena.stats());
918        assert!(p.is_func());
919        if let Ok(View::Func(_, functor, args)) = p.view(&arena) {
920            assert_eq!(functor.atom_name(&arena).unwrap(), "foo");
921            assert_eq!(p.arity(), 6);
922            assert_eq!(args.len(), 6);
923        } else {
924            panic!("unexpected view");
925        }
926
927        let s = format!("{}", p.display(&arena));
928        assert_eq!(
929            s,
930            "foo(nil, unit, point(1, 2.0, date{1970-01-01T00:00:01+00:00}, hello, Hello, \"A str\\ning. Longer string.\", [hello, Hello, \"A str\\ning. Longer string.\"], (\"A str\\ning. Longer string.\", \"A str\\ning. Longer string.\")), point(1, 2.0, date{1970-01-01T00:00:01+00:00}, hello, Hello, \"A str\\ning. Longer string.\", [hello, Hello, \"A str\\ning. Longer string.\"], (\"A str\\ning. Longer string.\", \"A str\\ning. Longer string.\")), nil, [1, 2.0 | date{1970-01-01T00:00:01+00:00}])"
931        );
932    }
933
934    #[test]
935    fn view_construction() {
936        let mut a1 = Arena::new();
937        let x = a1.atom("Hello, hello, quite long long string, world! X");
938        dbg!(a1.view(&x).unwrap());
939        dbg!(a1.stats());
940        let p = list![x, x => &mut a1];
941        dbg!(p);
942        let v = a1.view(&p).unwrap();
943        dbg!(v);
944    }
945
946    #[test]
947    #[should_panic]
948    fn arena_mismatch() {
949        let a1 = Arena::new();
950        let mut a2 = Arena::new();
951        let y = a2.str("Hello, hello, quite long long string, world! Y");
952        dbg!(a1.view(&y).unwrap());
953    }
954
955    #[test]
956    #[should_panic]
957    fn stale_term_str() {
958        let mut a = Arena::new();
959        let x = a.str("Hello, hello, quite long long string, world! Y");
960        dbg!(&a);
961        a.truncate(a.current_epoch()).unwrap();
962        dbg!(a.view(&x).unwrap());
963    }
964
965    #[test]
966    #[should_panic]
967    fn stale_term_list() {
968        let mut a = Arena::new();
969        let _x = list![1, 2, 3 => &mut a];
970        let epoch = a.begin_epoch().unwrap();
971        dbg!(&epoch);
972        let y = list![4, 5, 6 => &mut a];
973        dbg!(&a);
974        a.truncate(epoch).unwrap();
975        dbg!(&a);
976        dbg!(a.view(&y).unwrap());
977    }
978
979    #[test]
980    fn big_term() {
981        let mut a1 = Arena::new();
982        let x = a1.atom("Hello, hello, quite long long string, world! X");
983        let p = a1.func("foo", vec![x; 1_000_000]);
984        assert!(p.arity() == 1_000_000);
985        dbg!(a1.stats());
986    }
987
988    #[test]
989    fn interface() {
990        let a = &mut Arena::new();
991        let s = String::from("x");
992        let x1 = a.func(&s, &vec![Term::date(1000)]);
993        let x2 = a.func(s.as_str(), vec![Term::date(1000)]);
994        let x3 = a.func(s, &[Term::date(1000)]);
995        let _x4 = a.func("x", [Term::date(1000)]);
996        let _x5 = a.func("x", [x1, x2, x3]);
997        let _x6 = a.func("x", (5..=6).map(|x| x as f64));
998        let _x7 = a.func("x", vec![&x1, &x2, &x3]);
999        let _x8 = a.func("x", &[x1, x2, x3]);
1000        let x9 = func!(
1001            String::from("aaa");
1002            x1, 1u8, 1i8, 2.0,
1003            "x",
1004            "X",
1005            atom!("ATOM"),
1006            var!("var"),
1007            "a string",
1008            b"a binary",
1009            1,
1010            2,
1011            3,
1012            4,
1013            6,
1014            unit!(),
1015            list![1, 2, 3; tuple!()],
1016            list![1, 2, 3; nil!()],
1017            => a
1018        );
1019        dbg!(a.view(&x9).unwrap());
1020        dbg!(a.stats());
1021    }
1022
1023    #[test]
1024    fn into_test() {
1025        let mut arena = Arena::new();
1026        // You can mix numbers and strings; IntoTerm will pick the right constructor.
1027        let t1 = arena.term(1);
1028        let t2 = arena.term(2.0);
1029        let t3 = arena.term("x");
1030        let t4 = arena.term(b"bin" as &[u8]);
1031        let point1 = arena.func("point", [t1, t2, t3, t4]);
1032        // Equivalent to:
1033        let t1 = Term::int(1);
1034        let t2 = Term::real(2.0);
1035        let t3 = Term::str(&mut arena, "x");
1036        let t4 = Term::bin(&mut arena, b"bin");
1037        let point2 = arena.func("point", [t1, t2, t3, t4]);
1038        assert_eq!(arena.view(&point1).unwrap(), arena.view(&point2).unwrap());
1039        dbg!(arena.view(&point1).unwrap());
1040
1041        // You can also provide closures returning Term.
1042        let lazy = Term::func(&mut arena, "lazy", [|arena: &mut Arena| arena.atom("ok")]);
1043        dbg!(arena.view(&lazy).unwrap());
1044
1045        let list = arena.list([1, 2, 3]);
1046        dbg!(arena.view(&list).unwrap());
1047    }
1048
1049    #[test]
1050    fn arena_truncate_test() {
1051        let a = &mut Arena::new();
1052
1053        let t1 = a.str("a".repeat(1000));
1054        let _t5 = atom!("x".repeat(100) => a);
1055        let _t6 = var!("X".repeat(200) => a);
1056        let _t7 = a.bin(b"x".repeat(5000));
1057        let epoch1 = a.begin_epoch().unwrap();
1058        dbg!(a.stats());
1059        dbg!(&epoch1);
1060        let t2 = a.str("b".repeat(2000));
1061        let t3 = a.bin(b"b".repeat(3000));
1062        let _t4 = list![t1, t2, t3];
1063        let _t5 = atom!("z".repeat(4000) => a);
1064        let _t8 = var!("Z".repeat(2000) => a);
1065        let _t7 = a.bin(b"z".repeat(10_000));
1066        let epoch2 = a.begin_epoch().unwrap();
1067        dbg!(a.stats());
1068        dbg!(&epoch2);
1069        a.truncate(epoch2).unwrap();
1070        dbg!(a.stats());
1071    }
1072
1073    #[test]
1074    fn funcv() {
1075        let a = &mut Arena::new();
1076        let xs = [a.atom("foo"), a.atom("x"), a.atom("y")];
1077        let x = a.funcv(xs).unwrap();
1078        let ys = [a.atom("x"), a.atom("y")];
1079        let y = a.func("foo", ys);
1080        assert_eq!(x.arity(), y.arity());
1081        if let Ok(View::Func(_, functor, args)) = x.view(&a) {
1082            assert_eq!(functor.name(a).unwrap(), "foo");
1083            assert_eq!(args.len(), 2);
1084        }
1085        if let Ok(View::Func(_, functor, args)) = y.view(&a) {
1086            assert_eq!(functor.name(a).unwrap(), "foo");
1087            assert_eq!(args.len(), 2);
1088        }
1089    }
1090
1091    #[test]
1092    fn unpack() {
1093        let a = &mut Arena::new();
1094        let xs = [a.atom("foo"), a.atom("x"), a.atom("y")];
1095        let x = a.funcv(xs).unwrap();
1096
1097        let (foo, [x, y]) = x.unpack_func(a, &["foo", "boo"]).unwrap();
1098        dbg!((foo, x, y));
1099
1100        let z = tuple!(1 => a);
1101        assert_eq!(z.arity(), 1);
1102    }
1103
1104    #[test]
1105    fn arity_primitives_and_lists_are_zero() {
1106        let a = &mut Arena::new();
1107
1108        let t_int = Term::int(42);
1109        let t_real = Term::real(3.14);
1110        let t_atom = Term::atom(a, "ok");
1111        let t_var = Term::var(a, "X");
1112        let t_str = Term::str(a, "hello");
1113        let t_bin = Term::bin(a, &[1, 2, 3, 4]);
1114        let t_list = Term::list(a, &[Term::int(1), Term::int(2), Term::int(3)]);
1115
1116        assert_eq!(t_int.arity(), 0);
1117        assert_eq!(t_real.arity(), 0);
1118        assert_eq!(t_atom.arity(), 0);
1119        assert_eq!(t_var.arity(), 0);
1120        assert_eq!(t_str.arity(), 0);
1121        assert_eq!(t_bin.arity(), 0);
1122        assert_eq!(t_list.arity(), 0); // lists are 0 by current implementation
1123    }
1124
1125    #[test]
1126    fn arity_for_tuples_and_funcs() {
1127        let a = &mut Arena::new();
1128
1129        let t2 = Term::tuple(a, &[Term::int(1), Term::int(2)]);
1130        let t3 = Term::tuple(a, &[Term::int(1), Term::int(2), Term::int(3)]);
1131        assert_eq!(t2.arity(), 2);
1132        assert_eq!(t3.arity(), 3);
1133
1134        let f0 = Term::func(a, "nilary", &[] as &[Term]); // creates an atom `nilary`
1135        let f2 = Term::func(a, "pair", &[Term::int(1), Term::int(2)]);
1136        let f3 = Term::func(a, "triple", &[Term::int(1), Term::int(2), Term::int(3)]);
1137
1138        assert_eq!(f0.arity(), 0);
1139        assert_eq!(f2.arity(), 2);
1140        assert_eq!(f3.arity(), 3);
1141    }
1142
1143    #[test]
1144    fn name_and_kind_name() {
1145        let a = &mut Arena::new();
1146
1147        let atom = Term::atom(a, "foo");
1148        let var = Term::var(a, "X");
1149        let fun = Term::func(a, "bar", &[Term::int(1)]);
1150        let tup = Term::tuple(a, &[Term::int(1), Term::int(2)]);
1151        let lst = Term::list(a, &[Term::int(1), Term::int(2), Term::int(3)]);
1152
1153        // `name()` should resolve atom/var names and compound heads.
1154        assert_eq!(atom.name(&a).unwrap(), "foo");
1155        assert_eq!(var.name(&a).unwrap(), "X");
1156        assert_eq!(fun.name(&a).unwrap(), "bar");
1157
1158        // `kind_name()` should report stable kind strings
1159        assert_eq!(atom.kind_name(), "atom");
1160        assert_eq!(var.kind_name(), "var");
1161        assert_eq!(fun.kind_name(), "func");
1162        assert_eq!(tup.kind_name(), "tuple");
1163        assert_eq!(lst.kind_name(), "list");
1164        assert_eq!(Term::int(7).kind_name(), "int");
1165        assert_eq!(Term::str(a, "s").kind_name(), "str");
1166    }
1167
1168    #[test]
1169    fn is_func_tuple_list() {
1170        let a = &mut Arena::new();
1171
1172        let f2 = Term::func(a, "pair", &[Term::int(1), Term::int(2)]);
1173        let tup = Term::tuple(a, &[Term::int(1), Term::int(2)]);
1174        let lst = Term::list(a, &[Term::int(1), Term::int(2), Term::int(3)]);
1175
1176        assert!(f2.is_func());
1177        assert!(tup.is_tuple());
1178        assert!(lst.is_list());
1179
1180        assert!(!f2.is_tuple());
1181        assert!(!f2.is_list());
1182        assert!(!tup.is_func());
1183        assert!(!lst.is_func());
1184        assert!(!lst.is_tuple());
1185    }
1186
1187    #[test]
1188    fn is_inline_obvious_cases() {
1189        let a = &mut Arena::new();
1190
1191        let i = Term::int(42);
1192        let f0 = Term::func(a, "nilary", &[] as &[Term]);
1193        let tup = Term::tuple(a, &[Term::int(1), Term::int(2)]);
1194        let lst = Term::list(a, &[Term::int(1), Term::int(2)]);
1195
1196        // Obvious truthy/falsey cases that don't depend on small/large thresholds.
1197        assert!(i.is_inline());
1198        assert!(f0.is_inline()); // f0 is an atom
1199        assert!(!tup.is_inline());
1200        assert!(!lst.is_inline());
1201    }
1202
1203    #[test]
1204    fn is_atom_var_str_bin_number_minimal() {
1205        let a = &mut Arena::new();
1206
1207        let at = Term::atom(a, "foo");
1208        let vr = Term::var(a, "X");
1209        let st = Term::str(a, "hi");
1210        let bi = Term::bin(a, &[1, 2, 3, 4]);
1211        let i = Term::int(7);
1212
1213        assert!(at.is_atom());
1214        assert!(vr.is_var());
1215        assert!(st.is_str());
1216        assert!(bi.is_bin());
1217
1218        assert!(!at.is_var());
1219        assert!(!vr.is_atom());
1220        assert!(!st.is_bin());
1221        assert!(!bi.is_str());
1222
1223        // is_number is true for ints (and, by spec, also for real/date—tested here with int).
1224        assert!(i.is_number());
1225        assert!(!at.is_number());
1226        assert!(!st.is_number());
1227        assert!(!bi.is_number());
1228    }
1229
1230    #[test]
1231    fn nil_and_tuple_edge_behavior() {
1232        let a = &mut Arena::new();
1233
1234        // NIL should count as a list per your is_list() (*self == Self::NIL)
1235        assert!(Term::NIL.is_list());
1236        assert!(!Term::NIL.is_tuple());
1237        assert!(!Term::NIL.is_func());
1238        assert_eq!(Term::NIL.arity(), 0);
1239
1240        // Regular tuple is a tuple; arity equals length.
1241        let t = Term::tuple(a, &[Term::int(1), Term::int(2), Term::int(3)]);
1242        assert!(t.is_tuple());
1243        assert_eq!(t.arity(), 3);
1244        assert!(!t.is_list());
1245        assert!(!t.is_func());
1246    }
1247
1248    #[test]
1249    fn arity_consistency_with_predicates() {
1250        let a = &mut Arena::new();
1251
1252        let f3 = Term::func(a, "triple", &[Term::int(1), Term::int(2), Term::int(3)]);
1253        let t2 = Term::tuple(a, &[Term::int(1), Term::int(2)]);
1254        let l2 = Term::list(a, &[Term::int(1), Term::int(2)]);
1255        let v = Term::var(a, "X");
1256
1257        assert!(f3.is_func());
1258        assert_eq!(f3.arity(), 3);
1259
1260        assert!(t2.is_tuple());
1261        assert_eq!(t2.arity(), 2);
1262
1263        assert!(l2.is_list());
1264        assert_eq!(l2.arity(), 0); // lists are defined as 0-arity
1265
1266        assert!(v.is_var());
1267        assert_eq!(v.arity(), 0); // variables are 0-arity
1268    }
1269
1270    #[test]
1271    fn name_and_kind_name_roundtrip() {
1272        let a = &mut Arena::new();
1273
1274        let atom = Term::atom(a, "foo");
1275        let var = Term::var(a, "X");
1276        let fun = Term::func(a, "bar", &[Term::int(1)]);
1277        let tup = Term::tuple(a, &[Term::int(1), Term::int(2)]);
1278        let lst = Term::list(a, &[Term::int(1), Term::int(2), Term::int(3)]);
1279
1280        // name(): atoms, variables, and compound heads resolve
1281        assert_eq!(atom.name(&a).unwrap(), "foo");
1282        assert_eq!(var.name(&a).unwrap(), "X");
1283        assert_eq!(fun.name(&a).unwrap(), "bar");
1284
1285        // kind_name(): stable kind labels
1286        assert_eq!(atom.kind_name(), "atom");
1287        assert_eq!(var.kind_name(), "var");
1288        assert_eq!(fun.kind_name(), "func");
1289        assert_eq!(tup.kind_name(), "tuple");
1290        assert_eq!(lst.kind_name(), "list");
1291        assert_eq!(Term::int(7).kind_name(), "int");
1292        assert_eq!(Term::str(a, "s").kind_name(), "str");
1293    }
1294
1295    #[test]
1296    fn unpack_primitives_ok() {
1297        let a = &mut Arena::new();
1298
1299        let t_int = Term::int(42);
1300        let t_real = Term::real(3.5);
1301        let t_date = Term::date(2);
1302        let t_str = Term::str(a, "hello");
1303        let t_bin = Term::bin(a, &[1u8, 2, 3, 4]);
1304
1305        assert_eq!(t_int.unpack_int(a).unwrap(), 42);
1306        assert!((t_real.unpack_real(a).unwrap() - 3.5).abs() < f64::EPSILON);
1307        assert_eq!(t_date.unpack_date(a).unwrap(), 2);
1308
1309        assert_eq!(t_str.unpack_str(a).unwrap(), "hello");
1310        assert_eq!(t_bin.unpack_bin(a).unwrap(), &[1, 2, 3, 4]);
1311    }
1312
1313    #[test]
1314    fn unpack_primitives_wrong_type_errs() {
1315        let a = &mut Arena::new();
1316
1317        let not_int = Term::str(a, "nope");
1318        let not_real = Term::int(1);
1319        let not_date = Term::str(a, "2024-01-02");
1320
1321        assert!(not_int.unpack_int(a).is_err());
1322        assert!(not_real.unpack_real(a).is_err());
1323        assert!(not_date.unpack_date(a).is_err());
1324
1325        let not_str = Term::int(5);
1326        let not_bin = Term::str(a, "bytes");
1327        assert!(not_str.unpack_str(a).is_err());
1328        assert!(not_bin.unpack_bin(a).is_err());
1329    }
1330
1331    #[test]
1332    fn unpack_atom_and_var_with_allowed_names() {
1333        let a = &mut Arena::new();
1334
1335        let at_ok = Term::atom(a, "foo");
1336        let at_no = Term::atom(a, "bar");
1337        let vr_ok = Term::var(a, "X");
1338        let vr_no = Term::var(a, "Y");
1339
1340        // Allowed lists
1341        let allowed_atoms = ["foo", "baz"];
1342        let allowed_vars = ["X", "Z"];
1343
1344        assert_eq!(at_ok.unpack_atom(a, &allowed_atoms).unwrap(), "foo");
1345        assert!(at_no.unpack_atom(a, &allowed_atoms).is_err());
1346
1347        assert_eq!(vr_ok.unpack_var(a, &allowed_vars).unwrap(), "X");
1348        assert!(vr_no.unpack_var(a, &allowed_vars).is_err());
1349
1350        // Empty allowed_names means "any"
1351        assert_eq!(at_no.name(a).unwrap(), "bar");
1352        assert_eq!(vr_no.var_name(a).unwrap(), "Y");
1353    }
1354
1355    #[test]
1356    fn unpack_func_any_and_arity_specific() {
1357        let a = &mut Arena::new();
1358
1359        let f0 = Term::func(a, "nilary", &[] as &[Term]);
1360        let f2 = Term::func(a, "pair", &[Term::int(1), Term::int(2)]);
1361        let f3 = Term::func(a, "triple", &[Term::int(1), Term::int(2), Term::int(3)]);
1362
1363        // Any arity, name filtering
1364        {
1365            let (name, args) = f2.unpack_func_any(a, &["pair", "other"]).unwrap();
1366            assert_eq!(name.name(a).unwrap(), "pair");
1367            assert_eq!(args.len(), 2);
1368            assert_eq!(args[0].unpack_int(a).unwrap(), 1);
1369            assert_eq!(args[1].unpack_int(a).unwrap(), 2);
1370
1371            // empty allowed_names accepts anything
1372            let (name0, args0) = f0.unpack_func_any(a, &[]).unwrap();
1373            assert_eq!(name0.name(a).unwrap(), "nilary");
1374            assert!(args0.is_empty());
1375
1376            // disallowed name should error
1377            assert!(f3.unpack_func_any(a, &["not_triple"]).is_err());
1378        }
1379
1380        // Fixed arity (const generic)
1381        {
1382            let (name2, [x, y]) = f2.unpack_func(a, &["pair"]).unwrap();
1383            assert_eq!(name2.name(a).unwrap(), "pair");
1384            assert_eq!(x.unpack_int(a).unwrap(), 1);
1385            assert_eq!(y.unpack_int(a).unwrap(), 2);
1386
1387            // Arity mismatch should error
1388            assert!(f3.unpack_func::<2>(a, &["triple"]).is_err());
1389
1390            // Name not allowed should error
1391            assert!(f2.unpack_func::<2>(a, &["other"]).is_err());
1392        }
1393    }
1394
1395    #[test]
1396    fn unpack_list_proper_and_tail() {
1397        let a = &mut Arena::new();
1398
1399        let l0 = Term::list(a, &[] as &[Term]);
1400        let (elems0, tail0) = l0.unpack_list(a).unwrap();
1401        assert!(elems0.is_empty());
1402        assert_eq!(*tail0, Term::NIL);
1403
1404        let l3 = Term::list(a, &[Term::int(1), Term::int(2), Term::int(3)]);
1405        let (elems3, tail3) = l3.unpack_list(a).unwrap();
1406        assert_eq!(elems3.len(), 3);
1407        assert_eq!(elems3[0].unpack_int(a).unwrap(), 1);
1408        assert_eq!(elems3[1].unpack_int(a).unwrap(), 2);
1409        assert_eq!(elems3[2].unpack_int(a).unwrap(), 3);
1410        assert_eq!(tail3, &Term::NIL);
1411
1412        // Non-list should error
1413        let not_list = Term::int(9);
1414        assert!(not_list.unpack_list(a).is_err());
1415    }
1416
1417    #[test]
1418    fn unpack_tuple_any_and_fixed() {
1419        let a = &mut Arena::new();
1420
1421        let t0 = Term::tuple(a, &[] as &[Term]);
1422        let t3 = Term::tuple(a, &[Term::int(1), Term::int(2), Term::int(3)]);
1423
1424        // any arity
1425        let elems0 = t0.unpack_tuple_any(a).unwrap();
1426        assert!(elems0.is_empty());
1427
1428        let elems3 = t3.unpack_tuple_any(a).unwrap();
1429        assert_eq!(elems3.len(), 3);
1430        assert_eq!(elems3[0].unpack_int(a).unwrap(), 1);
1431        assert_eq!(elems3[1].unpack_int(a).unwrap(), 2);
1432        assert_eq!(elems3[2].unpack_int(a).unwrap(), 3);
1433
1434        // fixed arity
1435        let arr3 = t3.unpack_tuple::<3>(a).unwrap();
1436        assert_eq!(arr3[0].unpack_int(a).unwrap(), 1);
1437        assert_eq!(arr3[1].unpack_int(a).unwrap(), 2);
1438        assert_eq!(arr3[2].unpack_int(a).unwrap(), 3);
1439
1440        // wrong arity should error
1441        assert!(t3.unpack_tuple::<2>(a).is_err());
1442
1443        // non-tuple should error
1444        assert!(Term::int(1).unpack_tuple_any(a).is_err());
1445        assert!(Term::int(1).unpack_tuple::<0>(a).is_err());
1446    }
1447
1448    #[test]
1449    fn unpack_atom_var_wrong_type_errs() {
1450        let a = &mut Arena::new();
1451
1452        let not_atom = Term::int(1);
1453        let not_var = Term::str(a, "X");
1454        assert!(not_atom.atom_name(a).is_err());
1455        assert!(not_var.var_name(a).is_err());
1456    }
1457
1458    #[test]
1459    fn unpack_func_wrong_type_errs() {
1460        let a = &mut Arena::new();
1461
1462        // tuple is not a func
1463        let tup = Term::tuple(a, &[Term::int(1), Term::int(2)]);
1464        assert!(tup.func_name(a).is_err());
1465        assert!(tup.unpack_func::<2>(a, &[]).is_err());
1466
1467        // atom can be unpacked with unpack_func* functions
1468        let at = Term::atom(a, "f");
1469        assert!(!at.unpack_func_any(a, &[]).is_err());
1470        assert!(!at.unpack_func::<0>(a, &[]).is_err());
1471    }
1472
1473    #[test]
1474    fn fmt_nil_to_string() {
1475        let arena = Arena::new();
1476        let t = Term::NIL;
1477        assert_eq!(t.display(&arena).to_string(), "nil");
1478    }
1479
1480    #[test]
1481    fn fmt_unit_format_macro() {
1482        let arena = Arena::new();
1483        let t = Term::UNIT;
1484        assert_eq!(format!("{}", t.display(&arena)), "unit");
1485    }
1486
1487    #[test]
1488    fn fmt_int_positive() {
1489        let arena = Arena::new();
1490        let t = Term::int(42);
1491        assert_eq!(format!("{}", t.display(&arena)), "42");
1492    }
1493
1494    #[test]
1495    fn fmt_int_negative_to_string() {
1496        let arena = Arena::new();
1497        let t = Term::int(-9001);
1498        assert_eq!(t.display(&arena).to_string(), "-9001");
1499    }
1500
1501    #[test]
1502    fn fmt_str_quotes() {
1503        let mut arena = Arena::new();
1504        let t = Term::str(&mut arena, "hello");
1505        assert_eq!(format!("{}", t.display(&arena)), r#""hello""#);
1506    }
1507
1508    #[test]
1509    fn fmt_str_with_escape_chars() {
1510        let mut arena = Arena::new();
1511        let t = Term::str(&mut arena, "a\nb\tc");
1512        assert_eq!(t.display(&arena).to_string(), "\"a\\nb\\tc\"");
1513    }
1514
1515    #[test]
1516    fn fmt_date_epoch_zero() {
1517        let arena = Arena::new();
1518        // 0 ms -> 1970-01-01 00:00:00 UTC
1519        let t = Term::date(0);
1520        assert_eq!(
1521            format!("{}", t.display(&arena)),
1522            "date{1970-01-01T00:00:00+00:00}"
1523        );
1524    }
1525
1526    #[test]
1527    fn fmt_date_epoch_ms_trunc_to_seconds() {
1528        let arena = Arena::new();
1529        let t = Term::date(1_234);
1530        assert_eq!(
1531            t.display(&arena).to_string(),
1532            "date{1970-01-01T00:00:01.234+00:00}"
1533        );
1534    }
1535
1536    #[test]
1537    fn fmt_date_specific_moment() {
1538        let arena = Arena::new();
1539        let t = Term::date(1_727_525_530_123i64);
1540        assert_eq!(
1541            format!("{}", t.display(&arena)),
1542            "date{2024-09-28T12:12:10.123+00:00}"
1543        );
1544    }
1545
1546    #[test]
1547    fn fmt_date_specific_moment_in_the_past() {
1548        let arena = Arena::new();
1549        let t = Term::date(-5_382_698_399_999i64);
1550        assert_eq!(
1551            format!("{}", t.display(&arena)),
1552            "date{1799-06-06T06:00:00.001+00:00}"
1553        );
1554    }
1555
1556    #[test]
1557    fn fmt_write_into_string_buffer() {
1558        let arena = Arena::new();
1559        let t = Term::int(7);
1560        let mut buf = String::new();
1561        // write! returns fmt::Result; ensure it's Ok and content matches
1562        write!(&mut buf, "val={}", t.display(&arena)).expect("formatting failed");
1563        assert_eq!(buf, "val=7");
1564    }
1565}