diff --git a/src/error.rs b/src/error.rs new file mode 100644 index 0000000..0d02b2f --- /dev/null +++ b/src/error.rs @@ -0,0 +1,84 @@ +use std::{error::Error, fmt::Display, ops::Range}; + +use crate::world::World; + +pub type CompileResult = Result, Vec>; + +#[derive(Debug)] +pub struct CompileMessage { + pub level: Level, + pub message: String, + pub location: Location, +} + +#[derive(Debug)] +pub enum Level { + Error, + Warning, +} + +#[derive(Debug)] +pub struct Location { + pub file_path: Option, + pub board: Option, + pub stat: Option, + pub span: Option>, +} + +impl Display for CompileMessage { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + let level = match self.level { + Level::Error => "Error", + Level::Warning => "Warning", + }; + write!(f, "{}: {}", level, self.message) + } +} + +impl Error for CompileMessage {} + +impl CompileMessage { + pub fn rich_format(&self, world: &World) -> String { + format!( + "{}\n{}", + self.to_string(), + self.location.rich_format(&world) + ) + } +} + +impl Location { + pub fn rich_format(&self, world: &World) -> String { + let board = self.board.map(|i| &world.boards[i]); + let stat = self.stat.and_then(|i| board.map(|b| &b.stats[i])); + + let mut parts: Vec> = vec![]; + parts.push(self.file_path.clone()); + parts.push(board.map(|board| board.name.clone())); + parts.push(stat.map(|stat| format!("stat({},{})", stat.x, stat.y))); + parts.push(self.span.as_ref().and_then(|span| { + stat.map(|stat| { + let mut line = 1; + let mut col = 1; + for c in (&stat.code[0..span.start]).chars() { + if c == '\n' { + line += 1; + col = 1; + } else { + col += 1; + } + } + format!("line {line}, column {col}") + }) + })); + + while parts.len() > 0 && parts[parts.len() - 1].is_none() { + parts.pop(); + } + let parts: Vec<_> = parts + .into_iter() + .map(|x| x.unwrap_or("???".into())) + .collect(); + parts.join(", ") + } +} diff --git a/src/labels/parse.rs b/src/labels/parse.rs index 7d36a55..6650922 100644 --- a/src/labels/parse.rs +++ b/src/labels/parse.rs @@ -3,7 +3,11 @@ use std::ops::Range; use compact_str::CompactString; use grammar::Tag; -use crate::{peg::ParseState, world::Stat}; +use crate::{ + error::{CompileMessage, Level, Location}, + peg::ParseState, + world::Stat, +}; pub type ParsedStat = Vec; @@ -24,7 +28,7 @@ pub struct LabelName { pub local: Option, } -pub fn parse_stat_labels(stat: &Stat) -> ParsedStat { +pub fn parse_stat_labels(stat: &Stat) -> (ParsedStat, Vec) { let code = &stat.code; let mut parser = ParseState::new(code); assert!( @@ -33,6 +37,25 @@ pub fn parse_stat_labels(stat: &Stat) -> ParsedStat { code ); + let mut messages = vec![]; + for cap in parser.walk_captures() { + match cap.kind() { + Tag::WarnTrailing => { + messages.push(CompileMessage { + level: Level::Warning, + message: "Trailing characters at end of line".into(), + location: Location { + file_path: None, + board: None, + stat: None, + span: None, + }, + }); + } + _ => {} + } + } + // Find all #Label captures and record which ones were references let mut label_captures = vec![]; for cap in parser.captures() { @@ -83,7 +106,7 @@ pub fn parse_stat_labels(stat: &Stat) -> ParsedStat { if last_index < code.len() { result.push(Chunk::Verbatim(code[last_index..code.len()].into())); } - result + (result, messages) } mod grammar { @@ -139,7 +162,7 @@ mod grammar { "zap" sp message ) / message // shorthand send - ) s eol; // note: this allows trailing (ignored) whitespace + ) warn_trailing eol; // // Common definitions @@ -205,6 +228,9 @@ mod grammar { &'t'..'w' ("tiger" / "torch" / "transporter" / "water") ) eow; + // Warnings + warn_trailing = (#WarnTrailing:(!eol ANY)+)?; // TODO: Document precedence rules + // // Generic helpers // diff --git a/src/labels/process.rs b/src/labels/process.rs index fe870a2..7957050 100644 --- a/src/labels/process.rs +++ b/src/labels/process.rs @@ -1,19 +1,31 @@ use compact_str::CompactString; use rustc_hash::FxHashMap; -use crate::world::Board; +use crate::{ + error::{CompileMessage, CompileResult}, + world::Board, +}; use super::{ parse::{Chunk, ParsedStat, parse_stat_labels}, sanitize::Registry, }; -pub fn process_labels(board: &mut Board) { +pub fn process_labels(board: &mut Board) -> CompileResult { // Parse stats into chunks + let mut messages: Vec = vec![]; let mut stats: Vec = board .stats .iter() - .map(|stat| parse_stat_labels(&stat)) + .enumerate() + .map(|(index, stat)| { + let (parsed_stat, mut new_messages) = parse_stat_labels(&stat); + new_messages + .iter_mut() + .for_each(|m| m.location.stat = Some(index)); + messages.extend(new_messages); + parsed_stat + }) .collect(); let mut registry = Registry::new(); @@ -39,6 +51,8 @@ pub fn process_labels(board: &mut Board) { .collect(); old_stat.code = new_code; } + + Ok(messages) } /// Resolve ".local" labels to "name.local" form. @@ -244,28 +258,28 @@ mod test { #[test] fn test_label_sanitization() { let mut board = board_from_text("tests/labels/sanitize.txt"); - process_labels(&mut board); + let _ = process_labels(&mut board); assert_snapshot!(board_to_text(board)); } #[test] fn test_anonymous_labels() { let mut board = board_from_text("tests/labels/anonymous.txt"); - process_labels(&mut board); + let _ = process_labels(&mut board); assert_snapshot!(board_to_text(board)); } #[test] fn test_local_labels() { let mut board = board_from_text("tests/labels/local.txt"); - process_labels(&mut board); + let _ = process_labels(&mut board); assert_snapshot!(board_to_text(board)); } #[test] fn test_namespaces() { let mut board = board_from_text("tests/labels/namespaces.txt"); - process_labels(&mut board); + let _ = process_labels(&mut board); assert_snapshot!(board_to_text(board)); } } diff --git a/src/main.rs b/src/main.rs index 2c76a81..8fbf229 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,4 +1,5 @@ mod encoding; +mod error; mod labels; mod peg; mod preprocess; @@ -36,8 +37,22 @@ fn main() -> Result<(), Box> { } // Resolve labels to proper ZZT-OOP - for mut board in &mut world.boards { - process_labels(&mut board); + let mut messages = vec![]; + for (i, mut board) in world.boards.iter_mut().enumerate() { + let mut result = process_labels(&mut board).unwrap(); + for message in result.iter_mut() { + message.location.board = Some(i); + message.location.file_path = Some(world_filename.clone()); + } + messages.extend(result); + } + + // Print diagnostics + for (i, message) in messages.iter().enumerate() { + if i > 0 { + println!(); + } + println!("{}", message.rich_format(&world)); } // Try to write a modified world file diff --git a/src/preprocess/peg.rs b/src/preprocess/peg.rs new file mode 100644 index 0000000..e69de29