mirror of
https://github.com/imjasonh/nescript
synced 2026-07-08 17:06:04 +00:00
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
3050 lines
113 KiB
Rust
3050 lines
113 KiB
Rust
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)
|
||
}
|