use std::{ fmt::Display, iter::Peekable, str::{Chars, FromStr}, }; use super::Result; use crate::{channel::DutyCycle, program::ChanSpec}; #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum Token { Eof, LineSeparator, Pause(usize), PauseLen(u32), Jump(usize), ChanSpec(ChanSpec), SetVolume(u8), SetPitch(u8), SetNoiseMode(bool), SetPulseDuty(DutyCycle), } impl Token { pub const fn is_eol(self) -> bool { matches!(self, Self::Eof | Self::LineSeparator) } } pub struct Lexer<'s> { src: &'s str, chars: Peekable>, pos: usize, } impl<'s> Lexer<'s> { pub fn new(src: &'s str) -> Self { let chars = src.chars().peekable(); Self { src, chars, pos: 0 } } fn filter_char(&mut self, ch: Option, advance: bool) -> Result { match ch { Some(c) if c.is_control() && !matches!(c, '\n' | '\r' | '\t') => { Err(invalid_char(c)) } Some(c) => { if advance { self.pos += c.len_utf8(); } Ok(c) } None => Ok('\0'), } } fn peek(&mut self) -> Result { let c = self.chars.peek().copied(); self.filter_char(c, false) } fn next(&mut self) -> Result { let c = self.chars.next(); self.filter_char(c, true) } fn next_int(&mut self) -> Result where T: FromStr, ::Err: Display, { let start = self.pos; while self.peek()?.is_ascii_digit() { self.next()?; } let str = &self.src[start..self.pos]; str.parse::().map_err(|e| e.to_string()) } fn next_note(&mut self) -> Result { if self.peek()?.is_ascii_digit() { return self.next_int(); } let note = match self.next()? { 'a' => 80, 'b' => 82, 'c' => 83, 'd' => 85, 'e' => 87, 'f' => 88, 'g' => 90, c => unexpected(c)?, }; let octave = { let c = self.next()?; c.to_digit(10).ok_or_else(|| format!("invalid octave: {c}")) }?; let off = match self.peek()? { '#' => { self.next()?; 1 } 'b' => { self.next()?; -1 } _ => 0, }; let pitch_u32 = (note + octave * 12).wrapping_add_signed(off); let pitch = u8::try_from(pitch_u32).map_err(|e| format!("{e}"))?; Ok(pitch) } fn next_bool(&mut self) -> Result { match self.next_int()? { 0 => Ok(false), _ => Ok(true), } } fn next_duty_cycle(&mut self) -> Result { match self.next_int()? { 12 => Ok(DutyCycle::Percent12), 25 => Ok(DutyCycle::Percent25), 50 => Ok(DutyCycle::Percent50), 75 => Ok(DutyCycle::Percent25Neg), n => Err(format!("invalid duty cycle: {n}")), } } fn skip_comment(&mut self) -> Result<()> { loop { let next = self.next()?; if next == '\n' || next == '\0' { break; } } Ok(()) } fn next_pause(&mut self) -> Result { let mut count = 1; if self.peek()?.is_ascii_digit() { count = self.next_int()?; } Ok(Token::Pause(count)) } pub fn next_token(&mut self) -> Result { use Token as T; loop { let peek = self.peek()?; if peek == ';' { self.skip_comment()?; } else if matches!(peek, ' ' | '\t' | '\r') { self.next()?; } else { break; } } let token = match self.next()? { // chan spec 'a' => Token::ChanSpec(ChanSpec::PulseA), 'b' => Token::ChanSpec(ChanSpec::PulseB), 't' => Token::ChanSpec(ChanSpec::Triangle), 'n' => Token::ChanSpec(ChanSpec::Noise), // volume 'v' => T::SetVolume(self.next_int()?), // pitch 'p' => T::SetPitch(self.next_note()?), // duty cycle 'd' => T::SetPulseDuty(self.next_duty_cycle()?), // noise mode 'm' => T::SetNoiseMode(self.next_bool()?), // pause '-' => self.next_pause()?, // jump 'j' => T::Jump(self.next_int()?), // pause len 'P' => T::PauseLen(self.next_int()?), // eof '\0' => T::Eof, // new line '\n' => T::LineSeparator, // unexpected c => unexpected(c)?, }; Ok(token) } } impl Iterator for Lexer<'_> { type Item = Result; fn next(&mut self) -> Option { Some(self.next_token()) } } fn invalid_char(ch: char) -> String { match ch as u32 { c @ 0x00..=0x7f => format!("invalid character (codepoint 0x{c:2x})"), c => format!("invalid character (codepoint U+{c:04x})"), } } fn unexpected(c: char) -> Result { let msg = match c { '\0' => "unexpected end of file".to_owned(), '\n' => "unexpected newline character".to_owned(), '\t' => "unexpected tab character".to_owned(), '\r' => "unexpected return character".to_owned(), c => format!("unexpected character {c}"), }; Err(msg) }