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