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

debug: add symbol export, source maps, bounds checks, overrun counter

Implements four items from docs/future-work.md's "Debug instrumentation"
section so debugging on real ROMs is no longer a guessing game:

1. Mesen `.mlb` symbol export via `--symbols <path>`. The linker now
   returns a `LinkedRom { rom, labels, fixed_bank_file_offset }` struct
   from `link_banked_with_ppu_detailed`; `src/linker/debug_symbols.rs`
   renders that plus the analyzer's var allocations into a Mesen-
   compatible label listing (function entry points get `P:` entries
   at PRG-relative offsets; user vars get `R:` entries).

2. Source maps via `--source-map <path>`. IR lowering now emits a
   `SourceLoc(span)` op before every statement; the codegen turns each
   one into a `__src_<N>` label-definition pseudo-op and records the
   span in a side table. Source-marker emission is opt-in
   (`with_source_map(true)`) because labels become peephole block
   boundaries — leaving the markers off preserves byte-identical
   release ROMs.

3. Array bounds checking under `--debug`. Every `ArrayLoad` /
   `ArrayStore` now emits a `CMP #size; BCC ok; JMP __debug_halt; ok:`
   guard, and the codegen emits one shared `__debug_halt` trap at the
   end of the fixed bank (writes $BC to the debug port then wedges in
   a tight `JMP $`). Release builds skip the whole thing.

4. Frame-overrun detection under `--debug`. `gen_nmi` now takes a
   `debug_mode` flag; when on, it checks `ZP_FRAME_FLAG` at the top of
   the handler and increments a counter at `$07FF`
   (`DEBUG_FRAME_OVERRUN_ADDR`) if the flag was still set — meaning
   the main loop didn't reach `wait_frame` before the next vblank.
   User code can read the counter via `peek(0x07FF)`. This is the
   abbreviated form the future-work doc suggested: a bump-a-counter
   hook rather than a full cycle-budget tracker, which would need a
   new builtin. The codegen emits a `__debug_mode` marker label in
   debug mode so the linker can select the overrun-aware NMI variant.

Release ROMs for every committed example are byte-identical before
and after this change (verified with `git diff examples/` after a
full rebuild). All 512 lib tests and 71 integration tests pass;
`cargo fmt` clean; `cargo clippy --all-targets -- -D warnings` clean.

https://claude.ai/code/session_01MaNVcDmK9gsspRkdxowQAM
This commit is contained in:
Claude 2026-04-14 02:39:36 +00:00
parent 33351f8b32
commit b575921c8e
No known key found for this signature in database
9 changed files with 1081 additions and 23 deletions

View file

@ -1,6 +1,11 @@
mod debug_symbols;
#[cfg(test)]
mod tests;
pub use debug_symbols::{render_mlb, render_source_map};
use std::collections::HashMap;
use crate::asm;
use crate::asm::{AddressingMode as AM, Instruction, Opcode::*};
use crate::assets::{BackgroundData, MusicData, PaletteData, SfxData};
@ -8,6 +13,31 @@ use crate::parser::ast::{HeaderFormat, Mapper, Mirroring};
use crate::rom::RomBuilder;
use crate::runtime;
/// 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,
}
/// Link compiled code into a complete NES ROM.
pub struct Linker {
mirroring: Mirroring,
@ -213,6 +243,34 @@ impl Linker {
backgrounds: &[BackgroundData],
switchable_banks: &[PrgBank],
) -> Vec<u8> {
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 {
assert!(
switchable_banks.is_empty() || self.mapper != Mapper::NROM,
"NROM does not support switchable PRG banks (got {} banks)",
@ -239,7 +297,7 @@ impl Linker {
palettes: &[PaletteData],
backgrounds: &[BackgroundData],
switchable_banks: &[PrgBank],
) -> Vec<u8> {
) -> LinkedRom {
// ROM layout.
//
// NROM: a single 16 KB PRG bank mapped at $C000-$FFFF.
@ -429,7 +487,11 @@ impl Linker {
// 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.
all_instructions.extend(runtime::gen_nmi(has_ppu_updates, has_audio));
// 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));
// IRQ handler
all_instructions.push(Instruction::new(NOP, AM::Label("__irq".into())));
@ -508,7 +570,17 @@ impl Linker {
}
builder.set_chr(chr);
builder.build()
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,
}
}
/// Generate instructions to load the default palette into the PPU.