use std::collections::HashMap;
#[derive(Debug)]
struct ScopeFrame {
name: String,
child_counter: u32,
seen: HashMap<String, usize>,
}
#[derive(Debug)]
pub struct IdGenerator {
frames: Vec<ScopeFrame>,
}
impl IdGenerator {
#[must_use]
pub fn new(file_path: &str) -> Self {
Self {
frames: vec![ScopeFrame {
name: file_path.to_owned(),
child_counter: 0,
seen: HashMap::new(),
}],
}
}
pub fn record_name(&mut self, name: &str) -> String {
let Some(frame) = self.frames.last_mut() else {
return name.to_owned();
};
let count = frame.seen.entry(name.to_owned()).or_insert(0);
let n = *count;
*count += 1;
if n == 0 {
name.to_owned()
} else {
format!("{name}#{n}")
}
}
pub fn push_named_scope(&mut self, name: &str) -> String {
let leaf = self.record_name(name);
self.frames.push(ScopeFrame {
name: leaf.clone(),
child_counter: 0,
seen: HashMap::new(),
});
leaf
}
pub fn push_recorded_scope(&mut self, leaf: String) {
self.frames.push(ScopeFrame {
name: leaf,
child_counter: 0,
seen: HashMap::new(),
});
}
pub fn push_anonymous_scope(&mut self) -> u32 {
let Some(parent) = self.frames.last_mut() else {
return 0;
};
let idx = parent.child_counter;
parent.child_counter += 1;
let name = format!("${idx}");
self.frames.push(ScopeFrame {
name,
child_counter: 0,
seen: HashMap::new(),
});
idx
}
pub fn pop_scope(&mut self) {
debug_assert!(
self.frames.len() > 1,
"IdGenerator::pop_scope called at root; walker push/pop is unbalanced"
);
if self.frames.len() > 1 {
self.frames.pop();
}
}
pub fn named_id(&mut self, name: &str) -> String {
let leaf = self.record_name(name);
if self.frames.len() == 1 {
format!("{}::{leaf}", self.frames[0].name)
} else {
format!("{}::{leaf}", self.current_prefix())
}
}
pub fn anonymous_id(&mut self) -> String {
let Some(frame) = self.frames.last_mut() else {
return String::new();
};
let idx = frame.child_counter;
frame.child_counter += 1;
format!("{}::${idx}", self.current_prefix())
}
pub fn field_id(&mut self, base_id: &str, field_name: &str) -> String {
let key = format!("field:{base_id}::{field_name}");
let Some(frame) = self.frames.last_mut() else {
return format!("{base_id}.{field_name}");
};
let count = frame.seen.entry(key).or_insert(0);
let n = *count;
*count += 1;
if n == 0 {
format!("{base_id}.{field_name}")
} else {
format!("{base_id}.{field_name}#{n}")
}
}
#[must_use]
pub fn current_prefix(&self) -> String {
let mut out = String::new();
for (i, frame) in self.frames.iter().enumerate() {
if i > 0 {
out.push_str("::");
}
out.push_str(&frame.name);
}
out
}
#[must_use]
pub fn depth(&self) -> usize {
self.frames.len()
}
pub fn reset_counter(&mut self) {
if let Some(frame) = self.frames.last_mut() {
frame.child_counter = 0;
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn basic_named_ids() {
let mut id_gen = IdGenerator::new("src/main.rs");
assert_eq!(id_gen.named_id("User"), "src/main.rs::User");
}
#[test]
fn nested_scopes() {
let mut id_gen = IdGenerator::new("src/lib.rs");
id_gen.push_named_scope("Parser");
id_gen.push_named_scope("parse");
assert_eq!(
id_gen.named_id("config"),
"src/lib.rs::Parser::parse::config"
);
id_gen.pop_scope();
assert_eq!(id_gen.named_id("new"), "src/lib.rs::Parser::new");
}
#[test]
fn anonymous_ids_increment() {
let mut id_gen = IdGenerator::new("test.ts");
id_gen.push_named_scope("main");
let id0 = id_gen.anonymous_id();
let id1 = id_gen.anonymous_id();
let id2 = id_gen.anonymous_id();
assert_eq!(id0, "test.ts::main::$0");
assert_eq!(id1, "test.ts::main::$1");
assert_eq!(id2, "test.ts::main::$2");
}
#[test]
fn anonymous_scopes() {
let mut id_gen = IdGenerator::new("test.py");
id_gen.push_named_scope("process");
let _stmt_idx = id_gen.push_anonymous_scope(); let inner = id_gen.anonymous_id();
assert_eq!(inner, "test.py::process::$0::$0");
id_gen.pop_scope();
let _stmt_idx2 = id_gen.push_anonymous_scope(); let inner2 = id_gen.anonymous_id();
assert_eq!(inner2, "test.py::process::$1::$0");
}
#[test]
fn field_ids() {
let mut id_gen = IdGenerator::new("test.rs");
let base = id_gen.named_id("expr");
let left = id_gen.field_id(&base, "left");
let right = id_gen.field_id(&base, "right");
assert_eq!(left, "test.rs::expr.left");
assert_eq!(right, "test.rs::expr.right");
}
#[test]
fn depth_tracking() {
let mut id_gen = IdGenerator::new("f.ts");
assert_eq!(id_gen.depth(), 1);
id_gen.push_named_scope("fn");
assert_eq!(id_gen.depth(), 2);
id_gen.push_anonymous_scope();
assert_eq!(id_gen.depth(), 3);
id_gen.pop_scope();
assert_eq!(id_gen.depth(), 2);
}
#[test]
fn duplicate_named_ids_at_same_scope_get_suffixed() {
let mut id_gen = IdGenerator::new("repro.py");
assert_eq!(id_gen.named_id("foo"), "repro.py::foo");
assert_eq!(id_gen.named_id("foo"), "repro.py::foo#1");
assert_eq!(id_gen.named_id("foo"), "repro.py::foo#2");
}
#[test]
fn push_named_scope_uses_disambiguated_leaf() {
let mut id_gen = IdGenerator::new("repro.py");
let first = id_gen.push_named_scope("foo");
assert_eq!(first, "foo");
assert_eq!(id_gen.named_id("body"), "repro.py::foo::body");
id_gen.pop_scope();
let second = id_gen.push_named_scope("foo");
assert_eq!(second, "foo#1");
assert_eq!(id_gen.named_id("body"), "repro.py::foo#1::body");
id_gen.pop_scope();
let third = id_gen.push_named_scope("foo");
assert_eq!(third, "foo#2");
}
#[test]
fn named_id_and_push_share_disambiguation() {
let mut id_gen = IdGenerator::new("test.rs");
assert_eq!(id_gen.named_id("Foo"), "test.rs::Foo");
let leaf = id_gen.push_named_scope("Foo");
assert_eq!(leaf, "Foo#1");
id_gen.pop_scope();
assert_eq!(id_gen.named_id("Foo"), "test.rs::Foo#2");
}
#[test]
fn anonymous_and_named_interleave_cleanly() {
let mut id_gen = IdGenerator::new("f.rs");
let a = id_gen.anonymous_id();
let x1 = id_gen.named_id("x");
let b = id_gen.anonymous_id();
let x2 = id_gen.named_id("x");
assert_eq!(a, "f.rs::$0");
assert_eq!(x1, "f.rs::x");
assert_eq!(b, "f.rs::$1");
assert_eq!(x2, "f.rs::x#1");
}
#[test]
fn field_ids_disambiguate_under_repeat() {
let mut id_gen = IdGenerator::new("f.qvr");
let base = id_gen.named_id("call");
let a0 = id_gen.field_id(&base, "args");
let a1 = id_gen.field_id(&base, "args");
let a2 = id_gen.field_id(&base, "args");
assert_eq!(a0, "f.qvr::call.args");
assert_eq!(a1, "f.qvr::call.args#1");
assert_eq!(a2, "f.qvr::call.args#2");
}
#[test]
fn sibling_scopes_have_fresh_seen_tables() {
let mut id_gen = IdGenerator::new("f.rs");
id_gen.push_named_scope("a");
let inner_a = id_gen.named_id("foo");
id_gen.pop_scope();
id_gen.push_named_scope("b");
let inner_b = id_gen.named_id("foo");
id_gen.pop_scope();
assert_eq!(inner_a, "f.rs::a::foo");
assert_eq!(inner_b, "f.rs::b::foo");
}
#[test]
#[cfg_attr(
debug_assertions,
should_panic(expected = "walker push/pop is unbalanced")
)]
fn pop_at_root_debug_panics_release_noops() {
let mut id_gen = IdGenerator::new("f.rs");
id_gen.pop_scope();
assert_eq!(id_gen.depth(), 1);
}
}