Skip to main content

seqc/
chan_yield_lint.rs

1//! `chan.yield` Reachability Lint
2//!
3//! Errors when `chan.yield` is called from a word with no cooperative
4//! peer — i.e. a word that is not reachable from any `strand.spawn` or
5//! `strand.weave`. Such a call is a no-op masquerading as concurrency
6//! machinery and almost always indicates a deleted/forgotten spawn.
7//!
8//! # Cooperative set
9//!
10//! A user word is *cooperative* iff at least one of the following holds:
11//!
12//! 1. Its body contains a `strand.spawn` or `strand.weave` call (the
13//!    "spawner-self" rule — yielding from the same word that just
14//!    spawned a peer is canonical).
15//! 2. It appears as a literal-quotation body passed directly to
16//!    `strand.spawn` / `strand.weave` (the seed roots).
17//! 3. It is transitively called by some cooperative word.
18//!
19//! Quotations passed to other combinators (`if`, `when`, `map`, `dip`,
20//! ...) inherit their enclosing word's classification; we do not try to
21//! track quotations stored in data and later invoked via `call`. The
22//! design doc accepts that conservatism as a false-positive risk that
23//! has not appeared in practice.
24
25use 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
90/// Compute the set of user-word names that are cooperative.
91fn 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    // Seeds: every word that either (a) contains a spawn/weave call, or
97    // (b) is the literal-quotation body passed directly to spawn/weave
98    // and resolves to a single user word call. We approximate (b) by
99    // collecting all user-word calls reachable from each spawn-quotation
100    // body — they all become seeds.
101    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    // Transitive closure: every user word reachable from a seed via
111    // user→user call edges joins the cooperative set.
112    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
129/// Walk a statement list and:
130/// - record `spawner = true` if any `strand.spawn` / `strand.weave` call
131///   is encountered in this lexical scope (recurses into quotations,
132///   if branches, and match arms — because seeing a spawn anywhere in
133///   the enclosing word still triggers the spawner-self rule);
134/// - for every `strand.spawn` / `strand.weave` whose immediately
135///   preceding statement is a literal `Quotation`, add every user-word
136///   call inside that quotation body to `seeds`.
137fn 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
176/// Collect every user-word name called inside `body`, recursing into
177/// quotations, if branches, and match arms.
178fn 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
211/// Record the span of every `chan.yield` call in `body`, recursing into
212/// quotations, if branches, and match arms.
213fn 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;