use hashbrown::HashMap;
#[derive(Copy, Clone, Eq, PartialEq, Hash, Ord, PartialOrd, Debug)]
pub struct ClassId(u32);
impl ClassId {
#[must_use]
pub const fn new(idx: u32) -> Self {
Self(idx)
}
#[must_use]
pub const fn index(self) -> u32 {
self.0
}
}
#[derive(Copy, Clone, Eq, PartialEq, Hash, Ord, PartialOrd, Debug)]
pub struct RoleId(u32);
impl RoleId {
#[must_use]
pub const fn new(idx: u32) -> Self {
Self(idx)
}
#[must_use]
pub const fn index(self) -> u32 {
self.0
}
}
#[derive(Copy, Clone, Eq, PartialEq, Hash, Ord, PartialOrd, Debug)]
pub struct IndividualId(u32);
impl IndividualId {
#[must_use]
pub const fn new(idx: u32) -> Self {
Self(idx)
}
#[must_use]
pub const fn index(self) -> u32 {
self.0
}
}
#[derive(Copy, Clone, Eq, PartialEq, Hash, Ord, PartialOrd, Debug)]
pub enum Role {
Named(RoleId),
Inverse(RoleId),
}
impl Role {
#[must_use]
pub const fn named(id: RoleId) -> Self {
Self::Named(id)
}
#[must_use]
pub const fn inverse(id: RoleId) -> Self {
Self::Inverse(id)
}
#[must_use]
pub const fn role_id(self) -> RoleId {
match self {
Self::Named(id) | Self::Inverse(id) => id,
}
}
#[must_use]
pub const fn is_inverse(self) -> bool {
matches!(self, Self::Inverse(_))
}
#[must_use]
pub const fn flip(self) -> Self {
match self {
Self::Named(id) => Self::Inverse(id),
Self::Inverse(id) => Self::Named(id),
}
}
}
#[derive(Copy, Clone, Eq, PartialEq, Hash, Ord, PartialOrd, Debug)]
pub struct ConceptId(u32);
impl ConceptId {
#[must_use]
pub const fn new(idx: u32) -> Self {
Self(idx)
}
#[must_use]
pub const fn index(self) -> u32 {
self.0
}
}
#[derive(Clone, Eq, PartialEq, Hash, Debug)]
pub enum ConceptExpr {
Top,
Bot,
Atomic(ClassId),
Nominal(IndividualId),
SelfRestriction(Role),
Not(ConceptId),
And(Box<[ConceptId]>),
Or(Box<[ConceptId]>),
Some(Role, ConceptId),
All(Role, ConceptId),
Min(u32, Role, ConceptId),
Max(u32, Role, ConceptId),
}
#[derive(Default, Debug)]
pub struct ConceptPool {
by_id: Vec<ConceptExpr>,
by_expr: HashMap<ConceptExpr, ConceptId>,
bot_id_cache: std::sync::OnceLock<ConceptId>,
bot_id_cache_hits: std::sync::atomic::AtomicU64,
}
impl Clone for ConceptPool {
fn clone(&self) -> Self {
Self {
by_id: self.by_id.clone(),
by_expr: self.by_expr.clone(),
bot_id_cache: self.bot_id_cache.clone(),
bot_id_cache_hits: std::sync::atomic::AtomicU64::new(
self.bot_id_cache_hits
.load(std::sync::atomic::Ordering::Relaxed),
),
}
}
}
impl ConceptPool {
#[must_use]
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub fn len(&self) -> usize {
self.by_id.len()
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.by_id.is_empty()
}
pub fn iter_exprs(&self) -> impl Iterator<Item = &ConceptExpr> {
self.by_id.iter()
}
pub fn iter_with_ids(&self) -> impl Iterator<Item = (ConceptId, &ConceptExpr)> {
self.by_id.iter().enumerate().map(|(i, e)| {
(
ConceptId(u32::try_from(i).expect("pool size fits in u32")),
e,
)
})
}
#[must_use]
pub fn bot_id(&self) -> Option<ConceptId> {
if let Some(&cached) = self.bot_id_cache.get() {
self.bot_id_cache_hits
.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
return Some(cached);
}
let found = self.iter_with_ids().find_map(|(id, e)| {
if matches!(e, ConceptExpr::Bot) {
Some(id)
} else {
None
}
});
if let Some(id) = found {
let _ = self.bot_id_cache.set(id);
}
found
}
#[must_use]
pub fn bot_id_cache_hits(&self) -> u64 {
self.bot_id_cache_hits
.load(std::sync::atomic::Ordering::Relaxed)
}
#[must_use]
pub fn get(&self, id: ConceptId) -> &ConceptExpr {
&self.by_id[id.0 as usize]
}
fn intern_raw(&mut self, expr: ConceptExpr) -> ConceptId {
if let Some(&id) = self.by_expr.get(&expr) {
return id;
}
let id =
ConceptId(u32::try_from(self.by_id.len()).expect("ConceptPool size exceeds u32::MAX"));
self.by_expr.insert(expr.clone(), id);
self.by_id.push(expr);
id
}
pub fn top(&mut self) -> ConceptId {
self.intern_raw(ConceptExpr::Top)
}
pub fn bot(&mut self) -> ConceptId {
self.intern_raw(ConceptExpr::Bot)
}
pub fn atomic(&mut self, c: ClassId) -> ConceptId {
self.intern_raw(ConceptExpr::Atomic(c))
}
pub fn nominal(&mut self, i: IndividualId) -> ConceptId {
self.intern_raw(ConceptExpr::Nominal(i))
}
pub fn self_restriction(&mut self, r: Role) -> ConceptId {
self.intern_raw(ConceptExpr::SelfRestriction(r))
}
pub fn not(&mut self, c: ConceptId) -> ConceptId {
self.intern_raw(ConceptExpr::Not(c))
}
pub fn some(&mut self, r: Role, c: ConceptId) -> ConceptId {
self.intern_raw(ConceptExpr::Some(r, c))
}
pub fn all(&mut self, r: Role, c: ConceptId) -> ConceptId {
self.intern_raw(ConceptExpr::All(r, c))
}
pub fn min(&mut self, n: u32, r: Role, c: ConceptId) -> ConceptId {
if n == 0 {
return self.top();
}
self.intern_raw(ConceptExpr::Min(n, r, c))
}
pub fn max(&mut self, n: u32, r: Role, c: ConceptId) -> ConceptId {
self.intern_raw(ConceptExpr::Max(n, r, c))
}
pub fn and(&mut self, args: impl IntoIterator<Item = ConceptId>) -> ConceptId {
let mut v: Vec<ConceptId> = Vec::new();
let mut any_bot = false;
for arg in args {
match self.get(arg) {
ConceptExpr::Top => {} ConceptExpr::Bot => any_bot = true,
ConceptExpr::And(inner) => v.extend_from_slice(inner),
_ => v.push(arg),
}
}
if any_bot {
return self.bot();
}
v.sort_unstable();
v.dedup();
if v.is_empty() {
return self.top();
}
if v.len() == 1 {
return v[0];
}
self.intern_raw(ConceptExpr::And(v.into_boxed_slice()))
}
pub fn or(&mut self, args: impl IntoIterator<Item = ConceptId>) -> ConceptId {
let mut v: Vec<ConceptId> = Vec::new();
let mut any_top = false;
for arg in args {
match self.get(arg) {
ConceptExpr::Bot => {} ConceptExpr::Top => any_top = true,
ConceptExpr::Or(inner) => v.extend_from_slice(inner),
_ => v.push(arg),
}
}
if any_top {
return self.top();
}
v.sort_unstable();
v.dedup();
if v.is_empty() {
return self.bot();
}
if v.len() == 1 {
return v[0];
}
self.intern_raw(ConceptExpr::Or(v.into_boxed_slice()))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn intern_dedups_atomics() {
let mut pool = ConceptPool::new();
let a1 = pool.atomic(ClassId::new(0));
let a2 = pool.atomic(ClassId::new(0));
assert_eq!(a1, a2);
assert_eq!(pool.len(), 1);
}
#[test]
fn distinct_atomic_ids_distinct() {
let mut pool = ConceptPool::new();
let a = pool.atomic(ClassId::new(0));
let b = pool.atomic(ClassId::new(1));
assert_ne!(a, b);
assert_eq!(pool.len(), 2);
}
#[test]
fn and_is_commutative() {
let mut pool = ConceptPool::new();
let a = pool.atomic(ClassId::new(0));
let b = pool.atomic(ClassId::new(1));
let ab = pool.and([a, b]);
let ba = pool.and([b, a]);
assert_eq!(ab, ba);
assert_eq!(pool.len(), 3);
}
#[test]
fn and_dedups_duplicate_operands() {
let mut pool = ConceptPool::new();
let a = pool.atomic(ClassId::new(0));
let id1 = pool.and([a, a, a]);
let id2 = pool.and([a]);
assert_eq!(id1, id2);
}
#[test]
fn shared_sub_concepts_share_ids() {
let mut pool = ConceptPool::new();
let r = Role::named(RoleId::new(0));
let a = pool.atomic(ClassId::new(0));
let s1 = pool.some(r, a);
let s2 = pool.some(r, a);
assert_eq!(s1, s2);
assert_eq!(pool.len(), 2);
}
#[test]
fn get_returns_interned_expr() {
let mut pool = ConceptPool::new();
let a = pool.atomic(ClassId::new(7));
match pool.get(a) {
ConceptExpr::Atomic(c) => assert_eq!(*c, ClassId::new(7)),
other => panic!("expected Atomic, got {other:?}"),
}
}
#[test]
fn role_round_trip() {
let r = Role::named(RoleId::new(42));
assert_eq!(r.role_id(), RoleId::new(42));
assert_eq!(r.role_id().index(), 42);
}
#[test]
fn and_flattens_nested_and() {
let mut p = ConceptPool::new();
let a = p.atomic(ClassId::new(0));
let b = p.atomic(ClassId::new(1));
let c = p.atomic(ClassId::new(2));
let inner = p.and([a, b]);
let outer = p.and([inner, c]);
match p.get(outer) {
ConceptExpr::And(args) => assert_eq!(args.len(), 3),
other => panic!("expected flat And, got {other:?}"),
}
}
#[test]
fn and_drops_top() {
let mut p = ConceptPool::new();
let a = p.atomic(ClassId::new(0));
let t = p.top();
let result = p.and([a, t]);
assert_eq!(result, a);
}
#[test]
fn and_with_bot_collapses_to_bot() {
let mut p = ConceptPool::new();
let a = p.atomic(ClassId::new(0));
let b = p.bot();
let result = p.and([a, b]);
assert_eq!(result, b);
}
#[test]
fn empty_and_is_top() {
let mut p = ConceptPool::new();
let empty: Vec<ConceptId> = Vec::new();
let result = p.and(empty);
assert_eq!(result, p.top());
}
#[test]
fn or_flattens_nested_or() {
let mut p = ConceptPool::new();
let a = p.atomic(ClassId::new(0));
let b = p.atomic(ClassId::new(1));
let c = p.atomic(ClassId::new(2));
let inner = p.or([a, b]);
let outer = p.or([inner, c]);
match p.get(outer) {
ConceptExpr::Or(args) => assert_eq!(args.len(), 3),
other => panic!("expected flat Or, got {other:?}"),
}
}
#[test]
fn or_drops_bot() {
let mut p = ConceptPool::new();
let a = p.atomic(ClassId::new(0));
let b = p.bot();
let result = p.or([a, b]);
assert_eq!(result, a);
}
#[test]
fn or_with_top_collapses_to_top() {
let mut p = ConceptPool::new();
let a = p.atomic(ClassId::new(0));
let t = p.top();
let result = p.or([a, t]);
assert_eq!(result, t);
}
#[test]
fn empty_or_is_bot() {
let mut p = ConceptPool::new();
let empty: Vec<ConceptId> = Vec::new();
let result = p.or(empty);
assert_eq!(result, p.bot());
}
#[test]
fn phase3c_bot_id_returns_same_before_and_after_cache_population() {
let mut pool = ConceptPool::new();
let first = pool.bot_id();
let c0 = pool.atomic(ClassId::new(0));
let c1 = pool.atomic(ClassId::new(1));
let _and = pool.and([c0, c1]);
let second = pool.bot_id();
assert_eq!(
first, second,
"bot_id() before Bot interning must be stable across pool growth"
);
assert!(first.is_none(), "bot_id() returns None before Bot is interned");
let _bot_id = pool.bot();
let third = pool.bot_id();
let fourth = pool.bot_id();
assert!(third.is_some(), "after pool.bot(), bot_id() must be Some");
assert_eq!(
third, fourth,
"subsequent bot_id() calls must return the same id"
);
}
#[test]
fn phase3c_bot_id_cache_hits_counter_bumps_on_repeat_calls() {
let mut pool = ConceptPool::new();
let _ = pool.bot(); let _ = pool.bot_id(); let before = pool.bot_id_cache_hits();
let _ = pool.bot_id();
let _ = pool.bot_id();
let _ = pool.bot_id();
let after = pool.bot_id_cache_hits();
assert!(
after >= before + 3,
"bot_id_cache_hits should increment on cached calls; \
before={before} after={after}"
);
}
}