1
0
Fork 0
mirror of https://github.com/imjasonh/nescript synced 2026-07-08 17:06:04 +00:00
nescript/src/assets/resolve.rs
Claude 33351f8b32
lang: NES 2.0 headers and u16 struct fields
Implements two items from docs/future-work.md's language-feature gaps:

NES 2.0 header support: `RomBuilder` gains a `header_format` field
and a matching `enable_nes2()` method. When enabled, byte 7 bits 2-3
are set to `10` and bytes 8-15 are populated per the NES 2.0 spec
(submapper, PRG/CHR MSBs, PRG/CHR RAM, timing). The header stays
16 bytes. Programs opt in via `game Foo { header: nes2 }`; the
default remains iNES 1.0 so every committed example ROM is byte
identical. `validate_ines` now detects and reports which format it
parsed.

u16 struct fields: the analyzer's `register_struct` accepts `u16`
fields with a two-byte size and the struct-variable allocator tracks
per-field sizes so the synthesized `pos.x`/`pos.y` globals get the
right address span. IR lowering's `LValue::Field` and
`Expr::FieldAccess` follow the same wide path as u16 globals, and
struct-literal initialization writes both bytes for u16 fields.
Array and nested-struct fields stay rejected with a clearer
message. Existing u8/i8/bool struct programs are unaffected.

https://claude.ai/code/session_01MaNVcDmK9gsspRkdxowQAM
2026-04-14 02:05:51 +00:00

329 lines
11 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

use std::path::Path;
use crate::linker::SpriteData;
use crate::parser::ast::{AssetSource, Program};
/// Resolved palette data, ready for the linker to splice into PRG
/// ROM as a 32-byte data blob at the label returned by [`Self::label`].
/// Declarations shorter than 32 bytes are zero-padded so the runtime
/// can always push exactly 32 bytes to `$3F00-$3F1F`.
#[derive(Debug, Clone)]
pub struct PaletteData {
pub name: String,
/// Exactly 32 bytes. Index `i` is the value written to PPU
/// address `$3F00 + i`.
pub colors: [u8; 32],
}
impl PaletteData {
/// The ROM-level label under which the linker emits the 32-byte
/// blob. The IR codegen references this label when lowering
/// `set_palette Name`.
#[must_use]
pub fn label(&self) -> String {
format!("__palette_{}", self.name)
}
}
/// Resolved background data. `tiles` is the 960-byte nametable
/// (32 columns × 30 rows) and `attrs` is the 64-byte attribute
/// table. Both are zero-padded up from the declared sizes so the
/// runtime NMI helper can always push fixed-length data.
#[derive(Debug, Clone)]
pub struct BackgroundData {
pub name: String,
pub tiles: [u8; 960],
pub attrs: [u8; 64],
}
impl BackgroundData {
#[must_use]
pub fn tiles_label(&self) -> String {
format!("__bg_tiles_{}", self.name)
}
#[must_use]
pub fn attrs_label(&self) -> String {
format!("__bg_attrs_{}", self.name)
}
}
/// Resolve sprite declarations in a program into concrete CHR byte blobs and
/// assign each one a tile index in CHR ROM.
///
/// Tile index 0 is reserved for the built-in default smiley sprite, so user
/// sprites start at tile index 1. A single sprite declaration may occupy
/// multiple consecutive tiles if its CHR data is larger than 16 bytes.
///
/// `source_dir` is used as the base for `@binary` / `@chr` relative paths.
/// Missing files are silently skipped (not an error) so programs that
/// reference external assets for documentation purposes compile without
/// requiring the files to exist yet.
pub fn resolve_sprites(program: &Program, source_dir: &Path) -> Result<Vec<SpriteData>, String> {
let mut sprites = Vec::new();
// Tile index 0 is the built-in smiley; user sprites start at 1.
let mut next_tile: u8 = 1;
for sprite_decl in &program.sprites {
let chr_bytes = match &sprite_decl.chr_source {
AssetSource::Inline(bytes) => bytes.clone(),
AssetSource::Binary(path) => {
// Try to read raw bytes from the file. Missing files are
// skipped silently so declarations can reference assets
// that haven't been added yet.
let full_path = source_dir.join(path);
match std::fs::read(&full_path) {
Ok(bytes) => bytes,
Err(_) => continue,
}
}
AssetSource::Chr(path) => {
// PNG → CHR conversion. Missing files skipped silently.
let full_path = source_dir.join(path);
match crate::assets::png_to_chr(&full_path) {
Ok(bytes) => bytes,
Err(_) => continue,
}
}
};
// Each NES 8x8 tile is 16 bytes of 2-bitplane CHR data. A single
// sprite declaration can span multiple tiles when its CHR blob is
// longer than 16 bytes.
let tile_count = chr_bytes.len().div_ceil(16);
if tile_count == 0 {
continue;
}
if next_tile as usize + tile_count > 256 {
return Err(format!(
"sprite '{}' would exceed CHR ROM tile limit",
sprite_decl.name
));
}
sprites.push(SpriteData {
name: sprite_decl.name.clone(),
tile_index: next_tile,
chr_bytes,
});
next_tile += tile_count as u8;
}
Ok(sprites)
}
/// Resolve all `palette Name { ... }` declarations in `program` into
/// 32-byte fixed-size blobs suitable for splicing into PRG ROM.
/// Declarations with fewer than 32 colors are zero-padded.
#[must_use]
pub fn resolve_palettes(program: &Program) -> Vec<PaletteData> {
program
.palettes
.iter()
.map(|p| {
let mut colors = [0u8; 32];
for (i, c) in p.colors.iter().enumerate().take(32) {
colors[i] = *c;
}
PaletteData {
name: p.name.clone(),
colors,
}
})
.collect()
}
/// Resolve all `background Name { ... }` declarations in `program`
/// into fixed-size 960-byte tile maps and 64-byte attribute tables.
/// Declarations shorter than the maximum are zero-padded.
#[must_use]
pub fn resolve_backgrounds(program: &Program) -> Vec<BackgroundData> {
program
.backgrounds
.iter()
.map(|b| {
let mut tiles = [0u8; 960];
for (i, t) in b.tiles.iter().enumerate().take(960) {
tiles[i] = *t;
}
let mut attrs = [0u8; 64];
for (i, a) in b.attributes.iter().enumerate().take(64) {
attrs[i] = *a;
}
BackgroundData {
name: b.name.clone(),
tiles,
attrs,
}
})
.collect()
}
#[cfg(test)]
mod tests {
use super::*;
use crate::lexer::Span;
use crate::parser::ast::{GameDecl, HeaderFormat, Mapper, Mirroring, SpriteDecl};
fn make_program(sprite: SpriteDecl) -> Program {
Program {
game: GameDecl {
name: "Test".to_string(),
mapper: Mapper::NROM,
mirroring: Mirroring::Horizontal,
header: HeaderFormat::Ines1,
span: Span::dummy(),
},
globals: Vec::new(),
constants: Vec::new(),
enums: Vec::new(),
structs: Vec::new(),
functions: Vec::new(),
states: Vec::new(),
sprites: vec![sprite],
palettes: Vec::new(),
backgrounds: Vec::new(),
sfx: Vec::new(),
music: Vec::new(),
banks: Vec::new(),
start_state: "Main".to_string(),
span: Span::dummy(),
}
}
#[test]
fn resolve_inline_sprite() {
let sprite = SpriteDecl {
name: "Player".to_string(),
chr_source: AssetSource::Inline(vec![0u8; 16]),
span: Span::dummy(),
};
let program = make_program(sprite);
let sprites = resolve_sprites(&program, Path::new(".")).unwrap();
assert_eq!(sprites.len(), 1);
assert_eq!(sprites[0].name, "Player");
assert_eq!(sprites[0].tile_index, 1);
assert_eq!(sprites[0].chr_bytes.len(), 16);
}
#[test]
fn resolve_binary_file_reads_bytes() {
let dir = std::env::temp_dir();
let file_path = dir.join("nescript_resolve_test.bin");
let bytes: Vec<u8> = (0x40..0x50).collect();
std::fs::write(&file_path, &bytes).unwrap();
let sprite = SpriteDecl {
name: "Tile".to_string(),
chr_source: AssetSource::Binary(
file_path.file_name().unwrap().to_string_lossy().to_string(),
),
span: Span::dummy(),
};
let program = make_program(sprite);
let sprites = resolve_sprites(&program, &dir).unwrap();
assert_eq!(sprites.len(), 1);
assert_eq!(sprites[0].chr_bytes, bytes);
let _ = std::fs::remove_file(&file_path);
}
#[test]
fn resolve_missing_binary_skipped() {
let sprite = SpriteDecl {
name: "Missing".to_string(),
chr_source: AssetSource::Binary("nonexistent.bin".to_string()),
span: Span::dummy(),
};
let program = make_program(sprite);
let sprites = resolve_sprites(&program, Path::new(".")).unwrap();
// Missing binary file → silently skipped
assert!(sprites.is_empty());
}
use crate::parser::ast::{BackgroundDecl, PaletteDecl};
fn blank_program() -> Program {
Program {
game: GameDecl {
name: "Test".to_string(),
mapper: Mapper::NROM,
mirroring: Mirroring::Horizontal,
header: HeaderFormat::Ines1,
span: Span::dummy(),
},
globals: Vec::new(),
constants: Vec::new(),
enums: Vec::new(),
structs: Vec::new(),
functions: Vec::new(),
states: Vec::new(),
sprites: Vec::new(),
palettes: Vec::new(),
backgrounds: Vec::new(),
sfx: Vec::new(),
music: Vec::new(),
banks: Vec::new(),
start_state: "Main".to_string(),
span: Span::dummy(),
}
}
#[test]
fn resolve_palette_zero_pads_to_32_bytes() {
let mut program = blank_program();
program.palettes.push(PaletteDecl {
name: "Cool".to_string(),
colors: vec![0x0F, 0x01, 0x11, 0x21],
span: Span::dummy(),
});
let resolved = resolve_palettes(&program);
assert_eq!(resolved.len(), 1);
assert_eq!(resolved[0].name, "Cool");
assert_eq!(resolved[0].colors.len(), 32);
assert_eq!(&resolved[0].colors[..4], &[0x0F, 0x01, 0x11, 0x21]);
// Remainder is zero-padded.
assert!(resolved[0].colors[4..].iter().all(|&b| b == 0));
assert_eq!(resolved[0].label(), "__palette_Cool");
}
#[test]
fn resolve_palette_truncates_beyond_32_bytes() {
// The analyzer rejects >32-byte palettes with E0201; at the
// resolve level we defensively truncate so downstream code
// always sees exactly 32 bytes. This lets bad input still
// produce a valid ROM structure for diagnostic purposes.
let mut program = blank_program();
program.palettes.push(PaletteDecl {
name: "Big".to_string(),
colors: (0u8..40).collect(),
span: Span::dummy(),
});
let resolved = resolve_palettes(&program);
assert_eq!(resolved[0].colors.len(), 32);
assert_eq!(resolved[0].colors[0], 0);
assert_eq!(resolved[0].colors[31], 31);
}
#[test]
fn resolve_background_pads_tiles_and_attrs() {
let mut program = blank_program();
program.backgrounds.push(BackgroundDecl {
name: "Stage".to_string(),
tiles: vec![1, 2, 3],
attributes: vec![0xFF],
span: Span::dummy(),
});
let resolved = resolve_backgrounds(&program);
assert_eq!(resolved.len(), 1);
assert_eq!(resolved[0].name, "Stage");
assert_eq!(resolved[0].tiles.len(), 960);
assert_eq!(resolved[0].tiles[0], 1);
assert_eq!(resolved[0].tiles[2], 3);
assert!(resolved[0].tiles[3..].iter().all(|&b| b == 0));
assert_eq!(resolved[0].attrs.len(), 64);
assert_eq!(resolved[0].attrs[0], 0xFF);
assert!(resolved[0].attrs[1..].iter().all(|&b| b == 0));
assert_eq!(resolved[0].tiles_label(), "__bg_tiles_Stage");
assert_eq!(resolved[0].attrs_label(), "__bg_attrs_Stage");
}
}