Python Lex-Yacc

Введение

Примеры

Начало работы с PLY

Чтобы установить PLY на вашем компьютере для python2 / 3, выполните действия, описанные ниже:

  1. Скачать исходный код здесь .
  2. Распакуйте загруженный zip-файл
  3. Перейдите в распакованный ply-3.10 папку
  4. Выполните следующую команду в терминале: python setup.py install

Если вы выполнили все вышеперечисленное, теперь вы сможете использовать модуль PLY. Вы можете проверить это, открыв интерпретатор питона и набрав import ply.lex .

Примечание: Не используйте pip установить PLY, он установит битый дистрибутив на вашей машине.

"Привет, мир!" PLY - простой калькулятор

Давайте продемонстрируем силу PLY на простом примере: эта программа примет арифметическое выражение в качестве строкового ввода и попытается его решить.

Откройте ваш любимый редактор и скопируйте следующий код:

from ply import lex
import ply.yacc as yacc

tokens = (
    'PLUS',
    'MINUS',
    'TIMES',
    'DIV',
    'LPAREN',
    'RPAREN',
    'NUMBER',
)

t_ignore = '\t'

t_PLUS    = r'\+'
t_MINUS   = r'-'
t_TIMES   = r'\*'
t_DIV     = r'/'
t_LPAREN  = r'\('
t_RPAREN  = r'\)'

def t_NUMBER( t ) :
    r'[0-9]+'
    t.value = int( t.value )
    return t

def t_newline( t ):
  r'\n+'
  t.lexer.lineno += len( t.value )

def t_error( t ):
  print("Invalid Token:",t.value[0])
  t.lexer.skip( 1 )

lexer = lex.lex()

precedence = (
    ( 'left', 'PLUS', 'MINUS' ),
    ( 'left', 'TIMES', 'DIV' ),
    ( 'nonassoc', 'UMINUS' )
)

def p_add( p ) :
    'expr : expr PLUS expr'
    p[0] = p[1] + p[3]

def p_sub( p ) :
    'expr : expr MINUS expr'
    p[0] = p[1] - p[3]

def p_expr2uminus( p ) :
    'expr : MINUS expr %prec UMINUS'
    p[0] = - p[2]

def p_mult_div( p ) :
    '''expr : expr TIMES expr
            | expr DIV expr'''

    if p[2] == '*' :
        p[0] = p[1] * p[3]
    else :
        if p[3] == 0 :
            print("Can't divide by 0")
            raise ZeroDivisionError('integer division by 0')
        p[0] = p[1] / p[3]

def p_expr2NUM( p ) :
    'expr : NUMBER'
    p[0] = p[1]

def p_parens( p ) :
    'expr : LPAREN expr RPAREN'
    p[0] = p[2]

def p_error( p ):
    print("Syntax error in input!")

parser = yacc.yacc()

res = parser.parse("-4*-(3-5)") # the input
print(res)

 

Сохраните этот файл как calc.py и запустить его.

Выход:

 -8

 

Какой правильный ответ на -4 * - (3 - 5) .

Часть 1: токенизация ввода с помощью Lex

Есть два шага , что код , полученный в примере 1 , осуществляется: один был tokenizing входа, который означает , что он искал символы , которые представляют собой арифметическое выражение, а второй шаг синтаксического анализа, который включает в себя анализ извлеченных маркеров и оценку результата.

В этом разделе приводится простой пример того , как разметить пользовательского ввода, а затем разбивает его построчно.

    import ply.lex as lex

    # List of token names. This is always required
    tokens = [
       'NUMBER',
       'PLUS',
       'MINUS',
       'TIMES',
       'DIVIDE',
       'LPAREN',
       'RPAREN',
    ]

    # Regular expression rules for simple tokens
    t_PLUS    = r'\+'
    t_MINUS   = r'-'
    t_TIMES   = r'\*'
    t_DIVIDE  = r'/'
    t_LPAREN  = r'\('
    t_RPAREN  = r'\)'

    # A regular expression rule with some action code
    def t_NUMBER(t):
        r'\d+'
        t.value = int(t.value)    
        return t

    # Define a rule so we can track line numbers
    def t_newline(t):
        r'\n+'
        t.lexer.lineno += len(t.value)

    # A string containing ignored characters (spaces and tabs)
    t_ignore  = '\t'

    # Error handling rule
    def t_error(t):
        print("Illegal character '%s'" % t.value[0])
        t.lexer.skip(1)

    # Build the lexer
    lexer = lex.lex()

    # Give the lexer some input
    lexer.input(data)

    # Tokenize
    while True:
        tok = lexer.token()
        if not tok: 
            break      # No more input
        print(tok)

 

Сохраните этот файл как calclex.py.Мы будем использовать это при создании нашего парсера Yacc.

Сломать

  1. Импорт модуля с помощью import ply.lex

Все лексеры должны предоставить список под названием tokens , который определяет все возможные имена лексем , которые могут быть получены лексером. Этот список всегда обязателен.

  tokens = [
    'NUMBER',
    'PLUS',
    'MINUS',
    'TIMES',
    'DIVIDE',
    'LPAREN',
    'RPAREN',
 ] 

tokens также могут быть кортеж строк (а не строка), где каждая строка обозначает маркер , как и раньше.

Правило регулярного выражения для каждой строки может быть определено либо как строка, либо как функция. В любом случае, перед именем переменной должен стоять префикс t_, чтобы обозначить, что это правило для сопоставления токенов.

  • Для простых лексем, регулярное выражение может быть задано в виде строки: t_PLUS = r'\+'

Окончательные приготовления:

Построить лексер используя lexer = lex.lex() .

## Вы также можете поместить все в класс и вызвать экземпляр класса, чтобы определить лексер. Например:

##

 import ply.lex as lex  
class MyLexer(object):            
      ...     # everything relating to token rules and error handling comes here as usual 

      # Build the lexer
      def build(self, **kwargs):
          self.lexer = lex.lex(module=self, **kwargs)

      def test(self, data):
          self.lexer.input(data)
          for token in self.lexer.token():
              print(token)

      # Build the lexer and try it out

m = MyLexer()
m.build()           # Build the lexer
m.test("3 + 4")     #

 

Обеспечить ввод с помощью lexer.input(data) , где данные является строкой

Для того, чтобы получить маркеры, используйте lexer.token() , который возвращает лексемы совпавшие. Вы можете перебрать лексер в цикле, как в:

##

ибо я в лексере:

    print(i) 

Часть 2: парсинг токенизированного ввода с помощью Yacc

В этом разделе объясняется, как обрабатывается входной токен из части 1 - это делается с помощью контекстно-свободных грамматик (CFG). Грамматика должна быть указана, а токены обрабатываются в соответствии с грамматикой. Под капотом парсер использует LALR-парсер.

# Yacc example

import ply.yacc as yacc

# Get the token map from the lexer. This is required.
from calclex import tokens

def p_expression_plus(p):
    'expression : expression PLUS term'
    p[0] = p[1] + p[3]

def p_expression_minus(p):
    'expression : expression MINUS term'
    p[0] = p[1] - p[3]

def p_expression_term(p):
    'expression : term'
    p[0] = p[1]

def p_term_times(p):
    'term : term TIMES factor'
    p[0] = p[1] * p[3]

def p_term_div(p):
    'term : term DIVIDE factor'
    p[0] = p[1] / p[3]

def p_term_factor(p):
    'term : factor'
    p[0] = p[1]

def p_factor_num(p):
    'factor : NUMBER'
    p[0] = p[1]

def p_factor_expr(p):
    'factor : LPAREN expression RPAREN'
    p[0] = p[2]

# Error rule for syntax errors
def p_error(p):
    print("Syntax error in input!")

# Build the parser
parser = yacc.yacc()

while True:
   try:
       s = raw_input('calc >')
   except EOFError:
       break
   if not s: continue
   result = parser.parse(s)
   print(result)


Сломать

Каждое правило грамматики определяется функцией, в которой строка документации для этой функции содержит соответствующую контекстно-независимую грамматическую спецификацию. Операторы, составляющие тело функции, реализуют семантические действия правила. Каждая функция принимает один аргумент p, который представляет собой последовательность, содержащую значения каждого грамматического символа в соответствующем правиле. Значения p[i] сопоставляются грамматические символы , как показано здесь:

   def p_expression_plus(p):
      'expression : expression PLUS term'
      #   ^            ^        ^    ^
      #  p[0]         p[1]     p[2] p[3]

      p[0] = p[1] + p[3] 
  • Для лексем, «ценность» соответствующий p[i] является таким же , как p.value атрибута , назначенным в модуле лексического анализатора. Таким образом, PLUS будет иметь значение + .

Обратите внимание , что функция может иметь любое имя, до тех пор , как оно предваряется p_ .

  • p_error(p) правило, определяется , чтобы поймать синтаксические ошибки ( такие же , как yyerror в YACC / зубров).

Несколько правил грамматики могут быть объединены в одну функцию, что является хорошей идеей, если производства имеют схожую структуру.

   def p_binary_operators(p):
      '''expression : expression PLUS term
                    | expression MINUS term
         term       : term TIMES factor
                    | term DIVIDE factor'''
      if p[2] == '+':
          p[0] = p[1] + p[3]
      elif p[2] == '-':
          p[0] = p[1] - p[3]
      elif p[2] == '*':
          p[0] = p[1] * p[3]
      elif p[2] == '/':
          p[0] = p[1] / p[3]  

Символьные литералы могут использоваться вместо токенов.

   def p_binary_operators(p):
      '''expression : expression '+' term
                    | expression '-' term
         term       : term '*' factor
                    | term '/' factor'''
      if p[2] == '+':
          p[0] = p[1] + p[3]
      elif p[2] == '-':
          p[0] = p[1] - p[3]
      elif p[2] == '*':
          p[0] = p[1] * p[3]
      elif p[2] == '/':
          p[0] = p[1] / p[3]

 

Конечно, литералы должны быть указаны в модуле lexer.

  • Чтобы явно задать начальный символ, используйте start = 'foo' , где foo некоторая нетерминал.

Установка приоритета и ассоциативности может быть выполнена с помощью переменной приоритета.

##

   precedence = (
     ('nonassoc', 'LESSTHAN', 'GREATERTHAN'),  # Nonassociative operators
     ('left', 'PLUS', 'MINUS'),
     ('left', 'TIMES', 'DIVIDE'),
     ('right', 'UMINUS'),            # Unary minus operator
  )

 

Жетоны упорядочены по возрастанию. nonassoc означает , что эти маркеры не ассоциируют. Это значит , что - то вроде a < b < c незаконна тогда a < b по - прежнему законно.

Синтаксис

Параметры

Примечания