#![forbid(unsafe_code)]
use rustc_hash::FxHashMap;
use rand::{Rng, SeedableRng};
use rand_chacha::ChaCha8Rng;
#[derive(Debug, Clone, PartialEq)]
pub struct Hypervector {
bits: Vec<u64>,
dims: usize,
}
impl Hypervector {
pub fn new(dims: usize) -> Self {
let words = dims.div_ceil(64);
Self { bits: vec![0u64; words], dims }
}
#[inline(always)]
pub fn bits_get(&self, word: usize) -> u64 {
*self.bits.get(word).unwrap_or(&0)
}
pub fn get(&self, idx: usize) -> i8 {
if idx >= self.dims { return 0; }
let word = idx / 64;
let bit = idx % 64;
if (self.bits[word] >> bit) & 1 == 1 { 1 } else { -1 }
}
#[inline(always)]
pub fn set(&mut self, idx: usize, val: i8) {
if idx >= self.dims { return; }
let word = idx / 64;
let bit = idx % 64;
if val >= 0 {
self.bits[word] |= 1u64 << bit;
} else {
self.bits[word] &= !(1u64 << bit);
}
}
#[inline(always)]
pub fn dot(&self, other: &Hypervector) -> i64 {
let min_words = self.bits.len().min(other.bits.len());
let mut hamming: u64 = 0;
for i in 0..min_words {
hamming += (self.bits[i] ^ other.bits[i]).count_ones() as u64;
}
let dims = self.dims.min(other.dims) as i64;
dims - 2 * (hamming as i64)
}
#[inline(always)]
pub fn norm(&self) -> f64 {
(self.dims as f64).sqrt()
}
#[inline(always)]
pub fn cosine(&self, other: &Hypervector) -> f64 {
let d = self.dot(other) as f64;
let n = self.norm() * other.norm();
if n == 0.0 { 0.0 } else { d / n }
}
pub fn bundle_into(&mut self, other: &Hypervector) {
let words = self.bits.len().min(other.bits.len());
let mut acc: Vec<i32> = Vec::with_capacity(64);
for w in 0..words {
acc.clear();
acc.resize(64, 0i32);
for (b, slot) in acc.iter_mut().enumerate() {
let s = if (self.bits[w] >> b) & 1 == 1 { 1 } else { -1 };
let o = if (other.bits[w] >> b) & 1 == 1 { 1 } else { -1 };
*slot = s + o;
}
let mut new_word = 0u64;
for (b, slot) in acc.iter().enumerate() {
if *slot > 0 || (*slot == 0 && (self.bits[w] >> b) & 1 == 1) {
new_word |= 1u64 << b;
}
}
self.bits[w] = new_word;
}
}
}
pub fn make_hv(seed: u64, dims: usize) -> Hypervector {
let mut rng = ChaCha8Rng::seed_from_u64(seed);
let mut hv = Hypervector::new(dims);
for i in 0..dims {
hv.set(i, if rng.gen_bool(0.5) { 1 } else { -1 });
}
hv
}
pub fn bundle(hv: &mut Hypervector, other: &Hypervector) {
hv.bundle_into(other);
}
#[inline(always)]
pub fn bipolar_clamp(_hv: &mut Hypervector) {
}
#[inline(always)]
pub fn dot(a: &Hypervector, b: &Hypervector) -> f64 {
a.cosine(b)
}
#[derive(Debug, Clone)]
pub struct MemoryRecord {
pub id: i64,
pub content: String,
pub source: String,
pub timestamp: i64,
pub tier: i32, pub recall_count: i32,
pub error_flag: i32,
pub entropy: f64,
}
#[derive(Debug, Clone)]
pub struct ScoredMemory {
pub memory: MemoryRecord,
pub score: f64,
}
pub fn open_persistent(path: &str) -> Result<MemoryStore, String> {
let conn = rusqlite::Connection::open(path).map_err(|e| format!("DB: {}", e))?;
conn.execute_batch("PRAGMA journal_mode=WAL; PRAGMA synchronous=NORMAL;")
.map_err(|e| format!("PRAGMA: {}", e))?;
conn.execute_batch(
"CREATE TABLE IF NOT EXISTS cortex_memories (
id INTEGER PRIMARY KEY AUTOINCREMENT,
content TEXT NOT NULL,
source TEXT DEFAULT '',
timestamp INTEGER NOT NULL,
tier INTEGER DEFAULT 0,
recall_count INTEGER DEFAULT 0
);"
).map_err(|e| format!("schema: {}", e))?;
let mut store = MemoryStore::new(100);
let mut stmt = conn.prepare("SELECT id, content, source, timestamp, tier, recall_count, 0, 0.0 FROM cortex_memories ORDER BY id").map_err(|e| format!("query: {}", e))?;
let rows = stmt.query_map([], |row| {
Ok(MemoryRecord {
id: row.get(0)?,
content: row.get(1)?,
source: row.get(2)?,
timestamp: row.get(3)?,
tier: row.get(4)?,
recall_count: row.get(5)?,
error_flag: row.get(6)?,
entropy: row.get(7)?,
})
}).map_err(|e| format!("rows: {}", e))?;
for row in rows.flatten() {
store.memories.push(row);
}
Ok(store)
}
pub fn save_persistent(store: &MemoryStore, path: &str) -> Result<(), String> {
let conn = rusqlite::Connection::open(path).map_err(|e| format!("DB: {}", e))?;
conn.execute_batch("PRAGMA journal_mode=WAL; PRAGMA synchronous=NORMAL;")
.map_err(|e| format!("PRAGMA: {}", e))?;
conn.execute_batch(
"CREATE TABLE IF NOT EXISTS cortex_memories (
id INTEGER PRIMARY KEY AUTOINCREMENT,
content TEXT NOT NULL,
source TEXT DEFAULT '',
timestamp INTEGER NOT NULL,
tier INTEGER DEFAULT 0,
recall_count INTEGER DEFAULT 0
);"
).map_err(|e| format!("schema: {}", e))?;
let _ = conn.execute("DELETE FROM cortex_memories", []);
for m in &store.memories {
if let Err(e) = conn.execute(
"INSERT INTO cortex_memories (content, source, timestamp, tier, recall_count) VALUES (?1, ?2, ?3, ?4, ?5)",
rusqlite::params![m.content, m.source, m.timestamp, m.tier, m.recall_count],
) {
eprintln!("[memory] save: {}", e);
}
}
Ok(())
}
pub struct MemoryStore {
pub memories: Vec<MemoryRecord>,
token_hvs: FxHashMap<String, Hypervector>,
cooccur: FxHashMap<(String, String), u64>,
pub dims: usize,
}
impl MemoryStore {
pub fn new(dims: usize) -> Self {
Self { memories: Vec::new(), token_hvs: FxHashMap::default(), cooccur: FxHashMap::default(), dims }
}
pub fn ensure_token_hv(&mut self, token: &str) -> &Hypervector {
let dims = self.dims;
self.token_hvs.entry(token.to_string())
.or_insert_with(|| make_hv(fnv1a(token.as_bytes()), dims))
}
pub fn retain(&mut self, content: &str, source: &str, error: bool) -> i64 {
let tokens = scan_tokens(content);
if tokens.is_empty() { return 0; }
for i in 0..tokens.len() {
for j in (i + 1)..tokens.len().min(i + 5) {
let key = (tokens[i].clone(), tokens[j].clone());
*self.cooccur.entry(key).or_insert(0) += 1;
self.hebbian_update(&tokens[i], &tokens[j]);
}
}
let now = chrono::Utc::now().timestamp();
let id = self.memories.len() as i64 + 1;
self.memories.push(MemoryRecord {
id,
content: content.to_string(),
source: source.to_string(),
timestamp: now,
tier: 0,
recall_count: 0,
error_flag: if error { 1 } else { 0 },
entropy: 0.0,
});
id
}
fn hebbian_update(&mut self, a: &str, b: &str) {
let dims = self.dims;
let hv_b_val = {
let hv_b = self.token_hvs.entry(b.to_string()).or_insert_with(|| make_hv(b.len() as u64, dims));
let mut rng = ChaCha8Rng::seed_from_u64(
(a.len() as u64).wrapping_mul(31).wrapping_add(b.len() as u64)
);
let n_flip = (dims / 10).max(1);
let mut bits_to_set: Vec<(usize, i8)> = Vec::with_capacity(n_flip);
for _ in 0..n_flip {
let idx = rng.gen_range(0..dims);
bits_to_set.push((idx, hv_b.get(idx)));
}
bits_to_set
};
let hv_a = self.token_hvs.entry(a.to_string()).or_insert_with(|| make_hv(a.len() as u64, dims));
for (idx, val) in hv_b_val {
hv_a.set(idx, val);
}
}
pub fn encode_tokens(&mut self, tokens: &[String]) -> Hypervector {
let mut sorted: Vec<String> = tokens.to_vec();
sorted.sort();
sorted.dedup();
let k = sorted.len();
let dims = self.dims;
let hvs: Vec<Hypervector> = sorted.iter()
.map(|t| self.ensure_token_hv(t).clone())
.collect();
let mut hv = Hypervector::new(dims);
if k == 0 { return hv; }
let ones: Vec<i32> = {
let mut acc = vec![0i32; dims];
for t in &hvs {
for (i, slot) in acc.iter_mut().enumerate() {
if t.get(i) == 1 { *slot += 1; }
}
}
acc
};
for (i, o) in ones.iter().enumerate() {
if *o * 2 > k as i32 { hv.set(i, 1); }
}
hv
}
pub fn encode_query(&self, query: &str) -> Hypervector {
let tokens = scan_tokens(query);
let hvs: Vec<&Hypervector> = tokens.iter()
.filter_map(|t| self.token_hvs.get(t))
.collect();
let mut hv = Hypervector::new(self.dims);
let k = hvs.len();
if k == 0 { return hv; }
for i in 0..self.dims {
let ones = hvs.iter().filter(|h| h.get(i) == 1).count();
if ones * 2 > k { hv.set(i, 1); } else { hv.set(i, -1); }
}
hv
}
pub fn recall(&self, query: &str, top_n: usize) -> Vec<ScoredMemory> {
if self.memories.is_empty() { return vec![]; }
let q_hv = self.encode_query(query);
let mut scored: Vec<(usize, f64)> = self.memories.iter().enumerate().map(|(i, m)| {
let m_hv = self.encode_query(&m.content);
let score = q_hv.cosine(&m_hv);
(i, score)
}).collect();
if top_n < scored.len() {
scored.select_nth_unstable_by(top_n, |a, b| {
b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal)
});
}
scored.truncate(top_n);
scored.into_iter().map(|(i, score)| {
ScoredMemory { memory: self.memories[i].clone(), score }
}).collect()
}
pub fn predict(&self, query: &str, top_n: usize) -> Vec<(String, u64)> {
let tokens = scan_tokens(query);
let mut scores: FxHashMap<String, u64> = FxHashMap::default();
for t in &tokens {
for ((a, b), count) in &self.cooccur {
if a == t { *scores.entry(b.clone()).or_insert(0) += count; }
if b == t { *scores.entry(a.clone()).or_insert(0) += count; }
}
}
let mut sorted: Vec<(String, u64)> = scores.into_iter().collect();
if top_n < sorted.len() {
sorted.select_nth_unstable_by(top_n, |a, b| b.1.cmp(&a.1));
}
sorted.truncate(top_n);
sorted
}
pub fn consolidate(&mut self) {
let now = chrono::Utc::now().timestamp();
for m in &mut self.memories {
if m.recall_count >= 10 && m.tier < 2 { m.tier = 2; }
if m.recall_count >= 3 && m.tier < 1 && now - m.timestamp < 7 * 86400 { m.tier = 1; }
}
}
}
pub fn fnv_seed(bytes:&[u8])->u64{fnv1a(bytes)}
fn fnv1a(bytes: &[u8]) -> u64 {
let mut h: u64 = 0xcbf29ce484222325;
for &b in bytes {
h ^= b as u64;
h = h.wrapping_mul(0x100000001b3);
}
h
}
pub fn scan_tokens(text: &str) -> Vec<String> {
text.split(|c: char| !c.is_alphanumeric() && c != '_')
.filter(|t| t.len() >= 3 && t.chars().any(|c| c.is_alphabetic()))
.map(|t| t.to_ascii_lowercase())
.collect()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_make_hv() {
let hv = make_hv(42, 10000);
assert_eq!(hv.dims, 10000);
let mut has_pos = false;
let mut has_neg = false;
for i in 0..100 {
match hv.get(i) {
1 => has_pos = true,
-1 => has_neg = true,
_ => {}
}
}
assert!(has_pos, "HV should contain +1 values");
assert!(has_neg, "HV should contain -1 values");
}
#[test]
fn test_bundle_and_clamp() {
let mut a = Hypervector::new(100);
let mut b = Hypervector::new(100);
for i in 0..100 { a.set(i, 1); b.set(i, -1); }
bundle(&mut a, &b);
}
#[test]
fn test_retain_and_recall() {
let mut store = MemoryStore::new(100);
store.retain("Alice works at Google", "user", false);
let results = store.recall("Alice", 5);
assert_eq!(results.len(), 1);
assert!(results[0].score > 0.0);
}
#[test]
fn test_prediction() {
let mut store = MemoryStore::new(100);
store.retain("Alice works at Google", "user", false);
store.retain("Bob works at Google too", "user", false);
let preds = store.predict("Alice", 5);
assert!(!preds.is_empty());
assert!(preds.iter().any(|(t, _)| t.contains("google")));
}
#[test]
fn test_empty_store() {
let store = MemoryStore::new(100);
assert!(store.recall("anything", 5).is_empty());
}
}
#[cfg(test)]
mod v80_recall_tests {
use super::*;
#[test]
fn recall_discriminates_between_queries() {
let mut store = MemoryStore::new(2048);
store.retain("Alice works at Google", "user", false);
store.retain("Bob writes Rust compilers", "user", false);
store.retain("weather is sunny today", "user", false);
let alice = store.recall("Alice works at Google", 3);
let zzz = store.recall("zzzz qqqq unrelated", 3);
assert!(alice[0].score > zzz[0].score, "scores collapse: exact={} unrelated={}", alice[0].score, zzz[0].score);
assert!(alice[0].score > 0.2, "exact-query score too low: {}", alice[0].score);
}
}