1
0
Fork 0
mirror of https://github.com/imjasonh/nescript synced 2026-07-08 08:55:38 +00:00
nescript/examples/war/deck.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

172 lines
4.9 KiB
Text

// war/deck.ne — queue operations on the three u8[52] buffers.
//
// Each of deck_a, deck_b, and pot is a 52-entry circular buffer
// storing packed rank/suit bytes. The front index is the next
// card to draw; the count is the number of cards currently in
// the buffer. Everything wraps mod 52 — since 52 isn't a power
// of two, we use `if idx >= 52 { idx -= 52 }` instead of `%` to
// avoid the expensive software mod routine.
// ── Helpers shared by every deck ──────────────────────────
//
// Wrap a (front + count) sum back into the 0..51 range. Only
// needed inside push_back because `front` and `count` are both
// ≤ 51 by construction, so the sum is at most 102 and one
// subtraction is enough.
//
// Not marked `inline`: the conditional early return is one of
// the body shapes the inliner declines (W0110), and living
// with the JSR is cheaper than rewriting this as `v % DECK_SIZE`
// (which would pull in the software modulo routine).
fun wrap52(v: u8) -> u8 {
if v >= DECK_SIZE {
return v - DECK_SIZE
}
return v
}
// ── deck_a ────────────────────────────────────────────────
fun deck_a_empty() -> u8 {
if deck_a_count == 0 {
return 1
}
return 0
}
fun draw_front_a() -> u8 {
var card: u8 = deck_a[deck_a_front]
deck_a_front = wrap52(deck_a_front + 1)
deck_a_count -= 1
return card
}
fun push_back_a(card: u8) {
var slot: u8 = wrap52(deck_a_front + deck_a_count)
deck_a[slot] = card
deck_a_count += 1
}
// ── deck_b ────────────────────────────────────────────────
fun deck_b_empty() -> u8 {
if deck_b_count == 0 {
return 1
}
return 0
}
fun draw_front_b() -> u8 {
var card: u8 = deck_b[deck_b_front]
deck_b_front = wrap52(deck_b_front + 1)
deck_b_count -= 1
return card
}
fun push_back_b(card: u8) {
var slot: u8 = wrap52(deck_b_front + deck_b_count)
deck_b[slot] = card
deck_b_count += 1
}
// ── pot ───────────────────────────────────────────────────
fun push_back_pot(card: u8) {
pot[pot_count] = card
pot_count += 1
}
fun clear_pot() {
pot_count = 0
}
// Transfer every card currently in the pot into deck_a, in FIFO
// order (so the face-up and face-down cards layer naturally).
fun pot_to_a() {
var i: u8 = 0
while i < pot_count {
push_back_a(pot[i])
i += 1
}
pot_count = 0
}
fun pot_to_b() {
var i: u8 = 0
while i < pot_count {
push_back_b(pot[i])
i += 1
}
pot_count = 0
}
// ── Init + shuffle ────────────────────────────────────────
//
// Build a 52-card "master deck" in deck_a's backing array using
// a rank-major loop: for each rank 1..13, each suit 0..3, write
// the packed byte (rank << 4) | suit into deck_a[i]. Then
// bounded-random-swap-shuffle it. Finally, split the first 26
// into deck_b (copied), leaving the second 26 in deck_a's first
// half, and reset both queues' cursors.
//
// The random-swap shuffle is a bounded alternative to
// Fisher-Yates: it does 200 swaps between two random indices,
// where each index is rand() & 0x3F (0..63) and the swap is
// only done when both indices are < 52. Uses only bitwise ops
// (no multiply, no divide).
fun build_master_deck() {
var r: u8 = 1
var i: u8 = 0
while r <= RANK_KING {
var s: u8 = 0
while s < 4 {
// Pack rank into the high nibble, suit into the low.
// rank fits in 4 bits (max 13) and suit fits in 2
// bits, so the shift-and-or is exact.
var packed: u8 = r << 4
deck_a[i] = packed | s
i += 1
s += 1
}
r += 1
}
}
fun shuffle_deck_a() {
var k: u8 = 0
while k < 200 {
var i: u8 = rand_u8() & 0x3F
var j: u8 = rand_u8() & 0x3F
if i < DECK_SIZE {
if j < DECK_SIZE {
var tmp: u8 = deck_a[i]
deck_a[i] = deck_a[j]
deck_a[j] = tmp
}
}
k += 1
}
}
// After shuffle, split deck_a's 52 cards into two halves: the
// first 26 stay in deck_a, the second 26 move into deck_b.
// Reset both queues' front/count cursors in the process.
fun split_decks() {
var i: u8 = 0
while i < HALF_DECK {
deck_b[i] = deck_a[HALF_DECK + i]
i += 1
}
deck_a_front = 0
deck_a_count = HALF_DECK
deck_b_front = 0
deck_b_count = HALF_DECK
pot_count = 0
}
fun init_and_shuffle_decks() {
build_master_deck()
shuffle_deck_a()
split_decks()
}