Skip to main content

haystack_core/xeto/
resolver.rs

1// Xeto name resolver -- resolves unqualified spec names to fully qualified names.
2
3use std::collections::{HashMap, HashSet};
4
5use super::XetoError;
6
7/// Resolves unqualified Xeto spec names to fully qualified `lib::Name` forms.
8///
9/// Resolution order:
10/// 1. Already qualified (contains `::`): return as-is
11/// 2. Current library's own specs
12/// 3. Declared dependencies
13/// 4. `"sys"` builtins
14/// 5. All known libraries (fallback)
15pub struct XetoResolver {
16    /// Library name -> set of spec names defined in that library.
17    lib_specs: HashMap<String, HashSet<String>>,
18    /// Library name -> list of dependency library names.
19    lib_depends: HashMap<String, Vec<String>>,
20}
21
22impl XetoResolver {
23    /// Create a new empty resolver.
24    pub fn new() -> Self {
25        Self {
26            lib_specs: HashMap::new(),
27            lib_depends: HashMap::new(),
28        }
29    }
30
31    /// Register a library with its spec names and dependency list.
32    pub fn add_lib(&mut self, lib_name: &str, spec_names: HashSet<String>, depends: Vec<String>) {
33        self.lib_specs.insert(lib_name.to_string(), spec_names);
34        self.lib_depends.insert(lib_name.to_string(), depends);
35    }
36
37    /// Resolve a spec name to a fully qualified name within the context of a library.
38    ///
39    /// Returns `None` if the name cannot be resolved.
40    pub fn resolve(&self, name: &str, context_lib: &str) -> Option<String> {
41        // 1. Already qualified
42        if name.contains("::") {
43            return Some(name.to_string());
44        }
45
46        // 2. Current library's own specs
47        if let Some(specs) = self.lib_specs.get(context_lib)
48            && specs.contains(name)
49        {
50            return Some(format!("{}::{}", context_lib, name));
51        }
52
53        // 3. Declared dependencies
54        if let Some(deps) = self.lib_depends.get(context_lib) {
55            for dep in deps {
56                if let Some(specs) = self.lib_specs.get(dep.as_str())
57                    && specs.contains(name)
58                {
59                    return Some(format!("{}::{}", dep, name));
60                }
61            }
62        }
63
64        // 4. sys builtins
65        if let Some(specs) = self.lib_specs.get("sys")
66            && specs.contains(name)
67        {
68            return Some(format!("sys::{}", name));
69        }
70
71        // 5. All known libraries (fallback)
72        for (lib_name, specs) in &self.lib_specs {
73            if lib_name == context_lib || lib_name == "sys" {
74                continue;
75            }
76            if specs.contains(name) {
77                return Some(format!("{}::{}", lib_name, name));
78            }
79        }
80
81        None
82    }
83
84    /// Compute a topological ordering of libraries based on dependencies.
85    ///
86    /// Returns an error if a circular dependency is detected.
87    pub fn dependency_order(&self) -> Result<Vec<String>, XetoError> {
88        let mut result = Vec::new();
89        let mut visited = HashSet::new();
90        let mut in_progress = HashSet::new();
91
92        // Process all known libraries
93        let all_libs: Vec<String> = self.lib_specs.keys().cloned().collect();
94        for lib in &all_libs {
95            if !visited.contains(lib.as_str()) {
96                self.topo_visit(lib, &mut visited, &mut in_progress, &mut result)?;
97            }
98        }
99
100        Ok(result)
101    }
102
103    /// Recursive DFS for topological sort with cycle detection.
104    fn topo_visit(
105        &self,
106        lib: &str,
107        visited: &mut HashSet<String>,
108        in_progress: &mut HashSet<String>,
109        result: &mut Vec<String>,
110    ) -> Result<(), XetoError> {
111        if in_progress.contains(lib) {
112            return Err(XetoError::Resolve(format!(
113                "circular dependency detected involving '{}'",
114                lib
115            )));
116        }
117        if visited.contains(lib) {
118            return Ok(());
119        }
120
121        in_progress.insert(lib.to_string());
122
123        // Visit dependencies first
124        if let Some(deps) = self.lib_depends.get(lib) {
125            for dep in deps {
126                self.topo_visit(dep, visited, in_progress, result)?;
127            }
128        }
129
130        in_progress.remove(lib);
131        visited.insert(lib.to_string());
132        result.push(lib.to_string());
133
134        Ok(())
135    }
136}
137
138impl Default for XetoResolver {
139    fn default() -> Self {
140        Self::new()
141    }
142}
143
144#[cfg(test)]
145mod tests {
146    use super::*;
147
148    fn make_resolver() -> XetoResolver {
149        let mut resolver = XetoResolver::new();
150
151        let mut sys_specs = HashSet::new();
152        sys_specs.insert("Str".to_string());
153        sys_specs.insert("Number".to_string());
154        sys_specs.insert("Ref".to_string());
155        sys_specs.insert("Bool".to_string());
156        resolver.add_lib("sys", sys_specs, vec![]);
157
158        let mut ph_specs = HashSet::new();
159        ph_specs.insert("Entity".to_string());
160        ph_specs.insert("Site".to_string());
161        resolver.add_lib("ph", ph_specs, vec!["sys".to_string()]);
162
163        let mut phiot_specs = HashSet::new();
164        phiot_specs.insert("Equip".to_string());
165        phiot_specs.insert("Point".to_string());
166        phiot_specs.insert("Ahu".to_string());
167        resolver.add_lib(
168            "phIoT",
169            phiot_specs,
170            vec!["ph".to_string(), "sys".to_string()],
171        );
172
173        resolver
174    }
175
176    #[test]
177    fn resolve_qualified_returns_as_is() {
178        let resolver = make_resolver();
179        assert_eq!(
180            resolver.resolve("ph::Entity", "phIoT"),
181            Some("ph::Entity".to_string())
182        );
183    }
184
185    #[test]
186    fn resolve_finds_in_current_lib() {
187        let resolver = make_resolver();
188        assert_eq!(
189            resolver.resolve("Ahu", "phIoT"),
190            Some("phIoT::Ahu".to_string())
191        );
192    }
193
194    #[test]
195    fn resolve_finds_in_dependencies() {
196        let resolver = make_resolver();
197        assert_eq!(
198            resolver.resolve("Entity", "phIoT"),
199            Some("ph::Entity".to_string())
200        );
201    }
202
203    #[test]
204    fn resolve_finds_in_sys() {
205        let resolver = make_resolver();
206        assert_eq!(
207            resolver.resolve("Str", "phIoT"),
208            Some("sys::Str".to_string())
209        );
210    }
211
212    #[test]
213    fn resolve_unknown_returns_none() {
214        let resolver = make_resolver();
215        assert_eq!(resolver.resolve("UnknownType", "phIoT"), None);
216    }
217
218    #[test]
219    fn resolve_fallback_to_all_libs() {
220        // ph is not in phIoT's deps directly but has Entity
221        // Since ph IS in deps, let's test with a lib that
222        // doesn't depend on ph but can still find it via fallback
223        let mut resolver = XetoResolver::new();
224
225        let mut ph_specs = HashSet::new();
226        ph_specs.insert("Entity".to_string());
227        resolver.add_lib("ph", ph_specs, vec![]);
228
229        let mut other_specs = HashSet::new();
230        other_specs.insert("Foo".to_string());
231        resolver.add_lib("other", other_specs, vec![]); // no deps
232
233        // "Entity" is not in other's deps, but should be found via fallback
234        let result = resolver.resolve("Entity", "other");
235        assert_eq!(result, Some("ph::Entity".to_string()));
236    }
237
238    #[test]
239    fn dependency_ordering() {
240        let resolver = make_resolver();
241        let order = resolver.dependency_order().unwrap();
242
243        // sys should come before ph, ph before phIoT
244        let sys_pos = order.iter().position(|s| s == "sys").unwrap();
245        let ph_pos = order.iter().position(|s| s == "ph").unwrap();
246        let phiot_pos = order.iter().position(|s| s == "phIoT").unwrap();
247
248        assert!(sys_pos < ph_pos);
249        assert!(ph_pos < phiot_pos);
250    }
251
252    #[test]
253    fn circular_dependency_detected() {
254        let mut resolver = XetoResolver::new();
255
256        let a_specs = HashSet::new();
257        resolver.add_lib("a", a_specs, vec!["b".to_string()]);
258
259        let b_specs = HashSet::new();
260        resolver.add_lib("b", b_specs, vec!["a".to_string()]);
261
262        let result = resolver.dependency_order();
263        assert!(result.is_err());
264        let err = result.unwrap_err();
265        assert!(err.to_string().contains("circular dependency"));
266    }
267
268    #[test]
269    fn dependency_order_single_lib() {
270        let mut resolver = XetoResolver::new();
271        let specs = HashSet::new();
272        resolver.add_lib("solo", specs, vec![]);
273
274        let order = resolver.dependency_order().unwrap();
275        assert_eq!(order, vec!["solo"]);
276    }
277
278    #[test]
279    fn resolve_sys_builtin_from_any_context() {
280        let resolver = make_resolver();
281        assert_eq!(
282            resolver.resolve("Number", "ph"),
283            Some("sys::Number".to_string())
284        );
285    }
286}