diff --git a/src/analyzer/mod.rs b/src/analyzer/mod.rs index 76c5cc9..5391382 100644 --- a/src/analyzer/mod.rs +++ b/src/analyzer/mod.rs @@ -3,7 +3,7 @@ mod tests; use std::collections::{HashMap, HashSet}; -use crate::errors::{Diagnostic, ErrorCode}; +use crate::errors::{Diagnostic, ErrorCode, Label, Level}; use crate::lexer::Span; use crate::parser::ast::*; @@ -48,6 +48,7 @@ pub fn analyze(program: &Program) -> AnalysisResult { max_depths: HashMap::new(), stack_depth_limit: DEFAULT_STACK_DEPTH, in_loop: false, + used_vars: HashSet::new(), }; analyzer.analyze_program(program); @@ -70,6 +71,9 @@ struct Analyzer { max_depths: HashMap, stack_depth_limit: u32, in_loop: bool, + /// Names of variables that have been read somewhere in the program. + /// Used for the W0103 unused-variable warning. + used_vars: HashSet, } impl Analyzer { @@ -121,9 +125,34 @@ impl Analyzer { } } - // Type-check function bodies + // Type-check function bodies. Parameters are registered as + // symbols for the duration of the body check so that identifier + // references (and the W0103 used-variable tracker) can resolve + // them. They are unregistered afterwards to avoid leaking into + // the global scope. Parameters are also pre-marked as "used" so + // we do not emit W0103 for unused function arguments (which are + // a common and deliberate pattern). for fun in &program.functions { + let mut added_params = Vec::new(); + for param in &fun.params { + if !self.symbols.contains_key(¶m.name) { + self.symbols.insert( + param.name.clone(), + Symbol { + name: param.name.clone(), + sym_type: param.param_type.clone(), + is_const: false, + span: fun.span, + }, + ); + added_params.push(param.name.clone()); + } + self.mark_var_used(¶m.name); + } self.check_block(&fun.body, &state_names); + for name in &added_params { + self.symbols.remove(name); + } } // Build call graph @@ -141,6 +170,145 @@ impl Analyzer { // Compute max call depths from entry points (state handlers) self.compute_max_depths(program); + + // Check for unused global variables (W0103). Variables whose names + // start with '_' are exempt by convention. State-local variables are + // left out for now to avoid noise during early development. + for var in &program.globals { + if var.name.starts_with('_') { + continue; + } + if !self.used_vars.contains(&var.name) { + self.diagnostics.push(Diagnostic { + level: Level::Warning, + code: ErrorCode::W0103, + message: format!("unused variable '{}'", var.name), + span: var.span, + labels: Vec::