1use std::collections::{HashMap, HashSet};
2use std::path::PathBuf;
3
4use bynk_syntax::ast::ProviderDecl;
5use bynk_syntax::error::CompileError;
6use bynk_syntax::span::Span;
7
8pub fn detect_consumes_cycles(
15 consumes: &HashMap<String, Vec<String>>,
16 sites: &HashMap<(String, String), (PathBuf, Span)>,
17 errors: &mut Vec<(Option<PathBuf>, CompileError)>,
18) {
19 let mut visited: HashSet<String> = HashSet::new();
22 let mut reported: HashSet<Vec<String>> = HashSet::new();
23 for start in consumes.keys() {
24 if visited.contains(start) {
25 continue;
26 }
27 let mut stack: Vec<String> = Vec::new();
28 let mut on_stack: HashSet<String> = HashSet::new();
29 dfs_consumes(
30 start,
31 consumes,
32 sites,
33 &mut visited,
34 &mut stack,
35 &mut on_stack,
36 &mut reported,
37 errors,
38 );
39 }
40}
41
42#[allow(clippy::too_many_arguments)]
43fn dfs_consumes(
44 node: &str,
45 consumes: &HashMap<String, Vec<String>>,
46 sites: &HashMap<(String, String), (PathBuf, Span)>,
47 visited: &mut HashSet<String>,
48 stack: &mut Vec<String>,
49 on_stack: &mut HashSet<String>,
50 reported: &mut HashSet<Vec<String>>,
51 errors: &mut Vec<(Option<PathBuf>, CompileError)>,
52) {
53 if on_stack.contains(node) {
54 let start = stack.iter().position(|s| s == node).unwrap_or(0);
56 let mut cycle: Vec<String> = stack[start..].to_vec();
57 cycle.push(node.to_string());
58 let canon = canonicalise_cycle(&cycle);
60 if reported.insert(canon.clone()) {
61 let edge = (node.to_string(), cycle[1].clone());
67 let (file, span) = match sites.get(&edge) {
68 Some((path, span)) => (Some(path.clone()), *span),
69 None => (None, Span::default()),
70 };
71 errors.push((file, CompileError::new(
72 "bynk.context.consumes_cycle",
73 span,
74 format!(
75 "`consumes` cycle detected: {}",
76 cycle.join(" → ")
77 ),
78 )
79 .with_note(
80 "units must form an acyclic `consumes` graph; remove one of the `consumes` clauses or restructure",
81 )));
82 }
83 return;
84 }
85 if visited.contains(node) {
86 return;
87 }
88 visited.insert(node.to_string());
89 on_stack.insert(node.to_string());
90 stack.push(node.to_string());
91 if let Some(targets) = consumes.get(node) {
92 for t in targets {
93 dfs_consumes(
94 t, consumes, sites, visited, stack, on_stack, reported, errors,
95 );
96 }
97 }
98 stack.pop();
99 on_stack.remove(node);
100}
101
102fn canonicalise_cycle(cycle: &[String]) -> Vec<String> {
103 if cycle.is_empty() {
104 return Vec::new();
105 }
106 let body = &cycle[..cycle.len() - 1];
108 if body.is_empty() {
109 return Vec::new();
110 }
111 let mut min_idx = 0;
112 for (i, s) in body.iter().enumerate() {
113 if s < &body[min_idx] {
114 min_idx = i;
115 }
116 }
117 let mut rotated: Vec<String> = body[min_idx..].to_vec();
118 rotated.extend(body[..min_idx].iter().cloned());
119 rotated
120}
121
122pub fn detect_provider_dependency_cycles(
128 providers: &HashMap<String, ProviderDecl>,
129 errors: &mut Vec<CompileError>,
130) {
131 fn visit(
132 node: &str,
133 providers: &HashMap<String, ProviderDecl>,
134 visited: &mut HashSet<String>,
135 stack: &mut Vec<String>,
136 in_stack: &mut HashSet<String>,
137 cyclic: &mut HashSet<String>,
138 ) {
139 if visited.contains(node) {
140 return;
141 }
142 in_stack.insert(node.to_string());
143 stack.push(node.to_string());
144 if let Some(p) = providers.get(node) {
145 for dep in &p.given {
146 if dep.is_cross_context() {
149 continue;
150 }
151 if !providers.contains_key(dep.key()) {
153 continue;
154 }
155 if in_stack.contains(dep.key()) {
156 let start = stack.iter().position(|n| n == dep.key()).unwrap_or(0);
159 for n in &stack[start..] {
160 cyclic.insert(n.clone());
161 }
162 } else if !visited.contains(dep.key()) {
163 visit(dep.key(), providers, visited, stack, in_stack, cyclic);
164 }
165 }
166 }
167 stack.pop();
168 in_stack.remove(node);
169 visited.insert(node.to_string());
170 }
171
172 let mut visited: HashSet<String> = HashSet::new();
173 let mut cyclic: HashSet<String> = HashSet::new();
174 let mut keys: Vec<&String> = providers.keys().collect();
175 keys.sort();
176 for k in keys {
177 let mut stack: Vec<String> = Vec::new();
178 let mut in_stack: HashSet<String> = HashSet::new();
179 visit(
180 k,
181 providers,
182 &mut visited,
183 &mut stack,
184 &mut in_stack,
185 &mut cyclic,
186 );
187 }
188
189 let mut cyclic_sorted: Vec<&String> = cyclic.iter().collect();
190 cyclic_sorted.sort();
191 for cap in cyclic_sorted {
192 if let Some(p) = providers.get(cap) {
193 errors.push(
194 CompileError::new(
195 "bynk.provider.dependency_cycle",
196 p.span,
197 format!(
198 "provider `{}` for capability `{}` is part of a capability dependency cycle",
199 p.provider_name.name, cap,
200 ),
201 )
202 .with_note(
203 "a capability cannot depend on itself, directly or transitively, through \
204 provider `given`",
205 ),
206 );
207 }
208 }
209}
210
211#[cfg(test)]
212mod tests {
213 use super::*;
214 use std::collections::HashMap;
215 use std::path::Path;
216
217 fn strs(xs: &[&str]) -> Vec<String> {
218 xs.iter().map(|x| (*x).to_string()).collect()
219 }
220
221 fn graph(edges: &[(&str, &[&str])]) -> HashMap<String, Vec<String>> {
222 edges
223 .iter()
224 .map(|(k, vs)| ((*k).to_string(), strs(vs)))
225 .collect()
226 }
227
228 #[test]
230 fn canonicalise_cycle_is_stable_across_rotations() {
231 assert_eq!(
234 canonicalise_cycle(&strs(&["a", "b", "c", "a"])),
235 strs(&["a", "b", "c"])
236 );
237 assert_eq!(
238 canonicalise_cycle(&strs(&["b", "c", "a", "b"])),
239 strs(&["a", "b", "c"])
240 );
241 assert_eq!(
242 canonicalise_cycle(&strs(&["c", "a", "b", "c"])),
243 strs(&["a", "b", "c"])
244 );
245 }
246
247 #[test]
248 fn canonicalise_cycle_edge_cases() {
249 assert_eq!(canonicalise_cycle(&[]), Vec::<String>::new());
250 assert_eq!(canonicalise_cycle(&strs(&["a", "a"])), strs(&["a"])); assert_eq!(
252 canonicalise_cycle(&strs(&["b", "a", "b"])),
253 strs(&["a", "b"])
254 );
255 }
256
257 #[test]
259 fn detect_consumes_cycles_silent_on_acyclic() {
260 let g = graph(&[("a", &["b"]), ("b", &["c"]), ("c", &[])]);
261 let mut errors = Vec::new();
262 detect_consumes_cycles(&g, &no_sites(), &mut errors);
263 assert!(errors.is_empty());
264 }
265
266 #[test]
267 fn detect_consumes_cycles_reports_each_cycle_once() {
268 let mut e2 = Vec::new();
269 detect_consumes_cycles(
270 &graph(&[("a", &["b"]), ("b", &["a"])]),
271 &no_sites(),
272 &mut e2,
273 );
274 assert_eq!(e2.len(), 1);
275
276 let mut e3 = Vec::new();
277 detect_consumes_cycles(
278 &graph(&[("a", &["b"]), ("b", &["c"]), ("c", &["a"])]),
279 &no_sites(),
280 &mut e3,
281 );
282 assert_eq!(e3.len(), 1);
283
284 let mut eself = Vec::new();
285 detect_consumes_cycles(&graph(&[("a", &["a"])]), &no_sites(), &mut eself);
286 assert_eq!(eself.len(), 1);
287 }
288
289 #[test]
290 fn detect_consumes_cycles_reports_disjoint_cycles_separately() {
291 let mut errors = Vec::new();
292 detect_consumes_cycles(
293 &graph(&[("a", &["b"]), ("b", &["a"]), ("c", &["d"]), ("d", &["c"])]),
294 &no_sites(),
295 &mut errors,
296 );
297 assert_eq!(errors.len(), 2);
298 }
299
300 #[test]
304 fn detect_consumes_cycles_attributes_to_closing_edges_site() {
305 let sites: HashMap<(String, String), (PathBuf, Span)> = [(
308 ("a".to_string(), "a".to_string()),
309 (PathBuf::from("a.bynk"), Span::new(3, 8)),
310 )]
311 .into_iter()
312 .collect();
313 let mut errors = Vec::new();
314 detect_consumes_cycles(&graph(&[("a", &["a"])]), &sites, &mut errors);
315 assert_eq!(errors.len(), 1);
316 let (file, err) = &errors[0];
317 assert_eq!(file.as_deref(), Some(Path::new("a.bynk")));
318 assert_eq!(err.span, Span::new(3, 8));
319
320 let mut bare = Vec::new();
322 detect_consumes_cycles(&graph(&[("x", &["x"])]), &sites, &mut bare);
323 assert_eq!(bare.len(), 1);
324 assert_eq!(bare[0].0, None);
325 assert_eq!(bare[0].1.span, Span::default());
326 }
327
328 fn no_sites() -> HashMap<(String, String), (PathBuf, Span)> {
329 HashMap::new()
330 }
331}