1
0
Fork 0
mirror of https://github.com/imjasonh/nescript synced 2026-07-08 08:55:38 +00:00
nescript/docs/future-work.md

258 lines
12 KiB
Markdown
Raw Normal View History

# Future Work
cleanup: fix silent miscompiles and delete dead code exposed by code review Two correctness bugs were silently producing wrong ROMs: - `x << n` / `x >> n` always shifted by 1, regardless of `n`, because the IR lowering for `BinOp::ShiftLeft`/`ShiftRight` hardcoded the count. Now eval_const the RHS into a compile-time count; fall back to a new `IrOp::ShiftLeftVar` / `ShiftRightVar` (runtime loop) when the amount isn't constant. Strength reduction folds the variable form back to a fixed count once the optimizer knows the value. - `x / n` / `x % n` always returned 0, because the lowering emitted `LoadImm(t, 0)` for `BinOp::Div`/`Mod` with a comment saying the runtime call was "TODO for now". Added real `IrOp::Div` and `IrOp::Mod`, wired them through use-counting and DCE, gave codegen `__divide`-based implementations, and taught strength reduction to rewrite power-of-two divisors into shifts and modulo-by-2ⁿ into AND masks. Constant folding now handles `Mul`/`Div`/`Mod`/shifts too, which were previously left for the codegen to emit inefficient software calls. Dead code removed (no backward-compat shims kept): - `src/debug/` entirely. `DebugSymbols`, `SourceMap`, and the Mesen/.sym emitters had no callers outside their own tests; `main.rs` never wrote a symbol file. Documented the intent in `docs/future-work.md` so it comes back intentionally if needed. - `ErrorCode::E0202` (invalid cast) and `E0403` (unreachable state): defined, formatted, and marked `#[allow(dead_code)]` but never emitted. W0104 now carries the unreachable-state semantics too. - `Level::Info`: never constructed. - `load_background` / `set_palette` statements and their `BackgroundDecl` / `PaletteDecl` parser support: parsed and silently dropped by IR lowering (`// TODO: implement in asset pipeline`). Removed keywords, AST variants, parser paths, analyzer arms, and tests. `docs/future-work.md` documents the runtime palette/nametable design for when it comes back. Doc cleanup: - `docs/architecture.md` was describing files that don't exist (`analyzer/types.rs`, `optimizer/const_fold.rs`, `codegen/regalloc.rs`, `rom/header.rs`, `debug/symbols.rs`, …). Rewrote it to match the real flat `mod.rs` + `tests.rs` layout and the real pipeline order. - `docs/future-work.md` was a hybrid of open work and "recently completed" entries that duplicated the active stubs at the top of the file. Collapsed to just the gaps that are actually still open. - `README.md` claimed Mesen symbol export and 210 tests; updated both. - `docs/language-guide.md` and `spec.md` described `palette` decls, `set_palette` / `load_background`, `debug.overlay`, and error codes that were never emitted. Trimmed. - Stale comments on `Statement::Play`/`StartMusic`/`StopMusic` claimed the audio subsystem was "a no-op at codegen time". Tests: - Regression tests for every fix above (`lower_shift_left_with_literal _count_uses_that_count`, `lower_shift_right_with_variable_count _uses_runtime_variant`, `lower_divide_emits_div_op_not_load_imm _zero`, `lower_modulo_emits_mod_op_not_load_imm_zero`, `strength_reduce_div_by_power_of_two`, `strength_reduce_mod_by _power_of_two`, `strength_reduce_shift_var_with_constant_amount`). - Renamed the `program_with_sprites_and_palette` integration test (which was exercising the now-removed `load_background`/`set_palette`) to `program_with_inline_sprite_chr`. `examples/sprites_and_palettes.ne` lost its `palette`/`set_palette` usage. Nothing in the emulator test presses A, so the headless jsnes render shouldn't move, but the golden may need regeneration via `UPDATE_GOLDENS=1` if it does. https://claude.ai/code/session_012fKB251HvEUQwG3tizFyqt
2026-04-13 02:47:37 +00:00
This document tracks the gaps between what NEScript currently compiles and
what the spec describes. Items are grouped by area. Anything implemented and
tested is omitted — `git log` is the authoritative record of what shipped.
---
## PNG-sourced assets
palette/background: first-class declarations with reset-time load and runtime swaps Re-adds `palette Name { colors: [...] }` and `background Name { tiles: [...], attributes: [...] }` as first-class declarations, plus `set_palette Name` and `load_background Name` statements for runtime swaps. Unlike the previous iteration that quietly no-op'd, this one is fully wired through the pipeline and its behavior is pinned by both unit tests and an emulator golden. Pipeline: - Lexer: re-adds `palette`, `background`, `set_palette`, `load_background` keywords and tokenizes them. - AST: `PaletteDecl` (name + 1..=32 colour bytes) and `BackgroundDecl` (name + 0..=960 tile bytes + 0..=64 attribute bytes) live in `Program`. `Statement::SetPalette` and `Statement::LoadBackground` name-reference these declarations. - Parser: `palette Name { colors: [...] }` / `background Name { tiles: [...], attributes: [...] }` blocks and their statement forms parse via the existing byte-array helper. - Analyzer: validates colour indices ($00-$3F), palette length (<=32), nametable length (<=960), attribute length (<=64), and duplicate decl names. `set_palette` / `load_background` targets must reference a declared name (E0502 otherwise). When a program declares palette or background, the analyzer bumps the user zero-page allocator's starting address from `$10` to `$18` to reserve `$11-$17` for the runtime update handshake — programs that don't use the feature keep the old layout so their emulator goldens stay byte-exact. - Assets: `PaletteData` and `BackgroundData` resolve declarations into zero-padded fixed-size blobs (32 / 960 / 64 bytes) and expose `label()` / `tiles_label()` / `attrs_label()` for codegen to reference. - IR: new `IrOp::SetPalette(String)` and `IrOp::LoadBackground(String)`; lowering forwards the names verbatim. - Codegen: `gen_set_palette` writes the palette label pointer into ZP `$12/$13` and ORs bit 0 into the update flags at `$11`; `gen_load_background` does the same for tile/attribute pointers at `$14/$15/$16/$17` with bit 1. Both emit a `__ppu_update_used` marker so the linker splices in the NMI apply helper only when the feature is actually used. - Runtime: `gen_initial_palette_load` and `gen_initial_background_load` write the first declared palette/background at reset time (before rendering is enabled, where PPU writes are safe). `gen_nmi(has_ppu_updates)` takes a new flag; when true it splices `gen_ppu_update_apply` at the top of the NMI body, which checks the `$11` flags byte and copies pending palette / nametable data to `$3F00` / `$2000` inside vblank. All helpers use only ZP $02/$03 as scratch at reset time and never clobber ZP slots live across NMI. - Linker: new `link_banked_with_ppu` takes slice of `PaletteData` / `BackgroundData`; splices each blob as a labelled data block in PRG ROM, picks the first-declared as the reset-time load target, enables background rendering automatically when a background is declared, and threads `has_ppu_updates` into `gen_nmi`. Old `link_banked` remains as a thin wrapper for callers without palette/background data so existing tests don't shift. Tests: - Lexer: tokenization of the 4 new keywords (single added test case). - Parser: 5 new tests for `palette` / `background` decls with and without attributes, plus `set_palette` / `load_background` statements. - Analyzer: 9 new tests covering acceptance of declared palettes/backgrounds, E0502 for unknown names, E0201 for out-of-range NES colors and oversized blobs, E0501 for duplicate names, and the zero-page-layout guard (palette/bg decls bump ZP start; no decls keeps it at $10). - Resolver: 3 new tests for zero-padding, truncation of oversized decls, and label derivation. - IR: 2 new lowering tests for `set_palette` and `load_background`. - Integration: 5 new tests — blob contents spliced verbatim into PRG, `STA $12` / `STA $14` emitted by set_palette / load_background codegen, and a regression guard that programs without palette/background still land user vars at $10. - Emulator: new `examples/palette_and_background.ne` driven by a frame counter that toggles between `CoolBlues` / `WarmReds` and `TitleScreen` / `StageOne` every 90 frames. Golden PNG and audio hash checked in under `tests/emulator/goldens/` and verified via `node run_examples.mjs` — rendered image shows the blue `CoolBlues` palette with the nametable populated from `TitleScreen`. Docs: - `README.md` adds the feature to the headline list and the example table. - `docs/language-guide.md` restores the palette/background sections with the full 32-byte layout table and `set_palette` / `load_background` statement references. - `docs/future-work.md` replaces the "removed as dead code" entry with the remaining gaps (PNG-sourced palette and nametable assets, cross-vblank large background updates, memory-map reporting). - `spec.md` restores the grammar productions and usage examples. - `examples/README.md` lists the new demo. All 497 unit + integration tests pass. Clippy clean. All 21 emulator goldens match after the update pass. https://claude.ai/code/session_012fKB251HvEUQwG3tizFyqt
2026-04-13 11:11:33 +00:00
**What ships today.** `palette Name { colors: [...] }` and
`background Name { tiles: [...], attributes: [...] }` declarations with
inline byte arrays, plus `palette Name @palette("file.png")` and
`background Name @nametable("file.png")` for PNG-sourced variants.
The palette path maps each pixel to its nearest NES master-palette index
(via `nearest_nes_color()` in `src/assets/palette.rs`), deduplicates, and
emits the 32-byte blob; the nametable path slices a 256×240 PNG into the
32×30 tile grid, deduplicates (max 256 unique tiles), and emits the 960+64
byte nametable/attribute blobs. The nametable path now **also auto-generates
the per-tile CHR data** via `png_to_nametable_with_chr` and slots it into
CHR ROM after the user's sprite tile range — see
`examples/auto_chr_background.ne` for the end-to-end flow.
`--memory-map` reports per-blob PRG ROM addresses and a running total
alongside the variable layout.
palette/background: first-class declarations with reset-time load and runtime swaps Re-adds `palette Name { colors: [...] }` and `background Name { tiles: [...], attributes: [...] }` as first-class declarations, plus `set_palette Name` and `load_background Name` statements for runtime swaps. Unlike the previous iteration that quietly no-op'd, this one is fully wired through the pipeline and its behavior is pinned by both unit tests and an emulator golden. Pipeline: - Lexer: re-adds `palette`, `background`, `set_palette`, `load_background` keywords and tokenizes them. - AST: `PaletteDecl` (name + 1..=32 colour bytes) and `BackgroundDecl` (name + 0..=960 tile bytes + 0..=64 attribute bytes) live in `Program`. `Statement::SetPalette` and `Statement::LoadBackground` name-reference these declarations. - Parser: `palette Name { colors: [...] }` / `background Name { tiles: [...], attributes: [...] }` blocks and their statement forms parse via the existing byte-array helper. - Analyzer: validates colour indices ($00-$3F), palette length (<=32), nametable length (<=960), attribute length (<=64), and duplicate decl names. `set_palette` / `load_background` targets must reference a declared name (E0502 otherwise). When a program declares palette or background, the analyzer bumps the user zero-page allocator's starting address from `$10` to `$18` to reserve `$11-$17` for the runtime update handshake — programs that don't use the feature keep the old layout so their emulator goldens stay byte-exact. - Assets: `PaletteData` and `BackgroundData` resolve declarations into zero-padded fixed-size blobs (32 / 960 / 64 bytes) and expose `label()` / `tiles_label()` / `attrs_label()` for codegen to reference. - IR: new `IrOp::SetPalette(String)` and `IrOp::LoadBackground(String)`; lowering forwards the names verbatim. - Codegen: `gen_set_palette` writes the palette label pointer into ZP `$12/$13` and ORs bit 0 into the update flags at `$11`; `gen_load_background` does the same for tile/attribute pointers at `$14/$15/$16/$17` with bit 1. Both emit a `__ppu_update_used` marker so the linker splices in the NMI apply helper only when the feature is actually used. - Runtime: `gen_initial_palette_load` and `gen_initial_background_load` write the first declared palette/background at reset time (before rendering is enabled, where PPU writes are safe). `gen_nmi(has_ppu_updates)` takes a new flag; when true it splices `gen_ppu_update_apply` at the top of the NMI body, which checks the `$11` flags byte and copies pending palette / nametable data to `$3F00` / `$2000` inside vblank. All helpers use only ZP $02/$03 as scratch at reset time and never clobber ZP slots live across NMI. - Linker: new `link_banked_with_ppu` takes slice of `PaletteData` / `BackgroundData`; splices each blob as a labelled data block in PRG ROM, picks the first-declared as the reset-time load target, enables background rendering automatically when a background is declared, and threads `has_ppu_updates` into `gen_nmi`. Old `link_banked` remains as a thin wrapper for callers without palette/background data so existing tests don't shift. Tests: - Lexer: tokenization of the 4 new keywords (single added test case). - Parser: 5 new tests for `palette` / `background` decls with and without attributes, plus `set_palette` / `load_background` statements. - Analyzer: 9 new tests covering acceptance of declared palettes/backgrounds, E0502 for unknown names, E0201 for out-of-range NES colors and oversized blobs, E0501 for duplicate names, and the zero-page-layout guard (palette/bg decls bump ZP start; no decls keeps it at $10). - Resolver: 3 new tests for zero-padding, truncation of oversized decls, and label derivation. - IR: 2 new lowering tests for `set_palette` and `load_background`. - Integration: 5 new tests — blob contents spliced verbatim into PRG, `STA $12` / `STA $14` emitted by set_palette / load_background codegen, and a regression guard that programs without palette/background still land user vars at $10. - Emulator: new `examples/palette_and_background.ne` driven by a frame counter that toggles between `CoolBlues` / `WarmReds` and `TitleScreen` / `StageOne` every 90 frames. Golden PNG and audio hash checked in under `tests/emulator/goldens/` and verified via `node run_examples.mjs` — rendered image shows the blue `CoolBlues` palette with the nametable populated from `TitleScreen`. Docs: - `README.md` adds the feature to the headline list and the example table. - `docs/language-guide.md` restores the palette/background sections with the full 32-byte layout table and `set_palette` / `load_background` statement references. - `docs/future-work.md` replaces the "removed as dead code" entry with the remaining gaps (PNG-sourced palette and nametable assets, cross-vblank large background updates, memory-map reporting). - `spec.md` restores the grammar productions and usage examples. - `examples/README.md` lists the new demo. All 497 unit + integration tests pass. Clippy clean. All 21 emulator goldens match after the update pass. https://claude.ai/code/session_012fKB251HvEUQwG3tizFyqt
2026-04-13 11:11:33 +00:00
**Still TODO.**
- **Per-state background rendering control** — programs currently load a
single nametable at reset. Per-state swaps work but are limited by the
NMI-time write budget (~2273 cycles, enough for a palette but not a
full 1024-byte nametable).
- **Per-quadrant palette selection from PNG sources** — the
`png_to_nametable_with_chr` attribute path picks sub-palettes based on
brightness buckets, which is fine for grayscale demos but doesn't let
the user say "this 32×32 tile uses sub-palette 2". A separate
`palette_map:` shortcut exists for inline backgrounds; the PNG path
could grow a sibling `@palette_map("hint.png")` that overrides the
brightness buckets.
---
cleanup: fix silent miscompiles and delete dead code exposed by code review Two correctness bugs were silently producing wrong ROMs: - `x << n` / `x >> n` always shifted by 1, regardless of `n`, because the IR lowering for `BinOp::ShiftLeft`/`ShiftRight` hardcoded the count. Now eval_const the RHS into a compile-time count; fall back to a new `IrOp::ShiftLeftVar` / `ShiftRightVar` (runtime loop) when the amount isn't constant. Strength reduction folds the variable form back to a fixed count once the optimizer knows the value. - `x / n` / `x % n` always returned 0, because the lowering emitted `LoadImm(t, 0)` for `BinOp::Div`/`Mod` with a comment saying the runtime call was "TODO for now". Added real `IrOp::Div` and `IrOp::Mod`, wired them through use-counting and DCE, gave codegen `__divide`-based implementations, and taught strength reduction to rewrite power-of-two divisors into shifts and modulo-by-2ⁿ into AND masks. Constant folding now handles `Mul`/`Div`/`Mod`/shifts too, which were previously left for the codegen to emit inefficient software calls. Dead code removed (no backward-compat shims kept): - `src/debug/` entirely. `DebugSymbols`, `SourceMap`, and the Mesen/.sym emitters had no callers outside their own tests; `main.rs` never wrote a symbol file. Documented the intent in `docs/future-work.md` so it comes back intentionally if needed. - `ErrorCode::E0202` (invalid cast) and `E0403` (unreachable state): defined, formatted, and marked `#[allow(dead_code)]` but never emitted. W0104 now carries the unreachable-state semantics too. - `Level::Info`: never constructed. - `load_background` / `set_palette` statements and their `BackgroundDecl` / `PaletteDecl` parser support: parsed and silently dropped by IR lowering (`// TODO: implement in asset pipeline`). Removed keywords, AST variants, parser paths, analyzer arms, and tests. `docs/future-work.md` documents the runtime palette/nametable design for when it comes back. Doc cleanup: - `docs/architecture.md` was describing files that don't exist (`analyzer/types.rs`, `optimizer/const_fold.rs`, `codegen/regalloc.rs`, `rom/header.rs`, `debug/symbols.rs`, …). Rewrote it to match the real flat `mod.rs` + `tests.rs` layout and the real pipeline order. - `docs/future-work.md` was a hybrid of open work and "recently completed" entries that duplicated the active stubs at the top of the file. Collapsed to just the gaps that are actually still open. - `README.md` claimed Mesen symbol export and 210 tests; updated both. - `docs/language-guide.md` and `spec.md` described `palette` decls, `set_palette` / `load_background`, `debug.overlay`, and error codes that were never emitted. Trimmed. - Stale comments on `Statement::Play`/`StartMusic`/`StopMusic` claimed the audio subsystem was "a no-op at codegen time". Tests: - Regression tests for every fix above (`lower_shift_left_with_literal _count_uses_that_count`, `lower_shift_right_with_variable_count _uses_runtime_variant`, `lower_divide_emits_div_op_not_load_imm _zero`, `lower_modulo_emits_mod_op_not_load_imm_zero`, `strength_reduce_div_by_power_of_two`, `strength_reduce_mod_by _power_of_two`, `strength_reduce_shift_var_with_constant_amount`). - Renamed the `program_with_sprites_and_palette` integration test (which was exercising the now-removed `load_background`/`set_palette`) to `program_with_inline_sprite_chr`. `examples/sprites_and_palettes.ne` lost its `palette`/`set_palette` usage. Nothing in the emulator test presses A, so the headless jsnes render shouldn't move, but the golden may need regeneration via `UPDATE_GOLDENS=1` if it does. https://claude.ai/code/session_012fKB251HvEUQwG3tizFyqt
2026-04-13 02:47:37 +00:00
## User code distribution across switchable banks
**What ships today.** `bank Foo { fun bar() { ... } }` nesting places user
functions into a specific switchable bank. The codegen emits per-bank
instruction streams; the linker runs a two-pass assembly (discover labels
per-bank, then resolve with the merged label table) so banked code can
still reference fixed-bank symbols. Cross-bank calls — both fixed → banked
*and* banked → banked — are rewritten to `JSR __tramp_<name>`, where each
trampoline is a per-function stub in the fixed bank that reads the
caller's current bank from `ZP_BANK_CURRENT`, pushes it on the hardware
stack, switches to the target, JSRs the entry, then pulls and restores
the caller's bank. `gen_mapper_init` seeds `ZP_BANK_CURRENT` with the
fixed bank index at reset so the first cross-bank call from the fixed
bank still leaves the fixed bank mapped at $8000. See
`examples/uxrom_user_banked.ne` (fixed → banked) and
`examples/uxrom_banked_to_banked.ne` (banked → banked).
**Still TODO.**
- **Greedy size-packing.** Placement is explicit-only today — there is no
pass that takes a program with too much fixed-bank code and
automatically spills the biggest leaf functions to declared empty banks.
- **MMC3 per-state-handler split** — the `mmc3_per_state_split.ne`
example still uses the legacy fixed-bank placement for its handlers.
Extending the banked-fun syntax to state handlers (plus trampoline
emission on handler dispatch) would unify the two paths. The blocker
isn't the trampoline — those work for any caller now — but the
state-handler dispatcher in the IR codegen needs to learn that
state handlers can live in a switchable bank, and to JSR through a
trampoline whose entry is the handler label.
---
cleanup: fix silent miscompiles and delete dead code exposed by code review Two correctness bugs were silently producing wrong ROMs: - `x << n` / `x >> n` always shifted by 1, regardless of `n`, because the IR lowering for `BinOp::ShiftLeft`/`ShiftRight` hardcoded the count. Now eval_const the RHS into a compile-time count; fall back to a new `IrOp::ShiftLeftVar` / `ShiftRightVar` (runtime loop) when the amount isn't constant. Strength reduction folds the variable form back to a fixed count once the optimizer knows the value. - `x / n` / `x % n` always returned 0, because the lowering emitted `LoadImm(t, 0)` for `BinOp::Div`/`Mod` with a comment saying the runtime call was "TODO for now". Added real `IrOp::Div` and `IrOp::Mod`, wired them through use-counting and DCE, gave codegen `__divide`-based implementations, and taught strength reduction to rewrite power-of-two divisors into shifts and modulo-by-2ⁿ into AND masks. Constant folding now handles `Mul`/`Div`/`Mod`/shifts too, which were previously left for the codegen to emit inefficient software calls. Dead code removed (no backward-compat shims kept): - `src/debug/` entirely. `DebugSymbols`, `SourceMap`, and the Mesen/.sym emitters had no callers outside their own tests; `main.rs` never wrote a symbol file. Documented the intent in `docs/future-work.md` so it comes back intentionally if needed. - `ErrorCode::E0202` (invalid cast) and `E0403` (unreachable state): defined, formatted, and marked `#[allow(dead_code)]` but never emitted. W0104 now carries the unreachable-state semantics too. - `Level::Info`: never constructed. - `load_background` / `set_palette` statements and their `BackgroundDecl` / `PaletteDecl` parser support: parsed and silently dropped by IR lowering (`// TODO: implement in asset pipeline`). Removed keywords, AST variants, parser paths, analyzer arms, and tests. `docs/future-work.md` documents the runtime palette/nametable design for when it comes back. Doc cleanup: - `docs/architecture.md` was describing files that don't exist (`analyzer/types.rs`, `optimizer/const_fold.rs`, `codegen/regalloc.rs`, `rom/header.rs`, `debug/symbols.rs`, …). Rewrote it to match the real flat `mod.rs` + `tests.rs` layout and the real pipeline order. - `docs/future-work.md` was a hybrid of open work and "recently completed" entries that duplicated the active stubs at the top of the file. Collapsed to just the gaps that are actually still open. - `README.md` claimed Mesen symbol export and 210 tests; updated both. - `docs/language-guide.md` and `spec.md` described `palette` decls, `set_palette` / `load_background`, `debug.overlay`, and error codes that were never emitted. Trimmed. - Stale comments on `Statement::Play`/`StartMusic`/`StopMusic` claimed the audio subsystem was "a no-op at codegen time". Tests: - Regression tests for every fix above (`lower_shift_left_with_literal _count_uses_that_count`, `lower_shift_right_with_variable_count _uses_runtime_variant`, `lower_divide_emits_div_op_not_load_imm _zero`, `lower_modulo_emits_mod_op_not_load_imm_zero`, `strength_reduce_div_by_power_of_two`, `strength_reduce_mod_by _power_of_two`, `strength_reduce_shift_var_with_constant_amount`). - Renamed the `program_with_sprites_and_palette` integration test (which was exercising the now-removed `load_background`/`set_palette`) to `program_with_inline_sprite_chr`. `examples/sprites_and_palettes.ne` lost its `palette`/`set_palette` usage. Nothing in the emulator test presses A, so the headless jsnes render shouldn't move, but the golden may need regeneration via `UPDATE_GOLDENS=1` if it does. https://claude.ai/code/session_012fKB251HvEUQwG3tizFyqt
2026-04-13 02:47:37 +00:00
## Language feature gaps (post-v0.1)
cleanup: fix silent miscompiles and delete dead code exposed by code review Two correctness bugs were silently producing wrong ROMs: - `x << n` / `x >> n` always shifted by 1, regardless of `n`, because the IR lowering for `BinOp::ShiftLeft`/`ShiftRight` hardcoded the count. Now eval_const the RHS into a compile-time count; fall back to a new `IrOp::ShiftLeftVar` / `ShiftRightVar` (runtime loop) when the amount isn't constant. Strength reduction folds the variable form back to a fixed count once the optimizer knows the value. - `x / n` / `x % n` always returned 0, because the lowering emitted `LoadImm(t, 0)` for `BinOp::Div`/`Mod` with a comment saying the runtime call was "TODO for now". Added real `IrOp::Div` and `IrOp::Mod`, wired them through use-counting and DCE, gave codegen `__divide`-based implementations, and taught strength reduction to rewrite power-of-two divisors into shifts and modulo-by-2ⁿ into AND masks. Constant folding now handles `Mul`/`Div`/`Mod`/shifts too, which were previously left for the codegen to emit inefficient software calls. Dead code removed (no backward-compat shims kept): - `src/debug/` entirely. `DebugSymbols`, `SourceMap`, and the Mesen/.sym emitters had no callers outside their own tests; `main.rs` never wrote a symbol file. Documented the intent in `docs/future-work.md` so it comes back intentionally if needed. - `ErrorCode::E0202` (invalid cast) and `E0403` (unreachable state): defined, formatted, and marked `#[allow(dead_code)]` but never emitted. W0104 now carries the unreachable-state semantics too. - `Level::Info`: never constructed. - `load_background` / `set_palette` statements and their `BackgroundDecl` / `PaletteDecl` parser support: parsed and silently dropped by IR lowering (`// TODO: implement in asset pipeline`). Removed keywords, AST variants, parser paths, analyzer arms, and tests. `docs/future-work.md` documents the runtime palette/nametable design for when it comes back. Doc cleanup: - `docs/architecture.md` was describing files that don't exist (`analyzer/types.rs`, `optimizer/const_fold.rs`, `codegen/regalloc.rs`, `rom/header.rs`, `debug/symbols.rs`, …). Rewrote it to match the real flat `mod.rs` + `tests.rs` layout and the real pipeline order. - `docs/future-work.md` was a hybrid of open work and "recently completed" entries that duplicated the active stubs at the top of the file. Collapsed to just the gaps that are actually still open. - `README.md` claimed Mesen symbol export and 210 tests; updated both. - `docs/language-guide.md` and `spec.md` described `palette` decls, `set_palette` / `load_background`, `debug.overlay`, and error codes that were never emitted. Trimmed. - Stale comments on `Statement::Play`/`StartMusic`/`StopMusic` claimed the audio subsystem was "a no-op at codegen time". Tests: - Regression tests for every fix above (`lower_shift_left_with_literal _count_uses_that_count`, `lower_shift_right_with_variable_count _uses_runtime_variant`, `lower_divide_emits_div_op_not_load_imm _zero`, `lower_modulo_emits_mod_op_not_load_imm_zero`, `strength_reduce_div_by_power_of_two`, `strength_reduce_mod_by _power_of_two`, `strength_reduce_shift_var_with_constant_amount`). - Renamed the `program_with_sprites_and_palette` integration test (which was exercising the now-removed `load_background`/`set_palette`) to `program_with_inline_sprite_chr`. `examples/sprites_and_palettes.ne` lost its `palette`/`set_palette` usage. Nothing in the emulator test presses A, so the headless jsnes render shouldn't move, but the golden may need regeneration via `UPDATE_GOLDENS=1` if it does. https://claude.ai/code/session_012fKB251HvEUQwG3tizFyqt
2026-04-13 02:47:37 +00:00
From the spec's "Reserved for Future Versions" section:
cleanup: fix silent miscompiles and delete dead code exposed by code review Two correctness bugs were silently producing wrong ROMs: - `x << n` / `x >> n` always shifted by 1, regardless of `n`, because the IR lowering for `BinOp::ShiftLeft`/`ShiftRight` hardcoded the count. Now eval_const the RHS into a compile-time count; fall back to a new `IrOp::ShiftLeftVar` / `ShiftRightVar` (runtime loop) when the amount isn't constant. Strength reduction folds the variable form back to a fixed count once the optimizer knows the value. - `x / n` / `x % n` always returned 0, because the lowering emitted `LoadImm(t, 0)` for `BinOp::Div`/`Mod` with a comment saying the runtime call was "TODO for now". Added real `IrOp::Div` and `IrOp::Mod`, wired them through use-counting and DCE, gave codegen `__divide`-based implementations, and taught strength reduction to rewrite power-of-two divisors into shifts and modulo-by-2ⁿ into AND masks. Constant folding now handles `Mul`/`Div`/`Mod`/shifts too, which were previously left for the codegen to emit inefficient software calls. Dead code removed (no backward-compat shims kept): - `src/debug/` entirely. `DebugSymbols`, `SourceMap`, and the Mesen/.sym emitters had no callers outside their own tests; `main.rs` never wrote a symbol file. Documented the intent in `docs/future-work.md` so it comes back intentionally if needed. - `ErrorCode::E0202` (invalid cast) and `E0403` (unreachable state): defined, formatted, and marked `#[allow(dead_code)]` but never emitted. W0104 now carries the unreachable-state semantics too. - `Level::Info`: never constructed. - `load_background` / `set_palette` statements and their `BackgroundDecl` / `PaletteDecl` parser support: parsed and silently dropped by IR lowering (`// TODO: implement in asset pipeline`). Removed keywords, AST variants, parser paths, analyzer arms, and tests. `docs/future-work.md` documents the runtime palette/nametable design for when it comes back. Doc cleanup: - `docs/architecture.md` was describing files that don't exist (`analyzer/types.rs`, `optimizer/const_fold.rs`, `codegen/regalloc.rs`, `rom/header.rs`, `debug/symbols.rs`, …). Rewrote it to match the real flat `mod.rs` + `tests.rs` layout and the real pipeline order. - `docs/future-work.md` was a hybrid of open work and "recently completed" entries that duplicated the active stubs at the top of the file. Collapsed to just the gaps that are actually still open. - `README.md` claimed Mesen symbol export and 210 tests; updated both. - `docs/language-guide.md` and `spec.md` described `palette` decls, `set_palette` / `load_background`, `debug.overlay`, and error codes that were never emitted. Trimmed. - Stale comments on `Statement::Play`/`StartMusic`/`StopMusic` claimed the audio subsystem was "a no-op at codegen time". Tests: - Regression tests for every fix above (`lower_shift_left_with_literal _count_uses_that_count`, `lower_shift_right_with_variable_count _uses_runtime_variant`, `lower_divide_emits_div_op_not_load_imm _zero`, `lower_modulo_emits_mod_op_not_load_imm_zero`, `strength_reduce_div_by_power_of_two`, `strength_reduce_mod_by _power_of_two`, `strength_reduce_shift_var_with_constant_amount`). - Renamed the `program_with_sprites_and_palette` integration test (which was exercising the now-removed `load_background`/`set_palette`) to `program_with_inline_sprite_chr`. `examples/sprites_and_palettes.ne` lost its `palette`/`set_palette` usage. Nothing in the emulator test presses A, so the headless jsnes render shouldn't move, but the golden may need regeneration via `UPDATE_GOLDENS=1` if it does. https://claude.ai/code/session_012fKB251HvEUQwG3tizFyqt
2026-04-13 02:47:37 +00:00
| Feature | Description |
|--------------------|-----------------------------------------------------------------------|
| **Fixed-point** | `fixed8.8` type for sub-pixel movement with operator support. |
| **Text / HUD** | Font sheet declarations + layout system for scores, health, menus. |
| **Tilemaps** | Declarative level data with built-in collision queries. |
| **SRAM / saves** | Persistent storage declarations for battery-backed save data. |
NES 2.0 headers are now supported via `game Foo { header: nes2 }` — see
`src/rom/mod.rs`.
**Metasprites** are now supported via `metasprite Name { sprite: ...,
dx: [...], dy: [...], frame: [...] }` — see `examples/metasprite_demo.ne`.
The IR lowering expands `draw Hero at: (x, y)` into one `DrawSprite` op
per tile, with each tile's frame index offset by the underlying sprite's
base tile so the codegen sees a stream of regular draws and the OAM
cursor allocator picks them up unchanged. Negative offsets and
runtime-varying tile selection are still TODO — the current form takes
literal `u8` offsets.
cleanup: fix silent miscompiles and delete dead code exposed by code review Two correctness bugs were silently producing wrong ROMs: - `x << n` / `x >> n` always shifted by 1, regardless of `n`, because the IR lowering for `BinOp::ShiftLeft`/`ShiftRight` hardcoded the count. Now eval_const the RHS into a compile-time count; fall back to a new `IrOp::ShiftLeftVar` / `ShiftRightVar` (runtime loop) when the amount isn't constant. Strength reduction folds the variable form back to a fixed count once the optimizer knows the value. - `x / n` / `x % n` always returned 0, because the lowering emitted `LoadImm(t, 0)` for `BinOp::Div`/`Mod` with a comment saying the runtime call was "TODO for now". Added real `IrOp::Div` and `IrOp::Mod`, wired them through use-counting and DCE, gave codegen `__divide`-based implementations, and taught strength reduction to rewrite power-of-two divisors into shifts and modulo-by-2ⁿ into AND masks. Constant folding now handles `Mul`/`Div`/`Mod`/shifts too, which were previously left for the codegen to emit inefficient software calls. Dead code removed (no backward-compat shims kept): - `src/debug/` entirely. `DebugSymbols`, `SourceMap`, and the Mesen/.sym emitters had no callers outside their own tests; `main.rs` never wrote a symbol file. Documented the intent in `docs/future-work.md` so it comes back intentionally if needed. - `ErrorCode::E0202` (invalid cast) and `E0403` (unreachable state): defined, formatted, and marked `#[allow(dead_code)]` but never emitted. W0104 now carries the unreachable-state semantics too. - `Level::Info`: never constructed. - `load_background` / `set_palette` statements and their `BackgroundDecl` / `PaletteDecl` parser support: parsed and silently dropped by IR lowering (`// TODO: implement in asset pipeline`). Removed keywords, AST variants, parser paths, analyzer arms, and tests. `docs/future-work.md` documents the runtime palette/nametable design for when it comes back. Doc cleanup: - `docs/architecture.md` was describing files that don't exist (`analyzer/types.rs`, `optimizer/const_fold.rs`, `codegen/regalloc.rs`, `rom/header.rs`, `debug/symbols.rs`, …). Rewrote it to match the real flat `mod.rs` + `tests.rs` layout and the real pipeline order. - `docs/future-work.md` was a hybrid of open work and "recently completed" entries that duplicated the active stubs at the top of the file. Collapsed to just the gaps that are actually still open. - `README.md` claimed Mesen symbol export and 210 tests; updated both. - `docs/language-guide.md` and `spec.md` described `palette` decls, `set_palette` / `load_background`, `debug.overlay`, and error codes that were never emitted. Trimmed. - Stale comments on `Statement::Play`/`StartMusic`/`StopMusic` claimed the audio subsystem was "a no-op at codegen time". Tests: - Regression tests for every fix above (`lower_shift_left_with_literal _count_uses_that_count`, `lower_shift_right_with_variable_count _uses_runtime_variant`, `lower_divide_emits_div_op_not_load_imm _zero`, `lower_modulo_emits_mod_op_not_load_imm_zero`, `strength_reduce_div_by_power_of_two`, `strength_reduce_mod_by _power_of_two`, `strength_reduce_shift_var_with_constant_amount`). - Renamed the `program_with_sprites_and_palette` integration test (which was exercising the now-removed `load_background`/`set_palette`) to `program_with_inline_sprite_chr`. `examples/sprites_and_palettes.ne` lost its `palette`/`set_palette` usage. Nothing in the emulator test presses A, so the headless jsnes render shouldn't move, but the golden may need regeneration via `UPDATE_GOLDENS=1` if it does. https://claude.ai/code/session_012fKB251HvEUQwG3tizFyqt
2026-04-13 02:47:37 +00:00
### Struct / array field widths
Nested struct fields (`hero.pos.x`) and array struct fields
(`hero.inv[i]`) now compile end-to-end. The analyzer recursively
flattens the struct layout into per-leaf synthetic variables (with
intermediate `Struct(...)` symbols for the dotted prefixes), and the
parser loops the dotted chain in `parse_primary` and
`parse_assign_or_call` so the existing `format!("{name}.{field}")`
synthetic-name model still works without IR changes. Array-of-structs
is still rejected with E0201 — the synthetic-variable model can't
index per-element struct layouts without further codegen work, see
`src/analyzer/mod.rs::register_struct`.
---
cleanup: fix silent miscompiles and delete dead code exposed by code review Two correctness bugs were silently producing wrong ROMs: - `x << n` / `x >> n` always shifted by 1, regardless of `n`, because the IR lowering for `BinOp::ShiftLeft`/`ShiftRight` hardcoded the count. Now eval_const the RHS into a compile-time count; fall back to a new `IrOp::ShiftLeftVar` / `ShiftRightVar` (runtime loop) when the amount isn't constant. Strength reduction folds the variable form back to a fixed count once the optimizer knows the value. - `x / n` / `x % n` always returned 0, because the lowering emitted `LoadImm(t, 0)` for `BinOp::Div`/`Mod` with a comment saying the runtime call was "TODO for now". Added real `IrOp::Div` and `IrOp::Mod`, wired them through use-counting and DCE, gave codegen `__divide`-based implementations, and taught strength reduction to rewrite power-of-two divisors into shifts and modulo-by-2ⁿ into AND masks. Constant folding now handles `Mul`/`Div`/`Mod`/shifts too, which were previously left for the codegen to emit inefficient software calls. Dead code removed (no backward-compat shims kept): - `src/debug/` entirely. `DebugSymbols`, `SourceMap`, and the Mesen/.sym emitters had no callers outside their own tests; `main.rs` never wrote a symbol file. Documented the intent in `docs/future-work.md` so it comes back intentionally if needed. - `ErrorCode::E0202` (invalid cast) and `E0403` (unreachable state): defined, formatted, and marked `#[allow(dead_code)]` but never emitted. W0104 now carries the unreachable-state semantics too. - `Level::Info`: never constructed. - `load_background` / `set_palette` statements and their `BackgroundDecl` / `PaletteDecl` parser support: parsed and silently dropped by IR lowering (`// TODO: implement in asset pipeline`). Removed keywords, AST variants, parser paths, analyzer arms, and tests. `docs/future-work.md` documents the runtime palette/nametable design for when it comes back. Doc cleanup: - `docs/architecture.md` was describing files that don't exist (`analyzer/types.rs`, `optimizer/const_fold.rs`, `codegen/regalloc.rs`, `rom/header.rs`, `debug/symbols.rs`, …). Rewrote it to match the real flat `mod.rs` + `tests.rs` layout and the real pipeline order. - `docs/future-work.md` was a hybrid of open work and "recently completed" entries that duplicated the active stubs at the top of the file. Collapsed to just the gaps that are actually still open. - `README.md` claimed Mesen symbol export and 210 tests; updated both. - `docs/language-guide.md` and `spec.md` described `palette` decls, `set_palette` / `load_background`, `debug.overlay`, and error codes that were never emitted. Trimmed. - Stale comments on `Statement::Play`/`StartMusic`/`StopMusic` claimed the audio subsystem was "a no-op at codegen time". Tests: - Regression tests for every fix above (`lower_shift_left_with_literal _count_uses_that_count`, `lower_shift_right_with_variable_count _uses_runtime_variant`, `lower_divide_emits_div_op_not_load_imm _zero`, `lower_modulo_emits_mod_op_not_load_imm_zero`, `strength_reduce_div_by_power_of_two`, `strength_reduce_mod_by _power_of_two`, `strength_reduce_shift_var_with_constant_amount`). - Renamed the `program_with_sprites_and_palette` integration test (which was exercising the now-removed `load_background`/`set_palette`) to `program_with_inline_sprite_chr`. `examples/sprites_and_palettes.ne` lost its `palette`/`set_palette` usage. Nothing in the emulator test presses A, so the headless jsnes render shouldn't move, but the golden may need regeneration via `UPDATE_GOLDENS=1` if it does. https://claude.ai/code/session_012fKB251HvEUQwG3tizFyqt
2026-04-13 02:47:37 +00:00
## Audio pipeline
cleanup: fix silent miscompiles and delete dead code exposed by code review Two correctness bugs were silently producing wrong ROMs: - `x << n` / `x >> n` always shifted by 1, regardless of `n`, because the IR lowering for `BinOp::ShiftLeft`/`ShiftRight` hardcoded the count. Now eval_const the RHS into a compile-time count; fall back to a new `IrOp::ShiftLeftVar` / `ShiftRightVar` (runtime loop) when the amount isn't constant. Strength reduction folds the variable form back to a fixed count once the optimizer knows the value. - `x / n` / `x % n` always returned 0, because the lowering emitted `LoadImm(t, 0)` for `BinOp::Div`/`Mod` with a comment saying the runtime call was "TODO for now". Added real `IrOp::Div` and `IrOp::Mod`, wired them through use-counting and DCE, gave codegen `__divide`-based implementations, and taught strength reduction to rewrite power-of-two divisors into shifts and modulo-by-2ⁿ into AND masks. Constant folding now handles `Mul`/`Div`/`Mod`/shifts too, which were previously left for the codegen to emit inefficient software calls. Dead code removed (no backward-compat shims kept): - `src/debug/` entirely. `DebugSymbols`, `SourceMap`, and the Mesen/.sym emitters had no callers outside their own tests; `main.rs` never wrote a symbol file. Documented the intent in `docs/future-work.md` so it comes back intentionally if needed. - `ErrorCode::E0202` (invalid cast) and `E0403` (unreachable state): defined, formatted, and marked `#[allow(dead_code)]` but never emitted. W0104 now carries the unreachable-state semantics too. - `Level::Info`: never constructed. - `load_background` / `set_palette` statements and their `BackgroundDecl` / `PaletteDecl` parser support: parsed and silently dropped by IR lowering (`// TODO: implement in asset pipeline`). Removed keywords, AST variants, parser paths, analyzer arms, and tests. `docs/future-work.md` documents the runtime palette/nametable design for when it comes back. Doc cleanup: - `docs/architecture.md` was describing files that don't exist (`analyzer/types.rs`, `optimizer/const_fold.rs`, `codegen/regalloc.rs`, `rom/header.rs`, `debug/symbols.rs`, …). Rewrote it to match the real flat `mod.rs` + `tests.rs` layout and the real pipeline order. - `docs/future-work.md` was a hybrid of open work and "recently completed" entries that duplicated the active stubs at the top of the file. Collapsed to just the gaps that are actually still open. - `README.md` claimed Mesen symbol export and 210 tests; updated both. - `docs/language-guide.md` and `spec.md` described `palette` decls, `set_palette` / `load_background`, `debug.overlay`, and error codes that were never emitted. Trimmed. - Stale comments on `Statement::Play`/`StartMusic`/`StopMusic` claimed the audio subsystem was "a no-op at codegen time". Tests: - Regression tests for every fix above (`lower_shift_left_with_literal _count_uses_that_count`, `lower_shift_right_with_variable_count _uses_runtime_variant`, `lower_divide_emits_div_op_not_load_imm _zero`, `lower_modulo_emits_mod_op_not_load_imm_zero`, `strength_reduce_div_by_power_of_two`, `strength_reduce_mod_by _power_of_two`, `strength_reduce_shift_var_with_constant_amount`). - Renamed the `program_with_sprites_and_palette` integration test (which was exercising the now-removed `load_background`/`set_palette`) to `program_with_inline_sprite_chr`. `examples/sprites_and_palettes.ne` lost its `palette`/`set_palette` usage. Nothing in the emulator test presses A, so the headless jsnes render shouldn't move, but the golden may need regeneration via `UPDATE_GOLDENS=1` if it does. https://claude.ai/code/session_012fKB251HvEUQwG3tizFyqt
2026-04-13 02:47:37 +00:00
**What ships today.** Frame-walking pulse driver with `sfx Name { duty, pitch,
volume }` and `music Name { duty, volume, repeat, notes }` blocks; builtin
effects and tracks; a 60-entry period table; `__audio_used` marker that
elides the whole subsystem when no program statement references it. **Plus**
`channel: triangle` and `channel: noise` on `sfx` blocks, which splice in
per-channel slots that write to $4008-$400B (triangle) or $400C-$400F
(noise) when a program declares them. **Plus per-frame pitch envelopes
on Pulse-1 sfx** — a `pitch:` array with more than one distinct value
opts into a separate `__sfx_pitch_<name>` blob that the audio tick walks
in lockstep with the volume envelope, writing `$4002` on every NMI for a
real frequency-sweeping pulse channel. Pulse-only programs without
varying-pitch sfx still produce byte-identical driver code. See
`examples/noise_triangle_sfx.ne` and `examples/sfx_pitch_envelope.ne`.
cleanup: fix silent miscompiles and delete dead code exposed by code review Two correctness bugs were silently producing wrong ROMs: - `x << n` / `x >> n` always shifted by 1, regardless of `n`, because the IR lowering for `BinOp::ShiftLeft`/`ShiftRight` hardcoded the count. Now eval_const the RHS into a compile-time count; fall back to a new `IrOp::ShiftLeftVar` / `ShiftRightVar` (runtime loop) when the amount isn't constant. Strength reduction folds the variable form back to a fixed count once the optimizer knows the value. - `x / n` / `x % n` always returned 0, because the lowering emitted `LoadImm(t, 0)` for `BinOp::Div`/`Mod` with a comment saying the runtime call was "TODO for now". Added real `IrOp::Div` and `IrOp::Mod`, wired them through use-counting and DCE, gave codegen `__divide`-based implementations, and taught strength reduction to rewrite power-of-two divisors into shifts and modulo-by-2ⁿ into AND masks. Constant folding now handles `Mul`/`Div`/`Mod`/shifts too, which were previously left for the codegen to emit inefficient software calls. Dead code removed (no backward-compat shims kept): - `src/debug/` entirely. `DebugSymbols`, `SourceMap`, and the Mesen/.sym emitters had no callers outside their own tests; `main.rs` never wrote a symbol file. Documented the intent in `docs/future-work.md` so it comes back intentionally if needed. - `ErrorCode::E0202` (invalid cast) and `E0403` (unreachable state): defined, formatted, and marked `#[allow(dead_code)]` but never emitted. W0104 now carries the unreachable-state semantics too. - `Level::Info`: never constructed. - `load_background` / `set_palette` statements and their `BackgroundDecl` / `PaletteDecl` parser support: parsed and silently dropped by IR lowering (`// TODO: implement in asset pipeline`). Removed keywords, AST variants, parser paths, analyzer arms, and tests. `docs/future-work.md` documents the runtime palette/nametable design for when it comes back. Doc cleanup: - `docs/architecture.md` was describing files that don't exist (`analyzer/types.rs`, `optimizer/const_fold.rs`, `codegen/regalloc.rs`, `rom/header.rs`, `debug/symbols.rs`, …). Rewrote it to match the real flat `mod.rs` + `tests.rs` layout and the real pipeline order. - `docs/future-work.md` was a hybrid of open work and "recently completed" entries that duplicated the active stubs at the top of the file. Collapsed to just the gaps that are actually still open. - `README.md` claimed Mesen symbol export and 210 tests; updated both. - `docs/language-guide.md` and `spec.md` described `palette` decls, `set_palette` / `load_background`, `debug.overlay`, and error codes that were never emitted. Trimmed. - Stale comments on `Statement::Play`/`StartMusic`/`StopMusic` claimed the audio subsystem was "a no-op at codegen time". Tests: - Regression tests for every fix above (`lower_shift_left_with_literal _count_uses_that_count`, `lower_shift_right_with_variable_count _uses_runtime_variant`, `lower_divide_emits_div_op_not_load_imm _zero`, `lower_modulo_emits_mod_op_not_load_imm_zero`, `strength_reduce_div_by_power_of_two`, `strength_reduce_mod_by _power_of_two`, `strength_reduce_shift_var_with_constant_amount`). - Renamed the `program_with_sprites_and_palette` integration test (which was exercising the now-removed `load_background`/`set_palette`) to `program_with_inline_sprite_chr`. `examples/sprites_and_palettes.ne` lost its `palette`/`set_palette` usage. Nothing in the emulator test presses A, so the headless jsnes render shouldn't move, but the golden may need regeneration via `UPDATE_GOLDENS=1` if it does. https://claude.ai/code/session_012fKB251HvEUQwG3tizFyqt
2026-04-13 02:47:37 +00:00
**Still TODO for richer audio.**
- **DMC channel** — delta-modulation sample playback is not wired yet.
- **Multi-channel tracker playback** — one `notes` list per channel on
`music` blocks (the triangle/noise SFX are one-shot envelopes, not a
tracker).
- **`@sfx("file.nsf")` / `@music("file.ftm")`** — neither the NSF nor the
FamiTracker format is parsed yet.
- **Per-frame pitch envelopes on triangle / noise sfx** — the data
shape (a parallel pitch array on the `sfx` block) is the same as for
Pulse-1, but the runtime triangle/noise tick blocks currently only
write their volume registers (`$4008` / `$400C`). Extending them to
also walk a per-channel pitch envelope and write `$400A` / `$400E`
is the natural next step now that the pulse path is proven.
---
cleanup: fix silent miscompiles and delete dead code exposed by code review Two correctness bugs were silently producing wrong ROMs: - `x << n` / `x >> n` always shifted by 1, regardless of `n`, because the IR lowering for `BinOp::ShiftLeft`/`ShiftRight` hardcoded the count. Now eval_const the RHS into a compile-time count; fall back to a new `IrOp::ShiftLeftVar` / `ShiftRightVar` (runtime loop) when the amount isn't constant. Strength reduction folds the variable form back to a fixed count once the optimizer knows the value. - `x / n` / `x % n` always returned 0, because the lowering emitted `LoadImm(t, 0)` for `BinOp::Div`/`Mod` with a comment saying the runtime call was "TODO for now". Added real `IrOp::Div` and `IrOp::Mod`, wired them through use-counting and DCE, gave codegen `__divide`-based implementations, and taught strength reduction to rewrite power-of-two divisors into shifts and modulo-by-2ⁿ into AND masks. Constant folding now handles `Mul`/`Div`/`Mod`/shifts too, which were previously left for the codegen to emit inefficient software calls. Dead code removed (no backward-compat shims kept): - `src/debug/` entirely. `DebugSymbols`, `SourceMap`, and the Mesen/.sym emitters had no callers outside their own tests; `main.rs` never wrote a symbol file. Documented the intent in `docs/future-work.md` so it comes back intentionally if needed. - `ErrorCode::E0202` (invalid cast) and `E0403` (unreachable state): defined, formatted, and marked `#[allow(dead_code)]` but never emitted. W0104 now carries the unreachable-state semantics too. - `Level::Info`: never constructed. - `load_background` / `set_palette` statements and their `BackgroundDecl` / `PaletteDecl` parser support: parsed and silently dropped by IR lowering (`// TODO: implement in asset pipeline`). Removed keywords, AST variants, parser paths, analyzer arms, and tests. `docs/future-work.md` documents the runtime palette/nametable design for when it comes back. Doc cleanup: - `docs/architecture.md` was describing files that don't exist (`analyzer/types.rs`, `optimizer/const_fold.rs`, `codegen/regalloc.rs`, `rom/header.rs`, `debug/symbols.rs`, …). Rewrote it to match the real flat `mod.rs` + `tests.rs` layout and the real pipeline order. - `docs/future-work.md` was a hybrid of open work and "recently completed" entries that duplicated the active stubs at the top of the file. Collapsed to just the gaps that are actually still open. - `README.md` claimed Mesen symbol export and 210 tests; updated both. - `docs/language-guide.md` and `spec.md` described `palette` decls, `set_palette` / `load_background`, `debug.overlay`, and error codes that were never emitted. Trimmed. - Stale comments on `Statement::Play`/`StartMusic`/`StopMusic` claimed the audio subsystem was "a no-op at codegen time". Tests: - Regression tests for every fix above (`lower_shift_left_with_literal _count_uses_that_count`, `lower_shift_right_with_variable_count _uses_runtime_variant`, `lower_divide_emits_div_op_not_load_imm _zero`, `lower_modulo_emits_mod_op_not_load_imm_zero`, `strength_reduce_div_by_power_of_two`, `strength_reduce_mod_by _power_of_two`, `strength_reduce_shift_var_with_constant_amount`). - Renamed the `program_with_sprites_and_palette` integration test (which was exercising the now-removed `load_background`/`set_palette`) to `program_with_inline_sprite_chr`. `examples/sprites_and_palettes.ne` lost its `palette`/`set_palette` usage. Nothing in the emulator test presses A, so the headless jsnes render shouldn't move, but the golden may need regeneration via `UPDATE_GOLDENS=1` if it does. https://claude.ai/code/session_012fKB251HvEUQwG3tizFyqt
2026-04-13 02:47:37 +00:00
## Debug instrumentation
cleanup: fix silent miscompiles and delete dead code exposed by code review Two correctness bugs were silently producing wrong ROMs: - `x << n` / `x >> n` always shifted by 1, regardless of `n`, because the IR lowering for `BinOp::ShiftLeft`/`ShiftRight` hardcoded the count. Now eval_const the RHS into a compile-time count; fall back to a new `IrOp::ShiftLeftVar` / `ShiftRightVar` (runtime loop) when the amount isn't constant. Strength reduction folds the variable form back to a fixed count once the optimizer knows the value. - `x / n` / `x % n` always returned 0, because the lowering emitted `LoadImm(t, 0)` for `BinOp::Div`/`Mod` with a comment saying the runtime call was "TODO for now". Added real `IrOp::Div` and `IrOp::Mod`, wired them through use-counting and DCE, gave codegen `__divide`-based implementations, and taught strength reduction to rewrite power-of-two divisors into shifts and modulo-by-2ⁿ into AND masks. Constant folding now handles `Mul`/`Div`/`Mod`/shifts too, which were previously left for the codegen to emit inefficient software calls. Dead code removed (no backward-compat shims kept): - `src/debug/` entirely. `DebugSymbols`, `SourceMap`, and the Mesen/.sym emitters had no callers outside their own tests; `main.rs` never wrote a symbol file. Documented the intent in `docs/future-work.md` so it comes back intentionally if needed. - `ErrorCode::E0202` (invalid cast) and `E0403` (unreachable state): defined, formatted, and marked `#[allow(dead_code)]` but never emitted. W0104 now carries the unreachable-state semantics too. - `Level::Info`: never constructed. - `load_background` / `set_palette` statements and their `BackgroundDecl` / `PaletteDecl` parser support: parsed and silently dropped by IR lowering (`// TODO: implement in asset pipeline`). Removed keywords, AST variants, parser paths, analyzer arms, and tests. `docs/future-work.md` documents the runtime palette/nametable design for when it comes back. Doc cleanup: - `docs/architecture.md` was describing files that don't exist (`analyzer/types.rs`, `optimizer/const_fold.rs`, `codegen/regalloc.rs`, `rom/header.rs`, `debug/symbols.rs`, …). Rewrote it to match the real flat `mod.rs` + `tests.rs` layout and the real pipeline order. - `docs/future-work.md` was a hybrid of open work and "recently completed" entries that duplicated the active stubs at the top of the file. Collapsed to just the gaps that are actually still open. - `README.md` claimed Mesen symbol export and 210 tests; updated both. - `docs/language-guide.md` and `spec.md` described `palette` decls, `set_palette` / `load_background`, `debug.overlay`, and error codes that were never emitted. Trimmed. - Stale comments on `Statement::Play`/`StartMusic`/`StopMusic` claimed the audio subsystem was "a no-op at codegen time". Tests: - Regression tests for every fix above (`lower_shift_left_with_literal _count_uses_that_count`, `lower_shift_right_with_variable_count _uses_runtime_variant`, `lower_divide_emits_div_op_not_load_imm _zero`, `lower_modulo_emits_mod_op_not_load_imm_zero`, `strength_reduce_div_by_power_of_two`, `strength_reduce_mod_by _power_of_two`, `strength_reduce_shift_var_with_constant_amount`). - Renamed the `program_with_sprites_and_palette` integration test (which was exercising the now-removed `load_background`/`set_palette`) to `program_with_inline_sprite_chr`. `examples/sprites_and_palettes.ne` lost its `palette`/`set_palette` usage. Nothing in the emulator test presses A, so the headless jsnes render shouldn't move, but the golden may need regeneration via `UPDATE_GOLDENS=1` if it does. https://claude.ai/code/session_012fKB251HvEUQwG3tizFyqt
2026-04-13 02:47:37 +00:00
**What ships today.** `debug.log(...)` and `debug.assert(...)` lower to $4800
writes when `--debug` is passed, and are stripped entirely in release builds.
`--symbols <path>` writes a Mesen-compatible `.mlb` file listing function,
state-handler, and variable addresses (with PRG ROM offsets for code and
CPU addresses for RAM). `--source-map <path>` consumes the `SourceLoc` IR
op and writes a plain-text map of `<rom_offset> <file_id> <line> <col>`
entries for every lowered statement. **`--dbg <path>` writes a
ca65-compatible `.dbg` debug-info file** that Mesen / Mesen2 / fceuX pick
up automatically for source-level stepping, labelled variable inspection,
and symbol-based breakpoints. The file stitches together the linker's
label table, the `__src_<N>` IR markers, and the analyzer's variable
allocations into the `file`/`mod`/`seg`/`scope`/`span`/`line`/`sym`
records documented at
<https://cc65.github.io/doc/debugfile.html>. `ooffs` on the segment
record tracks the fixed bank's PRG-relative start, so banked ROMs
(MMC1/UxROM/MMC3) also map cleanly inside the debugger. Debug builds emit array bounds checks
(CMP against size, BCC past a `JMP __debug_halt` wedge) and bump an
overrun counter at `$07FF` in the NMI handler when the main loop didn't
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
reach `wait_frame` before the next vblank.
**Plus four query expressions** that mirror the counter/sticky pattern:
`debug.frame_overrun_count()` / `debug.frame_overran()` return the cumulative
overrun counter and a per-frame sticky bit so user code can write
`debug.assert(not debug.frame_overran())` guards, and
`debug.sprite_overflow_count()` / `debug.sprite_overflow()` do the same for
the NES PPU's sprite-per-scanline flag (`$2002` bit 5), which the NMI
handler samples once per frame in debug mode. All four sticky bits clear
on the next `wait_frame`.
**Still TODO.**
- **`debug.overlay(x, y, text)`** — needs the text/HUD subsystem (see
Language feature gaps).
---
cleanup: fix silent miscompiles and delete dead code exposed by code review Two correctness bugs were silently producing wrong ROMs: - `x << n` / `x >> n` always shifted by 1, regardless of `n`, because the IR lowering for `BinOp::ShiftLeft`/`ShiftRight` hardcoded the count. Now eval_const the RHS into a compile-time count; fall back to a new `IrOp::ShiftLeftVar` / `ShiftRightVar` (runtime loop) when the amount isn't constant. Strength reduction folds the variable form back to a fixed count once the optimizer knows the value. - `x / n` / `x % n` always returned 0, because the lowering emitted `LoadImm(t, 0)` for `BinOp::Div`/`Mod` with a comment saying the runtime call was "TODO for now". Added real `IrOp::Div` and `IrOp::Mod`, wired them through use-counting and DCE, gave codegen `__divide`-based implementations, and taught strength reduction to rewrite power-of-two divisors into shifts and modulo-by-2ⁿ into AND masks. Constant folding now handles `Mul`/`Div`/`Mod`/shifts too, which were previously left for the codegen to emit inefficient software calls. Dead code removed (no backward-compat shims kept): - `src/debug/` entirely. `DebugSymbols`, `SourceMap`, and the Mesen/.sym emitters had no callers outside their own tests; `main.rs` never wrote a symbol file. Documented the intent in `docs/future-work.md` so it comes back intentionally if needed. - `ErrorCode::E0202` (invalid cast) and `E0403` (unreachable state): defined, formatted, and marked `#[allow(dead_code)]` but never emitted. W0104 now carries the unreachable-state semantics too. - `Level::Info`: never constructed. - `load_background` / `set_palette` statements and their `BackgroundDecl` / `PaletteDecl` parser support: parsed and silently dropped by IR lowering (`// TODO: implement in asset pipeline`). Removed keywords, AST variants, parser paths, analyzer arms, and tests. `docs/future-work.md` documents the runtime palette/nametable design for when it comes back. Doc cleanup: - `docs/architecture.md` was describing files that don't exist (`analyzer/types.rs`, `optimizer/const_fold.rs`, `codegen/regalloc.rs`, `rom/header.rs`, `debug/symbols.rs`, …). Rewrote it to match the real flat `mod.rs` + `tests.rs` layout and the real pipeline order. - `docs/future-work.md` was a hybrid of open work and "recently completed" entries that duplicated the active stubs at the top of the file. Collapsed to just the gaps that are actually still open. - `README.md` claimed Mesen symbol export and 210 tests; updated both. - `docs/language-guide.md` and `spec.md` described `palette` decls, `set_palette` / `load_background`, `debug.overlay`, and error codes that were never emitted. Trimmed. - Stale comments on `Statement::Play`/`StartMusic`/`StopMusic` claimed the audio subsystem was "a no-op at codegen time". Tests: - Regression tests for every fix above (`lower_shift_left_with_literal _count_uses_that_count`, `lower_shift_right_with_variable_count _uses_runtime_variant`, `lower_divide_emits_div_op_not_load_imm _zero`, `lower_modulo_emits_mod_op_not_load_imm_zero`, `strength_reduce_div_by_power_of_two`, `strength_reduce_mod_by _power_of_two`, `strength_reduce_shift_var_with_constant_amount`). - Renamed the `program_with_sprites_and_palette` integration test (which was exercising the now-removed `load_background`/`set_palette`) to `program_with_inline_sprite_chr`. `examples/sprites_and_palettes.ne` lost its `palette`/`set_palette` usage. Nothing in the emulator test presses A, so the headless jsnes render shouldn't move, but the golden may need regeneration via `UPDATE_GOLDENS=1` if it does. https://claude.ai/code/session_012fKB251HvEUQwG3tizFyqt
2026-04-13 02:47:37 +00:00
## Code quality / tooling
cleanup: fix silent miscompiles and delete dead code exposed by code review Two correctness bugs were silently producing wrong ROMs: - `x << n` / `x >> n` always shifted by 1, regardless of `n`, because the IR lowering for `BinOp::ShiftLeft`/`ShiftRight` hardcoded the count. Now eval_const the RHS into a compile-time count; fall back to a new `IrOp::ShiftLeftVar` / `ShiftRightVar` (runtime loop) when the amount isn't constant. Strength reduction folds the variable form back to a fixed count once the optimizer knows the value. - `x / n` / `x % n` always returned 0, because the lowering emitted `LoadImm(t, 0)` for `BinOp::Div`/`Mod` with a comment saying the runtime call was "TODO for now". Added real `IrOp::Div` and `IrOp::Mod`, wired them through use-counting and DCE, gave codegen `__divide`-based implementations, and taught strength reduction to rewrite power-of-two divisors into shifts and modulo-by-2ⁿ into AND masks. Constant folding now handles `Mul`/`Div`/`Mod`/shifts too, which were previously left for the codegen to emit inefficient software calls. Dead code removed (no backward-compat shims kept): - `src/debug/` entirely. `DebugSymbols`, `SourceMap`, and the Mesen/.sym emitters had no callers outside their own tests; `main.rs` never wrote a symbol file. Documented the intent in `docs/future-work.md` so it comes back intentionally if needed. - `ErrorCode::E0202` (invalid cast) and `E0403` (unreachable state): defined, formatted, and marked `#[allow(dead_code)]` but never emitted. W0104 now carries the unreachable-state semantics too. - `Level::Info`: never constructed. - `load_background` / `set_palette` statements and their `BackgroundDecl` / `PaletteDecl` parser support: parsed and silently dropped by IR lowering (`// TODO: implement in asset pipeline`). Removed keywords, AST variants, parser paths, analyzer arms, and tests. `docs/future-work.md` documents the runtime palette/nametable design for when it comes back. Doc cleanup: - `docs/architecture.md` was describing files that don't exist (`analyzer/types.rs`, `optimizer/const_fold.rs`, `codegen/regalloc.rs`, `rom/header.rs`, `debug/symbols.rs`, …). Rewrote it to match the real flat `mod.rs` + `tests.rs` layout and the real pipeline order. - `docs/future-work.md` was a hybrid of open work and "recently completed" entries that duplicated the active stubs at the top of the file. Collapsed to just the gaps that are actually still open. - `README.md` claimed Mesen symbol export and 210 tests; updated both. - `docs/language-guide.md` and `spec.md` described `palette` decls, `set_palette` / `load_background`, `debug.overlay`, and error codes that were never emitted. Trimmed. - Stale comments on `Statement::Play`/`StartMusic`/`StopMusic` claimed the audio subsystem was "a no-op at codegen time". Tests: - Regression tests for every fix above (`lower_shift_left_with_literal _count_uses_that_count`, `lower_shift_right_with_variable_count _uses_runtime_variant`, `lower_divide_emits_div_op_not_load_imm _zero`, `lower_modulo_emits_mod_op_not_load_imm_zero`, `strength_reduce_div_by_power_of_two`, `strength_reduce_mod_by _power_of_two`, `strength_reduce_shift_var_with_constant_amount`). - Renamed the `program_with_sprites_and_palette` integration test (which was exercising the now-removed `load_background`/`set_palette`) to `program_with_inline_sprite_chr`. `examples/sprites_and_palettes.ne` lost its `palette`/`set_palette` usage. Nothing in the emulator test presses A, so the headless jsnes render shouldn't move, but the golden may need regeneration via `UPDATE_GOLDENS=1` if it does. https://claude.ai/code/session_012fKB251HvEUQwG3tizFyqt
2026-04-13 02:47:37 +00:00
### Register allocator
cleanup: fix silent miscompiles and delete dead code exposed by code review Two correctness bugs were silently producing wrong ROMs: - `x << n` / `x >> n` always shifted by 1, regardless of `n`, because the IR lowering for `BinOp::ShiftLeft`/`ShiftRight` hardcoded the count. Now eval_const the RHS into a compile-time count; fall back to a new `IrOp::ShiftLeftVar` / `ShiftRightVar` (runtime loop) when the amount isn't constant. Strength reduction folds the variable form back to a fixed count once the optimizer knows the value. - `x / n` / `x % n` always returned 0, because the lowering emitted `LoadImm(t, 0)` for `BinOp::Div`/`Mod` with a comment saying the runtime call was "TODO for now". Added real `IrOp::Div` and `IrOp::Mod`, wired them through use-counting and DCE, gave codegen `__divide`-based implementations, and taught strength reduction to rewrite power-of-two divisors into shifts and modulo-by-2ⁿ into AND masks. Constant folding now handles `Mul`/`Div`/`Mod`/shifts too, which were previously left for the codegen to emit inefficient software calls. Dead code removed (no backward-compat shims kept): - `src/debug/` entirely. `DebugSymbols`, `SourceMap`, and the Mesen/.sym emitters had no callers outside their own tests; `main.rs` never wrote a symbol file. Documented the intent in `docs/future-work.md` so it comes back intentionally if needed. - `ErrorCode::E0202` (invalid cast) and `E0403` (unreachable state): defined, formatted, and marked `#[allow(dead_code)]` but never emitted. W0104 now carries the unreachable-state semantics too. - `Level::Info`: never constructed. - `load_background` / `set_palette` statements and their `BackgroundDecl` / `PaletteDecl` parser support: parsed and silently dropped by IR lowering (`// TODO: implement in asset pipeline`). Removed keywords, AST variants, parser paths, analyzer arms, and tests. `docs/future-work.md` documents the runtime palette/nametable design for when it comes back. Doc cleanup: - `docs/architecture.md` was describing files that don't exist (`analyzer/types.rs`, `optimizer/const_fold.rs`, `codegen/regalloc.rs`, `rom/header.rs`, `debug/symbols.rs`, …). Rewrote it to match the real flat `mod.rs` + `tests.rs` layout and the real pipeline order. - `docs/future-work.md` was a hybrid of open work and "recently completed" entries that duplicated the active stubs at the top of the file. Collapsed to just the gaps that are actually still open. - `README.md` claimed Mesen symbol export and 210 tests; updated both. - `docs/language-guide.md` and `spec.md` described `palette` decls, `set_palette` / `load_background`, `debug.overlay`, and error codes that were never emitted. Trimmed. - Stale comments on `Statement::Play`/`StartMusic`/`StopMusic` claimed the audio subsystem was "a no-op at codegen time". Tests: - Regression tests for every fix above (`lower_shift_left_with_literal _count_uses_that_count`, `lower_shift_right_with_variable_count _uses_runtime_variant`, `lower_divide_emits_div_op_not_load_imm _zero`, `lower_modulo_emits_mod_op_not_load_imm_zero`, `strength_reduce_div_by_power_of_two`, `strength_reduce_mod_by _power_of_two`, `strength_reduce_shift_var_with_constant_amount`). - Renamed the `program_with_sprites_and_palette` integration test (which was exercising the now-removed `load_background`/`set_palette`) to `program_with_inline_sprite_chr`. `examples/sprites_and_palettes.ne` lost its `palette`/`set_palette` usage. Nothing in the emulator test presses A, so the headless jsnes render shouldn't move, but the golden may need regeneration via `UPDATE_GOLDENS=1` if it does. https://claude.ai/code/session_012fKB251HvEUQwG3tizFyqt
2026-04-13 02:47:37 +00:00
All IR temps currently spill to a recycled zero-page slot (`$80-$FF`). The
peephole pass mops up the most obvious waste, but a real CFG-aware allocator
that holds short-lived temps in `A`/`X`/`Y` would cut a noticeable number of
LDA/STA pairs.
analyzer+ir: automatically overlay state-local variables Before this change, state-local variables (`state Foo { var x: u8 = 0 }`) were silently no-ops: the analyzer allocated a ZP slot for them, but the codegen's `var_addrs` map only covered IR globals and scope-qualified function locals — so every `LoadVar` / `StoreVar` whose `VarId` pointed at a state-local resolved to no address and emitted nothing. Existing examples compiled and matched their goldens because none of them observed the dropped writes within the 180-frame harness window. The overlay changes the analyzer's state-local pass to snapshot both the ZP and RAM cursors after the globals have been laid out, then rewind to that snapshot before each state's locals and track the running max. `ZP_CURRENT_STATE` keeps exactly one state active at runtime, so every state's locals are mutually exclusive with every other state's and can share the same bytes. The IR lowerer now pushes each state's locals into the IR globals table (with `init_value=None`) so the codegen resolves their addresses the same way it does program globals, and prepends the declared initializers to each state's `on_enter` handler (synthesizing an empty one where needed) so a freshly-entered state re-establishes its bytes before user code runs. `--memory-map` now tags each allocation with its owning state (`[@Title]`, `[@Playing]`, ...) and counts distinct bytes rather than summed allocation sizes so overlaid slots don't double-count. The `AnalysisResult.state_local_owners` map exposes the ownership to any tool that wants to group allocations the same way. Only `state_machine.ne` and `platformer.ne` declare state-level vars, so they're the only example ROMs whose bytes change. `platformer.ne`'s audio golden shifts slightly (the now-working `blink` counter in Title adds a few cycles per frame before the auto-transition to Playing, which offsets APU register writes within each frame); its video golden and every other example ROM stay byte-for-byte identical. Fixes #22. https://claude.ai/code/session_015kvJu3iEFLSRJoShPBfm3X
2026-04-17 02:20:07 +00:00
### State-local memory overlay follow-ups
State-local variables are now overlaid across mutually-exclusive states
(see the analyzer's per-state allocation cursor rewind and the IR
lowerer's `on_enter` initializer prologue), but a few pieces are still
missing:
- **Same-named locals across different states.** `register_var` stores
state-locals under their bare name, so two states each declaring
`var timer: u8` collide with E0501. A per-state symbol-table scope
prefix would let each state carve its own namespace while keeping
the overlay.
- **Struct-literal and array-literal initializers on state-locals.**
The on-enter prologue lowers scalar initializers cleanly, and
struct-literal initializers fall back to per-field stores, but
array-literal initializers (`var xs: u8[4] = [1,2,3,4]`) are
skipped. A runtime `memcpy` from a ROM blob into the overlay
slot (mirroring the reset-time global path) is the natural
lowering.
- **Handler-local overlay.** Handler-local `var`s declared inside
`on_frame { ... }` are already per-handler scoped via
`current_scope_prefix`, but they get a dedicated RAM slot for the
program's lifetime. Overlaying them inside each handler's stack
frame — using a per-handler bump allocator that resets on each
call — would shave a few bytes more on programs with many deep
handlers.
cleanup: fix silent miscompiles and delete dead code exposed by code review Two correctness bugs were silently producing wrong ROMs: - `x << n` / `x >> n` always shifted by 1, regardless of `n`, because the IR lowering for `BinOp::ShiftLeft`/`ShiftRight` hardcoded the count. Now eval_const the RHS into a compile-time count; fall back to a new `IrOp::ShiftLeftVar` / `ShiftRightVar` (runtime loop) when the amount isn't constant. Strength reduction folds the variable form back to a fixed count once the optimizer knows the value. - `x / n` / `x % n` always returned 0, because the lowering emitted `LoadImm(t, 0)` for `BinOp::Div`/`Mod` with a comment saying the runtime call was "TODO for now". Added real `IrOp::Div` and `IrOp::Mod`, wired them through use-counting and DCE, gave codegen `__divide`-based implementations, and taught strength reduction to rewrite power-of-two divisors into shifts and modulo-by-2ⁿ into AND masks. Constant folding now handles `Mul`/`Div`/`Mod`/shifts too, which were previously left for the codegen to emit inefficient software calls. Dead code removed (no backward-compat shims kept): - `src/debug/` entirely. `DebugSymbols`, `SourceMap`, and the Mesen/.sym emitters had no callers outside their own tests; `main.rs` never wrote a symbol file. Documented the intent in `docs/future-work.md` so it comes back intentionally if needed. - `ErrorCode::E0202` (invalid cast) and `E0403` (unreachable state): defined, formatted, and marked `#[allow(dead_code)]` but never emitted. W0104 now carries the unreachable-state semantics too. - `Level::Info`: never constructed. - `load_background` / `set_palette` statements and their `BackgroundDecl` / `PaletteDecl` parser support: parsed and silently dropped by IR lowering (`// TODO: implement in asset pipeline`). Removed keywords, AST variants, parser paths, analyzer arms, and tests. `docs/future-work.md` documents the runtime palette/nametable design for when it comes back. Doc cleanup: - `docs/architecture.md` was describing files that don't exist (`analyzer/types.rs`, `optimizer/const_fold.rs`, `codegen/regalloc.rs`, `rom/header.rs`, `debug/symbols.rs`, …). Rewrote it to match the real flat `mod.rs` + `tests.rs` layout and the real pipeline order. - `docs/future-work.md` was a hybrid of open work and "recently completed" entries that duplicated the active stubs at the top of the file. Collapsed to just the gaps that are actually still open. - `README.md` claimed Mesen symbol export and 210 tests; updated both. - `docs/language-guide.md` and `spec.md` described `palette` decls, `set_palette` / `load_background`, `debug.overlay`, and error codes that were never emitted. Trimmed. - Stale comments on `Statement::Play`/`StartMusic`/`StopMusic` claimed the audio subsystem was "a no-op at codegen time". Tests: - Regression tests for every fix above (`lower_shift_left_with_literal _count_uses_that_count`, `lower_shift_right_with_variable_count _uses_runtime_variant`, `lower_divide_emits_div_op_not_load_imm _zero`, `lower_modulo_emits_mod_op_not_load_imm_zero`, `strength_reduce_div_by_power_of_two`, `strength_reduce_mod_by _power_of_two`, `strength_reduce_shift_var_with_constant_amount`). - Renamed the `program_with_sprites_and_palette` integration test (which was exercising the now-removed `load_background`/`set_palette`) to `program_with_inline_sprite_chr`. `examples/sprites_and_palettes.ne` lost its `palette`/`set_palette` usage. Nothing in the emulator test presses A, so the headless jsnes render shouldn't move, but the golden may need regeneration via `UPDATE_GOLDENS=1` if it does. https://claude.ai/code/session_012fKB251HvEUQwG3tizFyqt
2026-04-13 02:47:37 +00:00
### Cross-block temp live-range analysis
cleanup: fix silent miscompiles and delete dead code exposed by code review Two correctness bugs were silently producing wrong ROMs: - `x << n` / `x >> n` always shifted by 1, regardless of `n`, because the IR lowering for `BinOp::ShiftLeft`/`ShiftRight` hardcoded the count. Now eval_const the RHS into a compile-time count; fall back to a new `IrOp::ShiftLeftVar` / `ShiftRightVar` (runtime loop) when the amount isn't constant. Strength reduction folds the variable form back to a fixed count once the optimizer knows the value. - `x / n` / `x % n` always returned 0, because the lowering emitted `LoadImm(t, 0)` for `BinOp::Div`/`Mod` with a comment saying the runtime call was "TODO for now". Added real `IrOp::Div` and `IrOp::Mod`, wired them through use-counting and DCE, gave codegen `__divide`-based implementations, and taught strength reduction to rewrite power-of-two divisors into shifts and modulo-by-2ⁿ into AND masks. Constant folding now handles `Mul`/`Div`/`Mod`/shifts too, which were previously left for the codegen to emit inefficient software calls. Dead code removed (no backward-compat shims kept): - `src/debug/` entirely. `DebugSymbols`, `SourceMap`, and the Mesen/.sym emitters had no callers outside their own tests; `main.rs` never wrote a symbol file. Documented the intent in `docs/future-work.md` so it comes back intentionally if needed. - `ErrorCode::E0202` (invalid cast) and `E0403` (unreachable state): defined, formatted, and marked `#[allow(dead_code)]` but never emitted. W0104 now carries the unreachable-state semantics too. - `Level::Info`: never constructed. - `load_background` / `set_palette` statements and their `BackgroundDecl` / `PaletteDecl` parser support: parsed and silently dropped by IR lowering (`// TODO: implement in asset pipeline`). Removed keywords, AST variants, parser paths, analyzer arms, and tests. `docs/future-work.md` documents the runtime palette/nametable design for when it comes back. Doc cleanup: - `docs/architecture.md` was describing files that don't exist (`analyzer/types.rs`, `optimizer/const_fold.rs`, `codegen/regalloc.rs`, `rom/header.rs`, `debug/symbols.rs`, …). Rewrote it to match the real flat `mod.rs` + `tests.rs` layout and the real pipeline order. - `docs/future-work.md` was a hybrid of open work and "recently completed" entries that duplicated the active stubs at the top of the file. Collapsed to just the gaps that are actually still open. - `README.md` claimed Mesen symbol export and 210 tests; updated both. - `docs/language-guide.md` and `spec.md` described `palette` decls, `set_palette` / `load_background`, `debug.overlay`, and error codes that were never emitted. Trimmed. - Stale comments on `Statement::Play`/`StartMusic`/`StopMusic` claimed the audio subsystem was "a no-op at codegen time". Tests: - Regression tests for every fix above (`lower_shift_left_with_literal _count_uses_that_count`, `lower_shift_right_with_variable_count _uses_runtime_variant`, `lower_divide_emits_div_op_not_load_imm _zero`, `lower_modulo_emits_mod_op_not_load_imm_zero`, `strength_reduce_div_by_power_of_two`, `strength_reduce_mod_by _power_of_two`, `strength_reduce_shift_var_with_constant_amount`). - Renamed the `program_with_sprites_and_palette` integration test (which was exercising the now-removed `load_background`/`set_palette`) to `program_with_inline_sprite_chr`. `examples/sprites_and_palettes.ne` lost its `palette`/`set_palette` usage. Nothing in the emulator test presses A, so the headless jsnes render shouldn't move, but the golden may need regeneration via `UPDATE_GOLDENS=1` if it does. https://claude.ai/code/session_012fKB251HvEUQwG3tizFyqt
2026-04-13 02:47:37 +00:00
The slot recycler is function-local per-block. Temps that flow across block
boundaries get a dedicated slot for the entire function, even if a later
block could reuse the slot.
cleanup: fix silent miscompiles and delete dead code exposed by code review Two correctness bugs were silently producing wrong ROMs: - `x << n` / `x >> n` always shifted by 1, regardless of `n`, because the IR lowering for `BinOp::ShiftLeft`/`ShiftRight` hardcoded the count. Now eval_const the RHS into a compile-time count; fall back to a new `IrOp::ShiftLeftVar` / `ShiftRightVar` (runtime loop) when the amount isn't constant. Strength reduction folds the variable form back to a fixed count once the optimizer knows the value. - `x / n` / `x % n` always returned 0, because the lowering emitted `LoadImm(t, 0)` for `BinOp::Div`/`Mod` with a comment saying the runtime call was "TODO for now". Added real `IrOp::Div` and `IrOp::Mod`, wired them through use-counting and DCE, gave codegen `__divide`-based implementations, and taught strength reduction to rewrite power-of-two divisors into shifts and modulo-by-2ⁿ into AND masks. Constant folding now handles `Mul`/`Div`/`Mod`/shifts too, which were previously left for the codegen to emit inefficient software calls. Dead code removed (no backward-compat shims kept): - `src/debug/` entirely. `DebugSymbols`, `SourceMap`, and the Mesen/.sym emitters had no callers outside their own tests; `main.rs` never wrote a symbol file. Documented the intent in `docs/future-work.md` so it comes back intentionally if needed. - `ErrorCode::E0202` (invalid cast) and `E0403` (unreachable state): defined, formatted, and marked `#[allow(dead_code)]` but never emitted. W0104 now carries the unreachable-state semantics too. - `Level::Info`: never constructed. - `load_background` / `set_palette` statements and their `BackgroundDecl` / `PaletteDecl` parser support: parsed and silently dropped by IR lowering (`// TODO: implement in asset pipeline`). Removed keywords, AST variants, parser paths, analyzer arms, and tests. `docs/future-work.md` documents the runtime palette/nametable design for when it comes back. Doc cleanup: - `docs/architecture.md` was describing files that don't exist (`analyzer/types.rs`, `optimizer/const_fold.rs`, `codegen/regalloc.rs`, `rom/header.rs`, `debug/symbols.rs`, …). Rewrote it to match the real flat `mod.rs` + `tests.rs` layout and the real pipeline order. - `docs/future-work.md` was a hybrid of open work and "recently completed" entries that duplicated the active stubs at the top of the file. Collapsed to just the gaps that are actually still open. - `README.md` claimed Mesen symbol export and 210 tests; updated both. - `docs/language-guide.md` and `spec.md` described `palette` decls, `set_palette` / `load_background`, `debug.overlay`, and error codes that were never emitted. Trimmed. - Stale comments on `Statement::Play`/`StartMusic`/`StopMusic` claimed the audio subsystem was "a no-op at codegen time". Tests: - Regression tests for every fix above (`lower_shift_left_with_literal _count_uses_that_count`, `lower_shift_right_with_variable_count _uses_runtime_variant`, `lower_divide_emits_div_op_not_load_imm _zero`, `lower_modulo_emits_mod_op_not_load_imm_zero`, `strength_reduce_div_by_power_of_two`, `strength_reduce_mod_by _power_of_two`, `strength_reduce_shift_var_with_constant_amount`). - Renamed the `program_with_sprites_and_palette` integration test (which was exercising the now-removed `load_background`/`set_palette`) to `program_with_inline_sprite_chr`. `examples/sprites_and_palettes.ne` lost its `palette`/`set_palette` usage. Nothing in the emulator test presses A, so the headless jsnes render shouldn't move, but the golden may need regeneration via `UPDATE_GOLDENS=1` if it does. https://claude.ai/code/session_012fKB251HvEUQwG3tizFyqt
2026-04-13 02:47:37 +00:00
### WASM build target
cleanup: fix silent miscompiles and delete dead code exposed by code review Two correctness bugs were silently producing wrong ROMs: - `x << n` / `x >> n` always shifted by 1, regardless of `n`, because the IR lowering for `BinOp::ShiftLeft`/`ShiftRight` hardcoded the count. Now eval_const the RHS into a compile-time count; fall back to a new `IrOp::ShiftLeftVar` / `ShiftRightVar` (runtime loop) when the amount isn't constant. Strength reduction folds the variable form back to a fixed count once the optimizer knows the value. - `x / n` / `x % n` always returned 0, because the lowering emitted `LoadImm(t, 0)` for `BinOp::Div`/`Mod` with a comment saying the runtime call was "TODO for now". Added real `IrOp::Div` and `IrOp::Mod`, wired them through use-counting and DCE, gave codegen `__divide`-based implementations, and taught strength reduction to rewrite power-of-two divisors into shifts and modulo-by-2ⁿ into AND masks. Constant folding now handles `Mul`/`Div`/`Mod`/shifts too, which were previously left for the codegen to emit inefficient software calls. Dead code removed (no backward-compat shims kept): - `src/debug/` entirely. `DebugSymbols`, `SourceMap`, and the Mesen/.sym emitters had no callers outside their own tests; `main.rs` never wrote a symbol file. Documented the intent in `docs/future-work.md` so it comes back intentionally if needed. - `ErrorCode::E0202` (invalid cast) and `E0403` (unreachable state): defined, formatted, and marked `#[allow(dead_code)]` but never emitted. W0104 now carries the unreachable-state semantics too. - `Level::Info`: never constructed. - `load_background` / `set_palette` statements and their `BackgroundDecl` / `PaletteDecl` parser support: parsed and silently dropped by IR lowering (`// TODO: implement in asset pipeline`). Removed keywords, AST variants, parser paths, analyzer arms, and tests. `docs/future-work.md` documents the runtime palette/nametable design for when it comes back. Doc cleanup: - `docs/architecture.md` was describing files that don't exist (`analyzer/types.rs`, `optimizer/const_fold.rs`, `codegen/regalloc.rs`, `rom/header.rs`, `debug/symbols.rs`, …). Rewrote it to match the real flat `mod.rs` + `tests.rs` layout and the real pipeline order. - `docs/future-work.md` was a hybrid of open work and "recently completed" entries that duplicated the active stubs at the top of the file. Collapsed to just the gaps that are actually still open. - `README.md` claimed Mesen symbol export and 210 tests; updated both. - `docs/language-guide.md` and `spec.md` described `palette` decls, `set_palette` / `load_background`, `debug.overlay`, and error codes that were never emitted. Trimmed. - Stale comments on `Statement::Play`/`StartMusic`/`StopMusic` claimed the audio subsystem was "a no-op at codegen time". Tests: - Regression tests for every fix above (`lower_shift_left_with_literal _count_uses_that_count`, `lower_shift_right_with_variable_count _uses_runtime_variant`, `lower_divide_emits_div_op_not_load_imm _zero`, `lower_modulo_emits_mod_op_not_load_imm_zero`, `strength_reduce_div_by_power_of_two`, `strength_reduce_mod_by _power_of_two`, `strength_reduce_shift_var_with_constant_amount`). - Renamed the `program_with_sprites_and_palette` integration test (which was exercising the now-removed `load_background`/`set_palette`) to `program_with_inline_sprite_chr`. `examples/sprites_and_palettes.ne` lost its `palette`/`set_palette` usage. Nothing in the emulator test presses A, so the headless jsnes render shouldn't move, but the golden may need regeneration via `UPDATE_GOLDENS=1` if it does. https://claude.ai/code/session_012fKB251HvEUQwG3tizFyqt
2026-04-13 02:47:37 +00:00
To build a browser IDE we would need to route file I/O through a trait so the
core pipeline works on `&str → Vec<u8>` without touching `std::fs`. Today the
parser's preprocess pass and the asset resolver read files directly.
---
cleanup: fix silent miscompiles and delete dead code exposed by code review Two correctness bugs were silently producing wrong ROMs: - `x << n` / `x >> n` always shifted by 1, regardless of `n`, because the IR lowering for `BinOp::ShiftLeft`/`ShiftRight` hardcoded the count. Now eval_const the RHS into a compile-time count; fall back to a new `IrOp::ShiftLeftVar` / `ShiftRightVar` (runtime loop) when the amount isn't constant. Strength reduction folds the variable form back to a fixed count once the optimizer knows the value. - `x / n` / `x % n` always returned 0, because the lowering emitted `LoadImm(t, 0)` for `BinOp::Div`/`Mod` with a comment saying the runtime call was "TODO for now". Added real `IrOp::Div` and `IrOp::Mod`, wired them through use-counting and DCE, gave codegen `__divide`-based implementations, and taught strength reduction to rewrite power-of-two divisors into shifts and modulo-by-2ⁿ into AND masks. Constant folding now handles `Mul`/`Div`/`Mod`/shifts too, which were previously left for the codegen to emit inefficient software calls. Dead code removed (no backward-compat shims kept): - `src/debug/` entirely. `DebugSymbols`, `SourceMap`, and the Mesen/.sym emitters had no callers outside their own tests; `main.rs` never wrote a symbol file. Documented the intent in `docs/future-work.md` so it comes back intentionally if needed. - `ErrorCode::E0202` (invalid cast) and `E0403` (unreachable state): defined, formatted, and marked `#[allow(dead_code)]` but never emitted. W0104 now carries the unreachable-state semantics too. - `Level::Info`: never constructed. - `load_background` / `set_palette` statements and their `BackgroundDecl` / `PaletteDecl` parser support: parsed and silently dropped by IR lowering (`// TODO: implement in asset pipeline`). Removed keywords, AST variants, parser paths, analyzer arms, and tests. `docs/future-work.md` documents the runtime palette/nametable design for when it comes back. Doc cleanup: - `docs/architecture.md` was describing files that don't exist (`analyzer/types.rs`, `optimizer/const_fold.rs`, `codegen/regalloc.rs`, `rom/header.rs`, `debug/symbols.rs`, …). Rewrote it to match the real flat `mod.rs` + `tests.rs` layout and the real pipeline order. - `docs/future-work.md` was a hybrid of open work and "recently completed" entries that duplicated the active stubs at the top of the file. Collapsed to just the gaps that are actually still open. - `README.md` claimed Mesen symbol export and 210 tests; updated both. - `docs/language-guide.md` and `spec.md` described `palette` decls, `set_palette` / `load_background`, `debug.overlay`, and error codes that were never emitted. Trimmed. - Stale comments on `Statement::Play`/`StartMusic`/`StopMusic` claimed the audio subsystem was "a no-op at codegen time". Tests: - Regression tests for every fix above (`lower_shift_left_with_literal _count_uses_that_count`, `lower_shift_right_with_variable_count _uses_runtime_variant`, `lower_divide_emits_div_op_not_load_imm _zero`, `lower_modulo_emits_mod_op_not_load_imm_zero`, `strength_reduce_div_by_power_of_two`, `strength_reduce_mod_by _power_of_two`, `strength_reduce_shift_var_with_constant_amount`). - Renamed the `program_with_sprites_and_palette` integration test (which was exercising the now-removed `load_background`/`set_palette`) to `program_with_inline_sprite_chr`. `examples/sprites_and_palettes.ne` lost its `palette`/`set_palette` usage. Nothing in the emulator test presses A, so the headless jsnes render shouldn't move, but the golden may need regeneration via `UPDATE_GOLDENS=1` if it does. https://claude.ai/code/session_012fKB251HvEUQwG3tizFyqt
2026-04-13 02:47:37 +00:00
## Error message polish
cleanup: fix silent miscompiles and delete dead code exposed by code review Two correctness bugs were silently producing wrong ROMs: - `x << n` / `x >> n` always shifted by 1, regardless of `n`, because the IR lowering for `BinOp::ShiftLeft`/`ShiftRight` hardcoded the count. Now eval_const the RHS into a compile-time count; fall back to a new `IrOp::ShiftLeftVar` / `ShiftRightVar` (runtime loop) when the amount isn't constant. Strength reduction folds the variable form back to a fixed count once the optimizer knows the value. - `x / n` / `x % n` always returned 0, because the lowering emitted `LoadImm(t, 0)` for `BinOp::Div`/`Mod` with a comment saying the runtime call was "TODO for now". Added real `IrOp::Div` and `IrOp::Mod`, wired them through use-counting and DCE, gave codegen `__divide`-based implementations, and taught strength reduction to rewrite power-of-two divisors into shifts and modulo-by-2ⁿ into AND masks. Constant folding now handles `Mul`/`Div`/`Mod`/shifts too, which were previously left for the codegen to emit inefficient software calls. Dead code removed (no backward-compat shims kept): - `src/debug/` entirely. `DebugSymbols`, `SourceMap`, and the Mesen/.sym emitters had no callers outside their own tests; `main.rs` never wrote a symbol file. Documented the intent in `docs/future-work.md` so it comes back intentionally if needed. - `ErrorCode::E0202` (invalid cast) and `E0403` (unreachable state): defined, formatted, and marked `#[allow(dead_code)]` but never emitted. W0104 now carries the unreachable-state semantics too. - `Level::Info`: never constructed. - `load_background` / `set_palette` statements and their `BackgroundDecl` / `PaletteDecl` parser support: parsed and silently dropped by IR lowering (`// TODO: implement in asset pipeline`). Removed keywords, AST variants, parser paths, analyzer arms, and tests. `docs/future-work.md` documents the runtime palette/nametable design for when it comes back. Doc cleanup: - `docs/architecture.md` was describing files that don't exist (`analyzer/types.rs`, `optimizer/const_fold.rs`, `codegen/regalloc.rs`, `rom/header.rs`, `debug/symbols.rs`, …). Rewrote it to match the real flat `mod.rs` + `tests.rs` layout and the real pipeline order. - `docs/future-work.md` was a hybrid of open work and "recently completed" entries that duplicated the active stubs at the top of the file. Collapsed to just the gaps that are actually still open. - `README.md` claimed Mesen symbol export and 210 tests; updated both. - `docs/language-guide.md` and `spec.md` described `palette` decls, `set_palette` / `load_background`, `debug.overlay`, and error codes that were never emitted. Trimmed. - Stale comments on `Statement::Play`/`StartMusic`/`StopMusic` claimed the audio subsystem was "a no-op at codegen time". Tests: - Regression tests for every fix above (`lower_shift_left_with_literal _count_uses_that_count`, `lower_shift_right_with_variable_count _uses_runtime_variant`, `lower_divide_emits_div_op_not_load_imm _zero`, `lower_modulo_emits_mod_op_not_load_imm_zero`, `strength_reduce_div_by_power_of_two`, `strength_reduce_mod_by _power_of_two`, `strength_reduce_shift_var_with_constant_amount`). - Renamed the `program_with_sprites_and_palette` integration test (which was exercising the now-removed `load_background`/`set_palette`) to `program_with_inline_sprite_chr`. `examples/sprites_and_palettes.ne` lost its `palette`/`set_palette` usage. Nothing in the emulator test presses A, so the headless jsnes render shouldn't move, but the golden may need regeneration via `UPDATE_GOLDENS=1` if it does. https://claude.ai/code/session_012fKB251HvEUQwG3tizFyqt
2026-04-13 02:47:37 +00:00
### Unused error codes
cleanup: fix silent miscompiles and delete dead code exposed by code review Two correctness bugs were silently producing wrong ROMs: - `x << n` / `x >> n` always shifted by 1, regardless of `n`, because the IR lowering for `BinOp::ShiftLeft`/`ShiftRight` hardcoded the count. Now eval_const the RHS into a compile-time count; fall back to a new `IrOp::ShiftLeftVar` / `ShiftRightVar` (runtime loop) when the amount isn't constant. Strength reduction folds the variable form back to a fixed count once the optimizer knows the value. - `x / n` / `x % n` always returned 0, because the lowering emitted `LoadImm(t, 0)` for `BinOp::Div`/`Mod` with a comment saying the runtime call was "TODO for now". Added real `IrOp::Div` and `IrOp::Mod`, wired them through use-counting and DCE, gave codegen `__divide`-based implementations, and taught strength reduction to rewrite power-of-two divisors into shifts and modulo-by-2ⁿ into AND masks. Constant folding now handles `Mul`/`Div`/`Mod`/shifts too, which were previously left for the codegen to emit inefficient software calls. Dead code removed (no backward-compat shims kept): - `src/debug/` entirely. `DebugSymbols`, `SourceMap`, and the Mesen/.sym emitters had no callers outside their own tests; `main.rs` never wrote a symbol file. Documented the intent in `docs/future-work.md` so it comes back intentionally if needed. - `ErrorCode::E0202` (invalid cast) and `E0403` (unreachable state): defined, formatted, and marked `#[allow(dead_code)]` but never emitted. W0104 now carries the unreachable-state semantics too. - `Level::Info`: never constructed. - `load_background` / `set_palette` statements and their `BackgroundDecl` / `PaletteDecl` parser support: parsed and silently dropped by IR lowering (`// TODO: implement in asset pipeline`). Removed keywords, AST variants, parser paths, analyzer arms, and tests. `docs/future-work.md` documents the runtime palette/nametable design for when it comes back. Doc cleanup: - `docs/architecture.md` was describing files that don't exist (`analyzer/types.rs`, `optimizer/const_fold.rs`, `codegen/regalloc.rs`, `rom/header.rs`, `debug/symbols.rs`, …). Rewrote it to match the real flat `mod.rs` + `tests.rs` layout and the real pipeline order. - `docs/future-work.md` was a hybrid of open work and "recently completed" entries that duplicated the active stubs at the top of the file. Collapsed to just the gaps that are actually still open. - `README.md` claimed Mesen symbol export and 210 tests; updated both. - `docs/language-guide.md` and `spec.md` described `palette` decls, `set_palette` / `load_background`, `debug.overlay`, and error codes that were never emitted. Trimmed. - Stale comments on `Statement::Play`/`StartMusic`/`StopMusic` claimed the audio subsystem was "a no-op at codegen time". Tests: - Regression tests for every fix above (`lower_shift_left_with_literal _count_uses_that_count`, `lower_shift_right_with_variable_count _uses_runtime_variant`, `lower_divide_emits_div_op_not_load_imm _zero`, `lower_modulo_emits_mod_op_not_load_imm_zero`, `strength_reduce_div_by_power_of_two`, `strength_reduce_mod_by _power_of_two`, `strength_reduce_shift_var_with_constant_amount`). - Renamed the `program_with_sprites_and_palette` integration test (which was exercising the now-removed `load_background`/`set_palette`) to `program_with_inline_sprite_chr`. `examples/sprites_and_palettes.ne` lost its `palette`/`set_palette` usage. Nothing in the emulator test presses A, so the headless jsnes render shouldn't move, but the golden may need regeneration via `UPDATE_GOLDENS=1` if it does. https://claude.ai/code/session_012fKB251HvEUQwG3tizFyqt
2026-04-13 02:47:37 +00:00
`ErrorCode` only defines codes that are actually emitted. Previously there
were placeholder variants (`E0202` invalid cast, `E0403` unreachable state)
marked `#[allow(dead_code)]`; those were removed during cleanup. If those
semantics come back, add the codes at that point.
---
cleanup: fix silent miscompiles and delete dead code exposed by code review Two correctness bugs were silently producing wrong ROMs: - `x << n` / `x >> n` always shifted by 1, regardless of `n`, because the IR lowering for `BinOp::ShiftLeft`/`ShiftRight` hardcoded the count. Now eval_const the RHS into a compile-time count; fall back to a new `IrOp::ShiftLeftVar` / `ShiftRightVar` (runtime loop) when the amount isn't constant. Strength reduction folds the variable form back to a fixed count once the optimizer knows the value. - `x / n` / `x % n` always returned 0, because the lowering emitted `LoadImm(t, 0)` for `BinOp::Div`/`Mod` with a comment saying the runtime call was "TODO for now". Added real `IrOp::Div` and `IrOp::Mod`, wired them through use-counting and DCE, gave codegen `__divide`-based implementations, and taught strength reduction to rewrite power-of-two divisors into shifts and modulo-by-2ⁿ into AND masks. Constant folding now handles `Mul`/`Div`/`Mod`/shifts too, which were previously left for the codegen to emit inefficient software calls. Dead code removed (no backward-compat shims kept): - `src/debug/` entirely. `DebugSymbols`, `SourceMap`, and the Mesen/.sym emitters had no callers outside their own tests; `main.rs` never wrote a symbol file. Documented the intent in `docs/future-work.md` so it comes back intentionally if needed. - `ErrorCode::E0202` (invalid cast) and `E0403` (unreachable state): defined, formatted, and marked `#[allow(dead_code)]` but never emitted. W0104 now carries the unreachable-state semantics too. - `Level::Info`: never constructed. - `load_background` / `set_palette` statements and their `BackgroundDecl` / `PaletteDecl` parser support: parsed and silently dropped by IR lowering (`// TODO: implement in asset pipeline`). Removed keywords, AST variants, parser paths, analyzer arms, and tests. `docs/future-work.md` documents the runtime palette/nametable design for when it comes back. Doc cleanup: - `docs/architecture.md` was describing files that don't exist (`analyzer/types.rs`, `optimizer/const_fold.rs`, `codegen/regalloc.rs`, `rom/header.rs`, `debug/symbols.rs`, …). Rewrote it to match the real flat `mod.rs` + `tests.rs` layout and the real pipeline order. - `docs/future-work.md` was a hybrid of open work and "recently completed" entries that duplicated the active stubs at the top of the file. Collapsed to just the gaps that are actually still open. - `README.md` claimed Mesen symbol export and 210 tests; updated both. - `docs/language-guide.md` and `spec.md` described `palette` decls, `set_palette` / `load_background`, `debug.overlay`, and error codes that were never emitted. Trimmed. - Stale comments on `Statement::Play`/`StartMusic`/`StopMusic` claimed the audio subsystem was "a no-op at codegen time". Tests: - Regression tests for every fix above (`lower_shift_left_with_literal _count_uses_that_count`, `lower_shift_right_with_variable_count _uses_runtime_variant`, `lower_divide_emits_div_op_not_load_imm _zero`, `lower_modulo_emits_mod_op_not_load_imm_zero`, `strength_reduce_div_by_power_of_two`, `strength_reduce_mod_by _power_of_two`, `strength_reduce_shift_var_with_constant_amount`). - Renamed the `program_with_sprites_and_palette` integration test (which was exercising the now-removed `load_background`/`set_palette`) to `program_with_inline_sprite_chr`. `examples/sprites_and_palettes.ne` lost its `palette`/`set_palette` usage. Nothing in the emulator test presses A, so the headless jsnes render shouldn't move, but the golden may need regeneration via `UPDATE_GOLDENS=1` if it does. https://claude.ai/code/session_012fKB251HvEUQwG3tizFyqt
2026-04-13 02:47:37 +00:00
## Open design questions
cleanup: fix silent miscompiles and delete dead code exposed by code review Two correctness bugs were silently producing wrong ROMs: - `x << n` / `x >> n` always shifted by 1, regardless of `n`, because the IR lowering for `BinOp::ShiftLeft`/`ShiftRight` hardcoded the count. Now eval_const the RHS into a compile-time count; fall back to a new `IrOp::ShiftLeftVar` / `ShiftRightVar` (runtime loop) when the amount isn't constant. Strength reduction folds the variable form back to a fixed count once the optimizer knows the value. - `x / n` / `x % n` always returned 0, because the lowering emitted `LoadImm(t, 0)` for `BinOp::Div`/`Mod` with a comment saying the runtime call was "TODO for now". Added real `IrOp::Div` and `IrOp::Mod`, wired them through use-counting and DCE, gave codegen `__divide`-based implementations, and taught strength reduction to rewrite power-of-two divisors into shifts and modulo-by-2ⁿ into AND masks. Constant folding now handles `Mul`/`Div`/`Mod`/shifts too, which were previously left for the codegen to emit inefficient software calls. Dead code removed (no backward-compat shims kept): - `src/debug/` entirely. `DebugSymbols`, `SourceMap`, and the Mesen/.sym emitters had no callers outside their own tests; `main.rs` never wrote a symbol file. Documented the intent in `docs/future-work.md` so it comes back intentionally if needed. - `ErrorCode::E0202` (invalid cast) and `E0403` (unreachable state): defined, formatted, and marked `#[allow(dead_code)]` but never emitted. W0104 now carries the unreachable-state semantics too. - `Level::Info`: never constructed. - `load_background` / `set_palette` statements and their `BackgroundDecl` / `PaletteDecl` parser support: parsed and silently dropped by IR lowering (`// TODO: implement in asset pipeline`). Removed keywords, AST variants, parser paths, analyzer arms, and tests. `docs/future-work.md` documents the runtime palette/nametable design for when it comes back. Doc cleanup: - `docs/architecture.md` was describing files that don't exist (`analyzer/types.rs`, `optimizer/const_fold.rs`, `codegen/regalloc.rs`, `rom/header.rs`, `debug/symbols.rs`, …). Rewrote it to match the real flat `mod.rs` + `tests.rs` layout and the real pipeline order. - `docs/future-work.md` was a hybrid of open work and "recently completed" entries that duplicated the active stubs at the top of the file. Collapsed to just the gaps that are actually still open. - `README.md` claimed Mesen symbol export and 210 tests; updated both. - `docs/language-guide.md` and `spec.md` described `palette` decls, `set_palette` / `load_background`, `debug.overlay`, and error codes that were never emitted. Trimmed. - Stale comments on `Statement::Play`/`StartMusic`/`StopMusic` claimed the audio subsystem was "a no-op at codegen time". Tests: - Regression tests for every fix above (`lower_shift_left_with_literal _count_uses_that_count`, `lower_shift_right_with_variable_count _uses_runtime_variant`, `lower_divide_emits_div_op_not_load_imm _zero`, `lower_modulo_emits_mod_op_not_load_imm_zero`, `strength_reduce_div_by_power_of_two`, `strength_reduce_mod_by _power_of_two`, `strength_reduce_shift_var_with_constant_amount`). - Renamed the `program_with_sprites_and_palette` integration test (which was exercising the now-removed `load_background`/`set_palette`) to `program_with_inline_sprite_chr`. `examples/sprites_and_palettes.ne` lost its `palette`/`set_palette` usage. Nothing in the emulator test presses A, so the headless jsnes render shouldn't move, but the golden may need regeneration via `UPDATE_GOLDENS=1` if it does. https://claude.ai/code/session_012fKB251HvEUQwG3tizFyqt
2026-04-13 02:47:37 +00:00
1. **Inline asm label syntax.** `.label:` (ca65 style) vs `label:` (generic)?
Today the inline-asm parser accepts `label:` but not `.label`; migrating
would be cheap but would invalidate any copy-pasted ca65 fragments.
2. **Debug port address.** $4800 is conventional but not universal. Should
we support multiple debug output methods?
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
3. **OAM allocation strategy.** Sequential allocation remains the default;
the `cycle_sprites` opt-in keyword rotates the DMA offset each frame so
scenes past the 8-per-scanline budget flicker instead of dropping the
same sprite every frame. Open question: should automatic cycling become
a `game` attribute (`sprite_flicker: true`) that emits the increment
without requiring a per-frame call, and/or add a `draw ... priority:
pinned` modifier for HUD sprites that must stay at low OAM slots?
cleanup: fix silent miscompiles and delete dead code exposed by code review Two correctness bugs were silently producing wrong ROMs: - `x << n` / `x >> n` always shifted by 1, regardless of `n`, because the IR lowering for `BinOp::ShiftLeft`/`ShiftRight` hardcoded the count. Now eval_const the RHS into a compile-time count; fall back to a new `IrOp::ShiftLeftVar` / `ShiftRightVar` (runtime loop) when the amount isn't constant. Strength reduction folds the variable form back to a fixed count once the optimizer knows the value. - `x / n` / `x % n` always returned 0, because the lowering emitted `LoadImm(t, 0)` for `BinOp::Div`/`Mod` with a comment saying the runtime call was "TODO for now". Added real `IrOp::Div` and `IrOp::Mod`, wired them through use-counting and DCE, gave codegen `__divide`-based implementations, and taught strength reduction to rewrite power-of-two divisors into shifts and modulo-by-2ⁿ into AND masks. Constant folding now handles `Mul`/`Div`/`Mod`/shifts too, which were previously left for the codegen to emit inefficient software calls. Dead code removed (no backward-compat shims kept): - `src/debug/` entirely. `DebugSymbols`, `SourceMap`, and the Mesen/.sym emitters had no callers outside their own tests; `main.rs` never wrote a symbol file. Documented the intent in `docs/future-work.md` so it comes back intentionally if needed. - `ErrorCode::E0202` (invalid cast) and `E0403` (unreachable state): defined, formatted, and marked `#[allow(dead_code)]` but never emitted. W0104 now carries the unreachable-state semantics too. - `Level::Info`: never constructed. - `load_background` / `set_palette` statements and their `BackgroundDecl` / `PaletteDecl` parser support: parsed and silently dropped by IR lowering (`// TODO: implement in asset pipeline`). Removed keywords, AST variants, parser paths, analyzer arms, and tests. `docs/future-work.md` documents the runtime palette/nametable design for when it comes back. Doc cleanup: - `docs/architecture.md` was describing files that don't exist (`analyzer/types.rs`, `optimizer/const_fold.rs`, `codegen/regalloc.rs`, `rom/header.rs`, `debug/symbols.rs`, …). Rewrote it to match the real flat `mod.rs` + `tests.rs` layout and the real pipeline order. - `docs/future-work.md` was a hybrid of open work and "recently completed" entries that duplicated the active stubs at the top of the file. Collapsed to just the gaps that are actually still open. - `README.md` claimed Mesen symbol export and 210 tests; updated both. - `docs/language-guide.md` and `spec.md` described `palette` decls, `set_palette` / `load_background`, `debug.overlay`, and error codes that were never emitted. Trimmed. - Stale comments on `Statement::Play`/`StartMusic`/`StopMusic` claimed the audio subsystem was "a no-op at codegen time". Tests: - Regression tests for every fix above (`lower_shift_left_with_literal _count_uses_that_count`, `lower_shift_right_with_variable_count _uses_runtime_variant`, `lower_divide_emits_div_op_not_load_imm _zero`, `lower_modulo_emits_mod_op_not_load_imm_zero`, `strength_reduce_div_by_power_of_two`, `strength_reduce_mod_by _power_of_two`, `strength_reduce_shift_var_with_constant_amount`). - Renamed the `program_with_sprites_and_palette` integration test (which was exercising the now-removed `load_background`/`set_palette`) to `program_with_inline_sprite_chr`. `examples/sprites_and_palettes.ne` lost its `palette`/`set_palette` usage. Nothing in the emulator test presses A, so the headless jsnes render shouldn't move, but the golden may need regeneration via `UPDATE_GOLDENS=1` if it does. https://claude.ai/code/session_012fKB251HvEUQwG3tizFyqt
2026-04-13 02:47:37 +00:00
4. **Error recovery granularity.** How aggressively should the parser
recover? More recovery means more errors per compile but also risks
cascading false errors.