arena_terms/
lib.rs

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