mirror of
https://github.com/imjasonh/nescript
synced 2026-07-08 08:55:38 +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:
parent
8a6441071e
commit
d609b77cd7
5 changed files with 162 additions and 6 deletions
|
|
@ -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");
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -310,16 +310,21 @@ impl LoweringContext {
|
|||
let arg_temps: Vec<_> = args.iter().map(|a| self.lower_expr(a)).collect();
|
||||
self.emit(IrOp::Call(None, name.clone(), arg_temps));
|
||||
}
|
||||
Statement::Scroll(_, _, _) => {
|
||||
// TODO: implement scroll hardware writes
|
||||
Statement::Scroll(x_expr, y_expr, _) => {
|
||||
let x = self.lower_expr(x_expr);
|
||||
let y = self.lower_expr(y_expr);
|
||||
self.emit(IrOp::Scroll(x, y));
|
||||
}
|
||||
Statement::LoadBackground(_, _) | Statement::SetPalette(_, _) => {
|
||||
// TODO: implement in asset pipeline
|
||||
}
|
||||
Statement::DebugLog(_, _) | Statement::DebugAssert(_, _) => {
|
||||
// Debug statements don't produce IR ops in release mode.
|
||||
// In debug mode, the AST-based codegen handles them directly
|
||||
// (IR codegen path for debug is a future enhancement).
|
||||
Statement::DebugLog(args, _) => {
|
||||
let temps: Vec<_> = args.iter().map(|a| self.lower_expr(a)).collect();
|
||||
self.emit(IrOp::DebugLog(temps));
|
||||
}
|
||||
Statement::DebugAssert(cond, _) => {
|
||||
let t = self.lower_expr(cond);
|
||||
self.emit(IrOp::DebugAssert(t));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -129,6 +129,14 @@ pub enum IrOp {
|
|||
ReadInput(IrTemp, u8),
|
||||
WaitFrame,
|
||||
Transition(String),
|
||||
/// Write PPU scroll registers (two writes to $2005: X then Y).
|
||||
Scroll(IrTemp, IrTemp),
|
||||
/// Debug: write a list of temps to the emulator debug port ($4800).
|
||||
/// Stripped in release mode by the codegen.
|
||||
DebugLog(Vec<IrTemp>),
|
||||
/// Debug: runtime assertion — if `cond` is zero, halt with debug marker.
|
||||
/// Stripped in release mode by the codegen.
|
||||
DebugAssert(IrTemp),
|
||||
|
||||
// Source mapping
|
||||
SourceLoc(Span),
|
||||
|
|
|
|||
|
|
@ -140,6 +140,7 @@ fn compile(input: &PathBuf, debug: bool, asm_dump: bool, use_ir: bool) -> Result
|
|||
use nescript::codegen::IrCodeGen;
|
||||
IrCodeGen::new(&analysis.var_allocations, &ir_program)
|
||||
.with_sprites(&sprites)
|
||||
.with_debug(debug)
|
||||
.generate(&ir_program)
|
||||
} else {
|
||||
CodeGen::new(&analysis.var_allocations, &program.constants)
|
||||
|
|
|
|||
|
|
@ -379,6 +379,18 @@ fn collect_source_temps(op: &IrOp, used: &mut HashSet<IrTemp>) {
|
|||
used.insert(*f);
|
||||
}
|
||||
}
|
||||
IrOp::Scroll(x, y) => {
|
||||
used.insert(*x);
|
||||
used.insert(*y);
|
||||
}
|
||||
IrOp::DebugLog(args) => {
|
||||
for a in args {
|
||||
used.insert(*a);
|
||||
}
|
||||
}
|
||||
IrOp::DebugAssert(cond) => {
|
||||
used.insert(*cond);
|
||||
}
|
||||
IrOp::ReadInput(_, _) | IrOp::WaitFrame | IrOp::Transition(_) | IrOp::SourceLoc(_) => {}
|
||||
}
|
||||
}
|
||||
|
|
@ -412,6 +424,9 @@ fn op_dest(op: &IrOp) -> Option<IrTemp> {
|
|||
| IrOp::DrawSprite { .. }
|
||||
| IrOp::WaitFrame
|
||||
| IrOp::Transition(_)
|
||||
| IrOp::Scroll(_, _)
|
||||
| IrOp::DebugLog(_)
|
||||
| IrOp::DebugAssert(_)
|
||||
| IrOp::SourceLoc(_) => None,
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue