1
0
Fork 0
mirror of https://github.com/imjasonh/nescript synced 2026-07-08 17:06:04 +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

@ -80,6 +80,22 @@ pub const ZP_PENDING_BG_TILES_HI: u8 = 0x15;
pub const ZP_PENDING_BG_ATTRS_LO: u8 = 0x16;
pub const ZP_PENDING_BG_ATTRS_HI: u8 = 0x17;
// ── Debug instrumentation ──
//
// These slots are only touched by debug-mode ROMs. In release
// builds the analyzer is free to allocate over them.
/// Debug-mode frame-overrun counter. Incremented by the NMI
/// handler whenever it fires while the previous frame's ready
/// flag is still set — which means the main loop didn't consume
/// it, so user code spent more than one vblank-to-vblank window
/// processing the last frame. Read it with `peek(0x07FF)` in
/// user code to see how many overruns have happened since reset,
/// or watch the address in a Mesen memory viewer. Placed at the
/// top of main RAM to minimise the chance of a collision with
/// analyzer-allocated variables (which grow from $0300 upward).
pub const DEBUG_FRAME_OVERRUN_ADDR: u16 = 0x07FF;
/// Generate the NES hardware initialization sequence.
/// This runs at RESET and sets up the hardware before user code.
pub fn gen_init() -> Vec<Instruction> {
@ -209,8 +225,17 @@ pub fn gen_enable_rendering(show_background: bool) -> Vec<Instruction> {
/// save/restore window used to silently clobber `ZP_CURRENT_STATE`
/// whenever a music note was played (the tick's period-table
/// lookup stashes the table's high byte into $03).
///
/// `debug_mode` enables frame-overrun detection: before touching
/// the frame-ready flag, the handler checks whether it's already
/// set — if it is, the previous frame's main-loop work never
/// finished (i.e. the program ran over its vblank budget) and
/// the handler bumps the counter at
/// [`DEBUG_FRAME_OVERRUN_ADDR`]. Release-mode ROMs never call
/// this with `debug_mode=true`, so the counter slot stays free
/// for user allocation.
#[must_use]
pub fn gen_nmi(has_ppu_updates: bool, has_audio: bool) -> Vec<Instruction> {
pub fn gen_nmi(has_ppu_updates: bool, has_audio: bool, debug_mode: bool) -> Vec<Instruction> {
let mut out = Vec::new();
// Save registers
@ -275,6 +300,31 @@ pub fn gen_nmi(has_ppu_updates: bool, has_audio: bool) -> Vec<Instruction> {
AM::LabelRelative("__read_input".into()),
));
// Debug frame-overrun check. The frame flag is "set on NMI,
// cleared by wait_frame". If we see it set at the top of a
// new NMI, the main loop never reached its wait_frame since
// the previous vblank — i.e. the frame overran. Bump a
// counter at `DEBUG_FRAME_OVERRUN_ADDR` in that case so user
// code can `peek(0x07FF)` to see how many overruns have
// happened. The check is gated on `debug_mode` so release
// builds emit nothing here.
if debug_mode {
// Read the previous flag. If zero, skip the bump.
out.push(Instruction::new(LDA, AM::ZeroPage(ZP_FRAME_FLAG)));
out.push(Instruction::new(
BEQ,
AM::LabelRelative("__debug_no_overrun".into()),
));
out.push(Instruction::new(
INC,
AM::Absolute(DEBUG_FRAME_OVERRUN_ADDR),
));
out.push(Instruction::new(
NOP,
AM::Label("__debug_no_overrun".into()),
));
}
// Set frame-ready flag
out.push(Instruction::new(LDA, AM::Immediate(0x01)));
out.push(Instruction::new(STA, AM::ZeroPage(ZP_FRAME_FLAG)));