#[cfg(test)] mod tests; use std::collections::{HashMap, HashSet}; use crate::errors::{Diagnostic, ErrorCode, Label, Level}; use crate::lexer::Span; use crate::parser::ast::*; /// Symbol information stored in the scope. #[derive(Debug, Clone)] pub struct Symbol { pub name: String, pub sym_type: NesType, pub is_const: bool, pub span: Span, } /// Memory assignment for a variable. #[derive(Debug, Clone)] pub struct VarAllocation { pub name: String, pub address: u16, pub size: u16, } /// Result of semantic analysis. pub struct AnalysisResult { pub symbols: HashMap, pub var_allocations: Vec, pub diagnostics: Vec, pub call_graph: HashMap>, pub max_depths: HashMap, } /// Default call stack depth limit for the NES runtime. const DEFAULT_STACK_DEPTH: u32 = 8; /// Upper bound (exclusive) for user-variable zero-page allocation. /// Addresses `$80-$FF` are reserved for IR codegen temp slots, so user /// globals must fit into `$10-$7F`. const ZP_USER_CAP: u8 = 0x80; /// Exclusive upper bound of usable RAM. The NES has 2 KB of internal /// RAM at `$0000-$07FF`; the allocator uses up through `$07FF`. const RAM_END: u16 = 0x0800; /// Analyze a parsed program for semantic errors. pub fn analyze(program: &Program) -> AnalysisResult { let mut analyzer = Analyzer { symbols: HashMap::new(), var_allocations: Vec::new(), diagnostics: Vec::new(), next_ram_addr: 0x0300, // $0300 is first usable RAM after OAM buffer next_zp_addr: 0x10, // $10 is first usable zero-page after reserved area call_graph: HashMap::new(), max_depths: HashMap::new(), stack_depth_limit: DEFAULT_STACK_DEPTH, in_loop: false, used_vars: HashSet::new(), function_signatures: HashMap::new(), current_return_type: None, in_function_body: false, struct_layouts: HashMap::new(), }; analyzer.analyze_program(program); AnalysisResult { symbols: analyzer.symbols, var_allocations: analyzer.var_allocations, diagnostics: analyzer.diagnostics, call_graph: analyzer.call_graph, max_depths: analyzer.max_depths, } } struct Analyzer { symbols: HashMap, var_allocations: Vec, diagnostics: Vec, next_ram_addr: u16, next_zp_addr: u8, call_graph: HashMap>, 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, /// Function name to parameter types (in order). Used to validate /// call arity and argument types. function_signatures: HashMap>, /// Return type of the function currently being analyzed, or None /// when the function has no declared return type. Only meaningful /// when `in_function_body` is true. current_return_type: Option, /// True while analyzing a function body (as opposed to a state /// handler's `on_enter` / `on_exit` / `on_frame` block). Used to /// distinguish "void function" from "state handler" when checking /// `return value` statements. in_function_body: bool, /// Struct name to layout. Each field has an offset in bytes from /// the base address of the struct. struct_layouts: HashMap, } /// Layout info for a struct type. #[derive(Debug, Clone)] pub struct StructLayout { pub size: u16, pub fields: Vec<(String, NesType, u16)>, // (name, type, offset) } impl Analyzer { fn analyze_program(&mut self, program: &Program) { // Register struct layouts first so later declarations can // reference them (for variable sizing, etc.). for s in &program.structs { self.register_struct(s); } // Register constants for c in &program.constants { self.register_const(c); } // Register enum variants as constants with values 0, 1, 2, ... for e in &program.enums { self.register_enum(e); } // Register and allocate globals for var in &program.globals { self.register_var(var); } // Register functions as symbols for fun in &program.functions { self.register_fun(fun); } // Register state-local variables for state in &program.states { for var in &state.locals { self.register_var(var); } } // Validate state references let state_names: Vec<&str> = program.states.iter().map(|s| s.name.as_str()).collect(); // Check start state exists if !state_names.contains(&program.start_state.as_str()) { self.diagnostics.push(Diagnostic::error( ErrorCode::E0404, format!("start state '{}' is not defined", program.start_state), program.span, )); } // Type-check all state bodies for state in &program.states { if let Some(block) = &state.on_enter { self.check_block(block, &state_names); } if let Some(block) = &state.on_exit { self.check_block(block, &state_names); } if let Some(block) = &state.on_frame { self.check_block(block, &state_names); } // `on scanline(N)` is only valid with mappers that have a // scanline-counting IRQ source (currently only MMC3). if !state.on_scanline.is_empty() && program.game.mapper != Mapper::MMC3 { self.diagnostics.push(Diagnostic::error( ErrorCode::E0203, "`on scanline` requires the MMC3 mapper", state.span, )); } for (_, block) in &state.on_scanline { self.check_block(block, &state_names); } } // 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.current_return_type.clone_from(&fun.return_type); self.in_function_body = true; self.check_block(&fun.body, &state_names); self.current_return_type = None; self.in_function_body = false; for name in &added_params { self.symbols.remove(name); } } // Build call graph self.build_call_graph(program); // Detect recursion let recursive_fns = detect_recursion(&self.call_graph); for name in &recursive_fns { self.diagnostics.push(Diagnostic::error( ErrorCode::E0402, format!("recursion detected in function '{name}'"), program.span, )); } // Compute max call depths from entry points (state handlers) self.compute_max_depths(program); // Check for unused variables (W0103). Variables whose names // start with '_' are exempt by convention. Both globals and // state-local variables are checked. for var in &program.globals { self.check_unused_var(var); } for state in &program.states { for var in &state.locals { self.check_unused_var(var); } } // Check for unreachable states (W0104). self.check_unreachable_states(program); } /// Mark a variable name as having been read somewhere in the program. fn mark_var_used(&mut self, name: &str) { self.used_vars.insert(name.to_string()); } /// Emit W0103 if `var` is never read anywhere. Variables named /// with a leading `_` are exempt by convention. fn check_unused_var(&mut self, var: &VarDecl) { if var.name.starts_with('_') { return; } if self.used_vars.contains(&var.name) { return; } self.diagnostics.push(Diagnostic { level: Level::Warning, code: ErrorCode::W0103, message: format!("unused variable '{}'", var.name), span: var.span, labels: Vec::