use serde::{Deserialize, Serialize};
pub const ARITY_CAP: u8 = 8;
pub const BODY_CAP: u8 = 8;
pub const VAR_CAP: u8 = 8;
pub const BINDING_PATTERNS: usize = 256;
pub const TERM_SENTINEL: TermId = TermId(0);
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
pub struct TermId(pub u32);
impl TermId {
pub const fn new(v: u32) -> Self {
Self(v)
}
pub const fn sentinel() -> Self {
TERM_SENTINEL
}
pub fn is_sentinel(self) -> bool {
self.0 == 0
}
pub fn as_u32(self) -> u32 {
self.0
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
pub struct PredicateId(pub u32);
impl PredicateId {
pub const fn new(v: u32) -> Self {
Self(v)
}
pub fn as_u32(self) -> u32 {
self.0
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
pub struct RuleId(pub u32);
impl RuleId {
pub const fn new(v: u32) -> Self {
Self(v)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default, Serialize, Deserialize)]
pub struct CatalogId(pub u32);
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct SourceId(pub u32);
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
pub struct EpochId(pub u64);
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct SubstitutionId(pub u32);
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct ProofNodeId(pub u32);
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default, Serialize, Deserialize)]
pub struct PlanId(pub u32);
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct Atom8 {
pub pred_id: PredicateId,
pub arity: u8,
pub binding_mask: u8,
pub args: [TermId; ARITY_CAP as usize],
}
impl Atom8 {
pub fn new(pred_id: PredicateId, arity: u8, args: &[TermId]) -> Self {
let mut padded = [TERM_SENTINEL; ARITY_CAP as usize];
let take = arity.min(ARITY_CAP) as usize;
padded[..take].copy_from_slice(&args[..take]);
Self {
pred_id,
arity: arity.min(ARITY_CAP),
binding_mask: 0,
args: padded,
}
}
pub fn with_binding(mut self, binding_mask: u8) -> Self {
self.binding_mask = binding_mask;
self
}
pub fn is_bound(&self, i: u8) -> bool {
i < ARITY_CAP && (self.binding_mask & (1u8 << i)) != 0
}
pub fn live_args(&self) -> impl Iterator<Item = &TermId> {
self.args.iter().take(self.arity as usize)
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Rule8 {
pub rule_id: RuleId,
pub head: Atom8,
pub body: [Atom8; BODY_CAP as usize],
pub body_len: u8,
pub body_mask: u8,
pub negation_mask: u8,
pub builtin_mask: u8,
pub var_count: u8,
pub var_live_mask: u8,
pub feature_mask: u8,
pub proof_mask: u8,
pub plan_id: PlanId,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[repr(u8)]
pub enum FeatureBit {
Facts = 0,
HornRules = 1,
Equality = 2,
TypedComparisons = 3,
StratifiedNegation = 4,
BoundedRecursion = 5,
ControlledAggregates = 6,
ContractedForeign = 7,
}
impl FeatureBit {
pub const ALL: [FeatureBit; 8] = [
FeatureBit::Facts,
FeatureBit::HornRules,
FeatureBit::Equality,
FeatureBit::TypedComparisons,
FeatureBit::StratifiedNegation,
FeatureBit::BoundedRecursion,
FeatureBit::ControlledAggregates,
FeatureBit::ContractedForeign,
];
pub fn bit(self) -> u8 {
self as u8
}
pub fn mask(self) -> u8 {
1u8 << (self as u8)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum ProofMode {
PositiveOnly,
NegativeOnly,
Both,
Hashed,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub struct QueryAtom8 {
pub atom: Atom8,
pub output_mask: u8,
pub proof_mode: ProofMode,
pub epoch: EpochId,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub struct FactRow8 {
pub pred_id: PredicateId,
pub arity: u8,
pub args: [TermId; ARITY_CAP as usize],
pub source_id: SourceId,
pub fact_hash: [u8; 32],
}
impl FactRow8 {
pub fn new(pred_id: PredicateId, arity: u8, args: &[TermId], source_id: SourceId) -> Self {
let mut padded = [TERM_SENTINEL; ARITY_CAP as usize];
let take = arity.min(ARITY_CAP) as usize;
padded[..take].copy_from_slice(&args[..take]);
let row_no_hash = FactRow8 {
pred_id,
arity: arity.min(ARITY_CAP),
args: padded,
source_id,
fact_hash: [0u8; 32],
};
let h = row_no_hash.canonical_hash();
FactRow8 {
fact_hash: h,
..row_no_hash
}
}
pub fn canonical_hash(&self) -> [u8; 32] {
use crate::hash::DOMAIN_PROLOG8_FACT;
let mut hasher = blake3::Hasher::new_keyed(&DOMAIN_PROLOG8_FACT);
hasher.update(&self.pred_id.0.to_le_bytes());
hasher.update(&[self.arity]);
hasher.update(&self.source_id.0.to_le_bytes());
for i in 0..self.arity as usize {
hasher.update(&self.args[i].0.to_le_bytes());
}
hasher.finalize().into()
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct FactBlockMeta {
pub min_terms: [TermId; ARITY_CAP as usize],
pub max_terms: [TermId; ARITY_CAP as usize],
pub constant_columns: u8,
pub epoch_min: EpochId,
pub epoch_max: EpochId,
pub source_root: [u8; 32],
pub fact_root: [u8; 32],
pub capability_mask_or: u32,
pub capability_mask_and: u32,
}
impl Default for FactBlockMeta {
fn default() -> Self {
Self {
min_terms: [TERM_SENTINEL; ARITY_CAP as usize],
max_terms: [TERM_SENTINEL; ARITY_CAP as usize],
constant_columns: 0,
epoch_min: EpochId(0),
epoch_max: EpochId(u64::MAX),
source_root: [0u8; 32],
fact_root: [0u8; 32],
capability_mask_or: 0,
capability_mask_and: u32::MAX,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct FactBlock8 {
pub pred_id: PredicateId,
pub arity: u8,
pub arg_order: [u8; ARITY_CAP as usize],
pub row_count: u32,
pub rows: Vec<FactRow8>,
pub metadata: FactBlockMeta,
pub block_hash: [u8; 32],
}
impl FactBlock8 {
pub fn new(pred_id: PredicateId, arity: u8, rows: Vec<FactRow8>) -> Self {
let mut block = FactBlock8 {
pred_id,
arity: arity.min(ARITY_CAP),
arg_order: [0u8; ARITY_CAP as usize],
row_count: rows.len() as u32,
rows,
metadata: FactBlockMeta::default(),
block_hash: [0u8; 32],
};
block.recompute_metadata();
block.block_hash = block.canonical_hash();
block
}
pub fn recompute_metadata(&mut self) {
if self.rows.is_empty() {
return;
}
let arity = self.arity as usize;
let mut mins = [TermId(u32::MAX); ARITY_CAP as usize];
let mut maxs = [TERM_SENTINEL; ARITY_CAP as usize];
let mut const_mask = 0xFFu8;
let first = self.rows[0];
let mut hasher = blake3::Hasher::new_keyed(&crate::hash::DOMAIN_PROLOG8_BLOCK);
for row in &self.rows {
hasher.update(&row.fact_hash);
for i in 0..arity {
if row.args[i].0 < mins[i].0 {
mins[i] = row.args[i];
}
if row.args[i].0 > maxs[i].0 {
maxs[i] = row.args[i];
}
if row.args[i] != first.args[i] {
const_mask &= !(1u8 << i);
}
}
}
self.metadata.min_terms = mins;
self.metadata.max_terms = maxs;
self.metadata.constant_columns = const_mask & ((1u8 << arity).wrapping_sub(1));
self.metadata.fact_root = hasher.finalize().into();
}
pub fn canonical_hash(&self) -> [u8; 32] {
let mut hasher = blake3::Hasher::new_keyed(&crate::hash::DOMAIN_PROLOG8_BLOCK_HEADER);
hasher.update(&self.pred_id.0.to_le_bytes());
hasher.update(&[self.arity]);
hasher.update(&self.row_count.to_le_bytes());
hasher.update(&self.metadata.fact_root);
hasher.finalize().into()
}
pub fn skip_for(&self, query: &Atom8, epoch: EpochId) -> bool {
if epoch < self.metadata.epoch_min || epoch > self.metadata.epoch_max {
return true;
}
for i in 0..self.arity as usize {
if query.is_bound(i as u8) {
let q = query.args[i];
if q.0 < self.metadata.min_terms[i].0 || q.0 > self.metadata.max_terms[i].0 {
return true;
}
}
}
false
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum ProofKind {
Fact,
Rule,
Builtin,
Negation,
AntiJoin,
Aggregate,
Foreign,
MissingFact,
FailedJoin,
BlockedAlternative,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ProofNode {
pub node_id: ProofNodeId,
pub kind: ProofKind,
pub pred_id: PredicateId,
pub rule_id: Option<RuleId>,
pub fact_hash: Option<[u8; 32]>,
pub children: [ProofNodeId; 8],
pub child_count: u8,
pub substitution_id: SubstitutionId,
pub node_hash: [u8; 32],
}
impl ProofNode {
pub fn canonical_hash(&self) -> [u8; 32] {
let mut hasher = blake3::Hasher::new_keyed(&crate::hash::DOMAIN_PROLOG8_PROOF_NODE);
hasher.update(&self.node_id.0.to_le_bytes());
hasher.update(&[self.kind_byte()]);
hasher.update(&self.pred_id.0.to_le_bytes());
hasher.update(&[self.rule_id.is_some() as u8]);
if let Some(rid) = self.rule_id {
hasher.update(&rid.0.to_le_bytes());
}
hasher.update(&[self.fact_hash.is_some() as u8]);
if let Some(fh) = self.fact_hash {
hasher.update(&fh);
}
hasher.update(&[self.child_count]);
for i in 0..self.child_count as usize {
hasher.update(&self.children[i].0.to_le_bytes());
}
hasher.update(&self.substitution_id.0.to_le_bytes());
hasher.finalize().into()
}
fn kind_byte(&self) -> u8 {
match self.kind {
ProofKind::Fact => 0,
ProofKind::Rule => 1,
ProofKind::Builtin => 2,
ProofKind::Negation => 3,
ProofKind::AntiJoin => 4,
ProofKind::Aggregate => 5,
ProofKind::Foreign => 6,
ProofKind::MissingFact => 7,
ProofKind::FailedJoin => 8,
ProofKind::BlockedAlternative => 9,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum DecisionKind {
Allow,
Deny,
Escalate,
Unknown,
Invalid,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Receipt {
pub engine_version: String,
pub catalog_root: [u8; 32],
pub rule_root: [u8; 32],
pub fact_root: [u8; 32],
pub input_root: [u8; 32],
pub proof_root: [u8; 32],
pub output_root: [u8; 32],
pub decision: DecisionKind,
pub epoch: EpochId,
pub receipt_hash: [u8; 32],
}
impl Receipt {
pub fn compute_hash(&self) -> [u8; 32] {
let mut hasher = blake3::Hasher::new_keyed(&crate::hash::DOMAIN_PROLOG8_RECEIPT);
hasher.update(self.engine_version.as_bytes());
hasher.update(&self.catalog_root);
hasher.update(&self.rule_root);
hasher.update(&self.fact_root);
hasher.update(&self.input_root);
hasher.update(&self.proof_root);
hasher.update(&self.output_root);
hasher.update(&[self.decision_byte()]);
hasher.update(&self.epoch.0.to_le_bytes());
hasher.finalize().into()
}
fn decision_byte(&self) -> u8 {
match self.decision {
DecisionKind::Allow => 0,
DecisionKind::Deny => 1,
DecisionKind::Escalate => 2,
DecisionKind::Unknown => 3,
DecisionKind::Invalid => 4,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn arity_cap_constant() {
assert_eq!(ARITY_CAP, 8);
assert_eq!(BODY_CAP, 8);
assert_eq!(VAR_CAP, 8);
assert_eq!(BINDING_PATTERNS, 256);
}
#[test]
fn atom8_pads_unused_args_with_sentinel() {
let a = Atom8::new(PredicateId(1), 3, &[TermId(10), TermId(11), TermId(12)]);
assert_eq!(a.arity, 3);
assert_eq!(a.args[0], TermId(10));
assert_eq!(a.args[3], TERM_SENTINEL);
}
#[test]
fn atom8_clamps_oversize_arity() {
let a = Atom8::new(PredicateId(1), 99, &[TermId(1); 16]);
assert_eq!(a.arity, ARITY_CAP);
}
#[test]
fn fact_row_hash_is_deterministic() {
let r1 = FactRow8::new(PredicateId(1), 2, &[TermId(10), TermId(11)], SourceId(0));
let r2 = FactRow8::new(PredicateId(1), 2, &[TermId(10), TermId(11)], SourceId(0));
assert_eq!(r1.fact_hash, r2.fact_hash);
let r3 = FactRow8::new(PredicateId(1), 2, &[TermId(10), TermId(12)], SourceId(0));
assert_ne!(r1.fact_hash, r3.fact_hash);
}
#[test]
fn fact_block_skip_for_out_of_range_term() {
let rows = vec![
FactRow8::new(PredicateId(1), 2, &[TermId(10), TermId(20)], SourceId(0)),
FactRow8::new(PredicateId(1), 2, &[TermId(11), TermId(21)], SourceId(0)),
];
let block = FactBlock8::new(PredicateId(1), 2, rows);
let mut q = Atom8::new(PredicateId(1), 2, &[TermId(99), TERM_SENTINEL]);
q.binding_mask = 0b01;
assert!(block.skip_for(&q, EpochId(0)));
}
#[test]
fn feature_bit_mask_round_trips() {
for bit in FeatureBit::ALL {
let m = bit.mask();
assert_eq!(m.count_ones(), 1);
assert!((m >> bit.bit()) & 1 == 1);
}
}
}