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