g_code/parse/
parser.rs

1use rust_decimal::Error;
2
3fn decimal_err_into_str(err: Error) -> &'static str {
4    match err {
5        Error::ExceedsMaximumPossibleValue => {
6            "number is exceeds than the maximum that can be represented"
7        }
8        Error::LessThanMinimumPossibleValue => {
9            "number is less than the minimum that can be represented"
10        }
11        Error::ScaleExceedsMaximumPrecision(_) => {
12            "precision necessary to represent exceeds the maximum possible"
13        }
14        Error::ErrorString(_) => "cannot parse as decimal (unable to display root cause)",
15        Error::Underflow => "number contains more fractional digits than can be represented",
16        Error::ConversionTo(_) => "cannot convert to/from decimal type",
17    }
18}
19
20peg::parser! {
21    pub grammar g_code() for str {
22        use super::super::token::*;
23        use super::super::ast::*;
24        use rust_decimal::Decimal;
25
26        pub rule newline() -> Newline = pos:position!() inner:(quiet!{ $("\r\n" / "\r" / "\n") } / expected!("newline")) {
27            Newline {
28                pos
29            }
30        };
31        pub rule dot() -> &'input str = quiet! { $(".") } / expected!("decimal point");
32        pub rule star() -> &'input str = quiet!{ $("*") } / expected!("checksum asterisk");
33        pub rule minus() -> &'input str = quiet!{ $("-") } / expected!("minus sign");
34        pub rule percent() -> Percent = pos:position!()  inner:(quiet! { $("%") } / expected!("percent sign")) {
35            Percent {
36                pos,
37            }
38        };
39        rule quotation_mark() -> &'input str = quiet! { $("\"") } / expected!("quotation mark");
40        rule ascii_except_quote_or_newline() -> &'input str = quiet! { $(['\t'  | ' '..='!'| '#'..='~']*) } / expected!("ASCII character except quote or newline");
41        pub rule string() -> &'input str = $(quotation_mark() ascii_except_quote_or_newline() ((quotation_mark() quotation_mark())+ ascii_except_quote_or_newline())* quotation_mark());
42        rule ascii_except_closing_parenthesis_or_newline() -> &'input str = quiet! { $(['\t' | ' '..='(' | '*'..='~']*) } / expected!("ASCII character except closing parenthesis or newline");
43        rule opening_parenthesis() -> &'input str = quiet! { $("(") } / expected!("opening parenthesis");
44        rule closing_parenthesis() -> &'input str = quiet! { $(")") } / expected!("closing parenthesis");
45
46        rule inline_comment_raw() -> &'input str = precedence! {
47            x:$(opening_parenthesis() inline_comment_raw() closing_parenthesis()) { x }
48            --
49            x:$(opening_parenthesis() ascii_except_closing_parenthesis_or_newline() closing_parenthesis()) { x }
50        };
51        pub rule inline_comment() -> InlineComment<'input> = pos:position!() inner:inline_comment_raw() {
52            InlineComment {
53                inner,
54                pos,
55            }
56        };
57        rule ascii_character_except_newline() -> &'input str = quiet!{ $(['\t' | ' '..='~']*) } / expected!("ASCII character");
58        rule semicolon() -> &'input str = quiet! { $(";") } / expected!("semicolon");
59        pub rule comment() -> Comment<'input> = pos:position!() inner:$(semicolon() ascii_character_except_newline()) {
60            Comment {
61                inner,
62                pos,
63            }
64        };
65        pub rule integer() -> &'input str = quiet! { $(['0'..='9']+) } / expected!("integer");
66        pub rule letter() -> &'input str = quiet! { $(['a'..='z' | 'A'..='Z']) } / expected!("letter");
67        pub rule letters() -> &'input str = quiet! { $(['a'..='z' | 'A'..='Z']+) } / expected!("letters");
68        pub rule whitespace() -> Whitespace<'input> = pos:position!() inner:(quiet! { $([' ' | '\t' ]+) } / expected!("whitespace")) {
69            Whitespace {
70                inner,
71                pos,
72            }
73        };
74
75        rule checksum() -> Checksum = left:position!() star:star() checksum:integer() right:position!() {?
76            Ok(Checksum {
77                inner: checksum.parse::<u8>().map_err(|e| "checksum is not an unsigned byte")?,
78                span: Span(left, right)
79            })
80        };
81
82        rule field() -> Field<'input>
83            = left:position!() letters:letters() neg:minus()? lhs:integer()  dot:dot() rhs:integer()? right:position!() {?
84                let lhs_start = left + letters.len() + neg.as_ref().map(|_| 1).unwrap_or(0);
85                let lhs_end = lhs_start + lhs.len();
86                let rhs_start = lhs_end + 1;
87                let rhs_end = rhs_start + rhs.map(|x| x.len()).unwrap_or(0);
88                Ok(Field {
89                    letters,
90                    value: Value::Rational(lhs.parse::<Decimal>()
91                        .map_err(decimal_err_into_str)
92                        .and_then(|lhs| if let Some(rhs_str) = rhs {
93                            rhs_str.parse::<i64>()
94                                .map_err(|e| "fractional part does not fit in an i64")
95                                .and_then(|rhs| if rhs_str.len() > 28 { Err("scale is higher than allowed maximum precision") } else { Ok(rhs)})
96                                .map(|rhs| Decimal::new(rhs, rhs_str.len() as u32))
97                                .map(|rhs| lhs + rhs)
98                                .map(|value| if neg.is_some() { -value } else { value })
99                        } else {
100                            Ok(
101                                if neg.is_some() { -lhs } else { lhs }
102                            )
103                        })?),
104                    raw_value: if neg.is_some() { vec!["-", lhs, ".", rhs.unwrap_or("")] } else { vec![lhs, ".", rhs.unwrap_or("")] },
105                    span: Span(left, right)
106                })
107            }
108            / left:position!() letters:letters() neg:minus()? dot:dot() rhs_str:integer() right:position!() {?
109                let rhs_start = left + letters.len() + neg.as_ref().map(|_| 1).unwrap_or(0) + 1;
110                let rhs_end = right;
111                Ok(Field {
112                    letters,
113                    value: Value::Rational(rhs_str.parse::<i64>()
114                        .map_err(|e| "fractional part does not fit in an i64")
115                        .and_then(|rhs| if rhs_str.len() > 28 { Err("scale is higher than allowed maximum precision") } else { Ok(rhs)})
116                        .map(|rhs| Decimal::new(rhs, rhs_str.len() as u32))
117                        .map(|rhs| if neg.is_some() { -rhs } else { rhs })?),
118                    raw_value: if neg.is_some() { vec!["-", ".", rhs_str] } else { vec![".", rhs_str] },
119                    span: Span(left, right)
120                })
121            }
122            / left:position!() letters:letters() value:integer() right:position!() {?
123                Ok(Field {
124                    letters,
125                    value: Value::Integer(value.parse::<usize>().map_err(|e| "integer does not fit in usize")?),
126                    raw_value: vec![value],
127                    span: Span(left, right)
128                })
129            }
130
131            / left:position!() letters:letters() minus:minus() value:integer() right:position!() {?
132                let value_start = left + letters.len() + 1;
133                let value_end = right;
134                Ok(Field {
135                    letters,
136                    value: Value::Rational(-value.parse::<Decimal>().map_err(decimal_err_into_str)?),
137                    raw_value: vec!["-", value],
138                    span: Span(left, right)
139                })
140            }
141            / left:position!() letters:letters() string:string() right:position!() {
142                Field {
143                    letters,
144                    value: Value::String(string),
145                    raw_value: vec![string],
146                    span: Span(left, right)
147                }
148            };
149
150        pub rule flag() -> Flag<'input> = left:position!() letter:letter() right:position!() {
151            Flag { letter, span: Span(left, right) }
152        };
153
154        rule line_component() -> LineComponent<'input>
155            = field:field() { LineComponent { field: Some(field), ..Default::default() } }
156            / flag:flag() { LineComponent { flag: Some(flag), ..Default::default() }}
157            / whitespace:whitespace() { LineComponent { whitespace: Some(whitespace), ..Default::default() } }
158            / inline_comment:inline_comment() { LineComponent { inline_comment: Some(inline_comment), ..Default::default() } };
159
160        pub rule line() -> Line<'input> =
161                left:position!()
162                     // Hacky way of imitating lalrpop following https://github.com/kevinmehall/rust-peg/blob/master/peg-macros/grammar.rustpeg#L90
163                    line_components:line_component()*
164                    checksum:checksum()?
165                    comment:comment()?
166                right:position!() {
167            Line {
168                line_components,
169                checksum,
170                comment,
171                span: Span(left, right)
172            }
173        };
174
175        /// Parse a g-code file
176        pub rule file_parser() -> File<'input>
177            = left:position!() start_percent:percent() lines:(a:line() b:newline() { (a, b) })* last_line:line() end_percent:percent() right:position!() {
178                File {
179                    percents: vec![start_percent, end_percent],
180                    lines,
181                    last_line: if last_line.line_components.is_empty() && last_line.checksum.is_none() && last_line.comment.is_none() {
182                        None
183                    } else {
184                        Some(last_line)
185                    },
186                    span: Span(left, right)
187                }
188            }
189            / left:position!() lines:(a:line() b:newline() { (a, b) })* last_line:line() right:position!() {
190                File {
191                    percents: vec![],
192                    lines,
193                    last_line: if last_line.line_components.is_empty() && last_line.checksum.is_none() && last_line.comment.is_none() {
194                        None
195                    } else {
196                        Some(last_line)
197                    },
198                    span: Span(left, right)
199                }
200            };
201
202        /// The snippet parser is identical to the [file_parser], but it does not allow a leading and trailing percent symbol
203        pub rule snippet_parser() -> Snippet<'input> = left:position!() lines:(a:line() b:newline() { (a, b) })* last_line:line() right:position!() {
204            Snippet {
205                lines,
206                last_line: if last_line.line_components.is_empty() && last_line.checksum.is_none() && last_line.comment.is_none() {
207                    None
208                } else {
209                    Some(last_line)
210                },
211                span: Span(left, right)
212            }
213        }
214    }
215}