1
0
Fork 0
mirror of https://github.com/imjasonh/nescript synced 2026-07-16 12:12:04 +00:00

Analyzer: validate function call arity

Tracks parameter types per function in a signature map, and validates
that each Statement::Call and Expr::Call has the right number of
arguments (E0203). Also type-checks each argument against its declared
parameter type. Previously, calls with the wrong argument count silently
passed semantic analysis and only broke later at codegen or runtime.

https://claude.ai/code/session_01W6eQFStA66EuMKHUFo2rx3
This commit is contained in:
Claude 2026-04-12 10:49:36 +00:00
parent 3007266edf
commit 28e1627953
No known key found for this signature in database
2 changed files with 87 additions and 5 deletions

View file

@ -49,6 +49,7 @@ pub fn analyze(program: &Program) -> AnalysisResult {
stack_depth_limit: DEFAULT_STACK_DEPTH, stack_depth_limit: DEFAULT_STACK_DEPTH,
in_loop: false, in_loop: false,
used_vars: HashSet::new(), used_vars: HashSet::new(),
function_signatures: HashMap::new(),
}; };
analyzer.analyze_program(program); analyzer.analyze_program(program);
@ -74,6 +75,9 @@ struct Analyzer {
/// Names of variables that have been read somewhere in the program. /// Names of variables that have been read somewhere in the program.
/// Used for the W0103 unused-variable warning. /// Used for the W0103 unused-variable warning.
used_vars: HashSet<String>, used_vars: HashSet<String>,
/// Function name to parameter types (in order). Used to validate
/// call arity and argument types.
function_signatures: HashMap<String, Vec<NesType>>,
} }
impl Analyzer { impl Analyzer {
@ -231,10 +235,13 @@ impl Analyzer {
Expr::UnaryOp(_, inner, _) | Expr::Cast(inner, _, _) => { Expr::UnaryOp(_, inner, _) | Expr::Cast(inner, _, _) => {
self.walk_expr_reads(inner); self.walk_expr_reads(inner);
} }
Expr::Call(_, args, _) => { Expr::Call(name, args, span) => {
// Function name is validated separately via E0503; here we // If the function is known, validate its call signature.
// just recurse into argument expressions so their reads // Undefined-function errors are surfaced elsewhere (for
// get tracked (and undefined-var errors surface). // 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 { for arg in args {
self.walk_expr_reads(arg); self.walk_expr_reads(arg);
} }
@ -380,6 +387,9 @@ impl Analyzer {
span: fun.span, span: fun.span,
}, },
); );
let param_types: Vec<NesType> = 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 { fn allocate_ram(&mut self, size: u16) -> u16 {
@ -567,7 +577,9 @@ impl Analyzer {
let _ = self.infer_type(expr); let _ = self.infer_type(expr);
} }
Statement::Call(name, args, span) => { 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( self.diagnostics.push(Diagnostic::error(
ErrorCode::E0503, ErrorCode::E0503,
format!("undefined function '{name}'"), 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) { fn check_expr_type(&mut self, expr: &Expr, expected: &NesType) {
let actual = self.infer_type(expr); let actual = self.infer_type(expr);
if let Some(actual) = actual { if let Some(actual) = actual {

View file

@ -224,6 +224,52 @@ fn analyze_undefined_function() {
assert!(errors.contains(&ErrorCode::E0503)); 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] #[test]
fn analyze_const_assignment_error() { fn analyze_const_assignment_error() {
let errors = analyze_errors( let errors = analyze_errors(