//! Core atom types
//!
//! This is the heart of AtomSpace - just ~150 lines vs thousands in C++.
use serde::{Deserialize, Serialize};
use std::sync::Arc;
/// 16-byte hash (128 bits) - enough for billions of unique atoms
pub type Hash = [u8; 16];
/// Reference-counted atom handle
pub type AtomRef = Arc<Atom>;
/// An atom is either a Node (named) or Link (connecting atoms)
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
pub enum Atom {
Node(Node),
Link(Link),
}
/// A node is a named, typed entity
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
pub struct Node {
pub typ: String,
pub name: String,
}
/// A link connects atoms with a typed relationship
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
pub struct Link {
pub typ: String,
pub outgoing: Vec<Hash>, // Hashes of target atoms
}
impl Atom {
/// Create a new node
pub fn node(typ: impl Into<String>, name: impl Into<String>) -> Self {
Atom::Node(Node {
typ: typ.into(),
name: name.into(),
})
}
/// Create a new link
pub fn link(typ: impl Into<String>, outgoing: Vec<Hash>) -> Self {
Atom::Link(Link {
typ: typ.into(),
outgoing,
})
}
/// Get the type of this atom
pub fn typ(&self) -> &str {
match self {
Atom::Node(n) => &n.typ,
Atom::Link(l) => &l.typ,
}
}
/// Compute unique hash for this atom
///
/// Uses BLAKE3 for speed (3x faster than SHA256)
pub fn hash(&self) -> Hash {
let mut hasher = blake3::Hasher::new();
match self {
Atom::Node(n) => {
hasher.update(b"N:");
hasher.update(n.typ.as_bytes());
hasher.update(b":");
hasher.update(n.name.as_bytes());
}
Atom::Link(l) => {
hasher.update(b"L:");
hasher.update(l.typ.as_bytes());
hasher.update(b":[");
for (i, h) in l.outgoing.iter().enumerate() {
if i > 0 {
hasher.update(b",");
}
hasher.update(h);
}
hasher.update(b"]");
}
}
let full_hash = hasher.finalize();
let mut short_hash = [0u8; 16];
short_hash.copy_from_slice(&full_hash.as_bytes()[..16]);
short_hash
}
/// Is this a node?
pub fn is_node(&self) -> bool {
matches!(self, Atom::Node(_))
}
/// Is this a link?
pub fn is_link(&self) -> bool {
matches!(self, Atom::Link(_))
}
/// Get name (for nodes only)
pub fn name(&self) -> Option<&str> {
match self {
Atom::Node(n) => Some(&n.name),
Atom::Link(_) => None,
}
}
/// Get outgoing atoms (for links only)
pub fn outgoing(&self) -> Option<&[Hash]> {
match self {
Atom::Node(_) => None,
Atom::Link(l) => Some(&l.outgoing),
}
}
/// Serialize to bytes
pub fn to_bytes(&self) -> Vec<u8> {
bincode::serialize(self).expect("serialization should not fail")
}
/// Deserialize from bytes
pub fn from_bytes(bytes: &[u8]) -> Result<Self, bincode::Error> {
bincode::deserialize(bytes)
}
}
/// Values that can be attached to atoms
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum Value {
/// Truth value: (strength, confidence)
Truth(f64, f64),
/// Attention value: (short-term, long-term)
Attention(f64, f64),
/// Count (observation frequency)
Count(u64),
/// Float vector (embeddings, etc)
FloatVec(Vec<f64>),
/// String
String(String),
/// Generic JSON
Json(serde_json::Value),
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_node_hash() {
let n1 = Atom::node("ConceptNode", "dog");
let n2 = Atom::node("ConceptNode", "dog");
let n3 = Atom::node("ConceptNode", "cat");
assert_eq!(n1.hash(), n2.hash()); // Same content = same hash
assert_ne!(n1.hash(), n3.hash()); // Different content = different hash
}
#[test]
fn test_link_hash() {
let dog = Atom::node("ConceptNode", "dog");
let animal = Atom::node("ConceptNode", "animal");
let l1 = Atom::link("InheritanceLink", vec![dog.hash(), animal.hash()]);
let l2 = Atom::link("InheritanceLink", vec![dog.hash(), animal.hash()]);
let l3 = Atom::link("InheritanceLink", vec![animal.hash(), dog.hash()]); // Reversed
assert_eq!(l1.hash(), l2.hash());
assert_ne!(l1.hash(), l3.hash()); // Order matters
}
#[test]
fn test_serialization() {
let atom = Atom::node("ConceptNode", "test");
let bytes = atom.to_bytes();
let restored = Atom::from_bytes(&bytes).unwrap();
assert_eq!(atom, restored);
}
}