1
0
Fork 0
mirror of https://github.com/imjasonh/nescript synced 2026-07-10 01:37:45 +00:00
nescript/src/analyzer/tests.rs

257 lines
5.7 KiB
Rust
Raw Normal View History

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 super::*;
use crate::errors::ErrorCode;
use crate::parser;
fn analyze_ok(input: &str) -> AnalysisResult {
let (prog, diags) = parser::parse(input);
assert!(diags.is_empty(), "parse errors: {diags:?}");
let prog = prog.unwrap();
let result = analyze(&prog);
assert!(
result.diagnostics.iter().all(|d| !d.is_error()),
"analysis errors: {:?}",
result.diagnostics
);
result
}
fn analyze_errors(input: &str) -> Vec<ErrorCode> {
let (prog, parse_diags) = parser::parse(input);
if prog.is_none() {
return parse_diags.into_iter().map(|d| d.code).collect();
}
let result = analyze(&prog.unwrap());
result.diagnostics.into_iter().map(|d| d.code).collect()
}
#[test]
fn analyze_minimal_program() {
let result = analyze_ok(
r#"
game "Test" { mapper: NROM }
var px: u8 = 128
on frame { px = 1 }
start Main
"#,
);
assert!(result.symbols.contains_key("px"));
assert_eq!(result.var_allocations.len(), 1);
}
#[test]
fn analyze_allocates_zero_page() {
let result = analyze_ok(
r#"
game "Test" { mapper: NROM }
var x: u8 = 0
var y: u8 = 0
on frame { x = 1 }
start Main
"#,
);
// u8 vars should be allocated in zero page starting at $10
assert_eq!(result.var_allocations[0].address, 0x10);
assert_eq!(result.var_allocations[1].address, 0x11);
}
#[test]
fn analyze_duplicate_var() {
let errors = analyze_errors(
r#"
game "Test" { mapper: NROM }
var x: u8 = 0
var x: u8 = 1
on frame { x = 1 }
start Main
"#,
);
assert!(errors.contains(&ErrorCode::E0501));
}
#[test]
fn analyze_undefined_transition() {
let errors = analyze_errors(
r#"
game "Test" { mapper: NROM }
state Main {
on frame { transition Nonexistent }
}
start Main
"#,
);
assert!(errors.contains(&ErrorCode::E0404));
}
#[test]
fn analyze_valid_transition() {
let _result = analyze_ok(
r#"
game "Test" { mapper: NROM }
state Main {
on frame { transition Other }
}
state Other {
on frame { wait_frame }
}
start Main
"#,
);
}
#[test]
fn analyze_start_state_exists() {
let errors = analyze_errors(
r#"
game "Test" { mapper: NROM }
state Main {
on frame { wait_frame }
}
start Nonexistent
"#,
);
assert!(errors.contains(&ErrorCode::E0404));
}
#[test]
fn analyze_const_symbol() {
let result = analyze_ok(
r#"
game "Test" { mapper: NROM }
const SPEED: u8 = 2
var px: u8 = 0
on frame { px = SPEED }
start Main
"#,
);
let sym = result.symbols.get("SPEED").unwrap();
assert!(sym.is_const);
}
#[test]
fn analyze_function_registered() {
let result = analyze_ok(
r#"
game "Test" { mapper: NROM }
fun add(a: u8, b: u8) -> u8 { return a }
on frame { wait_frame }
start Main
"#,
);
assert!(result.symbols.contains_key("add"));
}
#[test]
fn analyze_recursion_detected() {
let errors = analyze_errors(
r#"
game "Test" { mapper: NROM }
fun a() { a() }
on frame { wait_frame }
start Main
"#,
);
assert!(errors.contains(&ErrorCode::E0402));
}
#[test]
fn analyze_mutual_recursion() {
let errors = analyze_errors(
r#"
game "Test" { mapper: NROM }
fun a() { b() }
fun b() { a() }
on frame { wait_frame }
start Main
"#,
);
assert!(errors.contains(&ErrorCode::E0402));
}
#[test]
fn analyze_call_depth_ok() {
// 3 levels of nesting — well within the default limit of 8
let result = analyze_ok(
r#"
game "Test" { mapper: NROM }
fun c() { wait_frame }
fun b() { c() }
fun a() { b() }
on frame { a() }
start Main
"#,
);
// The frame handler's depth should be <= 8
for &depth in result.max_depths.values() {
assert!(depth <= 8, "depth {depth} should be within limit");
}
}
#[test]
fn analyze_call_depth_exceeded() {
// Build a call chain deeper than 8: f1 -> f2 -> ... -> f10
let result = analyze_errors(
r#"
game "Test" { mapper: NROM }
fun f10() { wait_frame }
fun f9() { f10() }
fun f8() { f9() }
fun f7() { f8() }
fun f6() { f7() }
fun f5() { f6() }
fun f4() { f5() }
fun f3() { f4() }
fun f2() { f3() }
fun f1() { f2() }
on frame { f1() }
start Main
"#,
);
assert!(
result.contains(&ErrorCode::E0401),
"expected E0401 for exceeded call depth, got: {result:?}"
);
}
#[test]
fn analyze_undefined_function() {
let errors = analyze_errors(
r#"
game "Test" { mapper: NROM }
on frame { no_such_fn() }
start Main
"#,
);
assert!(errors.contains(&ErrorCode::E0503));
}
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 analyze_const_assignment_error() {
let errors = analyze_errors(
r#"
game "Test" { mapper: NROM }
const SPEED: u8 = 2
on frame { SPEED = 5 }
start Main
"#,
);
assert!(
errors.contains(&ErrorCode::E0203),
"assigning to const should produce E0203, got: {errors:?}"
);
}
#[test]
fn analyze_break_outside_loop() {
let errors = analyze_errors(
r#"
game "Test" { mapper: NROM }
on frame { break }
start Main
"#,
);
assert!(
errors.contains(&ErrorCode::E0203),
"break outside loop should produce E0203, got: {errors:?}"
);
}