use std::collections::HashMap;
use std::fmt;
#[derive(Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct FileId(u32);
impl FileId {
pub const NONE: Self = Self(u32::MAX);
}
impl fmt::Debug for FileId {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "FileId({})", self.0)
}
}
#[derive(Debug, Clone)]
pub struct PathInterner {
to_id: HashMap<String, FileId>,
to_path: Vec<String>,
}
impl PathInterner {
pub fn new() -> Self {
Self {
to_id: HashMap::new(),
to_path: Vec::new(),
}
}
pub fn with_capacity(cap: usize) -> Self {
Self {
to_id: HashMap::with_capacity(cap),
to_path: Vec::with_capacity(cap),
}
}
pub fn intern(&mut self, path: &str) -> FileId {
if let Some(&id) = self.to_id.get(path) {
return id;
}
let id = FileId(self.to_path.len() as u32);
self.to_path.push(path.to_owned());
self.to_id.insert(path.to_owned(), id);
id
}
}
impl PathInterner {
pub fn intern_owned(&mut self, path: String) -> FileId {
if let Some(&id) = self.to_id.get(&path) {
return id;
}
let id = FileId(self.to_path.len() as u32);
self.to_path.push(path.clone());
self.to_id.insert(path, id);
id
}
pub fn get(&self, path: &str) -> Option<FileId> {
self.to_id.get(path).copied()
}
#[inline]
pub fn resolve(&self, id: FileId) -> &str {
&self.to_path[id.0 as usize]
}
pub fn try_resolve(&self, id: FileId) -> Option<&str> {
self.to_path.get(id.0 as usize).map(String::as_str)
}
pub fn len(&self) -> usize {
self.to_path.len()
}
pub fn is_empty(&self) -> bool {
self.to_path.is_empty()
}
pub fn iter(&self) -> impl Iterator<Item = (FileId, &str)> {
self.to_path
.iter()
.enumerate()
.map(|(i, p)| (FileId(i as u32), p.as_str()))
}
}
impl Default for PathInterner {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn intern_returns_same_id_for_same_path() {
let mut interner = PathInterner::new();
let a = interner.intern("src/main.rs");
let b = interner.intern("src/main.rs");
assert_eq!(a, b);
assert_eq!(interner.len(), 1);
}
#[test]
fn distinct_paths_get_distinct_ids() {
let mut interner = PathInterner::new();
let a = interner.intern("src/main.rs");
let b = interner.intern("src/lib.rs");
assert_ne!(a, b);
assert_eq!(interner.len(), 2);
}
#[test]
fn resolve_round_trips() {
let mut interner = PathInterner::new();
let id = interner.intern("src/core/graph_index/mod.rs");
assert_eq!(interner.resolve(id), "src/core/graph_index/mod.rs");
}
#[test]
fn intern_owned_avoids_double_allocation() {
let mut interner = PathInterner::new();
let id1 = interner.intern_owned("src/foo.rs".to_owned());
let id2 = interner.intern("src/foo.rs");
assert_eq!(id1, id2);
assert_eq!(interner.len(), 1);
}
#[test]
fn get_returns_none_for_unknown() {
let interner = PathInterner::new();
assert_eq!(interner.get("nonexistent.rs"), None);
}
#[test]
fn get_returns_some_for_interned() {
let mut interner = PathInterner::new();
let id = interner.intern("src/lib.rs");
assert_eq!(interner.get("src/lib.rs"), Some(id));
}
#[test]
fn try_resolve_returns_none_for_invalid_id() {
let interner = PathInterner::new();
assert_eq!(interner.try_resolve(FileId(999)), None);
assert_eq!(interner.try_resolve(FileId::NONE), None);
}
#[test]
fn file_id_none_is_never_returned_by_intern() {
let mut interner = PathInterner::new();
for i in 0..100 {
let id = interner.intern(&format!("file_{i}.rs"));
assert_ne!(id, FileId::NONE);
}
}
#[test]
fn iter_yields_insertion_order() {
let mut interner = PathInterner::new();
interner.intern("b.rs");
interner.intern("a.rs");
interner.intern("c.rs");
let paths: Vec<&str> = interner.iter().map(|(_, p)| p).collect();
assert_eq!(paths, &["b.rs", "a.rs", "c.rs"]);
}
#[test]
fn file_id_is_copy_and_small() {
assert_eq!(std::mem::size_of::<FileId>(), 4);
let id = FileId(42);
let copy = id;
assert_eq!(id, copy);
}
#[test]
fn file_id_ordering_is_stable() {
let a = FileId(1);
let b = FileId(2);
assert!(a < b);
let mut ids = vec![b, a];
ids.sort();
assert_eq!(ids, vec![a, b]);
}
#[test]
fn with_capacity_works() {
let interner = PathInterner::with_capacity(1000);
assert!(interner.is_empty());
assert_eq!(interner.len(), 0);
}
}