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

Analyzer: W0104 warn on dead code after terminator

When a block contains a statement after \`return\`, \`break\`,
\`continue\`, or \`transition\`, the trailing statement is unreachable
and the analyzer now emits W0104 pointing at it. The warning includes
a secondary label on the terminator showing where execution stops.

Adds \`Statement::span\` helper to the AST so diagnostics can point at
arbitrary statements cleanly.

https://claude.ai/code/session_01W6eQFStA66EuMKHUFo2rx3
This commit is contained in:
Claude 2026-04-12 11:08:14 +00:00
parent e688397594
commit f8743cf95e
No known key found for this signature in database
3 changed files with 112 additions and 0 deletions

View file

@ -573,8 +573,26 @@ impl Analyzer {
}
fn check_block(&mut self, block: &Block, state_names: &[&str]) {
let mut terminated_by: Option<Span> = None;
let mut warned_dead_code = false;
for stmt in &block.statements {
if let Some(term_span) = terminated_by {
if !warned_dead_code {
self.diagnostics.push(
Diagnostic::warning(
ErrorCode::W0104,
"unreachable code after return / break / transition",
stmt.span(),
)
.with_label(term_span, "execution stops here"),
);
warned_dead_code = true;
}
}
self.check_statement(stmt, state_names);
if stmt_is_terminator(stmt) && terminated_by.is_none() {
terminated_by = Some(stmt.span());
}
}
}
@ -1008,6 +1026,18 @@ fn is_small_constant(expr: &Expr) -> bool {
matches!(expr, Expr::IntLiteral(_, _))
}
/// True if this statement unconditionally ends block execution —
/// subsequent statements in the same block cannot be reached.
fn stmt_is_terminator(stmt: &Statement) -> bool {
matches!(
stmt,
Statement::Return(_, _)
| Statement::Break(_)
| Statement::Continue(_)
| Statement::Transition(_, _)
)
}
fn block_can_exit_or_yield(block: &Block) -> bool {
block.statements.iter().any(stmt_can_exit_or_yield)
}

View file

@ -298,6 +298,64 @@ fn analyze_return_wrong_type() {
);
}
#[test]
fn analyze_dead_code_after_break() {
let src = r#"
game "Test" { mapper: NROM }
var x: u8 = 0
on frame {
loop {
break
x += 1
}
}
start Main
"#;
let errors = analyze_errors(src);
assert!(
errors.contains(&ErrorCode::W0104),
"code after break should trigger W0104, got: {errors:?}"
);
}
#[test]
fn analyze_dead_code_after_transition() {
let src = r#"
game "Test" { mapper: NROM }
state A {
on frame {
transition B
wait_frame
}
}
state B { on frame { wait_frame } }
start A
"#;
let errors = analyze_errors(src);
assert!(
errors.contains(&ErrorCode::W0104),
"code after transition should trigger W0104, got: {errors:?}"
);
}
#[test]
fn analyze_dead_code_after_return_in_fn() {
let src = r#"
game "Test" { mapper: NROM }
fun foo() -> u8 {
return 5
return 6
}
on frame { wait_frame }
start Main
"#;
let errors = analyze_errors(src);
assert!(
errors.contains(&ErrorCode::W0104),
"code after return should trigger W0104, got: {errors:?}"
);
}
#[test]
fn analyze_ram_overflow_emits_e0301() {
// Two arrays totalling >2 KB cannot fit in NES RAM, triggering

View file

@ -247,6 +247,30 @@ pub enum Statement {
DebugAssert(Expr, Span),
}
impl Statement {
pub fn span(&self) -> Span {
match self {
Self::VarDecl(v) => v.span,
Self::Draw(d) => d.span,
Self::Assign(_, _, _, s)
| Self::If(_, _, _, _, s)
| Self::While(_, _, s)
| Self::Loop(_, s)
| Self::Break(s)
| Self::Continue(s)
| Self::Return(_, s)
| Self::Transition(_, s)
| Self::WaitFrame(s)
| Self::Call(_, _, s)
| Self::LoadBackground(_, s)
| Self::SetPalette(_, s)
| Self::Scroll(_, _, s)
| Self::DebugLog(_, s)
| Self::DebugAssert(_, s) => *s,
}
}
}
#[derive(Debug, Clone)]
pub struct DrawStmt {
pub sprite_name: String,