####################################################################################################
#
# PythonicGcodeMachine - A Python G-code Toolkit
# 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 .
#
####################################################################################################
"""Module to implement a RS-274 G-code parser.
Usage::
parser = GcodeParser()
ast_line = parser.parse(gcode_line)
ast_program = parser.parse_lines(gcode_lines)
**Implementation**
The parser is generated automatically from the grammar defined in this class using the generator
`PLY `_ which implement a LALR(1) parser similar to the
tools **lex** and **yacc**.
The parser construct an `abstract syntax tree (AST)
`_ during the parsing.
User can subclass this parser to support a derived G-code flavour.
**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
"""
__all__ = [
'GcodeParserError',
'GcodeParser',
'GcodeParserMixin',
'GcodeGrammarMixin',
]
####################################################################################################
from pathlib import Path
# https://rply.readthedocs.io/en/latest/
from ply import yacc
from . import Ast
from .Lexer import GcodeLexer
####################################################################################################
class GcodeParserError(ValueError):
pass
####################################################################################################
class GcodeGrammarMixin:
"""Mixin to implement the grammar.
**Production Language for RS-274**
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 .
"""
# Build the operation map
# Note: sphinx show locals if not _foo
__operation_map__ = {}
for _cls_name in Ast.__all__:
_cls = getattr(Ast, _cls_name)
if hasattr(_cls, '__gcode__'):
__operation_map__[_cls.__gcode__] = _cls
##############################################
# Start symbol
def p_line(self, p):
'''line : DIVIDED_BY line_right
| line_right
'''
if len(p) == 3:
self._line.deleted = True
# p[0] = self._line
def p_line_right(self, p):
'''line_right : line_content
| line_content EOF_COMMENT
'''
if len(p) == 3:
self._line.comment = p[2]
# p[0] = self._line
def p_line_content(self, p):
'line_content : segments'
# p[0] = self._line
def p_numbered_line(self, p):
'line_content : line_number segments'
self._line.line_number = p[1]
# p[0] = self._line
def p_line_number(self, p):
'''line_number : N POSITIVE_INTEGER
| N POSITIVE_REAL
'''
p[0] = p[2]
def p_segments(self, p):
'''segments : segment
| segments segment
'''
if len(p) == 2:
self._line += p[1]
else:
self._line += p[2]
def p_segment(self, p):
'''segment : mid_line_word
| comment
| parameter_setting
'''
p[0] = p[1]
##############################################
def p_comment(self, p):
'comment : ordinary_comment'
# 'comment : message | ordinary_comment':
p[0] = p[1]
def p_ordinary_comment(self, p):
'ordinary_comment : INLINE_COMMENT'
p[0] = Ast.Comment(p[1], self._machine)
# 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'
p[0] = Ast.Word(p[1], p[2], self._machine)
def p_mid_line_letter(self, p):
# LETTER
'''mid_line_letter : A
| B
| C
| D
| F
| G
| H
| I
| J
| K
| L
| M
| P
| Q
| R
| S
| T
| X
| Y
| Z
'''
p[0] = str(p[1])
def p_parameter_setting(self, p):
'parameter_setting : PARAMETER_SIGN parameter_index EQUAL_SIGN real_value'
p[0] = Ast.ParameterSetting(p[2], p[4], self._machine)
def p_parameter_value(self, p):
'parameter_value : PARAMETER_SIGN parameter_index'
p[0] = Ast.Parameter(p[2], self._machine)
def p_parameter_index(self, p):
'parameter_index : real_value'
p[0] = p[1]
def p_real_value(self, p):
'''real_value : POSITIVE_INTEGER
| POSITIVE_REAL
| REAL
| expression
| parameter_value
| unary_combo
'''
p[0] = p[1]
##############################################
def p_unary_combo(self, p):
'''unary_combo : ordinary_unary_combo
| arc_tangent_combo
'''
p[0] = p[1]
def p_ordinary_unary_combo(self, p):
'ordinary_unary_combo : ordinary_unary_operation expression'
p[0] = self.__operation_map__[p[1]](p[2])
def p_expression(self, p):
'expression : LEFT_BRACKET inner_expression RIGHT_BRACKET'
p[0] = p[2]
def p_inner_expression(self, p):
'''inner_expression : real_value
| inner_expression binary_operation real_value
'''
if len(p) == 2:
p[0] = p[1]
else:
p[0] = self.__operation_map__[p[2]](p[1], p[3])
def p_arc_tangent_combo(self, p):
# atan[1.5]/[1.0]
'arc_tangent_combo : ARC_TANGENT expression DIVIDED_BY expression'
p[0] = DividedBy(ArcTangent(p[2]), p[4])
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
'''
p[0] = p[1]
def p_binary_operation(self, p):
'''binary_operation : binary_operation1
| binary_operation2
| binary_operation3
'''
p[0] = p[1]
def p_binary_operation1(self, p):
'binary_operation1 : POWER'
p[0] = p[1]
def p_binary_operation2(self, p):
'''binary_operation2 : DIVIDED_BY
| MODULO
| TIMES
'''
p[0] = p[1]
def p_binary_operation3(self, p):
'''binary_operation3 : AND
| EXCLUSIVE_OR
| MINUS
| NON_EXCLUSIVE_OR
| PLUS
'''
p[0] = p[1]
##############################################
# def p_empty(self, p):
# 'empty :'
# pass
##############################################
def p_error(self, p):
raise GcodeParserError(p.lexpos)
####################################################################################################
class GcodeParserMixin:
"""Mixin to implement a RS-274 G-code parser"""
__lexer_cls__ = GcodeLexer
##############################################
def __init__(self, machine=None):
self._machine = machine
self._build()
self._reset()
##############################################
@property
def machine(self):
return self._machine
##############################################
def _reset(self):
self._line = None
##############################################
def _build(self, **kwargs):
"""Build the parser"""
self._lexer = self.__lexer_cls__()
self.tokens = self._lexer.tokens
self._parser = yacc.yacc(
module=self,
debug=False,
optimize=1,
tabmodule='_parsetab',
# outputdir=Path(__file__).parent.joinpath('ply'),
)
##############################################
def parse(self, line):
"""Parse a G-code line.
Return a :class:`PythonicGcodeMachine.Gcode.Rs274.Ast.Line` instance.
"""
line = line.strip()
self._line = Ast.Line(machine=self._machine)
ast = self._parser.parse(
line,
lexer=self._lexer._lexer,
# debug=True,
)
line = self._line
self._reset()
return line
##############################################
def parse_lines(self, lines):
"""Parse a G-code lines
Return a :class:`PythonicGcodeMachine.Gcode.Rs274.Ast.Program` instance.
"""
if not isinstance(lines, (list, tuple)):
lines = lines.split('\n')
program = Ast.Program(machine=self._machine)
for line in lines:
try:
program += self.parse(line)
except GcodeParserError as exception:
print('Parse Error:', line)
raise exception
return program
####################################################################################################
class GcodeParser(GcodeParserMixin, GcodeGrammarMixin):
pass