1
0
Fork 0
mirror of https://github.com/imjasonh/nescript synced 2026-07-08 17:06:04 +00:00
nescript/examples/loop_break_continue.ne
Claude 548787ac8a
W0110 inline fallback warning + docs refresh
W0110: when a function marked `inline` has a body shape the IR
lowerer can't splice (conditional early return, loops, nested
control flow, empty void body), the analyzer now emits a
warning at the declaration site so the declined hint is
visible instead of silently falling back to a regular JSR.

Implementation:
  - New `W0110` error code in `src/errors/diagnostic.rs` (warning level).
  - New `pub fn can_inline_fun(return_type, body) -> bool` in
    `src/ir/lowering.rs`, extracted from the existing capture
    logic so the analyzer and the IR lowerer share the same
    eligibility rules and can never drift.
  - New `check_inline_declinability` analyzer pass called from
    the tail of `analyze_program`, mirroring the existing
    `check_sprite_scanline_budget` / `check_unreachable_states`
    passes. Emits W0110 with help + note text pointing at the
    two accepted body shapes.
  - `capture_inline_bodies` now defers to `can_inline_fun`
    instead of duplicating the match pattern, so the two sides
    stay in lockstep by construction.

Four regression tests in `src/analyzer/tests.rs` cover the
conditional-return and while-loop declines plus the two
accepted shapes (single-return expression, void sequence).

Example source cleanups: `wrap52` in `examples/war/deck.ne`
and `abs_diff` in both `examples/arrays_and_functions.ne` and
`examples/loop_break_continue.ne` drop the `inline` keyword.
All three were dead hints — the `inline` was being silently
declined before this change, so removing it is source-only;
the three ROMs are byte-identical, all 32 emulator goldens
still match.

Docs refresh
  - `docs/language-guide.md`: rewrote the Inline Functions section
    (real behaviour + W0110), added W0105/W0106/W0107/W0108/W0109/
    W0110 to the warnings table, added the `debug.sprite_overflow*`
    builtins + sprite-per-scanline mitigations section to the
    Debug Mode docs, added a `cycle_sprites` statement entry and
    cross-referenced it from `draw`.
  - `docs/nes-reference.md`: fleshed out the "NEScript Memory
    Usage" block with the full ZP + high-RAM layout, including
    the new `$07EF` / `$07FC` / `$07FD` slots for sprite cycling
    and the debug sprite-overflow telemetry.
  - `docs/future-work.md`: documented all four debug query
    builtins in the "What ships today" block; updated the open
    "OAM allocation strategy" question to reference the shipped
    `cycle_sprites` path and ask about an automatic-flicker
    game attribute as a follow-up.
  - `docs/architecture.md`: updated the `ir/` and `optimizer/`
    module summaries to describe real inline splicing (now
    in lowering, not the optimizer).
  - `README.md`: reframed the `inline` bullet from "hint" to
    "real splicing for single-return / void-body shapes";
    expanded the debug-support bullet to mention the four
    query builtins and their stripping in release builds; added
    a new bullet for the three-layer sprite-per-scanline
    mitigations; bumped the test count from 497 → 694; updated
    the war.ne entry to mention the seven compiler bugs are all
    fixed and point readers at `git log` (instead of the
    deleted COMPILER_BUGS.md).
  - `examples/README.md`: same `git log`-pointing rewrite for
    the war.ne entry.

Deletions
  - `examples/war/COMPILER_BUGS.md` is removed. All seven
    catalogued bugs are fixed; the file's historical value
    lives in `git log` now. Every source-code comment and doc
    reference to the file has been updated to either point at
    `git log` or just describe the bug in place.

Test count: 616 unit + 75 integration + 3 doctests = 694 total.
Clippy / fmt clean. 32/32 emulator goldens match.

https://claude.ai/code/session_0143dTgh3UeRrtfHgQwzcv5z
2026-04-15 23:19:07 +00:00

110 lines
3.3 KiB
Text

// Loop / Break / Continue — demonstrates `loop`, `break`, and
// `continue` via a linear scan that finds the first "hazard" the
// player is overlapping.
//
// NEScript's sprite allocator assigns a static OAM slot per `draw`
// statement at compile time, so a `draw` inside a loop body would
// reuse the same slot on every iteration. We instead use the loop
// to COMPUTE state — "is the player touching a hazard, and which
// one?" — and then fold that result into how (and whether) the
// single player sprite is drawn. The hazards themselves get one
// unrolled `draw` per slot so each hazard gets its own OAM entry.
//
// Features shown:
// - `loop { ... }` as an infinite loop
// - `break` to exit on first match
// - `continue` to skip inactive slots
// - array indexing with a loop-carried variable
//
// Build: cargo run -- build examples/loop_break_continue.ne
game "Loop Demo" {
mapper: NROM
}
const NUM_HAZARDS: u8 = 4
const HIT_RADIUS: u8 = 12
// Four hazard slots. `active` is 1 if the slot is live.
var active: u8[4] = [1, 1, 0, 1]
var hazard_x: u8[4] = [40, 96, 160, 200]
var hazard_y: u8[4] = [60, 140, 100, 180]
var px: u8 = 120
var py: u8 = 120
// Search results, written by the loop each frame.
var hit: u8 = 0 // 1 if the player is touching any hazard
var hit_idx: u8 = 0 // which slot was hit (only meaningful if hit == 1)
// Small absolute-difference helper. Saves lines at each call site.
// Not marked `inline`: the conditional early return is one of
// the shapes the inliner declines (W0110).
fun abs_diff(a: u8, b: u8) -> u8 {
if a > b {
return a - b
}
return b - a
}
on frame {
// Player movement.
if button.right { px += 1 }
if button.left { px -= 1 }
if button.down { py += 1 }
if button.up { py -= 1 }
// Linear scan for the first active hazard the player is
// touching. The loop exits via `break` on the first hit, or
// by running i past the end.
hit = 0
var i: u8 = 0
loop {
if i >= NUM_HAZARDS {
break
}
if active[i] == 0 {
// Skip inactive slots — this is where `continue`
// shines: the increment and re-test happen at the top.
i += 1
continue
}
var dx: u8 = abs_diff(px, hazard_x[i])
var dy: u8 = abs_diff(py, hazard_y[i])
if dx < HIT_RADIUS {
if dy < HIT_RADIUS {
hit = 1
hit_idx = i
break
}
}
i += 1
}
// One draw per hazard slot — unrolled so each sprite gets its
// own static OAM entry. Inactive slots draw off-screen ($FE)
// so they don't flicker the visible ones.
if active[0] == 1 {
draw Hazard at: (hazard_x[0], hazard_y[0])
}
if active[1] == 1 {
draw Hazard at: (hazard_x[1], hazard_y[1])
}
if active[2] == 1 {
draw Hazard at: (hazard_x[2], hazard_y[2])
}
if active[3] == 1 {
draw Hazard at: (hazard_x[3], hazard_y[3])
}
// The player sprite reflects the collision state: when hit,
// draw a "bang" marker above the player head. The `hit_idx`
// is unused visually but proves the loop ran to completion.
draw Player at: (px, py)
if hit == 1 {
draw Spark at: (px, py - 8)
}
}
start Main