1
0
Fork 0
mirror of https://github.com/imjasonh/nescript synced 2026-07-08 17:06:04 +00:00
nescript/tests/integration_test.rs

551 lines
15 KiB
Rust
Raw Normal View History

use std::path::Path;
Implement NEScript compiler Milestone 1 ("Hello Sprite") Complete implementation of the NEScript compiler pipeline for M1: - Lexer: full tokenization with hex/binary/decimal literals, all keywords, operators - Parser: recursive descent with Pratt expression parsing (M1 subset) - Analyzer: symbol resolution, type checking, memory allocation - 6502 Assembler: full opcode encoding table (~150 valid combinations) - Code Generator: AST → 6502 instructions (direct, no IR for M1) - Runtime: NES hardware init, NMI handler, controller read, OAM DMA - Linker: NROM layout, vector table, palette loading, CHR data - ROM Builder: iNES header generation, PRG/CHR padding - CLI: `build` and `check` subcommands via clap 143 tests across all modules: - 22 lexer tests (literals, keywords, operators, error recovery) - 18 parser tests (expressions, statements, game structure, errors) - 7 analyzer tests (symbol resolution, memory allocation, transitions) - 30 assembler tests (every addressing mode, label resolution) - 7 codegen tests (var init, arithmetic, buttons, draw, comparisons) - 11 runtime tests (init sequence, NMI handler, controller read) - 10 ROM builder tests (iNES format, mirroring, banking, validation) - 5 linker tests (vector table, CHR data, palette loading) - 7 integration tests (end-to-end compilation, error detection) CI: GitHub Actions for check, fmt, clippy, test Pre-commit: script for local fmt + clippy + test validation https://claude.ai/code/session_01W6eQFStA66EuMKHUFo2rx3
2026-04-11 22:07:56 +00:00
use nescript::analyzer;
use nescript::assets;
Implement NEScript compiler Milestone 1 ("Hello Sprite") Complete implementation of the NEScript compiler pipeline for M1: - Lexer: full tokenization with hex/binary/decimal literals, all keywords, operators - Parser: recursive descent with Pratt expression parsing (M1 subset) - Analyzer: symbol resolution, type checking, memory allocation - 6502 Assembler: full opcode encoding table (~150 valid combinations) - Code Generator: AST → 6502 instructions (direct, no IR for M1) - Runtime: NES hardware init, NMI handler, controller read, OAM DMA - Linker: NROM layout, vector table, palette loading, CHR data - ROM Builder: iNES header generation, PRG/CHR padding - CLI: `build` and `check` subcommands via clap 143 tests across all modules: - 22 lexer tests (literals, keywords, operators, error recovery) - 18 parser tests (expressions, statements, game structure, errors) - 7 analyzer tests (symbol resolution, memory allocation, transitions) - 30 assembler tests (every addressing mode, label resolution) - 7 codegen tests (var init, arithmetic, buttons, draw, comparisons) - 11 runtime tests (init sequence, NMI handler, controller read) - 10 ROM builder tests (iNES format, mirroring, banking, validation) - 5 linker tests (vector table, CHR data, palette loading) - 7 integration tests (end-to-end compilation, error detection) CI: GitHub Actions for check, fmt, clippy, test Pre-commit: script for local fmt + clippy + test validation https://claude.ai/code/session_01W6eQFStA66EuMKHUFo2rx3
2026-04-11 22:07:56 +00:00
use nescript::codegen::CodeGen;
use nescript::ir;
Implement NEScript compiler Milestone 1 ("Hello Sprite") Complete implementation of the NEScript compiler pipeline for M1: - Lexer: full tokenization with hex/binary/decimal literals, all keywords, operators - Parser: recursive descent with Pratt expression parsing (M1 subset) - Analyzer: symbol resolution, type checking, memory allocation - 6502 Assembler: full opcode encoding table (~150 valid combinations) - Code Generator: AST → 6502 instructions (direct, no IR for M1) - Runtime: NES hardware init, NMI handler, controller read, OAM DMA - Linker: NROM layout, vector table, palette loading, CHR data - ROM Builder: iNES header generation, PRG/CHR padding - CLI: `build` and `check` subcommands via clap 143 tests across all modules: - 22 lexer tests (literals, keywords, operators, error recovery) - 18 parser tests (expressions, statements, game structure, errors) - 7 analyzer tests (symbol resolution, memory allocation, transitions) - 30 assembler tests (every addressing mode, label resolution) - 7 codegen tests (var init, arithmetic, buttons, draw, comparisons) - 11 runtime tests (init sequence, NMI handler, controller read) - 10 ROM builder tests (iNES format, mirroring, banking, validation) - 5 linker tests (vector table, CHR data, palette loading) - 7 integration tests (end-to-end compilation, error detection) CI: GitHub Actions for check, fmt, clippy, test Pre-commit: script for local fmt + clippy + test validation https://claude.ai/code/session_01W6eQFStA66EuMKHUFo2rx3
2026-04-11 22:07:56 +00:00
use nescript::linker::Linker;
use nescript::optimizer;
Implement NEScript compiler Milestone 1 ("Hello Sprite") Complete implementation of the NEScript compiler pipeline for M1: - Lexer: full tokenization with hex/binary/decimal literals, all keywords, operators - Parser: recursive descent with Pratt expression parsing (M1 subset) - Analyzer: symbol resolution, type checking, memory allocation - 6502 Assembler: full opcode encoding table (~150 valid combinations) - Code Generator: AST → 6502 instructions (direct, no IR for M1) - Runtime: NES hardware init, NMI handler, controller read, OAM DMA - Linker: NROM layout, vector table, palette loading, CHR data - ROM Builder: iNES header generation, PRG/CHR padding - CLI: `build` and `check` subcommands via clap 143 tests across all modules: - 22 lexer tests (literals, keywords, operators, error recovery) - 18 parser tests (expressions, statements, game structure, errors) - 7 analyzer tests (symbol resolution, memory allocation, transitions) - 30 assembler tests (every addressing mode, label resolution) - 7 codegen tests (var init, arithmetic, buttons, draw, comparisons) - 11 runtime tests (init sequence, NMI handler, controller read) - 10 ROM builder tests (iNES format, mirroring, banking, validation) - 5 linker tests (vector table, CHR data, palette loading) - 7 integration tests (end-to-end compilation, error detection) CI: GitHub Actions for check, fmt, clippy, test Pre-commit: script for local fmt + clippy + test validation https://claude.ai/code/session_01W6eQFStA66EuMKHUFo2rx3
2026-04-11 22:07:56 +00:00
use nescript::rom;
/// Compile a `NEScript` source string into a .nes ROM.
fn compile(source: &str) -> Vec<u8> {
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
);
// Run IR lowering and optimization (validates the pipeline works)
let mut ir_program = ir::lower(&program, &analysis);
optimizer::optimize(&mut ir_program);
let sprites = assets::resolve_sprites(&program, Path::new("."))
.expect("sprite resolution should succeed");
let codegen =
CodeGen::new(&analysis.var_allocations, &program.constants).with_sprites(&sprites);
Implement NEScript compiler Milestone 1 ("Hello Sprite") Complete implementation of the NEScript compiler pipeline for M1: - Lexer: full tokenization with hex/binary/decimal literals, all keywords, operators - Parser: recursive descent with Pratt expression parsing (M1 subset) - Analyzer: symbol resolution, type checking, memory allocation - 6502 Assembler: full opcode encoding table (~150 valid combinations) - Code Generator: AST → 6502 instructions (direct, no IR for M1) - Runtime: NES hardware init, NMI handler, controller read, OAM DMA - Linker: NROM layout, vector table, palette loading, CHR data - ROM Builder: iNES header generation, PRG/CHR padding - CLI: `build` and `check` subcommands via clap 143 tests across all modules: - 22 lexer tests (literals, keywords, operators, error recovery) - 18 parser tests (expressions, statements, game structure, errors) - 7 analyzer tests (symbol resolution, memory allocation, transitions) - 30 assembler tests (every addressing mode, label resolution) - 7 codegen tests (var init, arithmetic, buttons, draw, comparisons) - 11 runtime tests (init sequence, NMI handler, controller read) - 10 ROM builder tests (iNES format, mirroring, banking, validation) - 5 linker tests (vector table, CHR data, palette loading) - 7 integration tests (end-to-end compilation, error detection) CI: GitHub Actions for check, fmt, clippy, test Pre-commit: script for local fmt + clippy + test validation https://claude.ai/code/session_01W6eQFStA66EuMKHUFo2rx3
2026-04-11 22:07:56 +00:00
let instructions = codegen.generate(&program);
let linker = Linker::new(program.game.mirroring);
linker.link_with_assets(&instructions, &sprites)
Implement NEScript compiler Milestone 1 ("Hello Sprite") Complete implementation of the NEScript compiler pipeline for M1: - Lexer: full tokenization with hex/binary/decimal literals, all keywords, operators - Parser: recursive descent with Pratt expression parsing (M1 subset) - Analyzer: symbol resolution, type checking, memory allocation - 6502 Assembler: full opcode encoding table (~150 valid combinations) - Code Generator: AST → 6502 instructions (direct, no IR for M1) - Runtime: NES hardware init, NMI handler, controller read, OAM DMA - Linker: NROM layout, vector table, palette loading, CHR data - ROM Builder: iNES header generation, PRG/CHR padding - CLI: `build` and `check` subcommands via clap 143 tests across all modules: - 22 lexer tests (literals, keywords, operators, error recovery) - 18 parser tests (expressions, statements, game structure, errors) - 7 analyzer tests (symbol resolution, memory allocation, transitions) - 30 assembler tests (every addressing mode, label resolution) - 7 codegen tests (var init, arithmetic, buttons, draw, comparisons) - 11 runtime tests (init sequence, NMI handler, controller read) - 10 ROM builder tests (iNES format, mirroring, banking, validation) - 5 linker tests (vector table, CHR data, palette loading) - 7 integration tests (end-to-end compilation, error detection) CI: GitHub Actions for check, fmt, clippy, test Pre-commit: script for local fmt + clippy + test validation https://claude.ai/code/session_01W6eQFStA66EuMKHUFo2rx3
2026-04-11 22:07:56 +00:00
}
// ── M1 Tests ──
Implement NEScript compiler Milestone 1 ("Hello Sprite") Complete implementation of the NEScript compiler pipeline for M1: - Lexer: full tokenization with hex/binary/decimal literals, all keywords, operators - Parser: recursive descent with Pratt expression parsing (M1 subset) - Analyzer: symbol resolution, type checking, memory allocation - 6502 Assembler: full opcode encoding table (~150 valid combinations) - Code Generator: AST → 6502 instructions (direct, no IR for M1) - Runtime: NES hardware init, NMI handler, controller read, OAM DMA - Linker: NROM layout, vector table, palette loading, CHR data - ROM Builder: iNES header generation, PRG/CHR padding - CLI: `build` and `check` subcommands via clap 143 tests across all modules: - 22 lexer tests (literals, keywords, operators, error recovery) - 18 parser tests (expressions, statements, game structure, errors) - 7 analyzer tests (symbol resolution, memory allocation, transitions) - 30 assembler tests (every addressing mode, label resolution) - 7 codegen tests (var init, arithmetic, buttons, draw, comparisons) - 11 runtime tests (init sequence, NMI handler, controller read) - 10 ROM builder tests (iNES format, mirroring, banking, validation) - 5 linker tests (vector table, CHR data, palette loading) - 7 integration tests (end-to-end compilation, error detection) CI: GitHub Actions for check, fmt, clippy, test Pre-commit: script for local fmt + clippy + test validation https://claude.ai/code/session_01W6eQFStA66EuMKHUFo2rx3
2026-04-11 22:07:56 +00:00
#[test]
fn hello_sprite_compiles_to_valid_rom() {
let source = include_str!("integration/hello_sprite.ne");
let rom_data = compile(source);
let info = rom::validate_ines(&rom_data).expect("should be valid iNES");
assert_eq!(info.prg_banks, 1, "should be 1 PRG bank (16 KB)");
assert_eq!(info.chr_banks, 1, "should have CHR ROM");
assert_eq!(info.mapper, 0, "should be NROM (mapper 0)");
assert_eq!(rom_data.len(), 16 + 16384 + 8192);
}
#[test]
fn hello_sprite_has_correct_vectors() {
let source = include_str!("integration/hello_sprite.ne");
let rom_data = compile(source);
let prg_end = 16 + 16384;
let nmi = u16::from_le_bytes([rom_data[prg_end - 6], rom_data[prg_end - 5]]);
let reset = u16::from_le_bytes([rom_data[prg_end - 4], rom_data[prg_end - 3]]);
let irq = u16::from_le_bytes([rom_data[prg_end - 2], rom_data[prg_end - 1]]);
assert!(nmi >= 0xC000, "NMI vector should be in ROM space");
assert_eq!(reset, 0xC000, "RESET should point to $C000");
assert!(irq >= 0xC000, "IRQ vector should be in ROM space");
assert!(nmi != reset, "NMI and RESET should be different");
}
#[test]
fn minimal_program_compiles() {
let source = r#"
game "Minimal" { mapper: NROM }
on frame { wait_frame }
start Main
"#;
let rom_data = compile(source);
let info = rom::validate_ines(&rom_data).expect("should be valid iNES");
assert_eq!(info.mapper, 0);
}
#[test]
fn program_with_state_machine() {
let source = r#"
game "States" { mapper: NROM }
state Title {
on frame {
if button.start { transition Game }
}
}
state Game {
var score: u8 = 0
on frame {
score += 1
}
}
start Title
"#;
let rom_data = compile(source);
rom::validate_ines(&rom_data).expect("should be valid iNES");
}
#[test]
fn program_with_constants() {
let source = r#"
game "Constants" { mapper: NROM }
const SPEED: u8 = 3
var px: u8 = 100
on frame {
if button.right { px += SPEED }
}
start Main
"#;
let rom_data = compile(source);
rom::validate_ines(&rom_data).expect("should be valid iNES");
}
// ── M2 Tests ──
#[test]
fn program_with_functions() {
let source = r#"
game "Functions" { mapper: NROM }
var x: u8 = 0
fun add_ten(val: u8) -> u8 {
return val + 10
}
on frame {
x = add_ten(5)
}
start Main
"#;
let rom_data = compile(source);
rom::validate_ines(&rom_data).expect("should be valid iNES");
}
#[test]
fn program_with_while_loop() {
let source = r#"
game "Loops" { mapper: NROM }
var x: u8 = 0
on frame {
while x < 10 {
x += 1
}
}
start Main
"#;
let rom_data = compile(source);
rom::validate_ines(&rom_data).expect("should be valid iNES");
}
#[test]
fn program_with_fast_slow_vars() {
let source = r#"
game "Placement" { mapper: NROM }
fast var hot: u8 = 0
slow var cold: u8 = 0
on frame {
hot += 1
cold += 1
}
start Main
"#;
let rom_data = compile(source);
rom::validate_ines(&rom_data).expect("should be valid iNES");
}
#[test]
fn program_with_multi_state_transitions() {
let source = r#"
game "Multi" { mapper: NROM }
state Menu {
on enter { wait_frame }
on frame {
if button.start { transition Level1 }
}
}
state Level1 {
var timer: u8 = 0
on frame {
timer += 1
if timer > 60 {
transition Level2
}
}
}
state Level2 {
on frame {
if button.select { transition Menu }
}
}
start Menu
"#;
let rom_data = compile(source);
rom::validate_ines(&rom_data).expect("should be valid iNES");
}
#[test]
fn coin_cavern_compiles() {
let source = include_str!("../examples/coin_cavern.ne");
let rom_data = compile(source);
let info = rom::validate_ines(&rom_data).expect("should be valid iNES");
assert_eq!(info.mapper, 0);
}
#[test]
fn ir_pipeline_produces_ir() {
let source = r#"
game "IR" { mapper: NROM }
const SPEED: u8 = 2
var x: u8 = 0
fun double(n: u8) -> u8 { return n + n }
on frame {
x += SPEED
if x > 100 { x = 0 }
}
start Main
"#;
let (program, diags) = nescript::parser::parse(source);
assert!(diags.is_empty());
let program = program.unwrap();
let analysis = analyzer::analyze(&program);
assert!(analysis.diagnostics.iter().all(|d| !d.is_error()));
let mut ir_program = ir::lower(&program, &analysis);
let before_ops = ir_program.op_count();
optimizer::optimize(&mut ir_program);
let after_ops = ir_program.op_count();
// Optimizer should reduce or maintain op count (not increase)
assert!(after_ops <= before_ops, "optimizer should not increase ops");
// Should have functions for the user function + frame handler
assert!(ir_program.functions.len() >= 2);
}
Implement NEScript compiler Milestone 1 ("Hello Sprite") Complete implementation of the NEScript compiler pipeline for M1: - Lexer: full tokenization with hex/binary/decimal literals, all keywords, operators - Parser: recursive descent with Pratt expression parsing (M1 subset) - Analyzer: symbol resolution, type checking, memory allocation - 6502 Assembler: full opcode encoding table (~150 valid combinations) - Code Generator: AST → 6502 instructions (direct, no IR for M1) - Runtime: NES hardware init, NMI handler, controller read, OAM DMA - Linker: NROM layout, vector table, palette loading, CHR data - ROM Builder: iNES header generation, PRG/CHR padding - CLI: `build` and `check` subcommands via clap 143 tests across all modules: - 22 lexer tests (literals, keywords, operators, error recovery) - 18 parser tests (expressions, statements, game structure, errors) - 7 analyzer tests (symbol resolution, memory allocation, transitions) - 30 assembler tests (every addressing mode, label resolution) - 7 codegen tests (var init, arithmetic, buttons, draw, comparisons) - 11 runtime tests (init sequence, NMI handler, controller read) - 10 ROM builder tests (iNES format, mirroring, banking, validation) - 5 linker tests (vector table, CHR data, palette loading) - 7 integration tests (end-to-end compilation, error detection) CI: GitHub Actions for check, fmt, clippy, test Pre-commit: script for local fmt + clippy + test validation https://claude.ai/code/session_01W6eQFStA66EuMKHUFo2rx3
2026-04-11 22:07:56 +00:00
#[test]
fn error_test_missing_game() {
let source = "var x: u8 = 0\nstart Main";
let (_, diags) = nescript::parser::parse(source);
assert!(
diags.iter().any(nescript::errors::Diagnostic::is_error),
"should produce error"
);
}
#[test]
fn error_test_undefined_transition() {
let source = r#"
game "T" { mapper: NROM }
state Main {
on frame { transition Nonexistent }
}
start Main
"#;
let (program, parse_diags) = nescript::parser::parse(source);
assert!(parse_diags.is_empty());
let analysis = analyzer::analyze(&program.unwrap());
assert!(
analysis
.diagnostics
.iter()
.any(nescript::errors::Diagnostic::is_error),
"should detect undefined transition target"
);
}
#[test]
fn error_test_recursion_detected() {
let source = r#"
game "T" { mapper: NROM }
fun loop_forever() { loop_forever() }
on frame { wait_frame }
start Main
"#;
let (program, parse_diags) = nescript::parser::parse(source);
assert!(parse_diags.is_empty());
let analysis = analyzer::analyze(&program.unwrap());
assert!(
analysis
.diagnostics
.iter()
.any(|d| d.code == nescript::errors::ErrorCode::E0402),
"should detect recursion"
);
}
// ── M4 Tests ──
#[test]
fn program_with_scroll_and_cast() {
let source = r#"
game "M4 Test" { mapper: NROM }
var px: u8 = 0
var py: u8 = 0
var wide: u16 = 0
on frame {
if button.right { px += 1 }
wide = px as u16
scroll(px, py)
}
start Main
"#;
let rom_data = compile(source);
rom::validate_ines(&rom_data).expect("should be valid iNES");
}
// ── M3 Tests ──
#[test]
fn program_with_sprites_and_palette() {
let source = r#"
game "M3 Assets" { mapper: NROM }
sprite Player {
chr: [0x3C, 0x42, 0x81, 0x81, 0x81, 0x81, 0x42, 0x3C,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]
}
palette MainPal {
colors: [0x0F, 0x00, 0x10, 0x20]
}
background TitleBg {
chr: @binary("title.bin")
}
var px: u8 = 128
var py: u8 = 120
state Title {
on enter {
load_background TitleBg
set_palette MainPal
}
on frame {
if button.right { px += 2 }
if button.left { px -= 2 }
draw Player at: (px, py)
}
}
start Title
"#;
let rom_data = compile(source);
rom::validate_ines(&rom_data).expect("should be valid iNES");
}
// ── M5 Tests ──
/// Compile a source string using the mapper-aware linker.
fn compile_with_mapper(source: &str) -> Vec<u8> {
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
);
let mut ir_program = ir::lower(&program, &analysis);
nescript::optimizer::optimize(&mut ir_program);
let sprites = assets::resolve_sprites(&program, Path::new("."))
.expect("sprite resolution should succeed");
let codegen = nescript::codegen::CodeGen::new(&analysis.var_allocations, &program.constants)
.with_sprites(&sprites);
let instructions = codegen.generate(&program);
let linker = Linker::with_mapper(program.game.mirroring, program.game.mapper);
linker.link_with_assets(&instructions, &sprites)
}
#[test]
fn sprite_resolution_uses_tile_index() {
// The Player sprite has 16 unique bytes of CHR data. Because tile index 0
// is reserved for the built-in smiley, the compiler should place Player
// at tile index 1 and `draw Player` should store that tile index in OAM.
//
// We check this in two ways:
// 1. The CHR ROM contains Player's bytes at tile 1 (offset 16).
// 2. The PRG ROM contains the immediate-load sequence `A9 01 8D 01 02`
// (LDA #$01 ; STA $0201) — writing tile index 1 into OAM byte 1.
let source = r#"
game "SpriteTile" { mapper: NROM }
sprite Player {
chr: [0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17,
0x18, 0x19, 0x1A, 0x1B, 0x1C, 0x1D, 0x1E, 0x1F]
}
var px: u8 = 128
var py: u8 = 120
state Title {
on frame {
draw Player at: (px, py)
}
}
start Title
"#;
let rom_data = compile(source);
// CHR ROM begins right after PRG ROM (16 header + 16384 PRG).
let chr_start = 16 + 16384;
// Tile 1 lives at CHR offset 16 (16 bytes per tile).
let tile1 = &rom_data[chr_start + 16..chr_start + 32];
assert_eq!(
tile1,
&[
0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1A, 0x1B, 0x1C, 0x1D,
0x1E, 0x1F
],
"Player sprite CHR bytes should be placed at tile index 1",
);
// The default smiley tile at index 0 should still be non-zero (untouched).
let tile0 = &rom_data[chr_start..chr_start + 16];
assert_ne!(
tile0, &[0u8; 16],
"tile 0 should still contain the default smiley",
);
// In PRG ROM, look for `LDA #$01 ; STA $0201` which writes the Player's
// tile index (1) into the tile-index byte of the first OAM slot.
let prg = &rom_data[16..16 + 16384];
let pattern = [0xA9u8, 0x01, 0x8D, 0x01, 0x02];
assert!(
prg.windows(pattern.len()).any(|w| w == pattern),
"PRG ROM should contain LDA #$01 ; STA $0201 for draw Player",
);
}
Implement codegen for state dispatch, functions, arrays, math, scroll State machine dispatch: - Assign each state a numeric index, store in ZP $03 - Main loop dispatch table: CMP + BNE + JMP trampoline pattern (avoids branch range limits for large programs) - on_enter/on_exit handlers generated as JSR targets - Transition statement writes state index + JSR enter/exit handlers Function calls: - Function bodies emitted as labeled subroutines with RTS - Statement::Call generates parameter passing via ZP + JSR - Statement::Return generates RTS (with value in A if present) - Parameter slots at ZP $04-$07 Break/continue: - Loop stack tracks continue/break label pairs - Break generates JMP to break_label - Continue generates JMP to continue_label - While and Loop push/pop the stack Array indexing: - LValue::ArrayIndex generates TAX + STA absolute,X - Expr::ArrayIndex generates TAX + LDA absolute,X / ZP,X - Compound array assignments (+=, -=, &=, |=, ^=) load-modify-store Scroll: - scroll(x, y) writes to PPU $2005 twice (X then Y) Math: - Multiply generates JSR __multiply (shift-and-add routine) - Divide generates JSR __divide (restoring division) - Modulo loads remainder from $03 after divide - ShiftLeft generates ASL A, ShiftRight generates LSR A - Math routines wired into linker Error validations: - E0203 for assignment to const variables - Break/continue outside loop detection (in_loop tracking) 233 tests (8 new codegen + 2 analyzer + 2 integration), all passing. https://claude.ai/code/session_01W6eQFStA66EuMKHUFo2rx3
2026-04-12 02:04:49 +00:00
#[test]
fn program_with_arrays_and_math() {
let source = r#"
game "ArrayMath" { mapper: NROM }
var arr: u8[4] = [10, 20, 30, 40]
var idx: u8 = 0
var result: u8 = 0
on frame {
result = arr[idx] * 2
idx += 1
}
start Main
"#;
let rom_data = compile(source);
rom::validate_ines(&rom_data).expect("should be valid iNES");
}
#[test]
fn program_with_mmc1() {
let source = r#"
game "MMC1 Game" { mapper: MMC1 }
var px: u8 = 128
on frame {
if button.right { px += 2 }
}
start Main
"#;
let rom_data = compile_with_mapper(source);
let info = rom::validate_ines(&rom_data).expect("should be valid iNES");
assert_eq!(info.mapper, 1, "should be MMC1 (mapper 1)");
}
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
2026-04-12 10:23:43 +00:00
// ── 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");
}