2026-04-12 10:01:44 +00:00
|
|
|
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;
|
2026-04-12 10:01:44 +00:00
|
|
|
use nescript::assets;
|
Remove the legacy AST codegen — IR path is canonical now
The `--use-ast` path through `src/codegen/mod.rs` was a strictly
inferior subset of the IR codegen. Building every example with
`--use-ast` through the jsnes harness:
- `arrays_and_functions` — fully black (array init + function
return values + OAM-in-loop all broken)
- `structs_enums_for` — fully black (struct literal is a no-op,
all fields stay at 0)
- `inline_asm_demo` — fully black
- `bitwise_ops`, `loop_break_continue` — below sprite floors
(static `next_oam_slot` bug B)
- `match_demo` — panics at compile time with
`branch offset 153 out of range` (AST's if/else-chain
desugaring of `match` emits short branches that can't reach
the far arms in a multi-arm match)
Six of fourteen examples are non-functional under `--use-ast`.
The other eight happen to fall inside the subset AST handles
(no arrays, no structs, no function return values, no
multi-sprite loops, no long match chains).
`docs/future-work.md` already listed "Once working, delete the
AST-based codegen entirely" as the intended direction. It's
working, so this commit does the deletion.
What's removed:
- The `CodeGen` struct, its impl block, and every helper in
`src/codegen/mod.rs` (the AST codegen body) — ~1150 lines.
The file is now a module header that re-exports `IrCodeGen`.
- `src/codegen/tests.rs` — 15 AST-specific instruction-pattern
tests. Every feature they covered has an equivalent test in
`src/codegen/ir_codegen.rs::{tests,more_tests}` already.
- The `--use-ast` CLI flag and its branch in `src/main.rs`.
- `compile_with_ir_codegen` in `tests/integration_test.rs` —
`compile()` now does what it did, so they merged. All 40
integration tests go through the IR path.
- Outdated sections in `docs/future-work.md` that described the
IR codegen as "not yet implemented" and listed AST codegen
gaps as priority work.
What's kept:
- `src/codegen/ir_codegen.rs` — the real codegen.
- `src/codegen/peephole.rs` — post-codegen cleanup pass, now
run unconditionally from `main.rs`.
Test plan:
- `cargo test --release` — 313 unit + 37 integration tests pass
(was 328 + 37; the 15 dropped are the deleted AST-specific
tests).
- `cargo fmt --check` clean.
- `cargo clippy --release --all-targets -- -D warnings` clean.
- `node tests/emulator/run_examples.mjs` — 14/14 ROMs render
above their per-example nonBlack floors.
- The one tightening: `sprite_resolution_uses_tile_index` was
asserting on the old static-slot encoding
(`A9 01 8D 01 02`). Updated to the cursor-based form
(`A9 01 99 01 02`, i.e. STA AbsoluteY).
Net diff: 1581 deletions, 62 insertions.
https://claude.ai/code/session_014Z5y3Q9krLcAxYpZQJhZ5V
2026-04-12 20:37:59 +00:00
|
|
|
use nescript::codegen::IrCodeGen;
|
2026-04-11 23:34:35 +00:00
|
|
|
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;
|
2026-04-11 23:34:35 +00:00
|
|
|
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;
|
|
|
|
|
|
Remove the legacy AST codegen — IR path is canonical now
The `--use-ast` path through `src/codegen/mod.rs` was a strictly
inferior subset of the IR codegen. Building every example with
`--use-ast` through the jsnes harness:
- `arrays_and_functions` — fully black (array init + function
return values + OAM-in-loop all broken)
- `structs_enums_for` — fully black (struct literal is a no-op,
all fields stay at 0)
- `inline_asm_demo` — fully black
- `bitwise_ops`, `loop_break_continue` — below sprite floors
(static `next_oam_slot` bug B)
- `match_demo` — panics at compile time with
`branch offset 153 out of range` (AST's if/else-chain
desugaring of `match` emits short branches that can't reach
the far arms in a multi-arm match)
Six of fourteen examples are non-functional under `--use-ast`.
The other eight happen to fall inside the subset AST handles
(no arrays, no structs, no function return values, no
multi-sprite loops, no long match chains).
`docs/future-work.md` already listed "Once working, delete the
AST-based codegen entirely" as the intended direction. It's
working, so this commit does the deletion.
What's removed:
- The `CodeGen` struct, its impl block, and every helper in
`src/codegen/mod.rs` (the AST codegen body) — ~1150 lines.
The file is now a module header that re-exports `IrCodeGen`.
- `src/codegen/tests.rs` — 15 AST-specific instruction-pattern
tests. Every feature they covered has an equivalent test in
`src/codegen/ir_codegen.rs::{tests,more_tests}` already.
- The `--use-ast` CLI flag and its branch in `src/main.rs`.
- `compile_with_ir_codegen` in `tests/integration_test.rs` —
`compile()` now does what it did, so they merged. All 40
integration tests go through the IR path.
- Outdated sections in `docs/future-work.md` that described the
IR codegen as "not yet implemented" and listed AST codegen
gaps as priority work.
What's kept:
- `src/codegen/ir_codegen.rs` — the real codegen.
- `src/codegen/peephole.rs` — post-codegen cleanup pass, now
run unconditionally from `main.rs`.
Test plan:
- `cargo test --release` — 313 unit + 37 integration tests pass
(was 328 + 37; the 15 dropped are the deleted AST-specific
tests).
- `cargo fmt --check` clean.
- `cargo clippy --release --all-targets -- -D warnings` clean.
- `node tests/emulator/run_examples.mjs` — 14/14 ROMs render
above their per-example nonBlack floors.
- The one tightening: `sprite_resolution_uses_tile_index` was
asserting on the old static-slot encoding
(`A9 01 8D 01 02`). Updated to the cursor-based form
(`A9 01 99 01 02`, i.e. STA AbsoluteY).
Net diff: 1581 deletions, 62 insertions.
https://claude.ai/code/session_014Z5y3Q9krLcAxYpZQJhZ5V
2026-04-12 20:37:59 +00:00
|
|
|
/// Compile a `NEScript` source string into a .nes ROM. Runs the full
|
|
|
|
|
/// IR pipeline: parse → analyze → IR lower → optimize → IR codegen
|
|
|
|
|
/// → peephole → link. This is what the `nescript build` CLI does
|
|
|
|
|
/// (minus file IO and the dump flags), so these integration tests
|
|
|
|
|
/// exercise the same path end users hit.
|
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
|
|
|
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
|
|
|
|
|
);
|
|
|
|
|
|
2026-04-11 23:34:35 +00:00
|
|
|
let mut ir_program = ir::lower(&program, &analysis);
|
|
|
|
|
optimizer::optimize(&mut ir_program);
|
|
|
|
|
|
2026-04-12 10:01:44 +00:00
|
|
|
let sprites = assets::resolve_sprites(&program, Path::new("."))
|
|
|
|
|
.expect("sprite resolution should succeed");
|
|
|
|
|
|
Remove the legacy AST codegen — IR path is canonical now
The `--use-ast` path through `src/codegen/mod.rs` was a strictly
inferior subset of the IR codegen. Building every example with
`--use-ast` through the jsnes harness:
- `arrays_and_functions` — fully black (array init + function
return values + OAM-in-loop all broken)
- `structs_enums_for` — fully black (struct literal is a no-op,
all fields stay at 0)
- `inline_asm_demo` — fully black
- `bitwise_ops`, `loop_break_continue` — below sprite floors
(static `next_oam_slot` bug B)
- `match_demo` — panics at compile time with
`branch offset 153 out of range` (AST's if/else-chain
desugaring of `match` emits short branches that can't reach
the far arms in a multi-arm match)
Six of fourteen examples are non-functional under `--use-ast`.
The other eight happen to fall inside the subset AST handles
(no arrays, no structs, no function return values, no
multi-sprite loops, no long match chains).
`docs/future-work.md` already listed "Once working, delete the
AST-based codegen entirely" as the intended direction. It's
working, so this commit does the deletion.
What's removed:
- The `CodeGen` struct, its impl block, and every helper in
`src/codegen/mod.rs` (the AST codegen body) — ~1150 lines.
The file is now a module header that re-exports `IrCodeGen`.
- `src/codegen/tests.rs` — 15 AST-specific instruction-pattern
tests. Every feature they covered has an equivalent test in
`src/codegen/ir_codegen.rs::{tests,more_tests}` already.
- The `--use-ast` CLI flag and its branch in `src/main.rs`.
- `compile_with_ir_codegen` in `tests/integration_test.rs` —
`compile()` now does what it did, so they merged. All 40
integration tests go through the IR path.
- Outdated sections in `docs/future-work.md` that described the
IR codegen as "not yet implemented" and listed AST codegen
gaps as priority work.
What's kept:
- `src/codegen/ir_codegen.rs` — the real codegen.
- `src/codegen/peephole.rs` — post-codegen cleanup pass, now
run unconditionally from `main.rs`.
Test plan:
- `cargo test --release` — 313 unit + 37 integration tests pass
(was 328 + 37; the 15 dropped are the deleted AST-specific
tests).
- `cargo fmt --check` clean.
- `cargo clippy --release --all-targets -- -D warnings` clean.
- `node tests/emulator/run_examples.mjs` — 14/14 ROMs render
above their per-example nonBlack floors.
- The one tightening: `sprite_resolution_uses_tile_index` was
asserting on the old static-slot encoding
(`A9 01 8D 01 02`). Updated to the cursor-based form
(`A9 01 99 01 02`, i.e. STA AbsoluteY).
Net diff: 1581 deletions, 62 insertions.
https://claude.ai/code/session_014Z5y3Q9krLcAxYpZQJhZ5V
2026-04-12 20:37:59 +00:00
|
|
|
let codegen = IrCodeGen::new(&analysis.var_allocations, &ir_program).with_sprites(&sprites);
|
|
|
|
|
let mut instructions = codegen.generate(&ir_program);
|
|
|
|
|
nescript::codegen::peephole::optimize(&mut instructions);
|
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 linker = Linker::new(program.game.mirroring);
|
2026-04-12 10:01:44 +00:00
|
|
|
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
|
|
|
}
|
|
|
|
|
|
2026-04-11 23:34:35 +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");
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-11 23:34:35 +00:00
|
|
|
// ── 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)
|
|
|
|
|
}
|
2026-04-12 11:39:12 +00:00
|
|
|
start Main
|
|
|
|
|
"#;
|
|
|
|
|
let rom_data = compile(source);
|
2026-04-12 16:22:54 +00:00
|
|
|
rom::validate_ines(&rom_data).expect("should be valid iNES");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn program_with_on_scanline_mmc3() {
|
|
|
|
|
let source = r#"
|
|
|
|
|
game "Scanline" { mapper: MMC3 }
|
|
|
|
|
var sx: u8 = 0
|
|
|
|
|
state Main {
|
|
|
|
|
on frame { wait_frame }
|
|
|
|
|
on scanline(120) { scroll(sx, 0) }
|
|
|
|
|
}
|
|
|
|
|
start Main
|
|
|
|
|
"#;
|
|
|
|
|
let rom_data = compile(source);
|
2026-04-12 16:29:15 +00:00
|
|
|
rom::validate_ines(&rom_data).expect("should be valid iNES");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn program_with_on_scanline_per_state() {
|
|
|
|
|
// Two states, each with its own scanline handler at a different
|
|
|
|
|
// position. The IR codegen should emit per-state dispatch in
|
|
|
|
|
// both `__irq_user` and `__ir_mmc3_reload`.
|
|
|
|
|
let source = r#"
|
|
|
|
|
game "MultiSL" { mapper: MMC3 }
|
|
|
|
|
var s: u8 = 0
|
|
|
|
|
state A {
|
|
|
|
|
on frame { wait_frame }
|
|
|
|
|
on scanline(64) { scroll(0, 0) }
|
|
|
|
|
}
|
|
|
|
|
state B {
|
|
|
|
|
on frame { wait_frame }
|
|
|
|
|
on scanline(192) { scroll(0, 0) }
|
|
|
|
|
}
|
|
|
|
|
start A
|
|
|
|
|
"#;
|
|
|
|
|
let rom_data = compile(source);
|
2026-04-12 16:55:18 +00:00
|
|
|
rom::validate_ines(&rom_data).expect("should be valid iNES");
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-12 17:01:12 +00:00
|
|
|
#[test]
|
|
|
|
|
fn program_with_function_local_variables() {
|
|
|
|
|
// Functions with locally-declared variables should allocate
|
|
|
|
|
// their own backing storage and not corrupt caller state when
|
|
|
|
|
// nested.
|
|
|
|
|
let source = r#"
|
|
|
|
|
game "Locals" { mapper: NROM }
|
|
|
|
|
var out: u8 = 0
|
|
|
|
|
|
|
|
|
|
fun double(x: u8) -> u8 {
|
|
|
|
|
var t: u8 = x
|
|
|
|
|
t = t + t
|
|
|
|
|
return t
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fun double_sum(a: u8, b: u8) -> u8 {
|
|
|
|
|
var s1: u8 = double(a)
|
|
|
|
|
var s2: u8 = double(b)
|
|
|
|
|
return s1 + s2
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
on frame {
|
|
|
|
|
out = double_sum(10, 20)
|
|
|
|
|
wait_frame
|
|
|
|
|
}
|
|
|
|
|
start Main
|
|
|
|
|
"#;
|
|
|
|
|
let rom_data = compile(source);
|
|
|
|
|
rom::validate_ines(&rom_data).expect("should be valid iNES");
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-12 16:55:18 +00:00
|
|
|
#[test]
|
|
|
|
|
fn program_with_for_loop() {
|
|
|
|
|
let source = r#"
|
|
|
|
|
game "ForLoop" { mapper: NROM }
|
|
|
|
|
var arr: u8[8] = [0, 0, 0, 0, 0, 0, 0, 0]
|
|
|
|
|
var total: u8 = 0
|
|
|
|
|
on frame {
|
|
|
|
|
total = 0
|
|
|
|
|
for i in 0..8 {
|
|
|
|
|
total += arr[i]
|
|
|
|
|
}
|
|
|
|
|
wait_frame
|
|
|
|
|
}
|
|
|
|
|
start Main
|
|
|
|
|
"#;
|
|
|
|
|
let rom_data = compile(source);
|
2026-04-12 17:15:57 +00:00
|
|
|
rom::validate_ines(&rom_data).expect("should be valid iNES");
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-12 17:21:00 +00:00
|
|
|
#[test]
|
|
|
|
|
fn program_with_match_statement() {
|
|
|
|
|
// Note: the parser doesn't support `;` as a statement separator,
|
|
|
|
|
// so each arm body uses newlines between statements.
|
|
|
|
|
let source = r#"
|
|
|
|
|
game "Match" { mapper: NROM }
|
|
|
|
|
enum Mode { Idle, Run, Jump }
|
|
|
|
|
var mode: u8 = Idle
|
|
|
|
|
var x: u8 = 0
|
|
|
|
|
on frame {
|
|
|
|
|
match mode {
|
|
|
|
|
Idle => { if button.a { mode = Run } }
|
|
|
|
|
Run => {
|
|
|
|
|
x += 1
|
|
|
|
|
if button.b { mode = Jump }
|
|
|
|
|
}
|
|
|
|
|
Jump => {
|
|
|
|
|
x += 2
|
|
|
|
|
if button.a { mode = Idle }
|
|
|
|
|
}
|
|
|
|
|
_ => {}
|
|
|
|
|
}
|
|
|
|
|
wait_frame
|
|
|
|
|
}
|
|
|
|
|
start Main
|
|
|
|
|
"#;
|
|
|
|
|
let rom_data = compile(source);
|
|
|
|
|
rom::validate_ines(&rom_data).expect("should be valid iNES");
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-12 17:15:57 +00:00
|
|
|
#[test]
|
|
|
|
|
fn program_with_struct_literals() {
|
|
|
|
|
let source = r#"
|
|
|
|
|
game "Lit" { mapper: NROM }
|
|
|
|
|
struct Vec2 { x: u8, y: u8 }
|
|
|
|
|
var pos: Vec2 = Vec2 { x: 10, y: 20 }
|
|
|
|
|
on frame {
|
|
|
|
|
pos = Vec2 { x: 100, y: 50 }
|
|
|
|
|
if button.right {
|
|
|
|
|
pos = Vec2 { x: pos.x + 1, y: pos.y }
|
|
|
|
|
}
|
|
|
|
|
draw Smiley at: (pos.x, pos.y)
|
|
|
|
|
wait_frame
|
|
|
|
|
}
|
|
|
|
|
start Main
|
|
|
|
|
"#;
|
|
|
|
|
let rom_data = compile(source);
|
2026-04-12 11:39:12 +00:00
|
|
|
rom::validate_ines(&rom_data).expect("should be valid iNES");
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-12 16:18:05 +00:00
|
|
|
#[test]
|
|
|
|
|
fn program_with_structs() {
|
|
|
|
|
let source = r#"
|
|
|
|
|
game "Structs" { mapper: NROM }
|
|
|
|
|
struct Vec2 { x: u8, y: u8 }
|
|
|
|
|
struct Player { health: u8, lives: u8 }
|
|
|
|
|
|
|
|
|
|
var pos: Vec2
|
|
|
|
|
var hero: Player
|
|
|
|
|
|
|
|
|
|
on frame {
|
|
|
|
|
pos.x = 100
|
|
|
|
|
pos.y = 50
|
|
|
|
|
hero.health = 3
|
|
|
|
|
hero.lives = 5
|
|
|
|
|
if button.right { pos.x += 1 }
|
|
|
|
|
}
|
|
|
|
|
start Main
|
|
|
|
|
"#;
|
|
|
|
|
let rom_data = compile(source);
|
|
|
|
|
rom::validate_ines(&rom_data).expect("should be valid iNES");
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-12 11:39:12 +00:00
|
|
|
#[test]
|
|
|
|
|
fn program_with_enums() {
|
|
|
|
|
let source = r#"
|
|
|
|
|
game "Enums" { mapper: NROM }
|
|
|
|
|
enum Direction { Up, Down, Left, Right }
|
|
|
|
|
enum Mode { Idle, Running, Jumping }
|
|
|
|
|
|
|
|
|
|
var dir: u8 = 0
|
|
|
|
|
var mode: u8 = 0
|
|
|
|
|
|
|
|
|
|
on frame {
|
|
|
|
|
if button.right { dir = Right }
|
|
|
|
|
if button.left { dir = Left }
|
|
|
|
|
if dir == Right { mode = Running }
|
|
|
|
|
}
|
2026-04-12 17:30:21 +00:00
|
|
|
start Main
|
|
|
|
|
"#;
|
|
|
|
|
let rom_data = compile(source);
|
|
|
|
|
rom::validate_ines(&rom_data).expect("should be valid iNES");
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-12 17:40:34 +00:00
|
|
|
#[test]
|
|
|
|
|
fn program_with_poke_peek_intrinsics() {
|
|
|
|
|
let source = r#"
|
|
|
|
|
game "Hardware" { mapper: NROM }
|
|
|
|
|
var status: u8 = 0
|
|
|
|
|
on frame {
|
|
|
|
|
// Write to PPU address / data registers directly.
|
|
|
|
|
poke(0x2006, 0x3F)
|
|
|
|
|
poke(0x2006, 0x00)
|
|
|
|
|
poke(0x2007, 0x0F)
|
|
|
|
|
// Read PPU status.
|
|
|
|
|
status = peek(0x2002)
|
|
|
|
|
wait_frame
|
|
|
|
|
}
|
|
|
|
|
start Main
|
|
|
|
|
"#;
|
|
|
|
|
let rom_data = compile(source);
|
|
|
|
|
rom::validate_ines(&rom_data).expect("should be valid iNES");
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-12 17:34:17 +00:00
|
|
|
#[test]
|
|
|
|
|
fn program_with_raw_asm_block() {
|
|
|
|
|
// `raw asm` bypasses `{var}` substitution so the body is passed
|
|
|
|
|
// to the inline parser unchanged.
|
|
|
|
|
let source = r#"
|
|
|
|
|
game "RawAsm" { mapper: NROM }
|
|
|
|
|
var x: u8 = 0
|
|
|
|
|
on frame {
|
|
|
|
|
raw asm {
|
|
|
|
|
LDA #$42
|
|
|
|
|
STA $00
|
|
|
|
|
}
|
|
|
|
|
wait_frame
|
|
|
|
|
}
|
|
|
|
|
start Main
|
|
|
|
|
"#;
|
|
|
|
|
let rom_data = compile(source);
|
|
|
|
|
rom::validate_ines(&rom_data).expect("should be valid iNES");
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-12 17:30:21 +00:00
|
|
|
#[test]
|
|
|
|
|
fn program_with_inline_asm_variable_substitution() {
|
|
|
|
|
let source = r#"
|
|
|
|
|
game "AsmVar" { mapper: NROM }
|
|
|
|
|
var counter: u8 = 0
|
|
|
|
|
on frame {
|
|
|
|
|
asm {
|
|
|
|
|
LDA {counter}
|
|
|
|
|
CLC
|
|
|
|
|
ADC #$01
|
|
|
|
|
STA {counter}
|
|
|
|
|
}
|
|
|
|
|
wait_frame
|
|
|
|
|
}
|
2026-04-11 23:34:35 +00:00
|
|
|
start Main
|
|
|
|
|
"#;
|
|
|
|
|
let rom_data = compile(source);
|
|
|
|
|
rom::validate_ines(&rom_data).expect("should be valid iNES");
|
|
|
|
|
}
|
|
|
|
|
|
Inline assembly: asm { ... } blocks
- Lexer: after \`asm\` keyword, next \`{\` triggers raw-text capture of
the body until the matching \`}\`, emitted as a new \`AsmBody\` token
- Parser: \`asm { ... }\` produces \`Statement::InlineAsm(body, span)\`
- Analyzer: treats inline asm as opaque (no checks)
- IR: new \`IrOp::InlineAsm(String)\` variant that passes the body
through the optimizer unchanged
- \`src/asm/inline_parser.rs\`: minimal 6502 mnemonic parser supporting
every addressing mode we emit elsewhere (immediate, ZP/ABS with X/Y,
indirect, indirect-X/Y, labels, branches, implied, accumulator)
- Both IR and AST codegen splice parsed instructions inline
- Integration test covers a mix of implied + immediate + ZP + A modes
https://claude.ai/code/session_01W6eQFStA66EuMKHUFo2rx3
2026-04-12 11:16:18 +00:00
|
|
|
#[test]
|
|
|
|
|
fn program_with_inline_asm() {
|
|
|
|
|
let source = r#"
|
|
|
|
|
game "Asm" { mapper: NROM }
|
|
|
|
|
var x: u8 = 0
|
|
|
|
|
on frame {
|
|
|
|
|
asm {
|
|
|
|
|
LDA #$42
|
|
|
|
|
STA $10
|
|
|
|
|
INC $10
|
|
|
|
|
LSR A
|
|
|
|
|
CLC
|
|
|
|
|
ADC #$01
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
start Main
|
|
|
|
|
"#;
|
|
|
|
|
let rom_data = compile(source);
|
|
|
|
|
rom::validate_ines(&rom_data).expect("should be valid iNES");
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-11 23:34:35 +00:00
|
|
|
#[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"
|
|
|
|
|
);
|
|
|
|
|
}
|
2026-04-11 23:34:35 +00:00
|
|
|
|
|
|
|
|
#[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"
|
|
|
|
|
);
|
|
|
|
|
}
|
2026-04-12 00:09:47 +00:00
|
|
|
|
M4+M5: Optimizer passes, type casting, bank switching, math runtime
Milestone 4 — Optimization & Polish:
- Strength reduction: multiply by power-of-2 → shift left
- Zero-page promotion analysis: rank variables by access frequency
- `as` type casting expression in parser/AST/analyzer
- `scroll(x, y)` statement
- `--asm-dump` flag for viewing generated assembly
- Extended optimizer tests (strength reduction, frequency analysis)
Milestone 5 — Bank Switching & Release:
- Mapper support: MMC1 (1), UxROM (2), MMC3 (4) in parser and ROM builder
- Bank declarations: `bank Name: prg` / `bank Name: chr`
- Linker::with_mapper for mapper-aware ROM generation
- Software multiply (8x8→16, shift-and-add algorithm)
- Software divide (8÷8→8, restoring division algorithm)
- ROM tests for mapper encoding round-trip
- Integration test for MMC1 compilation
210 tests total (18 new), all pre-commit checks pass.
https://claude.ai/code/session_01W6eQFStA66EuMKHUFo2rx3
2026-04-12 00:22:11 +00:00
|
|
|
// ── 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");
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-12 00:09:47 +00:00
|
|
|
// ── 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");
|
|
|
|
|
}
|
M4+M5: Optimizer passes, type casting, bank switching, math runtime
Milestone 4 — Optimization & Polish:
- Strength reduction: multiply by power-of-2 → shift left
- Zero-page promotion analysis: rank variables by access frequency
- `as` type casting expression in parser/AST/analyzer
- `scroll(x, y)` statement
- `--asm-dump` flag for viewing generated assembly
- Extended optimizer tests (strength reduction, frequency analysis)
Milestone 5 — Bank Switching & Release:
- Mapper support: MMC1 (1), UxROM (2), MMC3 (4) in parser and ROM builder
- Bank declarations: `bank Name: prg` / `bank Name: chr`
- Linker::with_mapper for mapper-aware ROM generation
- Software multiply (8x8→16, shift-and-add algorithm)
- Software divide (8÷8→8, restoring division algorithm)
- ROM tests for mapper encoding round-trip
- Integration test for MMC1 compilation
210 tests total (18 new), all pre-commit checks pass.
https://claude.ai/code/session_01W6eQFStA66EuMKHUFo2rx3
2026-04-12 00:22:11 +00:00
|
|
|
|
|
|
|
|
// ── 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);
|
|
|
|
|
|
2026-04-12 10:01:44 +00:00
|
|
|
let sprites = assets::resolve_sprites(&program, Path::new("."))
|
|
|
|
|
.expect("sprite resolution should succeed");
|
|
|
|
|
|
Remove the legacy AST codegen — IR path is canonical now
The `--use-ast` path through `src/codegen/mod.rs` was a strictly
inferior subset of the IR codegen. Building every example with
`--use-ast` through the jsnes harness:
- `arrays_and_functions` — fully black (array init + function
return values + OAM-in-loop all broken)
- `structs_enums_for` — fully black (struct literal is a no-op,
all fields stay at 0)
- `inline_asm_demo` — fully black
- `bitwise_ops`, `loop_break_continue` — below sprite floors
(static `next_oam_slot` bug B)
- `match_demo` — panics at compile time with
`branch offset 153 out of range` (AST's if/else-chain
desugaring of `match` emits short branches that can't reach
the far arms in a multi-arm match)
Six of fourteen examples are non-functional under `--use-ast`.
The other eight happen to fall inside the subset AST handles
(no arrays, no structs, no function return values, no
multi-sprite loops, no long match chains).
`docs/future-work.md` already listed "Once working, delete the
AST-based codegen entirely" as the intended direction. It's
working, so this commit does the deletion.
What's removed:
- The `CodeGen` struct, its impl block, and every helper in
`src/codegen/mod.rs` (the AST codegen body) — ~1150 lines.
The file is now a module header that re-exports `IrCodeGen`.
- `src/codegen/tests.rs` — 15 AST-specific instruction-pattern
tests. Every feature they covered has an equivalent test in
`src/codegen/ir_codegen.rs::{tests,more_tests}` already.
- The `--use-ast` CLI flag and its branch in `src/main.rs`.
- `compile_with_ir_codegen` in `tests/integration_test.rs` —
`compile()` now does what it did, so they merged. All 40
integration tests go through the IR path.
- Outdated sections in `docs/future-work.md` that described the
IR codegen as "not yet implemented" and listed AST codegen
gaps as priority work.
What's kept:
- `src/codegen/ir_codegen.rs` — the real codegen.
- `src/codegen/peephole.rs` — post-codegen cleanup pass, now
run unconditionally from `main.rs`.
Test plan:
- `cargo test --release` — 313 unit + 37 integration tests pass
(was 328 + 37; the 15 dropped are the deleted AST-specific
tests).
- `cargo fmt --check` clean.
- `cargo clippy --release --all-targets -- -D warnings` clean.
- `node tests/emulator/run_examples.mjs` — 14/14 ROMs render
above their per-example nonBlack floors.
- The one tightening: `sprite_resolution_uses_tile_index` was
asserting on the old static-slot encoding
(`A9 01 8D 01 02`). Updated to the cursor-based form
(`A9 01 99 01 02`, i.e. STA AbsoluteY).
Net diff: 1581 deletions, 62 insertions.
https://claude.ai/code/session_014Z5y3Q9krLcAxYpZQJhZ5V
2026-04-12 20:37:59 +00:00
|
|
|
let codegen = IrCodeGen::new(&analysis.var_allocations, &ir_program).with_sprites(&sprites);
|
|
|
|
|
let mut instructions = codegen.generate(&ir_program);
|
|
|
|
|
nescript::codegen::peephole::optimize(&mut instructions);
|
M4+M5: Optimizer passes, type casting, bank switching, math runtime
Milestone 4 — Optimization & Polish:
- Strength reduction: multiply by power-of-2 → shift left
- Zero-page promotion analysis: rank variables by access frequency
- `as` type casting expression in parser/AST/analyzer
- `scroll(x, y)` statement
- `--asm-dump` flag for viewing generated assembly
- Extended optimizer tests (strength reduction, frequency analysis)
Milestone 5 — Bank Switching & Release:
- Mapper support: MMC1 (1), UxROM (2), MMC3 (4) in parser and ROM builder
- Bank declarations: `bank Name: prg` / `bank Name: chr`
- Linker::with_mapper for mapper-aware ROM generation
- Software multiply (8x8→16, shift-and-add algorithm)
- Software divide (8÷8→8, restoring division algorithm)
- ROM tests for mapper encoding round-trip
- Integration test for MMC1 compilation
210 tests total (18 new), all pre-commit checks pass.
https://claude.ai/code/session_01W6eQFStA66EuMKHUFo2rx3
2026-04-12 00:22:11 +00:00
|
|
|
|
|
|
|
|
let linker = Linker::with_mapper(program.game.mirroring, program.game.mapper);
|
2026-04-12 10:01:44 +00:00
|
|
|
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",
|
|
|
|
|
);
|
|
|
|
|
|
Remove the legacy AST codegen — IR path is canonical now
The `--use-ast` path through `src/codegen/mod.rs` was a strictly
inferior subset of the IR codegen. Building every example with
`--use-ast` through the jsnes harness:
- `arrays_and_functions` — fully black (array init + function
return values + OAM-in-loop all broken)
- `structs_enums_for` — fully black (struct literal is a no-op,
all fields stay at 0)
- `inline_asm_demo` — fully black
- `bitwise_ops`, `loop_break_continue` — below sprite floors
(static `next_oam_slot` bug B)
- `match_demo` — panics at compile time with
`branch offset 153 out of range` (AST's if/else-chain
desugaring of `match` emits short branches that can't reach
the far arms in a multi-arm match)
Six of fourteen examples are non-functional under `--use-ast`.
The other eight happen to fall inside the subset AST handles
(no arrays, no structs, no function return values, no
multi-sprite loops, no long match chains).
`docs/future-work.md` already listed "Once working, delete the
AST-based codegen entirely" as the intended direction. It's
working, so this commit does the deletion.
What's removed:
- The `CodeGen` struct, its impl block, and every helper in
`src/codegen/mod.rs` (the AST codegen body) — ~1150 lines.
The file is now a module header that re-exports `IrCodeGen`.
- `src/codegen/tests.rs` — 15 AST-specific instruction-pattern
tests. Every feature they covered has an equivalent test in
`src/codegen/ir_codegen.rs::{tests,more_tests}` already.
- The `--use-ast` CLI flag and its branch in `src/main.rs`.
- `compile_with_ir_codegen` in `tests/integration_test.rs` —
`compile()` now does what it did, so they merged. All 40
integration tests go through the IR path.
- Outdated sections in `docs/future-work.md` that described the
IR codegen as "not yet implemented" and listed AST codegen
gaps as priority work.
What's kept:
- `src/codegen/ir_codegen.rs` — the real codegen.
- `src/codegen/peephole.rs` — post-codegen cleanup pass, now
run unconditionally from `main.rs`.
Test plan:
- `cargo test --release` — 313 unit + 37 integration tests pass
(was 328 + 37; the 15 dropped are the deleted AST-specific
tests).
- `cargo fmt --check` clean.
- `cargo clippy --release --all-targets -- -D warnings` clean.
- `node tests/emulator/run_examples.mjs` — 14/14 ROMs render
above their per-example nonBlack floors.
- The one tightening: `sprite_resolution_uses_tile_index` was
asserting on the old static-slot encoding
(`A9 01 8D 01 02`). Updated to the cursor-based form
(`A9 01 99 01 02`, i.e. STA AbsoluteY).
Net diff: 1581 deletions, 62 insertions.
https://claude.ai/code/session_014Z5y3Q9krLcAxYpZQJhZ5V
2026-04-12 20:37:59 +00:00
|
|
|
// In PRG ROM, look for `LDA #$01 ; STA $0201,Y` which writes
|
|
|
|
|
// the Player's tile index (1) into the tile-index byte of the
|
|
|
|
|
// current OAM slot (the slot is computed at runtime via the
|
|
|
|
|
// OAM cursor in Y). The STA AbsoluteY opcode is $99.
|
2026-04-12 10:01:44 +00:00
|
|
|
let prg = &rom_data[16..16 + 16384];
|
Remove the legacy AST codegen — IR path is canonical now
The `--use-ast` path through `src/codegen/mod.rs` was a strictly
inferior subset of the IR codegen. Building every example with
`--use-ast` through the jsnes harness:
- `arrays_and_functions` — fully black (array init + function
return values + OAM-in-loop all broken)
- `structs_enums_for` — fully black (struct literal is a no-op,
all fields stay at 0)
- `inline_asm_demo` — fully black
- `bitwise_ops`, `loop_break_continue` — below sprite floors
(static `next_oam_slot` bug B)
- `match_demo` — panics at compile time with
`branch offset 153 out of range` (AST's if/else-chain
desugaring of `match` emits short branches that can't reach
the far arms in a multi-arm match)
Six of fourteen examples are non-functional under `--use-ast`.
The other eight happen to fall inside the subset AST handles
(no arrays, no structs, no function return values, no
multi-sprite loops, no long match chains).
`docs/future-work.md` already listed "Once working, delete the
AST-based codegen entirely" as the intended direction. It's
working, so this commit does the deletion.
What's removed:
- The `CodeGen` struct, its impl block, and every helper in
`src/codegen/mod.rs` (the AST codegen body) — ~1150 lines.
The file is now a module header that re-exports `IrCodeGen`.
- `src/codegen/tests.rs` — 15 AST-specific instruction-pattern
tests. Every feature they covered has an equivalent test in
`src/codegen/ir_codegen.rs::{tests,more_tests}` already.
- The `--use-ast` CLI flag and its branch in `src/main.rs`.
- `compile_with_ir_codegen` in `tests/integration_test.rs` —
`compile()` now does what it did, so they merged. All 40
integration tests go through the IR path.
- Outdated sections in `docs/future-work.md` that described the
IR codegen as "not yet implemented" and listed AST codegen
gaps as priority work.
What's kept:
- `src/codegen/ir_codegen.rs` — the real codegen.
- `src/codegen/peephole.rs` — post-codegen cleanup pass, now
run unconditionally from `main.rs`.
Test plan:
- `cargo test --release` — 313 unit + 37 integration tests pass
(was 328 + 37; the 15 dropped are the deleted AST-specific
tests).
- `cargo fmt --check` clean.
- `cargo clippy --release --all-targets -- -D warnings` clean.
- `node tests/emulator/run_examples.mjs` — 14/14 ROMs render
above their per-example nonBlack floors.
- The one tightening: `sprite_resolution_uses_tile_index` was
asserting on the old static-slot encoding
(`A9 01 8D 01 02`). Updated to the cursor-based form
(`A9 01 99 01 02`, i.e. STA AbsoluteY).
Net diff: 1581 deletions, 62 insertions.
https://claude.ai/code/session_014Z5y3Q9krLcAxYpZQJhZ5V
2026-04-12 20:37:59 +00:00
|
|
|
let pattern = [0xA9u8, 0x01, 0x99, 0x01, 0x02];
|
2026-04-12 10:01:44 +00:00
|
|
|
assert!(
|
|
|
|
|
prg.windows(pattern.len()).any(|w| w == pattern),
|
Remove the legacy AST codegen — IR path is canonical now
The `--use-ast` path through `src/codegen/mod.rs` was a strictly
inferior subset of the IR codegen. Building every example with
`--use-ast` through the jsnes harness:
- `arrays_and_functions` — fully black (array init + function
return values + OAM-in-loop all broken)
- `structs_enums_for` — fully black (struct literal is a no-op,
all fields stay at 0)
- `inline_asm_demo` — fully black
- `bitwise_ops`, `loop_break_continue` — below sprite floors
(static `next_oam_slot` bug B)
- `match_demo` — panics at compile time with
`branch offset 153 out of range` (AST's if/else-chain
desugaring of `match` emits short branches that can't reach
the far arms in a multi-arm match)
Six of fourteen examples are non-functional under `--use-ast`.
The other eight happen to fall inside the subset AST handles
(no arrays, no structs, no function return values, no
multi-sprite loops, no long match chains).
`docs/future-work.md` already listed "Once working, delete the
AST-based codegen entirely" as the intended direction. It's
working, so this commit does the deletion.
What's removed:
- The `CodeGen` struct, its impl block, and every helper in
`src/codegen/mod.rs` (the AST codegen body) — ~1150 lines.
The file is now a module header that re-exports `IrCodeGen`.
- `src/codegen/tests.rs` — 15 AST-specific instruction-pattern
tests. Every feature they covered has an equivalent test in
`src/codegen/ir_codegen.rs::{tests,more_tests}` already.
- The `--use-ast` CLI flag and its branch in `src/main.rs`.
- `compile_with_ir_codegen` in `tests/integration_test.rs` —
`compile()` now does what it did, so they merged. All 40
integration tests go through the IR path.
- Outdated sections in `docs/future-work.md` that described the
IR codegen as "not yet implemented" and listed AST codegen
gaps as priority work.
What's kept:
- `src/codegen/ir_codegen.rs` — the real codegen.
- `src/codegen/peephole.rs` — post-codegen cleanup pass, now
run unconditionally from `main.rs`.
Test plan:
- `cargo test --release` — 313 unit + 37 integration tests pass
(was 328 + 37; the 15 dropped are the deleted AST-specific
tests).
- `cargo fmt --check` clean.
- `cargo clippy --release --all-targets -- -D warnings` clean.
- `node tests/emulator/run_examples.mjs` — 14/14 ROMs render
above their per-example nonBlack floors.
- The one tightening: `sprite_resolution_uses_tile_index` was
asserting on the old static-slot encoding
(`A9 01 8D 01 02`). Updated to the cursor-based form
(`A9 01 99 01 02`, i.e. STA AbsoluteY).
Net diff: 1581 deletions, 62 insertions.
https://claude.ai/code/session_014Z5y3Q9krLcAxYpZQJhZ5V
2026-04-12 20:37:59 +00:00
|
|
|
"PRG ROM should contain LDA #$01 ; STA $0201,Y for draw Player",
|
2026-04-12 10:01:44 +00:00
|
|
|
);
|
M4+M5: Optimizer passes, type casting, bank switching, math runtime
Milestone 4 — Optimization & Polish:
- Strength reduction: multiply by power-of-2 → shift left
- Zero-page promotion analysis: rank variables by access frequency
- `as` type casting expression in parser/AST/analyzer
- `scroll(x, y)` statement
- `--asm-dump` flag for viewing generated assembly
- Extended optimizer tests (strength reduction, frequency analysis)
Milestone 5 — Bank Switching & Release:
- Mapper support: MMC1 (1), UxROM (2), MMC3 (4) in parser and ROM builder
- Bank declarations: `bank Name: prg` / `bank Name: chr`
- Linker::with_mapper for mapper-aware ROM generation
- Software multiply (8x8→16, shift-and-add algorithm)
- Software divide (8÷8→8, restoring division algorithm)
- ROM tests for mapper encoding round-trip
- Integration test for MMC1 compilation
210 tests total (18 new), all pre-commit checks pass.
https://claude.ai/code/session_01W6eQFStA66EuMKHUFo2rx3
2026-04-12 00:22:11 +00:00
|
|
|
}
|
|
|
|
|
|
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");
|
|
|
|
|
}
|
|
|
|
|
|
M4+M5: Optimizer passes, type casting, bank switching, math runtime
Milestone 4 — Optimization & Polish:
- Strength reduction: multiply by power-of-2 → shift left
- Zero-page promotion analysis: rank variables by access frequency
- `as` type casting expression in parser/AST/analyzer
- `scroll(x, y)` statement
- `--asm-dump` flag for viewing generated assembly
- Extended optimizer tests (strength reduction, frequency analysis)
Milestone 5 — Bank Switching & Release:
- Mapper support: MMC1 (1), UxROM (2), MMC3 (4) in parser and ROM builder
- Bank declarations: `bank Name: prg` / `bank Name: chr`
- Linker::with_mapper for mapper-aware ROM generation
- Software multiply (8x8→16, shift-and-add algorithm)
- Software divide (8÷8→8, restoring division algorithm)
- ROM tests for mapper encoding round-trip
- Integration test for MMC1 compilation
210 tests total (18 new), all pre-commit checks pass.
https://claude.ai/code/session_01W6eQFStA66EuMKHUFo2rx3
2026-04-12 00:22:11 +00:00
|
|
|
#[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)");
|
|
|
|
|
}
|
2026-04-12 10:23:43 +00:00
|
|
|
|
|
|
|
|
// ── IR Codegen Tests ──
|
Remove the legacy AST codegen — IR path is canonical now
The `--use-ast` path through `src/codegen/mod.rs` was a strictly
inferior subset of the IR codegen. Building every example with
`--use-ast` through the jsnes harness:
- `arrays_and_functions` — fully black (array init + function
return values + OAM-in-loop all broken)
- `structs_enums_for` — fully black (struct literal is a no-op,
all fields stay at 0)
- `inline_asm_demo` — fully black
- `bitwise_ops`, `loop_break_continue` — below sprite floors
(static `next_oam_slot` bug B)
- `match_demo` — panics at compile time with
`branch offset 153 out of range` (AST's if/else-chain
desugaring of `match` emits short branches that can't reach
the far arms in a multi-arm match)
Six of fourteen examples are non-functional under `--use-ast`.
The other eight happen to fall inside the subset AST handles
(no arrays, no structs, no function return values, no
multi-sprite loops, no long match chains).
`docs/future-work.md` already listed "Once working, delete the
AST-based codegen entirely" as the intended direction. It's
working, so this commit does the deletion.
What's removed:
- The `CodeGen` struct, its impl block, and every helper in
`src/codegen/mod.rs` (the AST codegen body) — ~1150 lines.
The file is now a module header that re-exports `IrCodeGen`.
- `src/codegen/tests.rs` — 15 AST-specific instruction-pattern
tests. Every feature they covered has an equivalent test in
`src/codegen/ir_codegen.rs::{tests,more_tests}` already.
- The `--use-ast` CLI flag and its branch in `src/main.rs`.
- `compile_with_ir_codegen` in `tests/integration_test.rs` —
`compile()` now does what it did, so they merged. All 40
integration tests go through the IR path.
- Outdated sections in `docs/future-work.md` that described the
IR codegen as "not yet implemented" and listed AST codegen
gaps as priority work.
What's kept:
- `src/codegen/ir_codegen.rs` — the real codegen.
- `src/codegen/peephole.rs` — post-codegen cleanup pass, now
run unconditionally from `main.rs`.
Test plan:
- `cargo test --release` — 313 unit + 37 integration tests pass
(was 328 + 37; the 15 dropped are the deleted AST-specific
tests).
- `cargo fmt --check` clean.
- `cargo clippy --release --all-targets -- -D warnings` clean.
- `node tests/emulator/run_examples.mjs` — 14/14 ROMs render
above their per-example nonBlack floors.
- The one tightening: `sprite_resolution_uses_tile_index` was
asserting on the old static-slot encoding
(`A9 01 8D 01 02`). Updated to the cursor-based form
(`A9 01 99 01 02`, i.e. STA AbsoluteY).
Net diff: 1581 deletions, 62 insertions.
https://claude.ai/code/session_014Z5y3Q9krLcAxYpZQJhZ5V
2026-04-12 20:37:59 +00:00
|
|
|
//
|
|
|
|
|
// These tests exercise specific end-to-end IR codegen behavior.
|
|
|
|
|
// They all use the top-level `compile()` helper now that it runs
|
|
|
|
|
// the full IR pipeline — there's no longer a separate legacy path
|
|
|
|
|
// to compare against.
|
2026-04-12 10:23:43 +00:00
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn ir_codegen_minimal_rom() {
|
|
|
|
|
let source = r#"
|
|
|
|
|
game "IR Test" { mapper: NROM }
|
|
|
|
|
var x: u8 = 42
|
|
|
|
|
on frame { wait_frame }
|
|
|
|
|
start Main
|
|
|
|
|
"#;
|
Remove the legacy AST codegen — IR path is canonical now
The `--use-ast` path through `src/codegen/mod.rs` was a strictly
inferior subset of the IR codegen. Building every example with
`--use-ast` through the jsnes harness:
- `arrays_and_functions` — fully black (array init + function
return values + OAM-in-loop all broken)
- `structs_enums_for` — fully black (struct literal is a no-op,
all fields stay at 0)
- `inline_asm_demo` — fully black
- `bitwise_ops`, `loop_break_continue` — below sprite floors
(static `next_oam_slot` bug B)
- `match_demo` — panics at compile time with
`branch offset 153 out of range` (AST's if/else-chain
desugaring of `match` emits short branches that can't reach
the far arms in a multi-arm match)
Six of fourteen examples are non-functional under `--use-ast`.
The other eight happen to fall inside the subset AST handles
(no arrays, no structs, no function return values, no
multi-sprite loops, no long match chains).
`docs/future-work.md` already listed "Once working, delete the
AST-based codegen entirely" as the intended direction. It's
working, so this commit does the deletion.
What's removed:
- The `CodeGen` struct, its impl block, and every helper in
`src/codegen/mod.rs` (the AST codegen body) — ~1150 lines.
The file is now a module header that re-exports `IrCodeGen`.
- `src/codegen/tests.rs` — 15 AST-specific instruction-pattern
tests. Every feature they covered has an equivalent test in
`src/codegen/ir_codegen.rs::{tests,more_tests}` already.
- The `--use-ast` CLI flag and its branch in `src/main.rs`.
- `compile_with_ir_codegen` in `tests/integration_test.rs` —
`compile()` now does what it did, so they merged. All 40
integration tests go through the IR path.
- Outdated sections in `docs/future-work.md` that described the
IR codegen as "not yet implemented" and listed AST codegen
gaps as priority work.
What's kept:
- `src/codegen/ir_codegen.rs` — the real codegen.
- `src/codegen/peephole.rs` — post-codegen cleanup pass, now
run unconditionally from `main.rs`.
Test plan:
- `cargo test --release` — 313 unit + 37 integration tests pass
(was 328 + 37; the 15 dropped are the deleted AST-specific
tests).
- `cargo fmt --check` clean.
- `cargo clippy --release --all-targets -- -D warnings` clean.
- `node tests/emulator/run_examples.mjs` — 14/14 ROMs render
above their per-example nonBlack floors.
- The one tightening: `sprite_resolution_uses_tile_index` was
asserting on the old static-slot encoding
(`A9 01 8D 01 02`). Updated to the cursor-based form
(`A9 01 99 01 02`, i.e. STA AbsoluteY).
Net diff: 1581 deletions, 62 insertions.
https://claude.ai/code/session_014Z5y3Q9krLcAxYpZQJhZ5V
2026-04-12 20:37:59 +00:00
|
|
|
let rom_data = compile(source);
|
2026-04-12 10:23:43 +00:00
|
|
|
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
|
|
|
|
|
"#;
|
Remove the legacy AST codegen — IR path is canonical now
The `--use-ast` path through `src/codegen/mod.rs` was a strictly
inferior subset of the IR codegen. Building every example with
`--use-ast` through the jsnes harness:
- `arrays_and_functions` — fully black (array init + function
return values + OAM-in-loop all broken)
- `structs_enums_for` — fully black (struct literal is a no-op,
all fields stay at 0)
- `inline_asm_demo` — fully black
- `bitwise_ops`, `loop_break_continue` — below sprite floors
(static `next_oam_slot` bug B)
- `match_demo` — panics at compile time with
`branch offset 153 out of range` (AST's if/else-chain
desugaring of `match` emits short branches that can't reach
the far arms in a multi-arm match)
Six of fourteen examples are non-functional under `--use-ast`.
The other eight happen to fall inside the subset AST handles
(no arrays, no structs, no function return values, no
multi-sprite loops, no long match chains).
`docs/future-work.md` already listed "Once working, delete the
AST-based codegen entirely" as the intended direction. It's
working, so this commit does the deletion.
What's removed:
- The `CodeGen` struct, its impl block, and every helper in
`src/codegen/mod.rs` (the AST codegen body) — ~1150 lines.
The file is now a module header that re-exports `IrCodeGen`.
- `src/codegen/tests.rs` — 15 AST-specific instruction-pattern
tests. Every feature they covered has an equivalent test in
`src/codegen/ir_codegen.rs::{tests,more_tests}` already.
- The `--use-ast` CLI flag and its branch in `src/main.rs`.
- `compile_with_ir_codegen` in `tests/integration_test.rs` —
`compile()` now does what it did, so they merged. All 40
integration tests go through the IR path.
- Outdated sections in `docs/future-work.md` that described the
IR codegen as "not yet implemented" and listed AST codegen
gaps as priority work.
What's kept:
- `src/codegen/ir_codegen.rs` — the real codegen.
- `src/codegen/peephole.rs` — post-codegen cleanup pass, now
run unconditionally from `main.rs`.
Test plan:
- `cargo test --release` — 313 unit + 37 integration tests pass
(was 328 + 37; the 15 dropped are the deleted AST-specific
tests).
- `cargo fmt --check` clean.
- `cargo clippy --release --all-targets -- -D warnings` clean.
- `node tests/emulator/run_examples.mjs` — 14/14 ROMs render
above their per-example nonBlack floors.
- The one tightening: `sprite_resolution_uses_tile_index` was
asserting on the old static-slot encoding
(`A9 01 8D 01 02`). Updated to the cursor-based form
(`A9 01 99 01 02`, i.e. STA AbsoluteY).
Net diff: 1581 deletions, 62 insertions.
https://claude.ai/code/session_014Z5y3Q9krLcAxYpZQJhZ5V
2026-04-12 20:37:59 +00:00
|
|
|
let rom_data = compile(source);
|
2026-04-12 10:23:43 +00:00
|
|
|
rom::validate_ines(&rom_data).expect("should be valid iNES");
|
|
|
|
|
}
|
2026-04-12 10:33:58 +00:00
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn ir_codegen_multi_state_dispatch() {
|
|
|
|
|
// Exercise the IR main-loop dispatch with multiple states and a
|
|
|
|
|
// transition.
|
|
|
|
|
let source = r#"
|
|
|
|
|
game "IR States" { mapper: NROM }
|
|
|
|
|
var timer: u8 = 0
|
|
|
|
|
state Title {
|
|
|
|
|
on frame {
|
|
|
|
|
if button.start { transition Play }
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
state Play {
|
|
|
|
|
on frame {
|
|
|
|
|
timer += 1
|
|
|
|
|
if timer > 60 { transition Title }
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
start Title
|
|
|
|
|
"#;
|
Remove the legacy AST codegen — IR path is canonical now
The `--use-ast` path through `src/codegen/mod.rs` was a strictly
inferior subset of the IR codegen. Building every example with
`--use-ast` through the jsnes harness:
- `arrays_and_functions` — fully black (array init + function
return values + OAM-in-loop all broken)
- `structs_enums_for` — fully black (struct literal is a no-op,
all fields stay at 0)
- `inline_asm_demo` — fully black
- `bitwise_ops`, `loop_break_continue` — below sprite floors
(static `next_oam_slot` bug B)
- `match_demo` — panics at compile time with
`branch offset 153 out of range` (AST's if/else-chain
desugaring of `match` emits short branches that can't reach
the far arms in a multi-arm match)
Six of fourteen examples are non-functional under `--use-ast`.
The other eight happen to fall inside the subset AST handles
(no arrays, no structs, no function return values, no
multi-sprite loops, no long match chains).
`docs/future-work.md` already listed "Once working, delete the
AST-based codegen entirely" as the intended direction. It's
working, so this commit does the deletion.
What's removed:
- The `CodeGen` struct, its impl block, and every helper in
`src/codegen/mod.rs` (the AST codegen body) — ~1150 lines.
The file is now a module header that re-exports `IrCodeGen`.
- `src/codegen/tests.rs` — 15 AST-specific instruction-pattern
tests. Every feature they covered has an equivalent test in
`src/codegen/ir_codegen.rs::{tests,more_tests}` already.
- The `--use-ast` CLI flag and its branch in `src/main.rs`.
- `compile_with_ir_codegen` in `tests/integration_test.rs` —
`compile()` now does what it did, so they merged. All 40
integration tests go through the IR path.
- Outdated sections in `docs/future-work.md` that described the
IR codegen as "not yet implemented" and listed AST codegen
gaps as priority work.
What's kept:
- `src/codegen/ir_codegen.rs` — the real codegen.
- `src/codegen/peephole.rs` — post-codegen cleanup pass, now
run unconditionally from `main.rs`.
Test plan:
- `cargo test --release` — 313 unit + 37 integration tests pass
(was 328 + 37; the 15 dropped are the deleted AST-specific
tests).
- `cargo fmt --check` clean.
- `cargo clippy --release --all-targets -- -D warnings` clean.
- `node tests/emulator/run_examples.mjs` — 14/14 ROMs render
above their per-example nonBlack floors.
- The one tightening: `sprite_resolution_uses_tile_index` was
asserting on the old static-slot encoding
(`A9 01 8D 01 02`). Updated to the cursor-based form
(`A9 01 99 01 02`, i.e. STA AbsoluteY).
Net diff: 1581 deletions, 62 insertions.
https://claude.ai/code/session_014Z5y3Q9krLcAxYpZQJhZ5V
2026-04-12 20:37:59 +00:00
|
|
|
let rom_data = compile(source);
|
2026-04-12 10:33:58 +00:00
|
|
|
let info = rom::validate_ines(&rom_data).expect("should be valid iNES");
|
|
|
|
|
assert_eq!(info.mapper, 0);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn ir_codegen_multi_oam() {
|
|
|
|
|
// Draw multiple sprites and verify OAM slots are allocated sequentially.
|
|
|
|
|
let source = r#"
|
|
|
|
|
game "IR MultiOAM" { mapper: NROM }
|
|
|
|
|
var a: u8 = 10
|
|
|
|
|
var b: u8 = 20
|
|
|
|
|
var c: u8 = 30
|
|
|
|
|
on frame {
|
|
|
|
|
draw One at: (a, a)
|
|
|
|
|
draw Two at: (b, b)
|
|
|
|
|
draw Three at: (c, c)
|
|
|
|
|
}
|
|
|
|
|
start Main
|
|
|
|
|
"#;
|
Remove the legacy AST codegen — IR path is canonical now
The `--use-ast` path through `src/codegen/mod.rs` was a strictly
inferior subset of the IR codegen. Building every example with
`--use-ast` through the jsnes harness:
- `arrays_and_functions` — fully black (array init + function
return values + OAM-in-loop all broken)
- `structs_enums_for` — fully black (struct literal is a no-op,
all fields stay at 0)
- `inline_asm_demo` — fully black
- `bitwise_ops`, `loop_break_continue` — below sprite floors
(static `next_oam_slot` bug B)
- `match_demo` — panics at compile time with
`branch offset 153 out of range` (AST's if/else-chain
desugaring of `match` emits short branches that can't reach
the far arms in a multi-arm match)
Six of fourteen examples are non-functional under `--use-ast`.
The other eight happen to fall inside the subset AST handles
(no arrays, no structs, no function return values, no
multi-sprite loops, no long match chains).
`docs/future-work.md` already listed "Once working, delete the
AST-based codegen entirely" as the intended direction. It's
working, so this commit does the deletion.
What's removed:
- The `CodeGen` struct, its impl block, and every helper in
`src/codegen/mod.rs` (the AST codegen body) — ~1150 lines.
The file is now a module header that re-exports `IrCodeGen`.
- `src/codegen/tests.rs` — 15 AST-specific instruction-pattern
tests. Every feature they covered has an equivalent test in
`src/codegen/ir_codegen.rs::{tests,more_tests}` already.
- The `--use-ast` CLI flag and its branch in `src/main.rs`.
- `compile_with_ir_codegen` in `tests/integration_test.rs` —
`compile()` now does what it did, so they merged. All 40
integration tests go through the IR path.
- Outdated sections in `docs/future-work.md` that described the
IR codegen as "not yet implemented" and listed AST codegen
gaps as priority work.
What's kept:
- `src/codegen/ir_codegen.rs` — the real codegen.
- `src/codegen/peephole.rs` — post-codegen cleanup pass, now
run unconditionally from `main.rs`.
Test plan:
- `cargo test --release` — 313 unit + 37 integration tests pass
(was 328 + 37; the 15 dropped are the deleted AST-specific
tests).
- `cargo fmt --check` clean.
- `cargo clippy --release --all-targets -- -D warnings` clean.
- `node tests/emulator/run_examples.mjs` — 14/14 ROMs render
above their per-example nonBlack floors.
- The one tightening: `sprite_resolution_uses_tile_index` was
asserting on the old static-slot encoding
(`A9 01 8D 01 02`). Updated to the cursor-based form
(`A9 01 99 01 02`, i.e. STA AbsoluteY).
Net diff: 1581 deletions, 62 insertions.
https://claude.ai/code/session_014Z5y3Q9krLcAxYpZQJhZ5V
2026-04-12 20:37:59 +00:00
|
|
|
let rom_data = compile(source);
|
2026-04-12 10:33:58 +00:00
|
|
|
rom::validate_ines(&rom_data).expect("should be valid iNES");
|
|
|
|
|
}
|
Fix three compiler bugs exposed by array-using examples
Landing bug A from the previous writeup plus two adjacent bugs
that the fix exposed. All three miscompile anything that uses a
u8[N] global with a literal initializer.
1. Array-literal globals are now actually initialized.
`lower_program` only expanded `Expr::StructLiteral` into per-
field synthetic globals — `Expr::ArrayLiteral` hit
`eval_const`, returned `None`, and the array boot-cleared to
zero. `IrGlobal` now carries an `init_array: Vec<u8>`
populated by lowering, and the IR codegen startup loop emits
one `LDA #byte; STA base+i` pair per element.
2. Local variables no longer overlap array globals.
`IrCodeGen::new` advanced `local_ram_next` past
`max_global_base + 1` — for an array at `$0300-$0303` it
placed the first handler-local at `$0301`, inside the array.
The frame handler's stores through the local then corrupted
the array mid-frame. The allocator now walks the analyzer's
`VarAllocation` list and advances past `address + size` for
every RAM global, not just the base.
3. Peephole `remove_redundant_loads` honors indexed LDAs.
The pass tracked `LDA Immediate/ZeroPage/Absolute` but let
`LDA AbsoluteX/AbsoluteY/ZeroPageX/IndirectX/IndirectY` fall
through the match, leaving the A-equivalence tracker
unchanged. A later `LDA #v` that happened to match a stale
entry from BEFORE the indexed load would then be dropped as
"already in A" — a silent miscompile that turned every
`draw Sprite at: (arr[i], arr[j])` pattern into garbage
(the second array index would be computed from `arr[i]`'s
value, reading way out of bounds). Indexed LDAs now clear
the tracker.
Regression tests:
- `src/codegen/peephole.rs`: a synthetic
`LDA #0; TAX; LDA AbsX(arr1); STA temp; LDA #0; TAX;
LDA AbsX(arr2); ...` sequence asserts both `LDA #0`s survive.
- `src/ir/tests.rs`: verifies `var xs: u8[4] = [1,2,3,4]`
populates `IrGlobal::init_array` with `[1,2,3,4]`.
- `tests/integration_test.rs`: two IR-codegen tests — one checks
the startup instructions contain `LDA #v; STA base+i` for
every element, the other compiles a handler-local var
alongside an array global and asserts no post-init stores
land inside the array.
Smoke test impact (14/14 still passing, now more visible):
- arrays_and_functions: 56 -> 104 nonBlack, now animated
- loop_break_continue: 52 -> 208 (player + 3 hazards visible)
- structs_enums_for: 52 -> 104 (player + enemy visible)
Existing examples unchanged; no remaining work for bug B
(static OAM slot allocation in loops) — that's the next PR.
https://claude.ai/code/session_014Z5y3Q9krLcAxYpZQJhZ5V
2026-04-12 19:32:22 +00:00
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn ir_codegen_array_literal_globals_emit_per_byte_init() {
|
|
|
|
|
// Regression test: `var xs: u8[4] = [10, 20, 30, 40]` used to
|
|
|
|
|
// compile to a zero-initialized array because `eval_const`
|
|
|
|
|
// returned `None` for `Expr::ArrayLiteral` and no startup
|
|
|
|
|
// stores were emitted. The fix captures the literal values
|
|
|
|
|
// in `IrGlobal::init_array` and has the IR codegen emit one
|
|
|
|
|
// `LDA #imm; STA base+i` per byte during startup.
|
|
|
|
|
use nescript::asm::{AddressingMode, Opcode};
|
|
|
|
|
use nescript::codegen::IrCodeGen;
|
|
|
|
|
|
|
|
|
|
let source = r#"
|
|
|
|
|
game "ArrLit" { mapper: NROM }
|
|
|
|
|
var xs: u8[4] = [10, 20, 30, 40]
|
|
|
|
|
on frame { wait_frame }
|
|
|
|
|
start Main
|
|
|
|
|
"#;
|
|
|
|
|
let (prog, diags) = nescript::parser::parse(source);
|
|
|
|
|
assert!(diags.is_empty(), "parse errors: {diags:?}");
|
|
|
|
|
let prog = prog.unwrap();
|
|
|
|
|
let analysis = analyzer::analyze(&prog);
|
|
|
|
|
let mut ir_program = ir::lower(&prog, &analysis);
|
|
|
|
|
optimizer::optimize(&mut ir_program);
|
|
|
|
|
|
|
|
|
|
let xs_addr = analysis
|
|
|
|
|
.var_allocations
|
|
|
|
|
.iter()
|
|
|
|
|
.find(|a| a.name == "xs")
|
|
|
|
|
.expect("xs should be allocated")
|
|
|
|
|
.address;
|
|
|
|
|
|
|
|
|
|
let codegen = IrCodeGen::new(&analysis.var_allocations, &ir_program);
|
|
|
|
|
let instructions = codegen.generate(&ir_program);
|
|
|
|
|
|
|
|
|
|
// For each element, look for `LDA #val` followed shortly by
|
|
|
|
|
// `STA absolute(xs_addr + i)`. We don't require them to be
|
|
|
|
|
// adjacent because the peephole passes can reshuffle, but a
|
|
|
|
|
// store of the correct value to the correct address must
|
|
|
|
|
// exist.
|
|
|
|
|
for (i, &expected) in [10u8, 20, 30, 40].iter().enumerate() {
|
|
|
|
|
let target = xs_addr + i as u16;
|
|
|
|
|
let has_store = instructions.windows(2).any(|w| {
|
|
|
|
|
matches!(w[0].mode, AddressingMode::Immediate(v) if v == expected)
|
|
|
|
|
&& w[0].opcode == Opcode::LDA
|
|
|
|
|
&& w[1].opcode == Opcode::STA
|
|
|
|
|
&& matches!(w[1].mode, AddressingMode::Absolute(a) if a == target)
|
|
|
|
|
});
|
|
|
|
|
assert!(
|
|
|
|
|
has_store,
|
|
|
|
|
"expected `LDA #{expected}; STA ${target:04X}` for xs[{i}] but did not find it"
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn ir_codegen_locals_do_not_overlap_array_globals() {
|
|
|
|
|
// Regression test for the local-allocator off-by-array-size
|
|
|
|
|
// bug. `IrCodeGen::new` used to start handler-local vars at
|
|
|
|
|
// `max_global_base + 1`, which for an array global at
|
|
|
|
|
// `$0300-$0303` put the first local at `$0301` — inside the
|
|
|
|
|
// array. Any store through that local then corrupted the
|
|
|
|
|
// array mid-frame. The fix advances past the global's END,
|
|
|
|
|
// not its base.
|
|
|
|
|
//
|
|
|
|
|
// We verify by asking the IR codegen what addresses it
|
|
|
|
|
// assigned. Since `var_addrs` is private, we check indirectly
|
|
|
|
|
// via emitted instructions: any `STA $030N` for N > 3 that
|
|
|
|
|
// isn't part of the startup init must be writing to a local
|
|
|
|
|
// whose address is outside the array. If the bug regressed,
|
|
|
|
|
// we'd see `STA $0302` or similar in the frame handler's
|
|
|
|
|
// computation code.
|
|
|
|
|
use nescript::asm::{AddressingMode, Opcode};
|
|
|
|
|
use nescript::codegen::IrCodeGen;
|
|
|
|
|
|
|
|
|
|
let source = r#"
|
|
|
|
|
game "LocalVsArr" { mapper: NROM }
|
|
|
|
|
var xs: u8[4] = [11, 22, 33, 44]
|
|
|
|
|
on frame {
|
|
|
|
|
var tmp: u8 = 0
|
|
|
|
|
tmp = xs[0]
|
|
|
|
|
tmp += 1
|
|
|
|
|
wait_frame
|
|
|
|
|
}
|
|
|
|
|
start Main
|
|
|
|
|
"#;
|
|
|
|
|
let (prog, diags) = nescript::parser::parse(source);
|
|
|
|
|
assert!(diags.is_empty(), "parse errors: {diags:?}");
|
|
|
|
|
let prog = prog.unwrap();
|
|
|
|
|
let analysis = analyzer::analyze(&prog);
|
|
|
|
|
let mut ir_program = ir::lower(&prog, &analysis);
|
|
|
|
|
optimizer::optimize(&mut ir_program);
|
|
|
|
|
|
|
|
|
|
let xs_alloc = analysis
|
|
|
|
|
.var_allocations
|
|
|
|
|
.iter()
|
|
|
|
|
.find(|a| a.name == "xs")
|
|
|
|
|
.expect("xs should be allocated");
|
|
|
|
|
let xs_base = xs_alloc.address;
|
|
|
|
|
let xs_end = xs_base + xs_alloc.size; // one past last element
|
|
|
|
|
|
|
|
|
|
let codegen = IrCodeGen::new(&analysis.var_allocations, &ir_program);
|
|
|
|
|
let instructions = codegen.generate(&ir_program);
|
|
|
|
|
|
|
|
|
|
// Collect the (ordered) list of `STA absolute` targets and
|
|
|
|
|
// immediate values preceding each store. The first four
|
|
|
|
|
// stores into `[xs_base, xs_end)` should be the `LDA #imm;
|
|
|
|
|
// STA addr` init pairs — those are fine. Any STA into the
|
|
|
|
|
// array AFTER the init sequence would indicate a local var
|
|
|
|
|
// was allocated inside the array.
|
|
|
|
|
let mut init_stores_seen = 0usize;
|
|
|
|
|
for w in instructions.windows(2) {
|
|
|
|
|
if w[1].opcode != Opcode::STA {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
let AddressingMode::Absolute(addr) = w[1].mode else {
|
|
|
|
|
continue;
|
|
|
|
|
};
|
|
|
|
|
if addr < xs_base || addr >= xs_end {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
if w[0].opcode == Opcode::LDA
|
|
|
|
|
&& matches!(w[0].mode, AddressingMode::Immediate(_))
|
|
|
|
|
&& init_stores_seen < 4
|
|
|
|
|
{
|
|
|
|
|
init_stores_seen += 1;
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
panic!(
|
|
|
|
|
"store into xs array (${addr:04X}) after init sequence — \
|
|
|
|
|
local probably overlapping with array global"
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
assert_eq!(
|
|
|
|
|
init_stores_seen, 4,
|
|
|
|
|
"expected 4 init stores for xs[0..4], found {init_stores_seen}"
|
|
|
|
|
);
|
|
|
|
|
}
|