1
0
Fork 0
mirror of https://github.com/imjasonh/nescript synced 2026-07-09 09:18:01 +00:00

Remove the legacy AST codegen — IR path is canonical now

The `--use-ast` path through `src/codegen/mod.rs` was a strictly
inferior subset of the IR codegen. Building every example with
`--use-ast` through the jsnes harness:

- `arrays_and_functions` — fully black (array init + function
  return values + OAM-in-loop all broken)
- `structs_enums_for` — fully black (struct literal is a no-op,
  all fields stay at 0)
- `inline_asm_demo` — fully black
- `bitwise_ops`, `loop_break_continue` — below sprite floors
  (static `next_oam_slot` bug B)
- `match_demo` — panics at compile time with
  `branch offset 153 out of range` (AST's if/else-chain
  desugaring of `match` emits short branches that can't reach
  the far arms in a multi-arm match)

Six of fourteen examples are non-functional under `--use-ast`.
The other eight happen to fall inside the subset AST handles
(no arrays, no structs, no function return values, no
multi-sprite loops, no long match chains).

`docs/future-work.md` already listed "Once working, delete the
AST-based codegen entirely" as the intended direction. It's
working, so this commit does the deletion.

What's removed:
- The `CodeGen` struct, its impl block, and every helper in
  `src/codegen/mod.rs` (the AST codegen body) — ~1150 lines.
  The file is now a module header that re-exports `IrCodeGen`.
- `src/codegen/tests.rs` — 15 AST-specific instruction-pattern
  tests. Every feature they covered has an equivalent test in
  `src/codegen/ir_codegen.rs::{tests,more_tests}` already.
- The `--use-ast` CLI flag and its branch in `src/main.rs`.
- `compile_with_ir_codegen` in `tests/integration_test.rs` —
  `compile()` now does what it did, so they merged. All 40
  integration tests go through the IR path.
- Outdated sections in `docs/future-work.md` that described the
  IR codegen as "not yet implemented" and listed AST codegen
  gaps as priority work.

What's kept:
- `src/codegen/ir_codegen.rs` — the real codegen.
- `src/codegen/peephole.rs` — post-codegen cleanup pass, now
  run unconditionally from `main.rs`.

Test plan:
- `cargo test --release` — 313 unit + 37 integration tests pass
  (was 328 + 37; the 15 dropped are the deleted AST-specific
  tests).
- `cargo fmt --check` clean.
- `cargo clippy --release --all-targets -- -D warnings` clean.
- `node tests/emulator/run_examples.mjs` — 14/14 ROMs render
  above their per-example nonBlack floors.
- The one tightening: `sprite_resolution_uses_tile_index` was
  asserting on the old static-slot encoding
  (`A9 01 8D 01 02`). Updated to the cursor-based form
  (`A9 01 99 01 02`, i.e. STA AbsoluteY).

Net diff: 1581 deletions, 62 insertions.

https://claude.ai/code/session_014Z5y3Q9krLcAxYpZQJhZ5V
This commit is contained in:
Claude 2026-04-12 20:37:59 +00:00
parent 54acb9ee38
commit a757336681
No known key found for this signature in database
6 changed files with 61 additions and 1580 deletions

File diff suppressed because it is too large Load diff

View file

@ -1,278 +0,0 @@
use super::*;
use crate::analyzer;
use crate::asm::{AddressingMode as AM, Opcode::*};
use crate::parser;
fn compile_to_instructions(src: &str) -> Vec<Instruction> {
let (prog, diags) = parser::parse(src);
assert!(diags.is_empty(), "parse errors: {diags:?}");
let prog = prog.unwrap();
let analysis = analyzer::analyze(&prog);
assert!(
analysis.diagnostics.iter().all(|d| !d.is_error()),
"analysis errors: {:?}",
analysis.diagnostics
);
let codegen = CodeGen::new(&analysis.var_allocations, &prog.constants);
codegen.generate(&prog)
}
fn has_instruction(instructions: &[Instruction], opcode: crate::asm::Opcode, mode: &AM) -> bool {
instructions
.iter()
.any(|i| i.opcode == opcode && i.mode == *mode)
}
#[test]
fn codegen_var_init() {
let src = r#"
game "Test" { mapper: NROM }
var px: u8 = 128
on frame { wait_frame }
start Main
"#;
let insts = compile_to_instructions(src);
// Should have LDA #128 and STA to zero page
assert!(has_instruction(&insts, LDA, &AM::Immediate(128)));
}
#[test]
fn codegen_plus_assign() {
let src = r#"
game "Test" { mapper: NROM }
var px: u8 = 0
on frame { px += 2 }
start Main
"#;
let insts = compile_to_instructions(src);
// Should have CLC, ADC #2
assert!(has_instruction(&insts, CLC, &AM::Implied));
assert!(has_instruction(&insts, ADC, &AM::Immediate(2)));
}
#[test]
fn codegen_minus_assign() {
let src = r#"
game "Test" { mapper: NROM }
var px: u8 = 100
on frame { px -= 1 }
start Main
"#;
let insts = compile_to_instructions(src);
assert!(has_instruction(&insts, SEC, &AM::Implied));
assert!(has_instruction(&insts, SBC, &AM::Immediate(1)));
}
#[test]
fn codegen_button_check() {
let src = r#"
game "Test" { mapper: NROM }
var px: u8 = 0
on frame {
if button.right { px += 1 }
}
start Main
"#;
let insts = compile_to_instructions(src);
// Should read controller input and AND with right button mask (0x01)
assert!(has_instruction(&insts, AND, &AM::Immediate(0x01)));
}
#[test]
fn codegen_draw_sprite() {
let src = r#"
game "Test" { mapper: NROM }
var px: u8 = 64
var py: u8 = 64
on frame {
draw Smiley at: (px, py)
}
start Main
"#;
let insts = compile_to_instructions(src);
// Should write to OAM buffer at $0200-$0203
assert!(has_instruction(&insts, STA, &AM::Absolute(0x0200))); // Y
assert!(has_instruction(&insts, STA, &AM::Absolute(0x0201))); // tile
assert!(has_instruction(&insts, STA, &AM::Absolute(0x0202))); // attr
assert!(has_instruction(&insts, STA, &AM::Absolute(0x0203))); // X
}
#[test]
fn codegen_const_usage() {
let src = r#"
game "Test" { mapper: NROM }
const SPEED: u8 = 2
var px: u8 = 0
on frame { px += SPEED }
start Main
"#;
let insts = compile_to_instructions(src);
// Constant should be inlined as immediate
assert!(has_instruction(&insts, ADC, &AM::Immediate(2)));
}
#[test]
fn codegen_main_loop_structure() {
let src = r#"
game "Test" { mapper: NROM }
on frame { wait_frame }
start Main
"#;
let insts = compile_to_instructions(src);
// Should have JMP back to loop start
let has_jmp = insts.iter().any(|i| {
i.opcode == JMP && matches!(&i.mode, AM::Label(l) if l.starts_with("__main_loop"))
});
assert!(has_jmp, "should have JMP to main loop");
}
#[test]
fn codegen_comparison() {
let src = r#"
game "Test" { mapper: NROM }
var x: u8 = 0
on frame {
if x == 10 { x = 0 }
}
start Main
"#;
let insts = compile_to_instructions(src);
assert!(has_instruction(&insts, CMP, &AM::ZeroPage(0x02)));
}
#[test]
fn codegen_array_index_read() {
let src = r#"
game "Test" { mapper: NROM }
var arr: u8[4] = [1, 2, 3, 4]
var idx: u8 = 0
var result: u8 = 0
on frame {
result = arr[idx]
}
start Main
"#;
let insts = compile_to_instructions(src);
// Reading arr[idx] should use TAX + LDA,X
assert!(has_instruction(&insts, TAX, &AM::Implied));
}
#[test]
fn codegen_array_index_write() {
let src = r#"
game "Test" { mapper: NROM }
var arr: u8[4] = [1, 2, 3, 4]
var idx: u8 = 0
on frame {
arr[idx] = 42
}
start Main
"#;
let insts = compile_to_instructions(src);
// Writing arr[idx] = val should use TAX + STA,X
assert!(has_instruction(&insts, TAX, &AM::Implied));
}
#[test]
fn codegen_scroll() {
let src = r#"
game "Test" { mapper: NROM }
var sx: u8 = 0
var sy: u8 = 0
on frame {
scroll(sx, sy)
}
start Main
"#;
let insts = compile_to_instructions(src);
// scroll(x, y) should write to $2005 twice
let count_2005 = insts
.iter()
.filter(|i| i.opcode == STA && i.mode == AM::Absolute(0x2005))
.count();
assert_eq!(count_2005, 2, "scroll should write to $2005 twice");
}
#[test]
fn codegen_multiply() {
let src = r#"
game "Test" { mapper: NROM }
var a: u8 = 3
var b: u8 = 5
var result: u8 = 0
on frame {
result = a * b
}
start Main
"#;
let insts = compile_to_instructions(src);
// a * b should generate JSR __multiply
let has_jsr_multiply = insts
.iter()
.any(|i| i.opcode == JSR && matches!(&i.mode, AM::Label(l) if l == "__multiply"));
assert!(has_jsr_multiply, "multiply should generate JSR __multiply");
}
#[test]
fn codegen_shift_left() {
let src = r#"
game "Test" { mapper: NROM }
var x: u8 = 1
var result: u8 = 0
on frame {
result = x << 1
}
start Main
"#;
let insts = compile_to_instructions(src);
// x << 1 should generate ASL A
assert!(
has_instruction(&insts, ASL, &AM::Accumulator),
"shift left should generate ASL A"
);
}
#[test]
fn codegen_debug_log_stripped_in_release() {
// Without --debug, debug.log should not emit any $4800 writes
let src = r#"
game "T" { mapper: NROM }
var x: u8 = 42
on frame {
debug.log(x)
}
start Main
"#;
let insts = compile_to_instructions(src);
// Should NOT write to $4800
let has_debug = insts
.iter()
.any(|i| i.opcode == STA && i.mode == AM::Absolute(0x4800));
assert!(!has_debug, "debug.log should be stripped in release mode");
}
#[test]
fn codegen_debug_log_emits_in_debug_mode() {
use crate::analyzer;
use crate::parser;
let src = r#"
game "T" { mapper: NROM }
var x: u8 = 42
on frame {
debug.log(x)
}
start Main
"#;
let (prog, _) = parser::parse(src);
let prog = prog.unwrap();
let analysis = analyzer::analyze(&prog);
let codegen = CodeGen::new(&analysis.var_allocations, &prog.constants).with_debug(true);
let insts = codegen.generate(&prog);
// Should write to $4800 at least once
let has_debug = insts
.iter()
.any(|i| i.opcode == STA && i.mode == AM::Absolute(0x4800));
assert!(has_debug, "debug.log should write to $4800 in debug mode");
}

View file

@ -3,7 +3,7 @@ use std::path::{Path, PathBuf};
use nescript::analyzer;
use nescript::assets;
use nescript::codegen::{CodeGen, IrCodeGen};
use nescript::codegen::IrCodeGen;
use nescript::errors::render_diagnostics;
use nescript::ir;
use nescript::linker::Linker;
@ -41,11 +41,6 @@ enum Cli {
/// Dump a call graph showing which functions call which.
#[arg(long)]
call_graph: bool,
/// Use the legacy AST-based codegen. The default is the IR-based
/// codegen, which runs the optimizer passes before emitting 6502.
#[arg(long)]
use_ast: bool,
},
/// Type-check a source file without building
Check {
@ -66,7 +61,6 @@ fn main() {
dump_ir,
memory_map,
call_graph,
use_ast,
} => {
let output = output.unwrap_or_else(|| input.with_extension("nes"));
match compile(
@ -77,7 +71,6 @@ fn main() {
dump_ir,
memory_map,
call_graph,
use_ast,
},
) {
Ok(rom) => {
@ -226,7 +219,6 @@ struct CompileOptions {
dump_ir: bool,
memory_map: bool,
call_graph: bool,
use_ast: bool,
}
fn compile(input: &PathBuf, opts: &CompileOptions) -> Result<Vec<u8>, ()> {
@ -235,7 +227,6 @@ fn compile(input: &PathBuf, opts: &CompileOptions) -> Result<Vec<u8>, ()> {
let dump_ir = opts.dump_ir;
let memory_map = opts.memory_map;
let call_graph = opts.call_graph;
let use_ast = opts.use_ast;
let raw_source = std::fs::read_to_string(input).map_err(|e| {
eprintln!("error: failed to read {}: {e}", input.display());
})?;
@ -296,24 +287,14 @@ fn compile(input: &PathBuf, opts: &CompileOptions) -> Result<Vec<u8>, ()> {
eprintln!("error: {e}");
})?;
// Code generation: IR-based is the default. `--use-ast` switches to
// the legacy AST-based codegen for comparison and fallback.
let mut instructions = if use_ast {
CodeGen::new(&analysis.var_allocations, &program.constants)
.with_sprites(&sprites)
.with_enums(&program.enums)
.with_debug(debug)
.generate(&program)
} else {
IrCodeGen::new(&analysis.var_allocations, &ir_program)
.with_sprites(&sprites)
.with_debug(debug)
.generate(&ir_program)
};
// IR-based code generation. Lower → optimize → emit 6502.
let mut instructions = IrCodeGen::new(&analysis.var_allocations, &ir_program)
.with_sprites(&sprites)
.with_debug(debug)
.generate(&ir_program);
// Peephole optimization: cheap pass that removes redundant
// store-then-load pairs over IR temp slots. Biggest win for the
// IR codegen, but safe for the AST codegen too.
// Peephole pass: cleans up the IR codegen's temp-heavy output —
// dead stores, redundant loads, short-branch folds, etc.
nescript::codegen::peephole::optimize(&mut instructions);
if asm_dump {