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);
|
|
|
|
|
}
|
2026-04-11 23:32:12 +00:00
|
|
|
|
|
|
|
|
#[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:?}"
|
|
|
|
|
);
|
|
|
|
|
}
|