diff --git a/.gitignore b/.gitignore index 3eded00..6439a16 100644 --- a/.gitignore +++ b/.gitignore @@ -4,4 +4,4 @@ kcov* **/*.rs.bk tags **/*.rustfmt -Cargo.lock +Cargo.lock \ No newline at end of file diff --git a/Cargo.toml b/Cargo.toml index eb4a3f7..1579274 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,8 +10,8 @@ repository = "https://github.com/ithinuel/async-gcode" categories = ["asynchronous", "embedded", "no-std", "parsing"] [features] -default = ["std"] -std = [] +default = ["std", "future-stream"] +std = ["float-f64"] parse-comments = [] parse-trailing-comment = [] parse-checksum = [] @@ -19,14 +19,17 @@ parse-parameters = [] parse-expressions = [] optional-value = [] string-value = [] +float-f64 = [] +# Use future::stream as input interface. If not set, async-traits will be used insted. +future-stream = ["pin-project-lite", "futures"] [badges] maintenance = { status = "experimental" } [dev-dependencies] -futures-executor = { version = "0.3.21" } +futures-executor = { version = "0.3" } [dependencies] either = {version = "^1", default-features = false } -futures = { version = "0.3.21", default-features = false } -pin-project-lite = { version = "0.2.9" } +futures = { version = "0.3", optional = true, default-features = false } +pin-project-lite = { version = "0.2", optional = true } \ No newline at end of file diff --git a/examples/cli.rs b/examples/cli.rs index c93370a..61d2e4f 100644 --- a/examples/cli.rs +++ b/examples/cli.rs @@ -19,7 +19,11 @@ fn main() { )); while let Some(res) = parser.next().await { - println!("{:?}", res); + match res { + Ok(_r) => println!("{:?}", _r), + Err(Error::Io(_ie)) => println!("IOError {:?}", _ie), + Err(Error::Parse(_pe)) => println!("ParseError {:?}", _pe), + } } }); } diff --git a/src/lib.rs b/src/lib.rs index 823faa6..d57c504 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -98,10 +98,14 @@ mod types; mod parser; +pub use stream::{ByteStream, TryByteStream}; +pub use parser::AsyncParserState; pub use parser::Parser; pub use types::Literal; pub use types::RealValue; +pub use types::DecimalRepr; + #[cfg(any(feature = "parse-expressions", feature = "parse-parameters"))] pub use types::expressions::Expression; @@ -110,9 +114,12 @@ pub enum Error { /// Error no the gcode syntax UnexpectedByte(u8), - /// The parsed number excedded the expected range. + /// The parsed number exceeded the expected range. NumberOverflow, + /// Incompatible number conversions. + InvalidNumberConversion, + /// Format error during number parsing. Typically a dot without digits (at least one is /// required). BadNumberFormat, @@ -132,11 +139,14 @@ pub enum Error { #[derive(Debug, PartialEq, Clone)] pub enum GCode { + StatusCommand, BlockDelete, - LineNumber(u32), + LineNumber(Option), #[cfg(feature = "parse-comments")] Comment(String), Word(char, RealValue), + #[cfg(feature = "string-value")] + Text(RealValue), #[cfg(feature = "parse-parameters")] /// When `optional-value` is enabled, the index cannot be `RealValue::None`. ParameterSet(RealValue, RealValue), diff --git a/src/parser.rs b/src/parser.rs index e36e94f..772aa4f 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -25,30 +25,38 @@ mod expressions; #[cfg(test)] mod test; -use futures::{Stream, StreamExt}; - use crate::{ stream::{MyTryStreamExt, PushBackable}, types::{Comment, ParseResult}, utils::skip_whitespaces, - Error, GCode, + Error, GCode, Literal, RealValue }; +#[cfg(feature = "future-stream")] +use futures::StreamExt; + +#[cfg(feature = "parse-checksum")] use values::parse_number; #[cfg(not(feature = "parse-expressions"))] use values::parse_real_value; +#[cfg(feature = "string-value")] +use crate::parser::values::parse_text; + #[cfg(feature = "parse-expressions")] use expressions::parse_real_value; +use crate::stream::{ByteStream, UnpinTrait}; #[derive(PartialEq, Debug, Clone, Copy)] -enum AsyncParserState { +pub enum AsyncParserState { Start(bool), LineNumberOrSegment, + #[cfg(feature = "string-value")] + TextMode, Segment, ErrorRecovery, - #[cfg(all(feature = "parse-trailing-comment", feature = "parse-checksum"))] + #[cfg(any(all(feature = "parse-trailing-comment", feature = "parse-checksum"), feature="string-value"))] EoLOrTrailingComment, #[cfg(any(feature = "parse-trailing-comment", feature = "parse-checksum"))] EndOfLine, @@ -57,7 +65,7 @@ enum AsyncParserState { #[cfg(all(feature = "parse-trailing-comment", not(feature = "parse-comments")))] async fn parse_eol_comment(input: &mut S) -> Option> where - S: Stream> + Unpin + PushBackable, + S: ByteStream> + PushBackable, { loop { let b = match input.next().await? { @@ -75,9 +83,9 @@ where } #[cfg(all(feature = "parse-trailing-comment", feature = "parse-comments"))] -async fn parse_eol_comment(input: &mut S) -> Option> +async fn parse_eol_comment(input: &mut S, line_count: &mut u32) -> Option> where - S: Stream> + Unpin + PushBackable, + S: ByteStream> + PushBackable, { let mut v = Vec::new(); loop { @@ -85,6 +93,7 @@ where match b { b'\r' | b'\n' => { input.push_back(b); + *line_count += 1; break Some(match String::from_utf8(v) { Ok(s) => ParseResult::Ok(s), Err(_) => Error::InvalidUTF8String.into(), @@ -98,7 +107,7 @@ where #[cfg(not(feature = "parse-comments"))] async fn parse_inline_comment(input: &mut S) -> Option> where - S: Stream> + Unpin + PushBackable, + S: ByteStream> + PushBackable + UnpinTrait, { loop { match try_result!(input.next()) { @@ -115,7 +124,7 @@ where #[cfg(feature = "parse-comments")] async fn parse_inline_comment(input: &mut S) -> Option> where - S: Stream> + Unpin + PushBackable, + S: ByteStream> + PushBackable + UnpinTrait, { let mut v = Vec::new(); loop { @@ -145,9 +154,10 @@ type PushBack = crate::stream::xorsum_pushback::XorSumPushBack; async fn parse_eol( state: &mut AsyncParserState, input: &mut PushBack, + line_count: &mut u32, ) -> Option> where - S: Stream> + Unpin, + S: ByteStream> + UnpinTrait, { Some(loop { let b = try_result!(input.next()); @@ -158,6 +168,7 @@ where { input.reset_sum(0); } + *line_count += 1; break ParseResult::Ok(GCode::Execute); } b' ' => {} @@ -187,15 +198,16 @@ macro_rules! try_await_result { pub struct Parser where - S: Stream> + Unpin, + S: ByteStream>, { input: PushBack, state: AsyncParserState, + line_count: u32, } impl Parser where - S: Stream> + Unpin, + S: ByteStream> + UnpinTrait, E: From, { pub fn new(input: S) -> Self { @@ -205,8 +217,35 @@ where #[cfg(not(feature = "parse-checksum"))] input: input.push_backable(), state: AsyncParserState::Start(true), + line_count: 0, } } + + pub async fn reset(&mut self) { + #[cfg(feature = "parse-checksum")] + self.input.reset_sum(0); + #[cfg(not(feature = "future-stream"))] + self.input.recovery_check().await; + self.state = AsyncParserState::Start(true); + } + + pub fn get_state(&self) -> AsyncParserState { + self.state + } + + #[cfg(feature = "string-value")] + pub fn switch_to_text(&mut self) { + self.state = AsyncParserState::TextMode; + } + + pub fn get_current_line(&self) -> u32 { + self.line_count + } + + pub fn update_current_line(&mut self, line: u32) { + self.line_count = line; + } + pub async fn next(&mut self) -> Option> { let res = loop { let b = match self.input.next().await? { @@ -217,9 +256,12 @@ where // println!("{:?}: {:?}", self.state, char::from(b)); match self.state { AsyncParserState::Start(ref mut first_byte) => match b { + b'?' => { + break Ok(GCode::StatusCommand); + } b'\n' => { self.input.push_back(b); - break Ok(try_await!(parse_eol(&mut self.state, &mut self.input))); + break Ok(try_await!(parse_eol(&mut self.state, &mut self.input, &mut self.line_count))); } b'/' if *first_byte => { self.state = AsyncParserState::LineNumberOrSegment; @@ -236,22 +278,43 @@ where AsyncParserState::LineNumberOrSegment => match b.to_ascii_lowercase() { b'n' => { try_await_result!(skip_whitespaces(&mut self.input)); - let (n, ord) = try_await_result!(parse_number(&mut self.input)); - break if ord == 1 { - let b = try_await_result!(self.input.next()); - Err(Error::UnexpectedByte(b).into()) - } else if ord > 10000 { - Err(Error::NumberOverflow.into()) - } else { - self.state = AsyncParserState::Segment; - Ok(GCode::LineNumber(n)) - }; + let rv = try_await!(parse_real_value(&mut self.input)); + match rv { + RealValue::Literal(Literal::RealNumber(_rn)) => { + if _rn.scale() > 0 { + break Err(Error::UnexpectedByte(b'.').into()); + } + else { + let lnum = match _rn.integer_part().try_into() { + Ok(_i) => Some(_i), + Err(_) => None + }; + break Ok(GCode::LineNumber(lnum)); + } + } + #[cfg(feature = "optional-value")] + RealValue::None => { + break Ok(GCode::LineNumber(None)); + } + #[cfg(feature = "optional-value")] + _ => { + break Err(Error::InvalidNumberConversion.into()); + } + } } _ => { self.input.push_back(b); self.state = AsyncParserState::Segment; } }, + #[cfg(feature="string-value")] + AsyncParserState::TextMode => { + self.input.push_back(b); + try_await_result!(skip_whitespaces(&mut self.input)); + let rv = try_await!(parse_text(&mut self.input)); + self.state = AsyncParserState::EoLOrTrailingComment; + break Ok(GCode::Text(RealValue::Literal(Literal::String(rv)))); + }, AsyncParserState::Segment => match b.to_ascii_lowercase() { b' ' => {} letter @ b'a'..=b'z' => { @@ -262,7 +325,7 @@ where } b'\r' | b'\n' => { self.input.push_back(b); - break Ok(try_await!(parse_eol(&mut self.state, &mut self.input))); + break Ok(try_await!(parse_eol(&mut self.state, &mut self.input, &mut self.line_count))); } // param support feature #[cfg(feature = "parse-parameters")] @@ -334,49 +397,48 @@ where } #[cfg(all(feature = "parse-trailing-comment", feature = "parse-comments"))] b';' => { - let s = try_await!(parse_eol_comment(&mut self.input)); + let s = try_await!(parse_eol_comment(&mut self.input, &mut self.line_count)); self.state = AsyncParserState::EndOfLine; break Ok(GCode::Comment(s)); } _ => break Err(Error::UnexpectedByte(b).into()), }, - #[cfg(all( - feature = "parse-trailing-comment", - not(feature = "parse-comments"), - feature = "parse-checksum" - ))] - AsyncParserState::EoLOrTrailingComment => match b { - b';' => try_await_result!(parse_eol_comment(&mut self.input)), - _ => { - self.input.push_back(b); - break Ok(try_await!(parse_eol(&mut self.state, &mut self.input))); - } - }, - #[cfg(all( - feature = "parse-trailing-comment", - feature = "parse-comments", - feature = "parse-checksum" - ))] - AsyncParserState::EoLOrTrailingComment => match b { - b';' => { - let s = try_await!(parse_eol_comment(&mut self.input)); - self.state = AsyncParserState::EndOfLine; - break Ok(GCode::Comment(s)); + #[cfg(any(all(feature = "parse-trailing-comment", feature = "parse-checksum"), feature="string-value"))] + AsyncParserState::EoLOrTrailingComment => { + #[cfg(not(feature = "parse-comments"))] + { + match b { + b';' => try_await_result!(parse_eol_comment(&mut self.input)), + _ => { + self.input.push_back(b); + break Ok(try_await!(parse_eol(&mut self.state, &mut self.input, &mut self.line_count))); + } + } } - _ => { - self.input.push_back(b); - break Ok(try_await!(parse_eol(&mut self.state, &mut self.input))); + #[cfg(feature = "parse-comments")] + { + match b { + b';' => { + let s = try_await!(parse_eol_comment(&mut self.input, &mut self.line_count)); + self.state = AsyncParserState::EndOfLine; + break Ok(GCode::Comment(s)); + } + _ => { + self.input.push_back(b); + break Ok(try_await!(parse_eol(&mut self.state, &mut self.input, &mut self.line_count))); + } + } } }, #[cfg(any(feature = "parse-trailing-comment", feature = "parse-checksum"))] AsyncParserState::EndOfLine => { self.input.push_back(b); - break Ok(try_await!(parse_eol(&mut self.state, &mut self.input))); + break Ok(try_await!(parse_eol(&mut self.state, &mut self.input, &mut self.line_count))); } AsyncParserState::ErrorRecovery => match b { b'\r' | b'\n' => { self.input.push_back(b); - break Ok(try_await!(parse_eol(&mut self.state, &mut self.input))); + break Ok(try_await!(parse_eol(&mut self.state, &mut self.input, &mut self.line_count))); } _ => {} }, diff --git a/src/parser/test.rs b/src/parser/test.rs index 653725c..cbf9bac 100644 --- a/src/parser/test.rs +++ b/src/parser/test.rs @@ -1,10 +1,10 @@ -use futures::stream; - -use super::{Error, GCode, Parser, StreamExt}; +use super::{Error, GCode, Parser}; #[cfg(feature = "optional-value")] use crate::types::RealValue; +use crate::DecimalRepr; + #[cfg(feature = "parse-checksum")] mod parse_checksum; #[cfg(feature = "parse-expressions")] @@ -15,6 +15,10 @@ mod parse_parameters; mod parse_trailing_comment; fn block_on>(it: T) -> Vec> { + + use futures::stream; + use futures::StreamExt; + let mut parser = Parser::new(stream::iter(it).map(Result::<_, Error>::Ok)); futures_executor::block_on( @@ -54,6 +58,9 @@ fn spaces_are_not_allowed_before_block_delete() { #[test] fn error_in_underlying_stream_are_passed_through_and_parser_recovers_on_execute() { + use futures::stream; + use futures::StreamExt; + #[derive(Debug, Copy, Clone, PartialEq)] enum TestError { SomeError, @@ -89,6 +96,7 @@ fn error_in_underlying_stream_are_passed_through_and_parser_recovers_on_execute( Ok(GCode::Execute) ] ) + } #[test] @@ -96,25 +104,25 @@ fn line_number() { let input = "n23\n".bytes(); assert_eq!( block_on(input), - &[Ok(GCode::LineNumber(23)), Ok(GCode::Execute)] + &[Ok(GCode::LineNumber(Some(23))), Ok(GCode::Execute)] ); let input = "N0023\n".bytes(); assert_eq!( block_on(input), - &[Ok(GCode::LineNumber(23)), Ok(GCode::Execute)] + &[Ok(GCode::LineNumber(Some(23))), Ok(GCode::Execute)] ); let input = " N 0023 \n".bytes(); assert_eq!( block_on(input), - &[Ok(GCode::LineNumber(23)), Ok(GCode::Execute)] + &[Ok(GCode::LineNumber(Some(23))), Ok(GCode::Execute)] ); } #[test] -fn line_number_with_more_than_5_digits_are_not_ok() { - let input = "N000009\n".bytes(); +fn line_number_with_more_than_9_digits_are_not_ok() { + let input = "N9999999999\n".bytes(); assert_eq!( block_on(input), &[Err(Error::NumberOverflow), Ok(GCode::Execute)] @@ -127,7 +135,6 @@ fn line_number_can_only_be_intergers() { assert_eq!( block_on(input), &[ - Ok(GCode::LineNumber(0)), Err(Error::UnexpectedByte(b'.')), Ok(GCode::Execute) ] @@ -136,8 +143,7 @@ fn line_number_can_only_be_intergers() { assert_eq!( block_on(input), &[ - Ok(GCode::LineNumber(9)), - Err(Error::UnexpectedByte(b'.')), + Ok(GCode::LineNumber(Some(9))), Ok(GCode::Execute) ] ); @@ -190,23 +196,24 @@ fn word_with_number() { block_on(input), &[ Ok(GCode::Execute), - Ok(GCode::Word('g', (21.0).into())), - Ok(GCode::Word('h', (21.0).into())), - Ok(GCode::Word('i', (21.098).into())), + Ok(GCode::Word('g', (21.0).try_into().unwrap())), + Ok(GCode::Word('h', (21.0).try_into().unwrap())), + Ok(GCode::Word('i', (21.098).try_into().unwrap())), Ok(GCode::Execute), - Ok(GCode::Word('j', (-21.0).into())), - Ok(GCode::Word('k', (-21.0).into())), - Ok(GCode::Word('l', (-21.098).into())), + Ok(GCode::Word('j', (-21.0).try_into().unwrap())), + Ok(GCode::Word('k', (-21.0).try_into().unwrap())), + Ok(GCode::Word('l', (-21.098).try_into().unwrap())), Ok(GCode::Execute), - Ok(GCode::Word('m', (21.0).into())), - Ok(GCode::Word('n', (21.0).into())), - Ok(GCode::Word('p', (21.098).into())), + Ok(GCode::Word('m', (21.0).try_into().unwrap())), + Ok(GCode::Word('n', (21.0).try_into().unwrap())), + Ok(GCode::Word('p', (21.098).try_into().unwrap())), Ok(GCode::Execute), - Ok(GCode::Word('q', (0.098).into())), - Ok(GCode::Word('r', (-0.098).into())), - Ok(GCode::Word('s', (0.098).into())), + Ok(GCode::Word('q', (0.098).try_into().unwrap())), + Ok(GCode::Word('r', (-0.098).try_into().unwrap())), + Ok(GCode::Word('s', (0.098).try_into().unwrap())), Ok(GCode::Execute), - Ok(GCode::Word('t', (-21.33).into())) + // avoid precision loss with f64 conversion in this case + Ok(GCode::Word('t', DecimalRepr::new(-2133, 2).try_into().unwrap())), ] ); } @@ -218,11 +225,11 @@ fn word_may_not_have_a_value() { assert_eq!( block_on(input), &[ - Ok(GCode::Word('g', (75.0).into())), + Ok(GCode::Word('g', (75.0).try_into().unwrap())), Ok(GCode::Word('z', RealValue::None)), - Ok(GCode::Word('t', (48.0).into())), + Ok(GCode::Word('t', (48.0).try_into().unwrap())), Ok(GCode::Word('s', RealValue::None)), - Ok(GCode::Word('p', (0.3).into())), + Ok(GCode::Word('p', (0.3).try_into().unwrap())), Ok(GCode::Execute) ] ); @@ -237,7 +244,7 @@ fn word_may_not_have_a_value_but_ambiguous_sequence_will_error() { assert_eq!( block_on(input), &[ - Ok(GCode::Word('g', (75.0).into())), + Ok(GCode::Word('g', (75.0).try_into().unwrap())), Ok(GCode::Word('z', RealValue::None)), Err(Error::UnexpectedByte(b'4')), Ok(GCode::Execute) diff --git a/src/parser/values.rs b/src/parser/values.rs index 8659124..75296d8 100644 --- a/src/parser/values.rs +++ b/src/parser/values.rs @@ -7,8 +7,6 @@ //! crate shows that there's no subtantial benefit in terms of flash size from using the fixed //! point arithmetics. -use futures::{Stream, StreamExt}; - #[cfg(all(not(feature = "std"), feature = "string-value"))] use alloc::string::String; @@ -17,30 +15,39 @@ use crate::{ types::{Literal, ParseResult}, utils::skip_whitespaces, Error, + DecimalRepr }; - +use crate::stream::{ByteStream, UnpinTrait}; #[cfg(not(feature = "parse-expressions"))] use crate::types::RealValue; #[cfg(any(feature = "parse-parameters", feature = "parse-expressions"))] pub use crate::types::expressions::{Expression, Operator}; -pub(crate) async fn parse_number(input: &mut S) -> Option> +#[cfg(feature = "future-stream")] +use futures::StreamExt; + +pub(crate) async fn parse_number(input: &mut S) -> Option> where - S: Stream> + Unpin + PushBackable, + S: ByteStream> + PushBackable + UnpinTrait, + E: core::convert::From, { let mut n = 0; - let mut order = 1; + let mut order = 0; let res = loop { let b = match input.next().await? { Ok(b) => b, - Err(e) => return Some(Err(e)), + Err(e) => { + return Some(Err(e))}, }; match b { b'0'..=b'9' => { let digit = u32::from(b - b'0'); + if order > 8 { + return Some(Err(crate::Error::NumberOverflow.into())); + } n = n * 10 + digit; - order *= 10; + order +=1; } _ => { input.push_back(b); @@ -51,18 +58,17 @@ where Some(res) } -async fn parse_real_literal(input: &mut S) -> Option> +async fn parse_real_literal(input: &mut S) -> Option> where - S: Stream> + Unpin + PushBackable, + S: ByteStream> + PushBackable + UnpinTrait, + E: core::convert::From, { // extract sign: default to positiv let mut b = try_result!(input.next()); - let mut negativ = false; if b == b'-' || b == b'+' { negativ = b == b'-'; - // skip spaces after the sign try_result!(skip_whitespaces(input)); b = try_result!(input.next()); @@ -91,21 +97,18 @@ where None }; - //println!( - //"literal done: {} {:?} {:?}", - //if negativ { '-' } else { '+' }, - //int, - //dec - //); - let res = if int.is_none() && dec.is_none() { ParseResult::Parsing(Error::BadNumberFormat.into()) } else { - let int = int.map(f64::from).unwrap_or(0.); - let (dec, ord) = dec - .map(|(dec, ord)| (dec.into(), ord.into())) - .unwrap_or((0., 1.)); - ParseResult::Ok((if negativ { -1. } else { 1. }) * (int + dec / ord)) + let sign = if negativ { -1i32 } else { 1i32 }; + let int = int.unwrap_or(0); + let (n, ord) = dec.unwrap_or((0u32,0u8)); + let scaler = u32::pow(10, ord as u32); + ParseResult::Ok( + DecimalRepr::new( + sign * (((int * scaler) + n) as i32), ord + ) + ) }; Some(res) } @@ -113,7 +116,7 @@ where #[cfg(feature = "string-value")] async fn parse_string_literal(input: &mut S) -> Option> where - S: Stream> + Unpin + PushBackable, + S: ByteStream> + PushBackable, { #[cfg(not(feature = "std"))] use alloc::vec::Vec; @@ -140,7 +143,8 @@ where pub(crate) async fn parse_literal(input: &mut S) -> Option> where - S: Stream> + Unpin + PushBackable, + S: ByteStream> + PushBackable + UnpinTrait, + E: core::convert::From, { let b = try_result!(input.next()); Some(match b { @@ -154,10 +158,48 @@ where }) } +#[cfg(feature = "string-value")] +pub(crate) async fn parse_text(input: &mut S) -> Option> +where + S: ByteStream> + PushBackable + UnpinTrait, + E: core::convert::From, +{ + #[cfg(not(feature = "std"))] + use alloc::vec::Vec; + + let mut array = Vec::new(); + loop { + match input.next().await { + Some(Ok(b';')) => { + input.push_back(b';'); + break; + } + Some(Ok(b'\n')) => { + input.push_back(b'\n'); + break; + } + Some(Ok(b)) => { + array.push(b) + } + Some(Err(e)) => { + return Some(Err(e).into()) + } + None => { + break; + } + } + } + match String::from_utf8(array) { + Ok(string) => Some(ParseResult::Ok(string)), + Err(_) => Some(Error::InvalidUTF8String.into()), + } +} + #[cfg(not(feature = "parse-expressions"))] pub(crate) async fn parse_real_value(input: &mut S) -> Option> where - S: Stream> + Unpin + PushBackable, + S: ByteStream> + PushBackable + UnpinTrait, + E: core::convert::From, { let b = try_result!(input.next()); // println!("real value: {:?}", b as char); diff --git a/src/stream.rs b/src/stream.rs index 9a9bbcc..de2515c 100644 --- a/src/stream.rs +++ b/src/stream.rs @@ -1,6 +1,55 @@ -use futures::TryStream; -pub(crate) trait MyTryStreamExt: TryStream { +#[cfg(feature = "future-stream")] +pub use core::marker::Unpin as UnpinTrait; + +#[cfg(feature = "future-stream")] +pub use futures::stream::Stream as ByteStream; + +#[cfg(feature = "future-stream")] +pub use futures::stream::TryStream as TryByteStream; + +#[cfg(not(feature = "future-stream"))] +pub trait UnpinTrait {} + +#[cfg(not(feature = "future-stream"))] +#[allow(async_fn_in_trait)] +pub trait ByteStream { + /// Values yielded by the stream. + type Item; + async fn next(&mut self) -> Option; + + // Used to notify inner stream when there is a reset due timeout or something + async fn recovery_check(&mut self); +} + +#[cfg(not(feature = "future-stream"))] +impl UnpinTrait for S where S: ByteStream {} + +#[cfg(not(feature = "future-stream"))] +#[allow(async_fn_in_trait)] +pub trait TryByteStream: ByteStream { + /// The type of successful values yielded by this future + type Ok; + + /// The type of failures yielded by this future + type Error; + async fn try_next(&mut self) -> Option>; +} + +#[cfg(not(feature = "future-stream"))] +impl TryByteStream for S +where + S: ?Sized + ByteStream> + UnpinTrait, +{ + type Ok = T; + type Error = E; + + async fn try_next(&mut self) -> Option> { + self.next().await + } +} + +pub(crate) trait MyTryStreamExt: TryByteStream { #[cfg(not(feature = "parse-checksum"))] fn push_backable(self) -> pushback::PushBack where @@ -21,7 +70,7 @@ pub(crate) trait MyTryStreamExt: TryStream { xorsum_pushback::XorSumPushBack::new(self, initial_sum) } } -impl MyTryStreamExt for T where T: TryStream {} +impl MyTryStreamExt for T where T: TryByteStream {} pub(crate) trait PushBackable { type Item; @@ -30,30 +79,40 @@ pub(crate) trait PushBackable { #[cfg(not(feature = "parse-checksum"))] pub(crate) mod pushback { - use futures::{Stream, TryStream}; - use pin_project_lite::pin_project; - - use core::pin::Pin; - use core::task::{Context, Poll}; + use super::{ByteStream, TryByteStream}; use super::PushBackable; - - pin_project! { - pub(crate) struct PushBack { + #[cfg(feature = "future-stream")] + use core::pin::Pin; + #[cfg(feature = "future-stream")] + use futures::task::Poll; + #[cfg(feature = "future-stream")] + use futures::task::Context; + + #[cfg(feature = "future-stream")] + pin_project_lite::pin_project! { + pub(crate) struct PushBack { #[pin] stream: S, val: Option, } } - impl PushBack { + + #[cfg(not(feature = "future-stream"))] + pub(crate) struct PushBack { + stream: S, + val: Option, + } + + impl PushBack { pub fn new(stream: S) -> Self { Self { stream, val: None } } } impl PushBackable for PushBack where - S: TryStream, + S: TryByteStream, { type Item = S::Ok; fn push_back(&mut self, v: S::Ok) -> Option { @@ -61,9 +120,10 @@ pub(crate) mod pushback { } } - impl Stream for PushBack + #[cfg(feature = "future-stream")] + impl ByteStream for PushBack where - S: TryStream, + S: TryByteStream, { type Item = Result; fn poll_next(self: Pin<&mut Self>, ctx: &mut Context<'_>) -> Poll> { @@ -76,6 +136,27 @@ pub(crate) mod pushback { } } + #[cfg(not(feature = "future-stream"))] + impl ByteStream for PushBack + where + S: TryByteStream, + { + type Item = Result; + + async fn next(&mut self) -> Option { + if let Some(v) = self.val.take() { + Some(Ok(v)) + } + else { + self.stream.try_next().await + } + } + + async fn recovery_check(&mut self) { + self.stream.recovery_check().await + } + } + #[cfg(test)] mod test { use super::{PushBack, PushBackable}; @@ -124,16 +205,18 @@ pub(crate) mod pushback { #[cfg(feature = "parse-checksum")] pub(crate) mod xorsum_pushback { - use futures::{Stream, TryStream}; - use pin_project_lite::pin_project; + use super::PushBackable; + use super::{ByteStream, TryByteStream}; + + #[cfg(feature = "future-stream")] use core::pin::Pin; + #[cfg(feature = "future-stream")] use core::task::{Context, Poll}; - use super::PushBackable; - - pin_project! { - pub(crate) struct XorSumPushBack { + #[cfg(feature = "future-stream")] + pin_project_lite::pin_project! { + pub(crate) struct XorSumPushBack { #[pin] stream: S, head: Option, @@ -141,9 +224,17 @@ pub(crate) mod xorsum_pushback { } } + #[cfg(not(feature = "future-stream"))] + pub(crate) struct XorSumPushBack { + stream: S, + head: Option, + sum: S::Ok + } + + impl XorSumPushBack where - S: TryStream, + S: TryByteStream, S::Ok: core::ops::BitXorAssign + Copy, { pub fn new(stream: S, initial_sum: S::Ok) -> Self { @@ -165,7 +256,7 @@ pub(crate) mod xorsum_pushback { impl PushBackable for XorSumPushBack where - S: TryStream, + S: TryByteStream, S::Ok: core::ops::BitXorAssign + Copy, { type Item = S::Ok; @@ -175,9 +266,10 @@ pub(crate) mod xorsum_pushback { } } - impl Stream for XorSumPushBack + #[cfg(feature = "future-stream")] + impl ByteStream for XorSumPushBack where - S: TryStream, + S: TryByteStream, S::Ok: core::ops::BitXorAssign + Copy, { type Item = Result; @@ -197,6 +289,34 @@ pub(crate) mod xorsum_pushback { } } + + #[cfg(not(feature = "future-stream"))] + impl ByteStream for XorSumPushBack + where + S: TryByteStream, + S::Ok: core::ops::BitXorAssign + Copy, + { + type Item = Result; + + async fn next(&mut self) -> Option { + let item = if let Some(item) = self.head.take() { + item + } else { + match self.stream.try_next().await { + Some(Ok(item)) => item, + other => return other, + } + }; + self.sum ^= item; + Some(Ok(item)) + } + + #[cfg(not(feature = "future-stream"))] + async fn recovery_check(&mut self) { + self.stream.recovery_check().await; + } + } + #[cfg(test)] mod test { use super::{PushBackable, XorSumPushBack}; diff --git a/src/types.rs b/src/types.rs index 9d085dc..2b08ce5 100644 --- a/src/types.rs +++ b/src/types.rs @@ -3,6 +3,8 @@ any(feature = "parse-comments", feature = "string-value") ))] use alloc::string::String; +#[cfg(feature = "float-f64")] +use core::f64; #[derive(Debug)] pub(crate) enum ParseResult { @@ -30,20 +32,90 @@ pub type Comment = (); #[cfg(feature = "parse-comments")] pub type Comment = String; +#[derive(Debug, Clone, Copy)] +pub struct DecimalRepr { + integer: i32, + scale: u8, +} + +impl PartialEq for DecimalRepr { + #[inline] + fn eq(&self, other: &Self) -> bool { + if self.scale == other.scale { + self.integer == other.integer + } + else if self.scale > other.scale { + self.integer == (other.integer * 10i32.pow((self.scale - other.scale) as u32)) + } + else { + other.integer == (self.integer * 10i32.pow((other.scale - self.scale) as u32)) + } + } +} + +impl Default for DecimalRepr { + fn default() -> Self { + Self { + integer: 0i32, + scale: 0u8, + } + } +} + +impl DecimalRepr { + pub const fn new(integer: i32, scale: u8) -> DecimalRepr { + DecimalRepr { + integer, + scale, + } + } + #[cfg(feature = "float-f64")] + /// Convert from f64 with a maximum of 8 decimal positions + pub fn from_f64(number: f64) -> DecimalRepr { + let integer = (number * 100000000.0f64).trunc() as i32; + DecimalRepr { + integer, + scale: 8, + } + } + #[cfg(feature = "float-f64")] + pub fn to_f64(&self ) -> f64 { + self.integer as f64 / (10u64.pow(self.scale as u32) as f64) + } + + #[inline] + pub fn integer_part(&self) -> i32 { + self.integer + } + + #[inline] + pub fn scale(&self) -> u8 { + self.scale + } +} + #[derive(Debug, PartialEq, Clone)] pub enum Literal { - RealNumber(f64), + RealNumber(DecimalRepr), #[cfg(feature = "string-value")] String(String), } impl Literal { - pub fn as_real_number(&self) -> Option { + pub fn as_decimal(&self) -> Option { match self { Literal::RealNumber(rn) => Some(*rn), #[cfg(feature = "string-value")] _ => None, } } + #[cfg(feature = "float-f64")] + pub fn as_real_number(&self) -> Option { + match self { + Literal::RealNumber(rn) => Some(rn.to_f64()), + #[cfg(feature = "string-value")] + _ => None, + } + } #[cfg(feature = "string-value")] pub fn as_string(&self) -> Option<&str> { match self { @@ -52,19 +124,47 @@ impl Literal { } } } + +impl From for Literal { + fn from(from: DecimalRepr) -> Self { + Self::RealNumber(from) + } +} + impl From for Literal { fn from(from: i32) -> Self { - Self::RealNumber(from as f64) + Self::RealNumber(DecimalRepr::new(from, 0)) } } -impl From for Literal { - fn from(from: u32) -> Self { - Self::RealNumber(from as f64) +impl TryFrom for Literal { + + type Error = crate::Error; + + fn try_from(value: u32) -> Result { + Ok(Self::RealNumber( + DecimalRepr::new( + i32::try_from(value) + .map_err(|_| crate::Error::InvalidNumberConversion)?, 0 + ) + )) } } -impl From for Literal { - fn from(from: f64) -> Self { - Self::RealNumber(from) +#[cfg(feature = "float-f64")] +impl TryFrom for Literal { + type Error = crate::Error; + + fn try_from(value: f64) -> Result { + // As parser constraints: 5 decimals max + let scaled_value = value * 100000.0; + if scaled_value > f64::from(i32::MAX) { + Err(crate::Error::InvalidNumberConversion) + } else if scaled_value < f64::from(i32::MIN) { + Err(crate::Error::InvalidNumberConversion) + } else { + Ok(Self::RealNumber( + DecimalRepr::new(scaled_value as i32, 5) + )) + } } } @@ -85,7 +185,7 @@ pub enum RealValue { } impl Default for RealValue { fn default() -> Self { - Self::from(0.) + DecimalRepr::new(0, 0).into() } } impl> From for RealValue { @@ -94,6 +194,15 @@ impl> From for RealValue { } } +#[cfg(feature = "float-f64")] +impl TryFrom for RealValue { + type Error = crate::Error; + + fn try_from(from: f64) -> Result { + Ok(RealValue::Literal(from.try_into()?)) + } +} + #[cfg(any(feature = "parse-parameters", feature = "parse-expressions"))] pub(crate) mod expressions { use super::{Literal, RealValue}; diff --git a/src/utils.rs b/src/utils.rs index d7dc5b4..ba384af 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -1,5 +1,7 @@ -use crate::stream::PushBackable; -use futures::{Stream, StreamExt}; +use crate::stream::{ByteStream, PushBackable, UnpinTrait}; + +#[cfg(feature = "future-stream")] +use futures::StreamExt; #[doc(hidden)] #[macro_export] @@ -26,7 +28,7 @@ macro_rules! try_result { pub(crate) async fn skip_whitespaces(input: &mut S) -> Option> where - S: Stream> + Unpin + PushBackable, + S: ByteStream> + PushBackable + UnpinTrait, { loop { let b = match input.next().await? { @@ -41,23 +43,32 @@ where Some(Ok(())) } -#[cfg(itest)] +#[cfg(test)] mod test { - use super::{skip_whitespaces, stream, StreamExt}; - - #[cfg(not(feature = "parse-checksum"))] - use crate::stream::pushback::PushBack; - #[cfg(feature = "parse-checksum")] - use crate::stream::xorsum_pushback::XorSumPushBack; + use super::{skip_whitespaces}; + use futures::stream; + use futures::StreamExt; #[test] fn skips_white_spaces_and_pushes_back_the_first_non_space_byte() { - let mut data = PushBack::new(stream::iter( + + #[cfg(not(feature = "parse-checksum"))] + use crate::stream::pushback::PushBack as PushBack; + #[cfg(feature = "parse-checksum")] + use crate::stream::xorsum_pushback::XorSumPushBack as PushBack; + + let iter = stream::iter( b" d" .iter() .copied() .map(Result::<_, core::convert::Infallible>::Ok), - )); + ); + + #[cfg(not(feature = "parse-checksum"))] + let mut data = PushBack::new(iter); + #[cfg(feature = "parse-checksum")] + let mut data = PushBack::new(iter, 0); + futures_executor::block_on(async move { skip_whitespaces(&mut data).await; assert_eq!(Some(Ok(b'd')), data.next().await);