1
0
Fork 0
mirror of https://github.com/imjasonh/nescript synced 2026-07-09 09:18:01 +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,
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<String>,
/// Function name to parameter types (in order). Used to validate
/// call arity and argument types.
function_signatures: HashMap<String, Vec<NesType>>,
}
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<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 {
@ -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 {

View file

@ -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(