mirror of
https://github.com/imjasonh/nescript
synced 2026-07-09 01:16:12 +00:00
Analyzer: W0102 warn on infinite loop without yield
Emits W0102 when a \`loop\` body contains no \`break\`, \`return\`, \`transition\`, or \`wait_frame\`, since such loops spin forever and prevent vblank from being handled. The check recurses into nested if/while bodies so that \`break\` behind a condition still counts. https://claude.ai/code/session_01W6eQFStA66EuMKHUFo2rx3
This commit is contained in:
parent
6f0e4a0a74
commit
21a74652f2
3 changed files with 98 additions and 2 deletions
|
|
@ -561,11 +561,21 @@ impl Analyzer {
|
|||
self.check_block(body, state_names);
|
||||
self.in_loop = was_in_loop;
|
||||
}
|
||||
Statement::Loop(body, _) => {
|
||||
Statement::Loop(body, span) => {
|
||||
let was_in_loop = self.in_loop;
|
||||
self.in_loop = true;
|
||||
self.check_block(body, state_names);
|
||||
self.in_loop = was_in_loop;
|
||||
// W0102: loop body must contain a break, return,
|
||||
// transition, or wait_frame — otherwise the NES spins
|
||||
// forever inside the loop and vblank never gets handled.
|
||||
if !block_can_exit_or_yield(body) {
|
||||
self.diagnostics.push(Diagnostic::warning(
|
||||
ErrorCode::W0102,
|
||||
"infinite loop with no break, return, transition, or wait_frame",
|
||||
*span,
|
||||
).with_help("add `wait_frame`, `break`, `return`, or `transition` somewhere in the body"));
|
||||
}
|
||||
}
|
||||
Statement::Transition(name, span) => {
|
||||
if !state_names.contains(&name.as_str()) {
|
||||
|
|
@ -900,6 +910,38 @@ fn collect_calls_block(block: &Block, calls: &mut Vec<String>) {
|
|||
}
|
||||
}
|
||||
|
||||
/// Return true if the given block contains any statement that can
|
||||
/// either exit the enclosing loop (`break`, `return`, `transition`)
|
||||
/// or yield control back to the frame loop (`wait_frame`).
|
||||
///
|
||||
/// This is used by the W0102 check to decide whether an otherwise-
|
||||
/// unbounded `loop { }` is actually an infinite spin. We recurse into
|
||||
/// nested control-flow blocks so that a `break` inside a conditional
|
||||
/// body still counts as "can exit".
|
||||
fn block_can_exit_or_yield(block: &Block) -> bool {
|
||||
block.statements.iter().any(stmt_can_exit_or_yield)
|
||||
}
|
||||
|
||||
fn stmt_can_exit_or_yield(stmt: &Statement) -> bool {
|
||||
match stmt {
|
||||
Statement::Break(_)
|
||||
| Statement::Return(_, _)
|
||||
| Statement::Transition(_, _)
|
||||
| Statement::WaitFrame(_) => true,
|
||||
Statement::If(_, then_b, elifs, else_b, _) => {
|
||||
block_can_exit_or_yield(then_b)
|
||||
|| elifs.iter().any(|(_, b)| block_can_exit_or_yield(b))
|
||||
|| else_b.as_ref().is_some_and(block_can_exit_or_yield)
|
||||
}
|
||||
Statement::While(_, body, _) | Statement::Loop(body, _) => {
|
||||
// A nested loop with a wait_frame inside still yields
|
||||
// control, so check its body recursively.
|
||||
block_can_exit_or_yield(body)
|
||||
}
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
fn collect_calls_expr(expr: &Expr, calls: &mut Vec<String>) {
|
||||
match expr {
|
||||
Expr::Call(name, args, _) => {
|
||||
|
|
|
|||
|
|
@ -298,6 +298,54 @@ fn analyze_return_wrong_type() {
|
|||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn analyze_loop_without_exit_warns() {
|
||||
let errors = analyze_errors(
|
||||
r#"
|
||||
game "Test" { mapper: NROM }
|
||||
var x: u8 = 0
|
||||
on frame {
|
||||
loop { x += 1 }
|
||||
}
|
||||
start Main
|
||||
"#,
|
||||
);
|
||||
assert!(
|
||||
errors.contains(&ErrorCode::W0102),
|
||||
"infinite loop with no exit should produce W0102, got: {errors:?}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn analyze_loop_with_wait_frame_ok() {
|
||||
analyze_ok(
|
||||
r#"
|
||||
game "Test" { mapper: NROM }
|
||||
on frame {
|
||||
loop { wait_frame }
|
||||
}
|
||||
start Main
|
||||
"#,
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn analyze_loop_with_break_ok() {
|
||||
analyze_ok(
|
||||
r#"
|
||||
game "Test" { mapper: NROM }
|
||||
var x: u8 = 0
|
||||
on frame {
|
||||
loop {
|
||||
x += 1
|
||||
if x == 10 { break }
|
||||
}
|
||||
}
|
||||
start Main
|
||||
"#,
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn analyze_return_value_from_void_fn() {
|
||||
let errors = analyze_errors(
|
||||
|
|
|
|||
|
|
@ -45,7 +45,6 @@ pub enum ErrorCode {
|
|||
// W01xx: Warnings
|
||||
#[allow(dead_code)]
|
||||
W0101, // expensive multiply/divide operation
|
||||
#[allow(dead_code)]
|
||||
W0102, // loop without break or wait_frame
|
||||
W0103, // unused variable
|
||||
W0104, // unreachable state
|
||||
|
|
@ -118,6 +117,13 @@ impl Diagnostic {
|
|||
}
|
||||
}
|
||||
|
||||
/// Construct a diagnostic with the level implied by the code
|
||||
/// (identical to [`Diagnostic::error`], but reads better at call
|
||||
/// sites that emit a warning code).
|
||||
pub fn warning(code: ErrorCode, message: impl Into<String>, span: Span) -> Self {
|
||||
Self::error(code, message, span)
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn with_help(mut self, help: impl Into<String>) -> Self {
|
||||
self.help = Some(help.into());
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue