mirror of
https://github.com/imjasonh/nescript
synced 2026-07-08 17:06:04 +00:00
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
This commit is contained in:
parent
1fca6864ac
commit
39ca246151
32 changed files with 6306 additions and 0 deletions
17
tests/integration/hello_sprite.ne
Normal file
17
tests/integration/hello_sprite.ne
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
game "Hello Sprite" {
|
||||
mapper: NROM
|
||||
}
|
||||
|
||||
var px: u8 = 128
|
||||
var py: u8 = 120
|
||||
|
||||
on frame {
|
||||
if button.right { px += 2 }
|
||||
if button.left { px -= 2 }
|
||||
if button.down { py += 2 }
|
||||
if button.up { py -= 2 }
|
||||
|
||||
draw Smiley at: (px, py)
|
||||
}
|
||||
|
||||
start Main
|
||||
141
tests/integration_test.rs
Normal file
141
tests/integration_test.rs
Normal file
|
|
@ -0,0 +1,141 @@
|
|||
use nescript::analyzer;
|
||||
use nescript::codegen::CodeGen;
|
||||
use nescript::linker::Linker;
|
||||
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
|
||||
);
|
||||
|
||||
let codegen = CodeGen::new(&analysis.var_allocations, &program.constants);
|
||||
let instructions = codegen.generate(&program);
|
||||
|
||||
let linker = Linker::new(program.game.mirroring);
|
||||
linker.link(&instructions)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hello_sprite_compiles_to_valid_rom() {
|
||||
let source = include_str!("integration/hello_sprite.ne");
|
||||
let rom_data = compile(source);
|
||||
|
||||
// Validate iNES format
|
||||
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)");
|
||||
|
||||
// ROM should be 16 header + 16 KB PRG + 8 KB CHR
|
||||
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);
|
||||
|
||||
// Vector table at the end of PRG ROM
|
||||
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");
|
||||
}
|
||||
|
||||
#[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"
|
||||
);
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue