1use std::collections::HashMap;
6use std::path::Path;
7
8use crate::ast::{Program, Span, Statement, WordDef};
9use crate::lint::{LintDiagnostic, Severity};
10
11use super::state::{
12 InconsistentResource, ResourceKind, StackState, StackValue, TrackedResource, WordResourceInfo,
13};
14
15pub struct ProgramResourceAnalyzer {
21 word_info: HashMap<String, WordResourceInfo>,
23 file: std::path::PathBuf,
25 diagnostics: Vec<LintDiagnostic>,
27}
28
29impl ProgramResourceAnalyzer {
30 pub fn new(file: &Path) -> Self {
31 ProgramResourceAnalyzer {
32 word_info: HashMap::new(),
33 file: file.to_path_buf(),
34 diagnostics: Vec::new(),
35 }
36 }
37
38 pub fn analyze_program(&mut self, program: &Program) -> Vec<LintDiagnostic> {
40 self.diagnostics.clear();
41 self.word_info.clear();
42
43 for word in &program.words {
45 let info = self.collect_word_info(word);
46 self.word_info.insert(word.name.clone(), info);
47 }
48
49 for word in &program.words {
51 self.analyze_word_with_context(word);
52 }
53
54 std::mem::take(&mut self.diagnostics)
55 }
56
57 fn collect_word_info(&self, word: &WordDef) -> WordResourceInfo {
59 let mut state = StackState::new();
60
61 self.simulate_statements(&word.body, &mut state);
63
64 let returns: Vec<ResourceKind> = state
66 .remaining_resources()
67 .into_iter()
68 .map(|r| r.kind)
69 .collect();
70
71 WordResourceInfo { returns }
72 }
73
74 fn simulate_statements(&self, statements: &[Statement], state: &mut StackState) {
76 for stmt in statements {
77 self.simulate_statement(stmt, state);
78 }
79 }
80
81 fn simulate_statement(&self, stmt: &Statement, state: &mut StackState) {
83 match stmt {
84 Statement::IntLiteral(_)
85 | Statement::FloatLiteral(_)
86 | Statement::BoolLiteral(_)
87 | Statement::StringLiteral(_)
88 | Statement::Symbol(_) => {
89 state.push_unknown();
90 }
91
92 Statement::WordCall { name, span } => {
93 self.simulate_word_call(name, span.as_ref(), state);
94 }
95
96 Statement::Quotation { .. } => {
97 state.push_unknown();
98 }
99
100 Statement::If {
101 then_branch,
102 else_branch,
103 span: _,
104 } => {
105 state.pop(); let mut then_state = state.clone();
107 let mut else_state = state.clone();
108 self.simulate_statements(then_branch, &mut then_state);
109 if let Some(else_stmts) = else_branch {
110 self.simulate_statements(else_stmts, &mut else_state);
111 }
112 *state = then_state.join(&else_state);
113 }
114
115 Statement::Match { arms, span: _ } => {
116 state.pop();
117 let mut arm_states: Vec<StackState> = Vec::new();
118 for arm in arms {
119 let mut arm_state = state.clone();
120 self.simulate_statements(&arm.body, &mut arm_state);
121 arm_states.push(arm_state);
122 }
123 if let Some(joined) = arm_states.into_iter().reduce(|acc, s| acc.join(&s)) {
124 *state = joined;
125 }
126 }
127 }
128 }
129
130 fn simulate_word_common<F>(
138 name: &str,
139 span: Option<&Span>,
140 state: &mut StackState,
141 word_info: &HashMap<String, WordResourceInfo>,
142 mut on_resource_dropped: F,
143 ) -> bool
144 where
145 F: FnMut(&TrackedResource),
146 {
147 let line = span.map(|s| s.line).unwrap_or(0);
148
149 match name {
150 "strand.weave" => {
152 state.pop();
153 state.push_resource(ResourceKind::WeaveHandle, line, name);
154 }
155 "chan.make" => {
156 state.push_resource(ResourceKind::Channel, line, name);
157 }
158
159 "strand.weave-cancel" => {
161 if let Some(StackValue::Resource(r)) = state.pop()
162 && r.kind == ResourceKind::WeaveHandle
163 {
164 state.consume_resource(r);
165 }
166 }
167 "chan.close" => {
168 if let Some(StackValue::Resource(r)) = state.pop()
169 && r.kind == ResourceKind::Channel
170 {
171 state.consume_resource(r);
172 }
173 }
174
175 "drop" => {
177 let dropped = state.pop();
178 if let Some(StackValue::Resource(r)) = dropped {
179 let already_consumed = state.consumed.iter().any(|c| c.id == r.id);
181 if !already_consumed {
182 on_resource_dropped(&r);
183 }
184 }
185 }
186 "dup" => {
187 if let Some(top) = state.peek().cloned() {
190 state.stack.push(top);
191 }
192 }
193 "swap" => {
194 let a = state.pop();
195 let b = state.pop();
196 if let Some(av) = a {
197 state.stack.push(av);
198 }
199 if let Some(bv) = b {
200 state.stack.push(bv);
201 }
202 }
203 "over" => {
204 if state.depth() >= 2 {
206 let second = state.stack[state.depth() - 2].clone();
207 state.stack.push(second);
208 }
209 }
210 "rot" => {
211 let c = state.pop();
213 let b = state.pop();
214 let a = state.pop();
215 if let Some(bv) = b {
216 state.stack.push(bv);
217 }
218 if let Some(cv) = c {
219 state.stack.push(cv);
220 }
221 if let Some(av) = a {
222 state.stack.push(av);
223 }
224 }
225 "nip" => {
226 let b = state.pop();
228 let a = state.pop();
229 if let Some(StackValue::Resource(r)) = a {
230 let already_consumed = state.consumed.iter().any(|c| c.id == r.id);
231 if !already_consumed {
232 on_resource_dropped(&r);
233 }
234 }
235 if let Some(bv) = b {
236 state.stack.push(bv);
237 }
238 }
239 ">aux" => {
240 if let Some(val) = state.pop() {
242 state.aux_stack.push(val);
243 }
244 }
245 "aux>" => {
246 if let Some(val) = state.aux_stack.pop() {
248 state.stack.push(val);
249 }
250 }
251 "tuck" => {
252 let b = state.pop();
254 let a = state.pop();
255 if let Some(bv) = b.clone() {
256 state.stack.push(bv);
257 }
258 if let Some(av) = a {
259 state.stack.push(av);
260 }
261 if let Some(bv) = b {
262 state.stack.push(bv);
263 }
264 }
265
266 "strand.spawn" => {
268 state.pop();
269 let resources: Vec<TrackedResource> = state
270 .stack
271 .iter()
272 .filter_map(|v| match v {
273 StackValue::Resource(r) => Some(r.clone()),
274 StackValue::Unknown => None,
275 })
276 .collect();
277 for r in resources {
278 state.consume_resource(r);
279 }
280 state.push_unknown();
281 }
282
283 "map.set" => {
285 let value = state.pop();
287 state.pop(); state.pop(); if let Some(StackValue::Resource(r)) = value {
291 state.consume_resource(r);
292 }
293 state.push_unknown(); }
295
296 "list.push" | "list.prepend" => {
298 let value = state.pop();
300 state.pop(); if let Some(StackValue::Resource(r)) = value {
302 state.consume_resource(r);
303 }
304 state.push_unknown(); }
306
307 _ => {
309 if let Some(info) = word_info.get(name) {
310 for kind in &info.returns {
312 state.push_resource(*kind, line, name);
313 }
314 return true;
315 }
316 return false;
318 }
319 }
320 true
321 }
322
323 fn simulate_word_call(&self, name: &str, span: Option<&Span>, state: &mut StackState) {
325 Self::simulate_word_common(name, span, state, &self.word_info, |_| {});
327 }
328
329 fn analyze_word_with_context(&mut self, word: &WordDef) {
331 let mut state = StackState::new();
332
333 self.analyze_statements_with_context(&word.body, &mut state, word);
334
335 }
337
338 fn analyze_statements_with_context(
340 &mut self,
341 statements: &[Statement],
342 state: &mut StackState,
343 word: &WordDef,
344 ) {
345 for stmt in statements {
346 self.analyze_statement_with_context(stmt, state, word);
347 }
348 }
349
350 fn analyze_statement_with_context(
352 &mut self,
353 stmt: &Statement,
354 state: &mut StackState,
355 word: &WordDef,
356 ) {
357 match stmt {
358 Statement::IntLiteral(_)
359 | Statement::FloatLiteral(_)
360 | Statement::BoolLiteral(_)
361 | Statement::StringLiteral(_)
362 | Statement::Symbol(_) => {
363 state.push_unknown();
364 }
365
366 Statement::WordCall { name, span } => {
367 self.analyze_word_call_with_context(name, span.as_ref(), state, word);
368 }
369
370 Statement::Quotation { .. } => {
371 state.push_unknown();
372 }
373
374 Statement::If {
375 then_branch,
376 else_branch,
377 span: _,
378 } => {
379 state.pop();
380 let mut then_state = state.clone();
381 let mut else_state = state.clone();
382
383 self.analyze_statements_with_context(then_branch, &mut then_state, word);
384 if let Some(else_stmts) = else_branch {
385 self.analyze_statements_with_context(else_stmts, &mut else_state, word);
386 }
387
388 let merge_result = then_state.merge(&else_state);
390 for inconsistent in merge_result.inconsistent {
391 self.emit_branch_inconsistency_warning(&inconsistent, word);
392 }
393
394 *state = then_state.join(&else_state);
395 }
396
397 Statement::Match { arms, span: _ } => {
398 state.pop();
399 let mut arm_states: Vec<StackState> = Vec::new();
400
401 for arm in arms {
402 let mut arm_state = state.clone();
403 self.analyze_statements_with_context(&arm.body, &mut arm_state, word);
404 arm_states.push(arm_state);
405 }
406
407 if arm_states.len() >= 2 {
409 let first = &arm_states[0];
410 for other in &arm_states[1..] {
411 let merge_result = first.merge(other);
412 for inconsistent in merge_result.inconsistent {
413 self.emit_branch_inconsistency_warning(&inconsistent, word);
414 }
415 }
416 }
417
418 if let Some(joined) = arm_states.into_iter().reduce(|acc, s| acc.join(&s)) {
419 *state = joined;
420 }
421 }
422 }
423 }
424
425 fn analyze_word_call_with_context(
427 &mut self,
428 name: &str,
429 span: Option<&Span>,
430 state: &mut StackState,
431 word: &WordDef,
432 ) {
433 let mut dropped_resources: Vec<TrackedResource> = Vec::new();
435
436 let handled = Self::simulate_word_common(name, span, state, &self.word_info, |r| {
438 dropped_resources.push(r.clone())
439 });
440
441 for r in dropped_resources {
443 self.emit_drop_warning(&r, span, word);
444 }
445
446 if handled {
447 return;
448 }
449
450 match name {
452 "strand.resume" => {
454 let value = state.pop();
455 let handle = state.pop();
456 if let Some(h) = handle {
457 state.stack.push(h);
458 } else {
459 state.push_unknown();
460 }
461 if let Some(v) = value {
462 state.stack.push(v);
463 } else {
464 state.push_unknown();
465 }
466 state.push_unknown();
467 }
468
469 "2dup" => {
470 if state.depth() >= 2 {
471 let b = state.stack[state.depth() - 1].clone();
472 let a = state.stack[state.depth() - 2].clone();
473 state.stack.push(a);
474 state.stack.push(b);
475 } else {
476 state.push_unknown();
477 state.push_unknown();
478 }
479 }
480
481 "3drop" => {
482 for _ in 0..3 {
483 if let Some(StackValue::Resource(r)) = state.pop() {
484 let already_consumed = state.consumed.iter().any(|c| c.id == r.id);
485 if !already_consumed {
486 self.emit_drop_warning(&r, span, word);
487 }
488 }
489 }
490 }
491
492 "pick" | "roll" => {
493 state.pop();
494 state.push_unknown();
495 }
496
497 "chan.send" | "chan.receive" => {
498 state.pop();
499 state.pop();
500 state.push_unknown();
501 state.push_unknown();
502 }
503
504 _ => {}
506 }
507 }
508
509 fn emit_drop_warning(
510 &mut self,
511 resource: &TrackedResource,
512 span: Option<&Span>,
513 word: &WordDef,
514 ) {
515 let line = span
516 .map(|s| s.line)
517 .unwrap_or_else(|| word.source.as_ref().map(|s| s.start_line).unwrap_or(0));
518 let column = span.map(|s| s.column);
519
520 self.diagnostics.push(LintDiagnostic {
521 id: format!("resource-leak-{}", resource.kind.name().to_lowercase()),
522 message: format!(
523 "{} from `{}` (line {}) dropped without cleanup - {}",
524 resource.kind.name(),
525 resource.created_by,
526 resource.created_line + 1,
527 resource.kind.cleanup_suggestion()
528 ),
529 severity: Severity::Warning,
530 replacement: String::new(),
531 file: self.file.clone(),
532 line,
533 end_line: None,
534 start_column: column,
535 end_column: column.map(|c| c + 4),
536 word_name: word.name.clone(),
537 start_index: 0,
538 end_index: 0,
539 });
540 }
541
542 fn emit_branch_inconsistency_warning(
543 &mut self,
544 inconsistent: &InconsistentResource,
545 word: &WordDef,
546 ) {
547 let line = word.source.as_ref().map(|s| s.start_line).unwrap_or(0);
548 let branch = if inconsistent.consumed_in_else {
549 "else"
550 } else {
551 "then"
552 };
553
554 self.diagnostics.push(LintDiagnostic {
555 id: "resource-branch-inconsistent".to_string(),
556 message: format!(
557 "{} from `{}` (line {}) is consumed in {} branch but not the other - all branches must handle resources consistently",
558 inconsistent.resource.kind.name(),
559 inconsistent.resource.created_by,
560 inconsistent.resource.created_line + 1,
561 branch
562 ),
563 severity: Severity::Warning,
564 replacement: String::new(),
565 file: self.file.clone(),
566 line,
567 end_line: None,
568 start_column: None,
569 end_column: None,
570 word_name: word.name.clone(),
571 start_index: 0,
572 end_index: 0,
573 });
574 }
575}