examples: add 5 new programs covering match/loop/logic/bitwise/scanline
Fills the biggest feature-coverage gaps in the existing example set:
- match_demo.ne — match statement over a Screen enum,
driving a title / playing / paused /
game-over flow with a debounced controller.
- loop_break_continue.ne — `loop { ... }` with `break` and `continue`,
scanning an enemy array for the first hit.
- logic_ops.ne — keyword-based `and` / `or` / `not` gating
movement and scoring on alive/paused flags.
- bitwise_ops.ne — packed status-byte flags with `&` / `|` /
`^` / `>>` plus a health-bar render loop.
- scanline_split.ne — MMC3 `on scanline(120)` handler rewriting
the scroll register mid-frame for a
classic status-bar split.
All 14 examples (9 existing + 5 new) pass the jsnes smoke test
(`14/14 ROMs rendered successfully`) and still pass `cargo fmt`,
`cargo clippy -D warnings`, and `cargo test`.
Known limitations surfaced while authoring these examples, to be
fixed in follow-up commits:
1. Array-literal global initializers (`var xs: u8[4] = [1,2,3,4]`)
are silently dropped by `lower_program` — `eval_const` returns
None for `Expr::ArrayLiteral` and no synthetic per-element
init code is emitted. Affects `arrays_and_functions`,
`structs_enums_for`, `loop_break_continue`, and any future
array-using example. Arrays effectively boot at all-zero.
2. `draw` inside a loop body reuses one static OAM slot —
`next_oam_slot` increments at IR-codegen time rather than at
runtime, so N iterations all write to the same 4-byte OAM
entry. Affects `arrays_and_functions`, `structs_enums_for`,
`bitwise_ops` (health pips), and any loop that wants to
render per-iteration sprites.
Both bugs are latent and didn't surface until I tried to write
examples that exercise the relevant features — the existing
integration tests only check iNES header structure, and the
jsnes smoke test's "at least one sprite rendered" bar is
satisfied by one sprite even when several were intended.
https://claude.ai/code/session_014Z5y3Q9krLcAxYpZQJhZ5V
2026-04-12 19:05:18 +00:00
|
|
|
// 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.
|
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
|
|
|
// Not marked `inline`: the conditional early return is one of
|
|
|
|
|
// the shapes the inliner declines (W0110).
|
|
|
|
|
fun abs_diff(a: u8, b: u8) -> u8 {
|
examples: add 5 new programs covering match/loop/logic/bitwise/scanline
Fills the biggest feature-coverage gaps in the existing example set:
- match_demo.ne — match statement over a Screen enum,
driving a title / playing / paused /
game-over flow with a debounced controller.
- loop_break_continue.ne — `loop { ... }` with `break` and `continue`,
scanning an enemy array for the first hit.
- logic_ops.ne — keyword-based `and` / `or` / `not` gating
movement and scoring on alive/paused flags.
- bitwise_ops.ne — packed status-byte flags with `&` / `|` /
`^` / `>>` plus a health-bar render loop.
- scanline_split.ne — MMC3 `on scanline(120)` handler rewriting
the scroll register mid-frame for a
classic status-bar split.
All 14 examples (9 existing + 5 new) pass the jsnes smoke test
(`14/14 ROMs rendered successfully`) and still pass `cargo fmt`,
`cargo clippy -D warnings`, and `cargo test`.
Known limitations surfaced while authoring these examples, to be
fixed in follow-up commits:
1. Array-literal global initializers (`var xs: u8[4] = [1,2,3,4]`)
are silently dropped by `lower_program` — `eval_const` returns
None for `Expr::ArrayLiteral` and no synthetic per-element
init code is emitted. Affects `arrays_and_functions`,
`structs_enums_for`, `loop_break_continue`, and any future
array-using example. Arrays effectively boot at all-zero.
2. `draw` inside a loop body reuses one static OAM slot —
`next_oam_slot` increments at IR-codegen time rather than at
runtime, so N iterations all write to the same 4-byte OAM
entry. Affects `arrays_and_functions`, `structs_enums_for`,
`bitwise_ops` (health pips), and any loop that wants to
render per-iteration sprites.
Both bugs are latent and didn't surface until I tried to write
examples that exercise the relevant features — the existing
integration tests only check iNES header structure, and the
jsnes smoke test's "at least one sprite rendered" bar is
satisfied by one sprite even when several were intended.
https://claude.ai/code/session_014Z5y3Q9krLcAxYpZQJhZ5V
2026-04-12 19:05:18 +00:00
|
|
|
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
|