From 9d12d498a91caeb37ec5f7c7b20f5be6d5513119 Mon Sep 17 00:00:00 2001 From: Micha White Date: Tue, 3 Oct 2023 18:21:04 -0400 Subject: First attempt at PDN implementation --- pdn/src/grammar.rs | 443 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 443 insertions(+) create mode 100644 pdn/src/grammar.rs (limited to 'pdn/src/grammar.rs') diff --git a/pdn/src/grammar.rs b/pdn/src/grammar.rs new file mode 100644 index 0000000..9529b59 --- /dev/null +++ b/pdn/src/grammar.rs @@ -0,0 +1,443 @@ +use std::{iter::Peekable, sync::Arc}; + +use crate::tokens::{Color, PdnToken, PdnTokenBody, TokenHeader}; + +#[derive(Debug, Clone)] +pub struct PdnFile { + games: Vec, + game_separators: Vec, +} + +#[derive(Debug, Clone)] +pub struct Game { + header: Vec, + body: Vec, +} + +#[derive(Debug, Clone)] +pub struct PdnTag { + left_bracket: TokenHeader, + identifier_token: TokenHeader, + string_token: TokenHeader, + right_bracket: TokenHeader, + + identifier: Arc, + string: Arc, +} + +#[derive(Debug, Clone)] +pub enum BodyPart { + Move(GameMove), + Variation(Variation), + Comment(TokenHeader, Arc), + Setup(TokenHeader, Arc), + Nag(TokenHeader, usize), +} + +#[derive(Debug, Clone)] +pub struct Variation { + left_parenthesis: TokenHeader, + body: Vec, + right_parenthesis: TokenHeader, +} + +#[derive(Debug, Clone)] +pub struct GameMove { + move_number: Option<(TokenHeader, usize, Color)>, + game_move: Move, + move_strength: Option<(TokenHeader, Arc)>, +} + +#[derive(Debug, Clone)] +pub enum Move { + Normal(Square, TokenHeader, Square), + Capture(Square, Vec<(TokenHeader, Square)>), +} + +#[derive(Debug, Clone)] +pub enum Square { + Alpha(TokenHeader, char, char), + Num(TokenHeader, u8), +} + +/// Returns `Ok` if parsed successfully. If there are no tokens left, +/// `Err(None)` is returned. If the next token is not a square position, then +/// `Err(Some(token))` is returned. +fn parse_square(scanner: &mut impl Iterator) -> Result> { + let Some(token) = scanner.next() else { + return Err(None); + }; + let header = token.header; + let body = &token.body; + + match *body { + PdnTokenBody::AlphaSquare(letter, number) => Ok(Square::Alpha(header, letter, number)), + PdnTokenBody::NumSquare(number) => Ok(Square::Num(header, number)), + _ => Err(Some(token)), + } +} + +#[derive(Debug, Clone)] +pub enum MoveError { + EndOfFile, + NoStartSquare(Option), + NoEndSquare(Option), + InvalidCaptureSquares(Vec>), + NoMoveSeparator, +} + +fn parse_normal_move( + first_square: Square, + scanner: &mut impl Iterator, +) -> Result { + let Some(separator) = scanner.next() else { + return Err(MoveError::NoMoveSeparator); + }; + let square = match parse_square(scanner) { + Ok(square) => square, + Err(error) => return Err(MoveError::NoEndSquare(error)), + }; + Ok(Move::Normal(first_square, separator.header, square)) +} + +fn parse_capture_move( + first_square: Square, + scanner: &mut Peekable>, +) -> Result { + let mut captures = Vec::new(); + let mut errors = Vec::new(); + + while let Some(token) = scanner.peek() { + if token.body != PdnTokenBody::CaptureSeparator { + break; + } + + let separator = scanner.next().expect("separator should be next"); + match parse_square(scanner) { + Ok(square) => captures.push((separator.header, square)), + Err(error) => errors.push(error), + } + } + + if !errors.is_empty() { + Err(MoveError::InvalidCaptureSquares(errors)) + } else { + Ok(Move::Capture(first_square, captures)) + } +} + +fn parse_move(scanner: &mut Peekable>) -> Result { + let square = match parse_square(scanner) { + Ok(square) => square, + Err(error) => return Err(MoveError::NoStartSquare(error)), + }; + + let Some(token) = scanner.peek() else { + return Err(MoveError::NoMoveSeparator); + }; + let body = &token.body; + + match body { + PdnTokenBody::MoveSeparator => parse_normal_move(square, scanner), + PdnTokenBody::CaptureSeparator => parse_capture_move(square, scanner), + _ => Err(MoveError::NoMoveSeparator), + } +} + +#[derive(Debug, Clone)] +pub enum GameMoveError { + EndOfFile, + BadMove(MoveError), +} + +fn whitespace_if_found( + scanner: &mut Peekable>, +) -> Option { + let token = scanner.peek()?; + if let PdnTokenBody::Space(_) = token.body { + Some(scanner.next()?.header) + } else { + None + } +} + +fn parse_game_move( + scanner: &mut Peekable>, +) -> Result { + let Some(next_token) = scanner.peek() else { + return Err(GameMoveError::EndOfFile); + }; + + let move_number = match next_token.body { + PdnTokenBody::MoveNumber(number, color) => Some((next_token.header, number, color)), + _ => None, + }; + + if move_number.is_some() { + scanner.next(); + } + + whitespace_if_found(scanner); + + let game_move = parse_move(scanner); + + let move_strength = if let Some(token) = scanner.peek() { + if let PdnTokenBody::MoveStrength(string) = &token.body { + Some((token.header, string.clone())) + } else { + None + } + } else { + None + }; + + if move_strength.is_some() { + scanner.next(); + } + + match game_move { + Ok(game_move) => Ok(GameMove { + move_number, + game_move, + move_strength, + }), + Err(error) => Err(GameMoveError::BadMove(error)), + } +} + +#[derive(Debug, Clone)] +pub enum VariationError { + UnexpectedEnd(BodyError), + BadBody(BodyError), +} + +fn parse_variation( + scanner: &mut Peekable>, +) -> Result { + let left_parenthesis = scanner.next().expect("should start with left paren").header; + let body = parse_body_until(scanner, PdnTokenBody::RightParenthesis)?; + let right_parenthesis = scanner.next().expect("should end with right paren").header; + + Ok(Variation { + left_parenthesis, + body, + right_parenthesis, + }) +} + +#[derive(Debug, Clone)] +pub enum BodyPartError { + EndOfFile, + InvalidToken(PdnToken), + BadMove(GameMoveError), + BadVariation(VariationError), +} + +fn parse_body_part( + scanner: &mut Peekable>, +) -> Result { + let Some(token) = scanner.peek() else { + return Err(BodyPartError::EndOfFile); + }; + + match &token.body { + PdnTokenBody::MoveNumber(..) + | PdnTokenBody::AlphaSquare(..) + | PdnTokenBody::NumSquare(..) => match parse_game_move(scanner) { + Ok(mov) => Ok(BodyPart::Move(mov)), + Err(error) => Err(BodyPartError::BadMove(error)), + }, + PdnTokenBody::LeftParenthesis => match parse_variation(scanner) { + Ok(variation) => Ok(BodyPart::Variation(variation)), + Err(error) => Err(BodyPartError::BadVariation(error)), + }, + PdnTokenBody::Comment(string) => Ok(BodyPart::Comment(token.header, string.clone())), + PdnTokenBody::Setup(string) => Ok(BodyPart::Setup(token.header, string.clone())), + PdnTokenBody::Nag(number) => Ok(BodyPart::Nag(token.header, *number)), + _ => Err(BodyPartError::InvalidToken(token.clone())), + } +} + +pub type BodyError = Vec>; + +fn parse_body_until( + scanner: &mut Peekable>, + until: PdnTokenBody, +) -> Result, VariationError> { + let mut parts = Vec::new(); + + loop { + whitespace_if_found(scanner); + + let Some(token) = scanner.peek() else { + return Err(VariationError::UnexpectedEnd(parts)); + }; + + if token.body == until { + break; + } + + parts.push(parse_body_part(scanner)); + whitespace_if_found(scanner); + } + + if parts.iter().any(|r| r.is_err()) { + Err(VariationError::BadBody(parts)) + } else { + Ok(parts.iter().map(|r| r.as_ref().cloned().unwrap()).collect()) + } +} + +#[derive(Debug, Clone)] +pub enum PdnTagError { + EndOfFile, + NoStartBracket(PdnToken), + Unterminated(Vec), + NoIdentifier, + NoString, + NoEndBracket, +} + +fn parse_pdn_tag( + scanner: &mut Peekable>, +) -> Result { + whitespace_if_found(scanner); + + let Some(left_bracket) = scanner.next() else { + return Err(PdnTagError::EndOfFile); + }; + + if left_bracket.body != PdnTokenBody::LeftBracket { + return Err(PdnTagError::NoStartBracket(left_bracket)); + } + + whitespace_if_found(scanner); + + let Some(identifier_token) = scanner.next() else { + return Err(PdnTagError::Unterminated(vec![left_bracket])); + }; + + let PdnTokenBody::Identifier(identifier) = &identifier_token.body else { + return Err(PdnTagError::NoIdentifier); + }; + + whitespace_if_found(scanner); + + let Some(value_token) = scanner.next() else { + return Err(PdnTagError::Unterminated(vec![ + left_bracket, + identifier_token, + ])); + }; + + let PdnTokenBody::String(value) = &value_token.body else { + return Err(PdnTagError::NoIdentifier); + }; + + whitespace_if_found(scanner); + + let Some(right_bracket) = scanner.next() else { + return Err(PdnTagError::Unterminated(vec![ + left_bracket, + identifier_token, + value_token, + ])); + }; + + if right_bracket.body != PdnTokenBody::RightBracket { + return Err(PdnTagError::NoEndBracket); + } + + whitespace_if_found(scanner); + + Ok(PdnTag { + left_bracket: left_bracket.header, + identifier_token: identifier_token.header, + string_token: value_token.header, + right_bracket: right_bracket.header, + identifier: identifier.clone(), + string: value.clone(), + }) +} + +pub type HeaderError = Vec>; + +fn parse_header( + scanner: &mut Peekable>, +) -> Result, HeaderError> { + let mut tags = Vec::new(); + + loop { + let Some(token) = scanner.peek() else { + break; + }; + + if token.body != PdnTokenBody::LeftBracket { + break; + } + + tags.push(parse_pdn_tag(scanner)); + } + + if tags.iter().any(|r| r.is_err()) { + Err(tags) + } else { + Ok(tags.iter().map(|r| r.as_ref().cloned().unwrap()).collect()) + } +} + +#[derive(Debug, Clone)] +pub struct GameError { + header: Result, HeaderError>, + body: Result, VariationError>, +} + +fn parse_game(scanner: &mut Peekable>) -> Result { + let header = parse_header(scanner); + let body = parse_body_until(scanner, PdnTokenBody::Asterisk); + whitespace_if_found(scanner); + + if let Ok(header) = header { + if let Ok(body) = body { + Ok(Game { header, body }) + } else { + Err(GameError { + header: Ok(header), + body, + }) + } + } else { + Err(GameError { header, body }) + } +} + +pub type PdnError = Vec>; + +fn parse(scanner: &mut impl Iterator) -> Result { + let mut scanner = scanner.peekable(); + let mut games = Vec::new(); + let mut game_separators = Vec::new(); + + loop { + let Some(token) = scanner.peek() else { + break; + }; + + if token.body != PdnTokenBody::LeftBracket { + break; + } + + games.push(parse_game(&mut scanner)); + game_separators.push(scanner.next().unwrap().header); + } + + if games.iter().any(|r| r.is_err()) { + Err(games) + } else { + let games = games.iter().map(|r| r.as_ref().cloned().unwrap()).collect(); + Ok(PdnFile { + games, + game_separators, + }) + } +} -- cgit v1.2.3