2026-04-14 02:39:36 +00:00
|
|
|
mod debug_symbols;
|
Implement NEScript compiler Milestone 1 ("Hello Sprite")
Complete implementation of the NEScript compiler pipeline for M1:
- Lexer: full tokenization with hex/binary/decimal literals, all keywords, operators
- Parser: recursive descent with Pratt expression parsing (M1 subset)
- Analyzer: symbol resolution, type checking, memory allocation
- 6502 Assembler: full opcode encoding table (~150 valid combinations)
- Code Generator: AST → 6502 instructions (direct, no IR for M1)
- Runtime: NES hardware init, NMI handler, controller read, OAM DMA
- Linker: NROM layout, vector table, palette loading, CHR data
- ROM Builder: iNES header generation, PRG/CHR padding
- CLI: `build` and `check` subcommands via clap
143 tests across all modules:
- 22 lexer tests (literals, keywords, operators, error recovery)
- 18 parser tests (expressions, statements, game structure, errors)
- 7 analyzer tests (symbol resolution, memory allocation, transitions)
- 30 assembler tests (every addressing mode, label resolution)
- 7 codegen tests (var init, arithmetic, buttons, draw, comparisons)
- 11 runtime tests (init sequence, NMI handler, controller read)
- 10 ROM builder tests (iNES format, mirroring, banking, validation)
- 5 linker tests (vector table, CHR data, palette loading)
- 7 integration tests (end-to-end compilation, error detection)
CI: GitHub Actions for check, fmt, clippy, test
Pre-commit: script for local fmt + clippy + test validation
https://claude.ai/code/session_01W6eQFStA66EuMKHUFo2rx3
2026-04-11 22:07:56 +00:00
|
|
|
#[cfg(test)]
|
|
|
|
|
mod tests;
|
|
|
|
|
|
2026-04-14 02:39:36 +00:00
|
|
|
pub use debug_symbols::{render_mlb, render_source_map};
|
|
|
|
|
|
|
|
|
|
use std::collections::HashMap;
|
|
|
|
|
|
Implement NEScript compiler Milestone 1 ("Hello Sprite")
Complete implementation of the NEScript compiler pipeline for M1:
- Lexer: full tokenization with hex/binary/decimal literals, all keywords, operators
- Parser: recursive descent with Pratt expression parsing (M1 subset)
- Analyzer: symbol resolution, type checking, memory allocation
- 6502 Assembler: full opcode encoding table (~150 valid combinations)
- Code Generator: AST → 6502 instructions (direct, no IR for M1)
- Runtime: NES hardware init, NMI handler, controller read, OAM DMA
- Linker: NROM layout, vector table, palette loading, CHR data
- ROM Builder: iNES header generation, PRG/CHR padding
- CLI: `build` and `check` subcommands via clap
143 tests across all modules:
- 22 lexer tests (literals, keywords, operators, error recovery)
- 18 parser tests (expressions, statements, game structure, errors)
- 7 analyzer tests (symbol resolution, memory allocation, transitions)
- 30 assembler tests (every addressing mode, label resolution)
- 7 codegen tests (var init, arithmetic, buttons, draw, comparisons)
- 11 runtime tests (init sequence, NMI handler, controller read)
- 10 ROM builder tests (iNES format, mirroring, banking, validation)
- 5 linker tests (vector table, CHR data, palette loading)
- 7 integration tests (end-to-end compilation, error detection)
CI: GitHub Actions for check, fmt, clippy, test
Pre-commit: script for local fmt + clippy + test validation
https://claude.ai/code/session_01W6eQFStA66EuMKHUFo2rx3
2026-04-11 22:07:56 +00:00
|
|
|
use crate::asm;
|
|
|
|
|
use crate::asm::{AddressingMode as AM, Instruction, Opcode::*};
|
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
|
|
|
use crate::assets::{BackgroundData, MusicData, PaletteData, SfxData};
|
2026-04-14 02:05:51 +00:00
|
|
|
use crate::parser::ast::{HeaderFormat, Mapper, Mirroring};
|
Implement NEScript compiler Milestone 1 ("Hello Sprite")
Complete implementation of the NEScript compiler pipeline for M1:
- Lexer: full tokenization with hex/binary/decimal literals, all keywords, operators
- Parser: recursive descent with Pratt expression parsing (M1 subset)
- Analyzer: symbol resolution, type checking, memory allocation
- 6502 Assembler: full opcode encoding table (~150 valid combinations)
- Code Generator: AST → 6502 instructions (direct, no IR for M1)
- Runtime: NES hardware init, NMI handler, controller read, OAM DMA
- Linker: NROM layout, vector table, palette loading, CHR data
- ROM Builder: iNES header generation, PRG/CHR padding
- CLI: `build` and `check` subcommands via clap
143 tests across all modules:
- 22 lexer tests (literals, keywords, operators, error recovery)
- 18 parser tests (expressions, statements, game structure, errors)
- 7 analyzer tests (symbol resolution, memory allocation, transitions)
- 30 assembler tests (every addressing mode, label resolution)
- 7 codegen tests (var init, arithmetic, buttons, draw, comparisons)
- 11 runtime tests (init sequence, NMI handler, controller read)
- 10 ROM builder tests (iNES format, mirroring, banking, validation)
- 5 linker tests (vector table, CHR data, palette loading)
- 7 integration tests (end-to-end compilation, error detection)
CI: GitHub Actions for check, fmt, clippy, test
Pre-commit: script for local fmt + clippy + test validation
https://claude.ai/code/session_01W6eQFStA66EuMKHUFo2rx3
2026-04-11 22:07:56 +00:00
|
|
|
use crate::rom::RomBuilder;
|
|
|
|
|
use crate::runtime;
|
|
|
|
|
|
2026-04-14 02:39:36 +00:00
|
|
|
/// Detailed result of a link pass. In addition to the final iNES
|
|
|
|
|
/// ROM bytes this carries the assembler's symbol table — each label
|
|
|
|
|
/// defined anywhere in the assembled fixed bank mapped to its CPU
|
|
|
|
|
/// address — and the byte offset at which the fixed bank starts
|
|
|
|
|
/// inside the PRG ROM region of the file.
|
|
|
|
|
///
|
|
|
|
|
/// The CLI uses this metadata to emit Mesen-compatible `.mlb`
|
|
|
|
|
/// symbol files and source-to-ROM maps (via the `--symbols` /
|
|
|
|
|
/// `--source-map` flags). Callers that only care about the ROM
|
|
|
|
|
/// bytes can read `.rom` and discard the rest.
|
|
|
|
|
#[derive(Debug, Clone)]
|
|
|
|
|
pub struct LinkedRom {
|
|
|
|
|
/// Final iNES ROM bytes (header + PRG banks + CHR).
|
|
|
|
|
pub rom: Vec<u8>,
|
|
|
|
|
/// Every label defined in the fixed bank, mapped to its CPU
|
|
|
|
|
/// address in the $C000-$FFFF window. Populated by the
|
|
|
|
|
/// 6502 assembler's label pass.
|
|
|
|
|
pub labels: HashMap<String, u16>,
|
|
|
|
|
/// Byte offset of the fixed bank's first byte inside `rom`.
|
|
|
|
|
/// For NROM this is `16` (just past the 16-byte iNES header).
|
|
|
|
|
/// For banked mappers each switchable bank shifts it by 16 KB,
|
|
|
|
|
/// so the fixed bank starts at `16 + 16_384 * switchable_bank_count`.
|
|
|
|
|
pub fixed_bank_file_offset: usize,
|
|
|
|
|
}
|
|
|
|
|
|
Implement NEScript compiler Milestone 1 ("Hello Sprite")
Complete implementation of the NEScript compiler pipeline for M1:
- Lexer: full tokenization with hex/binary/decimal literals, all keywords, operators
- Parser: recursive descent with Pratt expression parsing (M1 subset)
- Analyzer: symbol resolution, type checking, memory allocation
- 6502 Assembler: full opcode encoding table (~150 valid combinations)
- Code Generator: AST → 6502 instructions (direct, no IR for M1)
- Runtime: NES hardware init, NMI handler, controller read, OAM DMA
- Linker: NROM layout, vector table, palette loading, CHR data
- ROM Builder: iNES header generation, PRG/CHR padding
- CLI: `build` and `check` subcommands via clap
143 tests across all modules:
- 22 lexer tests (literals, keywords, operators, error recovery)
- 18 parser tests (expressions, statements, game structure, errors)
- 7 analyzer tests (symbol resolution, memory allocation, transitions)
- 30 assembler tests (every addressing mode, label resolution)
- 7 codegen tests (var init, arithmetic, buttons, draw, comparisons)
- 11 runtime tests (init sequence, NMI handler, controller read)
- 10 ROM builder tests (iNES format, mirroring, banking, validation)
- 5 linker tests (vector table, CHR data, palette loading)
- 7 integration tests (end-to-end compilation, error detection)
CI: GitHub Actions for check, fmt, clippy, test
Pre-commit: script for local fmt + clippy + test validation
https://claude.ai/code/session_01W6eQFStA66EuMKHUFo2rx3
2026-04-11 22:07:56 +00:00
|
|
|
/// Link compiled code into a complete NES ROM.
|
|
|
|
|
pub struct Linker {
|
|
|
|
|
mirroring: Mirroring,
|
M4+M5: Optimizer passes, type casting, bank switching, math runtime
Milestone 4 — Optimization & Polish:
- Strength reduction: multiply by power-of-2 → shift left
- Zero-page promotion analysis: rank variables by access frequency
- `as` type casting expression in parser/AST/analyzer
- `scroll(x, y)` statement
- `--asm-dump` flag for viewing generated assembly
- Extended optimizer tests (strength reduction, frequency analysis)
Milestone 5 — Bank Switching & Release:
- Mapper support: MMC1 (1), UxROM (2), MMC3 (4) in parser and ROM builder
- Bank declarations: `bank Name: prg` / `bank Name: chr`
- Linker::with_mapper for mapper-aware ROM generation
- Software multiply (8x8→16, shift-and-add algorithm)
- Software divide (8÷8→8, restoring division algorithm)
- ROM tests for mapper encoding round-trip
- Integration test for MMC1 compilation
210 tests total (18 new), all pre-commit checks pass.
https://claude.ai/code/session_01W6eQFStA66EuMKHUFo2rx3
2026-04-12 00:22:11 +00:00
|
|
|
mapper: Mapper,
|
2026-04-14 02:05:51 +00:00
|
|
|
header_format: HeaderFormat,
|
Implement NEScript compiler Milestone 1 ("Hello Sprite")
Complete implementation of the NEScript compiler pipeline for M1:
- Lexer: full tokenization with hex/binary/decimal literals, all keywords, operators
- Parser: recursive descent with Pratt expression parsing (M1 subset)
- Analyzer: symbol resolution, type checking, memory allocation
- 6502 Assembler: full opcode encoding table (~150 valid combinations)
- Code Generator: AST → 6502 instructions (direct, no IR for M1)
- Runtime: NES hardware init, NMI handler, controller read, OAM DMA
- Linker: NROM layout, vector table, palette loading, CHR data
- ROM Builder: iNES header generation, PRG/CHR padding
- CLI: `build` and `check` subcommands via clap
143 tests across all modules:
- 22 lexer tests (literals, keywords, operators, error recovery)
- 18 parser tests (expressions, statements, game structure, errors)
- 7 analyzer tests (symbol resolution, memory allocation, transitions)
- 30 assembler tests (every addressing mode, label resolution)
- 7 codegen tests (var init, arithmetic, buttons, draw, comparisons)
- 11 runtime tests (init sequence, NMI handler, controller read)
- 10 ROM builder tests (iNES format, mirroring, banking, validation)
- 5 linker tests (vector table, CHR data, palette loading)
- 7 integration tests (end-to-end compilation, error detection)
CI: GitHub Actions for check, fmt, clippy, test
Pre-commit: script for local fmt + clippy + test validation
https://claude.ai/code/session_01W6eQFStA66EuMKHUFo2rx3
2026-04-11 22:07:56 +00:00
|
|
|
}
|
|
|
|
|
|
2026-04-12 10:01:44 +00:00
|
|
|
/// CHR data for a sprite, placed at a specific tile index in CHR ROM.
|
|
|
|
|
#[derive(Debug, Clone)]
|
|
|
|
|
pub struct SpriteData {
|
|
|
|
|
pub name: String,
|
|
|
|
|
pub tile_index: u8,
|
|
|
|
|
/// Raw CHR bytes (16 bytes per 8x8 tile).
|
|
|
|
|
pub chr_bytes: Vec<u8>,
|
|
|
|
|
}
|
|
|
|
|
|
banks: implement multi-bank PRG layout and bank-switching runtime
Prior to this commit the linker always shipped a single 16 KB PRG
bank regardless of the declared mapper, so the README's MMC1/UxROM/
MMC3 support was aspirational. This commit gives the three banked
mappers a real multi-bank ROM layout:
* RomBuilder.set_prg_banks() writes any number of 16 KB banks
back-to-back so the iNES header reflects the true PRG size.
* Linker.link_banked() places switchable banks first, fixed bank
last, so the fixed bank maps to $C000-$FFFF (the address window
where vectors and the runtime live).
* runtime::gen_mapper_init() emits reset-time mapper config:
MMC1 serial-writes a control-register value that pins the last
bank at $C000 with the correct mirroring, UxROM relies on the
power-on default, MMC3 writes the $8000/$8001/$A000/$E000
registers to get a known PRG and mirroring state.
* runtime::gen_bank_select() is a mapper-specific subroutine
(callable with the target bank in A) that maps any physical
bank to $8000-$BFFF.
* runtime::gen_bank_trampoline() generates a cross-bank call
stub in the fixed bank that saves the caller's bank, switches,
JSRs the target, and restores the fixed bank.
* The CLI and integration helper thread declared `bank X: prg`
declarations through to the linker so MMC1/UxROM/MMC3 programs
actually produce multi-bank ROMs.
Coverage:
* Runtime unit tests (18 new): mapper init patterns for every
supported mapper, bank-select signatures, trampoline dispatch
order, UxROM bus-conflict table contents.
* RomBuilder tests (6 new): multi-bank layout, padding,
byte-level fidelity, per-bank size validation, legacy
single-bank fallback.
* Linker tests (13 new): multi-bank ROM sizes across MMC1/
UxROM/MMC3, fixed-bank placement, switchable-bank payload
fidelity, bank-select subroutine detection, NROM rejection
of switchable banks.
* Integration e2e tests (16 new): compile real .ne sources
through the full pipeline and assert on iNES headers,
mapper init signatures in the fixed bank, vector locations,
and a regression check against `examples/mmc1_banked.ne`.
Total: 474 tests pass under `cargo test` with
`RUSTFLAGS="-D warnings"`.
https://claude.ai/code/session_01UCressA5e8k1XsuoJYLav2
2026-04-13 01:50:51 +00:00
|
|
|
/// A switchable PRG bank. Each switchable bank occupies a single
|
|
|
|
|
/// 16 KB slot in the ROM and can be mapped to $8000-$BFFF at runtime
|
|
|
|
|
/// by writing the bank's physical index to the mapper. The linker
|
|
|
|
|
/// places switchable banks in declaration order, followed by the
|
|
|
|
|
/// fixed bank at the end.
|
|
|
|
|
///
|
|
|
|
|
/// `entry_label` is the optional trampoline entry point inside this
|
|
|
|
|
/// bank — when set, the linker emits a `__tramp_<name>` stub in the
|
|
|
|
|
/// fixed bank that selects this bank and JSRs into the label.
|
|
|
|
|
/// `data` is raw bytes to splice verbatim (the compiler currently
|
|
|
|
|
/// only uses empty data and lets the linker pad with $FF).
|
|
|
|
|
#[derive(Debug, Clone)]
|
|
|
|
|
pub struct PrgBank {
|
|
|
|
|
pub name: String,
|
|
|
|
|
pub data: Vec<u8>,
|
|
|
|
|
pub entry_label: Option<String>,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl PrgBank {
|
|
|
|
|
/// Create an empty named bank. Convenience for the compiler,
|
|
|
|
|
/// which currently emits all user code into the fixed bank and
|
|
|
|
|
/// just wants switchable slots reserved for future use.
|
|
|
|
|
#[must_use]
|
|
|
|
|
pub fn empty(name: impl Into<String>) -> Self {
|
|
|
|
|
Self {
|
|
|
|
|
name: name.into(),
|
|
|
|
|
data: Vec::new(),
|
|
|
|
|
entry_label: None,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Create a bank with a raw byte payload and no trampoline entry.
|
|
|
|
|
#[must_use]
|
|
|
|
|
pub fn with_data(name: impl Into<String>, data: Vec<u8>) -> Self {
|
|
|
|
|
Self {
|
|
|
|
|
name: name.into(),
|
|
|
|
|
data,
|
|
|
|
|
entry_label: None,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-12 16:29:15 +00:00
|
|
|
/// True if `instructions` contains a label definition with the given
|
|
|
|
|
/// name. Labels are emitted as `NOP` pseudo-instructions whose mode
|
|
|
|
|
/// is `AddressingMode::Label(name)`.
|
|
|
|
|
fn has_label(instructions: &[Instruction], name: &str) -> bool {
|
|
|
|
|
instructions
|
|
|
|
|
.iter()
|
|
|
|
|
.any(|i| matches!(&i.mode, AM::Label(n) if n == name))
|
|
|
|
|
}
|
|
|
|
|
|
Implement NEScript compiler Milestone 1 ("Hello Sprite")
Complete implementation of the NEScript compiler pipeline for M1:
- Lexer: full tokenization with hex/binary/decimal literals, all keywords, operators
- Parser: recursive descent with Pratt expression parsing (M1 subset)
- Analyzer: symbol resolution, type checking, memory allocation
- 6502 Assembler: full opcode encoding table (~150 valid combinations)
- Code Generator: AST → 6502 instructions (direct, no IR for M1)
- Runtime: NES hardware init, NMI handler, controller read, OAM DMA
- Linker: NROM layout, vector table, palette loading, CHR data
- ROM Builder: iNES header generation, PRG/CHR padding
- CLI: `build` and `check` subcommands via clap
143 tests across all modules:
- 22 lexer tests (literals, keywords, operators, error recovery)
- 18 parser tests (expressions, statements, game structure, errors)
- 7 analyzer tests (symbol resolution, memory allocation, transitions)
- 30 assembler tests (every addressing mode, label resolution)
- 7 codegen tests (var init, arithmetic, buttons, draw, comparisons)
- 11 runtime tests (init sequence, NMI handler, controller read)
- 10 ROM builder tests (iNES format, mirroring, banking, validation)
- 5 linker tests (vector table, CHR data, palette loading)
- 7 integration tests (end-to-end compilation, error detection)
CI: GitHub Actions for check, fmt, clippy, test
Pre-commit: script for local fmt + clippy + test validation
https://claude.ai/code/session_01W6eQFStA66EuMKHUFo2rx3
2026-04-11 22:07:56 +00:00
|
|
|
/// A smiley face CHR tile for the default sprite (M1).
|
|
|
|
|
const DEFAULT_SPRITE_CHR: [u8; 16] = [
|
|
|
|
|
// Plane 0 (low bits)
|
|
|
|
|
0b0011_1100,
|
|
|
|
|
0b0100_0010,
|
|
|
|
|
0b1010_0101,
|
|
|
|
|
0b1000_0001,
|
|
|
|
|
0b1010_0101,
|
|
|
|
|
0b1001_1001,
|
|
|
|
|
0b0100_0010,
|
|
|
|
|
0b0011_1100,
|
|
|
|
|
// Plane 1 (high bits) — all zeros means color 1 only
|
|
|
|
|
0b0011_1100,
|
|
|
|
|
0b0111_1110,
|
|
|
|
|
0b1111_1111,
|
|
|
|
|
0b1111_1111,
|
|
|
|
|
0b1111_1111,
|
|
|
|
|
0b1111_1111,
|
|
|
|
|
0b0111_1110,
|
|
|
|
|
0b0011_1100,
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
/// Default palette data for M1 (writes to PPU $3F00).
|
|
|
|
|
const DEFAULT_PALETTE: [u8; 32] = [
|
|
|
|
|
// Background palettes
|
|
|
|
|
0x0F, 0x00, 0x10, 0x20, // palette 0 (black, dark gray, light gray, white)
|
|
|
|
|
0x0F, 0x06, 0x16, 0x26, // palette 1
|
|
|
|
|
0x0F, 0x09, 0x19, 0x29, // palette 2
|
|
|
|
|
0x0F, 0x01, 0x11, 0x21, // palette 3
|
|
|
|
|
// Sprite palettes
|
|
|
|
|
0x0F, 0x00, 0x10, 0x20, // sprite palette 0 (same as bg)
|
|
|
|
|
0x0F, 0x14, 0x24, 0x34, // sprite palette 1
|
|
|
|
|
0x0F, 0x1A, 0x2A, 0x3A, // sprite palette 2
|
|
|
|
|
0x0F, 0x12, 0x22, 0x32, // sprite palette 3
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
impl Linker {
|
|
|
|
|
pub fn new(mirroring: Mirroring) -> Self {
|
M4+M5: Optimizer passes, type casting, bank switching, math runtime
Milestone 4 — Optimization & Polish:
- Strength reduction: multiply by power-of-2 → shift left
- Zero-page promotion analysis: rank variables by access frequency
- `as` type casting expression in parser/AST/analyzer
- `scroll(x, y)` statement
- `--asm-dump` flag for viewing generated assembly
- Extended optimizer tests (strength reduction, frequency analysis)
Milestone 5 — Bank Switching & Release:
- Mapper support: MMC1 (1), UxROM (2), MMC3 (4) in parser and ROM builder
- Bank declarations: `bank Name: prg` / `bank Name: chr`
- Linker::with_mapper for mapper-aware ROM generation
- Software multiply (8x8→16, shift-and-add algorithm)
- Software divide (8÷8→8, restoring division algorithm)
- ROM tests for mapper encoding round-trip
- Integration test for MMC1 compilation
210 tests total (18 new), all pre-commit checks pass.
https://claude.ai/code/session_01W6eQFStA66EuMKHUFo2rx3
2026-04-12 00:22:11 +00:00
|
|
|
Self {
|
|
|
|
|
mirroring,
|
|
|
|
|
mapper: Mapper::NROM,
|
2026-04-14 02:05:51 +00:00
|
|
|
header_format: HeaderFormat::Ines1,
|
M4+M5: Optimizer passes, type casting, bank switching, math runtime
Milestone 4 — Optimization & Polish:
- Strength reduction: multiply by power-of-2 → shift left
- Zero-page promotion analysis: rank variables by access frequency
- `as` type casting expression in parser/AST/analyzer
- `scroll(x, y)` statement
- `--asm-dump` flag for viewing generated assembly
- Extended optimizer tests (strength reduction, frequency analysis)
Milestone 5 — Bank Switching & Release:
- Mapper support: MMC1 (1), UxROM (2), MMC3 (4) in parser and ROM builder
- Bank declarations: `bank Name: prg` / `bank Name: chr`
- Linker::with_mapper for mapper-aware ROM generation
- Software multiply (8x8→16, shift-and-add algorithm)
- Software divide (8÷8→8, restoring division algorithm)
- ROM tests for mapper encoding round-trip
- Integration test for MMC1 compilation
210 tests total (18 new), all pre-commit checks pass.
https://claude.ai/code/session_01W6eQFStA66EuMKHUFo2rx3
2026-04-12 00:22:11 +00:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn with_mapper(mirroring: Mirroring, mapper: Mapper) -> Self {
|
2026-04-14 02:05:51 +00:00
|
|
|
Self {
|
|
|
|
|
mirroring,
|
|
|
|
|
mapper,
|
|
|
|
|
header_format: HeaderFormat::Ines1,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Opt into the NES 2.0 header format for the emitted ROM.
|
|
|
|
|
/// Chainable builder method — returns `self` so callers can
|
|
|
|
|
/// write `Linker::with_mapper(m, p).with_header(HeaderFormat::Nes2)`.
|
|
|
|
|
#[must_use]
|
|
|
|
|
pub fn with_header(mut self, header: HeaderFormat) -> Self {
|
|
|
|
|
self.header_format = header;
|
|
|
|
|
self
|
Implement NEScript compiler Milestone 1 ("Hello Sprite")
Complete implementation of the NEScript compiler pipeline for M1:
- Lexer: full tokenization with hex/binary/decimal literals, all keywords, operators
- Parser: recursive descent with Pratt expression parsing (M1 subset)
- Analyzer: symbol resolution, type checking, memory allocation
- 6502 Assembler: full opcode encoding table (~150 valid combinations)
- Code Generator: AST → 6502 instructions (direct, no IR for M1)
- Runtime: NES hardware init, NMI handler, controller read, OAM DMA
- Linker: NROM layout, vector table, palette loading, CHR data
- ROM Builder: iNES header generation, PRG/CHR padding
- CLI: `build` and `check` subcommands via clap
143 tests across all modules:
- 22 lexer tests (literals, keywords, operators, error recovery)
- 18 parser tests (expressions, statements, game structure, errors)
- 7 analyzer tests (symbol resolution, memory allocation, transitions)
- 30 assembler tests (every addressing mode, label resolution)
- 7 codegen tests (var init, arithmetic, buttons, draw, comparisons)
- 11 runtime tests (init sequence, NMI handler, controller read)
- 10 ROM builder tests (iNES format, mirroring, banking, validation)
- 5 linker tests (vector table, CHR data, palette loading)
- 7 integration tests (end-to-end compilation, error detection)
CI: GitHub Actions for check, fmt, clippy, test
Pre-commit: script for local fmt + clippy + test validation
https://claude.ai/code/session_01W6eQFStA66EuMKHUFo2rx3
2026-04-11 22:07:56 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Link all code sections into a .nes ROM.
|
2026-04-12 10:01:44 +00:00
|
|
|
///
|
|
|
|
|
/// This is a thin wrapper around [`Linker::link_with_assets`] that passes
|
|
|
|
|
/// an empty sprite list, so the CHR ROM only contains the default smiley
|
|
|
|
|
/// tile at index 0.
|
Implement NEScript compiler Milestone 1 ("Hello Sprite")
Complete implementation of the NEScript compiler pipeline for M1:
- Lexer: full tokenization with hex/binary/decimal literals, all keywords, operators
- Parser: recursive descent with Pratt expression parsing (M1 subset)
- Analyzer: symbol resolution, type checking, memory allocation
- 6502 Assembler: full opcode encoding table (~150 valid combinations)
- Code Generator: AST → 6502 instructions (direct, no IR for M1)
- Runtime: NES hardware init, NMI handler, controller read, OAM DMA
- Linker: NROM layout, vector table, palette loading, CHR data
- ROM Builder: iNES header generation, PRG/CHR padding
- CLI: `build` and `check` subcommands via clap
143 tests across all modules:
- 22 lexer tests (literals, keywords, operators, error recovery)
- 18 parser tests (expressions, statements, game structure, errors)
- 7 analyzer tests (symbol resolution, memory allocation, transitions)
- 30 assembler tests (every addressing mode, label resolution)
- 7 codegen tests (var init, arithmetic, buttons, draw, comparisons)
- 11 runtime tests (init sequence, NMI handler, controller read)
- 10 ROM builder tests (iNES format, mirroring, banking, validation)
- 5 linker tests (vector table, CHR data, palette loading)
- 7 integration tests (end-to-end compilation, error detection)
CI: GitHub Actions for check, fmt, clippy, test
Pre-commit: script for local fmt + clippy + test validation
https://claude.ai/code/session_01W6eQFStA66EuMKHUFo2rx3
2026-04-11 22:07:56 +00:00
|
|
|
pub fn link(&self, user_code: &[Instruction]) -> Vec<u8> {
|
2026-04-12 10:01:44 +00:00
|
|
|
self.link_with_assets(user_code, &[])
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Link all code sections into a .nes ROM, placing sprite CHR data at
|
audio: complete the subsystem — asset pipeline, user decls, tracker-style driver
The audio subsystem was a sketch: `play name` / `start_music name` /
`stop_music` parsed, lowered, and emitted a few hardcoded register
writes from a builtin name table. No user-declared effects, no
per-frame envelope, no note streams, no real engine.
This flesh-out brings audio up to the quality bar of the rest of
the compiler (sprites, palettes, bank switching, scanline IRQ,
etc.) with a full data-driven pipeline:
## Asset pipeline (new `src/assets/audio.rs`)
- `sfx Name { duty, pitch, volume }` blocks compile into per-frame
pulse-1 envelopes. Pitch/volume arrays must match in length; each
entry is one NMI's worth of `$4000` data.
- `music Name { duty, volume, repeat, notes }` blocks compile into
flat `(pitch, duration)` streams for pulse 2. Pitch 0 is a rest,
1-60 indexes a builtin period table covering C1-B5.
- `resolve_sfx` / `resolve_music` walk the program for `play` /
`start_music` references and append builtin fallbacks for any
name that isn't user-declared — so `play coin` still works
without a `sfx Coin { ... }` block.
- Builtin effects (coin, jump, hit, click, cancel, shoot, step)
and tracks (theme, battle, victory, gameover) synthesize through
the same compile path as user decls — one data model, one driver.
## Runtime engine (`src/runtime/mod.rs`)
- `gen_audio_tick()` walks both channels every NMI: reads one
envelope byte through `(ZP_SFX_PTR),Y` -> writes `$4000`,
advances ptr, mutes on zero sentinel. Music decrements the note
counter, advances to the next `(pitch, dur)` pair on zero, looks
up the period through `(__period_table),Y`, loops on `0xFF 0xFF`.
- `gen_period_table()` emits a 60-entry equal-tempered table
(A4 = 440 Hz, NTSC 1.789773 MHz CPU clock) with length-counter
load bits pre-baked into each high byte.
- `gen_data_block()` emits a label + raw-bytes pseudo pair so
user sfx/music data can be spliced into PRG with regular labels
that the two-pass assembler resolves.
- New ZP layout: `$05/$06` music loop base, `$07` music state
(duty/volume/loop/active), `$0C-$0F` sfx and music pointers.
## IR codegen (`src/codegen/ir_codegen.rs`)
- `with_audio(sfx, music)` registers compile-time trigger constants
per blob name.
- `gen_play_sfx` emits: write period to `$4002`/`$4003`, load
envelope pointer into `ZP_SFX_PTR` via SymbolLo/SymbolHi of
`__sfx_<name>`, mark the sfx counter active.
- `gen_start_music` stamps the header byte into `ZP_MUSIC_STATE`
with the active bit OR'd in, seeds both ptr and loop base from
`__music_<name>`, primes the duration counter.
- `gen_stop_music` mutes pulse 2 and clears state.
## Linker (`src/linker/mod.rs`)
- New `link_with_all_assets(user_code, sprites, sfx, music)` path
that splices driver body, period table, and each sfx/music data
blob into PRG — all guarded on the `__audio_used` marker so
silent programs pay zero ROM cost.
## Assembler (`src/asm/opcodes.rs`, `src/asm/mod.rs`)
- New `AddressingMode::Bytes(Vec<u8>)` variant for raw-data
pseudo-instructions. `NOP+Bytes(v)` emits the payload verbatim,
letting the linker splice ROM data tables into a code section
and still have `Label` / `SymbolLo` / `SymbolHi` fixups resolve
correctly in the same assembly pass.
## Analyzer
- `play` / `start_music` now validate the name against user decls
and builtin tables. Unknown names emit E0505 with a helpful list
of builtins — previously a typo would silently compile to no-op.
## Parser
- New `sfx_decl` / `music_decl` grammar with property-style
configuration. Strict validation: duty 0-3, volume 0-15, pitch
arrays must match volume length, music notes must come in pairs,
pitch 0-60, duration ≥ 1.
## Tests
+170 new tests across every layer:
- `src/assets/audio.rs`: 17 tests (compile, resolve, builtins,
shadowing, label sanitation, nested reference walks)
- `src/parser/tests.rs`: 13 tests (valid/invalid sfx + music
declarations, property validation, play/start_music/stop_music)
- `src/analyzer/tests.rs`: 7 tests (builtin acceptance, user decl
acceptance, unknown-name rejection)
- `src/runtime/tests.rs`: 10 tests (audio tick labels, RTS end,
$4000 write, $4004 mute, period table assembly, A4 = 440 Hz,
length counter bits, data block verbatim emit)
- `src/linker/tests.rs`: 4 tests (sfx/music blob placement,
pointer resolution, elision when unused)
- `src/codegen/ir_codegen.rs`: rewrote the 4 existing audio tests
to match the new data-driven contract
- `tests/integration_test.rs`: 4 end-to-end tests including a
user-declared `sfx` + `music` program that verifies bytes land
in PRG ROM at the right addresses
## Docs
- New Audio section in `docs/language-guide.md` with syntax
reference, builtin tables, and an explanation of how the
driver works at compile and run time.
- `docs/architecture.md` updated to reflect the real audio
pipeline instead of the old "audio import stubs" stub.
- `docs/future-work.md` moves audio from "status: minimal" to
"status: full subsystem" with a narrower list of follow-up work
(triangle/noise/DMC channels, NSF/FTM imports, richer envelopes).
- `examples/audio_demo.ne` rewritten to showcase user-declared
`sfx LongCoin`, `sfx Zap`, `music Theme`, still demonstrating
builtin fallback via `play coin`.
Total: 424 tests passing (381 unit + 43 integration), clippy clean,
fmt clean, all 19 examples compile.
https://claude.ai/code/session_015WfaDttE3DpWn9rpyfpQd8
2026-04-13 01:10:21 +00:00
|
|
|
/// specific tile indices. No audio data is linked — use
|
|
|
|
|
/// [`Linker::link_with_all_assets`] for audio.
|
2026-04-12 10:01:44 +00:00
|
|
|
pub fn link_with_assets(&self, user_code: &[Instruction], sprites: &[SpriteData]) -> Vec<u8> {
|
audio: complete the subsystem — asset pipeline, user decls, tracker-style driver
The audio subsystem was a sketch: `play name` / `start_music name` /
`stop_music` parsed, lowered, and emitted a few hardcoded register
writes from a builtin name table. No user-declared effects, no
per-frame envelope, no note streams, no real engine.
This flesh-out brings audio up to the quality bar of the rest of
the compiler (sprites, palettes, bank switching, scanline IRQ,
etc.) with a full data-driven pipeline:
## Asset pipeline (new `src/assets/audio.rs`)
- `sfx Name { duty, pitch, volume }` blocks compile into per-frame
pulse-1 envelopes. Pitch/volume arrays must match in length; each
entry is one NMI's worth of `$4000` data.
- `music Name { duty, volume, repeat, notes }` blocks compile into
flat `(pitch, duration)` streams for pulse 2. Pitch 0 is a rest,
1-60 indexes a builtin period table covering C1-B5.
- `resolve_sfx` / `resolve_music` walk the program for `play` /
`start_music` references and append builtin fallbacks for any
name that isn't user-declared — so `play coin` still works
without a `sfx Coin { ... }` block.
- Builtin effects (coin, jump, hit, click, cancel, shoot, step)
and tracks (theme, battle, victory, gameover) synthesize through
the same compile path as user decls — one data model, one driver.
## Runtime engine (`src/runtime/mod.rs`)
- `gen_audio_tick()` walks both channels every NMI: reads one
envelope byte through `(ZP_SFX_PTR),Y` -> writes `$4000`,
advances ptr, mutes on zero sentinel. Music decrements the note
counter, advances to the next `(pitch, dur)` pair on zero, looks
up the period through `(__period_table),Y`, loops on `0xFF 0xFF`.
- `gen_period_table()` emits a 60-entry equal-tempered table
(A4 = 440 Hz, NTSC 1.789773 MHz CPU clock) with length-counter
load bits pre-baked into each high byte.
- `gen_data_block()` emits a label + raw-bytes pseudo pair so
user sfx/music data can be spliced into PRG with regular labels
that the two-pass assembler resolves.
- New ZP layout: `$05/$06` music loop base, `$07` music state
(duty/volume/loop/active), `$0C-$0F` sfx and music pointers.
## IR codegen (`src/codegen/ir_codegen.rs`)
- `with_audio(sfx, music)` registers compile-time trigger constants
per blob name.
- `gen_play_sfx` emits: write period to `$4002`/`$4003`, load
envelope pointer into `ZP_SFX_PTR` via SymbolLo/SymbolHi of
`__sfx_<name>`, mark the sfx counter active.
- `gen_start_music` stamps the header byte into `ZP_MUSIC_STATE`
with the active bit OR'd in, seeds both ptr and loop base from
`__music_<name>`, primes the duration counter.
- `gen_stop_music` mutes pulse 2 and clears state.
## Linker (`src/linker/mod.rs`)
- New `link_with_all_assets(user_code, sprites, sfx, music)` path
that splices driver body, period table, and each sfx/music data
blob into PRG — all guarded on the `__audio_used` marker so
silent programs pay zero ROM cost.
## Assembler (`src/asm/opcodes.rs`, `src/asm/mod.rs`)
- New `AddressingMode::Bytes(Vec<u8>)` variant for raw-data
pseudo-instructions. `NOP+Bytes(v)` emits the payload verbatim,
letting the linker splice ROM data tables into a code section
and still have `Label` / `SymbolLo` / `SymbolHi` fixups resolve
correctly in the same assembly pass.
## Analyzer
- `play` / `start_music` now validate the name against user decls
and builtin tables. Unknown names emit E0505 with a helpful list
of builtins — previously a typo would silently compile to no-op.
## Parser
- New `sfx_decl` / `music_decl` grammar with property-style
configuration. Strict validation: duty 0-3, volume 0-15, pitch
arrays must match volume length, music notes must come in pairs,
pitch 0-60, duration ≥ 1.
## Tests
+170 new tests across every layer:
- `src/assets/audio.rs`: 17 tests (compile, resolve, builtins,
shadowing, label sanitation, nested reference walks)
- `src/parser/tests.rs`: 13 tests (valid/invalid sfx + music
declarations, property validation, play/start_music/stop_music)
- `src/analyzer/tests.rs`: 7 tests (builtin acceptance, user decl
acceptance, unknown-name rejection)
- `src/runtime/tests.rs`: 10 tests (audio tick labels, RTS end,
$4000 write, $4004 mute, period table assembly, A4 = 440 Hz,
length counter bits, data block verbatim emit)
- `src/linker/tests.rs`: 4 tests (sfx/music blob placement,
pointer resolution, elision when unused)
- `src/codegen/ir_codegen.rs`: rewrote the 4 existing audio tests
to match the new data-driven contract
- `tests/integration_test.rs`: 4 end-to-end tests including a
user-declared `sfx` + `music` program that verifies bytes land
in PRG ROM at the right addresses
## Docs
- New Audio section in `docs/language-guide.md` with syntax
reference, builtin tables, and an explanation of how the
driver works at compile and run time.
- `docs/architecture.md` updated to reflect the real audio
pipeline instead of the old "audio import stubs" stub.
- `docs/future-work.md` moves audio from "status: minimal" to
"status: full subsystem" with a narrower list of follow-up work
(triangle/noise/DMC channels, NSF/FTM imports, richer envelopes).
- `examples/audio_demo.ne` rewritten to showcase user-declared
`sfx LongCoin`, `sfx Zap`, `music Theme`, still demonstrating
builtin fallback via `play coin`.
Total: 424 tests passing (381 unit + 43 integration), clippy clean,
fmt clean, all 19 examples compile.
https://claude.ai/code/session_015WfaDttE3DpWn9rpyfpQd8
2026-04-13 01:10:21 +00:00
|
|
|
self.link_with_all_assets(user_code, sprites, &[], &[])
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Link all code sections into a .nes ROM, placing both graphic
|
|
|
|
|
/// assets (sprite CHR) and audio assets (sfx envelopes, music
|
|
|
|
|
/// note streams) into the appropriate ROM regions.
|
|
|
|
|
///
|
|
|
|
|
/// Audio data is spliced into PRG ROM under labels derived from
|
|
|
|
|
/// each blob's name (see `SfxData::label` / `MusicData::label`).
|
|
|
|
|
/// The linker only emits these blobs and the audio-driver body
|
|
|
|
|
/// when user code contains the `__audio_used` marker label, so
|
|
|
|
|
/// programs that never touch audio pay zero ROM cost.
|
|
|
|
|
pub fn link_with_all_assets(
|
|
|
|
|
&self,
|
|
|
|
|
user_code: &[Instruction],
|
|
|
|
|
sprites: &[SpriteData],
|
|
|
|
|
sfx: &[SfxData],
|
|
|
|
|
music: &[MusicData],
|
|
|
|
|
) -> Vec<u8> {
|
banks: implement multi-bank PRG layout and bank-switching runtime
Prior to this commit the linker always shipped a single 16 KB PRG
bank regardless of the declared mapper, so the README's MMC1/UxROM/
MMC3 support was aspirational. This commit gives the three banked
mappers a real multi-bank ROM layout:
* RomBuilder.set_prg_banks() writes any number of 16 KB banks
back-to-back so the iNES header reflects the true PRG size.
* Linker.link_banked() places switchable banks first, fixed bank
last, so the fixed bank maps to $C000-$FFFF (the address window
where vectors and the runtime live).
* runtime::gen_mapper_init() emits reset-time mapper config:
MMC1 serial-writes a control-register value that pins the last
bank at $C000 with the correct mirroring, UxROM relies on the
power-on default, MMC3 writes the $8000/$8001/$A000/$E000
registers to get a known PRG and mirroring state.
* runtime::gen_bank_select() is a mapper-specific subroutine
(callable with the target bank in A) that maps any physical
bank to $8000-$BFFF.
* runtime::gen_bank_trampoline() generates a cross-bank call
stub in the fixed bank that saves the caller's bank, switches,
JSRs the target, and restores the fixed bank.
* The CLI and integration helper thread declared `bank X: prg`
declarations through to the linker so MMC1/UxROM/MMC3 programs
actually produce multi-bank ROMs.
Coverage:
* Runtime unit tests (18 new): mapper init patterns for every
supported mapper, bank-select signatures, trampoline dispatch
order, UxROM bus-conflict table contents.
* RomBuilder tests (6 new): multi-bank layout, padding,
byte-level fidelity, per-bank size validation, legacy
single-bank fallback.
* Linker tests (13 new): multi-bank ROM sizes across MMC1/
UxROM/MMC3, fixed-bank placement, switchable-bank payload
fidelity, bank-select subroutine detection, NROM rejection
of switchable banks.
* Integration e2e tests (16 new): compile real .ne sources
through the full pipeline and assert on iNES headers,
mapper init signatures in the fixed bank, vector locations,
and a regression check against `examples/mmc1_banked.ne`.
Total: 474 tests pass under `cargo test` with
`RUSTFLAGS="-D warnings"`.
https://claude.ai/code/session_01UCressA5e8k1XsuoJYLav2
2026-04-13 01:50:51 +00:00
|
|
|
self.link_banked(user_code, sprites, sfx, music, &[])
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Link with the full asset pipeline plus zero or more
|
|
|
|
|
/// switchable PRG banks. The switchable banks are written in
|
|
|
|
|
/// declaration order and the fixed bank (which contains the
|
|
|
|
|
/// runtime, NMI/IRQ handlers, vector table, bank-select
|
|
|
|
|
/// subroutine, and all user code) is always placed last.
|
|
|
|
|
///
|
|
|
|
|
/// For mappers that don't support banking (NROM) this is an
|
|
|
|
|
/// error if any switchable banks are supplied. For banked
|
|
|
|
|
/// mappers the linker also splices `gen_mapper_init` into the
|
|
|
|
|
/// reset path and emits a `__bank_select` subroutine plus one
|
|
|
|
|
/// `__tramp_<name>` trampoline for every bank that declares an
|
|
|
|
|
/// `entry_label`.
|
|
|
|
|
pub fn link_banked(
|
|
|
|
|
&self,
|
|
|
|
|
user_code: &[Instruction],
|
|
|
|
|
sprites: &[SpriteData],
|
|
|
|
|
sfx: &[SfxData],
|
|
|
|
|
music: &[MusicData],
|
|
|
|
|
switchable_banks: &[PrgBank],
|
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
|
|
|
) -> Vec<u8> {
|
|
|
|
|
self.link_banked_with_ppu(user_code, sprites, sfx, music, &[], &[], switchable_banks)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Link with full asset pipeline including palette and
|
|
|
|
|
/// background data blobs. Palettes and backgrounds each emit a
|
|
|
|
|
/// labelled data block inside PRG ROM; the first declared
|
|
|
|
|
/// palette / background is loaded at reset time before
|
|
|
|
|
/// rendering is enabled, and any additional ones become
|
|
|
|
|
/// addressable via `set_palette` / `load_background` (which
|
|
|
|
|
/// queue a vblank-safe write).
|
|
|
|
|
#[allow(clippy::too_many_arguments)]
|
|
|
|
|
pub fn link_banked_with_ppu(
|
|
|
|
|
&self,
|
|
|
|
|
user_code: &[Instruction],
|
|
|
|
|
sprites: &[SpriteData],
|
|
|
|
|
sfx: &[SfxData],
|
|
|
|
|
music: &[MusicData],
|
|
|
|
|
palettes: &[PaletteData],
|
|
|
|
|
backgrounds: &[BackgroundData],
|
|
|
|
|
switchable_banks: &[PrgBank],
|
banks: implement multi-bank PRG layout and bank-switching runtime
Prior to this commit the linker always shipped a single 16 KB PRG
bank regardless of the declared mapper, so the README's MMC1/UxROM/
MMC3 support was aspirational. This commit gives the three banked
mappers a real multi-bank ROM layout:
* RomBuilder.set_prg_banks() writes any number of 16 KB banks
back-to-back so the iNES header reflects the true PRG size.
* Linker.link_banked() places switchable banks first, fixed bank
last, so the fixed bank maps to $C000-$FFFF (the address window
where vectors and the runtime live).
* runtime::gen_mapper_init() emits reset-time mapper config:
MMC1 serial-writes a control-register value that pins the last
bank at $C000 with the correct mirroring, UxROM relies on the
power-on default, MMC3 writes the $8000/$8001/$A000/$E000
registers to get a known PRG and mirroring state.
* runtime::gen_bank_select() is a mapper-specific subroutine
(callable with the target bank in A) that maps any physical
bank to $8000-$BFFF.
* runtime::gen_bank_trampoline() generates a cross-bank call
stub in the fixed bank that saves the caller's bank, switches,
JSRs the target, and restores the fixed bank.
* The CLI and integration helper thread declared `bank X: prg`
declarations through to the linker so MMC1/UxROM/MMC3 programs
actually produce multi-bank ROMs.
Coverage:
* Runtime unit tests (18 new): mapper init patterns for every
supported mapper, bank-select signatures, trampoline dispatch
order, UxROM bus-conflict table contents.
* RomBuilder tests (6 new): multi-bank layout, padding,
byte-level fidelity, per-bank size validation, legacy
single-bank fallback.
* Linker tests (13 new): multi-bank ROM sizes across MMC1/
UxROM/MMC3, fixed-bank placement, switchable-bank payload
fidelity, bank-select subroutine detection, NROM rejection
of switchable banks.
* Integration e2e tests (16 new): compile real .ne sources
through the full pipeline and assert on iNES headers,
mapper init signatures in the fixed bank, vector locations,
and a regression check against `examples/mmc1_banked.ne`.
Total: 474 tests pass under `cargo test` with
`RUSTFLAGS="-D warnings"`.
https://claude.ai/code/session_01UCressA5e8k1XsuoJYLav2
2026-04-13 01:50:51 +00:00
|
|
|
) -> Vec<u8> {
|
2026-04-14 02:39:36 +00:00
|
|
|
self.link_banked_with_ppu_detailed(
|
|
|
|
|
user_code,
|
|
|
|
|
sprites,
|
|
|
|
|
sfx,
|
|
|
|
|
music,
|
|
|
|
|
palettes,
|
|
|
|
|
backgrounds,
|
|
|
|
|
switchable_banks,
|
|
|
|
|
)
|
|
|
|
|
.rom
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Like [`Linker::link_banked_with_ppu`] but returns the full
|
|
|
|
|
/// [`LinkedRom`] record, carrying the assembler label table and
|
|
|
|
|
/// the PRG offset of the fixed bank alongside the ROM bytes.
|
|
|
|
|
/// This is the entry point used by the CLI when emitting a
|
|
|
|
|
/// `.mlb` symbol file or a source-map file.
|
|
|
|
|
#[allow(clippy::too_many_arguments)]
|
|
|
|
|
pub fn link_banked_with_ppu_detailed(
|
|
|
|
|
&self,
|
|
|
|
|
user_code: &[Instruction],
|
|
|
|
|
sprites: &[SpriteData],
|
|
|
|
|
sfx: &[SfxData],
|
|
|
|
|
music: &[MusicData],
|
|
|
|
|
palettes: &[PaletteData],
|
|
|
|
|
backgrounds: &[BackgroundData],
|
|
|
|
|
switchable_banks: &[PrgBank],
|
|
|
|
|
) -> LinkedRom {
|
banks: implement multi-bank PRG layout and bank-switching runtime
Prior to this commit the linker always shipped a single 16 KB PRG
bank regardless of the declared mapper, so the README's MMC1/UxROM/
MMC3 support was aspirational. This commit gives the three banked
mappers a real multi-bank ROM layout:
* RomBuilder.set_prg_banks() writes any number of 16 KB banks
back-to-back so the iNES header reflects the true PRG size.
* Linker.link_banked() places switchable banks first, fixed bank
last, so the fixed bank maps to $C000-$FFFF (the address window
where vectors and the runtime live).
* runtime::gen_mapper_init() emits reset-time mapper config:
MMC1 serial-writes a control-register value that pins the last
bank at $C000 with the correct mirroring, UxROM relies on the
power-on default, MMC3 writes the $8000/$8001/$A000/$E000
registers to get a known PRG and mirroring state.
* runtime::gen_bank_select() is a mapper-specific subroutine
(callable with the target bank in A) that maps any physical
bank to $8000-$BFFF.
* runtime::gen_bank_trampoline() generates a cross-bank call
stub in the fixed bank that saves the caller's bank, switches,
JSRs the target, and restores the fixed bank.
* The CLI and integration helper thread declared `bank X: prg`
declarations through to the linker so MMC1/UxROM/MMC3 programs
actually produce multi-bank ROMs.
Coverage:
* Runtime unit tests (18 new): mapper init patterns for every
supported mapper, bank-select signatures, trampoline dispatch
order, UxROM bus-conflict table contents.
* RomBuilder tests (6 new): multi-bank layout, padding,
byte-level fidelity, per-bank size validation, legacy
single-bank fallback.
* Linker tests (13 new): multi-bank ROM sizes across MMC1/
UxROM/MMC3, fixed-bank placement, switchable-bank payload
fidelity, bank-select subroutine detection, NROM rejection
of switchable banks.
* Integration e2e tests (16 new): compile real .ne sources
through the full pipeline and assert on iNES headers,
mapper init signatures in the fixed bank, vector locations,
and a regression check against `examples/mmc1_banked.ne`.
Total: 474 tests pass under `cargo test` with
`RUSTFLAGS="-D warnings"`.
https://claude.ai/code/session_01UCressA5e8k1XsuoJYLav2
2026-04-13 01:50:51 +00:00
|
|
|
assert!(
|
|
|
|
|
switchable_banks.is_empty() || self.mapper != Mapper::NROM,
|
|
|
|
|
"NROM does not support switchable PRG banks (got {} banks)",
|
|
|
|
|
switchable_banks.len()
|
|
|
|
|
);
|
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
|
|
|
self.link_banked_inner(
|
|
|
|
|
user_code,
|
|
|
|
|
sprites,
|
|
|
|
|
sfx,
|
|
|
|
|
music,
|
|
|
|
|
palettes,
|
|
|
|
|
backgrounds,
|
|
|
|
|
switchable_banks,
|
|
|
|
|
)
|
banks: implement multi-bank PRG layout and bank-switching runtime
Prior to this commit the linker always shipped a single 16 KB PRG
bank regardless of the declared mapper, so the README's MMC1/UxROM/
MMC3 support was aspirational. This commit gives the three banked
mappers a real multi-bank ROM layout:
* RomBuilder.set_prg_banks() writes any number of 16 KB banks
back-to-back so the iNES header reflects the true PRG size.
* Linker.link_banked() places switchable banks first, fixed bank
last, so the fixed bank maps to $C000-$FFFF (the address window
where vectors and the runtime live).
* runtime::gen_mapper_init() emits reset-time mapper config:
MMC1 serial-writes a control-register value that pins the last
bank at $C000 with the correct mirroring, UxROM relies on the
power-on default, MMC3 writes the $8000/$8001/$A000/$E000
registers to get a known PRG and mirroring state.
* runtime::gen_bank_select() is a mapper-specific subroutine
(callable with the target bank in A) that maps any physical
bank to $8000-$BFFF.
* runtime::gen_bank_trampoline() generates a cross-bank call
stub in the fixed bank that saves the caller's bank, switches,
JSRs the target, and restores the fixed bank.
* The CLI and integration helper thread declared `bank X: prg`
declarations through to the linker so MMC1/UxROM/MMC3 programs
actually produce multi-bank ROMs.
Coverage:
* Runtime unit tests (18 new): mapper init patterns for every
supported mapper, bank-select signatures, trampoline dispatch
order, UxROM bus-conflict table contents.
* RomBuilder tests (6 new): multi-bank layout, padding,
byte-level fidelity, per-bank size validation, legacy
single-bank fallback.
* Linker tests (13 new): multi-bank ROM sizes across MMC1/
UxROM/MMC3, fixed-bank placement, switchable-bank payload
fidelity, bank-select subroutine detection, NROM rejection
of switchable banks.
* Integration e2e tests (16 new): compile real .ne sources
through the full pipeline and assert on iNES headers,
mapper init signatures in the fixed bank, vector locations,
and a regression check against `examples/mmc1_banked.ne`.
Total: 474 tests pass under `cargo test` with
`RUSTFLAGS="-D warnings"`.
https://claude.ai/code/session_01UCressA5e8k1XsuoJYLav2
2026-04-13 01:50:51 +00:00
|
|
|
}
|
|
|
|
|
|
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
|
|
|
#[allow(clippy::too_many_arguments)]
|
banks: implement multi-bank PRG layout and bank-switching runtime
Prior to this commit the linker always shipped a single 16 KB PRG
bank regardless of the declared mapper, so the README's MMC1/UxROM/
MMC3 support was aspirational. This commit gives the three banked
mappers a real multi-bank ROM layout:
* RomBuilder.set_prg_banks() writes any number of 16 KB banks
back-to-back so the iNES header reflects the true PRG size.
* Linker.link_banked() places switchable banks first, fixed bank
last, so the fixed bank maps to $C000-$FFFF (the address window
where vectors and the runtime live).
* runtime::gen_mapper_init() emits reset-time mapper config:
MMC1 serial-writes a control-register value that pins the last
bank at $C000 with the correct mirroring, UxROM relies on the
power-on default, MMC3 writes the $8000/$8001/$A000/$E000
registers to get a known PRG and mirroring state.
* runtime::gen_bank_select() is a mapper-specific subroutine
(callable with the target bank in A) that maps any physical
bank to $8000-$BFFF.
* runtime::gen_bank_trampoline() generates a cross-bank call
stub in the fixed bank that saves the caller's bank, switches,
JSRs the target, and restores the fixed bank.
* The CLI and integration helper thread declared `bank X: prg`
declarations through to the linker so MMC1/UxROM/MMC3 programs
actually produce multi-bank ROMs.
Coverage:
* Runtime unit tests (18 new): mapper init patterns for every
supported mapper, bank-select signatures, trampoline dispatch
order, UxROM bus-conflict table contents.
* RomBuilder tests (6 new): multi-bank layout, padding,
byte-level fidelity, per-bank size validation, legacy
single-bank fallback.
* Linker tests (13 new): multi-bank ROM sizes across MMC1/
UxROM/MMC3, fixed-bank placement, switchable-bank payload
fidelity, bank-select subroutine detection, NROM rejection
of switchable banks.
* Integration e2e tests (16 new): compile real .ne sources
through the full pipeline and assert on iNES headers,
mapper init signatures in the fixed bank, vector locations,
and a regression check against `examples/mmc1_banked.ne`.
Total: 474 tests pass under `cargo test` with
`RUSTFLAGS="-D warnings"`.
https://claude.ai/code/session_01UCressA5e8k1XsuoJYLav2
2026-04-13 01:50:51 +00:00
|
|
|
fn link_banked_inner(
|
|
|
|
|
&self,
|
|
|
|
|
user_code: &[Instruction],
|
|
|
|
|
sprites: &[SpriteData],
|
|
|
|
|
sfx: &[SfxData],
|
|
|
|
|
music: &[MusicData],
|
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
|
|
|
palettes: &[PaletteData],
|
|
|
|
|
backgrounds: &[BackgroundData],
|
banks: implement multi-bank PRG layout and bank-switching runtime
Prior to this commit the linker always shipped a single 16 KB PRG
bank regardless of the declared mapper, so the README's MMC1/UxROM/
MMC3 support was aspirational. This commit gives the three banked
mappers a real multi-bank ROM layout:
* RomBuilder.set_prg_banks() writes any number of 16 KB banks
back-to-back so the iNES header reflects the true PRG size.
* Linker.link_banked() places switchable banks first, fixed bank
last, so the fixed bank maps to $C000-$FFFF (the address window
where vectors and the runtime live).
* runtime::gen_mapper_init() emits reset-time mapper config:
MMC1 serial-writes a control-register value that pins the last
bank at $C000 with the correct mirroring, UxROM relies on the
power-on default, MMC3 writes the $8000/$8001/$A000/$E000
registers to get a known PRG and mirroring state.
* runtime::gen_bank_select() is a mapper-specific subroutine
(callable with the target bank in A) that maps any physical
bank to $8000-$BFFF.
* runtime::gen_bank_trampoline() generates a cross-bank call
stub in the fixed bank that saves the caller's bank, switches,
JSRs the target, and restores the fixed bank.
* The CLI and integration helper thread declared `bank X: prg`
declarations through to the linker so MMC1/UxROM/MMC3 programs
actually produce multi-bank ROMs.
Coverage:
* Runtime unit tests (18 new): mapper init patterns for every
supported mapper, bank-select signatures, trampoline dispatch
order, UxROM bus-conflict table contents.
* RomBuilder tests (6 new): multi-bank layout, padding,
byte-level fidelity, per-bank size validation, legacy
single-bank fallback.
* Linker tests (13 new): multi-bank ROM sizes across MMC1/
UxROM/MMC3, fixed-bank placement, switchable-bank payload
fidelity, bank-select subroutine detection, NROM rejection
of switchable banks.
* Integration e2e tests (16 new): compile real .ne sources
through the full pipeline and assert on iNES headers,
mapper init signatures in the fixed bank, vector locations,
and a regression check against `examples/mmc1_banked.ne`.
Total: 474 tests pass under `cargo test` with
`RUSTFLAGS="-D warnings"`.
https://claude.ai/code/session_01UCressA5e8k1XsuoJYLav2
2026-04-13 01:50:51 +00:00
|
|
|
switchable_banks: &[PrgBank],
|
2026-04-14 02:39:36 +00:00
|
|
|
) -> LinkedRom {
|
banks: implement multi-bank PRG layout and bank-switching runtime
Prior to this commit the linker always shipped a single 16 KB PRG
bank regardless of the declared mapper, so the README's MMC1/UxROM/
MMC3 support was aspirational. This commit gives the three banked
mappers a real multi-bank ROM layout:
* RomBuilder.set_prg_banks() writes any number of 16 KB banks
back-to-back so the iNES header reflects the true PRG size.
* Linker.link_banked() places switchable banks first, fixed bank
last, so the fixed bank maps to $C000-$FFFF (the address window
where vectors and the runtime live).
* runtime::gen_mapper_init() emits reset-time mapper config:
MMC1 serial-writes a control-register value that pins the last
bank at $C000 with the correct mirroring, UxROM relies on the
power-on default, MMC3 writes the $8000/$8001/$A000/$E000
registers to get a known PRG and mirroring state.
* runtime::gen_bank_select() is a mapper-specific subroutine
(callable with the target bank in A) that maps any physical
bank to $8000-$BFFF.
* runtime::gen_bank_trampoline() generates a cross-bank call
stub in the fixed bank that saves the caller's bank, switches,
JSRs the target, and restores the fixed bank.
* The CLI and integration helper thread declared `bank X: prg`
declarations through to the linker so MMC1/UxROM/MMC3 programs
actually produce multi-bank ROMs.
Coverage:
* Runtime unit tests (18 new): mapper init patterns for every
supported mapper, bank-select signatures, trampoline dispatch
order, UxROM bus-conflict table contents.
* RomBuilder tests (6 new): multi-bank layout, padding,
byte-level fidelity, per-bank size validation, legacy
single-bank fallback.
* Linker tests (13 new): multi-bank ROM sizes across MMC1/
UxROM/MMC3, fixed-bank placement, switchable-bank payload
fidelity, bank-select subroutine detection, NROM rejection
of switchable banks.
* Integration e2e tests (16 new): compile real .ne sources
through the full pipeline and assert on iNES headers,
mapper init signatures in the fixed bank, vector locations,
and a regression check against `examples/mmc1_banked.ne`.
Total: 474 tests pass under `cargo test` with
`RUSTFLAGS="-D warnings"`.
https://claude.ai/code/session_01UCressA5e8k1XsuoJYLav2
2026-04-13 01:50:51 +00:00
|
|
|
// ROM layout.
|
|
|
|
|
//
|
|
|
|
|
// NROM: a single 16 KB PRG bank mapped at $C000-$FFFF.
|
|
|
|
|
//
|
|
|
|
|
// Banked (MMC1, UxROM, MMC3): `switchable_banks` switchable
|
|
|
|
|
// 16 KB banks come first in physical order, followed by the
|
|
|
|
|
// fixed bank. The fixed bank holds the runtime, NMI/IRQ
|
|
|
|
|
// handlers, user code, bank-select routine, and all
|
|
|
|
|
// trampolines — everything needed for control flow to work
|
|
|
|
|
// at reset. The mapper is configured so the fixed bank
|
|
|
|
|
// maps to $C000-$FFFF and one of the switchable banks maps
|
|
|
|
|
// to $8000-$BFFF.
|
|
|
|
|
let total_banks = switchable_banks.len() + 1;
|
|
|
|
|
let fixed_bank_index = total_banks - 1;
|
Implement NEScript compiler Milestone 1 ("Hello Sprite")
Complete implementation of the NEScript compiler pipeline for M1:
- Lexer: full tokenization with hex/binary/decimal literals, all keywords, operators
- Parser: recursive descent with Pratt expression parsing (M1 subset)
- Analyzer: symbol resolution, type checking, memory allocation
- 6502 Assembler: full opcode encoding table (~150 valid combinations)
- Code Generator: AST → 6502 instructions (direct, no IR for M1)
- Runtime: NES hardware init, NMI handler, controller read, OAM DMA
- Linker: NROM layout, vector table, palette loading, CHR data
- ROM Builder: iNES header generation, PRG/CHR padding
- CLI: `build` and `check` subcommands via clap
143 tests across all modules:
- 22 lexer tests (literals, keywords, operators, error recovery)
- 18 parser tests (expressions, statements, game structure, errors)
- 7 analyzer tests (symbol resolution, memory allocation, transitions)
- 30 assembler tests (every addressing mode, label resolution)
- 7 codegen tests (var init, arithmetic, buttons, draw, comparisons)
- 11 runtime tests (init sequence, NMI handler, controller read)
- 10 ROM builder tests (iNES format, mirroring, banking, validation)
- 5 linker tests (vector table, CHR data, palette loading)
- 7 integration tests (end-to-end compilation, error detection)
CI: GitHub Actions for check, fmt, clippy, test
Pre-commit: script for local fmt + clippy + test validation
https://claude.ai/code/session_01W6eQFStA66EuMKHUFo2rx3
2026-04-11 22:07:56 +00:00
|
|
|
|
|
|
|
|
let mut all_instructions = Vec::new();
|
|
|
|
|
|
|
|
|
|
// RESET entry point
|
|
|
|
|
all_instructions.push(Instruction::new(NOP, AM::Label("__reset".into())));
|
|
|
|
|
|
|
|
|
|
// Hardware initialization
|
|
|
|
|
all_instructions.extend(runtime::gen_init());
|
|
|
|
|
|
banks: implement multi-bank PRG layout and bank-switching runtime
Prior to this commit the linker always shipped a single 16 KB PRG
bank regardless of the declared mapper, so the README's MMC1/UxROM/
MMC3 support was aspirational. This commit gives the three banked
mappers a real multi-bank ROM layout:
* RomBuilder.set_prg_banks() writes any number of 16 KB banks
back-to-back so the iNES header reflects the true PRG size.
* Linker.link_banked() places switchable banks first, fixed bank
last, so the fixed bank maps to $C000-$FFFF (the address window
where vectors and the runtime live).
* runtime::gen_mapper_init() emits reset-time mapper config:
MMC1 serial-writes a control-register value that pins the last
bank at $C000 with the correct mirroring, UxROM relies on the
power-on default, MMC3 writes the $8000/$8001/$A000/$E000
registers to get a known PRG and mirroring state.
* runtime::gen_bank_select() is a mapper-specific subroutine
(callable with the target bank in A) that maps any physical
bank to $8000-$BFFF.
* runtime::gen_bank_trampoline() generates a cross-bank call
stub in the fixed bank that saves the caller's bank, switches,
JSRs the target, and restores the fixed bank.
* The CLI and integration helper thread declared `bank X: prg`
declarations through to the linker so MMC1/UxROM/MMC3 programs
actually produce multi-bank ROMs.
Coverage:
* Runtime unit tests (18 new): mapper init patterns for every
supported mapper, bank-select signatures, trampoline dispatch
order, UxROM bus-conflict table contents.
* RomBuilder tests (6 new): multi-bank layout, padding,
byte-level fidelity, per-bank size validation, legacy
single-bank fallback.
* Linker tests (13 new): multi-bank ROM sizes across MMC1/
UxROM/MMC3, fixed-bank placement, switchable-bank payload
fidelity, bank-select subroutine detection, NROM rejection
of switchable banks.
* Integration e2e tests (16 new): compile real .ne sources
through the full pipeline and assert on iNES headers,
mapper init signatures in the fixed bank, vector locations,
and a regression check against `examples/mmc1_banked.ne`.
Total: 474 tests pass under `cargo test` with
`RUSTFLAGS="-D warnings"`.
https://claude.ai/code/session_01UCressA5e8k1XsuoJYLav2
2026-04-13 01:50:51 +00:00
|
|
|
// Mapper configuration: for banked mappers, set up the PRG
|
|
|
|
|
// layout so the fixed bank sits at $C000-$FFFF. NROM is a
|
|
|
|
|
// no-op here.
|
|
|
|
|
all_instructions.extend(runtime::gen_mapper_init(
|
|
|
|
|
self.mapper,
|
|
|
|
|
self.mirroring,
|
|
|
|
|
total_banks,
|
|
|
|
|
));
|
|
|
|
|
|
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
|
|
|
// Load the initial palette. If the program declared any
|
|
|
|
|
// `palette` blocks, use the first one; otherwise fall back
|
|
|
|
|
// to the built-in default palette so sprites show up in a
|
|
|
|
|
// reasonable colour scheme without any user setup.
|
platformer: end-to-end side-scroller demo + three runtime bug fixes
Adds `examples/platformer.ne`, a full side-scrolling game that
exercises nearly every subsystem of the compiler in one program:
custom CHR tileset, 32×30 background nametable with per-region
attribute palettes, 2×2 metasprite hero with gravity/jump physics,
wrap-around horizontal scrolling, moving enemies, coin pickups,
user-declared SFX + music, and a Title → Playing state machine
with autopilot so the headless jsnes harness captures real
gameplay at frame 180. Tile art + nametable are generated by
`scripts/gen_platformer_tiles.rs` (`cargo run --bin gen_platformer_tiles`).
Building this out uncovered three independent runtime bugs that
together made the example render as black-on-black smileys. All
three are fixed in this commit:
1. **`gen_init` enabled sprite rendering before the linker's
initial palette/background load runs.** The PPU's v-register
auto-increments on every `$2007` write *during active
rendering*, so the palette load (32 B) and nametable load
(1024 B) were scrambled past the first ~72 bytes — every
existing program with a `background Level { ... }` block was
silently rendering zero-filled VRAM. Fix: leave `PPU_MASK = 0`
at the end of `gen_init` and emit a new `gen_enable_rendering`
call *after* all initial VRAM writes complete.
2. **Audio tick corrupted `ZP_CURRENT_STATE`.** The audio
driver's period-table lookup reused `$02/$03` as a temporary
indirect pointer with a comment claiming the slots were free
because the tick doesn't call mul/div. But `$03` is also
`ZP_CURRENT_STATE` used by the state dispatch loop, so every
music note silently overwrote the state index with the high
byte of `__period_table` (`0xC5` in the platformer ROM),
wedging the state machine forever. Fix: `gen_nmi` now PHAs
`$02/$03` on entry and PLA-restores them on exit, and the
audio tick JSR moves inside that save/restore window (it used
to be spliced by the linker *before* the register saves, so
even A/X/Y were technically being trashed pre-save). Only
`audio_demo`'s audio hash shifts (its note timings move a few
cycles); every other golden is unchanged.
3. **Sub-palette mirroring footgun.** Writing a 32-byte palette
blob sequentially causes the sprite sub-palettes' "index 0"
slots at `$3F10/$3F14/$3F18/$3F1C` to clobber the background
universal colour at `$3F00/$3F04/$3F08/$3F0C` via NES hardware
mirroring. The example's palette sets all eight first bytes
to `$22` (sky blue) for this reason; `docs/future-work.md`
picks up a TODO to warn on inconsistent first-byte values in
the analyzer.
Also:
- `docs/platformer.gif` — 6-second recording of the example
running in jsnes, generated by the new
`tests/emulator/record_gif.mjs` puppeteer helper (encodes via
`gifenc`, committed as a dev-dependency under
`tests/emulator/package.json`).
- README / examples/README tables and the 497-test count are
updated to cover the new example.
https://claude.ai/code/session_01BcCcHi6FUmTh8jC7UgkA3A
2026-04-13 13:04:26 +00:00
|
|
|
//
|
|
|
|
|
// IMPORTANT: `gen_init` leaves rendering fully disabled so
|
|
|
|
|
// these $2006/$2007 writes are safe. We re-enable rendering
|
|
|
|
|
// via `gen_enable_rendering` once all initial VRAM loads
|
|
|
|
|
// complete — writing to $2007 with either the sprite or the
|
|
|
|
|
// background layer active corrupts the PPU's internal
|
|
|
|
|
// address register, which used to clobber everything past
|
|
|
|
|
// about the first 72 bytes of a 1024-byte nametable load.
|
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
|
|
|
if let Some(first_palette) = palettes.first() {
|
|
|
|
|
all_instructions.extend(runtime::gen_initial_palette_load(&first_palette.label()));
|
|
|
|
|
} else {
|
|
|
|
|
all_instructions.extend(self.gen_palette_load());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Load the initial background if the program declared any.
|
|
|
|
|
// Most programs don't, so the common case emits nothing
|
|
|
|
|
// here and leaves nametable 0 zero-filled.
|
platformer: end-to-end side-scroller demo + three runtime bug fixes
Adds `examples/platformer.ne`, a full side-scrolling game that
exercises nearly every subsystem of the compiler in one program:
custom CHR tileset, 32×30 background nametable with per-region
attribute palettes, 2×2 metasprite hero with gravity/jump physics,
wrap-around horizontal scrolling, moving enemies, coin pickups,
user-declared SFX + music, and a Title → Playing state machine
with autopilot so the headless jsnes harness captures real
gameplay at frame 180. Tile art + nametable are generated by
`scripts/gen_platformer_tiles.rs` (`cargo run --bin gen_platformer_tiles`).
Building this out uncovered three independent runtime bugs that
together made the example render as black-on-black smileys. All
three are fixed in this commit:
1. **`gen_init` enabled sprite rendering before the linker's
initial palette/background load runs.** The PPU's v-register
auto-increments on every `$2007` write *during active
rendering*, so the palette load (32 B) and nametable load
(1024 B) were scrambled past the first ~72 bytes — every
existing program with a `background Level { ... }` block was
silently rendering zero-filled VRAM. Fix: leave `PPU_MASK = 0`
at the end of `gen_init` and emit a new `gen_enable_rendering`
call *after* all initial VRAM writes complete.
2. **Audio tick corrupted `ZP_CURRENT_STATE`.** The audio
driver's period-table lookup reused `$02/$03` as a temporary
indirect pointer with a comment claiming the slots were free
because the tick doesn't call mul/div. But `$03` is also
`ZP_CURRENT_STATE` used by the state dispatch loop, so every
music note silently overwrote the state index with the high
byte of `__period_table` (`0xC5` in the platformer ROM),
wedging the state machine forever. Fix: `gen_nmi` now PHAs
`$02/$03` on entry and PLA-restores them on exit, and the
audio tick JSR moves inside that save/restore window (it used
to be spliced by the linker *before* the register saves, so
even A/X/Y were technically being trashed pre-save). Only
`audio_demo`'s audio hash shifts (its note timings move a few
cycles); every other golden is unchanged.
3. **Sub-palette mirroring footgun.** Writing a 32-byte palette
blob sequentially causes the sprite sub-palettes' "index 0"
slots at `$3F10/$3F14/$3F18/$3F1C` to clobber the background
universal colour at `$3F00/$3F04/$3F08/$3F0C` via NES hardware
mirroring. The example's palette sets all eight first bytes
to `$22` (sky blue) for this reason; `docs/future-work.md`
picks up a TODO to warn on inconsistent first-byte values in
the analyzer.
Also:
- `docs/platformer.gif` — 6-second recording of the example
running in jsnes, generated by the new
`tests/emulator/record_gif.mjs` puppeteer helper (encodes via
`gifenc`, committed as a dev-dependency under
`tests/emulator/package.json`).
- README / examples/README tables and the 497-test count are
updated to cover the new example.
https://claude.ai/code/session_01BcCcHi6FUmTh8jC7UgkA3A
2026-04-13 13:04:26 +00:00
|
|
|
let has_user_background = !backgrounds.is_empty();
|
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
|
|
|
if let Some(first_bg) = backgrounds.first() {
|
|
|
|
|
all_instructions.extend(runtime::gen_initial_background_load(
|
|
|
|
|
&first_bg.tiles_label(),
|
|
|
|
|
&first_bg.attrs_label(),
|
|
|
|
|
));
|
|
|
|
|
}
|
Implement NEScript compiler Milestone 1 ("Hello Sprite")
Complete implementation of the NEScript compiler pipeline for M1:
- Lexer: full tokenization with hex/binary/decimal literals, all keywords, operators
- Parser: recursive descent with Pratt expression parsing (M1 subset)
- Analyzer: symbol resolution, type checking, memory allocation
- 6502 Assembler: full opcode encoding table (~150 valid combinations)
- Code Generator: AST → 6502 instructions (direct, no IR for M1)
- Runtime: NES hardware init, NMI handler, controller read, OAM DMA
- Linker: NROM layout, vector table, palette loading, CHR data
- ROM Builder: iNES header generation, PRG/CHR padding
- CLI: `build` and `check` subcommands via clap
143 tests across all modules:
- 22 lexer tests (literals, keywords, operators, error recovery)
- 18 parser tests (expressions, statements, game structure, errors)
- 7 analyzer tests (symbol resolution, memory allocation, transitions)
- 30 assembler tests (every addressing mode, label resolution)
- 7 codegen tests (var init, arithmetic, buttons, draw, comparisons)
- 11 runtime tests (init sequence, NMI handler, controller read)
- 10 ROM builder tests (iNES format, mirroring, banking, validation)
- 5 linker tests (vector table, CHR data, palette loading)
- 7 integration tests (end-to-end compilation, error detection)
CI: GitHub Actions for check, fmt, clippy, test
Pre-commit: script for local fmt + clippy + test validation
https://claude.ai/code/session_01W6eQFStA66EuMKHUFo2rx3
2026-04-11 22:07:56 +00:00
|
|
|
|
platformer: end-to-end side-scroller demo + three runtime bug fixes
Adds `examples/platformer.ne`, a full side-scrolling game that
exercises nearly every subsystem of the compiler in one program:
custom CHR tileset, 32×30 background nametable with per-region
attribute palettes, 2×2 metasprite hero with gravity/jump physics,
wrap-around horizontal scrolling, moving enemies, coin pickups,
user-declared SFX + music, and a Title → Playing state machine
with autopilot so the headless jsnes harness captures real
gameplay at frame 180. Tile art + nametable are generated by
`scripts/gen_platformer_tiles.rs` (`cargo run --bin gen_platformer_tiles`).
Building this out uncovered three independent runtime bugs that
together made the example render as black-on-black smileys. All
three are fixed in this commit:
1. **`gen_init` enabled sprite rendering before the linker's
initial palette/background load runs.** The PPU's v-register
auto-increments on every `$2007` write *during active
rendering*, so the palette load (32 B) and nametable load
(1024 B) were scrambled past the first ~72 bytes — every
existing program with a `background Level { ... }` block was
silently rendering zero-filled VRAM. Fix: leave `PPU_MASK = 0`
at the end of `gen_init` and emit a new `gen_enable_rendering`
call *after* all initial VRAM writes complete.
2. **Audio tick corrupted `ZP_CURRENT_STATE`.** The audio
driver's period-table lookup reused `$02/$03` as a temporary
indirect pointer with a comment claiming the slots were free
because the tick doesn't call mul/div. But `$03` is also
`ZP_CURRENT_STATE` used by the state dispatch loop, so every
music note silently overwrote the state index with the high
byte of `__period_table` (`0xC5` in the platformer ROM),
wedging the state machine forever. Fix: `gen_nmi` now PHAs
`$02/$03` on entry and PLA-restores them on exit, and the
audio tick JSR moves inside that save/restore window (it used
to be spliced by the linker *before* the register saves, so
even A/X/Y were technically being trashed pre-save). Only
`audio_demo`'s audio hash shifts (its note timings move a few
cycles); every other golden is unchanged.
3. **Sub-palette mirroring footgun.** Writing a 32-byte palette
blob sequentially causes the sprite sub-palettes' "index 0"
slots at `$3F10/$3F14/$3F18/$3F1C` to clobber the background
universal colour at `$3F00/$3F04/$3F08/$3F0C` via NES hardware
mirroring. The example's palette sets all eight first bytes
to `$22` (sky blue) for this reason; `docs/future-work.md`
picks up a TODO to warn on inconsistent first-byte values in
the analyzer.
Also:
- `docs/platformer.gif` — 6-second recording of the example
running in jsnes, generated by the new
`tests/emulator/record_gif.mjs` puppeteer helper (encodes via
`gifenc`, committed as a dev-dependency under
`tests/emulator/package.json`).
- README / examples/README tables and the 497-test count are
updated to cover the new example.
https://claude.ai/code/session_01BcCcHi6FUmTh8jC7UgkA3A
2026-04-13 13:04:26 +00:00
|
|
|
// Now that all palette and nametable writes are done, turn
|
|
|
|
|
// rendering on. Programs with a declared background get
|
|
|
|
|
// bg+sprites ($1E); programs without get sprites only ($10)
|
|
|
|
|
// to preserve the pre-fix behaviour of example ROMs that
|
|
|
|
|
// rely on a hidden nametable.
|
|
|
|
|
all_instructions.extend(runtime::gen_enable_rendering(has_user_background));
|
|
|
|
|
|
Implement NEScript compiler Milestone 1 ("Hello Sprite")
Complete implementation of the NEScript compiler pipeline for M1:
- Lexer: full tokenization with hex/binary/decimal literals, all keywords, operators
- Parser: recursive descent with Pratt expression parsing (M1 subset)
- Analyzer: symbol resolution, type checking, memory allocation
- 6502 Assembler: full opcode encoding table (~150 valid combinations)
- Code Generator: AST → 6502 instructions (direct, no IR for M1)
- Runtime: NES hardware init, NMI handler, controller read, OAM DMA
- Linker: NROM layout, vector table, palette loading, CHR data
- ROM Builder: iNES header generation, PRG/CHR padding
- CLI: `build` and `check` subcommands via clap
143 tests across all modules:
- 22 lexer tests (literals, keywords, operators, error recovery)
- 18 parser tests (expressions, statements, game structure, errors)
- 7 analyzer tests (symbol resolution, memory allocation, transitions)
- 30 assembler tests (every addressing mode, label resolution)
- 7 codegen tests (var init, arithmetic, buttons, draw, comparisons)
- 11 runtime tests (init sequence, NMI handler, controller read)
- 10 ROM builder tests (iNES format, mirroring, banking, validation)
- 5 linker tests (vector table, CHR data, palette loading)
- 7 integration tests (end-to-end compilation, error detection)
CI: GitHub Actions for check, fmt, clippy, test
Pre-commit: script for local fmt + clippy + test validation
https://claude.ai/code/session_01W6eQFStA66EuMKHUFo2rx3
2026-04-11 22:07:56 +00:00
|
|
|
// User code (var init + main loop)
|
|
|
|
|
all_instructions.extend(user_code.iter().cloned());
|
|
|
|
|
|
banks: implement multi-bank PRG layout and bank-switching runtime
Prior to this commit the linker always shipped a single 16 KB PRG
bank regardless of the declared mapper, so the README's MMC1/UxROM/
MMC3 support was aspirational. This commit gives the three banked
mappers a real multi-bank ROM layout:
* RomBuilder.set_prg_banks() writes any number of 16 KB banks
back-to-back so the iNES header reflects the true PRG size.
* Linker.link_banked() places switchable banks first, fixed bank
last, so the fixed bank maps to $C000-$FFFF (the address window
where vectors and the runtime live).
* runtime::gen_mapper_init() emits reset-time mapper config:
MMC1 serial-writes a control-register value that pins the last
bank at $C000 with the correct mirroring, UxROM relies on the
power-on default, MMC3 writes the $8000/$8001/$A000/$E000
registers to get a known PRG and mirroring state.
* runtime::gen_bank_select() is a mapper-specific subroutine
(callable with the target bank in A) that maps any physical
bank to $8000-$BFFF.
* runtime::gen_bank_trampoline() generates a cross-bank call
stub in the fixed bank that saves the caller's bank, switches,
JSRs the target, and restores the fixed bank.
* The CLI and integration helper thread declared `bank X: prg`
declarations through to the linker so MMC1/UxROM/MMC3 programs
actually produce multi-bank ROMs.
Coverage:
* Runtime unit tests (18 new): mapper init patterns for every
supported mapper, bank-select signatures, trampoline dispatch
order, UxROM bus-conflict table contents.
* RomBuilder tests (6 new): multi-bank layout, padding,
byte-level fidelity, per-bank size validation, legacy
single-bank fallback.
* Linker tests (13 new): multi-bank ROM sizes across MMC1/
UxROM/MMC3, fixed-bank placement, switchable-bank payload
fidelity, bank-select subroutine detection, NROM rejection
of switchable banks.
* Integration e2e tests (16 new): compile real .ne sources
through the full pipeline and assert on iNES headers,
mapper init signatures in the fixed bank, vector locations,
and a regression check against `examples/mmc1_banked.ne`.
Total: 474 tests pass under `cargo test` with
`RUSTFLAGS="-D warnings"`.
https://claude.ai/code/session_01UCressA5e8k1XsuoJYLav2
2026-04-13 01:50:51 +00:00
|
|
|
// Bank-select subroutine plus a trampoline per declared bank
|
|
|
|
|
// that has an entry label. Emitted only for banked mappers
|
|
|
|
|
// (NROM has no switchable banks by definition). The helpers
|
|
|
|
|
// live in the fixed bank so they're always reachable at
|
|
|
|
|
// $C000-$FFFF regardless of which switchable bank is
|
|
|
|
|
// currently mapped at $8000.
|
|
|
|
|
if self.mapper != Mapper::NROM {
|
|
|
|
|
all_instructions.extend(runtime::gen_bank_select(self.mapper));
|
|
|
|
|
#[allow(clippy::cast_possible_truncation)]
|
|
|
|
|
let fixed_bank_num = fixed_bank_index as u8;
|
|
|
|
|
for (i, bank) in switchable_banks.iter().enumerate() {
|
|
|
|
|
if let Some(entry) = &bank.entry_label {
|
|
|
|
|
#[allow(clippy::cast_possible_truncation)]
|
|
|
|
|
let bank_num = i as u8;
|
|
|
|
|
all_instructions.extend(runtime::gen_bank_trampoline(
|
|
|
|
|
&bank.name,
|
|
|
|
|
entry,
|
|
|
|
|
bank_num,
|
|
|
|
|
fixed_bank_num,
|
|
|
|
|
));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if self.mapper == Mapper::UxROM {
|
|
|
|
|
// UxROM needs a 256-byte bank-select bus-conflict
|
|
|
|
|
// table in the fixed bank. The `__bank_select`
|
|
|
|
|
// routine for UxROM writes to $FFF0 so the byte
|
|
|
|
|
// at that address in ROM must match the bank being
|
|
|
|
|
// selected — we splice in a 0..255 table just before
|
|
|
|
|
// the vector area.
|
|
|
|
|
all_instructions.extend(runtime::gen_uxrom_bank_table());
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
Implement codegen for state dispatch, functions, arrays, math, scroll
State machine dispatch:
- Assign each state a numeric index, store in ZP $03
- Main loop dispatch table: CMP + BNE + JMP trampoline pattern
(avoids branch range limits for large programs)
- on_enter/on_exit handlers generated as JSR targets
- Transition statement writes state index + JSR enter/exit handlers
Function calls:
- Function bodies emitted as labeled subroutines with RTS
- Statement::Call generates parameter passing via ZP + JSR
- Statement::Return generates RTS (with value in A if present)
- Parameter slots at ZP $04-$07
Break/continue:
- Loop stack tracks continue/break label pairs
- Break generates JMP to break_label
- Continue generates JMP to continue_label
- While and Loop push/pop the stack
Array indexing:
- LValue::ArrayIndex generates TAX + STA absolute,X
- Expr::ArrayIndex generates TAX + LDA absolute,X / ZP,X
- Compound array assignments (+=, -=, &=, |=, ^=) load-modify-store
Scroll:
- scroll(x, y) writes to PPU $2005 twice (X then Y)
Math:
- Multiply generates JSR __multiply (shift-and-add routine)
- Divide generates JSR __divide (restoring division)
- Modulo loads remainder from $03 after divide
- ShiftLeft generates ASL A, ShiftRight generates LSR A
- Math routines wired into linker
Error validations:
- E0203 for assignment to const variables
- Break/continue outside loop detection (in_loop tracking)
233 tests (8 new codegen + 2 analyzer + 2 integration), all passing.
https://claude.ai/code/session_01W6eQFStA66EuMKHUFo2rx3
2026-04-12 02:04:49 +00:00
|
|
|
// Math runtime routines (included always for simplicity)
|
|
|
|
|
all_instructions.extend(runtime::gen_multiply());
|
|
|
|
|
all_instructions.extend(runtime::gen_divide());
|
|
|
|
|
|
audio: complete the subsystem — asset pipeline, user decls, tracker-style driver
The audio subsystem was a sketch: `play name` / `start_music name` /
`stop_music` parsed, lowered, and emitted a few hardcoded register
writes from a builtin name table. No user-declared effects, no
per-frame envelope, no note streams, no real engine.
This flesh-out brings audio up to the quality bar of the rest of
the compiler (sprites, palettes, bank switching, scanline IRQ,
etc.) with a full data-driven pipeline:
## Asset pipeline (new `src/assets/audio.rs`)
- `sfx Name { duty, pitch, volume }` blocks compile into per-frame
pulse-1 envelopes. Pitch/volume arrays must match in length; each
entry is one NMI's worth of `$4000` data.
- `music Name { duty, volume, repeat, notes }` blocks compile into
flat `(pitch, duration)` streams for pulse 2. Pitch 0 is a rest,
1-60 indexes a builtin period table covering C1-B5.
- `resolve_sfx` / `resolve_music` walk the program for `play` /
`start_music` references and append builtin fallbacks for any
name that isn't user-declared — so `play coin` still works
without a `sfx Coin { ... }` block.
- Builtin effects (coin, jump, hit, click, cancel, shoot, step)
and tracks (theme, battle, victory, gameover) synthesize through
the same compile path as user decls — one data model, one driver.
## Runtime engine (`src/runtime/mod.rs`)
- `gen_audio_tick()` walks both channels every NMI: reads one
envelope byte through `(ZP_SFX_PTR),Y` -> writes `$4000`,
advances ptr, mutes on zero sentinel. Music decrements the note
counter, advances to the next `(pitch, dur)` pair on zero, looks
up the period through `(__period_table),Y`, loops on `0xFF 0xFF`.
- `gen_period_table()` emits a 60-entry equal-tempered table
(A4 = 440 Hz, NTSC 1.789773 MHz CPU clock) with length-counter
load bits pre-baked into each high byte.
- `gen_data_block()` emits a label + raw-bytes pseudo pair so
user sfx/music data can be spliced into PRG with regular labels
that the two-pass assembler resolves.
- New ZP layout: `$05/$06` music loop base, `$07` music state
(duty/volume/loop/active), `$0C-$0F` sfx and music pointers.
## IR codegen (`src/codegen/ir_codegen.rs`)
- `with_audio(sfx, music)` registers compile-time trigger constants
per blob name.
- `gen_play_sfx` emits: write period to `$4002`/`$4003`, load
envelope pointer into `ZP_SFX_PTR` via SymbolLo/SymbolHi of
`__sfx_<name>`, mark the sfx counter active.
- `gen_start_music` stamps the header byte into `ZP_MUSIC_STATE`
with the active bit OR'd in, seeds both ptr and loop base from
`__music_<name>`, primes the duration counter.
- `gen_stop_music` mutes pulse 2 and clears state.
## Linker (`src/linker/mod.rs`)
- New `link_with_all_assets(user_code, sprites, sfx, music)` path
that splices driver body, period table, and each sfx/music data
blob into PRG — all guarded on the `__audio_used` marker so
silent programs pay zero ROM cost.
## Assembler (`src/asm/opcodes.rs`, `src/asm/mod.rs`)
- New `AddressingMode::Bytes(Vec<u8>)` variant for raw-data
pseudo-instructions. `NOP+Bytes(v)` emits the payload verbatim,
letting the linker splice ROM data tables into a code section
and still have `Label` / `SymbolLo` / `SymbolHi` fixups resolve
correctly in the same assembly pass.
## Analyzer
- `play` / `start_music` now validate the name against user decls
and builtin tables. Unknown names emit E0505 with a helpful list
of builtins — previously a typo would silently compile to no-op.
## Parser
- New `sfx_decl` / `music_decl` grammar with property-style
configuration. Strict validation: duty 0-3, volume 0-15, pitch
arrays must match volume length, music notes must come in pairs,
pitch 0-60, duration ≥ 1.
## Tests
+170 new tests across every layer:
- `src/assets/audio.rs`: 17 tests (compile, resolve, builtins,
shadowing, label sanitation, nested reference walks)
- `src/parser/tests.rs`: 13 tests (valid/invalid sfx + music
declarations, property validation, play/start_music/stop_music)
- `src/analyzer/tests.rs`: 7 tests (builtin acceptance, user decl
acceptance, unknown-name rejection)
- `src/runtime/tests.rs`: 10 tests (audio tick labels, RTS end,
$4000 write, $4004 mute, period table assembly, A4 = 440 Hz,
length counter bits, data block verbatim emit)
- `src/linker/tests.rs`: 4 tests (sfx/music blob placement,
pointer resolution, elision when unused)
- `src/codegen/ir_codegen.rs`: rewrote the 4 existing audio tests
to match the new data-driven contract
- `tests/integration_test.rs`: 4 end-to-end tests including a
user-declared `sfx` + `music` program that verifies bytes land
in PRG ROM at the right addresses
## Docs
- New Audio section in `docs/language-guide.md` with syntax
reference, builtin tables, and an explanation of how the
driver works at compile and run time.
- `docs/architecture.md` updated to reflect the real audio
pipeline instead of the old "audio import stubs" stub.
- `docs/future-work.md` moves audio from "status: minimal" to
"status: full subsystem" with a narrower list of follow-up work
(triangle/noise/DMC channels, NSF/FTM imports, richer envelopes).
- `examples/audio_demo.ne` rewritten to showcase user-declared
`sfx LongCoin`, `sfx Zap`, `music Theme`, still demonstrating
builtin fallback via `play coin`.
Total: 424 tests passing (381 unit + 43 integration), clippy clean,
fmt clean, all 19 examples compile.
https://claude.ai/code/session_015WfaDttE3DpWn9rpyfpQd8
2026-04-13 01:10:21 +00:00
|
|
|
// Audio subsystem — linked in whenever user code touched
|
|
|
|
|
// audio (detected via the `__audio_used` marker emitted by
|
|
|
|
|
// the IR codegen). The driver body, period table, and
|
|
|
|
|
// user/builtin data blobs are all spliced into PRG here.
|
|
|
|
|
//
|
|
|
|
|
// Order is important: the audio tick references both the
|
|
|
|
|
// period table and the data blobs by label, so those labels
|
|
|
|
|
// must be defined in the same assembly pass. The tick body
|
|
|
|
|
// also has to exist before `__nmi` because NMI JSRs into
|
|
|
|
|
// `__audio_tick` — so we emit it alongside the math
|
|
|
|
|
// routines, well before the NMI handler below.
|
|
|
|
|
let has_audio = has_label(user_code, "__audio_used");
|
2026-04-14 10:42:53 +00:00
|
|
|
let has_noise = has_label(user_code, "__noise_used");
|
|
|
|
|
let has_triangle = has_label(user_code, "__triangle_used");
|
audio: complete the subsystem — asset pipeline, user decls, tracker-style driver
The audio subsystem was a sketch: `play name` / `start_music name` /
`stop_music` parsed, lowered, and emitted a few hardcoded register
writes from a builtin name table. No user-declared effects, no
per-frame envelope, no note streams, no real engine.
This flesh-out brings audio up to the quality bar of the rest of
the compiler (sprites, palettes, bank switching, scanline IRQ,
etc.) with a full data-driven pipeline:
## Asset pipeline (new `src/assets/audio.rs`)
- `sfx Name { duty, pitch, volume }` blocks compile into per-frame
pulse-1 envelopes. Pitch/volume arrays must match in length; each
entry is one NMI's worth of `$4000` data.
- `music Name { duty, volume, repeat, notes }` blocks compile into
flat `(pitch, duration)` streams for pulse 2. Pitch 0 is a rest,
1-60 indexes a builtin period table covering C1-B5.
- `resolve_sfx` / `resolve_music` walk the program for `play` /
`start_music` references and append builtin fallbacks for any
name that isn't user-declared — so `play coin` still works
without a `sfx Coin { ... }` block.
- Builtin effects (coin, jump, hit, click, cancel, shoot, step)
and tracks (theme, battle, victory, gameover) synthesize through
the same compile path as user decls — one data model, one driver.
## Runtime engine (`src/runtime/mod.rs`)
- `gen_audio_tick()` walks both channels every NMI: reads one
envelope byte through `(ZP_SFX_PTR),Y` -> writes `$4000`,
advances ptr, mutes on zero sentinel. Music decrements the note
counter, advances to the next `(pitch, dur)` pair on zero, looks
up the period through `(__period_table),Y`, loops on `0xFF 0xFF`.
- `gen_period_table()` emits a 60-entry equal-tempered table
(A4 = 440 Hz, NTSC 1.789773 MHz CPU clock) with length-counter
load bits pre-baked into each high byte.
- `gen_data_block()` emits a label + raw-bytes pseudo pair so
user sfx/music data can be spliced into PRG with regular labels
that the two-pass assembler resolves.
- New ZP layout: `$05/$06` music loop base, `$07` music state
(duty/volume/loop/active), `$0C-$0F` sfx and music pointers.
## IR codegen (`src/codegen/ir_codegen.rs`)
- `with_audio(sfx, music)` registers compile-time trigger constants
per blob name.
- `gen_play_sfx` emits: write period to `$4002`/`$4003`, load
envelope pointer into `ZP_SFX_PTR` via SymbolLo/SymbolHi of
`__sfx_<name>`, mark the sfx counter active.
- `gen_start_music` stamps the header byte into `ZP_MUSIC_STATE`
with the active bit OR'd in, seeds both ptr and loop base from
`__music_<name>`, primes the duration counter.
- `gen_stop_music` mutes pulse 2 and clears state.
## Linker (`src/linker/mod.rs`)
- New `link_with_all_assets(user_code, sprites, sfx, music)` path
that splices driver body, period table, and each sfx/music data
blob into PRG — all guarded on the `__audio_used` marker so
silent programs pay zero ROM cost.
## Assembler (`src/asm/opcodes.rs`, `src/asm/mod.rs`)
- New `AddressingMode::Bytes(Vec<u8>)` variant for raw-data
pseudo-instructions. `NOP+Bytes(v)` emits the payload verbatim,
letting the linker splice ROM data tables into a code section
and still have `Label` / `SymbolLo` / `SymbolHi` fixups resolve
correctly in the same assembly pass.
## Analyzer
- `play` / `start_music` now validate the name against user decls
and builtin tables. Unknown names emit E0505 with a helpful list
of builtins — previously a typo would silently compile to no-op.
## Parser
- New `sfx_decl` / `music_decl` grammar with property-style
configuration. Strict validation: duty 0-3, volume 0-15, pitch
arrays must match volume length, music notes must come in pairs,
pitch 0-60, duration ≥ 1.
## Tests
+170 new tests across every layer:
- `src/assets/audio.rs`: 17 tests (compile, resolve, builtins,
shadowing, label sanitation, nested reference walks)
- `src/parser/tests.rs`: 13 tests (valid/invalid sfx + music
declarations, property validation, play/start_music/stop_music)
- `src/analyzer/tests.rs`: 7 tests (builtin acceptance, user decl
acceptance, unknown-name rejection)
- `src/runtime/tests.rs`: 10 tests (audio tick labels, RTS end,
$4000 write, $4004 mute, period table assembly, A4 = 440 Hz,
length counter bits, data block verbatim emit)
- `src/linker/tests.rs`: 4 tests (sfx/music blob placement,
pointer resolution, elision when unused)
- `src/codegen/ir_codegen.rs`: rewrote the 4 existing audio tests
to match the new data-driven contract
- `tests/integration_test.rs`: 4 end-to-end tests including a
user-declared `sfx` + `music` program that verifies bytes land
in PRG ROM at the right addresses
## Docs
- New Audio section in `docs/language-guide.md` with syntax
reference, builtin tables, and an explanation of how the
driver works at compile and run time.
- `docs/architecture.md` updated to reflect the real audio
pipeline instead of the old "audio import stubs" stub.
- `docs/future-work.md` moves audio from "status: minimal" to
"status: full subsystem" with a narrower list of follow-up work
(triangle/noise/DMC channels, NSF/FTM imports, richer envelopes).
- `examples/audio_demo.ne` rewritten to showcase user-declared
`sfx LongCoin`, `sfx Zap`, `music Theme`, still demonstrating
builtin fallback via `play coin`.
Total: 424 tests passing (381 unit + 43 integration), clippy clean,
fmt clean, all 19 examples compile.
https://claude.ai/code/session_015WfaDttE3DpWn9rpyfpQd8
2026-04-13 01:10:21 +00:00
|
|
|
if has_audio {
|
2026-04-14 10:42:53 +00:00
|
|
|
all_instructions.extend(runtime::gen_audio_tick(has_noise, has_triangle));
|
audio: complete the subsystem — asset pipeline, user decls, tracker-style driver
The audio subsystem was a sketch: `play name` / `start_music name` /
`stop_music` parsed, lowered, and emitted a few hardcoded register
writes from a builtin name table. No user-declared effects, no
per-frame envelope, no note streams, no real engine.
This flesh-out brings audio up to the quality bar of the rest of
the compiler (sprites, palettes, bank switching, scanline IRQ,
etc.) with a full data-driven pipeline:
## Asset pipeline (new `src/assets/audio.rs`)
- `sfx Name { duty, pitch, volume }` blocks compile into per-frame
pulse-1 envelopes. Pitch/volume arrays must match in length; each
entry is one NMI's worth of `$4000` data.
- `music Name { duty, volume, repeat, notes }` blocks compile into
flat `(pitch, duration)` streams for pulse 2. Pitch 0 is a rest,
1-60 indexes a builtin period table covering C1-B5.
- `resolve_sfx` / `resolve_music` walk the program for `play` /
`start_music` references and append builtin fallbacks for any
name that isn't user-declared — so `play coin` still works
without a `sfx Coin { ... }` block.
- Builtin effects (coin, jump, hit, click, cancel, shoot, step)
and tracks (theme, battle, victory, gameover) synthesize through
the same compile path as user decls — one data model, one driver.
## Runtime engine (`src/runtime/mod.rs`)
- `gen_audio_tick()` walks both channels every NMI: reads one
envelope byte through `(ZP_SFX_PTR),Y` -> writes `$4000`,
advances ptr, mutes on zero sentinel. Music decrements the note
counter, advances to the next `(pitch, dur)` pair on zero, looks
up the period through `(__period_table),Y`, loops on `0xFF 0xFF`.
- `gen_period_table()` emits a 60-entry equal-tempered table
(A4 = 440 Hz, NTSC 1.789773 MHz CPU clock) with length-counter
load bits pre-baked into each high byte.
- `gen_data_block()` emits a label + raw-bytes pseudo pair so
user sfx/music data can be spliced into PRG with regular labels
that the two-pass assembler resolves.
- New ZP layout: `$05/$06` music loop base, `$07` music state
(duty/volume/loop/active), `$0C-$0F` sfx and music pointers.
## IR codegen (`src/codegen/ir_codegen.rs`)
- `with_audio(sfx, music)` registers compile-time trigger constants
per blob name.
- `gen_play_sfx` emits: write period to `$4002`/`$4003`, load
envelope pointer into `ZP_SFX_PTR` via SymbolLo/SymbolHi of
`__sfx_<name>`, mark the sfx counter active.
- `gen_start_music` stamps the header byte into `ZP_MUSIC_STATE`
with the active bit OR'd in, seeds both ptr and loop base from
`__music_<name>`, primes the duration counter.
- `gen_stop_music` mutes pulse 2 and clears state.
## Linker (`src/linker/mod.rs`)
- New `link_with_all_assets(user_code, sprites, sfx, music)` path
that splices driver body, period table, and each sfx/music data
blob into PRG — all guarded on the `__audio_used` marker so
silent programs pay zero ROM cost.
## Assembler (`src/asm/opcodes.rs`, `src/asm/mod.rs`)
- New `AddressingMode::Bytes(Vec<u8>)` variant for raw-data
pseudo-instructions. `NOP+Bytes(v)` emits the payload verbatim,
letting the linker splice ROM data tables into a code section
and still have `Label` / `SymbolLo` / `SymbolHi` fixups resolve
correctly in the same assembly pass.
## Analyzer
- `play` / `start_music` now validate the name against user decls
and builtin tables. Unknown names emit E0505 with a helpful list
of builtins — previously a typo would silently compile to no-op.
## Parser
- New `sfx_decl` / `music_decl` grammar with property-style
configuration. Strict validation: duty 0-3, volume 0-15, pitch
arrays must match volume length, music notes must come in pairs,
pitch 0-60, duration ≥ 1.
## Tests
+170 new tests across every layer:
- `src/assets/audio.rs`: 17 tests (compile, resolve, builtins,
shadowing, label sanitation, nested reference walks)
- `src/parser/tests.rs`: 13 tests (valid/invalid sfx + music
declarations, property validation, play/start_music/stop_music)
- `src/analyzer/tests.rs`: 7 tests (builtin acceptance, user decl
acceptance, unknown-name rejection)
- `src/runtime/tests.rs`: 10 tests (audio tick labels, RTS end,
$4000 write, $4004 mute, period table assembly, A4 = 440 Hz,
length counter bits, data block verbatim emit)
- `src/linker/tests.rs`: 4 tests (sfx/music blob placement,
pointer resolution, elision when unused)
- `src/codegen/ir_codegen.rs`: rewrote the 4 existing audio tests
to match the new data-driven contract
- `tests/integration_test.rs`: 4 end-to-end tests including a
user-declared `sfx` + `music` program that verifies bytes land
in PRG ROM at the right addresses
## Docs
- New Audio section in `docs/language-guide.md` with syntax
reference, builtin tables, and an explanation of how the
driver works at compile and run time.
- `docs/architecture.md` updated to reflect the real audio
pipeline instead of the old "audio import stubs" stub.
- `docs/future-work.md` moves audio from "status: minimal" to
"status: full subsystem" with a narrower list of follow-up work
(triangle/noise/DMC channels, NSF/FTM imports, richer envelopes).
- `examples/audio_demo.ne` rewritten to showcase user-declared
`sfx LongCoin`, `sfx Zap`, `music Theme`, still demonstrating
builtin fallback via `play coin`.
Total: 424 tests passing (381 unit + 43 integration), clippy clean,
fmt clean, all 19 examples compile.
https://claude.ai/code/session_015WfaDttE3DpWn9rpyfpQd8
2026-04-13 01:10:21 +00:00
|
|
|
all_instructions.extend(runtime::gen_period_table());
|
|
|
|
|
// Emit one data block per sfx blob: a label followed by
|
|
|
|
|
// the envelope bytes. `play Name` codegen emits a
|
|
|
|
|
// SymbolLo/SymbolHi pair that resolves to this label.
|
|
|
|
|
for blob in sfx {
|
|
|
|
|
all_instructions.extend(runtime::gen_data_block(
|
|
|
|
|
&blob.label(),
|
|
|
|
|
blob.envelope.clone(),
|
|
|
|
|
));
|
|
|
|
|
}
|
|
|
|
|
// Same for music: label + note stream.
|
|
|
|
|
for blob in music {
|
|
|
|
|
all_instructions
|
|
|
|
|
.extend(runtime::gen_data_block(&blob.label(), blob.stream.clone()));
|
|
|
|
|
}
|
compiler: audio driver, u16 arithmetic, multi-scanline, slot recycling
Five language features and optimizations from the planned-work backlog:
- **Minimal audio driver**: `play`/`start_music`/`stop_music` now generate
APU pulse-1/pulse-2 writes from a builtin SFX/music name table, and
the NMI handler gains a `JSR __audio_tick` splice (via the linker's
`__audio_used` marker lookup) that ages an SFX countdown counter and
mutes pulse 1 when the tone expires. Programs that never trigger
audio pay zero ROM cost.
- **u16 arithmetic and comparisons**: new IR ops `LoadVarHi`, `StoreVarHi`,
`Add16`, `Sub16`, and six `Cmp*16` variants. The lowering context
tracks variable types via the analyzer's symbol table and routes
expressions through the 8-bit or 16-bit path based on operand width.
Add16 emits `CLC;ADC;ADC` with carry propagating naturally into the
high byte; compares dispatch high-byte-first with a short-circuit
low-byte fallback. Fixes a silent miscompile where `big += 1` on a
u16 var only incremented the low byte.
- **Multi-scanline handlers per state**: `gen_scanline_irq` now
dispatches on `(current_state, ZP_SCANLINE_STEP)` and reloads the
MMC3 counter with the delta to the next scanline in the same state.
`gen_scanline_reload` resets the step counter at the top of each
NMI so a state with multiple handlers fires them in ascending line
order. Previously only the first handler per state ever fired.
- **IR temp slot recycling**: `build_use_counts` pre-scans each
function to count per-temp uses; `retire_op_sources` decrements
the counts after each op and pushes dead slots back onto
`free_slots` for later allocation. `bitwise_ops.ne` used to crash
(debug) or miscompile (release) once it hit 128 concurrent temps;
with recycling the same function now uses ~4 slots instead of 136.
- **INC/DEC peephole fold + improved dead-load elimination**:
`fold_inc_dec` collapses `LDA addr; CLC; ADC #1; STA addr` into
a single `INC addr` (and the SEC/SBC variant into `DEC addr`),
saving 5 bytes and 5 cycles per increment. The fold is suppressed
when the next instruction reads carry. `remove_dead_loads` now
walks past INC/DEC/STX/STY (which don't touch A) to find the
actual next A-use, catching more dead loads.
Tests: 331 unit + 39 integration (up from 313 + 37), including new
guards for audio, u16, multi-scanline, and slot recycling.
https://claude.ai/code/session_01A8qk3gw2jWSzdiXBZPZSFE
2026-04-12 22:21:53 +00:00
|
|
|
}
|
|
|
|
|
|
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
|
|
|
// Palette and background data blobs. Each palette is a
|
|
|
|
|
// 32-byte block labelled `__palette_Name`; backgrounds are
|
|
|
|
|
// split into two blocks (`__bg_tiles_Name`, `__bg_attrs_Name`)
|
|
|
|
|
// so the reset loader and the NMI update helper can push
|
|
|
|
|
// them with independent pointers. We always splice the
|
|
|
|
|
// blobs whenever the program declares any palette or
|
|
|
|
|
// background — there's no equivalent of `__audio_used`
|
|
|
|
|
// because simply *declaring* a palette is enough to need
|
|
|
|
|
// its bytes in ROM (the reset loader reads them).
|
|
|
|
|
for pal in palettes {
|
|
|
|
|
all_instructions.extend(runtime::gen_data_block(&pal.label(), pal.colors.to_vec()));
|
|
|
|
|
}
|
|
|
|
|
for bg in backgrounds {
|
|
|
|
|
all_instructions.extend(runtime::gen_data_block(
|
|
|
|
|
&bg.tiles_label(),
|
|
|
|
|
bg.tiles.to_vec(),
|
|
|
|
|
));
|
|
|
|
|
all_instructions.extend(runtime::gen_data_block(
|
|
|
|
|
&bg.attrs_label(),
|
|
|
|
|
bg.attrs.to_vec(),
|
|
|
|
|
));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// The NMI needs the palette/nametable update helper whenever
|
|
|
|
|
// the program declared any palette or background, or the
|
|
|
|
|
// IR codegen emitted the `__ppu_update_used` marker (which
|
|
|
|
|
// signals that user code contains a `set_palette` or
|
|
|
|
|
// `load_background` statement). Either condition brings in
|
|
|
|
|
// the ~70-byte helper; programs that touch neither pay
|
|
|
|
|
// zero bytes.
|
|
|
|
|
let has_ppu_updates = !palettes.is_empty()
|
|
|
|
|
|| !backgrounds.is_empty()
|
|
|
|
|
|| has_label(user_code, "__ppu_update_used");
|
|
|
|
|
|
Implement NEScript compiler Milestone 1 ("Hello Sprite")
Complete implementation of the NEScript compiler pipeline for M1:
- Lexer: full tokenization with hex/binary/decimal literals, all keywords, operators
- Parser: recursive descent with Pratt expression parsing (M1 subset)
- Analyzer: symbol resolution, type checking, memory allocation
- 6502 Assembler: full opcode encoding table (~150 valid combinations)
- Code Generator: AST → 6502 instructions (direct, no IR for M1)
- Runtime: NES hardware init, NMI handler, controller read, OAM DMA
- Linker: NROM layout, vector table, palette loading, CHR data
- ROM Builder: iNES header generation, PRG/CHR padding
- CLI: `build` and `check` subcommands via clap
143 tests across all modules:
- 22 lexer tests (literals, keywords, operators, error recovery)
- 18 parser tests (expressions, statements, game structure, errors)
- 7 analyzer tests (symbol resolution, memory allocation, transitions)
- 30 assembler tests (every addressing mode, label resolution)
- 7 codegen tests (var init, arithmetic, buttons, draw, comparisons)
- 11 runtime tests (init sequence, NMI handler, controller read)
- 10 ROM builder tests (iNES format, mirroring, banking, validation)
- 5 linker tests (vector table, CHR data, palette loading)
- 7 integration tests (end-to-end compilation, error detection)
CI: GitHub Actions for check, fmt, clippy, test
Pre-commit: script for local fmt + clippy + test validation
https://claude.ai/code/session_01W6eQFStA66EuMKHUFo2rx3
2026-04-11 22:07:56 +00:00
|
|
|
// NMI handler
|
|
|
|
|
all_instructions.push(Instruction::new(NOP, AM::Label("__nmi".into())));
|
2026-04-12 16:29:15 +00:00
|
|
|
// If user code emits an MMC3 reload hook, splice in a JSR
|
|
|
|
|
// before the regular NMI runs. This reloads the scanline IRQ
|
|
|
|
|
// counter each frame so the handler fires at the right line.
|
|
|
|
|
// The presence of the `__ir_mmc3_reload` label is detected
|
|
|
|
|
// during assembly via the labels map; we unconditionally
|
|
|
|
|
// emit a conditional JSR whose target is resolved at link
|
|
|
|
|
// time. The helper emits an RTS so it's safe to call even
|
|
|
|
|
// when there's no work to do.
|
|
|
|
|
if has_label(user_code, "__ir_mmc3_reload") {
|
|
|
|
|
all_instructions.push(Instruction::new(JSR, AM::Label("__ir_mmc3_reload".into())));
|
|
|
|
|
}
|
platformer: end-to-end side-scroller demo + three runtime bug fixes
Adds `examples/platformer.ne`, a full side-scrolling game that
exercises nearly every subsystem of the compiler in one program:
custom CHR tileset, 32×30 background nametable with per-region
attribute palettes, 2×2 metasprite hero with gravity/jump physics,
wrap-around horizontal scrolling, moving enemies, coin pickups,
user-declared SFX + music, and a Title → Playing state machine
with autopilot so the headless jsnes harness captures real
gameplay at frame 180. Tile art + nametable are generated by
`scripts/gen_platformer_tiles.rs` (`cargo run --bin gen_platformer_tiles`).
Building this out uncovered three independent runtime bugs that
together made the example render as black-on-black smileys. All
three are fixed in this commit:
1. **`gen_init` enabled sprite rendering before the linker's
initial palette/background load runs.** The PPU's v-register
auto-increments on every `$2007` write *during active
rendering*, so the palette load (32 B) and nametable load
(1024 B) were scrambled past the first ~72 bytes — every
existing program with a `background Level { ... }` block was
silently rendering zero-filled VRAM. Fix: leave `PPU_MASK = 0`
at the end of `gen_init` and emit a new `gen_enable_rendering`
call *after* all initial VRAM writes complete.
2. **Audio tick corrupted `ZP_CURRENT_STATE`.** The audio
driver's period-table lookup reused `$02/$03` as a temporary
indirect pointer with a comment claiming the slots were free
because the tick doesn't call mul/div. But `$03` is also
`ZP_CURRENT_STATE` used by the state dispatch loop, so every
music note silently overwrote the state index with the high
byte of `__period_table` (`0xC5` in the platformer ROM),
wedging the state machine forever. Fix: `gen_nmi` now PHAs
`$02/$03` on entry and PLA-restores them on exit, and the
audio tick JSR moves inside that save/restore window (it used
to be spliced by the linker *before* the register saves, so
even A/X/Y were technically being trashed pre-save). Only
`audio_demo`'s audio hash shifts (its note timings move a few
cycles); every other golden is unchanged.
3. **Sub-palette mirroring footgun.** Writing a 32-byte palette
blob sequentially causes the sprite sub-palettes' "index 0"
slots at `$3F10/$3F14/$3F18/$3F1C` to clobber the background
universal colour at `$3F00/$3F04/$3F08/$3F0C` via NES hardware
mirroring. The example's palette sets all eight first bytes
to `$22` (sky blue) for this reason; `docs/future-work.md`
picks up a TODO to warn on inconsistent first-byte values in
the analyzer.
Also:
- `docs/platformer.gif` — 6-second recording of the example
running in jsnes, generated by the new
`tests/emulator/record_gif.mjs` puppeteer helper (encodes via
`gifenc`, committed as a dev-dependency under
`tests/emulator/package.json`).
- README / examples/README tables and the 497-test count are
updated to cover the new example.
https://claude.ai/code/session_01BcCcHi6FUmTh8jC7UgkA3A
2026-04-13 13:04:26 +00:00
|
|
|
// The audio tick JSR is emitted by `gen_nmi` itself, after
|
|
|
|
|
// the register and scratch-slot saves, so it can freely
|
|
|
|
|
// clobber A/X/Y and $02/$03 without corrupting user state.
|
2026-04-14 02:39:36 +00:00
|
|
|
// The codegen emits a `__debug_mode` marker whenever
|
|
|
|
|
// `--debug` is active; that tells the runtime to splice
|
|
|
|
|
// in the extra frame-overrun check at the top of NMI.
|
|
|
|
|
let debug_mode = has_label(user_code, "__debug_mode");
|
|
|
|
|
all_instructions.extend(runtime::gen_nmi(has_ppu_updates, has_audio, debug_mode));
|
Implement NEScript compiler Milestone 1 ("Hello Sprite")
Complete implementation of the NEScript compiler pipeline for M1:
- Lexer: full tokenization with hex/binary/decimal literals, all keywords, operators
- Parser: recursive descent with Pratt expression parsing (M1 subset)
- Analyzer: symbol resolution, type checking, memory allocation
- 6502 Assembler: full opcode encoding table (~150 valid combinations)
- Code Generator: AST → 6502 instructions (direct, no IR for M1)
- Runtime: NES hardware init, NMI handler, controller read, OAM DMA
- Linker: NROM layout, vector table, palette loading, CHR data
- ROM Builder: iNES header generation, PRG/CHR padding
- CLI: `build` and `check` subcommands via clap
143 tests across all modules:
- 22 lexer tests (literals, keywords, operators, error recovery)
- 18 parser tests (expressions, statements, game structure, errors)
- 7 analyzer tests (symbol resolution, memory allocation, transitions)
- 30 assembler tests (every addressing mode, label resolution)
- 7 codegen tests (var init, arithmetic, buttons, draw, comparisons)
- 11 runtime tests (init sequence, NMI handler, controller read)
- 10 ROM builder tests (iNES format, mirroring, banking, validation)
- 5 linker tests (vector table, CHR data, palette loading)
- 7 integration tests (end-to-end compilation, error detection)
CI: GitHub Actions for check, fmt, clippy, test
Pre-commit: script for local fmt + clippy + test validation
https://claude.ai/code/session_01W6eQFStA66EuMKHUFo2rx3
2026-04-11 22:07:56 +00:00
|
|
|
|
|
|
|
|
// IRQ handler
|
|
|
|
|
all_instructions.push(Instruction::new(NOP, AM::Label("__irq".into())));
|
|
|
|
|
all_instructions.extend(runtime::gen_irq());
|
|
|
|
|
|
|
|
|
|
// Assemble everything at $C000
|
|
|
|
|
let base_addr = 0xC000;
|
|
|
|
|
let result = asm::assemble(&all_instructions, base_addr);
|
|
|
|
|
|
|
|
|
|
// Build PRG ROM with vector table
|
|
|
|
|
let mut prg = result.bytes;
|
|
|
|
|
|
|
|
|
|
// Pad to fill the bank up to vector table location
|
|
|
|
|
// Vector table is at $FFFA-$FFFF (relative offset: $3FFA in a 16 KB bank)
|
|
|
|
|
let vector_offset = 0x3FFA;
|
|
|
|
|
if prg.len() > vector_offset {
|
|
|
|
|
panic!("PRG code exceeds 16 KB bank (code is {} bytes)", prg.len());
|
|
|
|
|
}
|
|
|
|
|
prg.resize(vector_offset, 0xFF);
|
|
|
|
|
|
2026-04-12 16:22:54 +00:00
|
|
|
// Write vector table. IR codegen emits a richer IRQ handler
|
|
|
|
|
// under `__irq_user` when the program has scanline handlers;
|
|
|
|
|
// prefer that over the generic RTI stub at `__irq`.
|
Implement NEScript compiler Milestone 1 ("Hello Sprite")
Complete implementation of the NEScript compiler pipeline for M1:
- Lexer: full tokenization with hex/binary/decimal literals, all keywords, operators
- Parser: recursive descent with Pratt expression parsing (M1 subset)
- Analyzer: symbol resolution, type checking, memory allocation
- 6502 Assembler: full opcode encoding table (~150 valid combinations)
- Code Generator: AST → 6502 instructions (direct, no IR for M1)
- Runtime: NES hardware init, NMI handler, controller read, OAM DMA
- Linker: NROM layout, vector table, palette loading, CHR data
- ROM Builder: iNES header generation, PRG/CHR padding
- CLI: `build` and `check` subcommands via clap
143 tests across all modules:
- 22 lexer tests (literals, keywords, operators, error recovery)
- 18 parser tests (expressions, statements, game structure, errors)
- 7 analyzer tests (symbol resolution, memory allocation, transitions)
- 30 assembler tests (every addressing mode, label resolution)
- 7 codegen tests (var init, arithmetic, buttons, draw, comparisons)
- 11 runtime tests (init sequence, NMI handler, controller read)
- 10 ROM builder tests (iNES format, mirroring, banking, validation)
- 5 linker tests (vector table, CHR data, palette loading)
- 7 integration tests (end-to-end compilation, error detection)
CI: GitHub Actions for check, fmt, clippy, test
Pre-commit: script for local fmt + clippy + test validation
https://claude.ai/code/session_01W6eQFStA66EuMKHUFo2rx3
2026-04-11 22:07:56 +00:00
|
|
|
let nmi_addr = result.labels.get("__nmi").copied().unwrap_or(0xC000);
|
|
|
|
|
let reset_addr = result.labels.get("__reset").copied().unwrap_or(0xC000);
|
2026-04-12 16:22:54 +00:00
|
|
|
let irq_addr = result
|
|
|
|
|
.labels
|
|
|
|
|
.get("__irq_user")
|
|
|
|
|
.or_else(|| result.labels.get("__irq"))
|
|
|
|
|
.copied()
|
|
|
|
|
.unwrap_or(0xC000);
|
Implement NEScript compiler Milestone 1 ("Hello Sprite")
Complete implementation of the NEScript compiler pipeline for M1:
- Lexer: full tokenization with hex/binary/decimal literals, all keywords, operators
- Parser: recursive descent with Pratt expression parsing (M1 subset)
- Analyzer: symbol resolution, type checking, memory allocation
- 6502 Assembler: full opcode encoding table (~150 valid combinations)
- Code Generator: AST → 6502 instructions (direct, no IR for M1)
- Runtime: NES hardware init, NMI handler, controller read, OAM DMA
- Linker: NROM layout, vector table, palette loading, CHR data
- ROM Builder: iNES header generation, PRG/CHR padding
- CLI: `build` and `check` subcommands via clap
143 tests across all modules:
- 22 lexer tests (literals, keywords, operators, error recovery)
- 18 parser tests (expressions, statements, game structure, errors)
- 7 analyzer tests (symbol resolution, memory allocation, transitions)
- 30 assembler tests (every addressing mode, label resolution)
- 7 codegen tests (var init, arithmetic, buttons, draw, comparisons)
- 11 runtime tests (init sequence, NMI handler, controller read)
- 10 ROM builder tests (iNES format, mirroring, banking, validation)
- 5 linker tests (vector table, CHR data, palette loading)
- 7 integration tests (end-to-end compilation, error detection)
CI: GitHub Actions for check, fmt, clippy, test
Pre-commit: script for local fmt + clippy + test validation
https://claude.ai/code/session_01W6eQFStA66EuMKHUFo2rx3
2026-04-11 22:07:56 +00:00
|
|
|
|
|
|
|
|
prg.extend_from_slice(&nmi_addr.to_le_bytes());
|
|
|
|
|
prg.extend_from_slice(&reset_addr.to_le_bytes());
|
|
|
|
|
prg.extend_from_slice(&irq_addr.to_le_bytes());
|
|
|
|
|
|
|
|
|
|
// Build ROM
|
|
|
|
|
let mut builder = RomBuilder::new(self.mirroring);
|
M4+M5: Optimizer passes, type casting, bank switching, math runtime
Milestone 4 — Optimization & Polish:
- Strength reduction: multiply by power-of-2 → shift left
- Zero-page promotion analysis: rank variables by access frequency
- `as` type casting expression in parser/AST/analyzer
- `scroll(x, y)` statement
- `--asm-dump` flag for viewing generated assembly
- Extended optimizer tests (strength reduction, frequency analysis)
Milestone 5 — Bank Switching & Release:
- Mapper support: MMC1 (1), UxROM (2), MMC3 (4) in parser and ROM builder
- Bank declarations: `bank Name: prg` / `bank Name: chr`
- Linker::with_mapper for mapper-aware ROM generation
- Software multiply (8x8→16, shift-and-add algorithm)
- Software divide (8÷8→8, restoring division algorithm)
- ROM tests for mapper encoding round-trip
- Integration test for MMC1 compilation
210 tests total (18 new), all pre-commit checks pass.
https://claude.ai/code/session_01W6eQFStA66EuMKHUFo2rx3
2026-04-12 00:22:11 +00:00
|
|
|
builder.set_mapper(crate::rom::mapper_number(self.mapper));
|
2026-04-14 02:05:51 +00:00
|
|
|
if self.header_format == HeaderFormat::Nes2 {
|
|
|
|
|
builder.enable_nes2();
|
|
|
|
|
}
|
banks: implement multi-bank PRG layout and bank-switching runtime
Prior to this commit the linker always shipped a single 16 KB PRG
bank regardless of the declared mapper, so the README's MMC1/UxROM/
MMC3 support was aspirational. This commit gives the three banked
mappers a real multi-bank ROM layout:
* RomBuilder.set_prg_banks() writes any number of 16 KB banks
back-to-back so the iNES header reflects the true PRG size.
* Linker.link_banked() places switchable banks first, fixed bank
last, so the fixed bank maps to $C000-$FFFF (the address window
where vectors and the runtime live).
* runtime::gen_mapper_init() emits reset-time mapper config:
MMC1 serial-writes a control-register value that pins the last
bank at $C000 with the correct mirroring, UxROM relies on the
power-on default, MMC3 writes the $8000/$8001/$A000/$E000
registers to get a known PRG and mirroring state.
* runtime::gen_bank_select() is a mapper-specific subroutine
(callable with the target bank in A) that maps any physical
bank to $8000-$BFFF.
* runtime::gen_bank_trampoline() generates a cross-bank call
stub in the fixed bank that saves the caller's bank, switches,
JSRs the target, and restores the fixed bank.
* The CLI and integration helper thread declared `bank X: prg`
declarations through to the linker so MMC1/UxROM/MMC3 programs
actually produce multi-bank ROMs.
Coverage:
* Runtime unit tests (18 new): mapper init patterns for every
supported mapper, bank-select signatures, trampoline dispatch
order, UxROM bus-conflict table contents.
* RomBuilder tests (6 new): multi-bank layout, padding,
byte-level fidelity, per-bank size validation, legacy
single-bank fallback.
* Linker tests (13 new): multi-bank ROM sizes across MMC1/
UxROM/MMC3, fixed-bank placement, switchable-bank payload
fidelity, bank-select subroutine detection, NROM rejection
of switchable banks.
* Integration e2e tests (16 new): compile real .ne sources
through the full pipeline and assert on iNES headers,
mapper init signatures in the fixed bank, vector locations,
and a regression check against `examples/mmc1_banked.ne`.
Total: 474 tests pass under `cargo test` with
`RUSTFLAGS="-D warnings"`.
https://claude.ai/code/session_01UCressA5e8k1XsuoJYLav2
2026-04-13 01:50:51 +00:00
|
|
|
|
|
|
|
|
// Multi-bank layout: each switchable bank is an independent
|
|
|
|
|
// 16 KB slot whose contents the linker takes verbatim from
|
|
|
|
|
// the caller, followed by the fixed bank (just assembled).
|
|
|
|
|
// For NROM (no switchable banks) this collapses to the
|
|
|
|
|
// legacy single-bank path.
|
|
|
|
|
if switchable_banks.is_empty() {
|
|
|
|
|
builder.set_prg(prg);
|
|
|
|
|
} else {
|
|
|
|
|
let mut banks: Vec<Vec<u8>> = Vec::with_capacity(total_banks);
|
|
|
|
|
for bank in switchable_banks {
|
|
|
|
|
assert!(
|
|
|
|
|
bank.data.len() <= 16384,
|
|
|
|
|
"switchable bank '{}' exceeds 16 KB ({} bytes)",
|
|
|
|
|
bank.name,
|
|
|
|
|
bank.data.len()
|
|
|
|
|
);
|
|
|
|
|
banks.push(bank.data.clone());
|
|
|
|
|
}
|
|
|
|
|
banks.push(prg);
|
|
|
|
|
builder.set_prg_banks(banks);
|
|
|
|
|
}
|
Implement NEScript compiler Milestone 1 ("Hello Sprite")
Complete implementation of the NEScript compiler pipeline for M1:
- Lexer: full tokenization with hex/binary/decimal literals, all keywords, operators
- Parser: recursive descent with Pratt expression parsing (M1 subset)
- Analyzer: symbol resolution, type checking, memory allocation
- 6502 Assembler: full opcode encoding table (~150 valid combinations)
- Code Generator: AST → 6502 instructions (direct, no IR for M1)
- Runtime: NES hardware init, NMI handler, controller read, OAM DMA
- Linker: NROM layout, vector table, palette loading, CHR data
- ROM Builder: iNES header generation, PRG/CHR padding
- CLI: `build` and `check` subcommands via clap
143 tests across all modules:
- 22 lexer tests (literals, keywords, operators, error recovery)
- 18 parser tests (expressions, statements, game structure, errors)
- 7 analyzer tests (symbol resolution, memory allocation, transitions)
- 30 assembler tests (every addressing mode, label resolution)
- 7 codegen tests (var init, arithmetic, buttons, draw, comparisons)
- 11 runtime tests (init sequence, NMI handler, controller read)
- 10 ROM builder tests (iNES format, mirroring, banking, validation)
- 5 linker tests (vector table, CHR data, palette loading)
- 7 integration tests (end-to-end compilation, error detection)
CI: GitHub Actions for check, fmt, clippy, test
Pre-commit: script for local fmt + clippy + test validation
https://claude.ai/code/session_01W6eQFStA66EuMKHUFo2rx3
2026-04-11 22:07:56 +00:00
|
|
|
|
2026-04-12 10:01:44 +00:00
|
|
|
// CHR ROM: tile 0 is reserved for the default smiley, followed by
|
|
|
|
|
// any user-declared sprites placed at their assigned tile indices.
|
Implement NEScript compiler Milestone 1 ("Hello Sprite")
Complete implementation of the NEScript compiler pipeline for M1:
- Lexer: full tokenization with hex/binary/decimal literals, all keywords, operators
- Parser: recursive descent with Pratt expression parsing (M1 subset)
- Analyzer: symbol resolution, type checking, memory allocation
- 6502 Assembler: full opcode encoding table (~150 valid combinations)
- Code Generator: AST → 6502 instructions (direct, no IR for M1)
- Runtime: NES hardware init, NMI handler, controller read, OAM DMA
- Linker: NROM layout, vector table, palette loading, CHR data
- ROM Builder: iNES header generation, PRG/CHR padding
- CLI: `build` and `check` subcommands via clap
143 tests across all modules:
- 22 lexer tests (literals, keywords, operators, error recovery)
- 18 parser tests (expressions, statements, game structure, errors)
- 7 analyzer tests (symbol resolution, memory allocation, transitions)
- 30 assembler tests (every addressing mode, label resolution)
- 7 codegen tests (var init, arithmetic, buttons, draw, comparisons)
- 11 runtime tests (init sequence, NMI handler, controller read)
- 10 ROM builder tests (iNES format, mirroring, banking, validation)
- 5 linker tests (vector table, CHR data, palette loading)
- 7 integration tests (end-to-end compilation, error detection)
CI: GitHub Actions for check, fmt, clippy, test
Pre-commit: script for local fmt + clippy + test validation
https://claude.ai/code/session_01W6eQFStA66EuMKHUFo2rx3
2026-04-11 22:07:56 +00:00
|
|
|
let mut chr = vec![0u8; 8192];
|
|
|
|
|
chr[..16].copy_from_slice(&DEFAULT_SPRITE_CHR);
|
2026-04-12 10:01:44 +00:00
|
|
|
for sprite in sprites {
|
|
|
|
|
let offset = sprite.tile_index as usize * 16;
|
|
|
|
|
let end = offset + sprite.chr_bytes.len();
|
|
|
|
|
if end <= chr.len() {
|
|
|
|
|
chr[offset..end].copy_from_slice(&sprite.chr_bytes);
|
|
|
|
|
}
|
|
|
|
|
}
|
Implement NEScript compiler Milestone 1 ("Hello Sprite")
Complete implementation of the NEScript compiler pipeline for M1:
- Lexer: full tokenization with hex/binary/decimal literals, all keywords, operators
- Parser: recursive descent with Pratt expression parsing (M1 subset)
- Analyzer: symbol resolution, type checking, memory allocation
- 6502 Assembler: full opcode encoding table (~150 valid combinations)
- Code Generator: AST → 6502 instructions (direct, no IR for M1)
- Runtime: NES hardware init, NMI handler, controller read, OAM DMA
- Linker: NROM layout, vector table, palette loading, CHR data
- ROM Builder: iNES header generation, PRG/CHR padding
- CLI: `build` and `check` subcommands via clap
143 tests across all modules:
- 22 lexer tests (literals, keywords, operators, error recovery)
- 18 parser tests (expressions, statements, game structure, errors)
- 7 analyzer tests (symbol resolution, memory allocation, transitions)
- 30 assembler tests (every addressing mode, label resolution)
- 7 codegen tests (var init, arithmetic, buttons, draw, comparisons)
- 11 runtime tests (init sequence, NMI handler, controller read)
- 10 ROM builder tests (iNES format, mirroring, banking, validation)
- 5 linker tests (vector table, CHR data, palette loading)
- 7 integration tests (end-to-end compilation, error detection)
CI: GitHub Actions for check, fmt, clippy, test
Pre-commit: script for local fmt + clippy + test validation
https://claude.ai/code/session_01W6eQFStA66EuMKHUFo2rx3
2026-04-11 22:07:56 +00:00
|
|
|
builder.set_chr(chr);
|
|
|
|
|
|
2026-04-14 02:39:36 +00:00
|
|
|
let rom = builder.build();
|
|
|
|
|
// The fixed bank sits after the iNES header (16 bytes) and
|
|
|
|
|
// any switchable banks (16 KB each). Callers use this to
|
|
|
|
|
// translate CPU addresses from `labels` into ROM file
|
|
|
|
|
// offsets when emitting Mesen `.mlb` files.
|
|
|
|
|
let fixed_bank_file_offset = 16 + switchable_banks.len() * 16_384;
|
|
|
|
|
LinkedRom {
|
|
|
|
|
rom,
|
|
|
|
|
labels: result.labels,
|
|
|
|
|
fixed_bank_file_offset,
|
|
|
|
|
}
|
Implement NEScript compiler Milestone 1 ("Hello Sprite")
Complete implementation of the NEScript compiler pipeline for M1:
- Lexer: full tokenization with hex/binary/decimal literals, all keywords, operators
- Parser: recursive descent with Pratt expression parsing (M1 subset)
- Analyzer: symbol resolution, type checking, memory allocation
- 6502 Assembler: full opcode encoding table (~150 valid combinations)
- Code Generator: AST → 6502 instructions (direct, no IR for M1)
- Runtime: NES hardware init, NMI handler, controller read, OAM DMA
- Linker: NROM layout, vector table, palette loading, CHR data
- ROM Builder: iNES header generation, PRG/CHR padding
- CLI: `build` and `check` subcommands via clap
143 tests across all modules:
- 22 lexer tests (literals, keywords, operators, error recovery)
- 18 parser tests (expressions, statements, game structure, errors)
- 7 analyzer tests (symbol resolution, memory allocation, transitions)
- 30 assembler tests (every addressing mode, label resolution)
- 7 codegen tests (var init, arithmetic, buttons, draw, comparisons)
- 11 runtime tests (init sequence, NMI handler, controller read)
- 10 ROM builder tests (iNES format, mirroring, banking, validation)
- 5 linker tests (vector table, CHR data, palette loading)
- 7 integration tests (end-to-end compilation, error detection)
CI: GitHub Actions for check, fmt, clippy, test
Pre-commit: script for local fmt + clippy + test validation
https://claude.ai/code/session_01W6eQFStA66EuMKHUFo2rx3
2026-04-11 22:07:56 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Generate instructions to load the default palette into the PPU.
|
|
|
|
|
fn gen_palette_load(&self) -> Vec<Instruction> {
|
|
|
|
|
let mut out = Vec::new();
|
|
|
|
|
|
|
|
|
|
// Set PPU address to $3F00 (palette start)
|
|
|
|
|
out.push(Instruction::new(LDA, AM::Absolute(0x2002))); // read PPU status to reset latch
|
|
|
|
|
out.push(Instruction::new(LDA, AM::Immediate(0x3F)));
|
|
|
|
|
out.push(Instruction::new(STA, AM::Absolute(0x2006))); // PPU addr high byte
|
|
|
|
|
out.push(Instruction::new(LDA, AM::Immediate(0x00)));
|
|
|
|
|
out.push(Instruction::new(STA, AM::Absolute(0x2006))); // PPU addr low byte
|
|
|
|
|
|
|
|
|
|
// Write all 32 palette bytes
|
|
|
|
|
for &color in &DEFAULT_PALETTE {
|
|
|
|
|
out.push(Instruction::new(LDA, AM::Immediate(color)));
|
|
|
|
|
out.push(Instruction::new(STA, AM::Absolute(0x2007))); // PPU data
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
out
|
|
|
|
|
}
|
|
|
|
|
}
|