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:
parent
3007266edf
commit
28e1627953
2 changed files with 87 additions and 5 deletions
|
|
@ -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 {
|
||||||
|
|
|
||||||
|
|
@ -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(
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue