mirror of
https://github.com/imjasonh/nescript
synced 2026-07-20 21:10:04 +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_FRAME_FLAG: u8 = 0x00;
|
||||||
const ZP_CURRENT_STATE: u8 = 0x03;
|
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`.
|
/// IR codegen that produces 6502 instructions from an `IrProgram`.
|
||||||
pub struct IrCodeGen<'a> {
|
pub struct IrCodeGen<'a> {
|
||||||
instructions: Vec<Instruction>,
|
instructions: Vec<Instruction>,
|
||||||
|
|
@ -54,6 +59,9 @@ pub struct IrCodeGen<'a> {
|
||||||
/// True while generating code inside a state frame handler.
|
/// True while generating code inside a state frame handler.
|
||||||
/// When set, `Return` terminators emit `JMP __ir_main_loop` instead of `RTS`.
|
/// When set, `Return` terminators emit `JMP __ir_main_loop` instead of `RTS`.
|
||||||
in_frame_handler: bool,
|
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],
|
_allocations: &'a [VarAllocation],
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -79,10 +87,19 @@ impl<'a> IrCodeGen<'a> {
|
||||||
function_names,
|
function_names,
|
||||||
next_oam_slot: 0,
|
next_oam_slot: 0,
|
||||||
in_frame_handler: false,
|
in_frame_handler: false,
|
||||||
|
debug_mode: false,
|
||||||
_allocations: allocations,
|
_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 {
|
fn function_exists(&self, name: &str) -> bool {
|
||||||
self.function_names.contains(name)
|
self.function_names.contains(name)
|
||||||
}
|
}
|
||||||
|
|
@ -442,6 +459,35 @@ impl<'a> IrCodeGen<'a> {
|
||||||
self.emit(JMP, AM::Label("__ir_main_loop".into()));
|
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(_) => {
|
IrOp::SourceLoc(_) => {
|
||||||
// No code for source location markers
|
// No code for source location markers
|
||||||
}
|
}
|
||||||
|
|
@ -728,4 +774,85 @@ mod more_tests {
|
||||||
.any(|i| i.opcode == STA && i.mode == AM::ZeroPage(0x03));
|
.any(|i| i.opcode == STA && i.mode == AM::ZeroPage(0x03));
|
||||||
assert!(has_store_state, "transition should write to current_state");
|
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();
|
let arg_temps: Vec<_> = args.iter().map(|a| self.lower_expr(a)).collect();
|
||||||
self.emit(IrOp::Call(None, name.clone(), arg_temps));
|
self.emit(IrOp::Call(None, name.clone(), arg_temps));
|
||||||
}
|
}
|
||||||
Statement::Scroll(_, _, _) => {
|
Statement::Scroll(x_expr, y_expr, _) => {
|
||||||
// TODO: implement scroll hardware writes
|
let x = self.lower_expr(x_expr);
|
||||||
|
let y = self.lower_expr(y_expr);
|
||||||
|
self.emit(IrOp::Scroll(x, y));
|
||||||
}
|
}
|
||||||
Statement::LoadBackground(_, _) | Statement::SetPalette(_, _) => {
|
Statement::LoadBackground(_, _) | Statement::SetPalette(_, _) => {
|
||||||
// TODO: implement in asset pipeline
|
// TODO: implement in asset pipeline
|
||||||
}
|
}
|
||||||
Statement::DebugLog(_, _) | Statement::DebugAssert(_, _) => {
|
Statement::DebugLog(args, _) => {
|
||||||
// Debug statements don't produce IR ops in release mode.
|
let temps: Vec<_> = args.iter().map(|a| self.lower_expr(a)).collect();
|
||||||
// In debug mode, the AST-based codegen handles them directly
|
self.emit(IrOp::DebugLog(temps));
|
||||||
// (IR codegen path for debug is a future enhancement).
|
}
|
||||||
|
Statement::DebugAssert(cond, _) => {
|
||||||
|
let t = self.lower_expr(cond);
|
||||||
|
self.emit(IrOp::DebugAssert(t));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -129,6 +129,14 @@ pub enum IrOp {
|
||||||
ReadInput(IrTemp, u8),
|
ReadInput(IrTemp, u8),
|
||||||
WaitFrame,
|
WaitFrame,
|
||||||
Transition(String),
|
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
|
// Source mapping
|
||||||
SourceLoc(Span),
|
SourceLoc(Span),
|
||||||
|
|
|
||||||
|
|
@ -140,6 +140,7 @@ fn compile(input: &PathBuf, debug: bool, asm_dump: bool, use_ir: bool) -> Result
|
||||||
use nescript::codegen::IrCodeGen;
|
use nescript::codegen::IrCodeGen;
|
||||||
IrCodeGen::new(&analysis.var_allocations, &ir_program)
|
IrCodeGen::new(&analysis.var_allocations, &ir_program)
|
||||||
.with_sprites(&sprites)
|
.with_sprites(&sprites)
|
||||||
|
.with_debug(debug)
|
||||||
.generate(&ir_program)
|
.generate(&ir_program)
|
||||||
} else {
|
} else {
|
||||||
CodeGen::new(&analysis.var_allocations, &program.constants)
|
CodeGen::new(&analysis.var_allocations, &program.constants)
|
||||||
|
|
|
||||||
|
|
@ -379,6 +379,18 @@ fn collect_source_temps(op: &IrOp, used: &mut HashSet<IrTemp>) {
|
||||||
used.insert(*f);
|
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(_) => {}
|
IrOp::ReadInput(_, _) | IrOp::WaitFrame | IrOp::Transition(_) | IrOp::SourceLoc(_) => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -412,6 +424,9 @@ fn op_dest(op: &IrOp) -> Option<IrTemp> {
|
||||||
| IrOp::DrawSprite { .. }
|
| IrOp::DrawSprite { .. }
|
||||||
| IrOp::WaitFrame
|
| IrOp::WaitFrame
|
||||||
| IrOp::Transition(_)
|
| IrOp::Transition(_)
|
||||||
|
| IrOp::Scroll(_, _)
|
||||||
|
| IrOp::DebugLog(_)
|
||||||
|
| IrOp::DebugAssert(_)
|
||||||
| IrOp::SourceLoc(_) => None,
|
| IrOp::SourceLoc(_) => None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue