Prolog‑like term library
Copyright (c) 2005–2025 IKH Software, Inc. support@ikhsoftware.com Released under the terms of the GNU Lesser General Public License, version 3.0 or (at your option) any later version (LGPL-3.0-or-later).
This document describes the design and API of the supplied arena-backed term library.
The library provides a compact, copyable [Term] type for representing Prolog-like data structures, together with an [Arena] that interns atoms, variables, strings, binaries, and compound terms.
All interned data resides in the Arena, while a Term itself is just a 16-byte handle containing a tag and an index into the arena’s storage.
Because terms are immutable, they can be freely reused and shared across different parts of a program, as well as nested within each other.
Copying or moving a Term is cheap: only the handle lives on the stack, while larger payloads are stored once and shared through the arena.
Users interact with the library through three main types:
- [
Term] – a 16‑byte handle identifying a value. Values may be integers, floats, dates, atoms, variables, UTF‑8 strings, binary blobs or compound structures (functions, lists, tuples). This handle implementsCopy,Clone,PartialEqand does not directly borrow from the arena. - [
Arena] – an allocator that interns names and sequence data. EveryTermreferencing interned data stores the arena’s ID so that an attempt to view a term with the wrong arena fails at runtime. - [
View<'a>] – a borrowed representation obtained viaTerm::view(&Arena)orArena::view(&Term). AViewdecodes inline data or dereferences indices back into the arena. It supports pattern‑matching and ordering according to Prolog’s standard term order. Obtaining a view does not allocate: it either returns references to inlined bytes or slices borrowed from the arena.
The crate also provides a trait [IntoTerm] for automatic conversion of many Rust types into Term values, and a suite of macros (list!, tuple!, func!, atom!, var!, date!, unit!, nil!) that make constructing compound terms ergonomic. The following sections describe each component in detail and illustrate typical usage.
Here is a quick example that demonstrates how to construct terms and inspect them using Rust’s pattern matching:
use ;
// create an arena
let mut arena = new;
// build some primitive terms
let a = arena.atom;
let b = arena.real;
let c = arena.date; // 2022-01-01T00:00:00Z
// build a long list from an iterator
let xs = arena.list;
// build a compound term using the func! macro
let term = func!;
// inspect the resulting term
if let Ok = term.view
Term
A [Term] is an opaque handle that stores a single discriminant and its payload. On 64‑bit targets the handle occupies exactly 16 bytes; this includes an eight‑bit tag indicating the kind of term and the remaining bytes for the inlined payload or a slice index. Users never construct Term values by directly instantiating the underlying enum – instead they use the provided constructors or macros.
Primitive constructors
The type provides associated functions to create primitive values:
Term::int(i: impl Into<i64>) -> Term– constructs an integer term. The full two’s‑complement representation is stored. For example:
use ;
let t1 = int; // 64‑bit integer
let t2 = int; // integers are widened to i64
-
Term::real(f: impl Into<f64>) -> Term– constructs a 64-bit floating-point (f64) term, storing its complete IEEE 754 double-precision representation. -
Term::date(ms: impl Into<i64>) -> Term– stores a date as milliseconds since the Unix epoch using a distinct tag. -
Term::atom(arena: &mut Arena, name: impl AsRef<str>) -> Term– interns an atom name. Short names (≤14 bytes of UTF‑8) are inlined directly in the handle; longer names are inserted into the arena and the handle stores an index to the interned string. Atoms are used to represent constant symbols. -
Term::var(arena: &mut Arena, name: impl AsRef<str>) -> Term– interns a variable name. Variable names are drawn from a separate namespace but follow the same inlining rules as atoms. -
Term::str(arena: &mut Arena, s: impl AsRef<str>) -> Term– interns a UTF‑8 string. Short strings (≤14 bytes) are inlined; longer strings are appended to the arena’s string storage and referenced by index and length. -
Term::bin(arena: &mut Arena, bytes: impl AsRef<[u8]>) -> Term– interns a binary blob. Like strings, short slices are inlined while longer slices are appended to the arena’s binary storage.
Compound constructors
Compound terms are slices of Term values stored in the arena. The library provides several constructors:
-
Term::func(arena: &mut Arena, functor: impl AsRef<str>, args: impl IntoIterator<Item = impl IntoTerm>) -> Term– Creates a compound term. If no arguments are supplied, the result is simply an atom. Internally, the functor name is converted to an atom (interned as needed), and both the functor and its arguments are stored consecutively in the arena. -
Term::list(arena: &mut Arena, terms: impl IntoIterator<Item = impl IntoTerm>) -> Term– Constructs a proper list, represented as a sequence of terms. If the iterator is empty, the atomnil(constantTerm::NIL) is returned. -
Term::listc(arena: &mut Arena, terms: impl IntoIterator<Item = impl IntoTerm>, tail: impl IntoTerm) -> Term– Constructs an improper list (a list with a tail). If the iterator is empty, the atomnil(constantTerm::NIL) is returned. If the tail isTerm::NIL, a proper list is constructed; otherwise, the tail is appended to the sequence of list elements. -
Term::tuple(arena: &mut Arena, terms: impl IntoIterator<Item = impl IntoTerm>) -> Term– Constructs a tuple. Tuples share the same storage mechanism as proper lists. A zero-element tuple yields the atomunit(constantTerm::UNIT).
In addition to these constructors, the type defines two constants:
Term::UNIT– The zero-arity tuple, represented by the atomunit.Term::NIL– The empty list, represented by the atomnil.
Inspecting terms with View
A [Term] can be decoded into a borrowed view using the view method. Term::view(&self, arena: &Arena) -> Result<View<'_>, TermError> verifies that the supplied arena matches the term’s arena ID and then decodes inlined data or dereferences indices. If the wrong arena is passed, an error of type TermError::ArenaMismatch is returned. The Arena::view method forwards to Term::view and can be used symmetrically.
use ;
let mut arena = new;
let atom = atom;
let num = int;
let list = list;
match list.view.unwrap
Because a View borrows from the arena, its lifetime ties together both the arena and the original Term. You must therefore ensure that the arena outlives any view.
Arena
An [Arena] owns all interned data and ensures that terms remain valid for the arena’s lifetime. Each arena has a randomly generated ArenaID; terms carry this ID so that view can detect mismatches. Creating a new arena is straightforward:
use Arena;
let mut arena = new;
The arena exposes methods mirroring the constructors on [Term], such as arena.int, arena.real, arena.date, arena.atom, arena.var, arena.str, arena.bin, arena.func, arena.list, arena.listc and arena.tuple. Each of these simply calls the corresponding Term:: constructor but allows you to write code in a fluent style. The arena also exposes a stats method that returns statistics about how many atoms, variables and bytes of string/binary data have been interned.
Interning occurs only when necessary. Very short names and sequences are stored directly inside the 16‑byte handle; longer data is appended to contiguous storage vectors inside the arena. The stats method allows you to inspect these counts for debugging or tuning purposes.
The following example creates a handful of terms and views them:
use ;
let mut a = new;
let x = a.atom;
let y = a.var;
let z = a.list;
match a.view.unwrap
println!;
Arena ensures that terms cannot be misused across arenas. Storing a Term into an arena that it does not belong to (e.g., as an argument to func) will not trigger an error, however, dereferencing it via Arena::view will return an error. Therefore, you should treat each arena as an isolated universe of terms.
View<'a>
The [View<'a>] type is a borrowed enumeration that describes the decoded contents of a term. It has variants corresponding to each term kind:
Obtaining a view does not allocate: it either returns references to inlined bytes or slices borrowed from the arena. When destructuring a view, you may inspect the functor name and arguments of a compound term or iterate through the elements of a list. Remember that the Arena reference stored in the view must be used when recursively viewing nested terms (the library uses arena_mismatch checks to enforce this).
Ordering and equality
View implements PartialEq, Eq, PartialOrd, and Ord, following Prolog’s standard order of terms with some adaptations.
The ordering is defined first by the kind of the term, and then by a value-based comparison within that kind:
- Variables — come first, ordered lexicographically by their names.
- Integers — ordered by numeric value.
- Dates — ordered by numeric value (Unix epoch in milliseconds).
- Reals — ordered by numeric value.
- Atoms — ordered lexicographically by name.
- Strings — ordered lexicographically.
- Funcs — ordered first by arity (number of arguments), then by functor name, and finally by lexicographic comparison of their arguments.
- Lists — ordered lexicographically by their elements, with the tail considered last.
- Binaries — ordered lexicographically by their byte values.
This ordering allows you to sort or deduplicate View values using standard collections:
use ;
use Ordering;
let mut arena = new;
let t1 = arena.atom;
let t2 = arena.int;
let t3 = arena.var;
let mut views = vec!;
views.sort;
assert_eq!; // variables come first
assert_eq!; // numbers next
assert_eq!; // atoms last here
The IntoTerm trait
The crate defines a trait [IntoTerm] used to convert various Rust values into Terms when constructing compound values. Its single method into_term(self, arena: &mut Arena) -> Term converts the receiver into a term using the given arena. Implementations exist for:
- Primitive integers (
i8,i16,i32,i64,u8,u16,u32) and floats (f32,f64), which callTerm::intorTerm::realrespectively. - Slices of bytes (
&[u8]),Vec<u8>. These are interned viaTerm::bin. &str,Cow<'a, str>and string types – interned as UTF‑8 strings viaTerm::str.Termitself and&Term– simply return or copy the handle.- Closures
FnOnce(&mut Arena) -> Term– evaluating the closure with the arena allows macros to pass arena reference implicitly.
Because of these implementations you rarely need to call Term::int or similar constructors directly when building compound terms. Instead, you can pass integers, floats, strings and other terms directly into the macros described below and they will automatically be converted.
The following demonstrates IntoTerm in action:
use ;
let list = arena.list;
// Equivalent to:
let list2 = arena.list;
assert_eq!;
// You can also provide closures returning Term.
let lazy = func;
AsRef<Term> implementation
Term implements AsRef<Term> by returning itself. This trivial implementation allows a Term to be used where an &Term is required. It is most often used internally by macros; you generally do not need to call as_ref explicitly.
From<T> implementations for Term
Rust’s From trait is implemented for common numeric types. Converting from an integer type (i8, i16, i32, i64, u8, u16, u32) into a Term calls Term::int and widens the value to i64. Converting from a floating type (f32, f64) calls Term::real. These implementations allow you to write Term::from(42u8) or use .into() in place of an explicit call to int or real:
use Term;
let i: Term = 10u32.into; // calls Term::int
let f: Term = 3.14f32.into; // calls Term::real
No From implementation exists for dates because it would not be clear whether create interger or date term. No From implementation exists for vars, atoms, strings, binary blobs because constructing these values always requires an arena. Use Term::date, Term::atom, Term::var, Term::str or Term::bin instead.
Macros: explicit vs. implicit arena
To make term construction more ergonomic the crate exports several macros. Each macro is defined in two forms:
- Implicit arena – the macro call expands to a closure of type
FnOnce(&mut Arena) -> Term. You can then pass your arena to this closure. This allows nested macro calls to share the same arena. For example,list![1, 2]returns a closure that builds a list when given an arena. - Explicit arena – by adding
=> &mut arenaat the end of the macro invocation you can construct the term immediately in a given arena. The macro simply calls the implicit form and then applies it to the provided arena.
All macros rely on the IntoTerm trait to convert their arguments into terms. Macros for constructing integers, reals, and strings are not provided, since these values can be used directly wherever an IntoTerm is expected. In such cases, they are automatically converted into the corresponding integer, real, or string term.
list!
The list! macro constructs proper or improper lists. A zero‑length tuple yields Term::NIL. Its syntax is:
list![ a, b, c ] // returns closure: |arena: &mut Arena| arena.list([a.into_term(arena), ...])
list![ a, b, c => &mut arena ]
list![ a, b; tail ] // improper list with tail
list![ a, b; tail => &mut arena ]
Arguments can be any type implementing IntoTerm. The improper form (; tail) uses Term::listc and stores the tail only when it is not Term::NIL.
Example:
use ;
let mut arena = new;
// Proper list with implicit arena; returns closure
let list_builder = list!;
let my_list = list_builder;
// Improper list with explicit arena
let imp = list!;
// Equivalent explicit form
let same_list = list!;
assert_eq!;
tuple!
The tuple! macro creates tuples in the same way as lists. A zero‑length tuple yields Term::UNIT. Syntax:
tuple![ a, b, c ]
tuple![ a, b, c => &mut arena ]
Example:
use ;
let mut arena = new;
// explicit form
let t = tuple!
func!
The func! macro constructs compound terms given a functor name and at least one argument. Zero‑arity functors are represented as atoms, so the macro requires at least one argument. Syntax:
func![ "name"; arg1, arg2, ... ]
func![ "name"; arg1, arg2, ... => &mut arena ]
Example:
use ;
let mut a = new;
let point = func!;
// Macros compose: constructing nested terms
let nested = func!;
assert!;
assert_eq!;
atom! and var!
The atom! macro constructs an atom. It is mostly a thin wrapper around Arena::atom but works with both explicit and implicit arena forms:
atom!( "foo" ) // returns closure
atom!( "foo" => &mut arena )
Variables can be created with var! macro. It is mostly a thin wrapper around Arena::atom but works with both explicit and implicit arena forms:
let x = var! // returns closure
let x = var!;
date!, unit! and nil!
The remaining macros construct special terms that do not require an arena:
date!(ms)– callsTerm::date(ms)directly.unit!()– returns the zero‑arity tuple constantTerm::UNIT.nil!()– returns the empty list constantTerm::NIL.
Putting it all together
The following example demonstrates how the pieces fit together to build a complex term and inspect it:
use ;
// create an arena
let mut arena = new;
// build some primitive terms
let a = arena.atom;
let b = arena.real;
let c = arena.date; // 2022-01-01T00:00:00Z
// build a list and tuple
let lst = list!
This example shows how IntoTerm and the macros allow you to mix primitive Rust types, existing Terms and closures when constructing compound terms. Because the macros return closures for implicit arenas, nested calls do not need to mention the arena repeatedly. When you do provide an explicit arena (via => &mut arena) the closure is applied immediately and returns a Term.