arena_terms/
lib.rs

1//! Copyright (c) 2005–2025 IKH Software, Inc.
2//!
3//! Released under the terms of the GNU Lesser General Public License, version 3.0
4//! or (at your option) any later version (LGPL-3.0-or-later).
5//!
6//! A lightweight, arena-backed representation of Prolog–like terms.
7//!
8//! This crate provides a compact [`Term`] type for representing Prolog-like
9//! data structures, along with a typed arena [`Arena`] used to
10//! intern atoms, variables, strings, binaries and compound terms.  The
11//! underlying representation is designed around a fixed‐width 16
12//! byte handle which carries both the tag and value of a term.
13//!
14//! The primary entry points are [`Arena`] (for allocating
15//! interned data) and [`Term`] (the user visible term handle).  Terms
16//! can be matched using the [`Term::view`] method which yields a
17//! [`View`] that borrows from the underlying arena.  Equality and
18//! ordering are well defined according to Prolog's standard order of
19//! terms.  Users may construct lists and tuples conveniently via
20//! macros exported from this crate.
21//!
22//! ## Example
23//! ```rust
24//! # use arena_terms::{Arena, func, IntoTerm, list, tuple, var, View};
25//! // create an arena
26//! let mut arena = Arena::new();
27//!
28//! // build some primitive terms
29//! let a = arena.atom("hello");
30//! let b = arena.real(3.14);
31//! let c = arena.date(1_640_995_200_000i64);  // 2022-01-01T00:00:00Z
32//!
33//! // build a long list from an iterator
34//! let xs = arena.list((0..1_000_000).map(|x| x as f64));
35//!
36//! // build a compound term using the func! macro
37//! let term = func![
38//!     "example";
39//!     123,                // IntoTerm: integer
40//!     "abc",              // IntoTerm: &str
41//!     list![a, b, c, xs], // nested list (xs is shared)
42//!     tuple!(b, a, xs),   // nested tuple (xs is shared)
43//!     var!("X"),          // variable (implicit arena)
44//!     => &mut arena
45//! ];
46//!
47//! // inspect the resulting term
48//! if let Ok(View::Func(ar, functor, args)) = term.view(&arena) {
49//!     assert_eq!(functor.name(ar).unwrap(), "example");
50//!     assert_eq!(args.len(), 5);
51//!     // view nested terms recursively
52//!     match args[2].view(ar).unwrap() {
53//!         View::List(_, elems, _) => assert_eq!(elems.len(), 4),
54//!         _ => unreachable!(),
55//!     }
56//! }
57//! ```
58//!
59//! # Crates.io
60//! Published at [crates.io/crates/arena-terms](https://crates.io/crates/arena-terms).
61
62
63use core::fmt;
64use smartstring::alias::String;
65use std::borrow::Cow;
66use std::cmp::Ordering;
67
68// The following type definitions describe the internal representation
69// of a term.  Rather than packing data into a single integer we use
70// a tagged enum to store the various kinds of terms.  Each variant
71// carries its associated data directly, for example a 64 bit integer
72// for numeric types or a small inline buffer for short atoms and
73// variables.  Long names or sequences store an index and length into
74// the appropriate arena.
75
76#[derive(Debug, Copy, Clone, PartialEq, PartialOrd)]
77struct TinyArray {
78    bytes: [u8; 14],
79    len: u8,
80}
81
82#[derive(Debug, Copy, Clone, PartialEq, PartialOrd)]
83struct Slice {
84    epoch_id: EpochID,
85    index: u32,
86    len: u32,
87}
88
89/// Internal handle describing the kind of a term and storing its data.
90///
91/// Each variant stores the associated value directly.  The `repr(u8)`
92/// attribute ensures the discriminant occupies a single byte, which
93/// together with the payloads yields a `Term` size of 16 bytes on
94/// 64‑bit targets.
95#[derive(Debug, Copy, Clone, PartialEq, PartialOrd)]
96#[repr(u8)]
97enum Handle {
98    Int(i64),
99    Real(f64),
100    Date(i64),
101    Var(TinyArray),
102    VarRef(Slice),
103    Atom(TinyArray),
104    AtomRef(Slice),
105    Str(TinyArray),
106    StrRef(Slice),
107    Bin(TinyArray),
108    BinRef(Slice),
109    FuncRef(Slice),
110    ListRef(Slice),
111    ListCRef(Slice),
112    TupleRef(Slice),
113}
114
115/// A compact, copyable handle referencing a term stored in a [`Arena`].
116///
117/// Internally a `Term` stores a single [`Handle`] enum variant.
118/// On 64‑bit targets the discriminant and associated payload occupy
119/// 16 bytes in total.  Users should never construct `Term` values
120/// directly; instead use the associated constructors or the
121/// convenience macros in the [`term`] module.
122/// Instances of `Term` are cheap to copy (`Copy` and `Clone`).
123
124// TODO: Consider implementing Hash, Eq, and Ord. Verify whether it is valid
125//       to provide PartialEq, Eq, PartialOrd, Ord, and Hash when:
126//       - Two different Term handles may point to the same term value, or
127//       - Two identical Term handles obtained from different arenas may
128//         represent distinct term values.
129#[derive(Copy, Clone, PartialEq, PartialOrd)]
130pub struct Term(Handle);
131
132impl AsRef<Term> for Term {
133    fn as_ref(&self) -> &Self {
134        self
135    }
136}
137
138macro_rules! impl_from_integers_for_term {
139    ($($t:ty),* $(,)?) => {$(
140        impl From<$t> for Term {
141            #[inline]
142            fn from(v: $t) -> Self { Term::int(v as i64) }
143        }
144    )*};
145}
146impl_from_integers_for_term!(i8, i16, i32, i64, u8, u16, u32);
147
148macro_rules! impl_from_floats_for_term {
149    ($($t:ty),* $(,)?) => {$(
150        impl From<$t> for Term {
151            #[inline]
152            fn from(v: $t) -> Self { Term::real(v as f64) }
153        }
154    )*};
155}
156impl_from_floats_for_term!(f32, f64);
157
158pub trait IntoTerm {
159    fn into_term(self, arena: &mut Arena) -> Term;
160}
161
162macro_rules! impl_intoterm_for_integers {
163    ($($t:ty),* $(,)?) => {$(
164        impl IntoTerm for $t {
165            #[inline]
166            fn into_term(self, _arena: &mut Arena) -> Term { Term::int(self as i64) }
167        }
168    )*};
169}
170impl_intoterm_for_integers!(i8, i16, i32, i64, u8, u16, u32);
171
172macro_rules! impl_intoterm_for_floats {
173    ($($t:ty),* $(,)?) => {$(
174        impl IntoTerm for $t {
175            #[inline]
176            fn into_term(self, _arena: &mut Arena) -> Term { Term::real(self as f64) }
177        }
178    )*};
179}
180impl_intoterm_for_floats!(f32, f64);
181
182impl<'a> IntoTerm for &'a str {
183    #[inline]
184    fn into_term(self, arena: &mut Arena) -> Term {
185        Term::str(arena, self)
186    }
187}
188
189impl<'a> IntoTerm for &'a [u8] {
190    #[inline]
191    fn into_term(self, arena: &mut Arena) -> Term {
192        Term::bin(arena, self)
193    }
194}
195
196impl<'a> IntoTerm for Cow<'a, str> {
197    #[inline]
198    fn into_term(self, arena: &mut Arena) -> Term {
199        match self {
200            Cow::Borrowed(s) => Term::str(arena, s),
201            Cow::Owned(s) => Term::str(arena, s),
202        }
203    }
204}
205
206impl<'a> IntoTerm for Cow<'a, [u8]> {
207    #[inline]
208    fn into_term(self, arena: &mut Arena) -> Term {
209        match self {
210            Cow::Borrowed(s) => Term::bin(arena, s),
211            Cow::Owned(s) => Term::bin(arena, s),
212        }
213    }
214}
215
216impl IntoTerm for String {
217    #[inline]
218    fn into_term(self, arena: &mut Arena) -> Term {
219        Term::str(arena, &self)
220    }
221}
222
223impl IntoTerm for std::string::String {
224    #[inline]
225    fn into_term(self, arena: &mut Arena) -> Term {
226        Term::str(arena, &self)
227    }
228}
229
230impl IntoTerm for Vec<u8> {
231    #[inline]
232    fn into_term(self, arena: &mut Arena) -> Term {
233        Term::bin(arena, &self)
234    }
235}
236
237impl IntoTerm for Term {
238    #[inline]
239    fn into_term(self, _arena: &mut Arena) -> Term {
240        self
241    }
242}
243
244impl IntoTerm for &Term {
245    #[inline]
246    fn into_term(self, _arena: &mut Arena) -> Term {
247        *self
248    }
249}
250
251impl<F> IntoTerm for F
252where
253    F: FnOnce(&mut Arena) -> Term,
254{
255    #[inline]
256    fn into_term(self, arena: &mut Arena) -> Term {
257        self(arena)
258    }
259}
260
261impl Term {
262    /// Construct a new integer term.  The full 64 bit two's complement
263    /// representation of `i` is stored in the payload.  No truncation
264    /// occurs.
265    #[inline]
266    pub fn int(i: impl Into<i64>) -> Self {
267        Self(Handle::Int(i.into()))
268    }
269
270    /// Construct a new floating point term.  The full 64 bit IEEE‑754
271    /// bit pattern is stored in the payload without truncation.
272    #[inline]
273    pub fn real(f: impl Into<f64>) -> Self {
274        Self(Handle::Real(f.into()))
275    }
276
277    /// Construct a new date term representing a Unix epoch in
278    /// milliseconds.  Dates share the same underlying storage as
279    /// integers but use a distinct tag so they do not compare equal
280    /// with integer terms.
281    #[inline]
282    pub fn date(ms: impl Into<i64>) -> Self {
283        Self(Handle::Date(ms.into()))
284    }
285
286    /// Construct or intern an atom into the arena and produce a term
287    /// referencing it.  Small atom names (≤14 bytes of UTF‑8) are
288    /// inlined directly into the handle; longer names are interned
289    /// into the arena and referenced by index and length.
290    #[inline]
291    pub fn atom(arena: &mut Arena, name: impl AsRef<str>) -> Self {
292        let name = name.as_ref();
293        let bytes = name.as_bytes();
294        if bytes.len() <= 14 {
295            let mut buf = [0u8; 14];
296            buf[..bytes.len()].copy_from_slice(bytes);
297            Self(Handle::Atom(TinyArray {
298                bytes: buf,
299                len: bytes.len() as u8,
300            }))
301        } else {
302            Self(Handle::AtomRef(arena.intern_str(name)))
303        }
304    }
305
306    /// Construct or intern a variable into the arena and produce a
307    /// term referencing it.  Small variable names (≤14 bytes) are
308    /// inlined directly into the handle; longer names are interned in
309    /// the arena and referenced by index.
310    #[inline]
311    pub fn var(arena: &mut Arena, name: impl AsRef<str>) -> Self {
312        let name = name.as_ref();
313        let bytes = name.as_bytes();
314        if bytes.len() <= 14 {
315            let mut buf = [0u8; 14];
316            buf[..bytes.len()].copy_from_slice(bytes);
317            Self(Handle::Var(TinyArray {
318                bytes: buf,
319                len: bytes.len() as u8,
320            }))
321        } else {
322            Self(Handle::VarRef(arena.intern_str(name)))
323        }
324    }
325
326    /// Construct or intern a UTF‑8 string into the arena and produce a
327    /// term referencing it.  Strings longer than 14 bytes are interned
328    /// in the arena; shorter strings are inlined.  Invalid UTF‑8 will
329    /// result in an error.
330    #[inline]
331    pub fn str(arena: &mut Arena, s: impl AsRef<str>) -> Self {
332        let s = s.as_ref();
333        let bytes = s.as_bytes();
334        if bytes.len() <= 14 {
335            let mut buf = [0u8; 14];
336            buf[..bytes.len()].copy_from_slice(bytes);
337            Self(Handle::Str(TinyArray {
338                bytes: buf,
339                len: bytes.len() as u8,
340            }))
341        } else {
342            Self(Handle::StrRef(arena.intern_str(s)))
343        }
344    }
345
346    /// Construct or intern a binary blob into the arena and produce a
347    /// term referencing it.  Blobs longer than 14 bytes are interned
348    /// in the arena; shorter blobs are inlined.
349    #[inline]
350    pub fn bin(arena: &mut Arena, bytes: impl AsRef<[u8]>) -> Self {
351        let bytes = bytes.as_ref();
352        if bytes.len() <= 14 {
353            let mut buf = [0u8; 14];
354            buf[..bytes.len()].copy_from_slice(bytes);
355            Self(Handle::Bin(TinyArray {
356                bytes: buf,
357                len: bytes.len() as u8,
358            }))
359        } else {
360            Self(Handle::BinRef(arena.intern_bytes(bytes)))
361        }
362    }
363
364    /// Construct a new compound term by interning the functor and
365    /// arguments in the arena.  The returned term references a slice
366    /// in the arena's term storage consisting of the functor atom as
367    /// the first entry followed by the argument handles.  A functor of
368    /// arity zero results in an atom.
369    #[inline]
370    pub fn func(
371        arena: &mut Arena,
372        functor: impl AsRef<str>,
373        args: impl IntoIterator<Item = impl IntoTerm>,
374    ) -> Self {
375        let functor_atom = Self::atom(arena, functor);
376        let mut args = args.into_iter();
377        let Some(first) = args.next() else {
378            return functor_atom;
379        };
380        Self(Handle::FuncRef(arena.intern_func(
381            functor_atom,
382            std::iter::once(first).chain(args),
383        )))
384    }
385
386    /// Construct a new compound term by interning the functor and its arguments
387    /// into the arena as a sequence of terms (functor first, then arguments).
388    /// A functor with no arguments yields the atom itself.  Errors if
389    /// no functor is provided or if the first term is not an atom.
390    #[inline]
391    pub fn funcv(
392        arena: &mut Arena,
393        terms: impl IntoIterator<Item = impl IntoTerm>,
394    ) -> Result<Self, TermError> {
395        let mut terms = terms.into_iter();
396        let Some(functor_atom) = terms.next() else {
397            return Err(TermError::MissingFunctor);
398        };
399        let functor_atom = functor_atom.into_term(arena);
400        if !functor_atom.is_atom() {
401            return Err(TermError::InvalidFunctor(functor_atom));
402        }
403        let Some(first) = terms.next() else {
404            return Ok(functor_atom);
405        };
406        Ok(Self(Handle::FuncRef(arena.intern_func(
407            functor_atom,
408            std::iter::once(first).chain(terms),
409        ))))
410    }
411
412    /// Constructs a new list. A list is represented internally as an
413    /// array of terms. If `terms` is empty, returns `nil`.
414    #[inline]
415    pub fn list(arena: &mut Arena, terms: impl IntoIterator<Item = impl IntoTerm>) -> Self {
416        let mut terms = terms.into_iter();
417        let Some(first) = terms.next() else {
418            return Self::NIL;
419        };
420        Self(Handle::ListRef(
421            arena.intern_seq(std::iter::once(first).chain(terms)),
422        ))
423    }
424
425    /// Constructs a new improper list. An improper list is represented as
426    /// a list and additional argument. If `terms` is empty, returns `nil`.
427    #[inline]
428    pub fn listc(
429        arena: &mut Arena,
430        terms: impl IntoIterator<Item = impl IntoTerm>,
431        tail: impl IntoTerm,
432    ) -> Self {
433        let mut terms = terms.into_iter();
434        let Some(first) = terms.next() else {
435            return Self::NIL;
436        };
437        let tail = tail.into_term(arena);
438        if tail != Term::NIL {
439            Self(Handle::ListCRef(arena.intern_seq_plus_one(
440                std::iter::once(first).chain(terms),
441                tail,
442            )))
443        } else {
444            Self(Handle::ListRef(
445                arena.intern_seq(std::iter::once(first).chain(terms)),
446            ))
447        }
448    }
449
450    /// Constructs a new tuple. A tuple is represented internally as an array
451    /// of terms.
452    #[inline]
453    pub fn tuple(arena: &mut Arena, terms: impl IntoIterator<Item = impl IntoTerm>) -> Self {
454        let mut terms = terms.into_iter();
455        let Some(first) = terms.next() else {
456            return Self::UNIT;
457        };
458        Self(Handle::TupleRef(
459            arena.intern_seq(std::iter::once(first).chain(terms)),
460        ))
461    }
462
463    /// Constant representing the zero‑arity tuple (unit).  Internally
464    /// this is the atom `"unit"` encoded as a small atom.  It may
465    /// be copied freely and does not depend on any arena.
466    pub const UNIT: Self = {
467        let buf: [u8; 14] = [b'u', b'n', b'i', b't', 0, 0, 0, 0, 0, 0, 0, 0, 0, 0];
468        Self(Handle::Atom(TinyArray { bytes: buf, len: 4 }))
469    };
470
471    /// Constant representing the empty list (nil).  Internally this is
472    /// the atom `"nil"` encoded as a small atom.  It may be copied
473    /// freely and does not depend on any arena.
474    pub const NIL: Self = {
475        let buf: [u8; 14] = [b'n', b'i', b'l', 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0];
476        Self(Handle::Atom(TinyArray { bytes: buf, len: 3 }))
477    };
478
479    /// Returns the value if `term` is an integer, otherwise an error.
480    #[inline]
481    pub fn unpack_int(&self, arena: &Arena) -> Result<i64, TermError> {
482        arena.unpack_int(self)
483    }
484
485    /// Returns the value if `term` is a real, otherwise an error.
486    #[inline]
487    pub fn unpack_real(&self, arena: &Arena) -> Result<f64, TermError> {
488        arena.unpack_real(self)
489    }
490
491    /// Returns the value if `term` is a date, otherwise an error.
492    #[inline]
493    pub fn unpack_date(&self, arena: &Arena) -> Result<i64, TermError> {
494        arena.unpack_date(self)
495    }
496
497    /// Returns the string slice if `term` is a string, otherwise an error.
498    #[inline]
499    pub fn unpack_str<'a>(&'a self, arena: &'a Arena) -> Result<&'a str, TermError> {
500        arena.unpack_str(self)
501    }
502
503    /// Returns the slice if `term` is a binary blob, otherwise an error.
504    #[inline]
505    pub fn unpack_bin<'a>(&'a self, arena: &'a Arena) -> Result<&'a [u8], TermError> {
506        arena.unpack_bin(self)
507    }
508
509    /// Returns the name if `term` is an atom, otherwise an error.
510    #[inline]
511    pub fn unpack_atom<'a>(
512        &'a self,
513        arena: &'a Arena,
514        allowed_names: &[&str],
515    ) -> Result<&'a str, TermError> {
516        arena.unpack_atom(self, allowed_names)
517    }
518
519    /// Returns the name if `term` is a variable, otherwise an error.
520    #[inline]
521    pub fn unpack_var<'a>(
522        &'a self,
523        arena: &'a Arena,
524        allowed_names: &[&str],
525    ) -> Result<&'a str, TermError> {
526        arena.unpack_var(self, allowed_names)
527    }
528
529    /// Returns the name and arguments if `term` is a compound term of any arity
530    /// or an atom and its name is in `allowed_names` (or if `allowed_names` is empty),
531    /// otherwise returns an error.
532    #[inline]
533    pub fn unpack_func_any<'a>(
534        &'a self,
535        arena: &'a Arena,
536        allowed_names: &[&str],
537    ) -> Result<(&'a Term, &'a [Term]), TermError> {
538        arena.unpack_func_any(self, allowed_names)
539    }
540
541    /// Returns the name and arguments if `term` is a compound term of arity `ARITY`
542    /// (or an atom if `ARITY == 0`) and its name is in `allowed_names` (or if `allowed_names` is empty),
543    /// otherwise returns an error.
544    #[inline]
545    pub fn unpack_func<'a, const ARITY: usize>(
546        &'a self,
547        arena: &'a Arena,
548        allowed_names: &[&str],
549    ) -> Result<(&'a Term, [Term; ARITY]), TermError> {
550        arena.unpack_func(self, allowed_names)
551    }
552
553    /// Returns the slice with list elements and the tail if `term` is a list,
554    /// otherwise returns an error.
555    #[inline]
556    pub fn unpack_list<'a>(
557        &'a self,
558        arena: &'a Arena,
559    ) -> Result<(&'a [Term], &'a Term), TermError> {
560        arena.unpack_list(self)
561    }
562
563    /// Returns the slice with tuple elements if `term` is a tuple of any arity,
564    /// otherwise returns an error.
565    #[inline]
566    pub fn unpack_tuple_any<'a>(&'a self, arena: &'a Arena) -> Result<&'a [Term], TermError> {
567        arena.unpack_tuple_any(self)
568    }
569
570    /// Returns the tuple elements if `term` is a tuple of arity `ARITY`,
571    /// otherwise returns an error.
572    #[inline]
573    pub fn unpack_tuple<const ARITY: usize>(
574        &self,
575        arena: &Arena,
576    ) -> Result<[Term; ARITY], TermError> {
577        arena.unpack_tuple(self)
578    }
579
580    /// Produce a [`View`] of this term that borrows from the given
581    /// [`Arena`].  This method decodes any inlined bytes and
582    /// dereferences indexes into the arena to yield structured
583    /// references.  See [`View`] for details.
584    #[inline]
585    pub fn view<'a>(&'a self, arena: &'a Arena) -> Result<View<'a>, TermError> {
586        match &self.0 {
587            Handle::Int(i) => Ok(View::Int(*i)),
588            Handle::Real(f) => Ok(View::Real(*f)),
589            Handle::Date(d) => Ok(View::Date(*d)),
590            Handle::Var(vs) => {
591                let s_bytes = &vs.bytes[..vs.len as usize];
592                let s = unsafe { core::str::from_utf8_unchecked(s_bytes) };
593                Ok(View::Var(s))
594            }
595            Handle::VarRef(vr) => Ok(View::Var(unsafe {
596                core::str::from_utf8_unchecked(
597                    arena
598                        .byte_slice(vr)
599                        .map_err(|_| TermError::InvalidTerm(*self))?,
600                )
601            })),
602            Handle::Atom(a) => {
603                let s_bytes = &a.bytes[..a.len as usize];
604                let s = unsafe { core::str::from_utf8_unchecked(s_bytes) };
605                Ok(View::Atom(s))
606            }
607            Handle::AtomRef(ar) => Ok(View::Atom(unsafe {
608                core::str::from_utf8_unchecked(
609                    arena
610                        .byte_slice(ar)
611                        .map_err(|_| TermError::InvalidTerm(*self))?,
612                )
613            })),
614            Handle::Str(ss) => {
615                let s_bytes = &ss.bytes[..ss.len as usize];
616                let s = unsafe { core::str::from_utf8_unchecked(s_bytes) };
617                Ok(View::Str(s))
618            }
619            Handle::StrRef(sr) => Ok(View::Str(unsafe {
620                core::str::from_utf8_unchecked(
621                    arena
622                        .byte_slice(sr)
623                        .map_err(|_| TermError::InvalidTerm(*self))?,
624                )
625            })),
626            Handle::Bin(bs) => {
627                let b = &bs.bytes[..bs.len as usize];
628                Ok(View::Bin(b))
629            }
630            Handle::BinRef(br) => Ok(View::Bin(
631                arena
632                    .byte_slice(br)
633                    .map_err(|_| TermError::InvalidTerm(*self))?,
634            )),
635            Handle::FuncRef(fr) => {
636                let slice = arena
637                    .term_slice(fr)
638                    .map_err(|_| TermError::InvalidTerm(*self))?;
639                // Functor is the first element of the slice
640                let functor = &slice[0];
641                let args = &slice[1..];
642                Ok(View::Func(arena, functor, args))
643            }
644            Handle::ListRef(lr) => {
645                let slice = arena
646                    .term_slice(lr)
647                    .map_err(|_| TermError::InvalidTerm(*self))?;
648                Ok(View::List(arena, slice, &Term::NIL))
649            }
650            Handle::ListCRef(lr) => {
651                let slice = arena
652                    .term_slice(lr)
653                    .map_err(|_| TermError::InvalidTerm(*self))?;
654                let last = slice.len() - 1;
655                Ok(View::List(arena, &slice[..last], &slice[last]))
656            }
657            Handle::TupleRef(tr) => {
658                let slice = arena
659                    .term_slice(tr)
660                    .map_err(|_| TermError::InvalidTerm(*self))?;
661                Ok(View::Tuple(arena, slice))
662            }
663        }
664    }
665
666    /// Returns `true` if the value fits directly in `Term` without arena storage,
667    /// i.e. `int`, `real`, `date`, or a small `atom`, `var`, `str`, or `bin`.
668    #[inline]
669    pub fn is_inline(&self) -> bool {
670        match &self.0 {
671            Handle::Int(_)
672            | Handle::Real(_)
673            | Handle::Date(_)
674            | Handle::Atom(_)
675            | Handle::Var(_)
676            | Handle::Str(_)
677            | Handle::Bin(_) => true,
678            Handle::AtomRef(_)
679            | Handle::VarRef(_)
680            | Handle::StrRef(_)
681            | Handle::BinRef(_)
682            | Handle::FuncRef(_)
683            | Handle::ListRef(_)
684            | Handle::ListCRef(_)
685            | Handle::TupleRef(_) => false,
686        }
687    }
688
689    /// Returns `true` if the term is a compound term.
690    #[inline]
691    pub fn is_func(&self) -> bool {
692        matches!(self.0, Handle::FuncRef(_))
693    }
694
695    /// Returns `true` if the term is a list.
696    #[inline]
697    pub fn is_list(&self) -> bool {
698        matches!(self.0, Handle::ListRef(_) | Handle::ListCRef(_)) || *self == Self::NIL
699    }
700
701    /// Returns `true` if the term is a tuple.
702    #[inline]
703    pub fn is_tuple(&self) -> bool {
704        matches!(self.0, Handle::TupleRef(_)) || *self == Self::UNIT
705    }
706
707    /// Returns `true` if the term is an integer.
708    #[inline]
709    pub fn is_int(&self) -> bool {
710        matches!(self.0, Handle::Int(_))
711    }
712
713    /// Returns `true` if the term is a real (floating-point) number.
714    #[inline]
715    pub fn is_real(&self) -> bool {
716        matches!(self.0, Handle::Real(_))
717    }
718
719    /// Returns `true` if the term is a date.
720    #[inline]
721    pub fn is_date(&self) -> bool {
722        matches!(self.0, Handle::Date(_))
723    }
724
725    /// Returns `true` if the term is an atom.
726    #[inline]
727    pub fn is_atom(&self) -> bool {
728        matches!(self.0, Handle::Atom(_) | Handle::AtomRef(_))
729    }
730
731    /// Returns `true` if the term is a variable.
732    #[inline]
733    pub fn is_var(&self) -> bool {
734        matches!(self.0, Handle::Var(_) | Handle::VarRef(_))
735    }
736
737    /// Returns `true` if the term is a number (`int`, `real`, or `date`).
738    #[inline]
739    pub fn is_number(&self) -> bool {
740        matches!(self.0, Handle::Int(_) | Handle::Real(_) | Handle::Date(_))
741    }
742
743    /// Returns `true` if the term is a string.
744    #[inline]
745    pub fn is_str(&self) -> bool {
746        matches!(self.0, Handle::Str(_) | Handle::StrRef(_))
747    }
748
749    /// Returns `true` if the term is a binary blob.
750    #[inline]
751    pub fn is_bin(&self) -> bool {
752        matches!(self.0, Handle::Bin(_) | Handle::BinRef(_))
753    }
754
755    /// Returns the arity of the term. Currently the arity of lists and variables is 0.
756    #[inline]
757    pub fn arity(&self) -> usize {
758        match &self.0 {
759            Handle::Atom(_)
760            | Handle::AtomRef(_)
761            | Handle::Int(_)
762            | Handle::Real(_)
763            | Handle::Date(_)
764            | Handle::Str(_)
765            | Handle::StrRef(_)
766            | Handle::Bin(_)
767            | Handle::BinRef(_) => 0,
768            Handle::FuncRef(Slice { len: n, .. }) => (n - 1) as usize,
769            Handle::TupleRef(Slice { len: n, .. }) => *n as usize,
770            Handle::ListRef(_) | Handle::ListCRef(_) | Handle::Var(_) | Handle::VarRef(_) => 0,
771        }
772    }
773
774    /// Returns the name of a compound term, atom, or variable.
775    /// Use [`atom_name`], [`func_name`], or [`var_name`]
776    /// to ensure the term is of a specific kind.
777    #[inline]
778    pub fn name<'a>(&'a self, arena: &'a Arena) -> Result<&'a str, TermError> {
779        arena.name(self)
780    }
781
782    /// Returns the name of an atom,
783    #[inline]
784    pub fn atom_name<'a>(&'a self, arena: &'a Arena) -> Result<&'a str, TermError> {
785        arena.unpack_atom(self, &[])
786    }
787
788    /// Returns the name of a variable.
789    #[inline]
790    pub fn var_name<'a>(&'a self, arena: &'a Arena) -> Result<&'a str, TermError> {
791        arena.unpack_var(self, &[])
792    }
793
794    /// Returns the name of a compund term.
795    #[inline]
796    pub fn func_name<'a>(&'a self, arena: &'a Arena) -> Result<&'a str, TermError> {
797        let (functor, _) = arena.unpack_func_any(self, &[])?;
798        arena.atom_name(functor)
799    }
800
801    /// Returns a string describing the kind of this term.
802    #[inline]
803    pub fn kind_name(&self) -> &'static str {
804        match &self.0 {
805            Handle::Int(_) => "int",
806            Handle::Real(_) => "real",
807            Handle::Date(_) => "date",
808            Handle::Var(_) | Handle::VarRef(_) => "var",
809            Handle::Atom(_) | Handle::AtomRef(_) => "atom",
810            Handle::Str(_) | Handle::StrRef(_) => "str",
811            Handle::Bin(_) | Handle::BinRef(_) => "bin",
812            Handle::FuncRef(_) => "func",
813            Handle::ListRef(_) | Handle::ListCRef(_) => "list",
814            Handle::TupleRef(_) => "tuple",
815        }
816    }
817}
818
819/// Implements the standard [`Debug`] formatter for [`Term`].
820///
821/// This prints a developer-friendly representation of the term,
822/// showing its kind (e.g. `int`, `atom`, `list`, `tuple`) and
823/// its internal value in a form useful for debugging.  
824///
825/// The output is not guaranteed to be stable across versions and
826/// should not be parsed; it is intended purely for diagnostics
827/// and logging.
828///
829/// # Example
830/// ```rust
831/// # use arena_terms::Term;
832/// let t = Term::int(42);
833/// println!("{:?}", t); // e.g. prints `Int(42)`
834/// ```
835impl fmt::Debug for Term {
836    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
837        match &self.0 {
838            Handle::Int(i) => f.debug_tuple("Int").field(i).finish(),
839            Handle::Real(r) => f.debug_tuple("Real").field(r).finish(),
840            Handle::Date(d) => f.debug_tuple("Date").field(d).finish(),
841            Handle::Var(v) => {
842                let name =
843                    core::str::from_utf8(&v.bytes[..v.len as usize]).unwrap_or("<invalid utf8>");
844                f.debug_struct("Var").field("name", &name).finish()
845            }
846            Handle::VarRef(v) => f
847                .debug_struct("VarRef")
848                .field("epoch_id", &v.epoch_id)
849                .field("index", &v.index)
850                .field("len", &v.len)
851                .finish(),
852            Handle::Atom(a) => {
853                let name =
854                    core::str::from_utf8(&a.bytes[..a.len as usize]).unwrap_or("<invalid utf8>");
855                f.debug_struct("Atom").field("name", &name).finish()
856            }
857            Handle::AtomRef(v) => f
858                .debug_struct("AtomRef")
859                .field("epoch_id", &v.epoch_id)
860                .field("index", &v.index)
861                .field("len", &v.len)
862                .finish(),
863            Handle::Str(s) => {
864                let value =
865                    core::str::from_utf8(&s.bytes[..s.len as usize]).unwrap_or("<invalid utf8>");
866                f.debug_struct("Str").field("value", &value).finish()
867            }
868            Handle::StrRef(v) => f
869                .debug_struct("StrRef")
870                .field("epoch_id", &v.epoch_id)
871                .field("index", &v.index)
872                .field("len", &v.len)
873                .finish(),
874            Handle::Bin(b) => {
875                let slice = &b.bytes[..b.len as usize];
876                f.debug_struct("Bin").field("bytes", &slice).finish()
877            }
878            Handle::BinRef(v) => f
879                .debug_struct("BinRef")
880                .field("epoch_id", &v.epoch_id)
881                .field("index", &v.index)
882                .field("len", &v.len)
883                .finish(),
884            Handle::FuncRef(v) => f
885                .debug_struct("Func")
886                .field("epoch_id", &v.epoch_id)
887                .field("index", &v.index)
888                .field("len", &v.len)
889                .finish(),
890            Handle::ListRef(v) => f
891                .debug_struct("List")
892                .field("epoch_id", &v.epoch_id)
893                .field("index", &v.index)
894                .field("len", &v.len)
895                .finish(),
896            Handle::ListCRef(v) => f
897                .debug_struct("ListC")
898                .field("epoch_id", &v.epoch_id)
899                .field("index", &v.index)
900                .field("len", &v.len)
901                .finish(),
902            Handle::TupleRef(v) => f
903                .debug_struct("Tuple")
904                .field("epoch_id", &v.epoch_id)
905                .field("index", &v.index)
906                .field("len", &v.len)
907                .finish(),
908        }
909    }
910}
911
912impl fmt::Debug for View<'_> {
913    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
914        match &self {
915            View::Int(i) => f.debug_tuple("Int").field(&i).finish(),
916            View::Real(r) => f.debug_tuple("Real").field(&r).finish(),
917            View::Date(d) => f.debug_tuple("Date").field(&d).finish(),
918            View::Var(v) => f.debug_tuple("Var").field(&v).finish(),
919            View::Atom(a) => f.debug_tuple("Atom").field(&a).finish(),
920            View::Str(s) => f.debug_tuple("Str").field(&s).finish(),
921            View::Bin(b) => f.debug_tuple("Bin").field(&b).finish(),
922            View::Func(a, fr, ts) => f
923                .debug_tuple("Func")
924                .field(&a.arena_id)
925                .field(&fr)
926                .field(&ts.iter().map(|t| t.view(a)).collect::<Vec<_>>())
927                .finish(),
928            View::List(a, ts, tail) => f
929                .debug_tuple("List")
930                .field(&a.arena_id)
931                .field(&ts.iter().map(|t| t.view(a)).collect::<Vec<_>>())
932                .field(&tail.view(a))
933                .finish(),
934            View::Tuple(a, ts) => f
935                .debug_tuple("Tuple")
936                .field(&a.arena_id)
937                .field(&ts.iter().map(|t| t.view(a)).collect::<Vec<_>>())
938                .finish(),
939        }
940    }
941}
942
943/// A borrowed view into the interned contents of a [`Term`].
944///
945/// Use [`Term::view`] to obtain a view.  Each variant of [`View`]
946/// represents the decoded form of a term and borrows any data
947/// referenced from the [`Arena`] or the term handle itself.  No
948/// allocations are performed when constructing a `View`; instead
949/// references into the underlying storage are returned directly.  The
950/// lifetime `'a` binds the returned references to both the borrowed
951/// `Term` and the supplied `Arena`.
952#[derive(Clone, Copy)]
953pub enum View<'a> {
954    /// An integer value.
955    Int(i64),
956    /// A floating point value.
957    Real(f64),
958    /// A date value (milliseconds since the Unix epoch).
959    Date(i64),
960    /// A variable name borrowed from the term or arena.
961    Var(&'a str),
962    /// An atom name borrowed from the term or arena.
963    Atom(&'a str),
964    /// A UTF‑8 string borrowed from the term or arena.
965    Str(&'a str),
966    /// A binary slice borrowed from the term or arena.
967    Bin(&'a [u8]),
968    /// A compound term view containing the functor name and a slice
969    /// of arguments.  Both the functor and the argument slice are
970    /// borrowed; the arguments themselves are `Term` handles owned
971    /// by the arena.
972    Func(&'a Arena, &'a Term, &'a [Term]),
973    /// A list view containing a slice of the list elements
974    /// and a reference to the tail term. The element slice and the tail are
975    /// borrowed; the elements themselves are `Term` handles owned by the arena.
976    /// The tail of a proper list will always reference Term::NIL.
977    List(&'a Arena, &'a [Term], &'a Term),
978    /// A tuple view containing a slice of the tuple elements.
979    /// The element slice are borrowed; the elements
980    /// themselves are `Term` handles owned by the arena.
981    Tuple(&'a Arena, &'a [Term]),
982}
983
984/// The arena interns atoms, variables, strings, binaries, and compound terms.  
985/// An `Arena` owns all memory for interned data. Terms store only indices into
986/// this arena and remain valid as long as the epoch they belong to is alive.
987///
988/// ### Epochs
989/// The arena is divided into *epochs*. Conceptually, epochs form a stack.  
990/// Allocation begins in epoch `0`, which starts at offset `0` in all
991/// underlying storages. At any time, the user can call `begin_epoch()`.  
992/// This operation:
993/// - Freezes the current epoch (recording its byte and term offsets).  
994/// - Starts a new *active* epoch for subsequent allocations.  
995///
996/// At any point, there are `K` alive epochs, where:
997/// - `K - 1` are frozen (no new data is added),  
998/// - The last one is active (all new allocations go there),  
999/// - and `K <= MAX_LIVE_EPOCHS` (typically very small number, currently 8).
1000///
1001/// Terms remain valid only while the epoch they were created in is alive.  
1002///
1003/// ### Truncation
1004/// The arena can be truncated back to a given epoch `m`, where
1005/// `0 <= m < MAX_LIVE_EPOCHS`:
1006/// - Epoch `m` and all epochs more recent than `m` are erased in O(1).  
1007/// - Terms from those epochs become invalid.  
1008/// - `truncate(0)` erases all data (synonym: `clear()`).  
1009/// - `truncate(current_epoch())` erases only the latest epoch  
1010///   (synonym: `truncate_current()`).  
1011///
1012/// Conceptually, epochs form a stack: you can `push` with `begin_epoch()`
1013/// and `pop` with `truncate_current()`. This makes it efficient to manage
1014/// temporary, scoped allocations. For example:
1015/// ```
1016/// # use arena_terms::Arena;
1017/// let mut arena = Arena::with_capacity(4096, 1024);
1018/// let epoch = arena.begin_epoch().unwrap();
1019/// // … build temporary terms here …
1020/// arena.truncate(epoch).unwrap(); // frees them all at once
1021/// ```
1022///
1023/// This is especially useful during iteration: each loop can create
1024/// short-lived terms, then discard them cleanly all at once at the end.
1025
1026#[derive(Default, Clone, Debug)]
1027pub struct Arena {
1028    /// Randomly generated Arena ID
1029    arena_id: ArenaID,
1030
1031    /// Index into the buffers of alive epochs.
1032    /// Always points to the "current" epoch (latest allocations).
1033    current_epoch: usize,
1034
1035    /// Randomly generated identifiers, one per epoch.
1036    /// Every handle (e.g., term, func, var) that references this arena
1037    /// carries the epoch ID that was current at allocation time.
1038    /// When a handle is later resolved, the epoch ID is checked to
1039    /// ensure it still belongs to the same arena instance.
1040    epoch_ids: [EpochID; MAX_LIVE_EPOCHS],
1041
1042    /// Storage for interned atoms, variables, strings, and binary blobs.
1043    /// Data are appended sequentially in the last active epoch.
1044    bytes: Vec<u8>,
1045
1046    /// For each epoch, the starting offset into `bytes`.
1047    /// Used to "rewind" or reclaim all data belonging to an expired epoch.
1048    byte_start_by_epoch: [usize; MAX_LIVE_EPOCHS],
1049
1050    /// Storage for compound terms (structured values).
1051    /// Terms are appended sequentially in the last active epoch.
1052    /// Each term is represented as a contiguous slice:
1053    ///   [functor_atom, arg1, arg2, …]
1054    /// The `Func` handle encodes both the slice’s starting index and length.
1055    terms: Vec<Term>,
1056
1057    /// For each epoch, the starting index into `terms`.
1058    /// Used to drop/pick up all terms from an expired epoch in bulk.
1059    term_start_by_epoch: [usize; MAX_LIVE_EPOCHS],
1060}
1061
1062pub const MAX_LIVE_EPOCHS: usize = 8;
1063
1064#[derive(Default, Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
1065pub struct EpochID(u32); // Random Epoch ID
1066
1067#[derive(Default, Debug, Clone, Copy, PartialEq, Eq)]
1068struct ArenaID(u32); // Random Arena ID
1069
1070#[derive(Debug, Clone, Copy)]
1071pub struct ArenaStats {
1072    pub current_epoch: EpochID,
1073    pub bytes_len: usize,
1074    pub terms_len: usize,
1075}
1076
1077impl Arena {
1078    /// Create a new, empty arena with given capacities.
1079    pub fn with_capacity(bytes_capacity: usize, terms_capacity: usize) -> Self {
1080        let mut epoch_ids = [EpochID(0); MAX_LIVE_EPOCHS];
1081        epoch_ids[0] = EpochID(rand::random());
1082
1083        Self {
1084            arena_id: ArenaID(rand::random()),
1085            current_epoch: 0,
1086            epoch_ids,
1087            bytes: Vec::with_capacity(bytes_capacity),
1088            byte_start_by_epoch: [0; MAX_LIVE_EPOCHS],
1089            terms: Vec::with_capacity(terms_capacity),
1090            term_start_by_epoch: [0; MAX_LIVE_EPOCHS],
1091        }
1092    }
1093
1094    /// Create a new, empty arena with default capacities.
1095    pub fn new() -> Self {
1096        Self::with_capacity(4096, 1024)
1097    }
1098
1099    /// Returns stats.
1100    pub fn stats(&self) -> ArenaStats {
1101        ArenaStats {
1102            current_epoch: self.epoch_ids[self.current_epoch],
1103            bytes_len: self.bytes.len(),
1104            terms_len: self.terms.len(),
1105        }
1106    }
1107
1108    /// Returns current epoch.
1109    pub fn current_epoch(&self) -> EpochID {
1110        self.epoch_ids[self.current_epoch]
1111    }
1112
1113    /// Freezes current epoch and begins a new one.
1114    pub fn begin_epoch(&mut self) -> Result<EpochID, TermError> {
1115        let new_epoch = self.current_epoch + 1;
1116        if new_epoch >= MAX_LIVE_EPOCHS {
1117            return Err(TermError::LiveEpochsExceeded);
1118        }
1119        self.epoch_ids[new_epoch] = EpochID(rand::random());
1120        self.byte_start_by_epoch[new_epoch] = self.bytes.len();
1121        self.term_start_by_epoch[new_epoch] = self.terms.len();
1122        self.current_epoch = new_epoch;
1123        Ok(self.epoch_ids[new_epoch])
1124    }
1125
1126    /// Erases arena in O(1).
1127    /// Does not shrink the allocated capacity.
1128    pub fn clear(&mut self) -> Result<(), TermError> {
1129        self.truncate(self.epoch_ids[0])
1130    }
1131
1132    /// Epoch `m` and all epochs more recent than `m` are erased in O(1)
1133    /// Does not shrink the allocated capacity.
1134    pub fn truncate_current(&mut self) -> Result<(), TermError> {
1135        self.truncate(self.epoch_ids[self.current_epoch])
1136    }
1137
1138    /// Epoch `m` and all epochs more recent than `m` are erased in O(1)
1139    /// Does not shrink the allocated capacity.
1140    pub fn truncate(&mut self, epoch_id: EpochID) -> Result<(), TermError> {
1141        let epoch = self
1142            .epoch_index(epoch_id)
1143            .map_err(|_| TermError::InvalidEpoch(epoch_id))?;
1144        self.bytes.truncate(self.byte_start_by_epoch[epoch]);
1145        self.terms.truncate(self.term_start_by_epoch[epoch]);
1146        self.current_epoch = epoch;
1147        Ok(())
1148    }
1149
1150    /// Searches epoch ID in alive epochs and returns its index.
1151    #[inline]
1152    fn epoch_index(&self, epoch_id: EpochID) -> Result<usize, InternalTermError> {
1153        let Some(epoch) = self.epoch_ids[..=self.current_epoch]
1154            .iter()
1155            .position(|&id| id == epoch_id)
1156        else {
1157            return Err(InternalTermError::InvalidEpoch(epoch_id));
1158        };
1159        Ok(epoch)
1160    }
1161
1162    /// Returns an error if the term's slice's epoch is not among the alive epochs,
1163    /// or if the slice's index/length is inconsistent with the epoch's range.
1164    #[inline]
1165    fn verify_byte_slice(&self, slice: &Slice) -> Result<(), InternalTermError> {
1166        let epoch = self.epoch_index(slice.epoch_id)?;
1167        let epoch_start = self.byte_start_by_epoch[epoch];
1168        let epoch_end = if epoch == self.current_epoch {
1169            self.bytes.len()
1170        } else {
1171            self.byte_start_by_epoch[epoch + 1]
1172        };
1173        if (slice.index as usize) < epoch_start
1174            || (slice.index as usize) + (slice.len as usize) > epoch_end
1175        {
1176            return Err(InternalTermError::InvalidSlice(*slice));
1177        }
1178        Ok(())
1179    }
1180
1181    /// Returns an error if the byte's slice's epoch is not among the alive epochs,
1182    /// or if the slice's index/length is inconsistent with the epoch's range.
1183    #[inline]
1184    fn verify_term_slice(&self, slice: &Slice) -> Result<(), InternalTermError> {
1185        let epoch = self.epoch_index(slice.epoch_id)?;
1186        let epoch_start = self.term_start_by_epoch[epoch];
1187        let epoch_end = if epoch == self.current_epoch {
1188            self.terms.len()
1189        } else {
1190            self.term_start_by_epoch[epoch + 1]
1191        };
1192        if (slice.index as usize) < epoch_start
1193            || (slice.index as usize) + (slice.len as usize) > epoch_end
1194        {
1195            return Err(InternalTermError::InvalidSlice(*slice));
1196        }
1197        Ok(())
1198    }
1199
1200    /// Produce a [`View`] of the given `term` that borrows from
1201    /// this [`Arena`].  This method decodes any inlined bytes and
1202    /// dereferences indexes into the arena to yield structured
1203    /// references.  See [`View`] for details.
1204    #[inline]
1205    pub fn view<'a>(&'a self, term: &'a Term) -> Result<View<'a>, TermError> {
1206        term.view(self)
1207    }
1208
1209    /// Convert a `value` into `Term`.
1210    #[inline]
1211    pub fn term<'a, T: IntoTerm>(&'a mut self, value: T) -> Term {
1212        value.into_term(self)
1213    }
1214
1215    /// Construct a new integer term.  The full 64 bit two's complement
1216    /// representation of `i` is stored in the payload.  No truncation
1217    /// occurs.
1218    #[inline]
1219    pub fn int(&mut self, i: impl Into<i64>) -> Term {
1220        Term::int(i)
1221    }
1222
1223    /// Construct a new floating point term.  The full 64 bit IEEE‑754
1224    /// bit pattern is stored in the payload without truncation.
1225    #[inline]
1226    pub fn real(&mut self, r: impl Into<f64>) -> Term {
1227        Term::real(r)
1228    }
1229
1230    /// Construct a new date term representing a Unix epoch in
1231    /// milliseconds.
1232    #[inline]
1233    pub fn date(&mut self, ms: impl Into<i64>) -> Term {
1234        Term::date(ms)
1235    }
1236
1237    /// Construct or intern an atom into the arena and produce a term
1238    /// referencing it.  Small atom names (≤14 bytes of UTF‑8) are
1239    /// inlined directly into the handle; longer names are interned
1240    /// into the arena and referenced by index and length.
1241    #[inline]
1242    pub fn atom(&mut self, name: impl AsRef<str>) -> Term {
1243        Term::atom(self, name)
1244    }
1245
1246    /// Construct or intern a variable into the arena and produce a
1247    /// term referencing it.  Small variable names (≤14 bytes) are
1248    /// inlined directly into the handle; longer names are interned in
1249    /// the arena and referenced by index.
1250    #[inline]
1251    pub fn var(&mut self, name: impl AsRef<str>) -> Term {
1252        Term::var(self, name)
1253    }
1254
1255    /// Construct or intern a UTF‑8 string into the arena and produce a
1256    /// term referencing it.  Strings longer than 14 bytes are interned
1257    /// in the arena; shorter strings are inlined.  Invalid UTF‑8 will
1258    /// result in an error.
1259    #[inline]
1260    pub fn str(&mut self, s: impl AsRef<str>) -> Term {
1261        Term::str(self, s)
1262    }
1263
1264    /// Construct or intern a binary blob into the arena and produce a
1265    /// term referencing it.  Blobs longer than 14 bytes are interned
1266    /// in the arena; shorter blobs are inlined.
1267    #[inline]
1268    pub fn bin(&mut self, bytes: impl AsRef<[u8]>) -> Term {
1269        Term::bin(self, bytes)
1270    }
1271
1272    /// Construct a new compound term by interning the functor and
1273    /// arguments in the arena.  The returned term references a slice
1274    /// in the arena's term storage consisting of the functor atom as
1275    /// the first entry followed by the argument handles.  A functor of
1276    /// arity zero results in an atom.
1277    #[inline]
1278    pub fn func(
1279        &mut self,
1280        functor: impl AsRef<str>,
1281        args: impl IntoIterator<Item = impl IntoTerm>,
1282    ) -> Term {
1283        Term::func(self, functor, args)
1284    }
1285
1286    /// Construct a new compound term by interning the functor and its arguments
1287    /// into the arena as a sequence of terms (functor first, then arguments).
1288    /// A functor with no arguments yields the atom itself.  Errors if
1289    /// no functor is provided or if the first term is not an atom.
1290    #[inline]
1291    pub fn funcv(
1292        &mut self,
1293        terms: impl IntoIterator<Item = impl IntoTerm>,
1294    ) -> Result<Term, TermError> {
1295        Term::funcv(self, terms)
1296    }
1297
1298    /// Constructs a new list. A list is represented internally as an
1299    /// array of terms. If `terms` is empty, returns `nil`.
1300    #[inline]
1301    pub fn list(&mut self, terms: impl IntoIterator<Item = impl IntoTerm>) -> Term {
1302        Term::list(self, terms)
1303    }
1304
1305    /// Constructs a new improper list. An improper list is represented as
1306    /// a list and additional argument. If `terms` is empty, returns `nil`.
1307    #[inline]
1308    pub fn listc(
1309        &mut self,
1310        terms: impl IntoIterator<Item = impl IntoTerm>,
1311        tail: impl IntoTerm,
1312    ) -> Term {
1313        Term::listc(self, terms, tail)
1314    }
1315
1316    /// Constructs a new tuple. A tuple is represented internally as an array
1317    /// of terms.
1318    #[inline]
1319    pub fn tuple(&mut self, terms: impl IntoIterator<Item = impl IntoTerm>) -> Term {
1320        Term::tuple(self, terms)
1321    }
1322
1323    /// Constant representing the zero‑arity tuple (unit).  Internally
1324    /// this is the atom `"unit"` encoded as a small atom.  It may
1325    /// be copied freely and does not depend on any arena.
1326    pub const UNIT: Term = Term::UNIT;
1327
1328    /// Constant representing the empty list (nil).  Internally this is
1329    /// the atom `"nil"` encoded as a small atom.  It may be copied
1330    /// freely and does not depend on any arena.
1331    pub const NIL: Term = Term::NIL;
1332
1333    /// Returns the name of a compound term, atom, or variable.
1334    /// Use [`unpack_atom`], [`unpack_func`], or [`unpack_var`]
1335    /// to ensure the term is of a specific kind.
1336    #[inline]
1337    pub fn name<'a>(&'a self, term: &'a Term) -> Result<&'a str, TermError> {
1338        match self.view(term)? {
1339            View::Var(name) | View::Atom(name) => Ok(name),
1340            View::Func(ar, functor, _) => Ok(functor.atom_name(ar)?),
1341            _ => Err(TermError::UnexpectedKind {
1342                expected: "var, atom, func",
1343                found: term.kind_name(),
1344            }),
1345        }
1346    }
1347
1348    /// Returns the name of an atom,
1349    #[inline]
1350    pub fn atom_name<'a>(&'a self, term: &'a Term) -> Result<&'a str, TermError> {
1351        self.unpack_atom(term, &[])
1352    }
1353
1354    /// Returns the name of a variable.
1355    #[inline]
1356    pub fn var_name<'a>(&'a self, term: &'a Term) -> Result<&'a str, TermError> {
1357        self.unpack_var(term, &[])
1358    }
1359
1360    /// Returns the name of a compund term.
1361    #[inline]
1362    pub fn func_name<'a>(&'a self, term: &'a Term) -> Result<&'a str, TermError> {
1363        let (functor, _) = self.unpack_func_any(term, &[])?;
1364        self.atom_name(functor)
1365    }
1366
1367    /// Returns the value if `term` is an integer, otherwise an error.
1368    #[inline]
1369    pub fn unpack_int(&self, term: &Term) -> Result<i64, TermError> {
1370        match self.view(term)? {
1371            View::Int(v) => Ok(v),
1372            _ => Err(TermError::UnexpectedKind {
1373                expected: "int",
1374                found: term.kind_name(),
1375            }),
1376        }
1377    }
1378
1379    /// Returns the value if `term` is a real, otherwise an error.
1380    #[inline]
1381    pub fn unpack_real(&self, term: &Term) -> Result<f64, TermError> {
1382        match self.view(term)? {
1383            View::Real(v) => Ok(v),
1384            _ => Err(TermError::UnexpectedKind {
1385                expected: "real",
1386                found: term.kind_name(),
1387            }),
1388        }
1389    }
1390
1391    /// Returns the value if `term` is a date, otherwise an error.
1392    #[inline]
1393    pub fn unpack_date(&self, term: &Term) -> Result<i64, TermError> {
1394        match self.view(term)? {
1395            View::Date(v) => Ok(v),
1396            _ => Err(TermError::UnexpectedKind {
1397                expected: "date",
1398                found: term.kind_name(),
1399            }),
1400        }
1401    }
1402
1403    /// Returns the string slice if `term` is a string, otherwise an error.
1404    #[inline]
1405    pub fn unpack_str<'a>(&'a self, term: &'a Term) -> Result<&'a str, TermError> {
1406        match self.view(term)? {
1407            View::Str(v) => Ok(v),
1408            _ => Err(TermError::UnexpectedKind {
1409                expected: "str",
1410                found: term.kind_name(),
1411            }),
1412        }
1413    }
1414
1415    /// Returns the slice if `term` is a binary blob, otherwise an error.
1416    #[inline]
1417    pub fn unpack_bin<'a>(&'a self, term: &'a Term) -> Result<&'a [u8], TermError> {
1418        match self.view(term)? {
1419            View::Bin(v) => Ok(v),
1420            _ => Err(TermError::UnexpectedKind {
1421                expected: "bin",
1422                found: term.kind_name(),
1423            }),
1424        }
1425    }
1426
1427    /// Returns the name if `term` is an atom, otherwise an error.
1428    #[inline]
1429    pub fn unpack_atom<'a>(
1430        &'a self,
1431        term: &'a Term,
1432        allowed_names: &[&str],
1433    ) -> Result<&'a str, TermError> {
1434        match self.view(term)? {
1435            View::Atom(name) => {
1436                if !allowed_names.is_empty() && !allowed_names.contains(&name) {
1437                    return Err(TermError::UnexpectedName(*term));
1438                }
1439                Ok(name)
1440            }
1441            _ => Err(TermError::UnexpectedKind {
1442                expected: "atom",
1443                found: term.kind_name(),
1444            }),
1445        }
1446    }
1447
1448    /// Returns the name if `term` is a variable, otherwise an error.
1449    #[inline]
1450    pub fn unpack_var<'a>(
1451        &'a self,
1452        term: &'a Term,
1453        allowed_names: &[&str],
1454    ) -> Result<&'a str, TermError> {
1455        match self.view(term)? {
1456            View::Var(name) => {
1457                if !allowed_names.is_empty() && !allowed_names.contains(&name) {
1458                    return Err(TermError::UnexpectedName(*term));
1459                }
1460                Ok(name)
1461            }
1462            _ => Err(TermError::UnexpectedKind {
1463                expected: "var",
1464                found: term.kind_name(),
1465            }),
1466        }
1467    }
1468
1469    /// Returns the name and arguments if `term` is a compound term of any arity
1470    /// or an atom and its name is in `allowed_names` (or if `allowed_names` is empty),
1471    /// otherwise returns an error.
1472    #[inline]
1473    pub fn unpack_func_any<'a>(
1474        &'a self,
1475        term: &'a Term,
1476        allowed_names: &[&str],
1477    ) -> Result<(&'a Term, &'a [Term]), TermError> {
1478        match self.view(term)? {
1479            View::Atom(name) => {
1480                if !allowed_names.is_empty() && !allowed_names.contains(&name) {
1481                    return Err(TermError::UnexpectedName(*term));
1482                }
1483                Ok((term, &[] as &[Term]))
1484            }
1485            View::Func(_, functor, args) => {
1486                if args.is_empty() {
1487                    return Err(TermError::InvalidTerm(*term));
1488                }
1489                if !allowed_names.is_empty() {
1490                    let name = self.atom_name(functor)?;
1491                    if !allowed_names.contains(&name) {
1492                        return Err(TermError::UnexpectedName(*term));
1493                    }
1494                }
1495                Ok((functor, args))
1496            }
1497            _ => Err(TermError::UnexpectedKind {
1498                expected: "func",
1499                found: term.kind_name(),
1500            }),
1501        }
1502    }
1503
1504    /// Returns the name and arguments if `term` is a compound term of arity `ARITY`
1505    /// (or an atom if `ARITY == 0`) and its name is in `allowed_names` (or if `allowed_names` is empty),
1506    /// otherwise returns an error.
1507    #[inline]
1508    pub fn unpack_func<'a, const ARITY: usize>(
1509        &'a self,
1510        term: &'a Term,
1511        allowed_names: &[&str],
1512    ) -> Result<(&'a Term, [Term; ARITY]), TermError> {
1513        let (functor, args) = self.unpack_func_any(term, allowed_names)?;
1514        if args.len() != ARITY {
1515            return Err(TermError::UnexpectedArity {
1516                expected: ARITY,
1517                found: args.len(),
1518            });
1519        }
1520        let arr: [_; ARITY] = args.try_into().unwrap();
1521        return Ok((functor, arr));
1522    }
1523
1524    /// Returns the slice with list elements and the tail if `term` is a list,
1525    /// otherwise returns an error.
1526    #[inline]
1527    pub fn unpack_list<'a>(&'a self, term: &'a Term) -> Result<(&'a [Term], &'a Term), TermError> {
1528        match self.view(term)? {
1529            View::Atom(_) if term == &Term::NIL => Ok((&[], &Term::NIL)),
1530            View::List(_, terms, tail) => Ok((terms, tail)),
1531            _ => Err(TermError::UnexpectedKind {
1532                expected: "list",
1533                found: term.kind_name(),
1534            }),
1535        }
1536    }
1537
1538    /// Returns the slice with tuple elements if `term` is a tuple of any arity,
1539    /// otherwise returns an error.
1540    #[inline]
1541    pub fn unpack_tuple_any<'a>(&'a self, term: &'a Term) -> Result<&'a [Term], TermError> {
1542        match self.view(term)? {
1543            View::Atom(_) if *term == Term::UNIT => Ok(&[]),
1544            View::Tuple(_, terms) => Ok(terms),
1545            _ => Err(TermError::UnexpectedKind {
1546                expected: "tuple",
1547                found: term.kind_name(),
1548            }),
1549        }
1550    }
1551
1552    /// Returns the tuple elements if `term` is a tuple of arity `ARITY`,
1553    /// otherwise returns an error.
1554    #[inline]
1555    pub fn unpack_tuple<const ARITY: usize>(
1556        &self,
1557        term: &Term,
1558    ) -> Result<[Term; ARITY], TermError> {
1559        let terms = self.unpack_tuple_any(term)?;
1560        if terms.len() != ARITY {
1561            return Err(TermError::UnexpectedArity {
1562                expected: ARITY,
1563                found: terms.len(),
1564            });
1565        }
1566        let arr: [_; ARITY] = terms.try_into().unwrap();
1567        return Ok(arr);
1568    }
1569
1570    /// Intern a UTF‑8 string into the arena and return its slice
1571    /// descriptor.  Strings are stored in a contiguous bump vector.
1572    #[inline]
1573    fn intern_str(&mut self, s: &str) -> Slice {
1574        let index = self.bytes.len();
1575        self.bytes.extend_from_slice(s.as_bytes());
1576        let len = s.len();
1577        Slice {
1578            epoch_id: self.epoch_ids[self.current_epoch],
1579            index: index as u32,
1580            len: len as u32,
1581        }
1582    }
1583
1584    /// Intern a binary blob into the arena and return its slice descriptor.
1585    #[inline]
1586    fn intern_bytes(&mut self, bytes: &[u8]) -> Slice {
1587        let index = self.bytes.len();
1588        self.bytes.extend_from_slice(bytes);
1589        let len = bytes.len();
1590        Slice {
1591            epoch_id: self.epoch_ids[self.current_epoch],
1592            index: index as u32,
1593            len: len as u32,
1594        }
1595    }
1596
1597    /// Intern a compound term slice (functor + args) into the term arena.
1598    #[inline]
1599    fn intern_func(
1600        &mut self,
1601        functor: Term,
1602        args: impl IntoIterator<Item = impl IntoTerm>,
1603    ) -> Slice {
1604        let index = self.terms.len();
1605        self.terms.push(functor);
1606        for x in args {
1607            let t = x.into_term(self);
1608            self.terms.push(t);
1609        }
1610        let len = self.terms.len() - index;
1611        Slice {
1612            epoch_id: self.epoch_ids[self.current_epoch],
1613            index: index as u32,
1614            len: len as u32,
1615        }
1616    }
1617
1618    /// Intern a seq term slice into the term arena.
1619    #[inline]
1620    fn intern_seq(&mut self, terms: impl IntoIterator<Item = impl IntoTerm>) -> Slice {
1621        let index = self.terms.len();
1622        for x in terms {
1623            let t = x.into_term(self);
1624            self.terms.push(t);
1625        }
1626        let len = self.terms.len() - index;
1627        Slice {
1628            epoch_id: self.epoch_ids[self.current_epoch],
1629            index: index as u32,
1630            len: len as u32,
1631        }
1632    }
1633
1634    /// Intern a seq term slice plus tail into the term arena.
1635    #[inline]
1636    fn intern_seq_plus_one(
1637        &mut self,
1638        terms: impl IntoIterator<Item = impl IntoTerm>,
1639        tail: impl IntoTerm,
1640    ) -> Slice {
1641        let index = self.terms.len();
1642        for x in terms {
1643            let t = x.into_term(self);
1644            self.terms.push(t);
1645        }
1646        let t = tail.into_term(self);
1647        self.terms.push(t);
1648        let len = self.terms.len() - index;
1649        Slice {
1650            epoch_id: self.epoch_ids[self.current_epoch],
1651            index: index as u32,
1652            len: len as u32,
1653        }
1654    }
1655
1656    /// Borrow a slice of bytes stored in the arena.
1657    /// should not be called directly by users; instead use
1658    /// [`Term::view`].
1659    #[inline]
1660    fn byte_slice<'a>(&'a self, slice: &Slice) -> Result<&'a [u8], InternalTermError> {
1661        self.verify_byte_slice(slice)?;
1662        Ok(&self.bytes[(slice.index as usize)..((slice.index + slice.len) as usize)])
1663    }
1664
1665    /// Borrow a slice of terms comprising a compound term.
1666    #[inline]
1667    fn term_slice<'a>(&'a self, slice: &Slice) -> Result<&'a [Term], InternalTermError> {
1668        self.verify_term_slice(slice)?;
1669        Ok(&self.terms[(slice.index as usize)..((slice.index + slice.len) as usize)])
1670    }
1671}
1672
1673/// Errors that may occur when constructing terms or interacting with arena.
1674#[derive(Debug, Clone)]
1675pub enum TermError {
1676    InvalidTerm(Term),
1677    LiveEpochsExceeded,
1678    InvalidEpoch(EpochID),
1679    MissingFunctor,
1680    InvalidFunctor(Term),
1681    UnexpectedKind {
1682        expected: &'static str,
1683        found: &'static str,
1684    },
1685    UnexpectedArity {
1686        expected: usize,
1687        found: usize,
1688    },
1689    UnexpectedName(Term),
1690}
1691
1692impl fmt::Display for TermError {
1693    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1694        match self {
1695            TermError::InvalidTerm(term) => {
1696                write!(f, "Invalid term {:?}", term)
1697            }
1698            TermError::LiveEpochsExceeded => {
1699                write!(f, "Epoch overflow")
1700            }
1701            TermError::InvalidEpoch(epoch_id) => {
1702                write!(f, "Invalid epoch {:?}", epoch_id)
1703            }
1704            TermError::MissingFunctor => {
1705                write!(f, "Missing functor")
1706            }
1707            TermError::InvalidFunctor(term) => {
1708                write!(f, "Invalid functor {:?}", term)
1709            }
1710            TermError::UnexpectedKind { expected, found } => {
1711                write!(f, "Type mismatch: expected {}, found {}", expected, found)
1712            }
1713            TermError::UnexpectedArity { expected, found } => {
1714                write!(f, "Arity mismatch: expected {}, found {}", expected, found)
1715            }
1716            TermError::UnexpectedName(term) => {
1717                write!(f, "Unexpected name in {:?}", term)
1718            }
1719        }
1720    }
1721}
1722
1723/// Internal errors that may occur when constructing terms or interacting with arena.
1724#[derive(Debug, Clone)]
1725enum InternalTermError {
1726    InvalidEpoch(EpochID),
1727    InvalidSlice(Slice),
1728}
1729
1730impl fmt::Display for InternalTermError {
1731    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1732        match self {
1733            InternalTermError::InvalidEpoch(epoch_id) => {
1734                write!(f, "Invalid epoch {:?}", epoch_id)
1735            }
1736            InternalTermError::InvalidSlice(slice) => {
1737                write!(f, "Invalid slice {:?}", slice)
1738            }
1739        }
1740    }
1741}
1742
1743impl std::error::Error for TermError {}
1744
1745impl<'a> PartialEq for View<'a> {
1746    fn eq(&self, other: &Self) -> bool {
1747        let order_a = kind_order(self);
1748        let order_b = kind_order(other);
1749        if order_a != order_b {
1750            return false;
1751        }
1752        match (self, other) {
1753            // Numbers: compare by numeric value irrespective of the exact type.
1754            (
1755                View::Int(_) | View::Real(_) | View::Date(_),
1756                View::Int(_) | View::Real(_) | View::Date(_),
1757            ) => {
1758                let a = numeric_value(self);
1759                let b = numeric_value(other);
1760                a == b
1761            }
1762            // Variables
1763            (View::Var(a), View::Var(b)) => a == b,
1764            // Atoms
1765            (View::Atom(a), View::Atom(b)) => a == b,
1766            // Strings
1767            (View::Str(a), View::Str(b)) => a == b,
1768            // Binaries
1769            (View::Bin(a), View::Bin(b)) => a == b,
1770            // Compounds: compare by length (arity+1) then by slice index.
1771            (View::Func(arena_a, functor_a, args_a), View::Func(arena_b, functor_b, args_b)) => {
1772                if args_a.len() != args_b.len() {
1773                    return false;
1774                }
1775                if functor_a != functor_b {
1776                    return false;
1777                }
1778                args_a.iter().zip(args_b.iter()).all(|(a, b)| {
1779                    a.view(arena_a).expect("arena mismatch")
1780                        == b.view(arena_b).expect("arena mismatch")
1781                })
1782            }
1783            (View::List(arena_a, args_a, tail_a), View::List(arena_b, args_b, tail_b)) => {
1784                if args_a.len() != args_b.len() {
1785                    return false;
1786                }
1787                args_a.iter().zip(args_b.iter()).all(|(a, b)| {
1788                    a.view(arena_a).expect("arena mismatch")
1789                        == b.view(arena_b).expect("arena mismatch")
1790                }) && tail_a.view(arena_a).expect("arena mismatch")
1791                    == tail_b.view(arena_b).expect("arena mismatch")
1792            }
1793            (View::Tuple(arena_a, args_a), View::Tuple(arena_b, args_b)) => {
1794                if args_a.len() != args_b.len() {
1795                    return false;
1796                }
1797                args_a.iter().zip(args_b.iter()).all(|(a, b)| {
1798                    a.view(arena_a).expect("arena mismatch")
1799                        == b.view(arena_b).expect("arena mismatch")
1800                })
1801            }
1802            _ => unreachable!(),
1803        }
1804    }
1805}
1806
1807impl<'a> Eq for View<'a> {}
1808
1809impl core::cmp::PartialOrd for View<'_> {
1810    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
1811        Some(self.cmp(other))
1812    }
1813}
1814
1815impl core::cmp::Ord for View<'_> {
1816    fn cmp(&self, other: &Self) -> Ordering {
1817        let order_a = kind_order(self);
1818        let order_b = kind_order(other);
1819        if order_a != order_b {
1820            return order_a.cmp(&order_b);
1821        }
1822        match (self, other) {
1823            // Numbers: compare by numeric value irrespective of the exact type.
1824            (
1825                View::Int(_) | View::Real(_) | View::Date(_),
1826                View::Int(_) | View::Real(_) | View::Date(_),
1827            ) => {
1828                let a = numeric_value(self);
1829                let b = numeric_value(other);
1830                a.total_cmp(&b)
1831            }
1832            // Variables
1833            (View::Var(a), View::Var(b)) => a.cmp(b),
1834            // Atoms
1835            (View::Atom(a), View::Atom(b)) => a.cmp(b),
1836            // Strings
1837            (View::Str(a), View::Str(b)) => a.cmp(b),
1838            // Binaries
1839            (View::Bin(a), View::Bin(b)) => a.cmp(b),
1840            // Compounds: compare by length (arity+1) then by slice index.
1841            (View::Func(arena_a, functor_a, args_a), View::Func(arena_b, functor_b, args_b)) => {
1842                let ord = args_a.len().cmp(&args_b.len());
1843                if ord != Ordering::Equal {
1844                    return ord;
1845                }
1846                let ord = functor_a
1847                    .view(arena_a)
1848                    .expect("arena mismatch")
1849                    .cmp(&functor_b.view(arena_b).expect("arena mismatch"));
1850                if ord != Ordering::Equal {
1851                    return ord;
1852                }
1853                for (arg_a, arg_b) in args_a.iter().zip(args_b.iter()).map(|(a, b)| {
1854                    (
1855                        a.view(arena_a).expect("arena mismatch"),
1856                        b.view(arena_b).expect("arena mismatch"),
1857                    )
1858                }) {
1859                    let ord = arg_a.cmp(&arg_b);
1860                    if ord != Ordering::Equal {
1861                        return ord;
1862                    }
1863                }
1864                Ordering::Equal
1865            }
1866            (View::List(arena_a, args_a, tail_a), View::List(arena_b, args_b, tail_b)) => {
1867                let ord = args_a.len().cmp(&args_b.len());
1868                if ord != Ordering::Equal {
1869                    return ord;
1870                }
1871                for (arg_a, arg_b) in args_a.iter().zip(args_b.iter()).map(|(a, b)| {
1872                    (
1873                        a.view(arena_a).expect("arena mismatch"),
1874                        b.view(arena_b).expect("arena mismatch"),
1875                    )
1876                }) {
1877                    let ord = arg_a.cmp(&arg_b);
1878                    if ord != Ordering::Equal {
1879                        return ord;
1880                    }
1881                }
1882                tail_a
1883                    .view(arena_a)
1884                    .expect("arena mismatch")
1885                    .cmp(&tail_b.view(arena_b).expect("arena mismatch"))
1886            }
1887            (View::Tuple(arena_a, args_a), View::Tuple(arena_b, args_b)) => {
1888                let ord = args_a.len().cmp(&args_b.len());
1889                if ord != Ordering::Equal {
1890                    return ord;
1891                }
1892                for (arg_a, arg_b) in args_a.iter().zip(args_b.iter()).map(|(a, b)| {
1893                    (
1894                        a.view(arena_a).expect("arena mismatch"),
1895                        b.view(arena_b).expect("arena mismatch"),
1896                    )
1897                }) {
1898                    let ord = arg_a.cmp(&arg_b);
1899                    if ord != Ordering::Equal {
1900                        return ord;
1901                    }
1902                }
1903                Ordering::Equal
1904            }
1905
1906            _ => unreachable!(),
1907        }
1908    }
1909}
1910
1911/// Compute the kind order used for comparing terms of different kinds.
1912/// According to Prolog standard order: variables < numbers < atoms < strings
1913/// < binaries < compounds.
1914fn kind_order(t: &View) -> u8 {
1915    match t {
1916        View::Var(_) => 0,
1917        View::Int(_) => 1,
1918        View::Date(_) => 2,
1919        View::Real(_) => 3,
1920        View::Atom(_) => 4,
1921        View::Str(_) => 5,
1922        View::Func(_, _, _) => 6,
1923        View::Tuple(_, _) => 7,
1924        View::List(_, _, _) => 8,
1925        View::Bin(_) => 9,
1926    }
1927}
1928
1929/// Extract a numeric value from a term for ordering purposes.  All
1930/// numeric kinds (int, real and date) are converted to `f64` for
1931/// comparison.  `Date` values use their millisecond representation as
1932/// the numeric value.
1933fn numeric_value(t: &View) -> f64 {
1934    match t {
1935        View::Int(i) => *i as f64,
1936        View::Real(f) => *f,
1937        View::Date(d) => *d as f64,
1938        _ => unreachable!(),
1939    }
1940}
1941
1942/// Convenience macros to construct func, list and tuple.
1943#[macro_export]
1944macro_rules! list {
1945    // with tail, explicit arena
1946    ($($arg:expr),* $(,)?; $tail:expr => $arena:expr) => {
1947        $crate::list!($($arg),* ; $tail)($arena)
1948    };
1949    // without tail, explicit arena
1950    ($($arg:expr),* $(,)? => $arena:expr) => {
1951        $crate::list!($($arg),*)($arena)
1952    };
1953    // with tail, implicit arena
1954    ($($arg:expr),* $(,)?; $tail:expr) => { (|__arena: &mut $crate::Arena| {
1955        let __args: &[$crate::Term] = &[$($arg.into_term(__arena)),*];
1956        let __tail: Term = $tail.into_term(__arena);
1957        __arena.listc(__args, __tail)
1958    })};
1959    // without tail, implicit arena
1960    ($($arg:expr),* $(,)?) => { (|__arena: &mut $crate::Arena| {
1961        let __args: &[$crate::Term] = &[$($arg.into_term(__arena)),*];
1962        __arena.list(__args)
1963    })};
1964}
1965
1966#[macro_export]
1967macro_rules! tuple {
1968    // explicit arena
1969    ($($arg:expr),* $(,)? => $arena:expr) => {
1970        $crate::tuple!($($arg),*)($arena)
1971    };
1972    // implicit arena
1973    ($($arg:expr),* $(,)?) => { (|__arena: &mut $crate::Arena| {
1974        let __args: &[$crate::Term] = &[$($arg.into_term(__arena)),*];
1975        __arena.tuple(__args)
1976    })};
1977}
1978
1979#[macro_export]
1980macro_rules! func {
1981    // explicit arena
1982    ($functor:expr; $($arg:expr),+ $(,)? => $arena:expr) => {
1983        $crate::func!($functor; $($arg),+)($arena)
1984    };
1985    // implicit arena
1986    ($functor:expr; $($arg:expr),+ $(,)?) => { (|__arena: &mut $crate::Arena| {
1987        let __args: &[$crate::Term] = &[$($arg.into_term(__arena)),+];
1988        __arena.func($functor, __args)
1989    })};
1990}
1991
1992#[macro_export]
1993macro_rules! atom {
1994    // explicit arena
1995    ($functor:expr => $arena:expr) => {
1996        $crate::atom!($functor)($arena)
1997    };
1998    // implicit arena
1999    ($functor:expr) => {
2000        (|__arena: &mut $crate::Arena| __arena.atom($functor))
2001    };
2002}
2003
2004#[macro_export]
2005macro_rules! var {
2006    // explicit arena
2007    ($name:expr => $arena:expr) => {
2008        $crate::var!($name)($arena)
2009    };
2010    // implicit arena
2011    ($name:expr) => {
2012        (|__arena: &mut $crate::Arena| __arena.var($name))
2013    };
2014}
2015
2016#[macro_export]
2017macro_rules! date {
2018    ($value:expr) => {
2019        $crate::Term::date($value)
2020    };
2021}
2022
2023#[macro_export]
2024macro_rules! unit {
2025    () => {
2026        $crate::Term::UNIT
2027    };
2028}
2029
2030#[macro_export]
2031macro_rules! nil {
2032    () => {
2033        $crate::Term::NIL
2034    };
2035}
2036
2037/// A wrapper that ties together a [`Term`] and its [`Arena`], forming the
2038/// basis for configurable pretty-printing. This type is designed as the
2039/// foundation on which flexible formatting and printing of terms will be built.
2040///
2041/// It already implements [`fmt::Display`], so you can seamlessly use it with
2042/// standard formatting macros (`format!`, `println!`, etc.) to render
2043/// terms. In the future, it will also support additional, customizable
2044/// formatting options for advanced pretty-printing.
2045///
2046/// ### Example
2047/// ```rust
2048/// use arena_terms::{Term, Arena, func, IntoTerm};
2049/// let mut arena = Arena::new();
2050/// let term = func!("foo"; 1, "hello, world!" => &mut arena);
2051///
2052/// println!("{}", term.display(&arena));
2053/// ```
2054///
2055/// Construct instances via [`Term::display`] or [`Arena::display`].
2056pub struct TermDisplay<'a> {
2057    /// The interned term to display.
2058    term: &'a Term,
2059    /// The arena where the term is stored.
2060    arena: &'a Arena,
2061}
2062
2063impl Term {
2064    /// Return a [`TermDisplay`] suitable for formatting with [`fmt::Display`].
2065    ///
2066    /// Use this method when you want to render a term:
2067    ///
2068    /// ```ignore
2069    /// println!("{}", term.display(&arena));
2070    /// ```
2071    #[inline]
2072    pub fn display<'a>(&'a self, arena: &'a Arena) -> TermDisplay<'a> {
2073        TermDisplay { term: self, arena }
2074    }
2075}
2076
2077/// Implements [`fmt::Display`] for [`TermDisplay`], enabling it to be
2078/// formatted and printed with standard formatting macros.
2079impl<'a> fmt::Display for TermDisplay<'a> {
2080    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2081        fn is_unquoted_atom(s: &str) -> bool {
2082            let mut chars = s.chars();
2083            match chars.next() {
2084                Some(c) if c.is_ascii_lowercase() => {}
2085                _ => return false,
2086            }
2087            chars.all(|c| c.is_ascii_alphanumeric() || c == '_')
2088        }
2089
2090        fn write_atom(f: &mut fmt::Formatter<'_>, arena: &Arena, functor: &Term) -> fmt::Result {
2091            let s = arena.atom_name(functor).map_err(|_e| fmt::Error)?;
2092            write_atom_str(f, s)
2093        }
2094
2095        fn write_atom_str(f: &mut fmt::Formatter<'_>, s: &str) -> fmt::Result {
2096            if s.is_empty() || !is_unquoted_atom(s) {
2097                let escaped = s.replace('\'', "\\'");
2098                write!(f, "'{}'", escaped)
2099            } else {
2100                write!(f, "{}", s)
2101            }
2102        }
2103
2104        fn write_str_quoted(f: &mut fmt::Formatter<'_>, s: &str) -> fmt::Result {
2105            let mut out = String::new();
2106            out.push('"');
2107            for ch in s.chars() {
2108                match ch {
2109                    '\\' => out.push_str("\\\\"),
2110                    '"' => out.push_str("\\\""),
2111                    '\n' => out.push_str("\\n"),
2112                    '\r' => out.push_str("\\r"),
2113                    '\t' => out.push_str("\\t"),
2114                    c if c.is_control() => out.push_str(&format!("\\x{:02X}\\", c as u32)),
2115                    c => out.push(c),
2116                }
2117            }
2118            out.push('"');
2119            f.write_str(&out)
2120        }
2121
2122        fn epoch_to_date_string(epoch_ms: i64, fmt: Option<&str>) -> String {
2123            use chrono::{DateTime, Utc};
2124
2125            let secs = epoch_ms.div_euclid(1000);
2126            let nsecs = (epoch_ms.rem_euclid(1000) * 1_000_000) as u32;
2127
2128            let dt_utc = DateTime::<Utc>::from_timestamp(secs, nsecs).unwrap();
2129
2130            String::from(match fmt {
2131                None => dt_utc.to_rfc3339(),
2132                Some(layout) => dt_utc.format(layout).to_string(),
2133            })
2134        }
2135
2136        fn write_args(f: &mut fmt::Formatter<'_>, arena: &Arena, args: &[Term]) -> fmt::Result {
2137            for (i, t) in args.iter().enumerate() {
2138                if i > 0 {
2139                    f.write_str(", ")?;
2140                }
2141                write!(f, "{}", t.display(arena))?;
2142            }
2143            Ok(())
2144        }
2145
2146        match self.term.view(self.arena).map_err(|_e| fmt::Error)? {
2147            View::Int(i) => write!(f, "{i}"),
2148            View::Real(r) => {
2149                if r.fract() == 0.0 {
2150                    write!(f, "{:.1}", r)
2151                } else {
2152                    write!(f, "{}", r)
2153                }
2154            }
2155            View::Date(epoch) => write!(f, "date{{{}}}", epoch_to_date_string(epoch, None)),
2156            View::Str(s) => write_str_quoted(f, s),
2157            View::Bin(bytes) => {
2158                write!(f, "hex{{")?;
2159                for b in bytes {
2160                    write!(f, "{:02X}", b)?;
2161                }
2162                write!(f, "}}")
2163            }
2164            View::Atom(a) => write_atom_str(f, a),
2165            View::Var(v) => write!(f, "{}", v),
2166
2167            View::Func(ar, functor, args) => {
2168                if args.is_empty() {
2169                    return write!(f, "/* invalid Func */");
2170                }
2171                write_atom(f, ar, functor)?;
2172                write!(f, "(")?;
2173                write_args(f, ar, args)?;
2174                write!(f, ")")
2175            }
2176
2177            View::Tuple(ar, items) => {
2178                if items.is_empty() {
2179                    write!(f, "()")
2180                } else {
2181                    write!(f, "(")?;
2182                    write_args(f, ar, items)?;
2183                    write!(f, ")")
2184                }
2185            }
2186
2187            View::List(ar, items, tail) => {
2188                if items.is_empty() {
2189                    write!(f, "[]")
2190                } else {
2191                    write!(f, "[")?;
2192                    write_args(f, ar, items)?;
2193                    if *tail != Term::NIL {
2194                        f.write_str(" | ")?;
2195                        write!(f, "{}", tail.display(ar))?;
2196                    }
2197                    write!(f, "]")
2198                }
2199            }
2200        }
2201    }
2202}
2203
2204#[cfg(test)]
2205mod tests {
2206    use super::*;
2207    use std::fmt::Write;
2208
2209    #[test]
2210    fn term_size_is_16_bytes() {
2211        assert_eq!(core::mem::size_of::<Term>(), 16);
2212    }
2213
2214    #[test]
2215    fn option_term_size_is_16_bytes() {
2216        assert_eq!(core::mem::size_of::<Option<Term>>(), 16);
2217    }
2218
2219    #[test]
2220    fn view_size_is_40_bytes() {
2221        assert_eq!(core::mem::size_of::<View>(), 40);
2222    }
2223
2224    #[test]
2225    fn option_view_size_is_40_bytes() {
2226        assert_eq!(core::mem::size_of::<Option<View>>(), 40);
2227    }
2228
2229    #[test]
2230    fn small_atom_interning() {
2231        let mut arena = Arena::new();
2232        let a1 = Term::atom(&mut arena, "foo");
2233        let a2 = Term::atom(&mut arena, "foo");
2234        assert_eq!(a1, a2);
2235        if let Ok(View::Atom(name)) = a1.view(&arena) {
2236            assert_eq!(name, "foo");
2237        } else {
2238            panic!("wrong view");
2239        }
2240    }
2241
2242    #[test]
2243    fn compound_construction_and_formatting() {
2244        let mut arena = Arena::new();
2245        let a = Term::int(1);
2246        let b = Term::real(2.0);
2247        let c = Term::date(1000);
2248        let d = Term::atom(&mut arena, "hello");
2249        let e = Term::var(&mut arena, "Hello");
2250        let f = Term::str(&mut arena, "A str\ning. Longer string.");
2251        let g = list![d, e, f => &mut arena];
2252        let h = tuple!(f, f => &mut arena);
2253        let p = Term::func(&mut arena, "point", &[a, b, c, d, e, f, g, h]);
2254        let p = func![
2255            "foo";
2256            Term::NIL,
2257            Term::UNIT,
2258            p,
2259            p,
2260            list![],
2261            list![a, b; c],
2262            => &mut arena
2263        ];
2264        dbg!(&p);
2265        dbg!(p.view(&arena).unwrap());
2266        dbg!(arena.stats());
2267        assert!(p.is_func());
2268        if let Ok(View::Func(_, functor, args)) = p.view(&arena) {
2269            assert_eq!(functor.atom_name(&arena).unwrap(), "foo");
2270            assert_eq!(p.arity(), 6);
2271            assert_eq!(args.len(), 6);
2272        } else {
2273            panic!("unexpected view");
2274        }
2275
2276        let s = format!("{}", p.display(&arena));
2277        assert_eq!(
2278            s,
2279            "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}])"
2280        );
2281    }
2282
2283    #[test]
2284    fn view_construction() {
2285        let mut a1 = Arena::new();
2286        let x = a1.atom("Hello, hello, quite long long string, world! X");
2287        dbg!(a1.view(&x).unwrap());
2288        dbg!(a1.stats());
2289        let p = list![x, x => &mut a1];
2290        dbg!(p);
2291        let v = a1.view(&p).unwrap();
2292        dbg!(v);
2293    }
2294
2295    #[test]
2296    #[should_panic]
2297    fn arena_mismatch() {
2298        let a1 = Arena::new();
2299        let mut a2 = Arena::new();
2300        let y = a2.str("Hello, hello, quite long long string, world! Y");
2301        dbg!(a1.view(&y).unwrap());
2302    }
2303
2304    #[test]
2305    #[should_panic]
2306    fn stale_term_str() {
2307        let mut a = Arena::new();
2308        let x = a.str("Hello, hello, quite long long string, world! Y");
2309        dbg!(&a);
2310        a.truncate(a.current_epoch()).unwrap();
2311        dbg!(a.view(&x).unwrap());
2312    }
2313
2314    #[test]
2315    #[should_panic]
2316    fn stale_term_list() {
2317        let mut a = Arena::new();
2318        let _x = list![1, 2, 3 => &mut a];
2319        let epoch = a.begin_epoch().unwrap();
2320        dbg!(&epoch);
2321        let y = list![4, 5, 6 => &mut a];
2322        dbg!(&a);
2323        a.truncate(epoch).unwrap();
2324        dbg!(&a);
2325        dbg!(a.view(&y).unwrap());
2326    }
2327
2328    #[test]
2329    fn big_term() {
2330        let mut a1 = Arena::new();
2331        let x = a1.atom("Hello, hello, quite long long string, world! X");
2332        let p = a1.func("foo", vec![x; 1_000_000]);
2333        assert!(p.arity() == 1_000_000);
2334        dbg!(a1.stats());
2335    }
2336
2337    #[test]
2338    fn interface() {
2339        let a = &mut Arena::new();
2340        let s = String::from("x");
2341        let x1 = a.func(&s, &vec![Term::date(1000)]);
2342        let x2 = a.func(s.as_str(), vec![Term::date(1000)]);
2343        let x3 = a.func(s, &[Term::date(1000)]);
2344        let _x4 = a.func("x", [Term::date(1000)]);
2345        let _x5 = a.func("x", [x1, x2, x3]);
2346        let _x6 = a.func("x", (5..=6).map(|x| x as f64));
2347        let _x7 = a.func("x", vec![&x1, &x2, &x3]);
2348        let _x8 = a.func("x", &[x1, x2, x3]);
2349        let x9 = func!(
2350            String::from("aaa");
2351            x1, 1u8, 1i8, 2.0,
2352            "x",
2353            "X",
2354            atom!("ATOM"),
2355            var!("var"),
2356            "a string",
2357            b"a binary",
2358            1,
2359            2,
2360            3,
2361            4,
2362            6,
2363            unit!(),
2364            list![1, 2, 3; tuple!()],
2365            list![1, 2, 3; nil!()],
2366            => a
2367        );
2368        dbg!(a.view(&x9).unwrap());
2369        dbg!(a.stats());
2370    }
2371
2372    #[test]
2373    fn into_test() {
2374        let mut arena = Arena::new();
2375        // You can mix numbers and strings; IntoTerm will pick the right constructor.
2376        let t1 = arena.term(1);
2377        let t2 = arena.term(2.0);
2378        let t3 = arena.term("x");
2379        let t4 = arena.term(b"bin" as &[u8]);
2380        let point1 = arena.func("point", [t1, t2, t3, t4]);
2381        // Equivalent to:
2382        let t1 = Term::int(1);
2383        let t2 = Term::real(2.0);
2384        let t3 = Term::str(&mut arena, "x");
2385        let t4 = Term::bin(&mut arena, b"bin");
2386        let point2 = arena.func("point", [t1, t2, t3, t4]);
2387        assert_eq!(arena.view(&point1).unwrap(), arena.view(&point2).unwrap());
2388        dbg!(arena.view(&point1).unwrap());
2389
2390        // You can also provide closures returning Term.
2391        let lazy = Term::func(&mut arena, "lazy", [|arena: &mut Arena| arena.atom("ok")]);
2392        dbg!(arena.view(&lazy).unwrap());
2393
2394        let list = arena.list([1, 2, 3]);
2395        dbg!(arena.view(&list).unwrap());
2396    }
2397
2398    #[test]
2399    fn arena_truncate_test() {
2400        let a = &mut Arena::new();
2401
2402        let t1 = a.str("a".repeat(1000));
2403        let _t5 = atom!("x".repeat(100) => a);
2404        let _t6 = var!("X".repeat(200) => a);
2405        let _t7 = a.bin(b"x".repeat(5000));
2406        let epoch1 = a.begin_epoch().unwrap();
2407        dbg!(a.stats());
2408        dbg!(&epoch1);
2409        let t2 = a.str("b".repeat(2000));
2410        let t3 = a.bin(b"b".repeat(3000));
2411        let _t4 = list![t1, t2, t3];
2412        let _t5 = atom!("z".repeat(4000) => a);
2413        let _t8 = var!("Z".repeat(2000) => a);
2414        let _t7 = a.bin(b"z".repeat(10_000));
2415        let epoch2 = a.begin_epoch().unwrap();
2416        dbg!(a.stats());
2417        dbg!(&epoch2);
2418        a.truncate(epoch2).unwrap();
2419        dbg!(a.stats());
2420    }
2421
2422    #[test]
2423    fn funcv() {
2424        let a = &mut Arena::new();
2425        let xs = [a.atom("foo"), a.atom("x"), a.atom("y")];
2426        let x = a.funcv(xs).unwrap();
2427        let ys = [a.atom("x"), a.atom("y")];
2428        let y = a.func("foo", ys);
2429        assert_eq!(x.arity(), y.arity());
2430        if let Ok(View::Func(_, functor, args)) = x.view(&a) {
2431            assert_eq!(functor.name(a).unwrap(), "foo");
2432            assert_eq!(args.len(), 2);
2433        }
2434        if let Ok(View::Func(_, functor, args)) = y.view(&a) {
2435            assert_eq!(functor.name(a).unwrap(), "foo");
2436            assert_eq!(args.len(), 2);
2437        }
2438    }
2439
2440    #[test]
2441    fn unpack() {
2442        let a = &mut Arena::new();
2443        let xs = [a.atom("foo"), a.atom("x"), a.atom("y")];
2444        let x = a.funcv(xs).unwrap();
2445
2446        let (foo, [x, y]) = x.unpack_func(a, &["foo", "boo"]).unwrap();
2447        dbg!((foo, x, y));
2448
2449        let z = tuple!(1 => a);
2450        assert_eq!(z.arity(), 1);
2451    }
2452
2453    #[test]
2454    fn arity_primitives_and_lists_are_zero() {
2455        let a = &mut Arena::new();
2456
2457        let t_int = Term::int(42);
2458        let t_real = Term::real(3.14);
2459        let t_atom = Term::atom(a, "ok");
2460        let t_var = Term::var(a, "X");
2461        let t_str = Term::str(a, "hello");
2462        let t_bin = Term::bin(a, &[1, 2, 3, 4]);
2463        let t_list = Term::list(a, &[Term::int(1), Term::int(2), Term::int(3)]);
2464
2465        assert_eq!(t_int.arity(), 0);
2466        assert_eq!(t_real.arity(), 0);
2467        assert_eq!(t_atom.arity(), 0);
2468        assert_eq!(t_var.arity(), 0);
2469        assert_eq!(t_str.arity(), 0);
2470        assert_eq!(t_bin.arity(), 0);
2471        assert_eq!(t_list.arity(), 0); // lists are 0 by current implementation
2472    }
2473
2474    #[test]
2475    fn arity_for_tuples_and_funcs() {
2476        let a = &mut Arena::new();
2477
2478        let t2 = Term::tuple(a, &[Term::int(1), Term::int(2)]);
2479        let t3 = Term::tuple(a, &[Term::int(1), Term::int(2), Term::int(3)]);
2480        assert_eq!(t2.arity(), 2);
2481        assert_eq!(t3.arity(), 3);
2482
2483        let f0 = Term::func(a, "nilary", &[] as &[Term]); // creates an atom `nilary`
2484        let f2 = Term::func(a, "pair", &[Term::int(1), Term::int(2)]);
2485        let f3 = Term::func(a, "triple", &[Term::int(1), Term::int(2), Term::int(3)]);
2486
2487        assert_eq!(f0.arity(), 0);
2488        assert_eq!(f2.arity(), 2);
2489        assert_eq!(f3.arity(), 3);
2490    }
2491
2492    #[test]
2493    fn name_and_kind_name() {
2494        let a = &mut Arena::new();
2495
2496        let atom = Term::atom(a, "foo");
2497        let var = Term::var(a, "X");
2498        let fun = Term::func(a, "bar", &[Term::int(1)]);
2499        let tup = Term::tuple(a, &[Term::int(1), Term::int(2)]);
2500        let lst = Term::list(a, &[Term::int(1), Term::int(2), Term::int(3)]);
2501
2502        // `name()` should resolve atom/var names and compound heads.
2503        assert_eq!(atom.name(&a).unwrap(), "foo");
2504        assert_eq!(var.name(&a).unwrap(), "X");
2505        assert_eq!(fun.name(&a).unwrap(), "bar");
2506
2507        // `kind_name()` should report stable kind strings
2508        assert_eq!(atom.kind_name(), "atom");
2509        assert_eq!(var.kind_name(), "var");
2510        assert_eq!(fun.kind_name(), "func");
2511        assert_eq!(tup.kind_name(), "tuple");
2512        assert_eq!(lst.kind_name(), "list");
2513        assert_eq!(Term::int(7).kind_name(), "int");
2514        assert_eq!(Term::str(a, "s").kind_name(), "str");
2515    }
2516
2517    #[test]
2518    fn is_func_tuple_list() {
2519        let a = &mut Arena::new();
2520
2521        let f2 = Term::func(a, "pair", &[Term::int(1), Term::int(2)]);
2522        let tup = Term::tuple(a, &[Term::int(1), Term::int(2)]);
2523        let lst = Term::list(a, &[Term::int(1), Term::int(2), Term::int(3)]);
2524
2525        assert!(f2.is_func());
2526        assert!(tup.is_tuple());
2527        assert!(lst.is_list());
2528
2529        assert!(!f2.is_tuple());
2530        assert!(!f2.is_list());
2531        assert!(!tup.is_func());
2532        assert!(!lst.is_func());
2533        assert!(!lst.is_tuple());
2534    }
2535
2536    #[test]
2537    fn is_inline_obvious_cases() {
2538        let a = &mut Arena::new();
2539
2540        let i = Term::int(42);
2541        let f0 = Term::func(a, "nilary", &[] as &[Term]);
2542        let tup = Term::tuple(a, &[Term::int(1), Term::int(2)]);
2543        let lst = Term::list(a, &[Term::int(1), Term::int(2)]);
2544
2545        // Obvious truthy/falsey cases that don't depend on small/large thresholds.
2546        assert!(i.is_inline());
2547        assert!(f0.is_inline()); // f0 is an atom
2548        assert!(!tup.is_inline());
2549        assert!(!lst.is_inline());
2550    }
2551
2552    #[test]
2553    fn is_atom_var_str_bin_number_minimal() {
2554        let a = &mut Arena::new();
2555
2556        let at = Term::atom(a, "foo");
2557        let vr = Term::var(a, "X");
2558        let st = Term::str(a, "hi");
2559        let bi = Term::bin(a, &[1, 2, 3, 4]);
2560        let i = Term::int(7);
2561
2562        assert!(at.is_atom());
2563        assert!(vr.is_var());
2564        assert!(st.is_str());
2565        assert!(bi.is_bin());
2566
2567        assert!(!at.is_var());
2568        assert!(!vr.is_atom());
2569        assert!(!st.is_bin());
2570        assert!(!bi.is_str());
2571
2572        // is_number is true for ints (and, by spec, also for real/date—tested here with int).
2573        assert!(i.is_number());
2574        assert!(!at.is_number());
2575        assert!(!st.is_number());
2576        assert!(!bi.is_number());
2577    }
2578
2579    #[test]
2580    fn nil_and_tuple_edge_behavior() {
2581        let a = &mut Arena::new();
2582
2583        // NIL should count as a list per your is_list() (*self == Self::NIL)
2584        assert!(Term::NIL.is_list());
2585        assert!(!Term::NIL.is_tuple());
2586        assert!(!Term::NIL.is_func());
2587        assert_eq!(Term::NIL.arity(), 0);
2588
2589        // Regular tuple is a tuple; arity equals length.
2590        let t = Term::tuple(a, &[Term::int(1), Term::int(2), Term::int(3)]);
2591        assert!(t.is_tuple());
2592        assert_eq!(t.arity(), 3);
2593        assert!(!t.is_list());
2594        assert!(!t.is_func());
2595    }
2596
2597    #[test]
2598    fn arity_consistency_with_predicates() {
2599        let a = &mut Arena::new();
2600
2601        let f3 = Term::func(a, "triple", &[Term::int(1), Term::int(2), Term::int(3)]);
2602        let t2 = Term::tuple(a, &[Term::int(1), Term::int(2)]);
2603        let l2 = Term::list(a, &[Term::int(1), Term::int(2)]);
2604        let v = Term::var(a, "X");
2605
2606        assert!(f3.is_func());
2607        assert_eq!(f3.arity(), 3);
2608
2609        assert!(t2.is_tuple());
2610        assert_eq!(t2.arity(), 2);
2611
2612        assert!(l2.is_list());
2613        assert_eq!(l2.arity(), 0); // lists are defined as 0-arity
2614
2615        assert!(v.is_var());
2616        assert_eq!(v.arity(), 0); // variables are 0-arity
2617    }
2618
2619    #[test]
2620    fn name_and_kind_name_roundtrip() {
2621        let a = &mut Arena::new();
2622
2623        let atom = Term::atom(a, "foo");
2624        let var = Term::var(a, "X");
2625        let fun = Term::func(a, "bar", &[Term::int(1)]);
2626        let tup = Term::tuple(a, &[Term::int(1), Term::int(2)]);
2627        let lst = Term::list(a, &[Term::int(1), Term::int(2), Term::int(3)]);
2628
2629        // name(): atoms, variables, and compound heads resolve
2630        assert_eq!(atom.name(&a).unwrap(), "foo");
2631        assert_eq!(var.name(&a).unwrap(), "X");
2632        assert_eq!(fun.name(&a).unwrap(), "bar");
2633
2634        // kind_name(): stable kind labels
2635        assert_eq!(atom.kind_name(), "atom");
2636        assert_eq!(var.kind_name(), "var");
2637        assert_eq!(fun.kind_name(), "func");
2638        assert_eq!(tup.kind_name(), "tuple");
2639        assert_eq!(lst.kind_name(), "list");
2640        assert_eq!(Term::int(7).kind_name(), "int");
2641        assert_eq!(Term::str(a, "s").kind_name(), "str");
2642    }
2643
2644    #[test]
2645    fn unpack_primitives_ok() {
2646        let a = &mut Arena::new();
2647
2648        let t_int = Term::int(42);
2649        let t_real = Term::real(3.5);
2650        let t_date = Term::date(2);
2651        let t_str = Term::str(a, "hello");
2652        let t_bin = Term::bin(a, &[1u8, 2, 3, 4]);
2653
2654        assert_eq!(t_int.unpack_int(a).unwrap(), 42);
2655        assert!((t_real.unpack_real(a).unwrap() - 3.5).abs() < f64::EPSILON);
2656        assert_eq!(t_date.unpack_date(a).unwrap(), 2);
2657
2658        assert_eq!(t_str.unpack_str(a).unwrap(), "hello");
2659        assert_eq!(t_bin.unpack_bin(a).unwrap(), &[1, 2, 3, 4]);
2660    }
2661
2662    #[test]
2663    fn unpack_primitives_wrong_type_errs() {
2664        let a = &mut Arena::new();
2665
2666        let not_int = Term::str(a, "nope");
2667        let not_real = Term::int(1);
2668        let not_date = Term::str(a, "2024-01-02");
2669
2670        assert!(not_int.unpack_int(a).is_err());
2671        assert!(not_real.unpack_real(a).is_err());
2672        assert!(not_date.unpack_date(a).is_err());
2673
2674        let not_str = Term::int(5);
2675        let not_bin = Term::str(a, "bytes");
2676        assert!(not_str.unpack_str(a).is_err());
2677        assert!(not_bin.unpack_bin(a).is_err());
2678    }
2679
2680    #[test]
2681    fn unpack_atom_and_var_with_allowed_names() {
2682        let a = &mut Arena::new();
2683
2684        let at_ok = Term::atom(a, "foo");
2685        let at_no = Term::atom(a, "bar");
2686        let vr_ok = Term::var(a, "X");
2687        let vr_no = Term::var(a, "Y");
2688
2689        // Allowed lists
2690        let allowed_atoms = ["foo", "baz"];
2691        let allowed_vars = ["X", "Z"];
2692
2693        assert_eq!(at_ok.unpack_atom(a, &allowed_atoms).unwrap(), "foo");
2694        assert!(at_no.unpack_atom(a, &allowed_atoms).is_err());
2695
2696        assert_eq!(vr_ok.unpack_var(a, &allowed_vars).unwrap(), "X");
2697        assert!(vr_no.unpack_var(a, &allowed_vars).is_err());
2698
2699        // Empty allowed_names means "any"
2700        assert_eq!(at_no.name(a).unwrap(), "bar");
2701        assert_eq!(vr_no.var_name(a).unwrap(), "Y");
2702    }
2703
2704    #[test]
2705    fn unpack_func_any_and_arity_specific() {
2706        let a = &mut Arena::new();
2707
2708        let f0 = Term::func(a, "nilary", &[] as &[Term]);
2709        let f2 = Term::func(a, "pair", &[Term::int(1), Term::int(2)]);
2710        let f3 = Term::func(a, "triple", &[Term::int(1), Term::int(2), Term::int(3)]);
2711
2712        // Any arity, name filtering
2713        {
2714            let (name, args) = f2.unpack_func_any(a, &["pair", "other"]).unwrap();
2715            assert_eq!(name.name(a).unwrap(), "pair");
2716            assert_eq!(args.len(), 2);
2717            assert_eq!(args[0].unpack_int(a).unwrap(), 1);
2718            assert_eq!(args[1].unpack_int(a).unwrap(), 2);
2719
2720            // empty allowed_names accepts anything
2721            let (name0, args0) = f0.unpack_func_any(a, &[]).unwrap();
2722            assert_eq!(name0.name(a).unwrap(), "nilary");
2723            assert!(args0.is_empty());
2724
2725            // disallowed name should error
2726            assert!(f3.unpack_func_any(a, &["not_triple"]).is_err());
2727        }
2728
2729        // Fixed arity (const generic)
2730        {
2731            let (name2, [x, y]) = f2.unpack_func(a, &["pair"]).unwrap();
2732            assert_eq!(name2.name(a).unwrap(), "pair");
2733            assert_eq!(x.unpack_int(a).unwrap(), 1);
2734            assert_eq!(y.unpack_int(a).unwrap(), 2);
2735
2736            // Arity mismatch should error
2737            assert!(f3.unpack_func::<2>(a, &["triple"]).is_err());
2738
2739            // Name not allowed should error
2740            assert!(f2.unpack_func::<2>(a, &["other"]).is_err());
2741        }
2742    }
2743
2744    #[test]
2745    fn unpack_list_proper_and_tail() {
2746        let a = &mut Arena::new();
2747
2748        let l0 = Term::list(a, &[] as &[Term]);
2749        let (elems0, tail0) = l0.unpack_list(a).unwrap();
2750        assert!(elems0.is_empty());
2751        assert_eq!(*tail0, Term::NIL);
2752
2753        let l3 = Term::list(a, &[Term::int(1), Term::int(2), Term::int(3)]);
2754        let (elems3, tail3) = l3.unpack_list(a).unwrap();
2755        assert_eq!(elems3.len(), 3);
2756        assert_eq!(elems3[0].unpack_int(a).unwrap(), 1);
2757        assert_eq!(elems3[1].unpack_int(a).unwrap(), 2);
2758        assert_eq!(elems3[2].unpack_int(a).unwrap(), 3);
2759        assert_eq!(tail3, &Term::NIL);
2760
2761        // Non-list should error
2762        let not_list = Term::int(9);
2763        assert!(not_list.unpack_list(a).is_err());
2764    }
2765
2766    #[test]
2767    fn unpack_tuple_any_and_fixed() {
2768        let a = &mut Arena::new();
2769
2770        let t0 = Term::tuple(a, &[] as &[Term]);
2771        let t3 = Term::tuple(a, &[Term::int(1), Term::int(2), Term::int(3)]);
2772
2773        // any arity
2774        let elems0 = t0.unpack_tuple_any(a).unwrap();
2775        assert!(elems0.is_empty());
2776
2777        let elems3 = t3.unpack_tuple_any(a).unwrap();
2778        assert_eq!(elems3.len(), 3);
2779        assert_eq!(elems3[0].unpack_int(a).unwrap(), 1);
2780        assert_eq!(elems3[1].unpack_int(a).unwrap(), 2);
2781        assert_eq!(elems3[2].unpack_int(a).unwrap(), 3);
2782
2783        // fixed arity
2784        let arr3 = t3.unpack_tuple::<3>(a).unwrap();
2785        assert_eq!(arr3[0].unpack_int(a).unwrap(), 1);
2786        assert_eq!(arr3[1].unpack_int(a).unwrap(), 2);
2787        assert_eq!(arr3[2].unpack_int(a).unwrap(), 3);
2788
2789        // wrong arity should error
2790        assert!(t3.unpack_tuple::<2>(a).is_err());
2791
2792        // non-tuple should error
2793        assert!(Term::int(1).unpack_tuple_any(a).is_err());
2794        assert!(Term::int(1).unpack_tuple::<0>(a).is_err());
2795    }
2796
2797    #[test]
2798    fn unpack_atom_var_wrong_type_errs() {
2799        let a = &mut Arena::new();
2800
2801        let not_atom = Term::int(1);
2802        let not_var = Term::str(a, "X");
2803        assert!(not_atom.atom_name(a).is_err());
2804        assert!(not_var.var_name(a).is_err());
2805    }
2806
2807    #[test]
2808    fn unpack_func_wrong_type_errs() {
2809        let a = &mut Arena::new();
2810
2811        // tuple is not a func
2812        let tup = Term::tuple(a, &[Term::int(1), Term::int(2)]);
2813        assert!(tup.func_name(a).is_err());
2814        assert!(tup.unpack_func::<2>(a, &[]).is_err());
2815
2816        // atom can be unpacked with unpack_func* functions
2817        let at = Term::atom(a, "f");
2818        assert!(!at.unpack_func_any(a, &[]).is_err());
2819        assert!(!at.unpack_func::<0>(a, &[]).is_err());
2820    }
2821
2822    #[test]
2823    fn fmt_nil_to_string() {
2824        let arena = Arena::new();
2825        let t = Term::NIL;
2826        assert_eq!(t.display(&arena).to_string(), "nil");
2827    }
2828
2829    #[test]
2830    fn fmt_unit_format_macro() {
2831        let arena = Arena::new();
2832        let t = Term::UNIT;
2833        assert_eq!(format!("{}", t.display(&arena)), "unit");
2834    }
2835
2836    #[test]
2837    fn fmt_int_positive() {
2838        let arena = Arena::new();
2839        let t = Term::int(42);
2840        assert_eq!(format!("{}", t.display(&arena)), "42");
2841    }
2842
2843    #[test]
2844    fn fmt_int_negative_to_string() {
2845        let arena = Arena::new();
2846        let t = Term::int(-9001);
2847        assert_eq!(t.display(&arena).to_string(), "-9001");
2848    }
2849
2850    #[test]
2851    fn fmt_str_quotes() {
2852        let mut arena = Arena::new();
2853        let t = Term::str(&mut arena, "hello");
2854        assert_eq!(format!("{}", t.display(&arena)), r#""hello""#);
2855    }
2856
2857    #[test]
2858    fn fmt_str_with_escape_chars() {
2859        let mut arena = Arena::new();
2860        let t = Term::str(&mut arena, "a\nb\tc");
2861        assert_eq!(t.display(&arena).to_string(), "\"a\\nb\\tc\"");
2862    }
2863
2864    #[test]
2865    fn fmt_date_epoch_zero() {
2866        let arena = Arena::new();
2867        // 0 ms -> 1970-01-01 00:00:00 UTC
2868        let t = Term::date(0);
2869        assert_eq!(
2870            format!("{}", t.display(&arena)),
2871            "date{1970-01-01T00:00:00+00:00}"
2872        );
2873    }
2874
2875    #[test]
2876    fn fmt_date_epoch_ms_trunc_to_seconds() {
2877        let arena = Arena::new();
2878        let t = Term::date(1_234);
2879        assert_eq!(
2880            t.display(&arena).to_string(),
2881            "date{1970-01-01T00:00:01.234+00:00}"
2882        );
2883    }
2884
2885    #[test]
2886    fn fmt_date_specific_moment() {
2887        let arena = Arena::new();
2888        let t = Term::date(1_727_525_530_123i64);
2889        assert_eq!(
2890            format!("{}", t.display(&arena)),
2891            "date{2024-09-28T12:12:10.123+00:00}"
2892        );
2893    }
2894
2895    #[test]
2896    fn fmt_date_specific_moment_in_the_past() {
2897        let arena = Arena::new();
2898        let t = Term::date(-5_382_698_399_999i64);
2899        assert_eq!(
2900            format!("{}", t.display(&arena)),
2901            "date{1799-06-06T06:00:00.001+00:00}"
2902        );
2903    }
2904
2905    #[test]
2906    fn fmt_write_into_string_buffer() {
2907        let arena = Arena::new();
2908        let t = Term::int(7);
2909        let mut buf = String::new();
2910        // write! returns fmt::Result; ensure it's Ok and content matches
2911        write!(&mut buf, "val={}", t.display(&arena)).expect("formatting failed");
2912        assert_eq!(buf, "val=7");
2913    }
2914}