Skip to main content

panproto_parse/
id_scheme.rs

1//! Scope-aware vertex ID generation for full-AST schemas.
2//!
3//! Generates stable, human-readable vertex IDs that encode the scope path
4//! from file to the specific AST node. IDs follow the pattern:
5//!
6//! ```text
7//! src/main.rs::parse_input::$3::$0.left
8//! ```
9//!
10//! where `src/main.rs` is the file, `parse_input` is the function,
11//! `$3` is the 4th statement in the function body, and `$0.left` is
12//! the left operand of the first expression.
13//!
14//! Statement indices are positional within blocks (shift on insertion).
15//! This is correct: the merge algorithm handles reindexing via ThOrder
16//! pushouts (`has_order: true`).
17
18/// Generates scope-aware vertex IDs for AST nodes.
19#[derive(Debug)]
20pub struct IdGenerator {
21    /// The scope stack: each entry is a scope name (file, function, block, etc.).
22    scope_stack: Vec<String>,
23    /// Counter for unnamed children within the current scope.
24    child_counter: Vec<u32>,
25}
26
27impl IdGenerator {
28    /// Create a new generator rooted at the given file path.
29    #[must_use]
30    pub fn new(file_path: &str) -> Self {
31        Self {
32            scope_stack: vec![file_path.to_owned()],
33            child_counter: vec![0],
34        }
35    }
36
37    /// Push a named scope (function, class, method, module, etc.).
38    ///
39    /// Named scopes appear in the ID as their name:
40    /// `file.rs::function_name::...`
41    pub fn push_named_scope(&mut self, name: &str) {
42        self.scope_stack.push(name.to_owned());
43        self.child_counter.push(0);
44    }
45
46    /// Push an anonymous scope (block, statement body, etc.).
47    ///
48    /// Anonymous scopes appear in the ID with a positional index:
49    /// `file.rs::function_name::$3::...`
50    pub fn push_anonymous_scope(&mut self) -> u32 {
51        let idx = self.child_counter.last().copied().unwrap_or(0);
52        if let Some(counter) = self.child_counter.last_mut() {
53            *counter += 1;
54        }
55
56        self.scope_stack.push(format!("${idx}"));
57        self.child_counter.push(0);
58        idx
59    }
60
61    /// Pop the current scope, returning to the parent.
62    pub fn pop_scope(&mut self) {
63        if self.scope_stack.len() > 1 {
64            self.scope_stack.pop();
65            self.child_counter.pop();
66        }
67    }
68
69    /// Generate an ID for a named node at the current scope level.
70    ///
71    /// Returns the full scope-qualified ID (e.g. `"src/main.rs::parse_input"`).
72    #[must_use]
73    pub fn named_id(&self, name: &str) -> String {
74        if self.scope_stack.len() == 1 {
75            format!("{}::{name}", self.scope_stack[0])
76        } else {
77            format!("{}::{name}", self.current_prefix())
78        }
79    }
80
81    /// Generate an ID for an anonymous (positional) node at the current scope level.
82    ///
83    /// The index is auto-incremented within the current scope.
84    /// Returns the full scope-qualified ID (e.g. `"src/main.rs::parse_input::$3"`).
85    pub fn anonymous_id(&mut self) -> String {
86        let idx = self.child_counter.last().copied().unwrap_or(0);
87        if let Some(counter) = self.child_counter.last_mut() {
88            *counter += 1;
89        }
90
91        format!("{}::${idx}", self.current_prefix())
92    }
93
94    /// Generate an ID with a field path suffix for expression sub-nodes.
95    ///
96    /// Used for expression tree paths like `$3::$0.left` where `.left`
97    /// is the field name within the parent expression.
98    #[must_use]
99    pub fn field_id(&self, base_id: &str, field_name: &str) -> String {
100        format!("{base_id}.{field_name}")
101    }
102
103    /// Get the current scope prefix (all scope components joined by `::`).
104    #[must_use]
105    pub fn current_prefix(&self) -> String {
106        self.scope_stack.join("::")
107    }
108
109    /// Get the current scope depth.
110    #[must_use]
111    pub fn depth(&self) -> usize {
112        self.scope_stack.len()
113    }
114
115    /// Reset the child counter for the current scope.
116    ///
117    /// Useful when entering a new block within the same scope level.
118    pub fn reset_counter(&mut self) {
119        if let Some(counter) = self.child_counter.last_mut() {
120            *counter = 0;
121        }
122    }
123}
124
125#[cfg(test)]
126mod tests {
127    use super::*;
128
129    #[test]
130    fn basic_named_ids() {
131        let id_gen = IdGenerator::new("src/main.rs");
132        assert_eq!(id_gen.named_id("User"), "src/main.rs::User");
133    }
134
135    #[test]
136    fn nested_scopes() {
137        let mut id_gen = IdGenerator::new("src/lib.rs");
138        id_gen.push_named_scope("Parser");
139        id_gen.push_named_scope("parse");
140        assert_eq!(
141            id_gen.named_id("config"),
142            "src/lib.rs::Parser::parse::config"
143        );
144        id_gen.pop_scope();
145        assert_eq!(id_gen.named_id("new"), "src/lib.rs::Parser::new");
146    }
147
148    #[test]
149    fn anonymous_ids_increment() {
150        let mut id_gen = IdGenerator::new("test.ts");
151        id_gen.push_named_scope("main");
152
153        let id0 = id_gen.anonymous_id();
154        let id1 = id_gen.anonymous_id();
155        let id2 = id_gen.anonymous_id();
156
157        assert_eq!(id0, "test.ts::main::$0");
158        assert_eq!(id1, "test.ts::main::$1");
159        assert_eq!(id2, "test.ts::main::$2");
160    }
161
162    #[test]
163    fn anonymous_scopes() {
164        let mut id_gen = IdGenerator::new("test.py");
165        id_gen.push_named_scope("process");
166        let _stmt_idx = id_gen.push_anonymous_scope(); // enters $0 scope
167        let inner = id_gen.anonymous_id();
168        assert_eq!(inner, "test.py::process::$0::$0");
169        id_gen.pop_scope();
170        let _stmt_idx2 = id_gen.push_anonymous_scope(); // enters $1 scope
171        let inner2 = id_gen.anonymous_id();
172        assert_eq!(inner2, "test.py::process::$1::$0");
173    }
174
175    #[test]
176    fn field_ids() {
177        let id_gen = IdGenerator::new("test.rs");
178        let base = id_gen.named_id("expr");
179        let left = id_gen.field_id(&base, "left");
180        let right = id_gen.field_id(&base, "right");
181        assert_eq!(left, "test.rs::expr.left");
182        assert_eq!(right, "test.rs::expr.right");
183    }
184
185    #[test]
186    fn depth_tracking() {
187        let mut id_gen = IdGenerator::new("f.ts");
188        assert_eq!(id_gen.depth(), 1);
189        id_gen.push_named_scope("fn");
190        assert_eq!(id_gen.depth(), 2);
191        id_gen.push_anonymous_scope();
192        assert_eq!(id_gen.depth(), 3);
193        id_gen.pop_scope();
194        assert_eq!(id_gen.depth(), 2);
195    }
196}