OverviewFilesCommitsIssuesDocsReleasesPeers

hypercore-kb / opencog-kb / basal-atomspace / rust-atomspace / src / atom.rs

182 lines · rust ref:
//! 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);
    }
}