diff --git a/Cargo.toml b/Cargo.toml index dbeb371..433e343 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -18,6 +18,7 @@ strum = { version = "0.26", features = ["derive"] } strum_macros = "0.26" tinyaudio = { version = "0.1", optional = true } bng_macros = { path = "bng_macros" } +const_format = "0.2.33" [features] default = ["play", "save"] diff --git a/bng_macros/src/lib.rs b/bng_macros/src/lib.rs index e30d5c1..2ea9f67 100644 --- a/bng_macros/src/lib.rs +++ b/bng_macros/src/lib.rs @@ -2,13 +2,7 @@ extern crate proc_macro; use proc_macro::TokenStream; use proc_macro2::Span; use quote::quote; -use syn::{parse_macro_input, Data, DataEnum, DeriveInput, Fields, Ident}; - -#[proc_macro_derive(ModifierParser)] -pub fn modifier_parser(input: TokenStream) -> TokenStream { - let ast = parse_macro_input!(input as DeriveInput); - impl_modifier_parser(ast) -} +use syn::{parse_macro_input, Data, DataEnum, DeriveInput, Ident}; #[proc_macro_derive(SlopeModifierParser)] pub fn slope_modifier_parser(input: TokenStream) -> TokenStream { @@ -22,44 +16,6 @@ pub fn quick_modifier_parser(input: TokenStream) -> TokenStream { impl_quick_modifier_parser(ast) } -fn impl_modifier_parser(ast: DeriveInput) -> TokenStream { - let name = &ast.ident; - if let Data::Enum(DataEnum { variants, .. }) = ast.data { - let match_arms = variants.iter().map(|variant| { - let variant_name = &variant.ident; - let variant_type = if let Fields::Unnamed(ref fields) = variant.fields { - &fields.unnamed[0].ty - } else { - panic!("Expected unnamed fields in enum variants"); - }; - let const_name = - Ident::new(&variant.ident.to_string().to_uppercase(), Span::call_site()); - quote! { - nom::combinator::map( - nom::sequence::preceded( - nom::character::complete::char(#name::#const_name), - nom::character::complete::#variant_type - ), - #name::#variant_name - ) - } - }); - - quote! { - impl lex::lexer::Parse for #name { - fn parse(input: &str) -> nom::IResult<&str, #name> { - nom::branch::alt(( - #(#match_arms),* - ))(input) - } - } - } - .into() - } else { - panic!("this macro only works on enums") - } -} - fn impl_slope_modifier_parser(ast: DeriveInput) -> TokenStream { let name = &ast.ident; if let Data::Enum(DataEnum { variants, .. }) = ast.data { @@ -98,11 +54,11 @@ fn impl_quick_modifier_parser(ast: DeriveInput) -> TokenStream { nom::branch::alt( ( nom::combinator::value( - lex::MORE, + lex::UP, nom::character::complete::char(#name::#const_name.0) ), nom::combinator::value( - lex::LESS, + lex::DOWN, nom::character::complete::char(#name::#const_name.1) ) ) diff --git a/src/bng/score.rs b/src/bng/score.rs index 6b7c783..679c7db 100644 --- a/src/bng/score.rs +++ b/src/bng/score.rs @@ -1,27 +1,31 @@ -use bng_macros::{ModifierParser, QuickModifierParser, SlopeModifierParser}; +use std::num::{NonZeroU16, NonZeroU8}; + +use bng_macros::{QuickModifierParser, SlopeModifierParser}; use fasteval::Instruction; mod lex; +#[cfg_attr(test, derive(Debug, PartialEq))] pub(super) enum Atom { Note(char), Rest, StartHere, Modifier(Modifier), QuickModifier(QuickModifier), - Loop(u8, Vec), + Loop(NonZeroU8, Vec), Tuple(Vec), Slope(SlopeModifier, Instruction, Vec), Comment, } +#[cfg_attr(test, derive(Debug, PartialEq))] pub(super) enum FlatAtom { Note(char), Rest, StartHere, Modifier(Modifier), QuickModifier(QuickModifier), - LoopStarts(u8), + LoopStarts(NonZeroU8), LoopEnds, TupleStarts, TupleEnds, @@ -39,12 +43,12 @@ impl Clone for FlatAtom { } } -#[derive(ModifierParser)] +#[cfg_attr(test, derive(Debug, PartialEq))] pub(super) enum Modifier { Volume(u8), Octave(u8), - Length(u8), - Tempo(u16), + Length(NonZeroU8), + Tempo(NonZeroU16), } impl Default for Modifier { @@ -54,6 +58,7 @@ impl Default for Modifier { } #[derive(QuickModifierParser)] +#[cfg_attr(test, derive(Debug, PartialEq))] pub(super) enum QuickModifier { Volume(bool), Octave(bool), @@ -62,6 +67,7 @@ pub(super) enum QuickModifier { } #[derive(Clone, Copy, SlopeModifierParser)] +#[cfg_attr(test, derive(Debug, PartialEq))] pub(super) enum SlopeModifier { Note, Volume, diff --git a/src/bng/score/lex.rs b/src/bng/score/lex.rs index 07f561c..bb580a8 100644 --- a/src/bng/score/lex.rs +++ b/src/bng/score/lex.rs @@ -2,10 +2,10 @@ use super::{Atom, FlatAtom, Modifier, QuickModifier, SlopeModifier}; pub(super) mod lexer; -pub(super) const MORE: bool = true; -pub(super) const LESS: bool = false; pub(super) const ON: bool = true; pub(super) const OFF: bool = false; +pub(super) const UP: bool = true; +pub(super) const DOWN: bool = false; struct WrappingTokens; impl WrappingTokens { diff --git a/src/bng/score/lex/lexer.rs b/src/bng/score/lex/lexer.rs index eeb420a..197e13e 100644 --- a/src/bng/score/lex/lexer.rs +++ b/src/bng/score/lex/lexer.rs @@ -1,12 +1,15 @@ -use std::collections::BTreeMap; +use std::{ + collections::BTreeMap, + num::{NonZeroU16, NonZeroU8}, +}; use clap::builder::TypedValueParser; use fasteval::{Compiler, Instruction}; use nom::{ branch::alt, bytes::complete::take_till1, - character::complete::{anychar, char, one_of, u8}, - combinator::{map_res, value}, + character::complete::{anychar, char, one_of, u16, u8}, + combinator::{map_opt, map_res, value}, multi::many0, sequence::{delimited, pair, preceded, separated_pair}, Err, IResult, Parser, @@ -14,18 +17,32 @@ use nom::{ use crate::bng::score::{Atom, FlatAtom, Modifier, QuickModifier, SlopeModifier}; +#[cfg(test)] +mod tests; + pub(crate) trait Parse: Sized { fn parse(input: &str) -> IResult<&str, Self>; } -fn atom_parser<'a>(notes: &'a str) -> impl Parser<&str, FlatAtom, nom::error::Error<&str>> { +impl Parse for Modifier { + fn parse(input: &str) -> IResult<&str, Self> { + alt(( + preceded(char(Modifier::VOLUME), u8).map(Modifier::Volume), + preceded(char(Modifier::OCTAVE), u8).map(Modifier::Octave), + preceded(char(Modifier::LENGTH), map_opt(u8, NonZeroU8::new)).map(Modifier::Length), + preceded(char(Modifier::TEMPO), map_opt(u16, NonZeroU16::new)).map(Modifier::Tempo), + ))(input) + } +} + +fn atom_parser(notes: &str) -> impl Parser<&str, FlatAtom, nom::error::Error<&str>> { alt(( one_of(notes).map(FlatAtom::Note), value(FlatAtom::Rest, char(Atom::REST)), value(FlatAtom::StartHere, char(Atom::START_HERE)), preceded(char(Atom::MODIFIER), Modifier::parse).map(FlatAtom::Modifier), QuickModifier::parse.map(FlatAtom::QuickModifier), - preceded(char(Atom::LOOP.0), u8).map(FlatAtom::LoopStarts), + preceded(char(Atom::LOOP.0), map_opt(u8, NonZeroU8::new)).map(FlatAtom::LoopStarts), value(FlatAtom::LoopEnds, char(Atom::LOOP.1)), value(FlatAtom::TupleStarts, char(Atom::TUPLE.0)), value(FlatAtom::TupleEnds, char(Atom::TUPLE.1)), diff --git a/src/bng/score/lex/lexer/tests.rs b/src/bng/score/lex/lexer/tests.rs new file mode 100644 index 0000000..7b269d2 --- /dev/null +++ b/src/bng/score/lex/lexer/tests.rs @@ -0,0 +1,204 @@ +mod modifier { + use std::num::{NonZeroU16, NonZeroU8}; + + use const_format::concatcp; + use nom::{ + error::{Error, ErrorKind}, + Err, + }; + + use crate::bng::score::{lex::lexer::Parse, Atom, Modifier}; + + pub(super) const SAMPLE_STR: &str = concatcp!( + Atom::TUPLE.0, + "acc", + Atom::TUPLE.1, + "ed", + Atom::COMMENT.0, + "hello" + ); + + #[test] + fn volume() { + assert_eq!( + Ok((SAMPLE_STR, Modifier::Volume(2))), + Modifier::parse(concatcp!(Modifier::VOLUME, 2u8, SAMPLE_STR)) + ); + assert_eq!( + Err(nom::Err::Error(Error::new( + concatcp!(Modifier::VOLUME, 2556u16, SAMPLE_STR), + ErrorKind::Char + ))), + Modifier::parse(concatcp!(Modifier::VOLUME, 2556u16, SAMPLE_STR)) + ); + } + + #[test] + fn octave() { + assert_eq!( + Ok((SAMPLE_STR, Modifier::Octave(2))), + Modifier::parse(concatcp!(Modifier::OCTAVE, 2u8, SAMPLE_STR)) + ); + assert_eq!( + Err(nom::Err::Error(Error::new( + concatcp!(Modifier::OCTAVE, 2556u16, SAMPLE_STR), + ErrorKind::Char + ))), + Modifier::parse(concatcp!(Modifier::OCTAVE, 2556u16, SAMPLE_STR)) + ); + } + + #[test] + fn length() { + assert_eq!( + Ok(( + SAMPLE_STR, + Modifier::Length(unsafe { NonZeroU8::new_unchecked(2) }) + )), + Modifier::parse(concatcp!(Modifier::LENGTH, 2u8, SAMPLE_STR)) + ); + assert_eq!( + Err(nom::Err::Error(Error::new( + concatcp!(Modifier::LENGTH, 2556u16, SAMPLE_STR), + ErrorKind::Char + ))), + Modifier::parse(concatcp!(Modifier::LENGTH, 2556u16, SAMPLE_STR)) + ); + assert_eq!( + Err(nom::Err::Error(Error::new( + concatcp!(Modifier::LENGTH, 0u8, SAMPLE_STR), + ErrorKind::Char + ))), + Modifier::parse(concatcp!(Modifier::LENGTH, 0u8, SAMPLE_STR)) + ); + } + + #[test] + fn tempo() { + assert_eq!( + Ok(( + SAMPLE_STR, + Modifier::Tempo(unsafe { NonZeroU16::new_unchecked(2) }) + )), + Modifier::parse(concatcp!(Modifier::TEMPO, 2u8, SAMPLE_STR)) + ); + assert_eq!( + Err(nom::Err::Error(Error::new( + concatcp!(655353u32, SAMPLE_STR), + ErrorKind::Digit + ))), + Modifier::parse(concatcp!(Modifier::TEMPO, 655353u32, SAMPLE_STR)) + ); + } +} + +use modifier::SAMPLE_STR as MODIFIER_SAMPLE_STRING; + +mod quick_modifier { + use const_format::concatcp; + use nom::{error::Error, Err}; + + use super::MODIFIER_SAMPLE_STRING as SAMPLE_STR; + use crate::bng::score::{ + lex::{lexer::Parse, DOWN, OFF, ON, UP}, + Atom, QuickModifier, + }; + + #[test] + fn volume() { + assert_eq!( + Ok((SAMPLE_STR, QuickModifier::Volume(UP))), + QuickModifier::parse(concatcp!(QuickModifier::VOLUME.0, SAMPLE_STR)) + ); + assert_eq!( + Ok((SAMPLE_STR, QuickModifier::Volume(DOWN))), + QuickModifier::parse(concatcp!(QuickModifier::VOLUME.1, SAMPLE_STR)) + ); + } + + #[test] + fn octave() { + assert_eq!( + Ok((SAMPLE_STR, QuickModifier::Octave(UP))), + QuickModifier::parse(concatcp!(QuickModifier::OCTAVE.0, SAMPLE_STR)) + ); + assert_eq!( + Ok((SAMPLE_STR, QuickModifier::Octave(DOWN))), + QuickModifier::parse(concatcp!(QuickModifier::OCTAVE.1, SAMPLE_STR)) + ); + } + + #[test] + fn length() { + assert_eq!( + Ok((SAMPLE_STR, QuickModifier::Length(UP))), + QuickModifier::parse(concatcp!(QuickModifier::LENGTH.0, SAMPLE_STR)) + ); + assert_eq!( + Ok((SAMPLE_STR, QuickModifier::Length(DOWN))), + QuickModifier::parse(concatcp!(QuickModifier::LENGTH.1, SAMPLE_STR)) + ); + } + + #[test] + fn pizz() { + assert_eq!( + Ok((SAMPLE_STR, QuickModifier::Pizz(ON))), + QuickModifier::parse(concatcp!(QuickModifier::PIZZ.0, SAMPLE_STR)) + ); + assert_eq!( + Ok((SAMPLE_STR, QuickModifier::Pizz(OFF))), + QuickModifier::parse(concatcp!(QuickModifier::PIZZ.1, SAMPLE_STR)) + ); + } +} + +#[cfg(test)] +mod slope_modifier { + use const_format::concatcp; + use nom::{error::Error, Err}; + + use crate::bng::score::{lex::lexer::Parse, Atom, SlopeModifier}; + + const SAMPLE_STR: &str = concatcp!(" 1-cos((PI*x)/2),acced", Atom::SLOPE.1); + + #[test] + fn note() { + assert_eq!( + Ok((SAMPLE_STR, SlopeModifier::Note)), + SlopeModifier::parse(concatcp!(SlopeModifier::NOTE, SAMPLE_STR)) + ) + } + + #[test] + fn volume() { + assert_eq!( + Ok((SAMPLE_STR, SlopeModifier::Volume)), + SlopeModifier::parse(concatcp!(SlopeModifier::VOLUME, SAMPLE_STR)) + ) + } + + #[test] + fn octave() { + assert_eq!( + Ok((SAMPLE_STR, SlopeModifier::Octave)), + SlopeModifier::parse(concatcp!(SlopeModifier::OCTAVE, SAMPLE_STR)) + ) + } + + #[test] + fn length() { + assert_eq!( + Ok((SAMPLE_STR, SlopeModifier::Length)), + SlopeModifier::parse(concatcp!(SlopeModifier::LENGTH, SAMPLE_STR)) + ) + } + + #[test] + fn tempo() { + assert_eq!( + Ok((SAMPLE_STR, SlopeModifier::Tempo)), + SlopeModifier::parse(concatcp!(SlopeModifier::TEMPO, SAMPLE_STR)) + ) + } +}