use crate::ast::{MatchArm, Program, Span, Statement, WordDef};
use crate::lint::{LintDiagnostic, Severity};
use std::collections::HashMap;
use std::path::Path;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub(crate) enum ResourceKind {
WeaveHandle,
Channel,
}
impl ResourceKind {
fn name(&self) -> &'static str {
match self {
ResourceKind::WeaveHandle => "WeaveHandle",
ResourceKind::Channel => "Channel",
}
}
fn cleanup_suggestion(&self) -> &'static str {
match self {
ResourceKind::WeaveHandle => "use `strand.weave-cancel` or resume to completion",
ResourceKind::Channel => "use `chan.close` when done",
}
}
}
#[derive(Debug, Clone)]
pub(crate) struct TrackedResource {
pub kind: ResourceKind,
pub id: usize,
pub created_line: usize,
pub created_by: String,
}
#[derive(Debug, Clone)]
pub(crate) enum StackValue {
Resource(TrackedResource),
Unknown,
}
#[derive(Debug, Clone)]
pub(crate) struct StackState {
stack: Vec<StackValue>,
consumed: Vec<TrackedResource>,
next_id: usize,
}
impl Default for StackState {
fn default() -> Self {
Self::new()
}
}
impl StackState {
pub fn new() -> Self {
StackState {
stack: Vec::new(),
consumed: Vec::new(),
next_id: 0,
}
}
pub fn push_unknown(&mut self) {
self.stack.push(StackValue::Unknown);
}
pub fn push_resource(&mut self, kind: ResourceKind, line: usize, word: &str) {
let resource = TrackedResource {
kind,
id: self.next_id,
created_line: line,
created_by: word.to_string(),
};
self.next_id += 1;
self.stack.push(StackValue::Resource(resource));
}
pub fn pop(&mut self) -> Option<StackValue> {
self.stack.pop()
}
pub fn peek(&self) -> Option<&StackValue> {
self.stack.last()
}
pub fn depth(&self) -> usize {
self.stack.len()
}
pub fn consume_resource(&mut self, resource: TrackedResource) {
self.consumed.push(resource);
}
pub fn remaining_resources(&self) -> Vec<&TrackedResource> {
self.stack
.iter()
.filter_map(|v| match v {
StackValue::Resource(r) => Some(r),
StackValue::Unknown => None,
})
.collect()
}
pub fn merge(&self, other: &StackState) -> BranchMergeResult {
let self_resources: HashMap<usize, &TrackedResource> = self
.stack
.iter()
.filter_map(|v| match v {
StackValue::Resource(r) => Some((r.id, r)),
StackValue::Unknown => None,
})
.collect();
let other_resources: HashMap<usize, &TrackedResource> = other
.stack
.iter()
.filter_map(|v| match v {
StackValue::Resource(r) => Some((r.id, r)),
StackValue::Unknown => None,
})
.collect();
let self_consumed: std::collections::HashSet<usize> =
self.consumed.iter().map(|r| r.id).collect();
let other_consumed: std::collections::HashSet<usize> =
other.consumed.iter().map(|r| r.id).collect();
let mut inconsistent = Vec::new();
for (id, resource) in &self_resources {
if other_consumed.contains(id) && !self_consumed.contains(id) {
inconsistent.push(InconsistentResource {
resource: (*resource).clone(),
consumed_in_else: true,
});
}
}
for (id, resource) in &other_resources {
if self_consumed.contains(id) && !other_consumed.contains(id) {
inconsistent.push(InconsistentResource {
resource: (*resource).clone(),
consumed_in_else: false,
});
}
}
BranchMergeResult { inconsistent }
}
pub fn join(&self, other: &StackState) -> StackState {
let other_consumed: std::collections::HashSet<usize> =
other.consumed.iter().map(|r| r.id).collect();
let definitely_consumed: Vec<TrackedResource> = self
.consumed
.iter()
.filter(|r| other_consumed.contains(&r.id))
.cloned()
.collect();
let mut joined_stack = self.stack.clone();
let other_resources: HashMap<usize, TrackedResource> = other
.stack
.iter()
.filter_map(|v| match v {
StackValue::Resource(r) => Some((r.id, r.clone())),
StackValue::Unknown => None,
})
.collect();
for (i, val) in joined_stack.iter_mut().enumerate() {
if matches!(val, StackValue::Unknown)
&& i < other.stack.len()
&& let StackValue::Resource(r) = &other.stack[i]
{
*val = StackValue::Resource(r.clone());
}
}
let self_resource_ids: std::collections::HashSet<usize> = joined_stack
.iter()
.filter_map(|v| match v {
StackValue::Resource(r) => Some(r.id),
StackValue::Unknown => None,
})
.collect();
for (id, resource) in other_resources {
if !self_resource_ids.contains(&id) && !definitely_consumed.iter().any(|r| r.id == id) {
joined_stack.push(StackValue::Resource(resource));
}
}
StackState {
stack: joined_stack,
consumed: definitely_consumed,
next_id: self.next_id.max(other.next_id),
}
}
}
#[derive(Debug)]
pub(crate) struct BranchMergeResult {
pub inconsistent: Vec<InconsistentResource>,
}
#[derive(Debug)]
pub(crate) struct InconsistentResource {
pub resource: TrackedResource,
pub consumed_in_else: bool,
}
#[derive(Debug, Clone, Default)]
pub(crate) struct WordResourceInfo {
pub returns: Vec<ResourceKind>,
}
pub struct ProgramResourceAnalyzer {
word_info: HashMap<String, WordResourceInfo>,
file: std::path::PathBuf,
diagnostics: Vec<LintDiagnostic>,
}
impl ProgramResourceAnalyzer {
pub fn new(file: &Path) -> Self {
ProgramResourceAnalyzer {
word_info: HashMap::new(),
file: file.to_path_buf(),
diagnostics: Vec::new(),
}
}
pub fn analyze_program(&mut self, program: &Program) -> Vec<LintDiagnostic> {
self.diagnostics.clear();
self.word_info.clear();
for word in &program.words {
let info = self.collect_word_info(word);
self.word_info.insert(word.name.clone(), info);
}
for word in &program.words {
self.analyze_word_with_context(word);
}
std::mem::take(&mut self.diagnostics)
}
fn collect_word_info(&self, word: &WordDef) -> WordResourceInfo {
let mut state = StackState::new();
self.simulate_statements(&word.body, &mut state);
let returns: Vec<ResourceKind> = state
.remaining_resources()
.into_iter()
.map(|r| r.kind)
.collect();
WordResourceInfo { returns }
}
fn simulate_statements(&self, statements: &[Statement], state: &mut StackState) {
for stmt in statements {
self.simulate_statement(stmt, state);
}
}
fn simulate_statement(&self, stmt: &Statement, state: &mut StackState) {
match stmt {
Statement::IntLiteral(_)
| Statement::FloatLiteral(_)
| Statement::BoolLiteral(_)
| Statement::StringLiteral(_)
| Statement::Symbol(_) => {
state.push_unknown();
}
Statement::WordCall { name, span } => {
self.simulate_word_call(name, span.as_ref(), state);
}
Statement::Quotation { .. } => {
state.push_unknown();
}
Statement::If {
then_branch,
else_branch,
span: _,
} => {
state.pop(); let mut then_state = state.clone();
let mut else_state = state.clone();
self.simulate_statements(then_branch, &mut then_state);
if let Some(else_stmts) = else_branch {
self.simulate_statements(else_stmts, &mut else_state);
}
*state = then_state.join(&else_state);
}
Statement::Match { arms, span: _ } => {
state.pop();
let mut arm_states: Vec<StackState> = Vec::new();
for arm in arms {
let mut arm_state = state.clone();
self.simulate_statements(&arm.body, &mut arm_state);
arm_states.push(arm_state);
}
if let Some(joined) = arm_states.into_iter().reduce(|acc, s| acc.join(&s)) {
*state = joined;
}
}
}
}
fn simulate_word_common<F>(
name: &str,
span: Option<&Span>,
state: &mut StackState,
word_info: &HashMap<String, WordResourceInfo>,
mut on_resource_dropped: F,
) -> bool
where
F: FnMut(&TrackedResource),
{
let line = span.map(|s| s.line).unwrap_or(0);
match name {
"strand.weave" => {
state.pop();
state.push_resource(ResourceKind::WeaveHandle, line, name);
}
"chan.make" => {
state.push_resource(ResourceKind::Channel, line, name);
}
"strand.weave-cancel" => {
if let Some(StackValue::Resource(r)) = state.pop()
&& r.kind == ResourceKind::WeaveHandle
{
state.consume_resource(r);
}
}
"chan.close" => {
if let Some(StackValue::Resource(r)) = state.pop()
&& r.kind == ResourceKind::Channel
{
state.consume_resource(r);
}
}
"drop" => {
let dropped = state.pop();
if let Some(StackValue::Resource(r)) = dropped {
let already_consumed = state.consumed.iter().any(|c| c.id == r.id);
if !already_consumed {
on_resource_dropped(&r);
}
}
}
"dup" => {
if let Some(top) = state.peek().cloned() {
state.stack.push(top);
}
}
"swap" => {
let a = state.pop();
let b = state.pop();
if let Some(av) = a {
state.stack.push(av);
}
if let Some(bv) = b {
state.stack.push(bv);
}
}
"over" => {
if state.depth() >= 2 {
let second = state.stack[state.depth() - 2].clone();
state.stack.push(second);
}
}
"rot" => {
let c = state.pop();
let b = state.pop();
let a = state.pop();
if let Some(bv) = b {
state.stack.push(bv);
}
if let Some(cv) = c {
state.stack.push(cv);
}
if let Some(av) = a {
state.stack.push(av);
}
}
"nip" => {
let b = state.pop();
let a = state.pop();
if let Some(StackValue::Resource(r)) = a {
let already_consumed = state.consumed.iter().any(|c| c.id == r.id);
if !already_consumed {
on_resource_dropped(&r);
}
}
if let Some(bv) = b {
state.stack.push(bv);
}
}
"tuck" => {
let b = state.pop();
let a = state.pop();
if let Some(bv) = b.clone() {
state.stack.push(bv);
}
if let Some(av) = a {
state.stack.push(av);
}
if let Some(bv) = b {
state.stack.push(bv);
}
}
"strand.spawn" => {
state.pop();
let resources: Vec<TrackedResource> = state
.stack
.iter()
.filter_map(|v| match v {
StackValue::Resource(r) => Some(r.clone()),
StackValue::Unknown => None,
})
.collect();
for r in resources {
state.consume_resource(r);
}
state.push_unknown();
}
"map.set" => {
let value = state.pop();
state.pop(); state.pop(); if let Some(StackValue::Resource(r)) = value {
state.consume_resource(r);
}
state.push_unknown(); }
"list.push" | "list.prepend" => {
let value = state.pop();
state.pop(); if let Some(StackValue::Resource(r)) = value {
state.consume_resource(r);
}
state.push_unknown(); }
_ => {
if let Some(info) = word_info.get(name) {
for kind in &info.returns {
state.push_resource(*kind, line, name);
}
return true;
}
return false;
}
}
true
}
fn simulate_word_call(&self, name: &str, span: Option<&Span>, state: &mut StackState) {
Self::simulate_word_common(name, span, state, &self.word_info, |_| {});
}
fn analyze_word_with_context(&mut self, word: &WordDef) {
let mut state = StackState::new();
self.analyze_statements_with_context(&word.body, &mut state, word);
}
fn analyze_statements_with_context(
&mut self,
statements: &[Statement],
state: &mut StackState,
word: &WordDef,
) {
for stmt in statements {
self.analyze_statement_with_context(stmt, state, word);
}
}
fn analyze_statement_with_context(
&mut self,
stmt: &Statement,
state: &mut StackState,
word: &WordDef,
) {
match stmt {
Statement::IntLiteral(_)
| Statement::FloatLiteral(_)
| Statement::BoolLiteral(_)
| Statement::StringLiteral(_)
| Statement::Symbol(_) => {
state.push_unknown();
}
Statement::WordCall { name, span } => {
self.analyze_word_call_with_context(name, span.as_ref(), state, word);
}
Statement::Quotation { .. } => {
state.push_unknown();
}
Statement::If {
then_branch,
else_branch,
span: _,
} => {
state.pop();
let mut then_state = state.clone();
let mut else_state = state.clone();
self.analyze_statements_with_context(then_branch, &mut then_state, word);
if let Some(else_stmts) = else_branch {
self.analyze_statements_with_context(else_stmts, &mut else_state, word);
}
let merge_result = then_state.merge(&else_state);
for inconsistent in merge_result.inconsistent {
self.emit_branch_inconsistency_warning(&inconsistent, word);
}
*state = then_state.join(&else_state);
}
Statement::Match { arms, span: _ } => {
state.pop();
let mut arm_states: Vec<StackState> = Vec::new();
for arm in arms {
let mut arm_state = state.clone();
self.analyze_statements_with_context(&arm.body, &mut arm_state, word);
arm_states.push(arm_state);
}
if arm_states.len() >= 2 {
let first = &arm_states[0];
for other in &arm_states[1..] {
let merge_result = first.merge(other);
for inconsistent in merge_result.inconsistent {
self.emit_branch_inconsistency_warning(&inconsistent, word);
}
}
}
if let Some(joined) = arm_states.into_iter().reduce(|acc, s| acc.join(&s)) {
*state = joined;
}
}
}
}
fn analyze_word_call_with_context(
&mut self,
name: &str,
span: Option<&Span>,
state: &mut StackState,
word: &WordDef,
) {
let mut dropped_resources: Vec<TrackedResource> = Vec::new();
let handled = Self::simulate_word_common(name, span, state, &self.word_info, |r| {
dropped_resources.push(r.clone())
});
for r in dropped_resources {
self.emit_drop_warning(&r, span, word);
}
if handled {
return;
}
match name {
"strand.resume" => {
let value = state.pop();
let handle = state.pop();
if let Some(h) = handle {
state.stack.push(h);
} else {
state.push_unknown();
}
if let Some(v) = value {
state.stack.push(v);
} else {
state.push_unknown();
}
state.push_unknown();
}
"2dup" => {
if state.depth() >= 2 {
let b = state.stack[state.depth() - 1].clone();
let a = state.stack[state.depth() - 2].clone();
state.stack.push(a);
state.stack.push(b);
} else {
state.push_unknown();
state.push_unknown();
}
}
"3drop" => {
for _ in 0..3 {
if let Some(StackValue::Resource(r)) = state.pop() {
let already_consumed = state.consumed.iter().any(|c| c.id == r.id);
if !already_consumed {
self.emit_drop_warning(&r, span, word);
}
}
}
}
"pick" | "roll" => {
state.pop();
state.push_unknown();
}
"chan.send" | "chan.receive" => {
state.pop();
state.pop();
state.push_unknown();
state.push_unknown();
}
_ => {}
}
}
fn emit_drop_warning(
&mut self,
resource: &TrackedResource,
span: Option<&Span>,
word: &WordDef,
) {
let line = span
.map(|s| s.line)
.unwrap_or_else(|| word.source.as_ref().map(|s| s.start_line).unwrap_or(0));
let column = span.map(|s| s.column);
self.diagnostics.push(LintDiagnostic {
id: format!("resource-leak-{}", resource.kind.name().to_lowercase()),
message: format!(
"{} from `{}` (line {}) dropped without cleanup - {}",
resource.kind.name(),
resource.created_by,
resource.created_line + 1,
resource.kind.cleanup_suggestion()
),
severity: Severity::Warning,
replacement: String::new(),
file: self.file.clone(),
line,
end_line: None,
start_column: column,
end_column: column.map(|c| c + 4),
word_name: word.name.clone(),
start_index: 0,
end_index: 0,
});
}
fn emit_branch_inconsistency_warning(
&mut self,
inconsistent: &InconsistentResource,
word: &WordDef,
) {
let line = word.source.as_ref().map(|s| s.start_line).unwrap_or(0);
let branch = if inconsistent.consumed_in_else {
"else"
} else {
"then"
};
self.diagnostics.push(LintDiagnostic {
id: "resource-branch-inconsistent".to_string(),
message: format!(
"{} from `{}` (line {}) is consumed in {} branch but not the other - all branches must handle resources consistently",
inconsistent.resource.kind.name(),
inconsistent.resource.created_by,
inconsistent.resource.created_line + 1,
branch
),
severity: Severity::Warning,
replacement: String::new(),
file: self.file.clone(),
line,
end_line: None,
start_column: None,
end_column: None,
word_name: word.name.clone(),
start_index: 0,
end_index: 0,
});
}
}
pub struct ResourceAnalyzer {
diagnostics: Vec<LintDiagnostic>,
file: std::path::PathBuf,
}
impl ResourceAnalyzer {
pub fn new(file: &Path) -> Self {
ResourceAnalyzer {
diagnostics: Vec::new(),
file: file.to_path_buf(),
}
}
pub fn analyze_word(&mut self, word: &WordDef) -> Vec<LintDiagnostic> {
self.diagnostics.clear();
let mut state = StackState::new();
self.analyze_statements(&word.body, &mut state, word);
let _ = state.remaining_resources();
std::mem::take(&mut self.diagnostics)
}
fn analyze_statements(
&mut self,
statements: &[Statement],
state: &mut StackState,
word: &WordDef,
) {
for stmt in statements {
self.analyze_statement(stmt, state, word);
}
}
fn analyze_statement(&mut self, stmt: &Statement, state: &mut StackState, word: &WordDef) {
match stmt {
Statement::IntLiteral(_)
| Statement::FloatLiteral(_)
| Statement::BoolLiteral(_)
| Statement::StringLiteral(_)
| Statement::Symbol(_) => {
state.push_unknown();
}
Statement::WordCall { name, span } => {
self.analyze_word_call(name, span.as_ref(), state, word);
}
Statement::Quotation { body, .. } => {
let _ = body; state.push_unknown();
}
Statement::If {
then_branch,
else_branch,
span: _,
} => {
self.analyze_if(then_branch, else_branch.as_ref(), state, word);
}
Statement::Match { arms, span: _ } => {
self.analyze_match(arms, state, word);
}
}
}
fn analyze_word_call(
&mut self,
name: &str,
span: Option<&Span>,
state: &mut StackState,
word: &WordDef,
) {
let line = span.map(|s| s.line).unwrap_or(0);
match name {
"strand.weave" => {
state.pop(); state.push_resource(ResourceKind::WeaveHandle, line, name);
}
"chan.make" => {
state.push_resource(ResourceKind::Channel, line, name);
}
"strand.weave-cancel" => {
if let Some(StackValue::Resource(r)) = state.pop()
&& r.kind == ResourceKind::WeaveHandle
{
state.consume_resource(r);
}
}
"chan.close" => {
if let Some(StackValue::Resource(r)) = state.pop()
&& r.kind == ResourceKind::Channel
{
state.consume_resource(r);
}
}
"strand.resume" => {
let value = state.pop(); let handle = state.pop();
if let Some(h) = handle {
state.stack.push(h);
} else {
state.push_unknown();
}
if let Some(v) = value {
state.stack.push(v);
} else {
state.push_unknown();
}
state.push_unknown(); }
"drop" => {
let dropped = state.pop();
if let Some(StackValue::Resource(r)) = dropped {
let already_consumed = state.consumed.iter().any(|c| c.id == r.id);
if !already_consumed {
self.emit_drop_warning(&r, span, word);
}
}
}
"dup" => {
if let Some(top) = state.peek().cloned() {
state.stack.push(top);
} else {
state.push_unknown();
}
}
"swap" => {
let a = state.pop();
let b = state.pop();
if let Some(av) = a {
state.stack.push(av);
}
if let Some(bv) = b {
state.stack.push(bv);
}
}
"over" => {
if state.depth() >= 2 {
let second = state.stack[state.depth() - 2].clone();
state.stack.push(second);
} else {
state.push_unknown();
}
}
"rot" => {
let c = state.pop();
let b = state.pop();
let a = state.pop();
if let Some(bv) = b {
state.stack.push(bv);
}
if let Some(cv) = c {
state.stack.push(cv);
}
if let Some(av) = a {
state.stack.push(av);
}
}
"nip" => {
let b = state.pop();
let a = state.pop();
if let Some(StackValue::Resource(r)) = a {
let already_consumed = state.consumed.iter().any(|c| c.id == r.id);
if !already_consumed {
self.emit_drop_warning(&r, span, word);
}
}
if let Some(bv) = b {
state.stack.push(bv);
}
}
"tuck" => {
let b = state.pop();
let a = state.pop();
if let Some(bv) = b.clone() {
state.stack.push(bv);
}
if let Some(av) = a {
state.stack.push(av);
}
if let Some(bv) = b {
state.stack.push(bv);
}
}
"2dup" => {
if state.depth() >= 2 {
let b = state.stack[state.depth() - 1].clone();
let a = state.stack[state.depth() - 2].clone();
state.stack.push(a);
state.stack.push(b);
} else {
state.push_unknown();
state.push_unknown();
}
}
"3drop" => {
for _ in 0..3 {
if let Some(StackValue::Resource(r)) = state.pop() {
let already_consumed = state.consumed.iter().any(|c| c.id == r.id);
if !already_consumed {
self.emit_drop_warning(&r, span, word);
}
}
}
}
"pick" => {
state.pop(); state.push_unknown();
}
"roll" => {
state.pop(); state.push_unknown();
}
"chan.send" | "chan.receive" => {
state.pop();
state.pop();
state.push_unknown();
state.push_unknown();
}
"strand.spawn" => {
state.pop(); let resources_on_stack: Vec<TrackedResource> = state
.stack
.iter()
.filter_map(|v| match v {
StackValue::Resource(r) => Some(r.clone()),
StackValue::Unknown => None,
})
.collect();
for r in resources_on_stack {
state.consume_resource(r);
}
state.push_unknown(); }
_ => {
}
}
}
fn analyze_if(
&mut self,
then_branch: &[Statement],
else_branch: Option<&Vec<Statement>>,
state: &mut StackState,
word: &WordDef,
) {
state.pop();
let mut then_state = state.clone();
let mut else_state = state.clone();
self.analyze_statements(then_branch, &mut then_state, word);
if let Some(else_stmts) = else_branch {
self.analyze_statements(else_stmts, &mut else_state, word);
}
let merge_result = then_state.merge(&else_state);
for inconsistent in merge_result.inconsistent {
self.emit_branch_inconsistency_warning(&inconsistent, word);
}
*state = then_state.join(&else_state);
}
fn analyze_match(&mut self, arms: &[MatchArm], state: &mut StackState, word: &WordDef) {
state.pop();
if arms.is_empty() {
return;
}
let mut arm_states: Vec<StackState> = Vec::new();
for arm in arms {
let mut arm_state = state.clone();
match &arm.pattern {
crate::ast::Pattern::Variant(_) => {
}
crate::ast::Pattern::VariantWithBindings { bindings, .. } => {
for _ in bindings {
arm_state.push_unknown();
}
}
}
self.analyze_statements(&arm.body, &mut arm_state, word);
arm_states.push(arm_state);
}
if arm_states.len() >= 2 {
let first = &arm_states[0];
for other in &arm_states[1..] {
let merge_result = first.merge(other);
for inconsistent in merge_result.inconsistent {
self.emit_branch_inconsistency_warning(&inconsistent, word);
}
}
}
if let Some(first) = arm_states.into_iter().reduce(|acc, s| acc.join(&s)) {
*state = first;
}
}
fn emit_drop_warning(
&mut self,
resource: &TrackedResource,
span: Option<&Span>,
word: &WordDef,
) {
let line = span
.map(|s| s.line)
.unwrap_or_else(|| word.source.as_ref().map(|s| s.start_line).unwrap_or(0));
let column = span.map(|s| s.column);
self.diagnostics.push(LintDiagnostic {
id: format!("resource-leak-{}", resource.kind.name().to_lowercase()),
message: format!(
"{} created at line {} dropped without cleanup - {}",
resource.kind.name(),
resource.created_line + 1,
resource.kind.cleanup_suggestion()
),
severity: Severity::Warning,
replacement: String::new(),
file: self.file.clone(),
line,
end_line: None,
start_column: column,
end_column: column.map(|c| c + 4), word_name: word.name.clone(),
start_index: 0,
end_index: 0,
});
}
fn emit_branch_inconsistency_warning(
&mut self,
inconsistent: &InconsistentResource,
word: &WordDef,
) {
let line = word.source.as_ref().map(|s| s.start_line).unwrap_or(0);
let branch = if inconsistent.consumed_in_else {
"else"
} else {
"then"
};
self.diagnostics.push(LintDiagnostic {
id: "resource-branch-inconsistent".to_string(),
message: format!(
"{} created at line {} is consumed in {} branch but not the other - all branches must handle resources consistently",
inconsistent.resource.kind.name(),
inconsistent.resource.created_line + 1,
branch
),
severity: Severity::Warning,
replacement: String::new(),
file: self.file.clone(),
line,
end_line: None,
start_column: None,
end_column: None,
word_name: word.name.clone(),
start_index: 0,
end_index: 0,
});
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::ast::{Statement, WordDef};
fn make_word_call(name: &str) -> Statement {
Statement::WordCall {
name: name.to_string(),
span: Some(Span::new(0, 0, name.len())),
}
}
#[test]
fn test_immediate_weave_drop() {
let word = WordDef {
name: "bad".to_string(),
effect: None,
body: vec![
Statement::Quotation {
span: None,
id: 0,
body: vec![make_word_call("gen")],
},
make_word_call("strand.weave"),
make_word_call("drop"),
],
source: None,
allowed_lints: vec![],
};
let mut analyzer = ResourceAnalyzer::new(Path::new("test.seq"));
let diagnostics = analyzer.analyze_word(&word);
assert_eq!(diagnostics.len(), 1);
assert!(diagnostics[0].id.contains("weavehandle"));
assert!(diagnostics[0].message.contains("dropped without cleanup"));
}
#[test]
fn test_weave_properly_cancelled() {
let word = WordDef {
name: "good".to_string(),
effect: None,
body: vec![
Statement::Quotation {
span: None,
id: 0,
body: vec![make_word_call("gen")],
},
make_word_call("strand.weave"),
make_word_call("strand.weave-cancel"),
],
source: None,
allowed_lints: vec![],
};
let mut analyzer = ResourceAnalyzer::new(Path::new("test.seq"));
let diagnostics = analyzer.analyze_word(&word);
assert!(
diagnostics.is_empty(),
"Expected no warnings for properly cancelled weave"
);
}
#[test]
fn test_branch_inconsistent_handling() {
let word = WordDef {
name: "bad".to_string(),
effect: None,
body: vec![
Statement::Quotation {
span: None,
id: 0,
body: vec![make_word_call("gen")],
},
make_word_call("strand.weave"),
Statement::BoolLiteral(true),
Statement::If {
then_branch: vec![make_word_call("strand.weave-cancel")],
else_branch: Some(vec![make_word_call("drop")]),
span: None,
},
],
source: None,
allowed_lints: vec![],
};
let mut analyzer = ResourceAnalyzer::new(Path::new("test.seq"));
let diagnostics = analyzer.analyze_word(&word);
assert!(!diagnostics.is_empty());
}
#[test]
fn test_both_branches_cancel() {
let word = WordDef {
name: "good".to_string(),
effect: None,
body: vec![
Statement::Quotation {
span: None,
id: 0,
body: vec![make_word_call("gen")],
},
make_word_call("strand.weave"),
Statement::BoolLiteral(true),
Statement::If {
then_branch: vec![make_word_call("strand.weave-cancel")],
else_branch: Some(vec![make_word_call("strand.weave-cancel")]),
span: None,
},
],
source: None,
allowed_lints: vec![],
};
let mut analyzer = ResourceAnalyzer::new(Path::new("test.seq"));
let diagnostics = analyzer.analyze_word(&word);
assert!(
diagnostics.is_empty(),
"Expected no warnings when both branches cancel"
);
}
#[test]
fn test_channel_leak() {
let word = WordDef {
name: "bad".to_string(),
effect: None,
body: vec![make_word_call("chan.make"), make_word_call("drop")],
source: None,
allowed_lints: vec![],
};
let mut analyzer = ResourceAnalyzer::new(Path::new("test.seq"));
let diagnostics = analyzer.analyze_word(&word);
assert_eq!(diagnostics.len(), 1);
assert!(diagnostics[0].id.contains("channel"));
}
#[test]
fn test_channel_properly_closed() {
let word = WordDef {
name: "good".to_string(),
effect: None,
body: vec![make_word_call("chan.make"), make_word_call("chan.close")],
source: None,
allowed_lints: vec![],
};
let mut analyzer = ResourceAnalyzer::new(Path::new("test.seq"));
let diagnostics = analyzer.analyze_word(&word);
assert!(
diagnostics.is_empty(),
"Expected no warnings for properly closed channel"
);
}
#[test]
fn test_swap_resource_tracking() {
let word = WordDef {
name: "test".to_string(),
effect: None,
body: vec![
make_word_call("chan.make"),
Statement::IntLiteral(1),
make_word_call("swap"),
make_word_call("drop"), make_word_call("drop"), ],
source: None,
allowed_lints: vec![],
};
let mut analyzer = ResourceAnalyzer::new(Path::new("test.seq"));
let diagnostics = analyzer.analyze_word(&word);
assert_eq!(
diagnostics.len(),
1,
"Expected warning for dropped channel: {:?}",
diagnostics
);
assert!(diagnostics[0].id.contains("channel"));
}
#[test]
fn test_over_resource_tracking() {
let word = WordDef {
name: "test".to_string(),
effect: None,
body: vec![
make_word_call("chan.make"),
Statement::IntLiteral(1),
make_word_call("over"),
make_word_call("drop"), make_word_call("drop"), make_word_call("drop"), ],
source: None,
allowed_lints: vec![],
};
let mut analyzer = ResourceAnalyzer::new(Path::new("test.seq"));
let diagnostics = analyzer.analyze_word(&word);
assert_eq!(
diagnostics.len(),
2,
"Expected 2 warnings for dropped channels: {:?}",
diagnostics
);
}
#[test]
fn test_channel_transferred_via_spawn() {
let word = WordDef {
name: "accept-loop".to_string(),
effect: None,
body: vec![
make_word_call("chan.make"),
make_word_call("dup"),
Statement::Quotation {
span: None,
id: 0,
body: vec![make_word_call("worker")],
},
make_word_call("strand.spawn"),
make_word_call("drop"),
make_word_call("drop"),
make_word_call("chan.send"),
],
source: None,
allowed_lints: vec![],
};
let mut analyzer = ResourceAnalyzer::new(Path::new("test.seq"));
let diagnostics = analyzer.analyze_word(&word);
assert!(
diagnostics.is_empty(),
"Expected no warnings when channel is transferred via strand.spawn: {:?}",
diagnostics
);
}
#[test]
fn test_else_branch_only_leak() {
let word = WordDef {
name: "test".to_string(),
effect: None,
body: vec![
make_word_call("chan.make"),
Statement::BoolLiteral(true),
Statement::If {
then_branch: vec![make_word_call("chan.close")],
else_branch: Some(vec![make_word_call("drop")]),
span: None,
},
],
source: None,
allowed_lints: vec![],
};
let mut analyzer = ResourceAnalyzer::new(Path::new("test.seq"));
let diagnostics = analyzer.analyze_word(&word);
assert!(
!diagnostics.is_empty(),
"Expected warnings for else-branch leak: {:?}",
diagnostics
);
}
#[test]
fn test_branch_join_both_consume() {
let word = WordDef {
name: "test".to_string(),
effect: None,
body: vec![
make_word_call("chan.make"),
Statement::BoolLiteral(true),
Statement::If {
then_branch: vec![make_word_call("chan.close")],
else_branch: Some(vec![make_word_call("chan.close")]),
span: None,
},
],
source: None,
allowed_lints: vec![],
};
let mut analyzer = ResourceAnalyzer::new(Path::new("test.seq"));
let diagnostics = analyzer.analyze_word(&word);
assert!(
diagnostics.is_empty(),
"Expected no warnings when both branches consume: {:?}",
diagnostics
);
}
#[test]
fn test_branch_join_neither_consume() {
let word = WordDef {
name: "test".to_string(),
effect: None,
body: vec![
make_word_call("chan.make"),
Statement::BoolLiteral(true),
Statement::If {
then_branch: vec![],
else_branch: Some(vec![]),
span: None,
},
make_word_call("drop"), ],
source: None,
allowed_lints: vec![],
};
let mut analyzer = ResourceAnalyzer::new(Path::new("test.seq"));
let diagnostics = analyzer.analyze_word(&word);
assert_eq!(
diagnostics.len(),
1,
"Expected warning for dropped channel: {:?}",
diagnostics
);
assert!(diagnostics[0].id.contains("channel"));
}
#[test]
fn test_cross_word_resource_tracking() {
use crate::ast::Program;
let make_chan = WordDef {
name: "make-chan".to_string(),
effect: None,
body: vec![make_word_call("chan.make")],
source: None,
allowed_lints: vec![],
};
let leak_it = WordDef {
name: "leak-it".to_string(),
effect: None,
body: vec![make_word_call("make-chan"), make_word_call("drop")],
source: None,
allowed_lints: vec![],
};
let program = Program {
words: vec![make_chan, leak_it],
includes: vec![],
unions: vec![],
};
let mut analyzer = ProgramResourceAnalyzer::new(Path::new("test.seq"));
let diagnostics = analyzer.analyze_program(&program);
assert_eq!(
diagnostics.len(),
1,
"Expected warning for dropped channel from make-chan: {:?}",
diagnostics
);
assert!(diagnostics[0].id.contains("channel"));
assert!(diagnostics[0].message.contains("make-chan"));
}
#[test]
fn test_cross_word_proper_cleanup() {
use crate::ast::Program;
let make_chan = WordDef {
name: "make-chan".to_string(),
effect: None,
body: vec![make_word_call("chan.make")],
source: None,
allowed_lints: vec![],
};
let use_it = WordDef {
name: "use-it".to_string(),
effect: None,
body: vec![make_word_call("make-chan"), make_word_call("chan.close")],
source: None,
allowed_lints: vec![],
};
let program = Program {
words: vec![make_chan, use_it],
includes: vec![],
unions: vec![],
};
let mut analyzer = ProgramResourceAnalyzer::new(Path::new("test.seq"));
let diagnostics = analyzer.analyze_program(&program);
assert!(
diagnostics.is_empty(),
"Expected no warnings for properly closed channel: {:?}",
diagnostics
);
}
#[test]
fn test_cross_word_chain() {
use crate::ast::Program;
let make_chan = WordDef {
name: "make-chan".to_string(),
effect: None,
body: vec![make_word_call("chan.make")],
source: None,
allowed_lints: vec![],
};
let wrap_chan = WordDef {
name: "wrap-chan".to_string(),
effect: None,
body: vec![make_word_call("make-chan")],
source: None,
allowed_lints: vec![],
};
let leak_chain = WordDef {
name: "leak-chain".to_string(),
effect: None,
body: vec![make_word_call("wrap-chan"), make_word_call("drop")],
source: None,
allowed_lints: vec![],
};
let program = Program {
words: vec![make_chan, wrap_chan, leak_chain],
includes: vec![],
unions: vec![],
};
let mut analyzer = ProgramResourceAnalyzer::new(Path::new("test.seq"));
let diagnostics = analyzer.analyze_program(&program);
assert_eq!(
diagnostics.len(),
1,
"Expected warning for dropped channel through chain: {:?}",
diagnostics
);
}
}