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
|
|
|
#[cfg(test)]
|
|
|
|
|
mod tests;
|
|
|
|
|
|
|
|
|
|
use crate::asm;
|
|
|
|
|
use crate::asm::{AddressingMode as AM, Instruction, Opcode::*};
|
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
|
|
|
use crate::parser::ast::{Mapper, Mirroring};
|
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 crate::rom::RomBuilder;
|
|
|
|
|
use crate::runtime;
|
|
|
|
|
|
|
|
|
|
/// Link compiled code into a complete NES ROM.
|
|
|
|
|
pub struct Linker {
|
|
|
|
|
mirroring: Mirroring,
|
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
|
|
|
mapper: Mapper,
|
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
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// A smiley face CHR tile for the default sprite (M1).
|
|
|
|
|
const DEFAULT_SPRITE_CHR: [u8; 16] = [
|
|
|
|
|
// Plane 0 (low bits)
|
|
|
|
|
0b0011_1100,
|
|
|
|
|
0b0100_0010,
|
|
|
|
|
0b1010_0101,
|
|
|
|
|
0b1000_0001,
|
|
|
|
|
0b1010_0101,
|
|
|
|
|
0b1001_1001,
|
|
|
|
|
0b0100_0010,
|
|
|
|
|
0b0011_1100,
|
|
|
|
|
// Plane 1 (high bits) — all zeros means color 1 only
|
|
|
|
|
0b0011_1100,
|
|
|
|
|
0b0111_1110,
|
|
|
|
|
0b1111_1111,
|
|
|
|
|
0b1111_1111,
|
|
|
|
|
0b1111_1111,
|
|
|
|
|
0b1111_1111,
|
|
|
|
|
0b0111_1110,
|
|
|
|
|
0b0011_1100,
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
/// Default palette data for M1 (writes to PPU $3F00).
|
|
|
|
|
const DEFAULT_PALETTE: [u8; 32] = [
|
|
|
|
|
// Background palettes
|
|
|
|
|
0x0F, 0x00, 0x10, 0x20, // palette 0 (black, dark gray, light gray, white)
|
|
|
|
|
0x0F, 0x06, 0x16, 0x26, // palette 1
|
|
|
|
|
0x0F, 0x09, 0x19, 0x29, // palette 2
|
|
|
|
|
0x0F, 0x01, 0x11, 0x21, // palette 3
|
|
|
|
|
// Sprite palettes
|
|
|
|
|
0x0F, 0x00, 0x10, 0x20, // sprite palette 0 (same as bg)
|
|
|
|
|
0x0F, 0x14, 0x24, 0x34, // sprite palette 1
|
|
|
|
|
0x0F, 0x1A, 0x2A, 0x3A, // sprite palette 2
|
|
|
|
|
0x0F, 0x12, 0x22, 0x32, // sprite palette 3
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
impl Linker {
|
|
|
|
|
pub fn new(mirroring: Mirroring) -> Self {
|
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
|
|
|
Self {
|
|
|
|
|
mirroring,
|
|
|
|
|
mapper: Mapper::NROM,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn with_mapper(mirroring: Mirroring, mapper: Mapper) -> Self {
|
|
|
|
|
Self { mirroring, mapper }
|
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
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Link all code sections into a .nes ROM.
|
|
|
|
|
pub fn link(&self, user_code: &[Instruction]) -> Vec<u8> {
|
|
|
|
|
// For NROM: everything fits in one 16 KB PRG bank ($C000-$FFFF)
|
|
|
|
|
// Layout:
|
|
|
|
|
// $C000: RESET handler (init + palette load + user code)
|
|
|
|
|
// ... : NMI handler
|
|
|
|
|
// ... : IRQ handler
|
|
|
|
|
// $FFFA: Vector table (NMI, RESET, IRQ)
|
|
|
|
|
|
|
|
|
|
let mut all_instructions = Vec::new();
|
|
|
|
|
|
|
|
|
|
// RESET entry point
|
|
|
|
|
all_instructions.push(Instruction::new(NOP, AM::Label("__reset".into())));
|
|
|
|
|
|
|
|
|
|
// Hardware initialization
|
|
|
|
|
all_instructions.extend(runtime::gen_init());
|
|
|
|
|
|
|
|
|
|
// Load default palette
|
|
|
|
|
all_instructions.extend(self.gen_palette_load());
|
|
|
|
|
|
|
|
|
|
// User code (var init + main loop)
|
|
|
|
|
all_instructions.extend(user_code.iter().cloned());
|
|
|
|
|
|
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
|
|
|
// Math runtime routines (included always for simplicity)
|
|
|
|
|
all_instructions.extend(runtime::gen_multiply());
|
|
|
|
|
all_instructions.extend(runtime::gen_divide());
|
|
|
|
|
|
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
|
|
|
// NMI handler
|
|
|
|
|
all_instructions.push(Instruction::new(NOP, AM::Label("__nmi".into())));
|
|
|
|
|
all_instructions.extend(runtime::gen_nmi());
|
|
|
|
|
|
|
|
|
|
// IRQ handler
|
|
|
|
|
all_instructions.push(Instruction::new(NOP, AM::Label("__irq".into())));
|
|
|
|
|
all_instructions.extend(runtime::gen_irq());
|
|
|
|
|
|
|
|
|
|
// Assemble everything at $C000
|
|
|
|
|
let base_addr = 0xC000;
|
|
|
|
|
let result = asm::assemble(&all_instructions, base_addr);
|
|
|
|
|
|
|
|
|
|
// Build PRG ROM with vector table
|
|
|
|
|
let mut prg = result.bytes;
|
|
|
|
|
|
|
|
|
|
// Pad to fill the bank up to vector table location
|
|
|
|
|
// Vector table is at $FFFA-$FFFF (relative offset: $3FFA in a 16 KB bank)
|
|
|
|
|
let vector_offset = 0x3FFA;
|
|
|
|
|
if prg.len() > vector_offset {
|
|
|
|
|
panic!("PRG code exceeds 16 KB bank (code is {} bytes)", prg.len());
|
|
|
|
|
}
|
|
|
|
|
prg.resize(vector_offset, 0xFF);
|
|
|
|
|
|
|
|
|
|
// Write vector table
|
|
|
|
|
let nmi_addr = result.labels.get("__nmi").copied().unwrap_or(0xC000);
|
|
|
|
|
let reset_addr = result.labels.get("__reset").copied().unwrap_or(0xC000);
|
|
|
|
|
let irq_addr = result.labels.get("__irq").copied().unwrap_or(0xC000);
|
|
|
|
|
|
|
|
|
|
prg.extend_from_slice(&nmi_addr.to_le_bytes());
|
|
|
|
|
prg.extend_from_slice(&reset_addr.to_le_bytes());
|
|
|
|
|
prg.extend_from_slice(&irq_addr.to_le_bytes());
|
|
|
|
|
|
|
|
|
|
// Build ROM
|
|
|
|
|
let mut builder = RomBuilder::new(self.mirroring);
|
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
|
|
|
builder.set_mapper(crate::rom::mapper_number(self.mapper));
|
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
|
|
|
builder.set_prg(prg);
|
|
|
|
|
|
|
|
|
|
// CHR ROM with default sprite tile
|
|
|
|
|
let mut chr = vec![0u8; 8192];
|
|
|
|
|
chr[..16].copy_from_slice(&DEFAULT_SPRITE_CHR);
|
|
|
|
|
builder.set_chr(chr);
|
|
|
|
|
|
|
|
|
|
builder.build()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Generate instructions to load the default palette into the PPU.
|
|
|
|
|
fn gen_palette_load(&self) -> Vec<Instruction> {
|
|
|
|
|
let mut out = Vec::new();
|
|
|
|
|
|
|
|
|
|
// Set PPU address to $3F00 (palette start)
|
|
|
|
|
out.push(Instruction::new(LDA, AM::Absolute(0x2002))); // read PPU status to reset latch
|
|
|
|
|
out.push(Instruction::new(LDA, AM::Immediate(0x3F)));
|
|
|
|
|
out.push(Instruction::new(STA, AM::Absolute(0x2006))); // PPU addr high byte
|
|
|
|
|
out.push(Instruction::new(LDA, AM::Immediate(0x00)));
|
|
|
|
|
out.push(Instruction::new(STA, AM::Absolute(0x2006))); // PPU addr low byte
|
|
|
|
|
|
|
|
|
|
// Write all 32 palette bytes
|
|
|
|
|
for &color in &DEFAULT_PALETTE {
|
|
|
|
|
out.push(Instruction::new(LDA, AM::Immediate(color)));
|
|
|
|
|
out.push(Instruction::new(STA, AM::Absolute(0x2007))); // PPU data
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
out
|
|
|
|
|
}
|
|
|
|
|
}
|