use crate::kernel::{Planner, WisdomEntry};
use crate::prelude::*;
const WISDOM_MARKER: &str = "oxifft-wisdom";
const WISDOM_LEGACY_HEADER: &str = "oxifft-wisdom-1.0";
pub const WISDOM_FORMAT_VERSION: u32 = 1;
#[derive(Debug, Clone, PartialEq, Eq)]
#[must_use]
pub struct WisdomImportResult {
pub imported: usize,
pub skipped_invalid: usize,
pub format_version: u32,
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[must_use]
pub struct WisdomMergeResult {
pub added: usize,
pub replaced: usize,
pub kept_existing: usize,
pub skipped_invalid: usize,
pub format_version: u32,
}
static GLOBAL_WISDOM: RwLock<Option<WisdomCache>> = RwLock::new(None);
#[derive(Debug, Clone, Default)]
pub struct WisdomCache {
entries: HashMap<u64, WisdomEntry>,
}
impl WisdomCache {
#[must_use]
pub fn new() -> Self {
Self {
entries: HashMap::new(),
}
}
#[must_use]
pub fn len(&self) -> usize {
self.entries.len()
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.entries.is_empty()
}
#[must_use]
pub fn lookup(&self, hash: u64) -> Option<&WisdomEntry> {
self.entries.get(&hash)
}
pub fn store(&mut self, entry: WisdomEntry) {
self.entries.insert(entry.problem_hash, entry);
}
pub fn clear(&mut self) {
self.entries.clear();
}
pub fn import_from_planner<T: crate::kernel::Float>(&mut self, planner: &Planner<T>) {
let exported = planner.wisdom_export();
let _ = self.import_string(&exported);
}
pub fn export_to_planner<T: crate::kernel::Float>(&self, planner: &mut Planner<T>) {
let exported = self.export_string();
let _ = planner.wisdom_import(&exported);
}
#[must_use]
pub fn export_string(&self) -> String {
use core::fmt::Write;
let mut result = format!("({WISDOM_MARKER}\n (format_version {WISDOM_FORMAT_VERSION})\n");
for entry in self.entries.values() {
let _ = writeln!(
result,
" ({} \"{}\" {})",
entry.problem_hash, entry.solver_name, entry.cost
);
}
result.push(')');
result
}
pub fn import_string(&mut self, s: &str) -> Result<WisdomImportResult, WisdomError> {
let s = s.trim();
let format_version = detect_format_version(s)?;
if format_version > WISDOM_FORMAT_VERSION {
return Err(WisdomError::IncompatibleVersion {
found: format_version,
expected: WISDOM_FORMAT_VERSION,
});
}
let mut imported = 0usize;
let mut skipped_invalid = 0usize;
for line in s.lines().skip(1) {
let line = line.trim();
if !is_entry_line(line) {
continue;
}
match parse_entry_line(line) {
Some(entry) if is_valid_entry(&entry) => {
self.entries.insert(entry.problem_hash, entry);
imported += 1;
}
Some(_) => {
skipped_invalid += 1;
}
None => {
skipped_invalid += 1;
}
}
}
Ok(WisdomImportResult {
imported,
skipped_invalid,
format_version,
})
}
pub fn merge_string(&mut self, s: &str) -> Result<WisdomMergeResult, WisdomError> {
let s = s.trim();
let format_version = detect_format_version(s)?;
if format_version > WISDOM_FORMAT_VERSION {
return Err(WisdomError::IncompatibleVersion {
found: format_version,
expected: WISDOM_FORMAT_VERSION,
});
}
let mut added = 0usize;
let mut replaced = 0usize;
let mut kept_existing = 0usize;
let mut skipped_invalid = 0usize;
for line in s.lines().skip(1) {
let line = line.trim();
if !is_entry_line(line) {
continue;
}
match parse_entry_line(line) {
Some(entry) if is_valid_entry(&entry) => {
match self.entries.get(&entry.problem_hash) {
None => {
self.entries.insert(entry.problem_hash, entry);
added += 1;
}
Some(existing) if entry.cost < existing.cost => {
self.entries.insert(entry.problem_hash, entry);
replaced += 1;
}
Some(_) => {
kept_existing += 1;
}
}
}
_ => {
skipped_invalid += 1;
}
}
}
Ok(WisdomMergeResult {
added,
replaced,
kept_existing,
skipped_invalid,
format_version,
})
}
}
fn detect_format_version(s: &str) -> Result<u32, WisdomError> {
let first_line = s.lines().next().unwrap_or("").trim();
if first_line.starts_with(&format!("({WISDOM_LEGACY_HEADER}")) {
return Ok(0);
}
if first_line.starts_with(&format!("({WISDOM_MARKER}")) {
for line in s.lines().skip(1).take(5) {
let line = line.trim();
if let Some(ver) = parse_format_version_line(line) {
return Ok(ver);
}
}
return Ok(1);
}
Err(WisdomError::ParseError(
"missing oxifft-wisdom header".to_string(),
))
}
fn parse_format_version_line(line: &str) -> Option<u32> {
let line = line.trim();
if !line.starts_with("(format_version ") || !line.ends_with(')') {
return None;
}
let inner = &line["(format_version ".len()..line.len() - 1];
inner.trim().parse::<u32>().ok()
}
fn is_entry_line(line: &str) -> bool {
line.starts_with('(')
&& line.ends_with(')')
&& !line.starts_with(&format!("({WISDOM_MARKER}"))
&& !line.starts_with(&format!("({WISDOM_LEGACY_HEADER}"))
&& !line.starts_with("(format_version ")
}
fn parse_entry_line(line: &str) -> Option<WisdomEntry> {
let inner = line.get(1..line.len().checked_sub(1)?)?;
let parts: Vec<&str> = inner.split_whitespace().collect();
if parts.len() < 3 {
return None;
}
let hash = parts[0].parse::<u64>().ok()?;
let solver_name = parts[1].trim_matches('"').to_string();
let cost = parts[2].parse::<f64>().ok()?;
Some(WisdomEntry {
problem_hash: hash,
solver_name,
cost,
})
}
fn is_valid_entry(entry: &WisdomEntry) -> bool {
entry.problem_hash != 0
&& !entry.solver_name.is_empty()
&& entry.cost.is_finite()
&& entry.cost >= 0.0
}
fn ensure_global_wisdom() {
#[cfg(feature = "std")]
{
let read_guard = GLOBAL_WISDOM.read().expect("Global wisdom lock poisoned");
if read_guard.is_none() {
drop(read_guard);
let mut write_guard = GLOBAL_WISDOM.write().expect("Global wisdom lock poisoned");
if write_guard.is_none() {
*write_guard = Some(WisdomCache::new());
}
}
}
#[cfg(not(feature = "std"))]
{
let read_guard = GLOBAL_WISDOM.read();
if read_guard.is_none() {
drop(read_guard);
let mut write_guard = GLOBAL_WISDOM.write();
if write_guard.is_none() {
*write_guard = Some(WisdomCache::new());
}
}
}
}
fn with_wisdom<F, R>(f: F) -> R
where
F: FnOnce(&WisdomCache) -> R,
{
ensure_global_wisdom();
#[cfg(feature = "std")]
{
let guard = GLOBAL_WISDOM.read().expect("Global wisdom lock poisoned");
f(guard.as_ref().expect("Global wisdom not initialized"))
}
#[cfg(not(feature = "std"))]
{
let guard = GLOBAL_WISDOM.read();
f(guard.as_ref().expect("Global wisdom not initialized"))
}
}
fn with_wisdom_mut<F, R>(f: F) -> R
where
F: FnOnce(&mut WisdomCache) -> R,
{
ensure_global_wisdom();
#[cfg(feature = "std")]
{
let mut guard = GLOBAL_WISDOM.write().expect("Global wisdom lock poisoned");
f(guard.as_mut().expect("Global wisdom not initialized"))
}
#[cfg(not(feature = "std"))]
{
let mut guard = GLOBAL_WISDOM.write();
f(guard.as_mut().expect("Global wisdom not initialized"))
}
}
#[must_use]
pub fn export_to_string() -> String {
with_wisdom(WisdomCache::export_string)
}
pub fn import_from_string(s: &str) -> Result<WisdomImportResult, WisdomError> {
with_wisdom_mut(|cache| cache.import_string(s))
}
pub fn merge_from_string(s: &str) -> Result<WisdomMergeResult, WisdomError> {
with_wisdom_mut(|cache| cache.merge_string(s))
}
#[cfg(feature = "std")]
pub fn export_to_file(path: &std::path::Path) -> std::io::Result<()> {
let wisdom = export_to_string();
std::fs::write(path, wisdom)
}
#[cfg(feature = "std")]
pub fn import_from_file(path: &std::path::Path) -> Result<WisdomImportResult, WisdomError> {
let contents = std::fs::read_to_string(path)?;
import_from_string(&contents)
}
#[cfg(feature = "std")]
pub fn merge_from_file(path: &std::path::Path) -> Result<WisdomMergeResult, WisdomError> {
let contents = std::fs::read_to_string(path)?;
merge_from_string(&contents)
}
#[cfg(feature = "std")]
pub fn import_system_wisdom() -> Result<WisdomImportResult, WisdomError> {
let paths = get_system_wisdom_paths();
for path in paths {
if path.exists() {
if let Ok(result) = import_from_file(&path) {
return Ok(result);
}
}
}
Err(WisdomError::IoError(std::io::Error::new(
std::io::ErrorKind::NotFound,
"No system wisdom found",
)))
}
#[cfg(feature = "std")]
#[must_use]
pub fn get_user_wisdom_path() -> Option<std::path::PathBuf> {
#[cfg(target_os = "linux")]
{
if let Some(config_dir) = std::env::var_os("XDG_CONFIG_HOME") {
let mut path = std::path::PathBuf::from(config_dir);
path.push("oxifft");
path.push("wisdom");
return Some(path);
}
if let Some(home) = std::env::var_os("HOME") {
let mut path = std::path::PathBuf::from(home);
path.push(".config");
path.push("oxifft");
path.push("wisdom");
return Some(path);
}
}
#[cfg(target_os = "macos")]
{
if let Some(home) = std::env::var_os("HOME") {
let mut path = std::path::PathBuf::from(home);
path.push("Library");
path.push("Application Support");
path.push("oxifft");
path.push("wisdom");
return Some(path);
}
}
#[cfg(target_os = "windows")]
{
if let Some(appdata) = std::env::var_os("APPDATA") {
let mut path = std::path::PathBuf::from(appdata);
path.push("oxifft");
path.push("wisdom");
return Some(path);
}
}
None
}
#[cfg(feature = "std")]
fn get_system_wisdom_paths() -> Vec<std::path::PathBuf> {
let mut paths = Vec::new();
if let Some(user_path) = get_user_wisdom_path() {
paths.push(user_path);
}
#[cfg(target_os = "linux")]
{
paths.push(std::path::PathBuf::from("/etc/oxifft/wisdom"));
paths.push(std::path::PathBuf::from("/usr/share/oxifft/wisdom"));
}
#[cfg(target_os = "macos")]
{
paths.push(std::path::PathBuf::from(
"/Library/Application Support/oxifft/wisdom",
));
}
paths
}
pub fn forget() {
with_wisdom_mut(WisdomCache::clear);
}
#[must_use]
pub fn wisdom_count() -> usize {
with_wisdom(WisdomCache::len)
}
pub fn store_wisdom(entry: WisdomEntry) {
with_wisdom_mut(|cache| cache.store(entry));
}
#[must_use]
pub fn lookup_wisdom(hash: u64) -> Option<WisdomEntry> {
with_wisdom(|cache| cache.lookup(hash).cloned())
}
#[derive(Debug)]
#[non_exhaustive]
pub enum WisdomError {
ParseError(String),
IncompatibleVersion {
found: u32,
expected: u32,
},
#[cfg(feature = "std")]
IoError(std::io::Error),
}
impl core::fmt::Display for WisdomError {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
match self {
Self::ParseError(msg) => write!(f, "Wisdom parse error: {msg}"),
Self::IncompatibleVersion { found, expected } => write!(
f,
"Wisdom format version {found} is not supported \
(this build understands up to version {expected})"
),
#[cfg(feature = "std")]
Self::IoError(e) => write!(f, "I/O error: {e}"),
}
}
}
#[cfg(feature = "std")]
impl std::error::Error for WisdomError {}
#[cfg(feature = "std")]
impl From<std::io::Error> for WisdomError {
fn from(e: std::io::Error) -> Self {
Self::IoError(e)
}
}
#[cfg(test)]
mod tests {
use super::*;
static GLOBAL_WISDOM_TEST_LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(());
#[test]
fn test_wisdom_cache_basic() {
let mut cache = WisdomCache::new();
assert!(cache.is_empty());
let entry = WisdomEntry {
problem_hash: 12345,
solver_name: "ct-dit".to_string(),
cost: 100.0,
};
cache.store(entry);
assert_eq!(cache.len(), 1);
assert!(!cache.is_empty());
let looked_up = cache.lookup(12345).expect("entry not found");
assert_eq!(looked_up.solver_name, "ct-dit");
assert!((looked_up.cost - 100.0).abs() < f64::EPSILON);
}
#[test]
fn test_wisdom_export_import_v1() {
let mut cache = WisdomCache::new();
cache.store(WisdomEntry {
problem_hash: 111,
solver_name: "rader".to_string(),
cost: 50.0,
});
cache.store(WisdomEntry {
problem_hash: 222,
solver_name: "bluestein".to_string(),
cost: 75.0,
});
let exported = cache.export_string();
assert!(exported.contains(WISDOM_MARKER));
assert!(exported.contains("format_version"));
assert!(exported.contains("111"));
assert!(exported.contains("rader"));
let mut cache2 = WisdomCache::new();
let result = cache2.import_string(&exported).expect("import failed");
assert_eq!(result.imported, 2);
assert_eq!(result.skipped_invalid, 0);
assert_eq!(result.format_version, WISDOM_FORMAT_VERSION);
assert_eq!(cache2.len(), 2);
let entry = cache2.lookup(111).expect("entry not found");
assert_eq!(entry.solver_name, "rader");
}
#[test]
fn test_wisdom_legacy_format_accepted() {
let legacy = "(oxifft-wisdom-1.0\n (111 \"rader\" 50)\n (222 \"bluestein\" 75)\n)";
let mut cache = WisdomCache::new();
let result = cache.import_string(legacy).expect("legacy import failed");
assert_eq!(result.format_version, 0);
assert_eq!(result.imported, 2);
assert_eq!(cache.len(), 2);
}
#[test]
fn test_wisdom_incompatible_version_rejected() {
let future_version = WISDOM_FORMAT_VERSION + 1;
let future_wisdom = format!(
"({WISDOM_MARKER}\n (format_version {future_version})\n (111 \"rader\" 50)\n)"
);
let mut cache = WisdomCache::new();
let err = cache
.import_string(&future_wisdom)
.expect_err("should have rejected future version");
assert!(
matches!(
err,
WisdomError::IncompatibleVersion {
found,
expected
} if found == future_version && expected == WISDOM_FORMAT_VERSION
),
"unexpected error: {err}"
);
}
#[test]
fn test_wisdom_import_skips_invalid_entries() {
let wisdom_str = format!(
"({WISDOM_MARKER}\n\
(format_version {WISDOM_FORMAT_VERSION})\n\
(0 \"ct-dit\" 1.0)\n\
(333 \"\" 1.0)\n\
(444 \"ct-dit\" NaN)\n\
(555 \"ct-dit\" -1.0)\n\
(999 \"ct-dit\" 42.0)\n\
)"
);
let mut cache = WisdomCache::new();
let result = cache
.import_string(&wisdom_str)
.expect("import should succeed");
assert_eq!(
result.imported, 1,
"only the valid entry should be imported"
);
assert_eq!(
result.skipped_invalid, 4,
"four invalid entries should be skipped"
);
assert!(cache.lookup(999).is_some());
assert!(cache.lookup(0).is_none());
assert!(cache.lookup(333).is_none());
}
#[test]
fn test_wisdom_merge_adds_new_entries() {
let mut cache_a = WisdomCache::new();
cache_a.store(WisdomEntry {
problem_hash: 100,
solver_name: "ct-dit".to_string(),
cost: 10.0,
});
let mut cache_b = WisdomCache::new();
cache_b.store(WisdomEntry {
problem_hash: 200,
solver_name: "bluestein".to_string(),
cost: 20.0,
});
let b_str = cache_b.export_string();
let merge = cache_a.merge_string(&b_str).expect("merge failed");
assert_eq!(merge.added, 1);
assert_eq!(merge.replaced, 0);
assert_eq!(merge.kept_existing, 0);
assert_eq!(merge.skipped_invalid, 0);
assert_eq!(cache_a.len(), 2);
}
#[test]
fn test_wisdom_merge_lower_cost_wins() {
let mut cache_a = WisdomCache::new();
cache_a.store(WisdomEntry {
problem_hash: 100,
solver_name: "ct-dit".to_string(),
cost: 50.0,
});
let incoming = format!(
"({WISDOM_MARKER}\n\
(format_version {WISDOM_FORMAT_VERSION})\n\
(100 \"stockham\" 20.0)\n\
)"
);
let merge = cache_a.merge_string(&incoming).expect("merge failed");
assert_eq!(merge.replaced, 1);
assert_eq!(merge.added, 0);
assert_eq!(merge.kept_existing, 0);
let entry = cache_a.lookup(100).expect("entry must still exist");
assert_eq!(entry.solver_name, "stockham");
assert!((entry.cost - 20.0).abs() < f64::EPSILON);
}
#[test]
fn test_wisdom_merge_keeps_existing_if_better() {
let mut cache_a = WisdomCache::new();
cache_a.store(WisdomEntry {
problem_hash: 100,
solver_name: "ct-dit".to_string(),
cost: 10.0,
});
let incoming = format!(
"({WISDOM_MARKER}\n\
(format_version {WISDOM_FORMAT_VERSION})\n\
(100 \"rader\" 99.0)\n\
)"
);
let merge = cache_a.merge_string(&incoming).expect("merge failed");
assert_eq!(merge.kept_existing, 1);
assert_eq!(merge.replaced, 0);
let entry = cache_a.lookup(100).expect("entry must still exist");
assert_eq!(entry.solver_name, "ct-dit"); }
#[test]
fn test_wisdom_merge_rejects_future_version() {
let future_version = WISDOM_FORMAT_VERSION + 5;
let future_wisdom = format!(
"({WISDOM_MARKER}\n (format_version {future_version})\n (100 \"rader\" 1.0)\n)"
);
let mut cache = WisdomCache::new();
let err = cache
.merge_string(&future_wisdom)
.expect_err("should have rejected future version");
assert!(matches!(
err,
WisdomError::IncompatibleVersion { found, .. } if found == future_version
));
}
#[test]
fn test_wisdom_version_mismatch_unknown_header() {
let mut cache = WisdomCache::new();
let result = cache.import_string("(totally-unknown-header\n)");
assert!(matches!(result, Err(WisdomError::ParseError(_))));
}
#[test]
fn test_global_wisdom_functions() {
let _guard = GLOBAL_WISDOM_TEST_LOCK
.lock()
.unwrap_or_else(|e| e.into_inner());
forget();
assert_eq!(wisdom_count(), 0);
store_wisdom(WisdomEntry {
problem_hash: 999,
solver_name: "generic".to_string(),
cost: 200.0,
});
assert_eq!(wisdom_count(), 1);
let entry = lookup_wisdom(999).expect("entry not found");
assert_eq!(entry.solver_name, "generic");
let exported = export_to_string();
forget();
assert_eq!(wisdom_count(), 0);
let result = import_from_string(&exported).expect("import failed");
assert_eq!(result.imported, 1);
assert_eq!(wisdom_count(), 1);
forget();
}
#[test]
fn test_global_merge_from_string() {
let _guard = GLOBAL_WISDOM_TEST_LOCK
.lock()
.unwrap_or_else(|e| e.into_inner());
forget();
store_wisdom(WisdomEntry {
problem_hash: 1,
solver_name: "ct-dit".to_string(),
cost: 30.0,
});
let incoming = format!(
"({WISDOM_MARKER}\n\
(format_version {WISDOM_FORMAT_VERSION})\n\
(1 \"stockham\" 5.0)\n\
(2 \"rader\" 10.0)\n\
)"
);
let merge = merge_from_string(&incoming).expect("merge failed");
assert_eq!(merge.replaced, 1);
assert_eq!(merge.added, 1);
assert_eq!(merge.kept_existing, 0);
assert_eq!(wisdom_count(), 2);
forget();
}
#[cfg(feature = "std")]
#[test]
fn test_import_export_file_roundtrip() {
let _guard = GLOBAL_WISDOM_TEST_LOCK
.lock()
.unwrap_or_else(|e| e.into_inner());
let dir = std::env::temp_dir();
let path = dir.join("oxifft_wisdom_test_roundtrip.txt");
forget();
store_wisdom(WisdomEntry {
problem_hash: 42,
solver_name: "bluestein".to_string(),
cost: 7.5,
});
export_to_file(&path).expect("export failed");
forget();
assert_eq!(wisdom_count(), 0);
let result = import_from_file(&path).expect("import failed");
assert_eq!(result.imported, 1);
assert_eq!(wisdom_count(), 1);
assert_eq!(
lookup_wisdom(42).expect("entry missing").solver_name,
"bluestein"
);
let _ = std::fs::remove_file(&path);
forget();
}
#[cfg(feature = "std")]
#[test]
fn test_merge_from_file() {
let _guard = GLOBAL_WISDOM_TEST_LOCK
.lock()
.unwrap_or_else(|e| e.into_inner());
let dir = std::env::temp_dir();
let path = dir.join("oxifft_wisdom_test_merge.txt");
forget();
store_wisdom(WisdomEntry {
problem_hash: 7,
solver_name: "ct-dit".to_string(),
cost: 100.0,
});
let content = format!(
"({WISDOM_MARKER}\n\
(format_version {WISDOM_FORMAT_VERSION})\n\
(7 \"stockham\" 25.0)\n\
)"
);
std::fs::write(&path, &content).expect("write failed");
let merge = merge_from_file(&path).expect("merge failed");
assert_eq!(merge.replaced, 1);
assert_eq!(
lookup_wisdom(7).expect("entry missing").solver_name,
"stockham"
);
let _ = std::fs::remove_file(&path);
forget();
}
#[test]
fn test_wisdom_clear() {
let mut cache = WisdomCache::new();
cache.store(WisdomEntry {
problem_hash: 1,
solver_name: "test".to_string(),
cost: 1.0,
});
assert!(!cache.is_empty());
cache.clear();
assert!(cache.is_empty());
}
#[cfg(feature = "std")]
#[test]
fn test_user_wisdom_path() {
let _path = get_user_wisdom_path();
}
}