diff --git a/PythonicGCodeMachine/GCode/Lexer.py b/PythonicGCodeMachine/GCode/Lexer.py index b6202a5ba35b43d5838fbc1397ea7fb4aa69fae4..90777dfc88ab97054f3cc9bb676e0aedb3ae89be 100644 --- a/PythonicGCodeMachine/GCode/Lexer.py +++ b/PythonicGCodeMachine/GCode/Lexer.py @@ -18,11 +18,16 @@ # #################################################################################################### +__all__ = ['GCodeLexerError', 'GCodeLexer, '] + #################################################################################################### import re -import PythonicGCodeMachine.Lexer as lexer +try: + import ply.lex as lexer +except ModuleNotFoundError: + import PythonicGCodeMachine.PythonLexYacc.lex as lexer #################################################################################################### @@ -50,7 +55,6 @@ class GCodeLexer: 'ARC_SINE', 'ARC_TANGENT', 'COSINE', - 'DECIMAL_POINT', 'DIVIDED_BY', 'EQUAL_SIGN', 'EXCLUSIVE_OR', @@ -58,7 +62,7 @@ class GCodeLexer: 'FIX_DOWN', 'FIX_UP', 'LEFT_BRACKET', - 'LEFT_PARENTHESIS', + # 'LEFT_PARENTHESIS', 'MINUS', 'MODULO', 'NATURAL_LOG_OF', @@ -67,7 +71,7 @@ class GCodeLexer: 'PLUS', 'POWER', 'RIGHT_BRACKET', - 'RIGHT_PARENTHESIS', + # 'RIGHT_PARENTHESIS', 'ROUND', 'SINE', 'SQUARE_ROOT', @@ -92,17 +96,16 @@ class GCodeLexer: t_ARC_COSINE = r'acos' t_ARC_SINE = r'asin' t_ARC_TANGENT = r'atan' - # t_BLOCK_DELETE = r'\/' + # t_BLOCK_DELETE = r'\/' # slash t_COSINE = r'cos' - t_DECIMAL_POINT = r'\.' - t_DIVIDED_BY = r'\/' + t_DIVIDED_BY = r'\/' # slash t_EQUAL_SIGN = r'=' t_EXCLUSIVE_OR = r'xor' t_E_RAISED_TO = r'exp' t_FIX_DOWN = r'fix' t_FIX_UP = r'fup' t_LEFT_BRACKET = r'\[' - t_LEFT_PARENTHESIS = r'\(' + # t_LEFT_PARENTHESIS = r'\(' t_MINUS = r'-' t_MODULO = r'mod' t_NATURAL_LOG_OF = r'ln' @@ -111,7 +114,7 @@ class GCodeLexer: t_PLUS = r'\+' t_POWER = r'\*\*' t_RIGHT_BRACKET = r'\]' - t_RIGHT_PARENTHESIS = r'\)' + # t_RIGHT_PARENTHESIS = r'\)' t_ROUND = r'round' t_SINE = r'sin' t_SQUARE_ROOT = r'sqrt' @@ -155,7 +158,7 @@ class GCodeLexer: def t_REAL(self, t): # r'((-)?((\d*\.\d+)(E[\+-]?\d+)?|([1-9]\d*E[\+-]?\d+)))' - r'((-)?\d*(\.)?\d+)' + r'((\+|-)?(\d+\.\d*|(\.)?\d+))' value = t.value if '.' in value: value = float(value) @@ -204,14 +207,20 @@ class GCodeLexer: self._lexer = lexer.lex( module=self, reflags=int(re.VERBOSE + re.IGNORECASE), + optimize=1, **kwargs, ) ############################################## + def input(self, data): + return self._lexer.input(data) + + ############################################## + def tokenize(self, data): - self._lexer.input(data) + self.input(data) while True: token = self._lexer.token() if not token: diff --git a/PythonicGCodeMachine/GCode/Parser.py b/PythonicGCodeMachine/GCode/Parser.py new file mode 100644 index 0000000000000000000000000000000000000000..af082f967f945d2ebaf6e17594203fb9507f5d15 --- /dev/null +++ b/PythonicGCodeMachine/GCode/Parser.py @@ -0,0 +1,288 @@ +#################################################################################################### +# +# PythonicGCodeMachine - @licence_header_description@ +# Copyright (C) 2018 Fabrice Salvaire +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# +#################################################################################################### + +__all__ = ['GCodeParserError', 'GCodeParser'] + +#################################################################################################### + +# https://rply.readthedocs.io/en/latest/ +from ply import yacc + +from .Lexer import GCodeLexer, GCodeLexerError + +#################################################################################################### + +class GCodeParserError(ValueError): + pass + +#################################################################################################### + +class GCodeParser: + + """Class to implement a CGode parser. + + For references, see + + * The NIST RS274NGC Interpreter — Version 3 — Appendix E. Production Rules for the RS274/NGC Language + * http://linuxcnc.org/docs/2.7/html/gcode/overview.html + + Production Language + + The symbols in the productions are mostly standard syntax notation. Meanings of the symbols + are: + + * ``=`` The symbol on the left of the equal sign is equivalent to the expression on the right + * ``+`` followed by + * ``|`` or + * ``.`` end of production (a production may have several lines) + * ``[]`` zero or one of the expression inside square brackets may occur + * ``{}`` zero to many of the expression inside curly braces may occur + * ``()`` exactly one of the expression inside parentheses must occur + + The productions are: + + * arc_tangent_combo = arc_tangent + expression + divided_by + expression . + * binary_operation = binary_operation1 | binary_operation2 | binary_operation3 . + * binary_operation1 = power . + * binary_operation2 = divided_by | modulo | times . + * binary_operation3 = and | exclusive_or | minus | non_exclusive_or | plus . + * comment = message | ordinary_comment . + * expression = left_bracket + real_value + { binary_operation + real_value } + right_bracket . + * line = [block_delete] + [line_number] + {segment} + end_of_line . + * line_number = letter_n + digit + [digit] + [digit] + [digit] + [digit] . + * message = left_parenthesis + {white_space} + letter_m + {white_space} + letter_s + + {white_space} + letter_g + {white_space} + comma + {comment_character} + + right_parenthesis . + * mid_line_letter = letter_a | letter_b | letter_c| letter_d | letter_f | letter_g | letter_h | letter_i + | letter_j | letter_k | letter_l | letter_m | letter_p | letter_q | letter_r | letter_s | letter_t + | letter_x | letter_y | letter_z . + * mid_line_word = mid_line_letter + real_value . + * ordinary_comment = left_parenthesis + {comment_character} + right_parenthesis . + * ordinary_unary_combo = ordinary_unary_operation + expression . + * ordinary_unary_operation = + absolute_value | arc_cosine | arc_sine | cosine | e_raised_to | + fix_down | fix_up | natural_log_of | round | sine | square_root | tangent . + * parameter_index = real_value . + * parameter_setting = parameter_sign + parameter_index + equal_sign + real_value . + * parameter_value = parameter_sign + parameter_index . + * real_number = + [ plus | minus ] + + (( digit + { digit } + [decimal_point] + {digit}) | ( decimal_point + digit + {digit})) . + * real_value = real_number | expression | parameter_value | unary_combo . + * segment = mid_line_word | comment | parameter_setting . + * unary_combo = ordinary_unary_combo | arc_tangent_combo . + + """ + + ############################################## + + # Start symbol + def p_line(self, p): + '''line : DIVIDED_BY line_right + | line_right + ''' + + def p_line_right(self, p): + '''line_right : line_content + | line_content EOF_COMMENT + ''' + + def p_line_content(self, p): + 'line_content : segments' + # p[0] = + + def p_numbered_line(self, p): + 'line_content : line_number segments' + + def p_line_number(self, p): + '''line_number : N POSITIVE_INTEGER + | N POSITIVE_REAL + ''' + + def p_segments(self, p): + '''segments : segment + | segments segment + ''' + + def p_segment(self, p): + '''segment : mid_line_word + | comment + | parameter_setting + ''' + + ############################################## + + def p_comment(self, p): + 'comment : ordinary_comment' + # 'comment : message | ordinary_comment': + + def p_ordinary_comment(self, p): + 'ordinary_comment : INLINE_COMMENT' + + # def p_message(self, p): + # 'message : left_parenthesis + {white_space} + letter_m + {white_space} + letter_s + + # {white_space} + letter_g + {white_space} + comma + {comment_character} + + # right_parenthesis . + + ############################################## + + def p_mid_line_word(self, p): + 'mid_line_word : mid_line_letter real_value' + + def p_mid_line_letter(self, p): + '''mid_line_letter : A + | B + | C + | D + | F + | G + | H + | I + | J + | K + | L + | M + | P + | Q + | R + | S + | T + | X + | Y + | Z + ''' + + def p_parameter_setting(self, p): + 'parameter_setting : PARAMETER_SIGN parameter_index EQUAL_SIGN real_value' + + def p_parameter_value(self, p): + 'parameter_value : PARAMETER_SIGN parameter_index' + + def p_parameter_index(self, p): + 'parameter_index : real_value' + + def p_real_value(self, p): + '''real_value : POSITIVE_INTEGER + | POSITIVE_REAL + | REAL + | expression + | parameter_value + | unary_combo + ''' + + ############################################## + + def p_unary_combo(self, p): + '''unary_combo : ordinary_unary_combo + | arc_tangent_combo + ''' + + def p_ordinary_unary_combo(self, p): + 'ordinary_unary_combo : ordinary_unary_operation expression' + + def p_expression(self, p): + 'expression : LEFT_BRACKET inner_expression RIGHT_BRACKET' + + def p_inner_expression(self, p): + '''inner_expression : real_value + | inner_expression binary_operation real_value + ''' + + def p_arc_tangent_combo(self, p): + # atan[1.5]/[1.0] + 'arc_tangent_combo : ARC_TANGENT expression DIVIDED_BY expression' + + def p_ordinary_unary_operation(self, p): + '''ordinary_unary_operation : ABSOLUTE_VALUE + | ARC_COSINE + | ARC_SINE + | COSINE + | E_RAISED_TO + | FIX_DOWN + | FIX_UP + | NATURAL_LOG_OF + | ROUND + | SINE + | SQUARE_ROOT + | TANGENT + ''' + + def p_binary_operation(self, p): + '''binary_operation : binary_operation1 + | binary_operation2 + | binary_operation3 + ''' + + def p_binary_operation1(self, p): + 'binary_operation1 : POWER' + + def p_binary_operation2(self, p): + '''binary_operation2 : DIVIDED_BY + | MODULO + | TIMES + ''' + + def p_binary_operation3(self, p): + '''binary_operation3 : AND + | EXCLUSIVE_OR + | MINUS + | NON_EXCLUSIVE_OR + | PLUS + ''' + + ############################################## + + # def p_empty(self, p): + # 'empty :' + # pass + + ############################################## + + def p_error(self, p): + raise GCodeParserError(p.lexpos) + + ############################################## + + def __init__(self): + self._build() + + ############################################## + + def _build(self, **kwargs): + """Build the parser""" + + self._lexer = GCodeLexer() + self.tokens = self._lexer.tokens + self._parser = yacc.yacc( + module=self, + # debug=True, + optimize=0, + ) + + ############################################## + + def parse(self, line): + + line = line.strip() + ast = self._parser.parse( + line, + lexer=self._lexer._lexer, + # debug=True, + ) diff --git a/PythonicGCodeMachine/PythonLexYacc/__init__.py b/PythonicGCodeMachine/PythonLexYacc/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/PythonicGCodeMachine/Lexer/__init__.py b/PythonicGCodeMachine/PythonLexYacc/lex.py similarity index 100% rename from PythonicGCodeMachine/Lexer/__init__.py rename to PythonicGCodeMachine/PythonLexYacc/lex.py diff --git a/requirements.txt b/requirements.txt index 17bf7fdca15b5a1079119e2ae2ac63fcb0957fab..954b8b705610bab484efd5afbded58ca48d3dd57 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1 +1,2 @@ -PyYAML>=3.10 +ply>=3.11 +PyYAML>=3.13 diff --git a/unit-test/GCode/test_Lexer.py b/unit-test/GCode/test_Lexer.py deleted file mode 100644 index 3cdb4e7afe152f8aec44ea289f888803c696df81..0000000000000000000000000000000000000000 --- a/unit-test/GCode/test_Lexer.py +++ /dev/null @@ -1,64 +0,0 @@ -#################################################################################################### -# -# PythonicGCodeMachine - @licence_header_description@ -# Copyright (C) 2018 Fabrice Salvaire -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . -# -#################################################################################################### - -#################################################################################################### - -import unittest - -#################################################################################################### - -from PythonicGCodeMachine.GCode.Lexer import GCodeLexer - -#################################################################################################### - -class TestGCodeLexer(unittest.TestCase): - - ############################################## - - def test_gcode_lexer(self): - - lexer = GCodeLexer() - - for gcode in ( - 'G0 X0 Y0 Z0', - 'g0 x0 y0 z0', - 'G0X0Y0Z0', - - 'N1 G0 X0 Y0 Z0', - 'N2 G0 X1.0 Y0 Z0', - 'N3.1 G0 X1.0 Y0 Z0', - - 'N3.1 G0 X1.0 Y0 Z0 ; a eof comment', - 'N3.1 (comment 1) G0 (comment 2) X1.0 (comment 3) Y0 (comment 4) Z0 ; a eof comment', - ): - tokens = lexer.tokenize(gcode) - print(gcode, list(tokens)) - - for gcode in ( - 'G0 (comment (wrong) 2) X0', - ): - tokens = lexer.tokenize(gcode) - print(gcode, list(tokens)) - -#################################################################################################### - -if __name__ == '__main__': - - unittest.main() diff --git a/unit-test/GCode/test_Parser.py b/unit-test/GCode/test_Parser.py new file mode 100644 index 0000000000000000000000000000000000000000..dc9f4119765256cb565b6383a76da36b0e9f5ca4 --- /dev/null +++ b/unit-test/GCode/test_Parser.py @@ -0,0 +1,134 @@ +#################################################################################################### +# +# PythonicGCodeMachine - @licence_header_description@ +# Copyright (C) 2018 Fabrice Salvaire +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# +#################################################################################################### + +#################################################################################################### + +import unittest + +#################################################################################################### + +from PythonicGCodeMachine.GCode.Lexer import GCodeLexer, GCodeLexerError +from PythonicGCodeMachine.GCode.Parser import GCodeParser, GCodeParserError + +#################################################################################################### + +def print_rule(): + print() + print('#'*100) + print() + + +#################################################################################################### + +class TestGCodeLexer(unittest.TestCase): + + ############################################## + + def test_gcode_lexer(self): + + print_rule() + + lexer = GCodeLexer() + + for gcode in ( + 'G0 X0 Y0 Z0', + 'g0 x0 y0 z0', + 'G0X0Y0Z0', + + r'/ G0 X0 Y0 Z0', + + 'N1 G0 X0 Y0 Z0', + 'N2 G0 X1.0 Y0 Z0', + 'N3.1 G0 X1.0 Y0 Z0', + + 'N3.1 G0 X1.0 Y0 Z0 ; a eof comment', + 'N3.1 (comment 1) G0 (comment 2) X1.0 (comment 3) Y0 (comment 4) Z0 ; a eof comment', + + '#3=1. G0 X [ 1 + acos[0] - [#3 ** [4.0/2]]]' + ): + print() + print(gcode) + try: + tokens = lexer.tokenize(gcode) + print(list(tokens)) + except GCodeLexerError as exception: + position, = exception.args + print(' ' * position + '^') + print('Lexer Error') + raise exception + + for gcode in ( + 'G0 (comment (wrong) 2) X0', + ): + with self.assertRaises(GCodeLexerError): + list(lexer.tokenize(gcode)) + +#################################################################################################### + +class TestGCodeParser(unittest.TestCase): + + ############################################## + + def test_gcode_parser(self): + + print_rule() + + parser = GCodeParser() + + for gcode in ( + 'G0 X0 Y0 Z0', + 'g0 x0 y0 z0', + 'G0X0Y0Z0', + + r'/ G0 X0 Y0 Z0', + + 'N1 G0 X0 Y0 Z0', + 'N2 G0 X1.0 Y0 Z0', + 'N3.1 G0 X1.0 Y0 Z0', + + 'N3.1 G0 X1.0 Y0 Z0 ; a eof comment', + 'N3.1 (comment 1) G0 (comment 2) X1.0 (comment 3) Y0 (comment 4) Z0 ; a eof comment', + + '#3=1. G0 X#3 Y0' + 'G0 #3=1. X#3 Y0' + + '#3=1. G0 X [ 1 + acos[0] - [#3 ** [4.0/2]]]' + ): + print() + print(gcode) + try: + parser.parse(gcode) + except GCodeParserError as exception: + position, = exception.args + print(' ' * position + '^') + print('Parser Error') + raise exception + + # for gcode in ( + # 'G0 (comment (wrong) 2) X0', + # ): + # with self.assertRaises(GCodeLexerError): + # list(lexer.tokenize(gcode)) + +#################################################################################################### + +if __name__ == '__main__': + + unittest.main()