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()