mirror of
https://github.com/imjasonh/nescript
synced 2026-07-08 08:55:38 +00:00
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
172 lines
4.9 KiB
Text
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()
|
|
}
|