Skip to main content

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