diff --git a/src/analyzer/mod.rs b/src/analyzer/mod.rs index 4b0b629..903ad5f 100644 --- a/src/analyzer/mod.rs +++ b/src/analyzer/mod.rs @@ -49,6 +49,7 @@ pub fn analyze(program: &Program) -> AnalysisResult { stack_depth_limit: DEFAULT_STACK_DEPTH, in_loop: false, used_vars: HashSet::new(), + function_signatures: HashMap::new(), }; analyzer.analyze_program(program); @@ -74,6 +75,9 @@ struct Analyzer { /// 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>, } impl Analyzer { @@ -231,10 +235,13 @@ impl Analyzer { Expr::UnaryOp(_, inner, _) | Expr::Cast(inner, _, _) => { self.walk_expr_reads(inner); } - Expr::Call(_, args, _) => { - // Function name is validated separately via E0503; here we - // just recurse into argument expressions so their reads - // get tracked (and undefined-var errors surface). + Expr::Call(name, args, span) => { + // If the function is known, validate its call signature. + // Undefined-function errors are surfaced elsewhere (for + // Statement::Call) and via the call-graph pass. + if self.function_signatures.contains_key(name) { + self.check_call_signature(name, args, *span); + } for arg in args { self.walk_expr_reads(arg); } @@ -380,6 +387,9 @@ impl Analyzer { span: fun.span, }, ); + let param_types: Vec = fun.params.iter().map(|p| p.param_type.clone()).collect(); + self.function_signatures + .insert(fun.name.clone(), param_types); } fn allocate_ram(&mut self, size: u16) -> u16 { @@ -567,7 +577,9 @@ impl Analyzer { let _ = self.infer_type(expr); } Statement::Call(name, args, span) => { - if !self.symbols.contains_key(name) { + if self.symbols.contains_key(name) { + self.check_call_signature(name, args, *span); + } else { self.diagnostics.push(Diagnostic::error( ErrorCode::E0503, format!("undefined function '{name}'"), @@ -630,6 +642,30 @@ impl Analyzer { } } + /// Check that a call site matches the function's declared signature: + /// argument count matches the parameter count, and each argument's + /// inferred type is compatible with the declared parameter type. + fn check_call_signature(&mut self, name: &str, args: &[Expr], span: Span) { + let Some(params) = self.function_signatures.get(name).cloned() else { + return; + }; + if params.len() != args.len() { + self.diagnostics.push(Diagnostic::error( + ErrorCode::E0203, + format!( + "wrong number of arguments to '{name}': expected {}, got {}", + params.len(), + args.len() + ), + span, + )); + return; + } + for (param_ty, arg) in params.iter().zip(args.iter()) { + self.check_expr_type(arg, param_ty); + } + } + fn check_expr_type(&mut self, expr: &Expr, expected: &NesType) { let actual = self.infer_type(expr); if let Some(actual) = actual { diff --git a/src/analyzer/tests.rs b/src/analyzer/tests.rs index bd30204..70a3f82 100644 --- a/src/analyzer/tests.rs +++ b/src/analyzer/tests.rs @@ -224,6 +224,52 @@ fn analyze_undefined_function() { assert!(errors.contains(&ErrorCode::E0503)); } +#[test] +fn analyze_call_arity_mismatch() { + let errors = analyze_errors( + r#" + game "Test" { mapper: NROM } + fun add(a: u8, b: u8) -> u8 { return a } + on frame { add(1) } + start Main + "#, + ); + assert!( + errors.contains(&ErrorCode::E0203), + "calling with wrong argument count should produce E0203, got: {errors:?}" + ); +} + +#[test] +fn analyze_call_arity_ok() { + analyze_ok( + r#" + game "Test" { mapper: NROM } + fun add(a: u8, b: u8) -> u8 { return a } + on frame { add(1, 2) } + start Main + "#, + ); +} + +#[test] +fn analyze_call_arity_in_expr_context() { + // Calls used as expressions should also be checked. + let errors = analyze_errors( + r#" + game "Test" { mapper: NROM } + fun two(a: u8, b: u8) -> u8 { return a } + var x: u8 = 0 + on frame { x = two(1) } + start Main + "#, + ); + assert!( + errors.contains(&ErrorCode::E0203), + "call arity error in expression context should still trigger E0203: {errors:?}" + ); +} + #[test] fn analyze_const_assignment_error() { let errors = analyze_errors(