1
0
Fork 0
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:
Claude 2026-04-12 10:54:30 +00:00
parent 6f0e4a0a74
commit 21a74652f2
No known key found for this signature in database
3 changed files with 98 additions and 2 deletions

View file

@ -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, _) => {

View file

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

View file

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