diff --git a/src/cli/main.rs b/src/cli/main.rs index 1ad981c..38899b2 100644 --- a/src/cli/main.rs +++ b/src/cli/main.rs @@ -1,6 +1,6 @@ mod cli; use std::{ - collections::HashMap, + collections::{HashMap, HashSet}, fs::File, io::{Cursor, Write, read_to_string, stdout}, iter::once, @@ -9,7 +9,7 @@ use std::{ use anyhow::{Context as _, anyhow}; use bliplib::{ compiler::{Compiler, Context, SAMPLE_RATE, VariableChange}, - parser::Parser, + parser::{LocatedVerboseError, Parser}, }; use clap::Parser as _; use cli::Cli; @@ -102,10 +102,12 @@ fn parse_and_compile(opts: &PlayOpts) -> anyhow::Result> { opts.slopes() .map(|(s, (v, e))| (s, VariableChange(*v, e.clone()))) .collect::>(), - HashMap::from(default_variables) + default_variables .iter() - .chain(opts.variables()) .map(|(v, _)| *v) + .chain(opts.variables().map(|(v, _)| *v)) + .collect::>() + .into_iter() .collect::>(), ); info!("reading input"); @@ -113,18 +115,31 @@ fn parse_and_compile(opts: &PlayOpts) -> anyhow::Result> { info!("parsing tokens"); let tokens = parser .parse_all(&input) - .map_err(|nom::error::Error { input, code }| { - anyhow!( - "{code:?} line {line} column {column} (at \"{at}\")", - code = code, - line = input.location_line(), - column = input.get_utf8_column(), - at = input - .chars() - .chain("...".chars()) - .take(10) - .collect::() - ) + .map_err(|e| match e { + nom::Err::Incomplete(n) => { + anyhow!("nom parsers said the input was incomplete and needed {n:?} bytes") + } + nom::Err::Error(LocatedVerboseError { location, error }) + | nom::Err::Failure(LocatedVerboseError { location, error }) => error + .unwrap_or(anyhow!("input did not match any known grammar (typo?)")) + .context(format!( + "line {line} column {column} (at \"{at}\")", + line = location.location_line(), + column = location.get_utf8_column(), + at = { + if location.len() > 10 { + location + .chars() + .take(10) + .chain("...".chars()) + .collect::() + } else if location.is_empty() { + String::from("EOF") + } else { + location.to_string() + } + } + )), }) .context("Failed to parse input")?; info!("found {} tokens", tokens.as_ref().len()); @@ -138,9 +153,10 @@ fn parse_and_compile(opts: &PlayOpts) -> anyhow::Result> { 'L'.to_string(), 'n'.to_string(), default_variables - .map(|(c, v)| (c.to_string(), v)) .into_iter() - .chain(opts.variables().map(|(a, b)| (a.to_string(), *b))), + .chain(opts.variables().map(|(a, b)| (*a, *b))) + .map(|(c, v)| (c.to_string(), v)) + .collect::>(), opts.instrument().clone(), opts.slopes() .map(|(_, (a, b))| (a.to_string(), b.clone())) diff --git a/src/parser.rs b/src/parser.rs index 811c0e4..14c0418 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -1,20 +1,19 @@ -use std::{any::type_name, borrow::Borrow, collections::BTreeMap, marker::PhantomData}; +use std::{ + any::type_name, + borrow::{Borrow, Cow}, + collections::BTreeMap, + marker::PhantomData, +}; +use anyhow::anyhow; use derive_new::new; use fasteval::Evaler; -use log::{debug, error, trace}; +use log::{debug, trace, warn}; use nom::{ - AsChar, Compare, Finish, IResult, Input, Parser as NomParser, - branch::alt, - bytes::complete::{tag, take, take_till}, - character::{ + branch::alt, bytes::complete::{tag, take, take_till}, character::{ complete::{char, space1, usize}, streaming::one_of, - }, - combinator::{all_consuming, cut, opt, value}, - error::{Error, ParseError}, - multi::many0, - sequence::{delimited, preceded}, + }, combinator::{all_consuming, cut, opt, value}, error::{ErrorKind, FromExternalError, ParseError}, multi::many0, sequence::{delimited, preceded}, AsChar, Compare, Input, Parser as _ }; use nom_locate::LocatedSpan; @@ -23,6 +22,82 @@ use crate::compiler::{ VariableChange, }; +pub type IResult> = nom::IResult; + +#[derive(new)] +pub struct VerboseParser>, I> { + parser: P, + context: Cow<'static, str>, + #[new(default)] + phantom: PhantomData, +} + +impl ParseError for LocatedVerboseError { + fn from_error_kind(input: I, _kind: ErrorKind) -> Self { + Self { + location: input, + error: None, + } + } + + fn append(_input: I, _kind: ErrorKind, other: Self) -> Self { + other + } +} + +impl>> nom::Parser for VerboseParser { + type Output = P::Output; + + type Error = LocatedVerboseError; + + fn process( + &mut self, + input: I, + ) -> nom::PResult { + use nom::Err::*; + use nom::Mode; + + let stack_verbose_error = |e: LocatedVerboseError| -> LocatedVerboseError { + LocatedVerboseError { + error: Some(if let Some(cause) = e.error { + cause.context(self.context.clone()) + } else { + anyhow::Error::msg(self.context.clone()) + }), + ..e + } + }; + match self.parser.process::(input) { + Ok(o) => Ok(o), + Err(Error(e)) => Err(Error(OM::Error::map(e, stack_verbose_error))), + Err(Failure(e)) => Err(Failure(stack_verbose_error(e))), + Err(Incomplete(e)) => Err(Incomplete(e)), + } + } +} + +#[derive(Debug)] +pub struct LocatedVerboseError { + pub location: I, + pub error: Option, +} + +pub fn expect( + parser: P, + error_message: impl Into>, +) -> impl nom::Parser> +where + P: nom::Parser>, +{ + VerboseParser::new(parser, error_message.into()) +} + +impl FromExternalError for LocatedVerboseError { + fn from_external_error(input: I, _kind: ErrorKind, e: anyhow::Error) -> Self { + Self { location: input, error: Some(e) } + } +} + #[derive(new)] pub struct Parser where @@ -39,7 +114,7 @@ where phantom: PhantomData<(NS, SS)>, } -impl<'a, N, NS, S, SS, SV, V> Parser +impl<'a, 'p, N, NS, S, SS, SV, V> Parser where N: AsRef<[NS]>, NS: AsRef, @@ -47,32 +122,34 @@ where SS: AsRef, SV: Borrow, V: AsRef<[char]>, + 'p: 'a, { pub fn parse_all( - &self, + &'p self, input: &'a str, - ) -> Result>> { + ) -> Result>>> { debug!("parsing input \"{input}\""); all_consuming(token_parser(self)) .parse_complete(LocatedSpan::new(input)) - .finish() - .map(|(_, o)| o) + .map(move |(_, o)| o) } } -fn token_parser<'a, I, N, NS, S, SS, SV, V>( - parser: &Parser, -) -> impl NomParser> +fn token_parser<'a, 'p, N, NS, S, SS, SV, V>( + parser: &'p Parser, +) -> impl nom::Parser< + LocatedSpan<&'a str>, + Output = TokenVec, + Error = LocatedVerboseError>, +> where - I: Input + AsRef + for<'z> nom::Compare<&'z str> + Copy, - ::Item: AsChar, - ::Item: PartialEq, N: AsRef<[NS]>, NS: AsRef, S: IntoIterator + Clone, SS: AsRef, SV: Borrow, V: AsRef<[char]>, + 'p: 'a, { trace!("making the TOKEN parser"); let space_or_comment = || { @@ -84,13 +161,20 @@ where many0(delimited( space_or_comment(), alt(( - Silence::parser().map(into_box), - Marker::parser().map(into_box), - Note::parser(parser.notes.as_ref()).map(into_box), - VariableChange::parser(&parser.variables).map(into_box), - Loop::parser(parser).map(into_box), - Tuplet::parser(parser).map(into_box), - Slope::parser(parser).map(into_box), + expect(Silence::parser(), "expected a silence").map(into_box), + expect(Marker::parser(), "expected a marker").map(into_box), + expect( + VariableChange::parser(&parser.variables).map(into_box), + "variable assignment", + ), + expect(Loop::parser(parser).map(into_box), "expected a loop"), + expect(Tuplet::parser(parser).map(into_box), "expected a tuplet"), + expect(Slope::parser(parser).map(into_box), "expected a slope"), + expect( + Note::parser(parser.notes.as_ref()), + "expected a note as last appeal (input didn't match anything known)", + ) + .map(into_box), )), space_or_comment(), )) @@ -102,7 +186,7 @@ fn into_box<'a>(token: impl Token + 'a) -> Box { } impl Silence { - fn parser() -> impl NomParser> + fn parser() -> impl nom::Parser> where I: Input, ::Item: AsChar, @@ -113,7 +197,7 @@ impl Silence { } impl Marker { - fn parser() -> impl NomParser> + fn parser() -> impl nom::Parser> where I: Input, ::Item: AsChar, @@ -126,7 +210,7 @@ impl Marker { impl Note { fn parser<'a, N, NS, I>( notes: N, - ) -> impl NomParser> + 'a + ) -> impl nom::Parser> + 'a where N: IntoIterator, NS: AsRef, @@ -168,14 +252,26 @@ impl VariableChange { V: AsRef<[char]>, { trace!("making the {} parser", type_name::()); + let variables_string = variables.as_ref().iter().collect::(); move |i: I| { preceded( char('$'), - cut( - one_of(variables.as_ref().iter().collect::().as_str()) - .and(expression_parser(variables.as_ref())) - .map(|(name, change)| VariableChange(name, change)), - ), + cut(expect( + one_of(variables_string.as_str()), + format!( + "got unknown variable '{}', expected one of these instead: {:?}", + i.as_ref().chars().nth(1).unwrap_or('?'), + variables_string.chars().collect::>() + ), + ) + .and(cut(expect( + expression_parser(variables.as_ref()), + format!( + "expected a valid expression to assign variable {} to", + i.as_ref().chars().nth(1).unwrap_or('?') + ), + ))) + .map(|(name, change)| VariableChange(name, change))), ) .parse(i) } @@ -183,19 +279,17 @@ impl VariableChange { } impl Loop { - fn parser( - parser: &Parser, - ) -> impl Fn(I) -> IResult + fn parser<'a, 'p, N, NS, S, SS, SV, V>( + parser: &'p Parser, + ) -> impl Fn(LocatedSpan<&'a str>) -> IResult, Self> where - I: Input + AsRef + for<'z> nom::Compare<&'z str> + Copy, - ::Item: AsChar, - ::Item: PartialEq, N: AsRef<[NS]>, NS: AsRef, S: IntoIterator + Clone, SS: AsRef, SV: Borrow, V: AsRef<[char]>, + 'p: 'a, { trace!("making the {} parser", type_name::()); move |input| { @@ -213,8 +307,17 @@ impl Loop { ) .map(LoopCount::Variable), ))) - .and(token_parser(parser)), - cut(char(')')), + .and(cut(take_till(|c| c == ')').and_then(cut(expect(all_consuming(token_parser(parser)), "input did not match any known grammar for inner tokens (typo?)"))))), + cut( + expect( + char(')'), + format!( + "the loop started at line {line} column {column} was not closed at this point", + line = input.location_line(), + column = input.get_utf8_column() + ) + ) + ), ) .map(|(c, v)| Self(c.unwrap_or_default(), v)) .parse(input) @@ -223,23 +326,30 @@ impl Loop { } impl Tuplet { - fn parser( - parser: &Parser, - ) -> impl Fn(I) -> IResult + fn parser<'a, 'p, N, NS, S, SS, SV, V>( + parser: &'p Parser, + ) -> impl Fn(LocatedSpan<&'a str>) -> IResult, Self> where - I: Input + for<'z> Compare<&'z str> + AsRef + Copy, - ::Item: AsChar, - ::Item: PartialEq, N: AsRef<[NS]>, NS: AsRef, S: IntoIterator + Clone, SS: AsRef, SV: Borrow, V: AsRef<[char]>, + 'p: 'a, { trace!("making the {} parser", type_name::()); |input| { - delimited(char('['), token_parser(parser), cut(char(']'))) + delimited(char('['), cut(take_till(|c| c == ']').and_then(cut(expect(all_consuming(token_parser(parser)), "input did not match any known grammar for inner tokens (typo?)")))), cut( + expect( + char(']'), + format!( + "the tuplet started at line {line} column {column} was not closed at this point", + line = input.location_line(), + column = input.get_utf8_column() + ) + ) + )) .map(Self) .parse(input) } @@ -247,19 +357,17 @@ impl Tuplet { } impl Slope { - fn parser<'p, I, N, NS, S, SS, SV, V>( + fn parser<'a, 'p, N, NS, S, SS, SV, V>( parser: &'p Parser, - ) -> impl Fn(I) -> IResult + ) -> impl Fn(LocatedSpan<&'a str>) -> IResult, Self> where - I: Input + for<'z> Compare<&'z str> + AsRef + Copy, - ::Item: AsChar, - ::Item: PartialEq, N: AsRef<[NS]>, NS: AsRef, S: IntoIterator + Clone, SS: AsRef, SV: Borrow, V: AsRef<[char]>, + 'p: 'a, { trace!("making the {} parser", type_name::()); move |input| { @@ -279,15 +387,36 @@ impl Slope { let iter: std::vec::IntoIter<(String, VariableChange)> = slopes.into_iter(); delimited( char('{'), - cut(alt(iter + cut(expect(alt(iter .map(|(k, v)| { - Box::new(move |input: I| value(v.clone(), tag(k.as_str())).parse(input)) - as Box IResult> + Box::new(move |input| value(v.clone(), tag(k.as_str())).parse(input)) + as Box< + dyn Fn( + LocatedSpan<&'a str>, + ) + -> IResult, VariableChange>, + > }) - .collect:: IResult>>>() - .as_mut_slice())) - .and(token_parser(parser)), - cut(char('}')), + .collect::, + ) + -> IResult, VariableChange>, + >, + >>() + .as_mut_slice()), format!("expected a slope name from available slope names ({:?})", parser.slopes.clone().into_iter().map(|(s1, _)| s1.as_ref().to_string()).collect::>()))) + .and(cut(take_till(|c| c == '}').and_then(cut(expect(all_consuming(token_parser(parser)), "input did not match any known grammar for inner tokens (typo?)"))))), + cut( + expect( + char('}'), + format!( + "the slope started at line {line} column {column} was not closed at this point", + line = input.location_line(), + column = input.get_utf8_column() + ) + ) + ), ) .map(|(i, v)| Self::new(i, v)) .parse(input) @@ -342,9 +471,7 @@ where } } -pub fn take_while_map>( - cond: F, -) -> impl FnMut(I) -> IResult +pub fn take_while_map(cond: F) -> impl FnMut(I) -> IResult> where I: Input + Copy, F: Fn(I) -> Option, @@ -364,24 +491,25 @@ where len -= 1; } } - error!("take_while_map found no match"); - Err(nom::Err::Incomplete(nom::Needed::Unknown)) + warn!("take_while_map found no match"); + Err(nom::Err::Error(LocatedVerboseError { + location: input, + error: Some(anyhow!("invalid expression")), + })) } } #[cfg(test)] mod tests { - use super::Parser; + use super::{IResult, Parser, expression_parser}; - use std::collections::HashMap; + use std::{borrow::Borrow, collections::HashMap}; - use nom::{IResult, Parser as _}; + use nom::Parser as _; + use nom_locate::LocatedSpan; - use crate::{ - compiler::{ - Loop, LoopCount, Marker, Note, Silence, Slope, TokenVec, Tuplet, VariableChange, - }, - parser::expression_parser, + use crate::compiler::{ + Loop, LoopCount, Marker, Note, Silence, Slope, TokenVec, Tuplet, VariableChange, }; fn very_fancy_slope() -> VariableChange { @@ -568,15 +696,26 @@ mod tests { #[test] fn r#loop() { - let slopes = Default::default(); - fn parser_builder<'s>( - slopes: &'s HashMap, - ) -> impl Fn(&str) -> IResult<&str, Loop> { - move |input: &str| { - Loop::parser(&Parser::new(["do", "ré", "mi"], slopes, ['n'])).parse(input) - } + let parser = Parser::new( + ["do", "ré", "mi"], + HashMap::::default(), + ['n'], + ); + fn parser_builder<'a, 'p, N, NS, S, SS, SV, V>( + parser: &'p Parser, + ) -> impl Fn(LocatedSpan<&'a str>) -> IResult, Loop> + where + N: AsRef<[NS]>, + NS: AsRef, + S: IntoIterator + Clone, + SS: AsRef, + SV: Borrow, + V: AsRef<[char]>, + 'p: 'a, + { + move |input| Loop::parser(parser).parse(input) } - let parser = parser_builder(&slopes); + let parser = parser_builder(&parser); let mut working_cases = vec![ ( "(.%)", @@ -604,7 +743,7 @@ mod tests { ]; let mut not_working_cases = vec!["", "(", ")", "(2", "(p)"]; for (test, expected) in working_cases.drain(..) { - let output = parser(test); + let output = parser(test.into()).map(|(ls, o)| (*ls, o)); if let Ok(result) = output { assert_eq!(expected, result, "case \"{test}\""); } else { @@ -612,7 +751,7 @@ mod tests { } } for test in not_working_cases.drain(..) { - let output = parser(test); + let output = parser(test.into()).map(|(ls, o)| (*ls, o)); assert!( output.is_err(), "result of \"{test}\" was not Err: {output:?}" @@ -622,15 +761,26 @@ mod tests { #[test] fn tuplet() { - let slopes = Default::default(); - fn parser_builder<'s>( - slopes: &'s HashMap, - ) -> impl Fn(&str) -> IResult<&str, Tuplet> { - move |input: &str| { - Tuplet::parser(&Parser::new(["do", "ré", "mi"], slopes, ['n'])).parse(input) - } + let parser = Parser::new( + ["do", "ré", "mi"], + HashMap::::default(), + ['n'], + ); + fn parser_builder<'a, 'p, N, NS, S, SS, SV, V>( + parser: &'p Parser, + ) -> impl Fn(LocatedSpan<&'a str>) -> IResult, Tuplet> + where + N: AsRef<[NS]>, + NS: AsRef, + S: IntoIterator + Clone, + SS: AsRef, + SV: Borrow, + V: AsRef<[char]>, + 'p: 'a, + { + move |input| Tuplet::parser(parser).parse(input) } - let parser = parser_builder(&slopes); + let parser = parser_builder(&parser); let mut working_cases = vec![ ( "[.%]", @@ -644,7 +794,7 @@ mod tests { ]; let mut not_working_cases = vec!["", "[", "]", "[2", "[p]"]; for (test, expected) in working_cases.drain(..) { - let output = parser(test); + let output = parser(test.into()).map(|(ls, o)| (*ls, o)); if let Ok(result) = output { assert_eq!(expected, result, "case \"{test}\""); } else { @@ -652,7 +802,7 @@ mod tests { } } for test in not_working_cases.drain(..) { - let output = parser(test); + let output = parser(test.into()).map(|(ls, o)| (*ls, o)); assert!( output.is_err(), "result of \"{test}\" was not Err: {output:?}" @@ -699,7 +849,7 @@ mod tests { "{}", ]; for (test, expected) in working_cases.drain(..) { - let output = parser(test); + let output = parser(test.into()).map(|(ls, o)| (*ls, o)); if let Ok(result) = output { assert_eq!(expected, result, "case \"{test}\""); } else { @@ -707,7 +857,7 @@ mod tests { } } for test in not_working_cases.drain(..) { - let output = parser(test); + let output = parser(test.into()).map(|(ls, o)| (*ls, o)); assert!( output.is_err(), "result of \"{test}\" was not Err: {output:?}"