1use std::path::{Path, PathBuf};
26
27use crate::ast::{Program, Span, Statement, WordDef};
28use crate::call_graph::CallGraph;
29use crate::lint::{LintDiagnostic, Severity};
30
31const LINT_ID: &str = "unreachable-chan-yield";
32
33pub struct ChanYieldAnalyzer {
34 file: PathBuf,
35}
36
37impl ChanYieldAnalyzer {
38 pub fn new(file: &Path) -> Self {
39 ChanYieldAnalyzer {
40 file: file.to_path_buf(),
41 }
42 }
43
44 pub fn analyze_program(
45 &self,
46 program: &Program,
47 call_graph: &CallGraph,
48 ) -> Vec<LintDiagnostic> {
49 let coop = cooperative_set(program, call_graph);
50
51 let mut diagnostics = Vec::new();
52 for word in &program.words {
53 if coop.contains(&word.name) {
54 continue;
55 }
56 let mut sites = Vec::new();
57 collect_chan_yield_sites(&word.body, &mut sites);
58 for span in sites {
59 diagnostics.push(self.diagnostic(word, span));
60 }
61 }
62 diagnostics
63 }
64
65 fn diagnostic(&self, word: &WordDef, span: Option<Span>) -> LintDiagnostic {
66 let line = span.as_ref().map(|s| s.line).unwrap_or(0);
67 let column = span.as_ref().map(|s| s.column);
68 LintDiagnostic {
69 id: LINT_ID.to_string(),
70 message: format!(
71 "`chan.yield` in `{}` has no peer to yield to — this code path is not \
72 reachable from any `strand.spawn` or `strand.weave`. Either remove the \
73 call, or run this word under `[ ... ] strand.spawn`.",
74 word.name
75 ),
76 severity: Severity::Error,
77 replacement: String::new(),
78 file: self.file.clone(),
79 line,
80 end_line: None,
81 start_column: column,
82 end_column: None,
83 word_name: word.name.clone(),
84 start_index: 0,
85 end_index: 0,
86 }
87 }
88}
89
90fn cooperative_set(program: &Program, call_graph: &CallGraph) -> std::collections::HashSet<String> {
92 use std::collections::HashSet;
93
94 let user_words: HashSet<&str> = program.words.iter().map(|w| w.name.as_str()).collect();
95
96 let mut seeds: HashSet<String> = HashSet::new();
102 for word in &program.words {
103 let mut spawner = false;
104 scan_for_seeds(&word.body, &user_words, &mut seeds, &mut spawner);
105 if spawner {
106 seeds.insert(word.name.clone());
107 }
108 }
109
110 let mut coop: HashSet<String> = HashSet::new();
113 let mut frontier: Vec<String> = seeds.into_iter().collect();
114 while let Some(w) = frontier.pop() {
115 if !coop.insert(w.clone()) {
116 continue;
117 }
118 if let Some(callees) = call_graph.callees(&w) {
119 for callee in callees {
120 if !coop.contains(callee) {
121 frontier.push(callee.clone());
122 }
123 }
124 }
125 }
126 coop
127}
128
129fn scan_for_seeds(
138 statements: &[Statement],
139 user_words: &std::collections::HashSet<&str>,
140 seeds: &mut std::collections::HashSet<String>,
141 spawner: &mut bool,
142) {
143 for (i, stmt) in statements.iter().enumerate() {
144 match stmt {
145 Statement::WordCall { name, .. } if is_spawn_or_weave(name) => {
146 *spawner = true;
147 if let Some(Statement::Quotation { body, .. }) = statements.get(i.wrapping_sub(1))
148 && i > 0
149 {
150 collect_user_word_calls(body, user_words, seeds);
151 }
152 }
153 Statement::If {
154 then_branch,
155 else_branch,
156 ..
157 } => {
158 scan_for_seeds(then_branch, user_words, seeds, spawner);
159 if let Some(else_stmts) = else_branch {
160 scan_for_seeds(else_stmts, user_words, seeds, spawner);
161 }
162 }
163 Statement::Quotation { body, .. } => {
164 scan_for_seeds(body, user_words, seeds, spawner);
165 }
166 Statement::Match { arms, .. } => {
167 for arm in arms {
168 scan_for_seeds(&arm.body, user_words, seeds, spawner);
169 }
170 }
171 _ => {}
172 }
173 }
174}
175
176fn collect_user_word_calls(
179 body: &[Statement],
180 user_words: &std::collections::HashSet<&str>,
181 out: &mut std::collections::HashSet<String>,
182) {
183 for stmt in body {
184 match stmt {
185 Statement::WordCall { name, .. } if user_words.contains(name.as_str()) => {
186 out.insert(name.clone());
187 }
188 Statement::If {
189 then_branch,
190 else_branch,
191 ..
192 } => {
193 collect_user_word_calls(then_branch, user_words, out);
194 if let Some(else_stmts) = else_branch {
195 collect_user_word_calls(else_stmts, user_words, out);
196 }
197 }
198 Statement::Quotation { body, .. } => {
199 collect_user_word_calls(body, user_words, out);
200 }
201 Statement::Match { arms, .. } => {
202 for arm in arms {
203 collect_user_word_calls(&arm.body, user_words, out);
204 }
205 }
206 _ => {}
207 }
208 }
209}
210
211fn collect_chan_yield_sites(body: &[Statement], out: &mut Vec<Option<Span>>) {
214 for stmt in body {
215 match stmt {
216 Statement::WordCall { name, span } if name == "chan.yield" => {
217 out.push(span.clone());
218 }
219 Statement::If {
220 then_branch,
221 else_branch,
222 ..
223 } => {
224 collect_chan_yield_sites(then_branch, out);
225 if let Some(else_stmts) = else_branch {
226 collect_chan_yield_sites(else_stmts, out);
227 }
228 }
229 Statement::Quotation { body, .. } => {
230 collect_chan_yield_sites(body, out);
231 }
232 Statement::Match { arms, .. } => {
233 for arm in arms {
234 collect_chan_yield_sites(&arm.body, out);
235 }
236 }
237 _ => {}
238 }
239 }
240}
241
242fn is_spawn_or_weave(name: &str) -> bool {
243 name == "strand.spawn" || name == "strand.weave"
244}
245
246#[cfg(test)]
247mod tests;