use std::collections::BTreeMap;
use std::path::{Path, PathBuf};
use crate::ast::{self, Item};
use crate::hash::{self, ContentHash};
pub struct Codebase {
pub(super) definitions: BTreeMap<ContentHash, Definition>,
pub(super) names: BTreeMap<String, ContentHash>,
pub(super) name_history: BTreeMap<ContentHash, Vec<NameEntry>>,
pub(super) root: PathBuf,
}
#[derive(Clone)]
pub struct Definition {
pub source: String,
pub module: String,
pub is_pub: bool,
pub params: Vec<(String, String)>,
pub return_ty: Option<String>,
pub dependencies: Vec<ContentHash>,
pub requires: Vec<String>,
pub ensures: Vec<String>,
pub first_seen: u64,
}
pub struct NameEntry {
pub name: String,
pub timestamp: u64,
}
pub struct AddResult {
pub added: usize,
pub updated: usize,
pub unchanged: usize,
}
pub struct CodebaseStats {
pub definitions: usize,
pub names: usize,
pub total_source_bytes: usize,
}
mod deps;
mod format;
mod persist;
use deps::extract_dependencies;
use format::{format_fn_source, format_type};
use persist::{atomic_write, codebase_dir, serialize_definition, unix_timestamp};
#[cfg(test)]
mod tests;
impl Codebase {
pub fn open() -> std::io::Result<Self> {
let root = codebase_dir().ok_or_else(|| {
std::io::Error::new(
std::io::ErrorKind::NotFound,
"cannot determine codebase directory (no $HOME)",
)
})?;
Self::open_at(&root)
}
pub fn open_at(root: &Path) -> std::io::Result<Self> {
std::fs::create_dir_all(root)?;
std::fs::create_dir_all(root.join("defs"))?;
let mut cb = Codebase {
definitions: BTreeMap::new(),
names: BTreeMap::new(),
name_history: BTreeMap::new(),
root: root.to_path_buf(),
};
cb.load()?;
Ok(cb)
}
pub fn add_file(&mut self, file: &ast::File) -> AddResult {
let fn_hashes = hash::hash_file(file);
let module = file.name.node.clone();
let now = unix_timestamp();
let mut added = 0usize;
let mut updated = 0usize;
let mut unchanged = 0usize;
for item in &file.items {
if let Item::Fn(func) = &item.node {
let name = func.name.node.clone();
let Some(hash) = fn_hashes.get(&name).copied() else {
continue;
};
if let Some(existing) = self.names.get(&name) {
if *existing == hash {
unchanged += 1;
continue;
}
updated += 1;
} else {
added += 1;
}
let deps = extract_dependencies(func, &fn_hashes);
let def = Definition {
source: format_fn_source(func),
module: module.clone(),
is_pub: func.is_pub,
params: func
.params
.iter()
.map(|p| (p.name.node.clone(), format_type(&p.ty.node)))
.collect(),
return_ty: func.return_ty.as_ref().map(|t| format_type(&t.node)),
dependencies: deps,
requires: func.requires.iter().map(|s| s.node.clone()).collect(),
ensures: func.ensures.iter().map(|s| s.node.clone()).collect(),
first_seen: self
.definitions
.get(&hash)
.map(|d| d.first_seen)
.unwrap_or(now),
};
self.definitions.insert(hash, def);
let entry = NameEntry {
name: name.clone(),
timestamp: now,
};
self.name_history.entry(hash).or_default().push(entry);
self.names.insert(name, hash);
}
}
AddResult {
added,
updated,
unchanged,
}
}
pub fn lookup(&self, name: &str) -> Option<&Definition> {
let hash = self.names.get(name)?;
self.definitions.get(hash)
}
pub fn hash_for_name(&self, name: &str) -> Option<&ContentHash> {
self.names.get(name)
}
pub fn lookup_hash(&self, hash: &ContentHash) -> Option<&Definition> {
self.definitions.get(hash)
}
pub fn list_names(&self) -> Vec<(&str, &ContentHash)> {
let mut list: Vec<(&str, &ContentHash)> =
self.names.iter().map(|(n, h)| (n.as_str(), h)).collect();
list.sort_by_key(|(name, _)| *name);
list
}
pub fn rename(&mut self, old_name: &str, new_name: &str) -> Result<(), String> {
let hash = self
.names
.get(old_name)
.copied()
.ok_or_else(|| format!("name '{}' not found", old_name))?;
if self.names.contains_key(new_name) {
return Err(format!("name '{}' already exists", new_name));
}
self.names.remove(old_name);
self.names.insert(new_name.to_string(), hash);
let entry = NameEntry {
name: new_name.to_string(),
timestamp: unix_timestamp(),
};
self.name_history.entry(hash).or_default().push(entry);
Ok(())
}
pub fn alias(&mut self, name: &str, alias: &str) -> Result<(), String> {
let hash = self
.names
.get(name)
.copied()
.ok_or_else(|| format!("name '{}' not found", name))?;
if self.names.contains_key(alias) {
return Err(format!("name '{}' already exists", alias));
}
self.names.insert(alias.to_string(), hash);
let entry = NameEntry {
name: alias.to_string(),
timestamp: unix_timestamp(),
};
self.name_history.entry(hash).or_default().push(entry);
Ok(())
}
pub fn name_history(&self, name: &str) -> Vec<(ContentHash, u64)> {
let mut result = Vec::new();
for (hash, entries) in &self.name_history {
for entry in entries {
if entry.name == name {
result.push((*hash, entry.timestamp));
}
}
}
result.sort_by_key(|(_, ts)| *ts);
result
}
pub fn names_for_hash(&self, hash: &ContentHash) -> Vec<&str> {
let mut names: Vec<&str> = self
.names
.iter()
.filter(|(_, h)| *h == hash)
.map(|(n, _)| n.as_str())
.collect();
names.sort();
names
}
pub fn dependencies(&self, hash: &ContentHash) -> Vec<(&str, &ContentHash)> {
let def = match self.definitions.get(hash) {
Some(d) => d,
None => return Vec::new(),
};
let mut result = Vec::new();
for dep_hash in &def.dependencies {
let name = self
.names
.iter()
.find(|(_, h)| *h == dep_hash)
.map(|(n, _)| n.as_str())
.unwrap_or("<unnamed>");
result.push((name, dep_hash));
}
result
}
pub fn dependents(&self, hash: &ContentHash) -> Vec<(&str, &ContentHash)> {
let mut result = Vec::new();
for (def_hash, def) in &self.definitions {
if def.dependencies.contains(hash) {
let name = self
.names
.iter()
.find(|(_, h)| *h == def_hash)
.map(|(n, _)| n.as_str())
.unwrap_or("<unnamed>");
result.push((name, def_hash));
}
}
result.sort_by_key(|(name, _)| *name);
result
}
pub fn stats(&self) -> CodebaseStats {
let total_source_bytes = self.definitions.values().map(|d| d.source.len()).sum();
CodebaseStats {
definitions: self.definitions.len(),
names: self.names.len(),
total_source_bytes,
}
}
pub fn save(&self) -> std::io::Result<()> {
let defs_dir = self.root.join("defs");
std::fs::create_dir_all(&defs_dir)?;
for (hash, def) in &self.definitions {
let hex = hash.to_hex();
let prefix = &hex[..2];
let prefix_dir = defs_dir.join(prefix);
std::fs::create_dir_all(&prefix_dir)?;
let def_path = prefix_dir.join(format!("{}.def", hex));
let content = serialize_definition(def);
atomic_write(&def_path, &content)?;
}
let names_path = self.root.join("names.txt");
let mut names_content = String::new();
let mut sorted_names: Vec<_> = self.names.iter().collect();
sorted_names.sort_by_key(|(n, _)| (*n).clone());
for (name, hash) in sorted_names {
names_content.push_str(name);
names_content.push('=');
names_content.push_str(&hash.to_hex());
names_content.push('\n');
}
atomic_write(&names_path, &names_content)?;
let history_path = self.root.join("history.txt");
let mut history_content = String::new();
let mut all_entries: Vec<(&ContentHash, &NameEntry)> = Vec::new();
for (hash, entries) in &self.name_history {
for entry in entries {
all_entries.push((hash, entry));
}
}
all_entries.sort_by_key(|(_, e)| e.timestamp);
for (hash, entry) in all_entries {
history_content.push_str(&entry.name);
history_content.push(' ');
history_content.push_str(&hash.to_hex());
history_content.push(' ');
history_content.push_str(&entry.timestamp.to_string());
history_content.push('\n');
}
atomic_write(&history_path, &history_content)?;
Ok(())
}
pub fn store_definition(&mut self, hash: ContentHash, def: Definition) {
self.definitions.insert(hash, def);
}
pub fn bind_name(&mut self, name: &str, hash: ContentHash) {
self.names.insert(name.to_string(), hash);
let entry = NameEntry {
name: name.to_string(),
timestamp: unix_timestamp(),
};
self.name_history.entry(hash).or_default().push(entry);
}
pub fn view(&self, name: &str) -> Option<String> {
let hash = self.names.get(name)?;
let def = self.definitions.get(hash)?;
let mut out = String::new();
out.push_str(&format!("-- {} {}\n", name, hash));
for req in &def.requires {
out.push_str(&format!("#[requires({})]\n", req));
}
for ens in &def.ensures {
out.push_str(&format!("#[ensures({})]\n", ens));
}
out.push_str(&def.source);
if !out.ends_with('\n') {
out.push('\n');
}
if !def.dependencies.is_empty() {
out.push_str("\n-- Dependencies:\n");
for dep_hash in &def.dependencies {
let dep_name = self
.names
.iter()
.find(|(_, h)| *h == dep_hash)
.map(|(n, _)| n.as_str())
.unwrap_or("<unnamed>");
out.push_str(&format!("-- {} {}\n", dep_name, dep_hash));
}
}
Some(out)
}
pub fn lookup_by_prefix(&self, prefix: &str) -> Option<(&ContentHash, &Definition)> {
let prefix = prefix.strip_prefix('#').unwrap_or(prefix);
for (hash, def) in &self.definitions {
let hex = hash.to_hex();
if hex.starts_with(prefix) {
return Some((hash, def));
}
let short = hash.to_short();
if short.starts_with(prefix) || short == prefix {
return Some((hash, def));
}
}
None
}
}