#![no_std]
#![forbid(unsafe_code)]
#![doc = include_str!("../README.md")]
#[cfg(feature = "std")]
extern crate std;
pub const MAX_PACKED: u8 = 243;
pub const SENTINEL_COUNT: usize = 256 - MAX_PACKED as usize;
pub const LEAVES: usize = MAX_PACKED as usize;
pub const TIERS: usize = 5;
pub const ROOT_SENTINEL: usize = LEAVES;
pub const PAYLOAD_BYTES: usize = LEAVES + 1;
pub const RADIX_243_POW_8: u64 = 12_157_665_459_056_928_801;
pub const DIGITS_PER_WORD: usize = 8;
pub const FNV1A64_BASIS: u64 = 0xcbf2_9ce4_8422_2325;
pub const FNV1A64_PRIME: u64 = 0x0000_0100_0000_01b3;
const _: () = assert!(3usize.pow(TIERS as u32) == LEAVES);
const _: () = assert!(LEAVES + SENTINEL_COUNT == 256);
const _: () = assert!(SENTINEL_COUNT == 13);
const _: () = assert!((MAX_PACKED as u64).pow(DIGITS_PER_WORD as u32) == RADIX_243_POW_8);
#[repr(u8)]
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Sentinel {
NullNode = 243,
MersenneOverflow = 244,
Tombstone = 245,
BusIndirect = 246,
Phase5 = 247,
Phase6 = 248,
Phase7 = 249,
Phase8 = 250,
Phase9 = 251,
Phase10 = 252,
Phase11 = 253,
Phase12 = 254,
Phase13 = 255,
}
const _: () = assert!(core::mem::size_of::<Sentinel>() == 1);
impl Sentinel {
#[inline(always)]
pub const fn from_u8(b: u8) -> Option<Self> {
match b {
243 => Some(Self::NullNode),
244 => Some(Self::MersenneOverflow),
245 => Some(Self::Tombstone),
246 => Some(Self::BusIndirect),
247 => Some(Self::Phase5),
248 => Some(Self::Phase6),
249 => Some(Self::Phase7),
250 => Some(Self::Phase8),
251 => Some(Self::Phase9),
252 => Some(Self::Phase10),
253 => Some(Self::Phase11),
254 => Some(Self::Phase12),
255 => Some(Self::Phase13),
_ => None,
}
}
}
#[inline(always)]
pub const fn fnv1a64_fold(mut hash: u64, bytes: &[u8]) -> u64 {
let mut i = 0;
while i < bytes.len() {
hash ^= bytes[i] as u64;
hash = hash.wrapping_mul(FNV1A64_PRIME);
i += 1;
}
hash
}
#[inline(always)]
pub const fn xorshift64(mut x: u64) -> u64 {
x ^= x << 13;
x ^= x >> 7;
x ^= x << 17;
x
}
#[repr(C, align(256))]
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct Word256 {
pub hash: u64,
pub parent: u32,
pub trits: [u8; PAYLOAD_BYTES],
}
const _: () = assert!(core::mem::size_of::<Word256>() == 256);
const _: () = assert!(core::mem::align_of::<Word256>() == 256);
const _: () = assert!(core::mem::offset_of!(Word256, hash) == 0);
const _: () = assert!(core::mem::offset_of!(Word256, parent) == 8);
const _: () = assert!(core::mem::offset_of!(Word256, trits) == 12);
const _: () = assert!(core::mem::offset_of!(Word256, trits) + ROOT_SENTINEL == 255);
const _: () = assert!(core::mem::size_of::<PertaroExpert243>() == 256);
const _: () = assert!(core::mem::align_of::<PertaroExpert243>() == 256);
impl Word256 {
pub const ZERO: Self = Self {
hash: 0,
parent: 0,
trits: [0; PAYLOAD_BYTES],
};
}
#[repr(transparent)]
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct PertaroExpert243(pub Word256);
impl PertaroExpert243 {
#[inline(always)]
pub const fn seed(&self) -> u64 {
self.0.hash
}
#[inline(always)]
pub const fn parent(&self) -> u32 {
self.0.parent
}
#[inline(always)]
pub const fn root_sentinel_byte(&self) -> u8 {
self.0.trits[ROOT_SENTINEL]
}
#[inline(always)]
pub const fn root_sentinel(&self) -> Option<Sentinel> {
Sentinel::from_u8(self.0.trits[ROOT_SENTINEL])
}
#[inline(always)]
pub const fn leaves(&self) -> &[u8; LEAVES] {
match self.0.trits.first_chunk::<LEAVES>() {
Some(c) => c,
None => unreachable!(),
}
}
#[inline(always)]
pub const fn trie_index(path: [u8; TIERS]) -> usize {
let mut idx = 0usize;
let mut i = 0;
while i < TIERS {
assert!(path[i] < 3, "pertaro trie tier outside 0..3");
idx = idx * 3 + path[i] as usize;
i += 1;
}
idx
}
#[inline(always)]
pub const fn default_field(seed: u64, out: &mut [u8; LEAVES]) {
let mut s = if seed == 0 { FNV1A64_BASIS } else { seed };
let mut idx = 0usize;
while idx < LEAVES {
s = xorshift64(s);
if s >= RADIX_243_POW_8 {
continue;
}
let mut word = s;
let mut k = 0;
while k < DIGITS_PER_WORD && idx < LEAVES {
out[idx] = (word % MAX_PACKED as u64) as u8;
word /= MAX_PACKED as u64;
idx += 1;
k += 1;
}
}
}
#[inline(always)]
pub const fn materialize(&self) -> Word256 {
let mut base = [0u8; LEAVES];
Self::default_field(self.0.hash, &mut base);
let mut out = self.0;
let mut idx = 0usize;
while idx < LEAVES {
let raw = self.0.trits[idx];
out.trits[idx] = if raw < MAX_PACKED {
((base[idx] as u16 + raw as u16) % MAX_PACKED as u16) as u8
} else if raw == Sentinel::NullNode as u8 {
base[idx]
} else {
raw
};
idx += 1;
}
out
}
#[inline(always)]
pub const fn reground_seed(&self, winner: u8, next: Sentinel) -> u64 {
assert!(
winner < MAX_PACKED,
"pertaro reground winner outside the 243 leaves"
);
let h = fnv1a64_fold(FNV1A64_BASIS, &self.0.hash.to_le_bytes());
let h = fnv1a64_fold(h, &[winner, next as u8]);
xorshift64(h)
}
}
#[cfg(test)]
mod tests {
use super::*;
fn word(seed: u64) -> Word256 {
Word256 {
hash: seed,
parent: 7,
trits: [0; PAYLOAD_BYTES],
}
}
#[test]
fn every_byte_is_a_coordinate_or_a_sentinel() {
for b in 0..MAX_PACKED {
assert!(Sentinel::from_u8(b).is_none(), "{b}");
}
for b in MAX_PACKED..=255 {
assert_eq!(Sentinel::from_u8(b).map(|s| s as u8), Some(b));
}
}
#[test]
fn default_field_is_deterministic_and_in_range() {
let mut a = [0u8; LEAVES];
let mut b = [0u8; LEAVES];
PertaroExpert243::default_field(0x1234_5678_9ABC_DEF0, &mut a);
PertaroExpert243::default_field(0x1234_5678_9ABC_DEF0, &mut b);
assert_eq!(a, b);
assert!(a.iter().all(|x| *x < MAX_PACKED));
let mut c = [0u8; LEAVES];
PertaroExpert243::default_field(0x1234_5678_9ABC_DEF1, &mut c);
assert_ne!(a, c);
}
#[test]
fn zero_seed_uses_the_basis() {
let mut z = [0u8; LEAVES];
let mut b = [0u8; LEAVES];
PertaroExpert243::default_field(0, &mut z);
PertaroExpert243::default_field(FNV1A64_BASIS, &mut b);
assert_eq!(z, b);
}
#[test]
fn radix_digits_are_balanced_over_many_seeds() {
let mut hist = [0u32; 243];
let mut f = [0u8; LEAVES];
for seed in 1..=2000u64 {
PertaroExpert243::default_field(seed.wrapping_mul(FNV1A64_PRIME), &mut f);
for d in f {
hist[d as usize] += 1;
}
}
let total: u32 = hist.iter().sum();
let mean = total / 243;
for (d, n) in hist.iter().enumerate() {
assert!(
*n > mean / 2 && *n < mean * 2,
"digit {d}: {n} vs mean {mean}"
);
}
}
#[test]
fn materialize_keeps_header_and_resolves_sentinels() {
let mut w = word(99);
w.trits[0] = 5;
w.trits[1] = Sentinel::NullNode as u8;
w.trits[2] = Sentinel::Tombstone as u8;
w.trits[3] = Sentinel::BusIndirect as u8;
w.trits[ROOT_SENTINEL] = Sentinel::Phase13 as u8;
let x = PertaroExpert243(w);
let out = x.materialize();
let mut base = [0u8; LEAVES];
PertaroExpert243::default_field(99, &mut base);
assert_eq!(out.hash, 99);
assert_eq!(out.parent, 7);
assert_eq!(out.trits[0], ((base[0] as u16 + 5) % 243) as u8);
assert_eq!(out.trits[1], base[1]);
assert_eq!(out.trits[2], Sentinel::Tombstone as u8);
assert_eq!(out.trits[3], Sentinel::BusIndirect as u8);
assert_eq!(out.trits[ROOT_SENTINEL], Sentinel::Phase13 as u8);
assert_eq!(x.root_sentinel(), Some(Sentinel::Phase13));
assert_eq!(x.leaves()[0], 5);
}
#[test]
fn trie_index_spans_all_leaves() {
assert_eq!(PertaroExpert243::trie_index([0; 5]), 0);
assert_eq!(PertaroExpert243::trie_index([2; 5]), LEAVES - 1);
assert_eq!(PertaroExpert243::trie_index([1, 0, 0, 0, 0]), 81);
assert_eq!(PertaroExpert243::trie_index([0, 0, 0, 0, 1]), 1);
}
#[test]
#[should_panic]
fn trie_index_refuses_a_bad_tier() {
let _ = PertaroExpert243::trie_index([0, 0, 3, 0, 0]);
}
#[test]
fn reground_is_deterministic_and_phase_sensitive() {
let x = PertaroExpert243(word(1));
let a = x.reground_seed(0, Sentinel::NullNode);
assert_eq!(a, x.reground_seed(0, Sentinel::NullNode));
assert_ne!(a, x.reground_seed(1, Sentinel::NullNode));
assert_ne!(a, x.reground_seed(0, Sentinel::Tombstone));
assert_ne!(a, 0);
}
#[test]
#[should_panic]
fn reground_refuses_a_sentinel_as_winner() {
let _ = PertaroExpert243(word(1)).reground_seed(243, Sentinel::NullNode);
}
}
pub const ARENA_TIERS: usize = 5;
pub const ARENA_NODES: usize = (3usize.pow(ARENA_TIERS as u32) - 1) / 2;
pub const FRONTIER_START: usize = (3usize.pow(ARENA_TIERS as u32 - 1) - 1) / 2;
pub const FRONTIER_NODES: usize = ARENA_NODES - FRONTIER_START;
pub const ARENA_BYTES: usize = ARENA_NODES * core::mem::size_of::<Word256>();
pub const L1_BUDGET_BYTES: usize = 32 * 1024;
const _: () = assert!(ARENA_NODES == 121);
const _: () = assert!(FRONTIER_START == 40);
const _: () = assert!(FRONTIER_NODES == 81);
const _: () = assert!(ARENA_BYTES == 30_976);
const _: () = assert!(ARENA_BYTES < L1_BUDGET_BYTES);
const _: () = assert!((ARENA_NODES + FRONTIER_NODES * 3) * 256 > L1_BUDGET_BYTES);
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub struct AnswerSetMask(pub [u64; 4]);
impl AnswerSetMask {
#[inline(always)]
pub const fn empty() -> Self {
Self([0; 4])
}
#[inline(always)]
pub const fn insert(&mut self, atom: u8) {
self.0[(atom >> 6) as usize] |= 1u64 << (atom & 63);
}
#[inline(always)]
pub const fn contains(&self, atom: u8) -> bool {
(self.0[(atom >> 6) as usize] & (1u64 << (atom & 63))) != 0
}
#[inline(always)]
pub const fn count_ones(&self) -> u32 {
self.0[0].count_ones()
+ self.0[1].count_ones()
+ self.0[2].count_ones()
+ self.0[3].count_ones()
}
#[inline(always)]
pub const fn is_empty(&self) -> bool {
self.0[0] | self.0[1] | self.0[2] | self.0[3] == 0
}
#[inline(always)]
pub const fn first_winner(&self) -> Option<u8> {
let mut w = 0;
while w < 4 {
if self.0[w] != 0 {
let atom = (w as u8) * 64 + self.0[w].trailing_zeros() as u8;
return if atom < MAX_PACKED { Some(atom) } else { None };
}
w += 1;
}
None
}
}
#[repr(C, align(256))]
#[derive(Clone, Copy)]
pub struct PertaroArena121 {
pub nodes: [PertaroExpert243; ARENA_NODES],
}
const _: () = assert!(core::mem::size_of::<PertaroArena121>() == ARENA_BYTES);
const _: () = assert!(core::mem::align_of::<PertaroArena121>() == 256);
impl PertaroArena121 {
pub const ZERO: Self = Self {
nodes: [PertaroExpert243(Word256::ZERO); ARENA_NODES],
};
#[inline(always)]
pub const fn node_index(tier: usize, offset: usize) -> usize {
const TIER_BASE: [usize; ARENA_TIERS] = [0, 1, 4, 13, 40];
assert!(tier < ARENA_TIERS, "pertaro arena tier outside 0..5");
assert!(
offset < 3usize.pow(tier as u32),
"pertaro arena offset outside the tier"
);
TIER_BASE[tier] + offset
}
#[inline(always)]
pub const fn children_of(parent: usize) -> [usize; 3] {
assert!(
parent < FRONTIER_START,
"pertaro arena: a frontier node has no children"
);
let base = 3 * parent + 1;
[base, base + 1, base + 2]
}
#[inline(always)]
pub const fn parent_of(idx: usize) -> usize {
assert!(
idx > 0 && idx < ARENA_NODES,
"pertaro arena: index outside 1..121"
);
(idx - 1) / 3
}
pub fn expand_from_root(&mut self, root_seed: u64, input_atom: u8, phase: Sentinel) {
assert!(
input_atom < MAX_PACKED,
"pertaro expand: input atom outside the 243 leaves"
);
self.nodes[0].0.hash = root_seed;
self.nodes[0].0.parent = 0;
self.nodes[0].0.trits[ROOT_SENTINEL] = phase as u8;
let mut parent = 0usize;
while parent < FRONTIER_START {
let children = Self::children_of(parent);
let mut seeds = [0u64; 3];
let mut b = 0usize;
while b < 3 {
let winner = ((input_atom as usize + b) % LEAVES) as u8;
seeds[b] = self.nodes[parent].reground_seed(winner, phase);
b += 1;
}
let mut b = 0usize;
while b < 3 {
let child = &mut self.nodes[children[b]].0;
child.hash = seeds[b];
child.parent = parent as u32;
child.trits[ROOT_SENTINEL] = phase as u8;
b += 1;
}
parent += 1;
}
}
#[inline(always)]
pub fn frontier(&self) -> &[PertaroExpert243] {
&self.nodes[FRONTIER_START..]
}
#[inline(always)]
pub fn frontier_mut(&mut self) -> &mut [PertaroExpert243] {
&mut self.nodes[FRONTIER_START..]
}
}
impl PertaroExpert243 {
pub fn gl_reduct_consequences(
&self,
materialized: &Word256,
s: &AnswerSetMask,
) -> AnswerSetMask {
let mut cn = AnswerSetMask::empty();
let mut idx = 0usize;
while idx < LEAVES {
let raw = self.0.trits[idx];
let v = materialized.trits[idx];
let derived = if raw == Sentinel::Tombstone as u8 || v == Sentinel::Tombstone as u8 {
false } else if raw == Sentinel::NullNode as u8 {
true } else {
v < MAX_PACKED && !s.contains(v) };
if derived {
cn.insert(idx as u8);
}
idx += 1;
}
cn
}
#[inline(always)]
pub fn is_answer_set(&self, materialized: &Word256, s: &AnswerSetMask) -> bool {
self.gl_reduct_consequences(materialized, s) == *s
}
pub fn prune_frontier(&mut self) -> AnswerSetMask {
let mat = self.materialize();
let s1 = self.gl_reduct_consequences(&mat, &AnswerSetMask::empty());
let s2 = self.gl_reduct_consequences(&mat, &s1);
let mut idx = 0usize;
while idx < LEAVES {
if self.0.trits[idx] < MAX_PACKED && !s2.contains(idx as u8) {
self.0.trits[idx] = Sentinel::Tombstone as u8;
}
idx += 1;
}
s2
}
}
#[cfg(test)]
mod arena_tests {
use super::*;
fn word(seed: u64) -> Word256 {
Word256 {
hash: seed,
parent: 0,
trits: [0; PAYLOAD_BYTES],
}
}
#[test]
fn mask_insert_contains_count_and_winner() {
let mut m = AnswerSetMask::empty();
assert!(m.is_empty());
assert_eq!(m.first_winner(), None);
m.insert(200);
m.insert(5);
m.insert(255);
assert!(m.contains(5) && m.contains(200) && m.contains(255));
assert!(!m.contains(6));
assert_eq!(m.count_ones(), 3);
assert_eq!(m.first_winner(), Some(5));
let mut only_sentinel = AnswerSetMask::empty();
only_sentinel.insert(250);
assert_eq!(only_sentinel.first_winner(), None);
}
#[test]
fn arena_indexing_round_trips() {
assert_eq!(PertaroArena121::node_index(0, 0), 0);
assert_eq!(PertaroArena121::node_index(4, 80), ARENA_NODES - 1);
for p in 0..FRONTIER_START {
for c in PertaroArena121::children_of(p) {
assert!(c < ARENA_NODES);
assert_eq!(PertaroArena121::parent_of(c), p);
}
}
assert_eq!(
PertaroArena121::children_of(FRONTIER_START - 1)[2],
ARENA_NODES - 1
);
}
#[test]
#[should_panic]
fn frontier_has_no_children() {
let _ = PertaroArena121::children_of(FRONTIER_START);
}
#[test]
fn expand_is_deterministic_links_parents_and_takes_every_atom() {
let mut a = PertaroArena121::ZERO;
let mut b = PertaroArena121::ZERO;
a.expand_from_root(0xDEAD_BEEF, 7, Sentinel::BusIndirect);
b.expand_from_root(0xDEAD_BEEF, 7, Sentinel::BusIndirect);
for i in 0..ARENA_NODES {
assert_eq!(a.nodes[i].0, b.nodes[i].0, "node {i}");
}
for i in 1..ARENA_NODES {
assert_eq!(a.nodes[i].parent() as usize, PertaroArena121::parent_of(i));
assert_eq!(a.nodes[i].root_sentinel(), Some(Sentinel::BusIndirect));
assert_ne!(a.nodes[i].seed(), 0);
}
let [c0, c1, c2] = PertaroArena121::children_of(0);
assert_ne!(a.nodes[c0].seed(), a.nodes[c1].seed());
assert_ne!(a.nodes[c1].seed(), a.nodes[c2].seed());
a.expand_from_root(1, 242, Sentinel::NullNode);
assert_eq!(a.frontier().len(), FRONTIER_NODES);
}
#[test]
fn reduct_derives_facts_blocks_tombstones_and_respects_naf() {
let mut w = word(99);
w.trits[0] = Sentinel::NullNode as u8; w.trits[1] = Sentinel::Tombstone as u8; w.trits[2] = 3; let x = PertaroExpert243(w);
let mat = x.materialize();
let v = mat.trits[2];
assert!(v < MAX_PACKED);
let cn_empty = x.gl_reduct_consequences(&mat, &AnswerSetMask::empty());
assert!(cn_empty.contains(0));
assert!(!cn_empty.contains(1));
assert!(cn_empty.contains(2));
let mut s = AnswerSetMask::empty();
s.insert(v);
let cn_s = x.gl_reduct_consequences(&mat, &s);
assert!(cn_s.contains(0));
assert!(!cn_s.contains(1));
assert!(
!cn_s.contains(2),
"rule 2 :- not {v} must be deleted when {v} is in S"
);
}
#[test]
fn facts_only_program_has_exactly_one_answer_set() {
let mut w = word(5);
for t in w.trits[..LEAVES].iter_mut() {
*t = Sentinel::Tombstone as u8;
}
w.trits[10] = Sentinel::NullNode as u8;
w.trits[20] = Sentinel::NullNode as u8;
let x = PertaroExpert243(w);
let mat = x.materialize();
let mut facts = AnswerSetMask::empty();
facts.insert(10);
facts.insert(20);
assert!(x.is_answer_set(&mat, &facts));
assert!(!x.is_answer_set(&mat, &AnswerSetMask::empty()));
let mut too_many = facts;
too_many.insert(30);
assert!(!x.is_answer_set(&mat, &too_many));
}
#[test]
fn prune_is_idempotent_and_leaves_only_supported_coordinates() {
let mut x = PertaroExpert243(word(0xC0FFEE));
for (i, t) in x.0.trits[..LEAVES].iter_mut().enumerate() {
*t = (i % 7) as u8;
}
x.0.trits[0] = Sentinel::NullNode as u8;
let s2 = x.prune_frontier();
assert!(s2.contains(0));
for i in 0..LEAVES {
let raw = x.0.trits[i];
if raw < MAX_PACKED {
assert!(
s2.contains(i as u8),
"leaf {i} survived pruning but is not in S2"
);
}
}
let again = x.prune_frontier();
assert_eq!(again, s2);
}
}