1
0
Fork 0
mirror of https://github.com/imjasonh/nescript synced 2026-07-08 17:06:04 +00:00
nescript/src/parser/mod.rs
Claude ad951a835f
examples: adopt the friendly asset syntax
Rewrites every example with non-trivial asset declarations to use
the pleasant QoL syntax introduced in the previous commit. Every
example still compiles to a byte-identical ROM (verified by a
temp-path diff before committing), so the committed `.nes` files
and the 23 emulator goldens are unchanged.

  * platformer.ne — the centerpiece end-to-end demo:
      - `palette Main` goes to grouped form with a shared
        `universal: 0x22` (sky blue), one shared colour per
        sub-palette, and named NES colours throughout; the
        long-standing `$3F10` mirror-trap warning is now handled
        by the parser and the manual pitfall comment is gone.
      - `sprite Tileset` is 15 tiles of ASCII pixel art instead
        of 240 bytes of inline hex.
      - `background Level` uses a `legend { '.': 15, '#': 9, ... }`
        block plus `map:` strings for the 32×30 nametable, and
        `palette_map:` rows for the attribute table. The map
        reads top-down like the rendered screen.
      - SFX latch-once `pitch: 0x30` scalars + `envelope:` alias.
      - `music Theme` uses note names + `tempo: 10` default.
  * audio_demo.ne — scalar sfx pitches, `envelope:` alias, and a
    note-name `C4, E4, G4, ...` music track.
  * palette_and_background.ne — grouped CoolBlues / WarmReds
    palettes with `universal: black` + named colours, plus
    `legend` + `map:` tilemaps for the two backgrounds.
  * sprites_and_palettes.ne — Arrow and Heart sprites rewritten
    as `pixels:` ASCII art.

Along the way, two small parser extensions support the rewrites:

  - `parse_pixel_art` now accepts `a/b/c` as aliases for `#/%/@`,
    matching the vocabulary every NES editor (and our own
    gen_platformer_tiles.rs generator) uses.
  - `palette_map_to_attrs` allows up to 16 metatile rows (the
    full attribute-table coverage, including the off-screen
    bottom half) and auto-replicates row 14 → row 15 when only
    15 rows are supplied so the visible bottom of the screen
    gets consistent sub-palette assignments by default. The old
    15-row cap couldn't match a hand-packed `0xAA` attribute
    table for the last row; the platformer required this to
    stay byte-identical.

`scripts/gen_platformer_tiles.rs` is updated to emit the new
syntax directly (pixel-art `pixels:` block + `legend`/`map:`/
`palette_map:` for the background), so regenerating the
platformer tiles stays a one-liner.

474 lib tests + 64 integration tests pass (3 new parser tests
for `palette_map:` 15/16/17 rows and the `abc` alias). All 23
emulator goldens still match pixel- and sample-for-sample.

https://claude.ai/code/session_01PzaSFj3VahDzxEYTKCESkz
2026-04-13 18:04:21 +00:00

3050 lines
113 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.

pub mod ast;
pub mod preprocess;
#[cfg(test)]
mod tests;
pub use preprocess::preprocess as preprocess_source;
use crate::errors::{Diagnostic, ErrorCode};
use crate::lexer::{Span, Token, TokenKind};
use ast::*;
pub struct Parser {
tokens: Vec<Token>,
pos: usize,
diagnostics: Vec<Diagnostic>,
/// When true, `parse_primary` refuses to consume an `Ident {`
/// pattern as a struct literal — the `{` is reserved for the
/// following `if` / `while` / `for` block. Struct literals in
/// conditions must be parenthesized: `if x == (Foo { a: 1 })`.
restrict_struct_literals: bool,
}
impl Parser {
pub fn new(tokens: Vec<Token>) -> Self {
Self {
tokens,
pos: 0,
diagnostics: Vec::new(),
restrict_struct_literals: false,
}
}
pub fn parse(mut self) -> (Option<Program>, Vec<Diagnostic>) {
match self.parse_program() {
Ok(program) => (Some(program), self.diagnostics),
Err(diag) => {
self.diagnostics.push(diag);
(None, self.diagnostics)
}
}
}
// ── Token helpers ──
fn peek(&self) -> &TokenKind {
self.tokens
.get(self.pos)
.map_or(&TokenKind::Eof, |t| &t.kind)
}
fn peek_at_offset(&self, offset: usize) -> Option<&TokenKind> {
self.tokens.get(self.pos + offset).map(|t| &t.kind)
}
fn current_span(&self) -> Span {
self.tokens.get(self.pos).map_or(Span::dummy(), |t| t.span)
}
fn advance(&mut self) -> &Token {
let tok = &self.tokens[self.pos];
if self.pos < self.tokens.len() - 1 {
self.pos += 1;
}
tok
}
fn expect(&mut self, expected: &TokenKind) -> Result<Span, Diagnostic> {
if self.peek() == expected {
let span = self.current_span();
self.advance();
Ok(span)
} else {
Err(Diagnostic::error(
ErrorCode::E0201,
format!("expected '{expected}', found '{}'", self.peek()),
self.current_span(),
))
}
}
fn expect_ident(&mut self) -> Result<(String, Span), Diagnostic> {
if let TokenKind::Ident(name) = self.peek().clone() {
let span = self.current_span();
self.advance();
Ok((name, span))
} else {
Err(Diagnostic::error(
ErrorCode::E0201,
format!("expected identifier, found '{}'", self.peek()),
self.current_span(),
))
}
}
/// Accept an identifier or a keyword that can be used as a name
/// (e.g., button names like "start", "select").
fn expect_name(&mut self) -> Result<(String, Span), Diagnostic> {
let span = self.current_span();
let name = match self.peek() {
TokenKind::Ident(n) => n.clone(),
// Keywords that double as button/property names
TokenKind::KwStart => "start".to_string(),
TokenKind::KwState => "state".to_string(),
TokenKind::KwBreak => "break".to_string(),
TokenKind::KwContinue => "continue".to_string(),
TokenKind::KwReturn => "return".to_string(),
_ => {
return Err(Diagnostic::error(
ErrorCode::E0201,
format!("expected name, found '{}'", self.peek()),
span,
));
}
};
self.advance();
Ok((name, span))
}
// ── Program ──
fn parse_program(&mut self) -> Result<Program, Diagnostic> {
let mut game = None;
let mut globals = Vec::new();
let mut constants = Vec::new();
let mut enums: Vec<EnumDecl> = Vec::new();
let mut structs: Vec<StructDecl> = Vec::new();
let mut functions = Vec::new();
let mut states = Vec::new();
let mut sprites = Vec::new();
let mut palettes = Vec::new();
let mut backgrounds = Vec::new();
let mut sfx = Vec::new();
let mut music = Vec::new();
let mut banks = Vec::new();
let mut start_state = None;
let mut on_frame = None;
let span = self.current_span();
while *self.peek() != TokenKind::Eof {
match self.peek().clone() {
TokenKind::KwGame => {
game = Some(self.parse_game_decl()?);
}
TokenKind::KwFast | TokenKind::KwSlow => {
globals.push(self.parse_var_decl()?);
}
TokenKind::KwVar => {
globals.push(self.parse_var_decl()?);
}
TokenKind::KwFun | TokenKind::KwInline => {
functions.push(self.parse_fun_decl()?);
}
TokenKind::KwConst => {
constants.push(self.parse_const_decl()?);
}
TokenKind::KwEnum => {
enums.push(self.parse_enum_decl()?);
}
TokenKind::KwStruct => {
structs.push(self.parse_struct_decl()?);
}
TokenKind::KwState => {
states.push(self.parse_state_decl()?);
}
TokenKind::KwSprite => {
sprites.push(self.parse_sprite_decl()?);
}
TokenKind::KwPalette => {
palettes.push(self.parse_palette_decl()?);
}
TokenKind::KwBackground => {
backgrounds.push(self.parse_background_decl()?);
}
TokenKind::KwSfx => {
sfx.push(self.parse_sfx_decl()?);
}
TokenKind::KwMusic => {
music.push(self.parse_music_decl()?);
}
TokenKind::KwBank => {
banks.push(self.parse_bank_decl()?);
}
TokenKind::KwOn => {
// Top-level `on frame` — implicit single state for M1
on_frame = Some(self.parse_on_frame()?);
}
TokenKind::KwStart => {
let kw_span = self.current_span();
self.advance();
let (name, _) = self.expect_ident()?;
if start_state.is_some() {
return Err(Diagnostic::error(
ErrorCode::E0505,
"multiple 'start' declarations",
kw_span,
));
}
start_state = Some(name);
}
_ => {
return Err(Diagnostic::error(
ErrorCode::E0201,
format!("unexpected token '{}' at top level", self.peek()),
self.current_span(),
));
}
}
}
let game = game.ok_or_else(|| {
Diagnostic::error(ErrorCode::E0504, "missing 'game' declaration", span)
})?;
// If there's a top-level `on frame` but no explicit states,
// wrap it in an implicit "Main" state
if !states.is_empty() || on_frame.is_none() {
// Multi-state or no frame handler — use states as-is
} else if let Some(frame_block) = on_frame {
states.push(StateDecl {
name: "Main".to_string(),
locals: Vec::new(),
on_enter: None,
on_exit: None,
on_frame: Some(frame_block),
on_scanline: Vec::new(),
span,
});
if start_state.is_none() {
start_state = Some("Main".to_string());
}
}
let start_state = start_state.ok_or_else(|| {
Diagnostic::error(ErrorCode::E0504, "missing 'start' declaration", span)
})?;
Ok(Program {
game,
globals,
constants,
enums,
structs,
functions,
states,
sprites,
palettes,
backgrounds,
sfx,
music,
banks,
start_state,
span,
})
}
fn parse_struct_decl(&mut self) -> Result<StructDecl, Diagnostic> {
let start = self.current_span();
self.expect(&TokenKind::KwStruct)?;
let (name, _) = self.expect_ident()?;
self.expect(&TokenKind::LBrace)?;
let mut fields = Vec::new();
while *self.peek() != TokenKind::RBrace && *self.peek() != TokenKind::Eof {
let field_span = self.current_span();
let (field_name, _) = self.expect_ident()?;
self.expect(&TokenKind::Colon)?;
let field_type = self.parse_type()?;
fields.push(StructField {
name: field_name,
field_type,
span: field_span,
});
if *self.peek() == TokenKind::Comma {
self.advance();
} else if *self.peek() != TokenKind::RBrace {
return Err(Diagnostic::error(
ErrorCode::E0201,
"expected ',' or '}' in struct body",
self.current_span(),
));
}
}
self.expect(&TokenKind::RBrace)?;
Ok(StructDecl {
name,
fields,
span: Span::new(start.file_id, start.start, self.current_span().end),
})
}
fn parse_enum_decl(&mut self) -> Result<EnumDecl, Diagnostic> {
let start = self.current_span();
self.expect(&TokenKind::KwEnum)?;
let (name, _) = self.expect_ident()?;
self.expect(&TokenKind::LBrace)?;
let mut variants = Vec::new();
while *self.peek() != TokenKind::RBrace && *self.peek() != TokenKind::Eof {
let span = self.current_span();
let (vname, _) = self.expect_ident()?;
variants.push((vname, span));
if *self.peek() == TokenKind::Comma {
self.advance();
} else if *self.peek() != TokenKind::RBrace {
return Err(Diagnostic::error(
ErrorCode::E0201,
"expected ',' or '}' in enum body",
self.current_span(),
));
}
}
self.expect(&TokenKind::RBrace)?;
if variants.len() > 256 {
return Err(Diagnostic::error(
ErrorCode::E0201,
"enum has more than 256 variants (u8 overflow)",
start,
));
}
Ok(EnumDecl {
name,
variants,
span: Span::new(start.file_id, start.start, self.current_span().end),
})
}
// ── Game declaration ──
fn parse_game_decl(&mut self) -> Result<GameDecl, Diagnostic> {
let start_span = self.current_span();
self.expect(&TokenKind::KwGame)?;
let name = if let TokenKind::StringLiteral(s) = self.peek().clone() {
self.advance();
s
} else {
return Err(Diagnostic::error(
ErrorCode::E0201,
"expected game name string",
self.current_span(),
));
};
self.expect(&TokenKind::LBrace)?;
let mut mapper = Mapper::NROM;
let mut mirroring = Mirroring::Horizontal;
while *self.peek() != TokenKind::RBrace && *self.peek() != TokenKind::Eof {
let (key, _) = self.expect_ident()?;
self.expect(&TokenKind::Colon)?;
match key.as_str() {
"mapper" => {
let (val, _) = self.expect_ident()?;
mapper = match val.as_str() {
"NROM" => Mapper::NROM,
"MMC1" => Mapper::MMC1,
"UxROM" => Mapper::UxROM,
"MMC3" => Mapper::MMC3,
_ => {
return Err(Diagnostic::error(
ErrorCode::E0201,
format!("unknown mapper '{val}'"),
self.current_span(),
)
.with_help("supported mappers: NROM, MMC1, UxROM, MMC3"));
}
};
}
"mirroring" => {
let (val, _) = self.expect_ident()?;
mirroring = match val.as_str() {
"horizontal" => Mirroring::Horizontal,
"vertical" => Mirroring::Vertical,
_ => {
return Err(Diagnostic::error(
ErrorCode::E0201,
format!("unknown mirroring '{val}'"),
self.current_span(),
));
}
};
}
_ => {
return Err(Diagnostic::error(
ErrorCode::E0201,
format!("unknown game property '{key}'"),
self.current_span(),
));
}
}
}
self.expect(&TokenKind::RBrace)?;
Ok(GameDecl {
name,
mapper,
mirroring,
span: Span::new(
start_span.file_id,
start_span.start,
self.current_span().end,
),
})
}
// ── Variable declaration ──
fn parse_var_decl(&mut self) -> Result<VarDecl, Diagnostic> {
let start = self.current_span();
let placement = match self.peek() {
TokenKind::KwFast => {
self.advance();
Placement::Fast
}
TokenKind::KwSlow => {
self.advance();
Placement::Slow
}
_ => Placement::Auto,
};
self.expect(&TokenKind::KwVar)?;
let (name, _) = self.expect_ident()?;
self.expect(&TokenKind::Colon)?;
let var_type = self.parse_type()?;
let init = if *self.peek() == TokenKind::Assign {
self.advance();
Some(self.parse_expr()?)
} else {
None
};
Ok(VarDecl {
name,
var_type,
init,
placement,
span: Span::new(start.file_id, start.start, self.current_span().end),
})
}
// ── Const declaration ──
fn parse_const_decl(&mut self) -> Result<ConstDecl, Diagnostic> {
let start = self.current_span();
self.expect(&TokenKind::KwConst)?;
let (name, _) = self.expect_ident()?;
self.expect(&TokenKind::Colon)?;
let const_type = self.parse_type()?;
self.expect(&TokenKind::Assign)?;
let value = self.parse_expr()?;
Ok(ConstDecl {
name,
const_type,
value,
span: Span::new(start.file_id, start.start, self.current_span().end),
})
}
// ── Function declaration ──
fn parse_fun_decl(&mut self) -> Result<FunDecl, Diagnostic> {
let start = self.current_span();
let is_inline = if *self.peek() == TokenKind::KwInline {
self.advance();
true
} else {
false
};
self.expect(&TokenKind::KwFun)?;
let (name, _) = self.expect_ident()?;
self.expect(&TokenKind::LParen)?;
let mut params = Vec::new();
while *self.peek() != TokenKind::RParen && *self.peek() != TokenKind::Eof {
let (pname, _) = self.expect_ident()?;
self.expect(&TokenKind::Colon)?;
let ptype = self.parse_type()?;
params.push(Param {
name: pname,
param_type: ptype,
});
if *self.peek() == TokenKind::Comma {
self.advance();
}
}
self.expect(&TokenKind::RParen)?;
let return_type = if *self.peek() == TokenKind::Arrow {
self.advance();
Some(self.parse_type()?)
} else {
None
};
let body = self.parse_block()?;
Ok(FunDecl {
name,
params,
return_type,
body,
is_inline,
span: Span::new(start.file_id, start.start, self.current_span().end),
})
}
// ── State declaration ──
fn parse_state_decl(&mut self) -> Result<StateDecl, Diagnostic> {
let start = self.current_span();
self.expect(&TokenKind::KwState)?;
let (name, _) = self.expect_ident()?;
self.expect(&TokenKind::LBrace)?;
let mut locals = Vec::new();
let mut on_enter = None;
let mut on_exit = None;
let mut on_frame = None;
let mut on_scanline: Vec<(u8, Block)> = Vec::new();
while *self.peek() != TokenKind::RBrace && *self.peek() != TokenKind::Eof {
match self.peek().clone() {
TokenKind::KwFast | TokenKind::KwSlow | TokenKind::KwVar => {
locals.push(self.parse_var_decl()?);
}
TokenKind::KwOn => {
self.advance();
let (event, event_span) = self.expect_ident()?;
match event.as_str() {
"enter" => {
on_enter = Some(self.parse_block()?);
}
"exit" => {
on_exit = Some(self.parse_block()?);
}
"frame" => {
on_frame = Some(self.parse_block()?);
}
"scanline" => {
// Syntax: `on scanline(N) { ... }`
self.expect(&TokenKind::LParen)?;
let line = if let TokenKind::IntLiteral(v) = self.peek().clone() {
self.advance();
if v > 239 {
return Err(Diagnostic::error(
ErrorCode::E0201,
format!("scanline value {v} out of range (0-239)"),
self.current_span(),
));
}
v as u8
} else {
return Err(Diagnostic::error(
ErrorCode::E0201,
"expected integer scanline number",
self.current_span(),
));
};
self.expect(&TokenKind::RParen)?;
let body = self.parse_block()?;
on_scanline.push((line, body));
}
_ => {
return Err(Diagnostic::error(
ErrorCode::E0201,
format!("unknown event handler 'on {event}'"),
event_span,
));
}
}
}
_ => {
return Err(Diagnostic::error(
ErrorCode::E0201,
format!("unexpected token '{}' in state body", self.peek()),
self.current_span(),
));
}
}
}
self.expect(&TokenKind::RBrace)?;
Ok(StateDecl {
name,
locals,
on_enter,
on_exit,
on_frame,
on_scanline,
span: Span::new(start.file_id, start.start, self.current_span().end),
})
}
// ── Bank declaration ──
fn parse_bank_decl(&mut self) -> Result<BankDecl, Diagnostic> {
let start = self.current_span();
self.expect(&TokenKind::KwBank)?;
let (name, _) = self.expect_ident()?;
self.expect(&TokenKind::Colon)?;
let (type_str, _) = self.expect_ident()?;
let bank_type = match type_str.as_str() {
"prg" => BankType::Prg,
"chr" => BankType::Chr,
_ => {
return Err(Diagnostic::error(
ErrorCode::E0201,
format!("expected 'prg' or 'chr', found '{type_str}'"),
self.current_span(),
));
}
};
Ok(BankDecl {
name,
bank_type,
span: Span::new(start.file_id, start.start, self.current_span().end),
})
}
// ── Top-level on frame ──
fn parse_on_frame(&mut self) -> Result<Block, Diagnostic> {
self.expect(&TokenKind::KwOn)?;
let (event, _) = self.expect_ident()?;
if event != "frame" {
return Err(Diagnostic::error(
ErrorCode::E0201,
format!("expected 'frame' after 'on', found '{event}'"),
self.current_span(),
));
}
self.parse_block()
}
// ── Sprite / Palette / Background declarations ──
/// Sprite declarations accept one of two shapes:
///
/// **Raw CHR bytes** — the original form, matching how CHR is
/// stored on the cart:
/// ```text
/// sprite Heart {
/// chr: [0x66, 0xFF, 0xFF, 0xFF, 0x7E, 0x3C, 0x18, 0x00,
/// 0x66, 0xFF, 0xFF, 0xFF, 0x7E, 0x3C, 0x18, 0x00]
/// }
/// // or from an external file:
/// sprite Player { chr: @chr("assets/player.png") }
/// ```
///
/// **Pixel-art strings** — each string is one row of pixels, each
/// character is one pixel's 2-bit palette index. Way easier to
/// hand-author than hex:
/// ```text
/// sprite Arrow {
/// pixels: [
/// "...##...",
/// "...###..",
/// "########",
/// "########",
/// "########",
/// "########",
/// "...###..",
/// "...##..."
/// ]
/// }
/// ```
///
/// Characters map to palette indices as follows:
///
/// | Char(s) | Index | Meaning |
/// |----------|-------|-------------------------------|
/// | `.` ` ` | 0 | transparent / background |
/// | `#` `1` | 1 | sub-palette colour 1 |
/// | `%` `2` | 2 | sub-palette colour 2 |
/// | `@` `3` | 3 | sub-palette colour 3 |
///
/// Rows must all be the same length, and both dimensions must be
/// multiples of 8 (the NES tile size). Multi-tile sprites (16×8,
/// 8×16, 16×16, …) are split into 8×8 tiles in row-major reading
/// order so consecutive tile indices line up with what `draw`
/// expects.
fn parse_sprite_decl(&mut self) -> Result<SpriteDecl, Diagnostic> {
let start = self.current_span();
self.expect(&TokenKind::KwSprite)?;
let (name, _) = self.expect_ident()?;
self.expect(&TokenKind::LBrace)?;
let mut chr_source: Option<AssetSource> = None;
while *self.peek() != TokenKind::RBrace && *self.peek() != TokenKind::Eof {
let (key, key_span) = self.expect_ident()?;
self.expect(&TokenKind::Colon)?;
match key.as_str() {
"chr" => {
if chr_source.is_some() {
return Err(Diagnostic::error(
ErrorCode::E0201,
"sprite 'chr' and 'pixels' are mutually exclusive",
key_span,
));
}
chr_source = Some(self.parse_asset_source()?);
}
"pixels" => {
if chr_source.is_some() {
return Err(Diagnostic::error(
ErrorCode::E0201,
"sprite 'pixels' and 'chr' are mutually exclusive",
key_span,
));
}
let bytes = self.parse_pixel_art(&name, key_span)?;
chr_source = Some(AssetSource::Inline(bytes));
}
_ => {
return Err(Diagnostic::error(
ErrorCode::E0201,
format!("unknown sprite property '{key}'"),
self.current_span(),
));
}
}
}
self.expect(&TokenKind::RBrace)?;
let chr_source = chr_source.ok_or_else(|| {
Diagnostic::error(
ErrorCode::E0201,
"sprite requires 'chr' or 'pixels' property",
start,
)
})?;
Ok(SpriteDecl {
name,
chr_source,
span: Span::new(start.file_id, start.start, self.current_span().end),
})
}
/// Parse a pixel-art block of the form
/// `[ "row0", "row1", ... ]` and lower it to CHR bytes.
///
/// Each character is one pixel; see [`Self::parse_sprite_decl`]
/// for the full character → palette-index mapping. All rows must
/// be the same length and both dimensions must be multiples of 8.
/// Multi-tile sprites are split into 8×8 tiles in row-major order
/// so `tile_index, tile_index+1, ...` traverses the tiles in the
/// same order your eye reads them.
fn parse_pixel_art(
&mut self,
sprite_name: &str,
key_span: Span,
) -> Result<Vec<u8>, Diagnostic> {
self.expect(&TokenKind::LBracket)?;
let mut rows: Vec<String> = Vec::new();
while *self.peek() != TokenKind::RBracket && *self.peek() != TokenKind::Eof {
match self.peek().clone() {
TokenKind::StringLiteral(s) => {
self.advance();
rows.push(s);
}
_ => {
return Err(Diagnostic::error(
ErrorCode::E0201,
format!(
"expected pixel row string in sprite '{sprite_name}', found '{}'",
self.peek()
),
self.current_span(),
));
}
}
if *self.peek() == TokenKind::Comma {
self.advance();
}
}
self.expect(&TokenKind::RBracket)?;
if rows.is_empty() {
return Err(Diagnostic::error(
ErrorCode::E0201,
format!("sprite '{sprite_name}' 'pixels' list is empty"),
key_span,
));
}
let width = rows[0].chars().count();
for (i, row) in rows.iter().enumerate() {
let len = row.chars().count();
if len != width {
return Err(Diagnostic::error(
ErrorCode::E0201,
format!(
"sprite '{sprite_name}' pixel row {i} has {len} cells but \
row 0 has {width}; every row must be the same width"
),
key_span,
));
}
}
let height = rows.len();
if width == 0 || !width.is_multiple_of(8) {
return Err(Diagnostic::error(
ErrorCode::E0201,
format!(
"sprite '{sprite_name}' width is {width}; must be a non-zero multiple of 8"
),
key_span,
));
}
if !height.is_multiple_of(8) {
return Err(Diagnostic::error(
ErrorCode::E0201,
format!("sprite '{sprite_name}' height is {height}; must be a multiple of 8"),
key_span,
));
}
// Convert rows to a 2-bit palette-index grid.
let mut grid: Vec<Vec<u8>> = Vec::with_capacity(height);
for (ry, row) in rows.iter().enumerate() {
let mut line = Vec::with_capacity(width);
for (rx, ch) in row.chars().enumerate() {
// Three vocabularies map to the same 0-3 index so
// artists can use whichever feels natural:
// `. # % @` — shade-intensity glyphs (dense = hi)
// `0 1 2 3` — literal palette-index digits
// `. a b c` — letter form used by most NES tools
let val = match ch {
'.' | ' ' | '0' => 0u8,
'#' | '1' | 'a' | 'A' => 1,
'%' | '2' | 'b' | 'B' => 2,
'@' | '3' | 'c' | 'C' => 3,
other => {
return Err(Diagnostic::error(
ErrorCode::E0201,
format!(
"sprite '{sprite_name}' pixel at ({rx}, {ry}) has \
invalid character '{other}'; use '.'/' '/'0' for \
index 0, '#'/'1'/'a' for 1, '%'/'2'/'b' for 2, \
'@'/'3'/'c' for 3"
),
key_span,
));
}
};
line.push(val);
}
grid.push(line);
}
// Encode into CHR tiles. Each 8×8 block becomes 16 bytes: the
// first 8 are bit 0 of each pixel (bitplane 0), the next 8 are
// bit 1 (bitplane 1). Tiles are emitted in row-major reading
// order so consecutive tile indices match what you'd expect.
let tiles_x = width / 8;
let tiles_y = height / 8;
let mut out = Vec::with_capacity(tiles_x * tiles_y * 16);
for ty in 0..tiles_y {
for tx in 0..tiles_x {
let mut plane0 = [0u8; 8];
let mut plane1 = [0u8; 8];
for row_in_tile in 0..8 {
let y = ty * 8 + row_in_tile;
let mut p0 = 0u8;
let mut p1 = 0u8;
for col_in_tile in 0..8 {
let x = tx * 8 + col_in_tile;
let v = grid[y][x];
let shift = 7 - col_in_tile;
if v & 0b01 != 0 {
p0 |= 1 << shift;
}
if v & 0b10 != 0 {
p1 |= 1 << shift;
}
}
plane0[row_in_tile] = p0;
plane1[row_in_tile] = p1;
}
out.extend_from_slice(&plane0);
out.extend_from_slice(&plane1);
}
}
Ok(out)
}
// ── Palette / Background declarations ──
/// `palette Name { colors: [c0, c1, ..., c31] }` — declares a
/// 32-byte PPU palette. Colors shorter than 32 are zero-padded
/// by the analyzer; colors longer than 32 are rejected.
/// Palette declarations accept one of two shapes. They cannot be mixed:
///
/// **Flat form** — a single 32-byte list matching the PPU layout:
/// ```text
/// palette Main {
/// colors: [0x0F, 0x01, 0x11, 0x21, /* bg0..bg3, sp0..sp3 */ ...]
/// }
/// ```
///
/// **Grouped form** — a per-slot declaration with an optional shared
/// universal colour:
/// ```text
/// palette Main {
/// universal: black // optional, defaults to black ($0F)
/// bg0: [dk_blue, blue, sky_blue] // 3 colours — universal prepended
/// bg1: [dk_purple, purple, lavender]
/// bg2: [dk_red, red, peach]
/// bg3: [dk_green, green, mint]
/// sp0: [dk_blue, blue, sky_blue]
/// sp1: [dk_red, red, peach]
/// sp2: [dk_green, green, mint]
/// sp3: [dk_gray, lt_gray, white]
/// }
/// ```
///
/// Grouped form auto-fixes the `$3F10 / $3F14 / $3F18 / $3F1C` PPU
/// mirror issue — every sub-palette's index-0 byte is forced to the
/// same universal value, so sequential writes to
/// `$3F00-$3F1F` never accidentally clobber the shared background
/// colour.
///
/// Any colour value (in either form) may be a raw byte literal
/// (`0x0F`) or a named NES colour (`black`, `dk_blue`, `sky_blue`, …).
/// See [`crate::assets::color_name_to_index`] for the full name list.
fn parse_palette_decl(&mut self) -> Result<PaletteDecl, Diagnostic> {
let start = self.current_span();
self.expect(&TokenKind::KwPalette)?;
let (name, _) = self.expect_ident()?;
self.expect(&TokenKind::LBrace)?;
// Flat-form output.
let mut flat_colors: Option<Vec<u8>> = None;
// Grouped-form scratch: 8 sub-palette slots, each up to 4
// colours. `None` means "user didn't declare this slot".
let mut slots: [Option<Vec<u8>>; 8] = Default::default();
let mut universal: Option<u8> = None;
let mut grouped_first_key: Option<Span> = None;
while *self.peek() != TokenKind::RBrace && *self.peek() != TokenKind::Eof {
let (key, key_span) = self.expect_ident()?;
self.expect(&TokenKind::Colon)?;
match key.as_str() {
"colors" => {
if grouped_first_key.is_some() {
return Err(Diagnostic::error(
ErrorCode::E0201,
"palette cannot mix 'colors' with grouped sub-palette \
fields (bg0..sp3 / universal); pick one form",
key_span,
));
}
flat_colors = Some(self.parse_color_array("colors")?);
}
"universal" => {
if flat_colors.is_some() {
return Err(Diagnostic::error(
ErrorCode::E0201,
"palette cannot mix 'colors' with 'universal'; pick one form",
key_span,
));
}
grouped_first_key.get_or_insert(key_span);
universal = Some(self.parse_color_value("universal")?);
}
slot_name @ ("bg0" | "bg1" | "bg2" | "bg3" | "sp0" | "sp1" | "sp2" | "sp3") => {
if flat_colors.is_some() {
return Err(Diagnostic::error(
ErrorCode::E0201,
format!(
"palette cannot mix 'colors' with '{slot_name}'; pick one form"
),
key_span,
));
}
grouped_first_key.get_or_insert(key_span);
let slot_idx = match slot_name {
"bg0" => 0,
"bg1" => 1,
"bg2" => 2,
"bg3" => 3,
"sp0" => 4,
"sp1" => 5,
"sp2" => 6,
"sp3" => 7,
_ => unreachable!(),
};
if slots[slot_idx].is_some() {
return Err(Diagnostic::error(
ErrorCode::E0501,
format!("duplicate sub-palette '{slot_name}'"),
key_span,
));
}
let entries = self.parse_color_array(slot_name)?;
if entries.len() > 4 {
return Err(Diagnostic::error(
ErrorCode::E0201,
format!(
"sub-palette '{slot_name}' has {} colours; maximum is 4 \
(3 + optional leading universal override)",
entries.len()
),
key_span,
));
}
slots[slot_idx] = Some(entries);
}
_ => {
return Err(Diagnostic::error(
ErrorCode::E0201,
format!("unknown palette property '{key}'"),
key_span,
));
}
}
if *self.peek() == TokenKind::Comma {
self.advance();
}
}
self.expect(&TokenKind::RBrace)?;
let colors = if let Some(flat) = flat_colors {
flat
} else if grouped_first_key.is_some() {
// Assemble the 32-byte blob from the grouped slots.
// `$0F` is the canonical "one true black" universal that
// every NES cart uses when nothing else is specified.
let uni = universal.unwrap_or(0x0F);
let mut out = vec![0u8; 32];
for (slot_idx, slot) in slots.iter().enumerate() {
let base = slot_idx * 4;
out[base] = uni;
if let Some(entries) = slot {
if entries.len() == 4 {
// Explicit override of the universal byte
// for this slot only.
out[base] = entries[0];
out[base + 1] = entries[1];
out[base + 2] = entries[2];
out[base + 3] = entries[3];
} else {
for (i, c) in entries.iter().enumerate() {
out[base + 1 + i] = *c;
}
}
}
}
out
} else {
return Err(Diagnostic::error(
ErrorCode::E0201,
"palette requires either 'colors' or at least one sub-palette field \
(bg0..bg3 / sp0..sp3)",
start,
));
};
Ok(PaletteDecl {
name,
colors,
span: Span::new(start.file_id, start.start, self.current_span().end),
})
}
/// Parse a single NES colour value: either a `u8` integer literal or
/// an identifier resolved via
/// [`crate::assets::color_name_to_index`]. Used by palette
/// declarations so either raw hex bytes (`0x0F`) or friendly names
/// (`black`, `sky_blue`) can appear anywhere a colour is expected.
fn parse_color_value(&mut self, prop: &str) -> Result<u8, Diagnostic> {
match self.peek().clone() {
TokenKind::IntLiteral(v) => {
self.advance();
if v > 0xFF {
return Err(Diagnostic::error(
ErrorCode::E0201,
format!("'{prop}' colour value {v} doesn't fit in a u8"),
self.current_span(),
));
}
Ok(v as u8)
}
TokenKind::Ident(name) => {
let span = self.current_span();
self.advance();
crate::assets::color_name_to_index(&name).ok_or_else(|| {
Diagnostic::error(
ErrorCode::E0201,
format!(
"unknown NES colour name '{name}' in '{prop}'; \
use a byte literal (0x00-0x3F) or a name like \
'black' / 'blue' / 'sky_blue'"
),
span,
)
})
}
_ => Err(Diagnostic::error(
ErrorCode::E0201,
format!(
"expected colour value for '{prop}' (byte literal or name), found '{}'",
self.peek()
),
self.current_span(),
)),
}
}
/// Parse `[color, color, ...]` where each element is either a byte
/// literal or a named NES colour. This is the "friendly" version of
/// [`Self::parse_byte_array`] used everywhere a palette byte is
/// expected.
fn parse_color_array(&mut self, prop: &str) -> Result<Vec<u8>, Diagnostic> {
self.expect(&TokenKind::LBracket)?;
let mut out = Vec::new();
while *self.peek() != TokenKind::RBracket && *self.peek() != TokenKind::Eof {
out.push(self.parse_color_value(prop)?);
if *self.peek() == TokenKind::Comma {
self.advance();
}
}
self.expect(&TokenKind::RBracket)?;
Ok(out)
}
/// Background declarations pick one of two authoring styles for
/// the 32×30 nametable:
///
/// **Raw bytes** — a flat list matching the PPU nametable layout,
/// 960 tile indices in row-major order, optionally followed by a
/// 64-byte attribute table:
/// ```text
/// background StageOne {
/// tiles: [0, 1, 2, 3, ...]
/// attributes: [0xFF, 0x55, ...]
/// }
/// ```
///
/// **Tilemap + legend** — a legend mapping single characters to
/// CHR tile indices, followed by a `map:` list-of-strings where
/// each character is one cell of the nametable. Rows shorter than
/// 32 cells are right-padded with tile 0; extra rows past row 30
/// are an error. Optional `palette_map:` is a 16×15 grid of
/// sub-palette indices `0`-`3`, one digit per 16×16 metatile,
/// auto-packed into the 64-byte attribute table (no more hand-
/// packing 2-bit pairs):
/// ```text
/// background StageOne {
/// legend {
/// '.': 0 // empty
/// '#': 1 // brick
/// 'X': 2 // coin
/// }
/// map: [
/// "................................",
/// "................................",
/// "......##........##..............",
/// "....##..##....##..##............",
/// // ... up to 30 rows, 32 cells each
/// ]
/// palette_map: [
/// "0000000011110000", // 16 metatile cols
/// "0000000011110000",
/// // ... up to 15 metatile rows
/// ]
/// }
/// ```
fn parse_background_decl(&mut self) -> Result<BackgroundDecl, Diagnostic> {
let start = self.current_span();
self.expect(&TokenKind::KwBackground)?;
let (name, _) = self.expect_ident()?;
self.expect(&TokenKind::LBrace)?;
// Raw-form scratch.
let mut tiles_raw: Option<Vec<u8>> = None;
let mut attributes_raw: Option<Vec<u8>> = None;
// Tilemap-form scratch.
let mut legend: Option<std::collections::HashMap<char, u8>> = None;
let mut legend_span: Option<Span> = None;
let mut map_rows: Option<(Vec<String>, Span)> = None;
let mut palette_rows: Option<(Vec<String>, Span)> = None;
while *self.peek() != TokenKind::RBrace && *self.peek() != TokenKind::Eof {
// `legend { ... }` uses a brace block rather than a
// `key: value` pair, so detect it specially.
if matches!(self.peek(), TokenKind::Ident(n) if n == "legend")
&& self.peek_at_offset(1) == Some(&TokenKind::LBrace)
{
let span = self.current_span();
self.advance(); // `legend`
self.advance(); // `{`
let mut map: std::collections::HashMap<char, u8> = std::collections::HashMap::new();
while *self.peek() != TokenKind::RBrace && *self.peek() != TokenKind::Eof {
let key_span = self.current_span();
let ch = match self.peek().clone() {
TokenKind::StringLiteral(s) => {
self.advance();
let mut chars = s.chars();
let c = chars.next().ok_or_else(|| {
Diagnostic::error(
ErrorCode::E0201,
"legend key must be a single character",
key_span,
)
})?;
if chars.next().is_some() {
return Err(Diagnostic::error(
ErrorCode::E0201,
format!("legend key '{s}' has more than one character"),
key_span,
));
}
c
}
_ => {
return Err(Diagnostic::error(
ErrorCode::E0201,
format!(
"expected string literal for legend key, found '{}'",
self.peek()
),
key_span,
));
}
};
self.expect(&TokenKind::Colon)?;
let tile = self.parse_u8_literal("legend value")?;
if map.insert(ch, tile).is_some() {
return Err(Diagnostic::error(
ErrorCode::E0501,
format!("duplicate legend entry '{ch}'"),
key_span,
));
}
if *self.peek() == TokenKind::Comma {
self.advance();
}
}
self.expect(&TokenKind::RBrace)?;
if legend.is_some() {
return Err(Diagnostic::error(
ErrorCode::E0501,
"duplicate 'legend' block",
span,
));
}
legend = Some(map);
legend_span = Some(span);
continue;
}
let (key, key_span) = self.expect_ident()?;
self.expect(&TokenKind::Colon)?;
match key.as_str() {
"tiles" => {
if tiles_raw.is_some() || map_rows.is_some() {
return Err(Diagnostic::error(
ErrorCode::E0501,
"duplicate tile data in background declaration",
key_span,
));
}
tiles_raw = Some(self.parse_byte_array("tiles")?);
}
"attributes" => {
if attributes_raw.is_some() || palette_rows.is_some() {
return Err(Diagnostic::error(
ErrorCode::E0501,
"duplicate attribute data in background declaration",
key_span,
));
}
attributes_raw = Some(self.parse_byte_array("attributes")?);
}
"map" => {
if map_rows.is_some() || tiles_raw.is_some() {
return Err(Diagnostic::error(
ErrorCode::E0501,
"duplicate tile data in background declaration",
key_span,
));
}
map_rows = Some((self.parse_string_array("map")?, key_span));
}
"palette_map" => {
if palette_rows.is_some() || attributes_raw.is_some() {
return Err(Diagnostic::error(
ErrorCode::E0501,
"duplicate attribute data in background declaration",
key_span,
));
}
palette_rows = Some((self.parse_string_array("palette_map")?, key_span));
}
_ => {
return Err(Diagnostic::error(
ErrorCode::E0201,
format!("unknown background property '{key}'"),
key_span,
));
}
}
if *self.peek() == TokenKind::Comma {
self.advance();
}
}
self.expect(&TokenKind::RBrace)?;
// Resolve the tile source.
let tiles = if let Some(flat) = tiles_raw {
flat
} else if let Some((rows, span)) = map_rows {
let legend = legend.as_ref().ok_or_else(|| {
Diagnostic::error(
ErrorCode::E0201,
"background 'map' requires a 'legend { ... }' block",
legend_span.unwrap_or(span),
)
})?;
tilemap_to_bytes(&name, &rows, legend, span)?
} else {
return Err(Diagnostic::error(
ErrorCode::E0201,
"background requires a 'tiles' array or a 'map' + 'legend'",
start,
));
};
// Resolve the attribute source.
let attributes = if let Some(flat) = attributes_raw {
flat
} else if let Some((rows, span)) = palette_rows {
palette_map_to_attrs(&name, &rows, span)?
} else {
Vec::new()
};
Ok(BackgroundDecl {
name,
tiles,
attributes,
span: Span::new(start.file_id, start.start, self.current_span().end),
})
}
/// Parse a `[string, string, ...]` list. Used by background
/// `map:` and `palette_map:` where each string is one row of the
/// grid and characters inside the string are cells.
fn parse_string_array(&mut self, prop: &str) -> Result<Vec<String>, Diagnostic> {
self.expect(&TokenKind::LBracket)?;
let mut out = Vec::new();
while *self.peek() != TokenKind::RBracket && *self.peek() != TokenKind::Eof {
match self.peek().clone() {
TokenKind::StringLiteral(s) => {
self.advance();
out.push(s);
}
_ => {
return Err(Diagnostic::error(
ErrorCode::E0201,
format!("expected string row in '{prop}', found '{}'", self.peek()),
self.current_span(),
));
}
}
if *self.peek() == TokenKind::Comma {
self.advance();
}
}
self.expect(&TokenKind::RBracket)?;
Ok(out)
}
// ── SFX / Music declarations ──
/// `sfx Name { duty: N, pitch: ..., volume: [..] }`.
///
/// The v1 audio driver latches the pulse-1 period **once** on
/// trigger, so there's no point giving a per-frame pitch array —
/// only `pitch[0]` is ever read. That's now reflected in the
/// syntax:
///
/// - `pitch: 0x50` — a single byte, latched at trigger time (the
/// natural form for the current driver).
/// - `pitch: [0x50, 0x50, ...]` — still accepted for
/// backwards-compatibility with existing sources; the analyzer
/// requires the array length to match `volume`.
///
/// `envelope:` is a friendlier alias for `volume:` — both mean the
/// same thing (the per-frame volume ramp that shapes the sound).
fn parse_sfx_decl(&mut self) -> Result<SfxDecl, Diagnostic> {
// Scalar pitches expand to a per-frame array once we know
// the envelope length, so we track both possibilities in
// this local enum while parsing. Declared here so clippy's
// `items_after_statements` stays happy.
enum PitchSrc {
Scalar(u8),
Array(Vec<u8>),
}
let start = self.current_span();
self.expect(&TokenKind::KwSfx)?;
let (name, _) = self.expect_ident()?;
self.expect(&TokenKind::LBrace)?;
let mut duty: u8 = 2;
let mut pitch_src: Option<PitchSrc> = None;
let mut volume: Option<Vec<u8>> = None;
let mut volume_key: &'static str = "volume";
while *self.peek() != TokenKind::RBrace && *self.peek() != TokenKind::Eof {
let (key, key_span) = self.expect_ident()?;
self.expect(&TokenKind::Colon)?;
match key.as_str() {
"duty" => {
duty = self.parse_u8_literal("duty")?;
if duty > 3 {
return Err(Diagnostic::error(
ErrorCode::E0201,
format!("sfx 'duty' must be 0-3, got {duty}"),
key_span,
));
}
}
"pitch" => {
// Either a scalar (new form) or a [bytes] array
// (legacy form). Branch on the leading token.
if *self.peek() == TokenKind::LBracket {
pitch_src = Some(PitchSrc::Array(self.parse_byte_array("pitch")?));
} else {
pitch_src = Some(PitchSrc::Scalar(self.parse_u8_literal("pitch")?));
}
}
"volume" | "envelope" => {
if volume.is_some() {
return Err(Diagnostic::error(
ErrorCode::E0201,
"sfx 'volume' / 'envelope' are aliases — pick one",
key_span,
));
}
// Remember which spelling the user chose so
// diagnostics below match their source.
let prop = if key.as_str() == "envelope" {
"envelope"
} else {
"volume"
};
volume_key = prop;
let vals = self.parse_byte_array(prop)?;
for v in &vals {
if *v > 15 {
return Err(Diagnostic::error(
ErrorCode::E0201,
format!("sfx '{prop}' entries must be 0-15, got {v}"),
key_span,
));
}
}
volume = Some(vals);
}
_ => {
return Err(Diagnostic::error(
ErrorCode::E0201,
format!("unknown sfx property '{key}'"),
key_span,
));
}
}
if *self.peek() == TokenKind::Comma {
self.advance();
}
}
self.expect(&TokenKind::RBrace)?;
let pitch_src = pitch_src.ok_or_else(|| {
Diagnostic::error(ErrorCode::E0201, "sfx requires 'pitch' property", start)
})?;
let volume = volume.ok_or_else(|| {
Diagnostic::error(
ErrorCode::E0201,
format!("sfx requires '{volume_key}' property (or its alias)"),
start,
)
})?;
if volume.is_empty() {
return Err(Diagnostic::error(
ErrorCode::E0201,
format!("sfx '{volume_key}' array must have at least one frame"),
start,
));
}
// Normalize to the legacy per-frame pitch array the rest of
// the compiler already consumes. Scalar pitches just repeat.
let pitch = match pitch_src {
PitchSrc::Scalar(v) => vec![v; volume.len()],
PitchSrc::Array(v) => {
if v.is_empty() {
return Err(Diagnostic::error(
ErrorCode::E0201,
"sfx 'pitch' array must have at least one frame",
start,
));
}
if v.len() != volume.len() {
return Err(Diagnostic::error(
ErrorCode::E0201,
format!(
"sfx 'pitch' and '{volume_key}' arrays must have the \
same length (pitch has {}, {volume_key} has {})",
v.len(),
volume.len()
),
start,
));
}
v
}
};
Ok(SfxDecl {
name,
duty,
pitch,
volume,
span: Span::new(start.file_id, start.start, self.current_span().end),
})
}
/// `music Name { duty, volume, repeat, tempo, notes }`.
///
/// Notes can be authored in two styles. The parser picks the style
/// based on whether a `tempo:` field is present:
///
/// **Raw form** (`tempo:` absent) — a flat list of `pitch, duration`
/// integer pairs. Every entry is a `u8` literal and pairs are
/// separated by commas:
/// ```text
/// music Theme {
/// notes: [
/// 37, 20, // C4 for 20 frames
/// 41, 20, // E4
/// 44, 20, // G4
/// 0, 10, // rest for 10 frames
/// ]
/// }
/// ```
///
/// **Note-name form** (`tempo:` present) — each entry is a note
/// name (`C4`, `Cs4`, `Db4`, …, `B5`) or `rest`, with an optional
/// per-note duration override. Entries are separated by commas,
/// and `tempo:` sets the default duration when none is given:
/// ```text
/// music Theme {
/// tempo: 20 // default frames per note
/// notes: [
/// C4, E4, G4, C5, // all use tempo (20 frames each)
/// G4 40, // this one is held twice as long
/// rest 10, // short rest
/// E4, C4
/// ]
/// }
/// ```
///
/// Integer literals still work inside the note-name form too —
/// useful for raw period-table indices when you don't feel like
/// spelling out a name.
fn parse_music_decl(&mut self) -> Result<MusicDecl, Diagnostic> {
let start = self.current_span();
self.expect(&TokenKind::KwMusic)?;
let (name, _) = self.expect_ident()?;
self.expect(&TokenKind::LBrace)?;
let mut duty: u8 = 2;
let mut volume: u8 = 10;
let mut loops: bool = true;
let mut tempo: Option<u8> = None;
// Defer note parsing until we've seen all the simple scalar
// fields, so `tempo:` can be declared after `notes:` if the
// user prefers that order — we stash the token position to
// rewind to.
let mut notes_pos: Option<(usize, Span)> = None;
while *self.peek() != TokenKind::RBrace && *self.peek() != TokenKind::Eof {
let (key, key_span) = self.expect_ident()?;
self.expect(&TokenKind::Colon)?;
match key.as_str() {
"duty" => {
duty = self.parse_u8_literal("duty")?;
if duty > 3 {
return Err(Diagnostic::error(
ErrorCode::E0201,
format!("music 'duty' must be 0-3, got {duty}"),
key_span,
));
}
}
"volume" => {
volume = self.parse_u8_literal("volume")?;
if volume > 15 {
return Err(Diagnostic::error(
ErrorCode::E0201,
format!("music 'volume' must be 0-15, got {volume}"),
key_span,
));
}
}
"tempo" => {
let t = self.parse_u8_literal("tempo")?;
if t == 0 {
return Err(Diagnostic::error(
ErrorCode::E0201,
"music 'tempo' must be >= 1 (frames per note)",
key_span,
));
}
tempo = Some(t);
}
"repeat" => match self.peek().clone() {
TokenKind::BoolLiteral(b) => {
self.advance();
loops = b;
}
_ => {
return Err(Diagnostic::error(
ErrorCode::E0201,
format!("expected bool for 'repeat', got '{}'", self.peek()),
key_span,
));
}
},
"notes" => {
if notes_pos.is_some() {
return Err(Diagnostic::error(
ErrorCode::E0501,
"duplicate 'notes' in music declaration",
key_span,
));
}
notes_pos = Some((self.pos, key_span));
// Skip past the notes list without parsing it yet
// — we need to know whether `tempo:` is set before
// picking between raw-pair form and note-name form.
self.skip_balanced_brackets()?;
}
_ => {
return Err(Diagnostic::error(
ErrorCode::E0201,
format!("unknown music property '{key}'"),
key_span,
));
}
}
if *self.peek() == TokenKind::Comma {
self.advance();
}
}
self.expect(&TokenKind::RBrace)?;
let (notes_token_pos, notes_span) = notes_pos.ok_or_else(|| {
Diagnostic::error(ErrorCode::E0201, "music requires 'notes' property", start)
})?;
// Rewind to the `[` of the notes list and parse it with the
// right format chosen by whether `tempo:` was set above.
let saved_pos = self.pos;
self.pos = notes_token_pos;
let notes = if let Some(default_dur) = tempo {
self.parse_named_notes(default_dur, notes_span)?
} else {
self.parse_flat_note_pairs(notes_span)?
};
// Restore the cursor past the closing brace so the outer
// program loop keeps marching through the source.
self.pos = saved_pos;
if notes.is_empty() {
return Err(Diagnostic::error(
ErrorCode::E0201,
"music 'notes' must contain at least one note",
start,
));
}
Ok(MusicDecl {
name,
duty,
volume,
loops,
notes,
span: Span::new(start.file_id, start.start, self.current_span().end),
})
}
/// Fast-forward `self.pos` past a matched `[` … `]` pair, used by
/// music parsing so the notes list can be re-scanned later once
/// `tempo:` presence is known.
fn skip_balanced_brackets(&mut self) -> Result<(), Diagnostic> {
self.expect(&TokenKind::LBracket)?;
let mut depth = 1i32;
while depth > 0 {
match self.peek().clone() {
TokenKind::LBracket => {
depth += 1;
self.advance();
}
TokenKind::RBracket => {
depth -= 1;
self.advance();
}
TokenKind::Eof => {
return Err(Diagnostic::error(
ErrorCode::E0201,
"unterminated '[' in music notes",
self.current_span(),
));
}
_ => {
self.advance();
}
}
}
Ok(())
}
/// Parse a legacy-form `notes: [pitch, duration, pitch, duration, ...]`
/// flat pair list. Used when the music block has no `tempo:` field.
fn parse_flat_note_pairs(&mut self, key_span: Span) -> Result<Vec<MusicNote>, Diagnostic> {
let flat = self.parse_byte_array("notes")?;
if flat.len() % 2 != 0 {
return Err(Diagnostic::error(
ErrorCode::E0201,
"music 'notes' must have an even number of entries \
(pitch, duration, pitch, duration, ...) when 'tempo' is not set",
key_span,
));
}
let mut out = Vec::with_capacity(flat.len() / 2);
for pair in flat.chunks(2) {
let pitch = pair[0];
let duration = pair[1];
if duration == 0 {
return Err(Diagnostic::error(
ErrorCode::E0201,
"music note duration must be >= 1",
key_span,
));
}
if pitch > 60 {
return Err(Diagnostic::error(
ErrorCode::E0201,
format!("music note pitch must be 0 (rest) or 1-60, got {pitch}"),
key_span,
));
}
out.push(MusicNote { pitch, duration });
}
Ok(out)
}
/// Parse a note-name form note list with entries like
/// `C4`, `Cs4 40`, `rest`, `rest 10`. Each entry is a pitch
/// (note-name identifier, `rest`, or integer literal) with an
/// optional inline duration; missing durations default to `tempo`.
/// Entries are comma-separated; trailing commas are allowed.
fn parse_named_notes(
&mut self,
default_dur: u8,
key_span: Span,
) -> Result<Vec<MusicNote>, Diagnostic> {
self.expect(&TokenKind::LBracket)?;
let mut out = Vec::new();
while *self.peek() != TokenKind::RBracket && *self.peek() != TokenKind::Eof {
// ── Parse the pitch ──
let pitch = match self.peek().clone() {
TokenKind::IntLiteral(v) => {
self.advance();
if v > 60 {
return Err(Diagnostic::error(
ErrorCode::E0201,
format!("music note pitch must be 0 (rest) or 1-60, got {v}"),
key_span,
));
}
v as u8
}
TokenKind::Ident(name) => {
let span = self.current_span();
self.advance();
crate::assets::note_name_to_index(&name).ok_or_else(|| {
Diagnostic::error(
ErrorCode::E0201,
format!(
"unknown note name '{name}'; use a name like C4/Cs4/Db4, \
'rest', or a numeric pitch index 0-60"
),
span,
)
})?
}
_ => {
return Err(Diagnostic::error(
ErrorCode::E0201,
format!(
"expected note name or pitch index in music notes, found '{}'",
self.peek()
),
self.current_span(),
));
}
};
// ── Optional duration override ──
//
// A bare integer literal before the next comma is the
// duration for this note. Otherwise use the block's tempo.
let duration = if let TokenKind::IntLiteral(v) = self.peek().clone() {
self.advance();
if v == 0 {
return Err(Diagnostic::error(
ErrorCode::E0201,
"music note duration must be >= 1",
key_span,
));
}
if v > 255 {
return Err(Diagnostic::error(
ErrorCode::E0201,
format!("music note duration {v} doesn't fit in a u8 (max 255)"),
key_span,
));
}
v as u8
} else {
default_dur
};
out.push(MusicNote { pitch, duration });
// Entries are comma-separated; trailing commas are fine.
if *self.peek() == TokenKind::Comma {
self.advance();
}
}
self.expect(&TokenKind::RBracket)?;
Ok(out)
}
/// Parse a `[byte, byte, ...]` array. Used by sfx/music property
/// parsing — the main `parse_asset_source` also does this, but
/// without the array-literal-only restriction we want here.
fn parse_byte_array(&mut self, prop: &str) -> Result<Vec<u8>, Diagnostic> {
self.expect(&TokenKind::LBracket)?;
let mut out = Vec::new();
while *self.peek() != TokenKind::RBracket && *self.peek() != TokenKind::Eof {
if let TokenKind::IntLiteral(v) = self.peek().clone() {
self.advance();
if v > 0xFF {
return Err(Diagnostic::error(
ErrorCode::E0201,
format!("'{prop}' entries must fit in a u8, got {v}"),
self.current_span(),
));
}
out.push(v as u8);
} else {
return Err(Diagnostic::error(
ErrorCode::E0201,
format!("expected byte value in '{prop}', found '{}'", self.peek()),
self.current_span(),
));
}
if *self.peek() == TokenKind::Comma {
self.advance();
}
}
self.expect(&TokenKind::RBracket)?;
Ok(out)
}
/// Parse a single u8 integer literal for a scalar property.
fn parse_u8_literal(&mut self, prop: &str) -> Result<u8, Diagnostic> {
match self.peek().clone() {
TokenKind::IntLiteral(v) => {
self.advance();
if v > 0xFF {
return Err(Diagnostic::error(
ErrorCode::E0201,
format!("'{prop}' must fit in a u8, got {v}"),
self.current_span(),
));
}
Ok(v as u8)
}
other => Err(Diagnostic::error(
ErrorCode::E0201,
format!("expected integer for '{prop}', got '{other}'"),
self.current_span(),
)),
}
}
fn parse_asset_source(&mut self) -> Result<AssetSource, Diagnostic> {
match self.peek() {
TokenKind::At => {
self.advance(); // consume '@'
let (kind, _) = self.expect_ident()?;
self.expect(&TokenKind::LParen)?;
let path = if let TokenKind::StringLiteral(s) = self.peek().clone() {
self.advance();
s
} else {
return Err(Diagnostic::error(
ErrorCode::E0201,
format!("expected string path, found '{}'", self.peek()),
self.current_span(),
));
};
self.expect(&TokenKind::RParen)?;
match kind.as_str() {
"chr" => Ok(AssetSource::Chr(path)),
"binary" => Ok(AssetSource::Binary(path)),
_ => Err(Diagnostic::error(
ErrorCode::E0201,
format!("unknown asset source kind '@{kind}'"),
self.current_span(),
)),
}
}
TokenKind::LBracket => {
self.advance();
let mut bytes = Vec::new();
while *self.peek() != TokenKind::RBracket && *self.peek() != TokenKind::Eof {
if let TokenKind::IntLiteral(v) = self.peek().clone() {
self.advance();
bytes.push(v as u8);
} else {
return Err(Diagnostic::error(
ErrorCode::E0201,
format!("expected byte value, found '{}'", self.peek()),
self.current_span(),
));
}
if *self.peek() == TokenKind::Comma {
self.advance();
}
}
self.expect(&TokenKind::RBracket)?;
Ok(AssetSource::Inline(bytes))
}
_ => Err(Diagnostic::error(
ErrorCode::E0201,
format!(
"expected asset source (@chr, @binary, or [...]), found '{}'",
self.peek()
),
self.current_span(),
)),
}
}
// ── Block ──
fn parse_block(&mut self) -> Result<Block, Diagnostic> {
let start = self.current_span();
self.expect(&TokenKind::LBrace)?;
let mut statements = Vec::new();
while *self.peek() != TokenKind::RBrace && *self.peek() != TokenKind::Eof {
statements.push(self.parse_statement()?);
// Allow optional `;` between statements for readability.
// Newlines are still the primary separator, but `;` lets
// users put short statements on the same line:
// `x += 1; y += 1`
while *self.peek() == TokenKind::Semicolon {
self.advance();
}
}
self.expect(&TokenKind::RBrace)?;
Ok(Block {
statements,
span: Span::new(start.file_id, start.start, self.current_span().end),
})
}
// ── Statements ──
fn parse_statement(&mut self) -> Result<Statement, Diagnostic> {
match self.peek().clone() {
TokenKind::KwFast | TokenKind::KwSlow | TokenKind::KwVar => {
let decl = self.parse_var_decl()?;
Ok(Statement::VarDecl(decl))
}
TokenKind::KwIf => self.parse_if(),
TokenKind::KwWhile => self.parse_while(),
TokenKind::KwFor => self.parse_for(),
TokenKind::KwMatch => self.parse_match(),
TokenKind::KwLoop => self.parse_loop(),
TokenKind::KwBreak => {
let span = self.current_span();
self.advance();
Ok(Statement::Break(span))
}
TokenKind::KwContinue => {
let span = self.current_span();
self.advance();
Ok(Statement::Continue(span))
}
TokenKind::KwReturn => {
let span = self.current_span();
self.advance();
let value = if *self.peek() == TokenKind::RBrace {
None
} else {
Some(self.parse_expr()?)
};
Ok(Statement::Return(value, span))
}
TokenKind::KwDraw => self.parse_draw(),
TokenKind::KwTransition => {
let span = self.current_span();
self.advance();
let (name, _) = self.expect_ident()?;
Ok(Statement::Transition(name, span))
}
TokenKind::KwWaitFrame => {
let span = self.current_span();
self.advance();
Ok(Statement::WaitFrame(span))
}
TokenKind::KwLoadBackground => {
let span = self.current_span();
self.advance();
let (name, _) = self.expect_ident()?;
Ok(Statement::LoadBackground(name, span))
}
TokenKind::KwSetPalette => {
let span = self.current_span();
self.advance();
let (name, _) = self.expect_ident()?;
Ok(Statement::SetPalette(name, span))
}
TokenKind::KwScroll => {
let span = self.current_span();
self.advance();
self.expect(&TokenKind::LParen)?;
let x = self.parse_expr()?;
self.expect(&TokenKind::Comma)?;
let y = self.parse_expr()?;
self.expect(&TokenKind::RParen)?;
Ok(Statement::Scroll(x, y, span))
}
TokenKind::KwDebug => self.parse_debug_statement(),
TokenKind::KwPlay => {
let span = self.current_span();
self.advance();
let (name, _) = self.expect_ident()?;
Ok(Statement::Play(name, span))
}
TokenKind::KwStartMusic => {
let span = self.current_span();
self.advance();
let (name, _) = self.expect_ident()?;
Ok(Statement::StartMusic(name, span))
}
TokenKind::KwStopMusic => {
let span = self.current_span();
self.advance();
Ok(Statement::StopMusic(span))
}
TokenKind::KwAsm => {
let span = self.current_span();
self.advance(); // KwAsm
// The lexer emits an AsmBody token after `asm` when it
// sees the opening brace. Consume it here.
if let TokenKind::AsmBody(body) = self.peek().clone() {
self.advance();
Ok(Statement::InlineAsm(body, span))
} else {
Err(Diagnostic::error(
ErrorCode::E0201,
"expected `{` after `asm`",
self.current_span(),
))
}
}
TokenKind::KwRaw => {
// `raw asm { ... }` — verbatim bytes, no `{var}`
// substitution.
let span = self.current_span();
self.advance(); // KwRaw
self.expect(&TokenKind::KwAsm)?;
if let TokenKind::AsmBody(body) = self.peek().clone() {
self.advance();
Ok(Statement::RawAsm(body, span))
} else {
Err(Diagnostic::error(
ErrorCode::E0201,
"expected `{` after `raw asm`",
self.current_span(),
))
}
}
TokenKind::Ident(_) => self.parse_assign_or_call(),
_ => Err(Diagnostic::error(
ErrorCode::E0201,
format!("unexpected token '{}' in statement position", self.peek()),
self.current_span(),
)),
}
}
fn parse_if(&mut self) -> Result<Statement, Diagnostic> {
let start = self.current_span();
self.expect(&TokenKind::KwIf)?;
let saved = self.restrict_struct_literals;
self.restrict_struct_literals = true;
let condition = self.parse_expr()?;
self.restrict_struct_literals = saved;
let then_block = self.parse_block()?;
let mut else_ifs = Vec::new();
let mut else_block = None;
while *self.peek() == TokenKind::KwElse {
self.advance();
if *self.peek() == TokenKind::KwIf {
self.advance();
self.restrict_struct_literals = true;
let cond = self.parse_expr()?;
self.restrict_struct_literals = saved;
let block = self.parse_block()?;
else_ifs.push((cond, block));
} else {
else_block = Some(self.parse_block()?);
break;
}
}
Ok(Statement::If(
condition, then_block, else_ifs, else_block, start,
))
}
fn parse_while(&mut self) -> Result<Statement, Diagnostic> {
let start = self.current_span();
self.expect(&TokenKind::KwWhile)?;
let saved = self.restrict_struct_literals;
self.restrict_struct_literals = true;
let condition = self.parse_expr()?;
self.restrict_struct_literals = saved;
let body = self.parse_block()?;
Ok(Statement::While(condition, body, start))
}
fn parse_loop(&mut self) -> Result<Statement, Diagnostic> {
let start = self.current_span();
self.expect(&TokenKind::KwLoop)?;
let body = self.parse_block()?;
Ok(Statement::Loop(body, start))
}
/// Parse `match expr { pat => { body }, pat => { body }, _ => { body } }`.
/// Desugars to a chain of `if expr == pat { body } else if ...`
/// at parse time — no dedicated AST variant needed.
fn parse_match(&mut self) -> Result<Statement, Diagnostic> {
let start = self.current_span();
self.expect(&TokenKind::KwMatch)?;
let saved = self.restrict_struct_literals;
self.restrict_struct_literals = true;
let scrutinee = self.parse_expr()?;
self.restrict_struct_literals = saved;
self.expect(&TokenKind::LBrace)?;
let mut arms: Vec<(Expr, Block)> = Vec::new();
let mut default: Option<Block> = None;
while *self.peek() != TokenKind::RBrace && *self.peek() != TokenKind::Eof {
// A default arm is `_ => { ... }`.
if let TokenKind::Ident(name) = self.peek().clone() {
if name == "_" {
self.advance();
self.expect(&TokenKind::FatArrow)?;
let body = self.parse_block()?;
default = Some(body);
if *self.peek() == TokenKind::Comma {
self.advance();
}
continue;
}
}
let pat_span = self.current_span();
let pat = self.parse_expr()?;
self.expect(&TokenKind::FatArrow)?;
let body = self.parse_block()?;
// Build `scrutinee == pat` as the branch condition.
let cond = Expr::BinaryOp(
Box::new(scrutinee.clone()),
BinOp::Eq,
Box::new(pat),
pat_span,
);
arms.push((cond, body));
if *self.peek() == TokenKind::Comma {
self.advance();
}
}
self.expect(&TokenKind::RBrace)?;
if arms.is_empty() {
// `match x { _ => body }` or empty match — emit the
// default block directly, or an empty no-op.
if let Some(body) = default {
return Ok(Statement::If(
Expr::BoolLiteral(true, start),
body,
Vec::new(),
None,
start,
));
}
return Ok(Statement::If(
Expr::BoolLiteral(false, start),
Block {
statements: Vec::new(),
span: start,
},
Vec::new(),
None,
start,
));
}
// Build an if/else-if chain. The first arm becomes the
// `then` block; subsequent arms become `else if` entries;
// the default arm (if any) becomes the final `else`.
let (first_cond, first_body) = arms.remove(0);
Ok(Statement::If(first_cond, first_body, arms, default, start))
}
fn parse_for(&mut self) -> Result<Statement, Diagnostic> {
let start = self.current_span();
self.expect(&TokenKind::KwFor)?;
let (var, _) = self.expect_ident()?;
self.expect(&TokenKind::KwIn)?;
let saved = self.restrict_struct_literals;
self.restrict_struct_literals = true;
let start_expr = self.parse_expr()?;
self.expect(&TokenKind::DotDot)?;
let end_expr = self.parse_expr()?;
self.restrict_struct_literals = saved;
let body = self.parse_block()?;
Ok(Statement::For {
var,
start: start_expr,
end: end_expr,
body,
span: start,
})
}
fn parse_draw(&mut self) -> Result<Statement, Diagnostic> {
let start = self.current_span();
self.expect(&TokenKind::KwDraw)?;
let (sprite_name, _) = self.expect_ident()?;
let mut x = None;
let mut y = None;
let mut frame = None;
// Parse keyword arguments: at: (x, y), frame: n
// Only consume an ident if it's followed by ':', indicating a keyword arg.
while matches!(self.peek(), TokenKind::Ident(_))
&& self.peek_at_offset(1) == Some(&TokenKind::Colon)
{
let (key, _) = self.expect_ident()?;
self.expect(&TokenKind::Colon)?;
match key.as_str() {
"at" => {
self.expect(&TokenKind::LParen)?;
x = Some(self.parse_expr()?);
self.expect(&TokenKind::Comma)?;
y = Some(self.parse_expr()?);
self.expect(&TokenKind::RParen)?;
}
"frame" => {
frame = Some(self.parse_expr()?);
}
_ => {
return Err(Diagnostic::error(
ErrorCode::E0201,
format!("unknown draw property '{key}'"),
self.current_span(),
));
}
}
}
let x = x.ok_or_else(|| {
Diagnostic::error(ErrorCode::E0201, "draw requires 'at: (x, y)'", start)
})?;
let y = y.ok_or_else(|| {
Diagnostic::error(ErrorCode::E0201, "draw requires 'at: (x, y)'", start)
})?;
Ok(Statement::Draw(DrawStmt {
sprite_name,
x,
y,
frame,
span: start,
}))
}
/// Parse debug.log(...) or debug.assert(...)
fn parse_debug_statement(&mut self) -> Result<Statement, Diagnostic> {
let start = self.current_span();
self.expect(&TokenKind::KwDebug)?;
self.expect(&TokenKind::Dot)?;
let (method, _) = self.expect_ident()?;
self.expect(&TokenKind::LParen)?;
match method.as_str() {
"log" => {
let mut args = Vec::new();
while *self.peek() != TokenKind::RParen && *self.peek() != TokenKind::Eof {
args.push(self.parse_expr()?);
if *self.peek() == TokenKind::Comma {
self.advance();
}
}
self.expect(&TokenKind::RParen)?;
Ok(Statement::DebugLog(args, start))
}
"assert" => {
let cond = self.parse_expr()?;
self.expect(&TokenKind::RParen)?;
Ok(Statement::DebugAssert(cond, start))
}
_ => Err(Diagnostic::error(
ErrorCode::E0201,
format!("unknown debug method '{method}' (expected 'log' or 'assert')"),
start,
)),
}
}
fn parse_assign_or_call(&mut self) -> Result<Statement, Diagnostic> {
let start = self.current_span();
let (name, _) = self.expect_ident()?;
// Check for button.X pattern
if name == "button" && *self.peek() == TokenKind::Dot {
// This shouldn't be a statement on its own
return Err(Diagnostic::error(
ErrorCode::E0201,
"button read is an expression, not a statement",
start,
));
}
match self.peek().clone() {
TokenKind::Assign => {
self.advance();
let value = self.parse_expr()?;
Ok(Statement::Assign(
LValue::Var(name),
AssignOp::Assign,
value,
start,
))
}
TokenKind::PlusAssign => {
self.advance();
let value = self.parse_expr()?;
Ok(Statement::Assign(
LValue::Var(name),
AssignOp::PlusAssign,
value,
start,
))
}
TokenKind::MinusAssign => {
self.advance();
let value = self.parse_expr()?;
Ok(Statement::Assign(
LValue::Var(name),
AssignOp::MinusAssign,
value,
start,
))
}
TokenKind::AmpAssign => {
self.advance();
let value = self.parse_expr()?;
Ok(Statement::Assign(
LValue::Var(name),
AssignOp::AmpAssign,
value,
start,
))
}
TokenKind::PipeAssign => {
self.advance();
let value = self.parse_expr()?;
Ok(Statement::Assign(
LValue::Var(name),
AssignOp::PipeAssign,
value,
start,
))
}
TokenKind::CaretAssign => {
self.advance();
let value = self.parse_expr()?;
Ok(Statement::Assign(
LValue::Var(name),
AssignOp::CaretAssign,
value,
start,
))
}
TokenKind::ShiftLeftAssign => {
self.advance();
let value = self.parse_expr()?;
Ok(Statement::Assign(
LValue::Var(name),
AssignOp::ShiftLeftAssign,
value,
start,
))
}
TokenKind::ShiftRightAssign => {
self.advance();
let value = self.parse_expr()?;
Ok(Statement::Assign(
LValue::Var(name),
AssignOp::ShiftRightAssign,
value,
start,
))
}
TokenKind::LBracket => {
// Array index assignment: name[index] = value
self.advance();
let index = self.parse_expr()?;
self.expect(&TokenKind::RBracket)?;
let op = self.parse_assign_op()?;
let value = self.parse_expr()?;
Ok(Statement::Assign(
LValue::ArrayIndex(name, Box::new(index)),
op,
value,
start,
))
}
TokenKind::Dot => {
// Field assignment: name.field = value
self.advance();
let (field, _) = self.expect_ident()?;
let op = self.parse_assign_op()?;
let value = self.parse_expr()?;
Ok(Statement::Assign(
LValue::Field(name, field),
op,
value,
start,
))
}
TokenKind::LParen => {
// Function call
self.advance();
let mut args = Vec::new();
while *self.peek() != TokenKind::RParen && *self.peek() != TokenKind::Eof {
args.push(self.parse_expr()?);
if *self.peek() == TokenKind::Comma {
self.advance();
}
}
self.expect(&TokenKind::RParen)?;
Ok(Statement::Call(name, args, start))
}
_ => Err(Diagnostic::error(
ErrorCode::E0201,
format!(
"expected assignment operator or '(' after identifier, found '{}'",
self.peek()
),
self.current_span(),
)),
}
}
fn parse_assign_op(&mut self) -> Result<AssignOp, Diagnostic> {
match self.peek() {
TokenKind::Assign => {
self.advance();
Ok(AssignOp::Assign)
}
TokenKind::PlusAssign => {
self.advance();
Ok(AssignOp::PlusAssign)
}
TokenKind::MinusAssign => {
self.advance();
Ok(AssignOp::MinusAssign)
}
TokenKind::AmpAssign => {
self.advance();
Ok(AssignOp::AmpAssign)
}
TokenKind::PipeAssign => {
self.advance();
Ok(AssignOp::PipeAssign)
}
TokenKind::CaretAssign => {
self.advance();
Ok(AssignOp::CaretAssign)
}
TokenKind::ShiftLeftAssign => {
self.advance();
Ok(AssignOp::ShiftLeftAssign)
}
TokenKind::ShiftRightAssign => {
self.advance();
Ok(AssignOp::ShiftRightAssign)
}
_ => Err(Diagnostic::error(
ErrorCode::E0201,
format!("expected assignment operator, found '{}'", self.peek()),
self.current_span(),
)),
}
}
// ── Type parsing ──
fn parse_type(&mut self) -> Result<NesType, Diagnostic> {
let base = match self.peek().clone() {
TokenKind::KwU8 => {
self.advance();
NesType::U8
}
TokenKind::KwI8 => {
self.advance();
NesType::I8
}
TokenKind::KwU16 => {
self.advance();
NesType::U16
}
TokenKind::KwBool => {
self.advance();
NesType::Bool
}
TokenKind::Ident(name) => {
// User-declared struct types are referenced by name.
// The analyzer validates that the name exists.
self.advance();
NesType::Struct(name)
}
_ => {
return Err(Diagnostic::error(
ErrorCode::E0201,
format!("expected type, found '{}'", self.peek()),
self.current_span(),
));
}
};
// Check for array suffix [N]
if *self.peek() == TokenKind::LBracket {
self.advance();
if let TokenKind::IntLiteral(size) = self.peek().clone() {
self.advance();
self.expect(&TokenKind::RBracket)?;
Ok(NesType::Array(Box::new(base), size))
} else {
Err(Diagnostic::error(
ErrorCode::E0201,
"expected array size",
self.current_span(),
))
}
} else {
Ok(base)
}
}
// ── Expression parsing (Pratt / precedence climbing) ──
fn parse_expr(&mut self) -> Result<Expr, Diagnostic> {
self.parse_or_expr()
}
fn parse_or_expr(&mut self) -> Result<Expr, Diagnostic> {
let mut left = self.parse_and_expr()?;
while *self.peek() == TokenKind::KwOr {
let span = self.current_span();
self.advance();
let right = self.parse_and_expr()?;
left = Expr::BinaryOp(Box::new(left), BinOp::Or, Box::new(right), span);
}
Ok(left)
}
fn parse_and_expr(&mut self) -> Result<Expr, Diagnostic> {
let mut left = self.parse_comparison()?;
while *self.peek() == TokenKind::KwAnd {
let span = self.current_span();
self.advance();
let right = self.parse_comparison()?;
left = Expr::BinaryOp(Box::new(left), BinOp::And, Box::new(right), span);
}
Ok(left)
}
fn parse_comparison(&mut self) -> Result<Expr, Diagnostic> {
let mut left = self.parse_bitwise_or()?;
loop {
let (op, span) = match self.peek() {
TokenKind::Eq => (BinOp::Eq, self.current_span()),
TokenKind::NotEq => (BinOp::NotEq, self.current_span()),
TokenKind::Lt => (BinOp::Lt, self.current_span()),
TokenKind::Gt => (BinOp::Gt, self.current_span()),
TokenKind::LtEq => (BinOp::LtEq, self.current_span()),
TokenKind::GtEq => (BinOp::GtEq, self.current_span()),
_ => break,
};
self.advance();
let right = self.parse_bitwise_or()?;
left = Expr::BinaryOp(Box::new(left), op, Box::new(right), span);
}
Ok(left)
}
fn parse_bitwise_or(&mut self) -> Result<Expr, Diagnostic> {
let mut left = self.parse_bitwise_xor()?;
while *self.peek() == TokenKind::Pipe {
let span = self.current_span();
self.advance();
let right = self.parse_bitwise_xor()?;
left = Expr::BinaryOp(Box::new(left), BinOp::BitwiseOr, Box::new(right), span);
}
Ok(left)
}
fn parse_bitwise_xor(&mut self) -> Result<Expr, Diagnostic> {
let mut left = self.parse_bitwise_and()?;
while *self.peek() == TokenKind::Caret {
let span = self.current_span();
self.advance();
let right = self.parse_bitwise_and()?;
left = Expr::BinaryOp(Box::new(left), BinOp::BitwiseXor, Box::new(right), span);
}
Ok(left)
}
fn parse_bitwise_and(&mut self) -> Result<Expr, Diagnostic> {
let mut left = self.parse_shift()?;
while *self.peek() == TokenKind::Amp {
let span = self.current_span();
self.advance();
let right = self.parse_shift()?;
left = Expr::BinaryOp(Box::new(left), BinOp::BitwiseAnd, Box::new(right), span);
}
Ok(left)
}
fn parse_shift(&mut self) -> Result<Expr, Diagnostic> {
let mut left = self.parse_additive()?;
loop {
let (op, span) = match self.peek() {
TokenKind::ShiftLeft => (BinOp::ShiftLeft, self.current_span()),
TokenKind::ShiftRight => (BinOp::ShiftRight, self.current_span()),
_ => break,
};
self.advance();
let right = self.parse_additive()?;
left = Expr::BinaryOp(Box::new(left), op, Box::new(right), span);
}
Ok(left)
}
fn parse_additive(&mut self) -> Result<Expr, Diagnostic> {
let mut left = self.parse_multiplicative()?;
loop {
let (op, span) = match self.peek() {
TokenKind::Plus => (BinOp::Add, self.current_span()),
TokenKind::Minus => (BinOp::Sub, self.current_span()),
_ => break,
};
self.advance();
let right = self.parse_multiplicative()?;
left = Expr::BinaryOp(Box::new(left), op, Box::new(right), span);
}
Ok(left)
}
fn parse_multiplicative(&mut self) -> Result<Expr, Diagnostic> {
let mut left = self.parse_unary()?;
loop {
let (op, span) = match self.peek() {
TokenKind::Star => (BinOp::Mul, self.current_span()),
TokenKind::Slash => (BinOp::Div, self.current_span()),
TokenKind::Percent => (BinOp::Mod, self.current_span()),
_ => break,
};
self.advance();
let right = self.parse_unary()?;
left = Expr::BinaryOp(Box::new(left), op, Box::new(right), span);
}
Ok(left)
}
fn parse_unary(&mut self) -> Result<Expr, Diagnostic> {
let expr = match self.peek().clone() {
TokenKind::Minus => {
let span = self.current_span();
self.advance();
let expr = self.parse_unary()?;
Expr::UnaryOp(UnaryOp::Negate, Box::new(expr), span)
}
TokenKind::KwNot => {
let span = self.current_span();
self.advance();
let expr = self.parse_unary()?;
Expr::UnaryOp(UnaryOp::Not, Box::new(expr), span)
}
TokenKind::Tilde => {
let span = self.current_span();
self.advance();
let expr = self.parse_unary()?;
Expr::UnaryOp(UnaryOp::BitNot, Box::new(expr), span)
}
_ => self.parse_primary()?,
};
self.parse_cast_suffix(expr)
}
fn parse_cast_suffix(&mut self, mut expr: Expr) -> Result<Expr, Diagnostic> {
while *self.peek() == TokenKind::KwAs {
let span = self.current_span();
self.advance();
let target_type = self.parse_type()?;
expr = Expr::Cast(Box::new(expr), target_type, span);
}
Ok(expr)
}
fn parse_primary(&mut self) -> Result<Expr, Diagnostic> {
match self.peek().clone() {
TokenKind::IntLiteral(v) => {
let span = self.current_span();
self.advance();
Ok(Expr::IntLiteral(v, span))
}
TokenKind::BoolLiteral(v) => {
let span = self.current_span();
self.advance();
Ok(Expr::BoolLiteral(v, span))
}
TokenKind::Ident(name) => {
let span = self.current_span();
self.advance();
// Check for button.X (player 1 default)
if name == "button" && *self.peek() == TokenKind::Dot {
self.advance();
let (button, _) = self.expect_name()?;
return Ok(Expr::ButtonRead(None, button, span));
}
// Check for p1.button.X / p2.button.X
if (name == "p1" || name == "p2") && *self.peek() == TokenKind::Dot {
self.advance();
// Expect 'button'
if let TokenKind::Ident(kw) = self.peek().clone() {
if kw == "button" {
self.advance();
self.expect(&TokenKind::Dot)?;
let (button, _) = self.expect_name()?;
let player = if name == "p1" {
Some(Player::P1)
} else {
Some(Player::P2)
};
return Ok(Expr::ButtonRead(player, button, span));
}
}
return Err(Diagnostic::error(
ErrorCode::E0201,
"expected 'button' after 'p1.' or 'p2.'",
self.current_span(),
));
}
// Check for array index
if *self.peek() == TokenKind::LBracket {
self.advance();
let index = self.parse_expr()?;
self.expect(&TokenKind::RBracket)?;
return Ok(Expr::ArrayIndex(name, Box::new(index), span));
}
// Check for function call
if *self.peek() == TokenKind::LParen {
self.advance();
let mut args = Vec::new();
while *self.peek() != TokenKind::RParen && *self.peek() != TokenKind::Eof {
args.push(self.parse_expr()?);
if *self.peek() == TokenKind::Comma {
self.advance();
}
}
self.expect(&TokenKind::RParen)?;
return Ok(Expr::Call(name, args, span));
}
// Check for field access: `name.field`
if *self.peek() == TokenKind::Dot {
self.advance();
let (field, _) = self.expect_ident()?;
return Ok(Expr::FieldAccess(name, field, span));
}
// Check for struct literal: `Name { field: expr, ... }`.
// Disabled in condition contexts to keep parsing
// unambiguous for `if`/`while`/`for`.
if !self.restrict_struct_literals && *self.peek() == TokenKind::LBrace {
self.advance();
let mut fields = Vec::new();
while *self.peek() != TokenKind::RBrace && *self.peek() != TokenKind::Eof {
let (field_name, _) = self.expect_ident()?;
self.expect(&TokenKind::Colon)?;
// Struct literal field values can contain
// their own nested struct literals, so we
// temporarily allow them regardless of the
// outer restriction.
let saved = self.restrict_struct_literals;
self.restrict_struct_literals = false;
let value = self.parse_expr()?;
self.restrict_struct_literals = saved;
fields.push((field_name, value));
if *self.peek() == TokenKind::Comma {
self.advance();
} else if *self.peek() != TokenKind::RBrace {
return Err(Diagnostic::error(
ErrorCode::E0201,
"expected ',' or '}' in struct literal",
self.current_span(),
));
}
}
self.expect(&TokenKind::RBrace)?;
return Ok(Expr::StructLiteral(name, fields, span));
}
Ok(Expr::Ident(name, span))
}
TokenKind::LBracket => {
let span = self.current_span();
self.advance();
let mut elements = Vec::new();
while *self.peek() != TokenKind::RBracket && *self.peek() != TokenKind::Eof {
elements.push(self.parse_expr()?);
if *self.peek() == TokenKind::Comma {
self.advance();
}
}
self.expect(&TokenKind::RBracket)?;
Ok(Expr::ArrayLiteral(elements, span))
}
TokenKind::LParen => {
self.advance();
let expr = self.parse_expr()?;
self.expect(&TokenKind::RParen)?;
Ok(expr)
}
_ => Err(Diagnostic::error(
ErrorCode::E0201,
format!("expected expression, found '{}'", self.peek()),
self.current_span(),
)),
}
}
}
/// Convert a tilemap authored as rows of characters into the flat
/// byte array the nametable expects. Each row is up to 32 characters
/// wide; shorter rows pad with the default tile 0, longer rows are an
/// error. The full tile map is 30 rows × 32 cols = 960 bytes; fewer
/// rows are zero-padded (the analyzer does the final padding, so we
/// just emit whatever the user declared without the trailing zeros
/// here).
fn tilemap_to_bytes(
bg_name: &str,
rows: &[String],
legend: &std::collections::HashMap<char, u8>,
span: Span,
) -> Result<Vec<u8>, Diagnostic> {
if rows.len() > 30 {
return Err(Diagnostic::error(
ErrorCode::E0201,
format!(
"background '{bg_name}' tilemap has {} rows; maximum is 30",
rows.len()
),
span,
));
}
let mut out = Vec::with_capacity(rows.len() * 32);
for (ry, row) in rows.iter().enumerate() {
let chars: Vec<char> = row.chars().collect();
if chars.len() > 32 {
return Err(Diagnostic::error(
ErrorCode::E0201,
format!(
"background '{bg_name}' tilemap row {ry} has {} cells; \
maximum is 32",
chars.len()
),
span,
));
}
for (rx, ch) in chars.iter().enumerate() {
let tile = legend.get(ch).copied().ok_or_else(|| {
Diagnostic::error(
ErrorCode::E0201,
format!(
"background '{bg_name}' tilemap cell ({rx}, {ry}) uses \
character '{ch}' which is not in the legend"
),
span,
)
})?;
out.push(tile);
}
// Pad the remainder of this row with tile 0 so subsequent
// rows land at the right column in the flat array.
out.resize(out.len() + (32 - chars.len()), 0);
}
Ok(out)
}
/// Convert a `palette_map:` grid (rows of digit characters `0`-`3`,
/// one per 16×16 metatile) into the 64-byte attribute table the PPU
/// expects.
///
/// The attribute layout is notoriously awkward: each attribute byte
/// covers a 32×32-pixel region (four 16×16 metatiles) packed as
/// `BR BL TR TL` — top-left in the low bits, bottom-right in the high
/// bits. The attribute table is a fixed 8×8 = 64 bytes covering 16
/// metatile rows, even though only the top 15 (the visible 240
/// scanlines) render on screen. Programs may declare up to 16 rows
/// so the off-screen half picks up sensible attribute bytes; if
/// exactly 15 are given, the parser auto-replicates row 14 down
/// into row 15 so the last attribute byte stays consistent with
/// what's visible.
fn palette_map_to_attrs(bg_name: &str, rows: &[String], span: Span) -> Result<Vec<u8>, Diagnostic> {
if rows.len() > 16 {
return Err(Diagnostic::error(
ErrorCode::E0201,
format!(
"background '{bg_name}' palette_map has {} rows; maximum is 16 \
(15 visible metatile rows + 1 off-screen row for the bottom \
half of the last attribute byte)",
rows.len()
),
span,
));
}
// Build a dense 16×16 grid of sub-palette indices (rows beyond
// declared are 0). Using 16 metatile rows keeps the packing loop
// branch-free.
let mut grid = [[0u8; 16]; 16];
for (ry, row) in rows.iter().enumerate() {
let chars: Vec<char> = row.chars().collect();
if chars.len() > 16 {
return Err(Diagnostic::error(
ErrorCode::E0201,
format!(
"background '{bg_name}' palette_map row {ry} has {} cells; \
maximum is 16 (one per 16×16 metatile)",
chars.len()
),
span,
));
}
for (rx, ch) in chars.iter().enumerate() {
let idx = match ch {
'0' => 0u8,
'1' => 1,
'2' => 2,
'3' => 3,
' ' | '.' => 0,
other => {
return Err(Diagnostic::error(
ErrorCode::E0201,
format!(
"background '{bg_name}' palette_map cell ({rx}, {ry}) \
has '{other}'; must be a sub-palette digit '0'-'3'"
),
span,
));
}
};
grid[ry][rx] = idx;
}
}
// If the user gave exactly 15 rows, replicate row 14 into row 15
// so the last attribute byte's bottom-half picks up the same
// sub-palette as the visible bottom of the screen. Users who
// want explicit control over the off-screen row can supply all
// 16 rows.
if rows.len() == 15 {
grid[15] = grid[14];
}
// Pack into the 8×8 attribute table. Each attribute byte covers
// a 2×2 block of metatiles:
// bits 0-1 = top-left (grid[ay*2 ][ax*2 ])
// bits 2-3 = top-right (grid[ay*2 ][ax*2+1])
// bits 4-5 = bottom-left (grid[ay*2+1][ax*2 ])
// bits 6-7 = bottom-right (grid[ay*2+1][ax*2+1])
let mut out = vec![0u8; 64];
for ay in 0..8 {
for ax in 0..8 {
let tl = grid[ay * 2][ax * 2] & 0b11;
let tr = grid[ay * 2][ax * 2 + 1] & 0b11;
let bl = grid[ay * 2 + 1][ax * 2] & 0b11;
let br = grid[ay * 2 + 1][ax * 2 + 1] & 0b11;
out[ay * 8 + ax] = tl | (tr << 2) | (bl << 4) | (br << 6);
}
}
Ok(out)
}
pub fn parse(source: &str) -> (Option<Program>, Vec<Diagnostic>) {
let (tokens, lex_diags) = crate::lexer::lex(source);
if lex_diags.iter().any(Diagnostic::is_error) {
return (None, lex_diags);
}
let (program, mut parse_diags) = Parser::new(tokens).parse();
let mut all_diags = lex_diags;
all_diags.append(&mut parse_diags);
(program, all_diags)
}