mirror of
https://github.com/imjasonh/nescript
synced 2026-07-08 08:55:38 +00:00
Implement IR-based code generator (--use-ir)
New src/codegen/ir_codegen.rs walks IrProgram and emits 6502 instructions. This enables optimizer passes to actually affect the output ROM. Design: - Each IR temp gets a zero-page slot at $80 + temp_index - Functions reset the temp counter at entry (temps don't outlive functions) - Globals map by name to their analyzer-assigned zero-page addresses - Operands are loaded into A, computed, stored back to the dest slot Handles all IrOp variants: - LoadImm, LoadVar, StoreVar (basic loads/stores) - Add/Sub (CLC+ADC / SEC+SBC) - Mul (JSR __multiply runtime routine) - And/Or/Xor (zero-page operands) - ShiftLeft/ShiftRight (repeated ASL/LSR) - Negate/Complement (EOR #$FF + optional two's complement) - CmpEq/Ne/Lt/Gt/LtEq/GtEq (CMP + conditional branch + 0/1) - ArrayLoad/ArrayStore (TAX + ZeroPageX/AbsoluteX) - Call (ZP param passing + JSR) - DrawSprite (OAM slot 0 write, uses sprite_tiles map) - ReadInput (LDA $01, P1 input) - WaitFrame (poll frame flag at $00) All terminators: - Jump (JMP to block label) - Branch (LDA temp + BNE true / JMP false) - Return (optional value in A + RTS) - Unreachable (BRK) IR lowering fixes: - ReadInput now has a destination IrTemp (was a side-effect-only op) - ButtonRead uses the proper input temp instead of uninitialized register - Logical AND/OR use new emit_move helper (OR with zero) instead of bogus raw VarId for path merging CLI: - New --use-ir flag on `build` subcommand to opt in to IR codegen - Default remains AST codegen (for now); IR codegen is experimental All 7 examples compile through the IR pipeline and produce valid iNES ROMs. Tests: 266 total (7 new ir_codegen unit + 2 new integration). https://claude.ai/code/session_01W6eQFStA66EuMKHUFo2rx3
This commit is contained in:
parent
5512567349
commit
1ede169f1e
8 changed files with 650 additions and 17 deletions
539
src/codegen/ir_codegen.rs
Normal file
539
src/codegen/ir_codegen.rs
Normal file
|
|
@ -0,0 +1,539 @@
|
|||
//! IR-based code generator.
|
||||
//!
|
||||
//! Walks an `IrProgram` and produces 6502 instructions. Uses a simple
|
||||
//! strategy: each IR temp is assigned a zero-page slot in the function's
|
||||
//! temp region. Operations load operands from their slots into A, compute,
|
||||
//! and store back. This is not efficient but is correct and easy to reason
|
||||
//! about. A proper register allocator would use A/X/Y directly for short
|
||||
//! live ranges.
|
||||
//!
|
||||
//! Zero-page layout (shared with AST codegen):
|
||||
//! - `$00` frame flag
|
||||
//! - `$01` input P1
|
||||
//! - `$02` scratch temp
|
||||
//! - `$03` `current_state`
|
||||
//! - `$04-$07` function call params
|
||||
//! - `$08` input P2
|
||||
//! - `$09-$0F` reserved
|
||||
//! - `$10+` user variables + IR temp slots
|
||||
//!
|
||||
//! IR temps are allocated starting at `TEMP_BASE` (`$80`), giving 128 bytes
|
||||
//! (`0x80-0xFF`) for IR temp storage per function. Functions reset the
|
||||
//! temp counter at entry.
|
||||
|
||||
use std::collections::HashMap;
|
||||
|
||||
use crate::analyzer::VarAllocation;
|
||||
use crate::asm::{AddressingMode as AM, Instruction, Opcode::*};
|
||||
use crate::ir::{IrBasicBlock, IrFunction, IrOp, IrProgram, IrTemp, IrTerminator, VarId};
|
||||
|
||||
/// Base zero-page address for IR temp slots.
|
||||
const TEMP_BASE: u8 = 0x80;
|
||||
|
||||
/// IR codegen that produces 6502 instructions from an `IrProgram`.
|
||||
pub struct IrCodeGen<'a> {
|
||||
instructions: Vec<Instruction>,
|
||||
/// Map from IR `VarId` to zero-page address.
|
||||
var_addrs: HashMap<VarId, u16>,
|
||||
/// Map from `IrTemp` to zero-page slot within the current function.
|
||||
temp_slots: HashMap<IrTemp, u8>,
|
||||
/// Next available temp slot for the current function.
|
||||
next_temp_slot: u8,
|
||||
/// Sprite name to tile index mapping.
|
||||
sprite_tiles: HashMap<String, u8>,
|
||||
_allocations: &'a [VarAllocation],
|
||||
}
|
||||
|
||||
impl<'a> IrCodeGen<'a> {
|
||||
pub fn new(allocations: &'a [VarAllocation], ir: &IrProgram) -> Self {
|
||||
// Map IR global VarIds to their allocated addresses.
|
||||
// Globals in IR are in the same order as in the analyzer, so we
|
||||
// can align them by name.
|
||||
let mut var_addrs = HashMap::new();
|
||||
for global in &ir.globals {
|
||||
if let Some(alloc) = allocations.iter().find(|a| a.name == global.name) {
|
||||
var_addrs.insert(global.var_id, alloc.address);
|
||||
}
|
||||
}
|
||||
Self {
|
||||
instructions: Vec::new(),
|
||||
var_addrs,
|
||||
temp_slots: HashMap::new(),
|
||||
next_temp_slot: 0,
|
||||
sprite_tiles: HashMap::new(),
|
||||
_allocations: allocations,
|
||||
}
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn with_sprites(mut self, sprites: &[crate::linker::SpriteData]) -> Self {
|
||||
for sprite in sprites {
|
||||
self.sprite_tiles
|
||||
.insert(sprite.name.clone(), sprite.tile_index);
|
||||
}
|
||||
self
|
||||
}
|
||||
|
||||
fn emit(&mut self, opcode: crate::asm::Opcode, mode: AM) {
|
||||
self.instructions.push(Instruction::new(opcode, mode));
|
||||
}
|
||||
|
||||
fn emit_label(&mut self, name: &str) {
|
||||
self.instructions
|
||||
.push(Instruction::new(NOP, AM::Label(name.to_string())));
|
||||
}
|
||||
|
||||
/// Return the zero-page address for an IR temp, allocating a new slot
|
||||
/// if needed.
|
||||
fn temp_addr(&mut self, temp: IrTemp) -> u8 {
|
||||
if let Some(&slot) = self.temp_slots.get(&temp) {
|
||||
return slot;
|
||||
}
|
||||
let slot = TEMP_BASE + self.next_temp_slot;
|
||||
self.next_temp_slot = self.next_temp_slot.wrapping_add(1);
|
||||
self.temp_slots.insert(temp, slot);
|
||||
slot
|
||||
}
|
||||
|
||||
/// Load a temp's value into A.
|
||||
fn load_temp(&mut self, temp: IrTemp) {
|
||||
let addr = self.temp_addr(temp);
|
||||
self.emit(LDA, AM::ZeroPage(addr));
|
||||
}
|
||||
|
||||
/// Store A into a temp's slot.
|
||||
fn store_temp(&mut self, temp: IrTemp) {
|
||||
let addr = self.temp_addr(temp);
|
||||
self.emit(STA, AM::ZeroPage(addr));
|
||||
}
|
||||
|
||||
/// Generate instructions for an entire IR program.
|
||||
/// Returns the flat list of 6502 instructions in the same order
|
||||
/// expected by the linker (variable init → main loop → function bodies).
|
||||
pub fn generate(mut self, ir: &IrProgram) -> Vec<Instruction> {
|
||||
// Emit variable initializers for globals with literal init values.
|
||||
for global in &ir.globals {
|
||||
if let Some(val) = global.init_value {
|
||||
if let Some(&addr) = self.var_addrs.get(&global.var_id) {
|
||||
self.emit(LDA, AM::Immediate(val as u8));
|
||||
if addr < 0x100 {
|
||||
self.emit(STA, AM::ZeroPage(addr as u8));
|
||||
} else {
|
||||
self.emit(STA, AM::Absolute(addr));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Emit each function body
|
||||
for func in &ir.functions {
|
||||
self.gen_function(func);
|
||||
}
|
||||
|
||||
self.instructions
|
||||
}
|
||||
|
||||
fn gen_function(&mut self, func: &IrFunction) {
|
||||
// Reset temp slot allocator per function
|
||||
self.temp_slots.clear();
|
||||
self.next_temp_slot = 0;
|
||||
|
||||
self.emit_label(&format!("__ir_fn_{}", func.name));
|
||||
|
||||
for block in &func.blocks {
|
||||
self.gen_block(block);
|
||||
}
|
||||
}
|
||||
|
||||
fn gen_block(&mut self, block: &IrBasicBlock) {
|
||||
self.emit_label(&format!("__ir_blk_{}", block.label));
|
||||
|
||||
for op in &block.ops {
|
||||
self.gen_op(op);
|
||||
}
|
||||
|
||||
self.gen_terminator(&block.terminator);
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_lines)]
|
||||
fn gen_op(&mut self, op: &IrOp) {
|
||||
match op {
|
||||
IrOp::LoadImm(dest, val) => {
|
||||
self.emit(LDA, AM::Immediate(*val));
|
||||
self.store_temp(*dest);
|
||||
}
|
||||
IrOp::LoadVar(dest, var) => {
|
||||
if let Some(&addr) = self.var_addrs.get(var) {
|
||||
if addr < 0x100 {
|
||||
self.emit(LDA, AM::ZeroPage(addr as u8));
|
||||
} else {
|
||||
self.emit(LDA, AM::Absolute(addr));
|
||||
}
|
||||
self.store_temp(*dest);
|
||||
}
|
||||
}
|
||||
IrOp::StoreVar(var, src) => {
|
||||
if let Some(&addr) = self.var_addrs.get(var) {
|
||||
self.load_temp(*src);
|
||||
if addr < 0x100 {
|
||||
self.emit(STA, AM::ZeroPage(addr as u8));
|
||||
} else {
|
||||
self.emit(STA, AM::Absolute(addr));
|
||||
}
|
||||
}
|
||||
}
|
||||
IrOp::Add(d, a, b) => {
|
||||
self.load_temp(*a);
|
||||
self.emit(CLC, AM::Implied);
|
||||
let b_addr = self.temp_addr(*b);
|
||||
self.emit(ADC, AM::ZeroPage(b_addr));
|
||||
self.store_temp(*d);
|
||||
}
|
||||
IrOp::Sub(d, a, b) => {
|
||||
self.load_temp(*a);
|
||||
self.emit(SEC, AM::Implied);
|
||||
let b_addr = self.temp_addr(*b);
|
||||
self.emit(SBC, AM::ZeroPage(b_addr));
|
||||
self.store_temp(*d);
|
||||
}
|
||||
IrOp::Mul(d, a, b) => {
|
||||
// Software multiply: multiplicand in A, multiplier in $02
|
||||
self.load_temp(*a);
|
||||
self.emit(PHA, AM::Implied); // Save for __multiply contract
|
||||
let b_addr = self.temp_addr(*b);
|
||||
self.emit(LDA, AM::ZeroPage(b_addr));
|
||||
self.emit(STA, AM::ZeroPage(0x02));
|
||||
self.emit(PLA, AM::Implied);
|
||||
self.emit(JSR, AM::Label("__multiply".into()));
|
||||
self.store_temp(*d);
|
||||
}
|
||||
IrOp::And(d, a, b) => {
|
||||
self.load_temp(*a);
|
||||
let b_addr = self.temp_addr(*b);
|
||||
self.emit(AND, AM::ZeroPage(b_addr));
|
||||
self.store_temp(*d);
|
||||
}
|
||||
IrOp::Or(d, a, b) => {
|
||||
self.load_temp(*a);
|
||||
let b_addr = self.temp_addr(*b);
|
||||
self.emit(ORA, AM::ZeroPage(b_addr));
|
||||
self.store_temp(*d);
|
||||
}
|
||||
IrOp::Xor(d, a, b) => {
|
||||
self.load_temp(*a);
|
||||
let b_addr = self.temp_addr(*b);
|
||||
self.emit(EOR, AM::ZeroPage(b_addr));
|
||||
self.store_temp(*d);
|
||||
}
|
||||
IrOp::ShiftLeft(d, a, count) => {
|
||||
self.load_temp(*a);
|
||||
for _ in 0..*count {
|
||||
self.emit(ASL, AM::Accumulator);
|
||||
}
|
||||
self.store_temp(*d);
|
||||
}
|
||||
IrOp::ShiftRight(d, a, count) => {
|
||||
self.load_temp(*a);
|
||||
for _ in 0..*count {
|
||||
self.emit(LSR, AM::Accumulator);
|
||||
}
|
||||
self.store_temp(*d);
|
||||
}
|
||||
IrOp::Negate(d, src) => {
|
||||
self.load_temp(*src);
|
||||
self.emit(EOR, AM::Immediate(0xFF));
|
||||
self.emit(CLC, AM::Implied);
|
||||
self.emit(ADC, AM::Immediate(1));
|
||||
self.store_temp(*d);
|
||||
}
|
||||
IrOp::Complement(d, src) => {
|
||||
self.load_temp(*src);
|
||||
self.emit(EOR, AM::Immediate(0xFF));
|
||||
self.store_temp(*d);
|
||||
}
|
||||
IrOp::CmpEq(d, a, b) => self.gen_cmp(*d, *a, *b, CmpKind::Eq),
|
||||
IrOp::CmpNe(d, a, b) => self.gen_cmp(*d, *a, *b, CmpKind::Ne),
|
||||
IrOp::CmpLt(d, a, b) => self.gen_cmp(*d, *a, *b, CmpKind::Lt),
|
||||
IrOp::CmpGt(d, a, b) => self.gen_cmp(*d, *a, *b, CmpKind::Gt),
|
||||
IrOp::CmpLtEq(d, a, b) => self.gen_cmp(*d, *a, *b, CmpKind::LtEq),
|
||||
IrOp::CmpGtEq(d, a, b) => self.gen_cmp(*d, *a, *b, CmpKind::GtEq),
|
||||
IrOp::ArrayLoad(dest, var, idx) => {
|
||||
if let Some(&base_addr) = self.var_addrs.get(var) {
|
||||
self.load_temp(*idx);
|
||||
self.emit(TAX, AM::Implied);
|
||||
if base_addr < 0x100 {
|
||||
self.emit(LDA, AM::ZeroPageX(base_addr as u8));
|
||||
} else {
|
||||
self.emit(LDA, AM::AbsoluteX(base_addr));
|
||||
}
|
||||
self.store_temp(*dest);
|
||||
}
|
||||
}
|
||||
IrOp::ArrayStore(var, idx, val) => {
|
||||
if let Some(&base_addr) = self.var_addrs.get(var) {
|
||||
self.load_temp(*idx);
|
||||
self.emit(TAX, AM::Implied);
|
||||
self.load_temp(*val);
|
||||
if base_addr < 0x100 {
|
||||
self.emit(STA, AM::ZeroPageX(base_addr as u8));
|
||||
} else {
|
||||
self.emit(STA, AM::AbsoluteX(base_addr));
|
||||
}
|
||||
}
|
||||
}
|
||||
IrOp::Call(dest, name, args) => {
|
||||
for (i, arg) in args.iter().enumerate() {
|
||||
self.load_temp(*arg);
|
||||
self.emit(STA, AM::ZeroPage(0x04 + i as u8));
|
||||
}
|
||||
self.emit(JSR, AM::Label(format!("__fn_{name}")));
|
||||
if let Some(d) = dest {
|
||||
// Return value is in A
|
||||
self.store_temp(*d);
|
||||
}
|
||||
}
|
||||
IrOp::DrawSprite {
|
||||
sprite_name,
|
||||
x,
|
||||
y,
|
||||
frame,
|
||||
} => {
|
||||
// Writes to OAM slot 0 for IR codegen (simple, single sprite).
|
||||
// Multi-OAM would require a slot counter like the AST codegen.
|
||||
self.load_temp(*y);
|
||||
self.emit(STA, AM::Absolute(0x0200));
|
||||
if let Some(f) = frame {
|
||||
self.load_temp(*f);
|
||||
} else if let Some(&tile) = self.sprite_tiles.get(sprite_name) {
|
||||
self.emit(LDA, AM::Immediate(tile));
|
||||
} else {
|
||||
self.emit(LDA, AM::Immediate(0));
|
||||
}
|
||||
self.emit(STA, AM::Absolute(0x0201));
|
||||
self.emit(LDA, AM::Immediate(0));
|
||||
self.emit(STA, AM::Absolute(0x0202));
|
||||
self.load_temp(*x);
|
||||
self.emit(STA, AM::Absolute(0x0203));
|
||||
}
|
||||
IrOp::ReadInput(dest) => {
|
||||
self.emit(LDA, AM::ZeroPage(0x01)); // ZP_INPUT_P1
|
||||
self.store_temp(*dest);
|
||||
}
|
||||
IrOp::WaitFrame => {
|
||||
// Poll frame flag at $00 until nonzero, then clear it
|
||||
let wait_label = format!("__ir_wait_{}", self.instructions.len());
|
||||
self.emit_label(&wait_label);
|
||||
self.emit(LDA, AM::ZeroPage(0x00));
|
||||
self.emit(BEQ, AM::LabelRelative(wait_label));
|
||||
self.emit(LDA, AM::Immediate(0));
|
||||
self.emit(STA, AM::ZeroPage(0x00));
|
||||
}
|
||||
IrOp::Transition(name) => {
|
||||
// Write state index 0 as a placeholder — the AST codegen
|
||||
// does the real state index mapping. For IR codegen demo
|
||||
// purposes we emit a no-op transition here.
|
||||
let _ = name;
|
||||
}
|
||||
IrOp::SourceLoc(_) => {
|
||||
// No code for source location markers
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn gen_cmp(&mut self, dest: IrTemp, a: IrTemp, b: IrTemp, kind: CmpKind) {
|
||||
self.load_temp(a);
|
||||
let b_addr = self.temp_addr(b);
|
||||
self.emit(CMP, AM::ZeroPage(b_addr));
|
||||
|
||||
let true_label = format!("__ir_cmp_t_{}", self.instructions.len());
|
||||
let end_label = format!("__ir_cmp_e_{}", self.instructions.len());
|
||||
|
||||
match kind {
|
||||
CmpKind::Eq => self.emit(BEQ, AM::LabelRelative(true_label.clone())),
|
||||
CmpKind::Ne => self.emit(BNE, AM::LabelRelative(true_label.clone())),
|
||||
CmpKind::Lt => self.emit(BCC, AM::LabelRelative(true_label.clone())),
|
||||
CmpKind::GtEq => self.emit(BCS, AM::LabelRelative(true_label.clone())),
|
||||
CmpKind::Gt => {
|
||||
// > : not equal AND carry set
|
||||
self.emit(BEQ, AM::LabelRelative(end_label.clone()));
|
||||
self.emit(BCS, AM::LabelRelative(true_label.clone()));
|
||||
}
|
||||
CmpKind::LtEq => {
|
||||
// <= : equal OR carry clear
|
||||
self.emit(BEQ, AM::LabelRelative(true_label.clone()));
|
||||
self.emit(BCC, AM::LabelRelative(true_label.clone()));
|
||||
}
|
||||
}
|
||||
// False path
|
||||
self.emit(LDA, AM::Immediate(0));
|
||||
self.emit(JMP, AM::Label(end_label.clone()));
|
||||
// True path
|
||||
self.emit_label(&true_label);
|
||||
self.emit(LDA, AM::Immediate(1));
|
||||
self.emit_label(&end_label);
|
||||
self.store_temp(dest);
|
||||
}
|
||||
|
||||
fn gen_terminator(&mut self, terminator: &IrTerminator) {
|
||||
match terminator {
|
||||
IrTerminator::Jump(label) => {
|
||||
self.emit(JMP, AM::Label(format!("__ir_blk_{label}")));
|
||||
}
|
||||
IrTerminator::Branch(cond, true_label, false_label) => {
|
||||
self.load_temp(*cond);
|
||||
// BNE true; JMP false
|
||||
self.emit(BNE, AM::LabelRelative(format!("__ir_blk_{true_label}")));
|
||||
self.emit(JMP, AM::Label(format!("__ir_blk_{false_label}")));
|
||||
}
|
||||
IrTerminator::Return(value) => {
|
||||
if let Some(v) = value {
|
||||
self.load_temp(*v);
|
||||
}
|
||||
self.emit(RTS, AM::Implied);
|
||||
}
|
||||
IrTerminator::Unreachable => {
|
||||
// Generate a BRK just in case
|
||||
self.emit(BRK, AM::Implied);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
enum CmpKind {
|
||||
Eq,
|
||||
Ne,
|
||||
Lt,
|
||||
Gt,
|
||||
LtEq,
|
||||
GtEq,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::analyzer;
|
||||
use crate::ir;
|
||||
use crate::parser;
|
||||
|
||||
fn lower_and_gen(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).generate(&ir_program)
|
||||
}
|
||||
|
||||
fn has_instruction(insts: &[Instruction], opcode: crate::asm::Opcode, mode: &AM) -> bool {
|
||||
insts.iter().any(|i| i.opcode == opcode && i.mode == *mode)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ir_codegen_minimal_program() {
|
||||
let insts = lower_and_gen(
|
||||
r#"
|
||||
game "T" { mapper: NROM }
|
||||
var x: u8 = 42
|
||||
on frame { x = 1 }
|
||||
start Main
|
||||
"#,
|
||||
);
|
||||
// Should initialize x = 42
|
||||
assert!(has_instruction(&insts, LDA, &AM::Immediate(42)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ir_codegen_plus_assign() {
|
||||
let insts = lower_and_gen(
|
||||
r#"
|
||||
game "T" { mapper: NROM }
|
||||
var x: u8 = 0
|
||||
on frame { x += 5 }
|
||||
start Main
|
||||
"#,
|
||||
);
|
||||
// Should emit CLC + ADC for the add
|
||||
assert!(has_instruction(&insts, CLC, &AM::Implied));
|
||||
assert!(insts.iter().any(|i| i.opcode == ADC));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ir_codegen_draw_sprite() {
|
||||
let insts = lower_and_gen(
|
||||
r#"
|
||||
game "T" { mapper: NROM }
|
||||
var px: u8 = 0
|
||||
var py: u8 = 0
|
||||
on frame { draw Smiley at: (px, py) }
|
||||
start Main
|
||||
"#,
|
||||
);
|
||||
// Should write to OAM slot 0
|
||||
assert!(has_instruction(&insts, STA, &AM::Absolute(0x0200)));
|
||||
assert!(has_instruction(&insts, STA, &AM::Absolute(0x0203)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ir_codegen_wait_frame() {
|
||||
let insts = lower_and_gen(
|
||||
r#"
|
||||
game "T" { mapper: NROM }
|
||||
on frame { wait_frame }
|
||||
start Main
|
||||
"#,
|
||||
);
|
||||
// Should poll frame flag
|
||||
assert!(has_instruction(&insts, LDA, &AM::ZeroPage(0x00)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ir_codegen_button_read() {
|
||||
let insts = lower_and_gen(
|
||||
r#"
|
||||
game "T" { mapper: NROM }
|
||||
var x: u8 = 0
|
||||
on frame {
|
||||
if button.right { x += 1 }
|
||||
}
|
||||
start Main
|
||||
"#,
|
||||
);
|
||||
// Should read input byte
|
||||
assert!(has_instruction(&insts, LDA, &AM::ZeroPage(0x01)));
|
||||
// Should AND with mask
|
||||
assert!(insts.iter().any(|i| i.opcode == AND));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ir_codegen_while_loop() {
|
||||
let insts = lower_and_gen(
|
||||
r#"
|
||||
game "T" { mapper: NROM }
|
||||
var x: u8 = 0
|
||||
on frame {
|
||||
while x < 10 { x += 1 }
|
||||
}
|
||||
start Main
|
||||
"#,
|
||||
);
|
||||
// Should emit CMP + conditional branch
|
||||
assert!(insts.iter().any(|i| i.opcode == CMP));
|
||||
assert!(insts.iter().any(|i| i.opcode == JMP || i.opcode == BNE));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ir_codegen_if_branch() {
|
||||
let insts = lower_and_gen(
|
||||
r#"
|
||||
game "T" { mapper: NROM }
|
||||
var x: u8 = 0
|
||||
on frame {
|
||||
if x == 0 { x = 1 }
|
||||
}
|
||||
start Main
|
||||
"#,
|
||||
);
|
||||
// Should emit CMP + branch
|
||||
assert!(insts.iter().any(|i| i.opcode == CMP));
|
||||
}
|
||||
}
|
||||
|
|
@ -1,6 +1,10 @@
|
|||
pub mod ir_codegen;
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
|
||||
pub use ir_codegen::IrCodeGen;
|
||||
|
||||
use std::collections::HashMap;
|
||||
|
||||
use crate::analyzer::VarAllocation;
|
||||
|
|
|
|||
|
|
@ -534,13 +534,15 @@ impl LoweringContext {
|
|||
t
|
||||
}
|
||||
Expr::ButtonRead(_, button, _) => {
|
||||
// Button reads are lowered to a ReadInput + mask check
|
||||
self.emit(IrOp::ReadInput);
|
||||
let t = self.fresh_temp();
|
||||
// Button reads: read the input byte, mask with the button bit.
|
||||
// (Player selection is ignored here; IR codegen handles it.)
|
||||
let input = self.fresh_temp();
|
||||
self.emit(IrOp::ReadInput(input));
|
||||
let mask = button_mask(button);
|
||||
let mask_temp = self.fresh_temp();
|
||||
self.emit(IrOp::LoadImm(mask_temp, mask));
|
||||
self.emit(IrOp::And(t, t, mask_temp));
|
||||
let t = self.fresh_temp();
|
||||
self.emit(IrOp::And(t, input, mask_temp));
|
||||
t
|
||||
}
|
||||
Expr::ArrayLiteral(_, _) => {
|
||||
|
|
@ -593,6 +595,14 @@ impl LoweringContext {
|
|||
t
|
||||
}
|
||||
|
||||
/// Emit an IR "move" from `src` to `dest`: `dest = src | 0`.
|
||||
/// Used to merge values from different control-flow paths.
|
||||
fn emit_move(&mut self, dest: IrTemp, src: IrTemp) {
|
||||
let zero = self.fresh_temp();
|
||||
self.emit(IrOp::LoadImm(zero, 0));
|
||||
self.emit(IrOp::Or(dest, src, zero));
|
||||
}
|
||||
|
||||
fn lower_logical_and(&mut self, left: &Expr, right: &Expr) -> IrTemp {
|
||||
let result = self.fresh_temp();
|
||||
let right_label = self.fresh_label("and_right");
|
||||
|
|
@ -609,7 +619,7 @@ impl LoweringContext {
|
|||
// Right side (only evaluated if left is true)
|
||||
self.start_block(&right_label);
|
||||
let r = self.lower_expr(right);
|
||||
self.emit(IrOp::StoreVar(VarId(self.next_var_id), r)); // temp storage
|
||||
self.emit_move(result, r);
|
||||
self.end_block(IrTerminator::Jump(end_label.clone()));
|
||||
|
||||
// False path
|
||||
|
|
@ -643,7 +653,7 @@ impl LoweringContext {
|
|||
// Right side
|
||||
self.start_block(&right_label);
|
||||
let r = self.lower_expr(right);
|
||||
self.emit(IrOp::StoreVar(VarId(self.next_var_id), r));
|
||||
self.emit_move(result, r);
|
||||
self.end_block(IrTerminator::Jump(end_label.clone()));
|
||||
|
||||
// Merge
|
||||
|
|
|
|||
|
|
@ -120,7 +120,8 @@ pub enum IrOp {
|
|||
y: IrTemp,
|
||||
frame: Option<IrTemp>,
|
||||
},
|
||||
ReadInput,
|
||||
/// Read the current player 1 input byte into a temp.
|
||||
ReadInput(IrTemp),
|
||||
WaitFrame,
|
||||
Transition(String),
|
||||
|
||||
|
|
|
|||
|
|
@ -152,7 +152,7 @@ fn lower_button_read() {
|
|||
.blocks
|
||||
.iter()
|
||||
.flat_map(|b| &b.ops)
|
||||
.any(|op| matches!(op, IrOp::ReadInput));
|
||||
.any(|op| matches!(op, IrOp::ReadInput(_)));
|
||||
assert!(has_input, "button read should emit ReadInput op");
|
||||
}
|
||||
|
||||
|
|
|
|||
27
src/main.rs
27
src/main.rs
|
|
@ -28,6 +28,11 @@ enum Cli {
|
|||
/// Dump generated 6502 assembly to stdout
|
||||
#[arg(long)]
|
||||
asm_dump: bool,
|
||||
|
||||
/// Use the experimental IR-based codegen instead of the
|
||||
/// AST-based codegen. Enables optimizer passes on the output.
|
||||
#[arg(long)]
|
||||
use_ir: bool,
|
||||
},
|
||||
/// Type-check a source file without building
|
||||
Check {
|
||||
|
|
@ -45,9 +50,10 @@ fn main() {
|
|||
output,
|
||||
debug,
|
||||
asm_dump,
|
||||
use_ir,
|
||||
} => {
|
||||
let output = output.unwrap_or_else(|| input.with_extension("nes"));
|
||||
match compile(&input, debug, asm_dump) {
|
||||
match compile(&input, debug, asm_dump, use_ir) {
|
||||
Ok(rom) => {
|
||||
std::fs::write(&output, rom).unwrap_or_else(|e| {
|
||||
eprintln!("error: failed to write {}: {e}", output.display());
|
||||
|
|
@ -80,7 +86,7 @@ fn dump_asm(instructions: &[nescript::asm::Instruction]) {
|
|||
}
|
||||
}
|
||||
|
||||
fn compile(input: &PathBuf, debug: bool, asm_dump: bool) -> Result<Vec<u8>, ()> {
|
||||
fn compile(input: &PathBuf, debug: bool, asm_dump: bool, use_ir: bool) -> Result<Vec<u8>, ()> {
|
||||
let raw_source = std::fs::read_to_string(input).map_err(|e| {
|
||||
eprintln!("error: failed to read {}: {e}", input.display());
|
||||
})?;
|
||||
|
|
@ -129,11 +135,18 @@ fn compile(input: &PathBuf, debug: bool, asm_dump: bool) -> Result<Vec<u8>, ()>
|
|||
eprintln!("error: {e}");
|
||||
})?;
|
||||
|
||||
// Code generation (still AST-based for M2; IR codegen comes in M3)
|
||||
let codegen = CodeGen::new(&analysis.var_allocations, &program.constants)
|
||||
.with_sprites(&sprites)
|
||||
.with_debug(debug);
|
||||
let instructions = codegen.generate(&program);
|
||||
// Code generation: choose between IR-based and AST-based codegen.
|
||||
let instructions = if use_ir {
|
||||
use nescript::codegen::IrCodeGen;
|
||||
IrCodeGen::new(&analysis.var_allocations, &ir_program)
|
||||
.with_sprites(&sprites)
|
||||
.generate(&ir_program)
|
||||
} else {
|
||||
CodeGen::new(&analysis.var_allocations, &program.constants)
|
||||
.with_sprites(&sprites)
|
||||
.with_debug(debug)
|
||||
.generate(&program)
|
||||
};
|
||||
|
||||
if asm_dump {
|
||||
dump_asm(&instructions);
|
||||
|
|
|
|||
|
|
@ -379,7 +379,7 @@ fn collect_source_temps(op: &IrOp, used: &mut HashSet<IrTemp>) {
|
|||
used.insert(*f);
|
||||
}
|
||||
}
|
||||
IrOp::ReadInput | IrOp::WaitFrame | IrOp::Transition(_) | IrOp::SourceLoc(_) => {}
|
||||
IrOp::ReadInput(_) | IrOp::WaitFrame | IrOp::Transition(_) | IrOp::SourceLoc(_) => {}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -406,10 +406,10 @@ fn op_dest(op: &IrOp) -> Option<IrTemp> {
|
|||
| IrOp::CmpGtEq(d, _, _)
|
||||
| IrOp::ArrayLoad(d, _, _) => Some(*d),
|
||||
IrOp::Call(dest, _, _) => *dest,
|
||||
IrOp::ReadInput(d) => Some(*d),
|
||||
IrOp::StoreVar(_, _)
|
||||
| IrOp::ArrayStore(_, _, _)
|
||||
| IrOp::DrawSprite { .. }
|
||||
| IrOp::ReadInput
|
||||
| IrOp::WaitFrame
|
||||
| IrOp::Transition(_)
|
||||
| IrOp::SourceLoc(_) => None,
|
||||
|
|
|
|||
|
|
@ -482,3 +482,69 @@ fn program_with_mmc1() {
|
|||
let info = rom::validate_ines(&rom_data).expect("should be valid iNES");
|
||||
assert_eq!(info.mapper, 1, "should be MMC1 (mapper 1)");
|
||||
}
|
||||
|
||||
// ── IR Codegen Tests ──
|
||||
|
||||
/// Compile a program using the IR-based codegen path instead of the
|
||||
/// AST-based codegen. Validates the full IR pipeline produces a valid ROM.
|
||||
fn compile_with_ir_codegen(source: &str) -> Vec<u8> {
|
||||
use nescript::codegen::IrCodeGen;
|
||||
|
||||
let (program, diags) = nescript::parser::parse(source);
|
||||
assert!(
|
||||
diags.is_empty(),
|
||||
"unexpected parse errors: {diags:?}\nsource:\n{source}"
|
||||
);
|
||||
let program = program.expect("parse should succeed");
|
||||
|
||||
let analysis = analyzer::analyze(&program);
|
||||
assert!(
|
||||
analysis.diagnostics.iter().all(|d| !d.is_error()),
|
||||
"unexpected analysis errors: {:?}",
|
||||
analysis.diagnostics
|
||||
);
|
||||
|
||||
// Lower to IR and run the optimizer
|
||||
let mut ir_program = ir::lower(&program, &analysis);
|
||||
optimizer::optimize(&mut ir_program);
|
||||
|
||||
// IR-based codegen
|
||||
let codegen = IrCodeGen::new(&analysis.var_allocations, &ir_program);
|
||||
let instructions = codegen.generate(&ir_program);
|
||||
|
||||
// Link into a ROM
|
||||
let linker = Linker::new(program.game.mirroring);
|
||||
linker.link(&instructions)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ir_codegen_minimal_rom() {
|
||||
let source = r#"
|
||||
game "IR Test" { mapper: NROM }
|
||||
var x: u8 = 42
|
||||
on frame { wait_frame }
|
||||
start Main
|
||||
"#;
|
||||
let rom_data = compile_with_ir_codegen(source);
|
||||
let info = rom::validate_ines(&rom_data).expect("should be valid iNES");
|
||||
assert_eq!(info.mapper, 0);
|
||||
assert_eq!(rom_data.len(), 16 + 16384 + 8192);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ir_codegen_full_pipeline() {
|
||||
let source = r#"
|
||||
game "IR Full" { mapper: NROM }
|
||||
var x: u8 = 0
|
||||
var y: u8 = 0
|
||||
on frame {
|
||||
if button.right { x += 1 }
|
||||
if button.left { x -= 1 }
|
||||
if x > 100 { x = 0 }
|
||||
draw Smiley at: (x, y)
|
||||
}
|
||||
start Main
|
||||
"#;
|
||||
let rom_data = compile_with_ir_codegen(source);
|
||||
rom::validate_ines(&rom_data).expect("should be valid iNES");
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue