1
0
Fork 0
mirror of https://github.com/imjasonh/nescript synced 2026-07-14 19:46:46 +00:00

IR codegen: scroll, debug.log, debug.assert

Adds Scroll / DebugLog / DebugAssert variants to IrOp, wires them into
the IR lowering, optimizer liveness tracking, and IR codegen. Scroll
emits two PPU $2005 writes; debug statements emit writes to $4800 when
IrCodeGen::with_debug(true) is set, stripped otherwise. These were the
last feature gaps versus the AST codegen path, so IR codegen is now a
full replacement.

https://claude.ai/code/session_01W6eQFStA66EuMKHUFo2rx3
This commit is contained in:
Claude 2026-04-12 10:43:53 +00:00
parent 8a6441071e
commit d609b77cd7
No known key found for this signature in database
5 changed files with 162 additions and 6 deletions

View file

@ -34,6 +34,11 @@ const TEMP_BASE: u8 = 0x80;
const ZP_FRAME_FLAG: u8 = 0x00;
const ZP_CURRENT_STATE: u8 = 0x03;
/// Emulator debug output port. Writes to this address are logged by
/// Mesen / fceux when the debugger is attached. Used by `debug.log`
/// and `debug.assert` when compiled with `--debug`.
const DEBUG_PORT: u16 = 0x4800;
/// IR codegen that produces 6502 instructions from an `IrProgram`.
pub struct IrCodeGen<'a> {
instructions: Vec<Instruction>,
@ -54,6 +59,9 @@ pub struct IrCodeGen<'a> {
/// True while generating code inside a state frame handler.
/// When set, `Return` terminators emit `JMP __ir_main_loop` instead of `RTS`.
in_frame_handler: bool,
/// When true, emit code for `debug.log` / `debug.assert`.
/// When false, these ops are stripped entirely.
debug_mode: bool,
_allocations: &'a [VarAllocation],
}
@ -79,10 +87,19 @@ impl<'a> IrCodeGen<'a> {
function_names,
next_oam_slot: 0,
in_frame_handler: false,
debug_mode: false,
_allocations: allocations,
}
}
/// Enable debug-mode code generation. When set, `debug.log` and
/// `debug.assert` emit runtime code; otherwise they are stripped.
#[must_use]
pub fn with_debug(mut self, debug: bool) -> Self {
self.debug_mode = debug;
self
}
fn function_exists(&self, name: &str) -> bool {
self.function_names.contains(name)
}
@ -442,6 +459,35 @@ impl<'a> IrCodeGen<'a> {
self.emit(JMP, AM::Label("__ir_main_loop".into()));
}
}
IrOp::Scroll(x, y) => {
// PPU scroll register $2005 takes two writes: X then Y
self.load_temp(*x);
self.emit(STA, AM::Absolute(0x2005));
self.load_temp(*y);
self.emit(STA, AM::Absolute(0x2005));
}
IrOp::DebugLog(args) => {
if self.debug_mode {
for arg in args {
self.load_temp(*arg);
self.emit(STA, AM::Absolute(DEBUG_PORT));
}
}
// In release mode, stripped entirely
}
IrOp::DebugAssert(cond) => {
if self.debug_mode {
// Load cond; if nonzero (true) skip; else halt
self.load_temp(*cond);
let pass_label = format!("__ir_assert_pass_{}", self.instructions.len());
self.emit(BNE, AM::LabelRelative(pass_label.clone()));
// Assertion failed: write marker to debug port and BRK
self.emit(LDA, AM::Immediate(0xFF));
self.emit(STA, AM::Absolute(DEBUG_PORT));
self.emit(BRK, AM::Implied);
self.emit_label(&pass_label);
}
}
IrOp::SourceLoc(_) => {
// No code for source location markers
}
@ -728,4 +774,85 @@ mod more_tests {
.any(|i| i.opcode == STA && i.mode == AM::ZeroPage(0x03));
assert!(has_store_state, "transition should write to current_state");
}
#[test]
fn ir_codegen_scroll_writes_ppu_register() {
let insts = lower_and_gen(
r#"
game "T" { mapper: NROM }
var sx: u8 = 0
var sy: u8 = 0
on frame { scroll(sx, sy) }
start Main
"#,
);
// Both X and Y scroll values should be written to $2005
let scroll_writes = insts
.iter()
.filter(|i| i.opcode == STA && i.mode == AM::Absolute(0x2005))
.count();
assert_eq!(scroll_writes, 2, "scroll should emit two STA $2005 writes");
}
fn lower_and_gen_debug(source: &str) -> Vec<Instruction> {
let (prog, _) = parser::parse(source);
let prog = prog.unwrap();
let analysis = analyzer::analyze(&prog);
let ir_program = ir::lower(&prog, &analysis);
IrCodeGen::new(&analysis.var_allocations, &ir_program)
.with_debug(true)
.generate(&ir_program)
}
#[test]
fn ir_codegen_debug_log_emits_in_debug_mode() {
let insts = lower_and_gen_debug(
r#"
game "T" { mapper: NROM }
var x: u8 = 42
on frame { debug.log(x) }
start Main
"#,
);
// Should write to the debug port $4800
let writes_debug_port = insts
.iter()
.any(|i| i.opcode == STA && i.mode == AM::Absolute(0x4800));
assert!(writes_debug_port, "debug.log should write to $4800");
}
#[test]
fn ir_codegen_debug_log_stripped_in_release() {
let insts = lower_and_gen(
r#"
game "T" { mapper: NROM }
var x: u8 = 42
on frame { debug.log(x) }
start Main
"#,
);
// No debug port writes in release mode
let writes_debug_port = insts
.iter()
.any(|i| i.opcode == STA && i.mode == AM::Absolute(0x4800));
assert!(
!writes_debug_port,
"debug.log should be stripped in release mode"
);
}
#[test]
fn ir_codegen_debug_assert_emits_in_debug_mode() {
let insts = lower_and_gen_debug(
r#"
game "T" { mapper: NROM }
var x: u8 = 42
on frame { debug.assert(x == 42) }
start Main
"#,
);
// Should emit a BRK for the fail path
let has_brk = insts.iter().any(|i| i.opcode == BRK);
assert!(has_brk, "debug.assert should emit BRK on failure path");
}
}