Пользователь
0,0
рейтинг
25 августа 2013 в 09:19

Разработка → Разбор кода и построение синтаксических деревьев с PLY. Основы tutorial


Что такое PLY?


PLY — это аббревиатура из первых букв выражения: Python Lex-Yacc.
Фактически, это порт утилит lex и yacc на python в красивой обертке.
Работать с ply очень просто и порог входа для начала использования практически нулевой.
Написан он на чистом питоне и представляет из себя LALR(1) парсер, но кому это интересно?
Я по натуре практик (как и большинсво из вас) поэтому пошли в бой!

Что будем делать?


На сайте есть пример написания очередного калькулятора, поэтому повторяться не будем. А сделаем что-то навроде парсера очень очень узкого подмножества PHP :)
Наша задача в конце статьи построить синтаксическое дерево для такого примера:

<?php
$val = 5;
$result = substr( "foobar", 2*(7-$val) );
echo "это наш результат: $result";


Пример очень маленький и взят с потолка. Но чтобы построить дерево кода нужно много и походу мы задействуем такой механизм PLY как state.



Lex


Lex — это штука, которая разбивает текст на базовые элементы языка. Ну или группирует текст в базовые элементы. Как-то так.

Что мы здесь видим, кроме бесполезного кода? Видим токены (базовые элементы):
PHP_START — '<?php'
PHP_VAR — '$result'
PHP_EQUAL — '='
PHP_NAME — 'substr'
PHP_OPEN — '('
PHP_STRING — «foobar», 'это наш результат: $result'
PHP_NUM — '2', '7'
PHP_CLOSE — ')'
PHP_ECHO — «echo»
PHP_COLON — ';'
PHP_COMA — ','
PHP_PLUSMINUS — '-'
PHP_DIVMUL — '*'

Для разбора текста на токены в PLY имеется ply.lex. И работает он с регулярными выражениями.
А еще он очень придирчивый к тому что мы пишем в коде. Он требует обязательного присутствия массива с именем tokens.
Для каждого элемента этого массива мы обязаны иметь в коде регулярку или функцию с именем t_ЭЛЕМЕНТ.
Почему он такой придирчивый? Потому что он не работает напрямую во время выполнения программы, а выполняется лишь единожды при первом запуске программы и создает файл lextab.py, в котором описаны таблицы переходов и регулярки. При следующем запуске программы он проверяет наличие этого файла и в случае его присутствия уже не пытается строить таблицы заново, а использует сгенерированные.
Назад к коду.
Если бы синтаксис PHP ограничивался тринадцатью выше перечисленными токенами, то мы бы написали следующее:

# coding=utf8
import ply.lex as lex

# без этой штуки ничего не сынтерпретируется, потому что этот массив шарится между лексером и парсером и кроме того используется внутренне библиотекой
tokens = (
    'PHPSTART', 'PHPVAR', 'PHPEQUAL', 'PHPFUNC',
    'PHPSTRING', 'PHPECHO', 'PHPCOLON', 'PHPCOMA',
    'PHPOPEN', 'PHPCLOSE', 'PHPNUM', 'PLUSMINUS', 'DIVMUL'
)

# определим регулярку для абстрактного идетификатора
ident = r'[a-z]\w*'

# для каждого токена из массива мы должны написать его определение вида t_ИМЯТОКЕНА = регулярка
t_PHPSTART = r'\<\?php'
t_PHPVAR = r'\$'+ident # очень удобно, правда?
t_PHPEQUAL = r'\='
t_PHPFUNC = ident
t_PHPSTRING = r'"(\\.|[^"])*"'
t_PHPECHO = r'echo'
t_PHPCOLON = r';'
t_PHPCOMA = r','
t_PHPOPEN = r'\('
t_PHPCLOSE = r'\)'
t_PHPNUM = r'\d+'
t_PLUSMINUS = r'\+|\-'
t_DIVMUL = r'/|\*'

# здесь мы игнорируем незначащие символы. Нам ведь все равно, написано $var=$value или $var   =  $value
t_ignore = ' \r\n\t\f'

# а здесь мы обрабатываем ошибки. Кстати заметьте формат названия функции
def t_error(t):
    print "Illegal character '%s'" % t.value[0]
    t.lexer.skip(1)

lexer = lex.lex(reflags=re.UNICODE | re.DOTALL)

data = '''
<?php
$result = substr("foobar", "bar");
echo $result;
'''

lexer.input(data)

while True:
    tok = lexer.token() # читаем следующий токен
    if not tok: break      # закончились печеньки
    print tok


В комментарих к коду я примерно описал что у нас там творится. Замечу лишь, что вместо регулярок, определяющих токен, можно писать функции в которых будут эти же регулярки, но кроме этого можно будет туда вписать что-то еще свое. Например:

def t_PHPVAR(t):
    r'\$[a-zA-Z]\w*'
    print 'Урра, мы распарсили переменную ' + t.value # value - это то что нашлось регуляркой
    return t


Кстати вывод примера, что написан выше будет таким:

LexToken(PHPSTART,'<?php',1,1)
LexToken(PHPVAR,'$val',1,7)
LexToken(PHPEQUAL,'=',1,12)
LexToken(PHPNUM,'5',1,14)
LexToken(PHPCOLON,';',1,15)
LexToken(PHPVAR,'$result',1,17)
LexToken(PHPEQUAL,'=',1,25)
LexToken(PHPFUNC,'substr',1,27)
LexToken(PHPOPEN,'(',1,33)
LexToken(PHPSTRING,'"foobar"',1,35)
LexToken(PHPCOMA,',',1,43)
LexToken(PHPNUM,'2',1,45)
LexToken(DIVMUL,'*',1,46)
LexToken(PHPOPEN,'(',1,47)
LexToken(PHPNUM,'7',1,48)
LexToken(PLUSMINUS,'-',1,49)
LexToken(PHPVAR,'$val',1,50)
LexToken(PHPCLOSE,')',1,54)
LexToken(PHPCLOSE,')',1,56)
LexToken(PHPCOLON,';',1,57)
LexToken(PHPFUNC,'echo',1,59)
LexToken(PHPSTRING,'"\xd1\x8d\xd1\x82\xd0\xbe \xd0\xbd\xd0\xb0\xd1\x88 \xd1\x80\xd0\xb5\xd0\xb7\xd1\x83\xd0\xbb\xd1\x8c\xd1\x82\xd0\xb0\xd1\x82: $result"',1,64)
LexToken(PHPCOLON,';',1,107)


Числа в конце — это номер строки и номер символа. Как видите, номер строки не изменяется. Его нужно менять самим. Как? При проходе токена с переходом на новую строку. Для этого уберем символ новой строки из игнорируемых токенов и добавим новую функцию t_newline:

t_ignore = ' \r\t\f'

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


Вот теперь у нас все работает:

LexToken(PHPSTART,'<?php',2,1)
LexToken(PHPVAR,'$val',3,7)
LexToken(PHPEQUAL,'=',3,12)
LexToken(PHPNUM,'5',3,14)
LexToken(PHPCOLON,';',3,15)
LexToken(PHPVAR,'$result',4,17)
LexToken(PHPEQUAL,'=',4,25)
LexToken(PHPFUNC,'substr',4,27)
LexToken(PHPOPEN,'(',4,33)
LexToken(PHPSTRING,'"foobar"',4,35)
LexToken(PHPCOMA,',',4,43)
LexToken(PHPNUM,'2',4,45)
LexToken(DIVMUL,'*',4,46)
LexToken(PHPOPEN,'(',4,47)
LexToken(PHPNUM,'7',4,48)
LexToken(PLUSMINUS,'-',4,49)
LexToken(PHPVAR,'$val',4,50)
LexToken(PHPCLOSE,')',4,54)
LexToken(PHPCLOSE,')',4,56)
LexToken(PHPCOLON,';',4,57)
LexToken(PHPFUNC,'echo',5,59)
LexToken(PHPSTRING,'"\xd1\x8d\xd1\x82\xd0\xbe \xd0\xbd\xd0\xb0\xd1\x88 \xd1\x80\xd0\xb5\xd0\xb7\xd1\x83\xd0\xbb\xd1\x8c\xd1\x82\xd0\xb0\xd1\x82: $result"',5,64)
LexToken(PHPCOLON,';',5,107)


Работает, но не полностью. Наша строка «это наш результат: $result» возвращается лексером как есть. А как парсить её будем? Создавать отдельный лексер? Неа. В PLY (да и в сишном lex) есть поддержка такой штуки как state. State — это переключалка на другой тип токенов. Вот это мы и будем использовать.

State

Итак, для того чтобы создать новый state нам нужно его предварительно объявить:

states = (
   ('string','exclusive'),
)


'string' — это название нового состояния, а exclusive — это тип. Вообще, может быть два типа состояний: exclusive и inclusive. Exclusive — общих токенов не видно (общие, это те которые мы писали до этого, они по умолчанию находятся в состоянии INITIAL). Inclusive — видны общие токены и в текущем состоянии мы просто добавляем к ним дополнительные.
В случае нашей строки нам не нужны другие токены, так как внутри у нас там все по другому. Впрочем парочку мы позаимствуем. Нам нужен токен PHPVAR, потому что переменная у нас выглядит одинаково и снаружи и внутри строки, и нужен будет еще один токен, который вы увидите походу ниже.
Чтобы сделать токен общим мы должны присвоить ему префикс ANY_, ну а чтобы сделать токен видимым только для какого-то определенного состояния — <состояние>_. Ниже приведены измененные токены.

# для каждого токена из массива мы должны написать его определение вида t_ИМЯТОКЕНА = регулярка
t_PHPSTART = r'\<\?php'
t_ANY_PHPVAR = r'\$'+ident # очень удобно, правда?
t_PHPEQUAL = r'\='
t_PHPFUNC = ident
t_PHPECHO = r'echo'
t_PHPCOLON = r';'
t_PHPCOMA = r','
t_PHPOPEN = r'\('
t_PHPCLOSE = r'\)'
t_PHPNUM = r'\d+'
t_PLUSMINUS = r'\+|\-'
t_DIVMUL = r'/|\*'

# изменили токен PHPSTRING и сделали из него функцию, добавили его во все состояния
def t_ANY_PHPSTRING(t): # нужен в обоих состояниях, потому что двойные кавычки матчатся и там и там.
    r'"'
    if t.lexer.current_state() == 'string':
        t.lexer.begin('INITIAL') # переходим в начальное состояние
    else:
        t.lexer.begin('string') # парсим строку
    return t

t_string_STR = r'(\\.|[^$"])+' # парсим пока не дойдем до переменной или до кавычки, попутно игнорируем экранки

# говорим что ничего не будем игнорировать
t_string_ignore = '' # это кстати обязательная переменная, без неё нельзя создать новый state

# ну и куда же мы без обработки ошибок
def t_string_error(t):
    print "Illegal character '%s'" % t.value[0]
    t.lexer.skip(1)


Кстати, заметили это: LexToken(PHPFUNC,'echo',5,59)?
PLY перед построением таблицы сортирует регулярные выражения по возрастанию и в процессе парсинга побеждает длиннейшее. Вот поэтому echo и не парсится как PHPECHO. Как это обойти? Легко. Просто изменим тип возвращаемого токена в функции.
Вот так:

@TOKEN(ident)
def t_PHPFUNC(t):
    if t.value.lower() == 'echo':
        t.type = 'PHPECHO'
    return t


Теперь echo возвращается как нужно. Кстати о декораторе TOKEN: вместо того, чтобы писать регулярку в начале тела функции, можно просто поместить её в переменную и применить к функции как декоратор. Что мы и сделали.

Вот. Теперь вроде все. Да не все.
Неплохо бы игнорировать комментарии.
Что же, добавим еще одну маленькую функцию в лексер:

def t_comment(t):
    r'(/\*(.|\n)*?\*/)|(//.*)'
    pass


Полный исходный код лексера (lexer.py)
# coding=utf8
import ply.lex as lex
from ply.lex import TOKEN
import re

states = (
   ('string','exclusive'),
)

# без этой штуки ничего не съинтерпретируется, потому что этот массив шарится между лексером и парсером и кроме того используется внутренне библиотекой
tokens = (
    'PHPSTART', 'PHPVAR', 'PHPEQUAL', 'PHPFUNC',
    'PHPSTRING', 'PHPECHO', 'PHPCOLON', 'PHPCOMA',
    'PHPOPEN', 'PHPCLOSE', 'PHPNUM', 'PLUSMINUS', 'DIVMUL',
    'STR'
)

# определим регулярку для абстрактного идетификатора
ident = r'[a-z]\w*'

# для каждого токена из массива мы должны написать его определение вида t_ИМЯТОКЕНА = регулярка
t_PHPSTART = r'\<\?php'
t_ANY_PHPVAR = r'\$'+ident # очень удобно, правда?
t_PHPEQUAL = r'\='
t_PHPCOLON = r';'
t_PHPCOMA = r','
t_PHPOPEN = r'\('
t_PHPCLOSE = r'\)'
t_PHPNUM = r'\d+'
t_PLUSMINUS = r'\+|\-'
t_DIVMUL = r'/|\*'

@TOKEN(ident)
def t_PHPFUNC(t):
    if t.value.lower() == 'echo':
        t.type = 'PHPECHO'
    return t

# игнорируем комментарии
def t_comment(t):
    r'(/\*(.|\n)*?\*/)|(//.*)'
    pass

# изменили токен PHPSTRING и сделали из него функцию, добавили его во все состояния
def t_ANY_PHPSTRING(t): # нужен в обоих состояниях, потому что двойные кавычки матчатся и там и там.
    r'"'
    if t.lexer.current_state() == 'string':
        t.lexer.begin('INITIAL') # переходим в начальное состояние
    else:
        t.lexer.begin('string') # парсим строку
    return t

t_string_STR = r'(\\.|[^$"])+' # парсим пока не дойдем до переменной или до кавычки, попутно игнорируем экранки

# говорим что ничего не будем игнорировать
t_string_ignore = '' # это кстати обязательная переменная, без неё нельзя создать новый state

# ну и куда же мы без обработки ошибок
def t_string_error(t):
    print "Illegal character '%s'" % t.value[0]
    t.lexer.skip(1)

# здесь мы игнорируем незначащие символы. Нам ведь все равно, написано $var=$value или $var   =  $value
t_ignore = ' \r\t\f'

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

# а здесь мы обрабатываем ошибки. Кстати заметьте формат названия функции
def t_error(t):
    print "Illegal character '%s'" % t.value[0]
    t.lexer.skip(1)

lexer = lex.lex(reflags=re.UNICODE | re.DOTALL | re.IGNORECASE)

if __name__=="__main__":
    data = '''
<?php
$val = 5;
$result = substr( "foobar", 2*(7-$val) );
echo "это наш результат: $result";
    '''

    lexer.input(data)

    while True:
        tok = lexer.token() # читаем следующий токен
        if not tok: break      # закончились печеньки
        print tok



Вот теперь переходим к парсеру (parser.py).

Yacc


Yacc — это штука (парсер), в которую мы передаем токены и описываем правила их соединения (грамматику). Походу работы этой программы мы можем создать абстрактное дерево или же сразу выполнять программу (как это сделано в примере с калькулятором на сайте).
В PLY для описания грамматики существует класс ply.yacc.
Описывать граматику в ply очень просто и это доставляет даже некоторое наслаждение. Для описания у нас снова существуют специальные функции p_имяфункции, где в doc-строках мы и описываем эту самую грамматику.
Давайте попробуем написать очень простую грамматику для нашего абстрактного примера с php:

php -> [PHPSTART phpbody]?
phpbody -> [phpline phpcolons]*
phpcolons -> [ PHPCOLON ]+
phpline -> assign | func | [PHPECHO args]
assign -> PHPVAR PHPEQUAL expr
expr -> [fact | expr PLUSMINUS fact]
fact -> [term | fact DIVMUL term]
term -> [arg | PHPOPEN expr PHPCLOSE]
func -> PHPFUNC PHPOPEN args PHPCLOSE
args -> [ expr [PHPCOMA expr]* ]?
arg -> string | phpvar | PHPNUM | func
string -> PHPSTRING str PHPSTRING
str -> [STR | str phpvar]?
phpvar -> PHPVAR


Текста написано уже много, и рассказывать еще можно очень долго. Но для понимания основ ply.yacc достаточно разобрать один пример. А дальше я уже выложу исходник парсера.

Итак, выдранный кусок из парсера:

def p_str(p):
    '''str :
           | STR
           | str phpvar'''
    if len(p) == 1:
        p[0] = Node('str', [''])
    elif len(p) == 2:
        p[0] = Node('str', [p[1]])
    else:
        p[0] = p[1].add_parts([p[2]])


По порядку. Парсинг начинается с первой сверху функции с шаблоном p_<функция>. Т.е. у меня например первой в исходнике стоит p_php, поэтому парсер начнет работу именно с неё (сверху вниз). Парсер работает с правилами в doc-строках, и после их матчинга возвращает управление в функцию с правилами и при этом передает переменную p. В переменной p хранится результат парсинга. Причем после обработки мы должны засунуть наши результаты в p[0]. Элементы, начиная с p[1], это правая часть наших правил — отматченные токены и правила.

    '''str :
           | STR
           | str phpvar'''


По сути правило выше — это скомбинированное (слитое) правило. '|' как несложно догадаться — это ИЛИ, ну или различные варианты токена.
Между двоеточием и левой/правой частью обязателен пробел. Так любит ply. Если правая часть может быть пустой, то после двоеточия ничего не пишем. Токены должны быть написаны большими буквами, а правила маленькими, без префикса p_. Т.е. в примере выше использовались правила p_str и p_phpvar.

Например, если мы имеем "" (пустую строку), то в p у нас будет только одно значение — p[0], причем оно не определно и мы должны его заполнить.
Если у нас строка «привет $vasya пупкин», то эта функция выполнится три раза. Первый раз для значения STR, и мы вернем это значение обратно в p[0], второй раз для $vasya, и добавим это к предыдущему значению (p[1] будет содержать ноду, которую мы вернули в p[0] на прошлой итерации), а третий раз добавим на выход «пупкин». Наверное я сумбурно описал, но это надо просто потрогать. Открыть среду исполнения и поиграться :)

Кстати, иногда более удобен вариант с разделенными вариантами (тафтология, простите):

def p_str_empty(p):
    '''str :'''
    p[0] = Node('str', [''])


def p_str_raw(p):
    '''str : STR'''
    p[0] = Node('str', [p[1]])

def p_str_var(p):
    '''str : str phpvar'''
    p[0] = p[1].add_parts([p[2]])


Он абсолютно идентичен предыдущему варианту кода. Просто фломастеры другие. Функции имеют разные названия (потому что разделять их надо в питоне), но префиксы идентичные (str), что и заставляет ply группировать их вместе как варианты одного правила.

Для удобства построения дерева я использовал такой простенький и эффективный класс:

class Node:
    def parts_str(self):
        st = []
        for part in self.parts:
            st.append( str( part ) )
        return "\n".join(st)

    def __repr__(self):
        return self.type + ":\n\t" + self.parts_str().replace("\n", "\n\t")

    def add_parts(self, parts):
        self.parts += parts
        return self

    def __init__(self, type, parts):
        self.type = type
        self.parts = parts


Он одновременно и хранит все нужное, и структурирует вывод, что чень удобно при отладке.

Полный код парсера
# coding=utf8

from lexer import tokens
import ply.yacc as yacc

class Node:
    def parts_str(self):
        st = []
        for part in self.parts:
            st.append( str( part ) )
        return "\n".join(st)

    def __repr__(self):
        return self.type + ":\n\t" + self.parts_str().replace("\n", "\n\t")

    def add_parts(self, parts):
        self.parts += parts
        return self

    def __init__(self, type, parts):
        self.type = type
        self.parts = parts

def p_php(p):
    '''php :
           | PHPSTART phpbody'''
    if len(p) == 1:
        p[0] = None
    else:
        p[0] = p[2]

def p_phpbody(p):
    '''phpbody :
               | phpbody phpline phpcolons'''
    if len(p) > 1:
        if p[1] is None:
            p[1] = Node('body', [])
        p[0] = p[1].add_parts([p[2]])
    else:
        p[0] = Node('body', [])

def p_phpcolons(p):
    '''phpcolons : PHPCOLON
                 | phpcolons PHPCOLON'''

def p_phpline(p):
    '''phpline : assign
               | func
               | PHPECHO args'''
    if len(p) == 2:
        p[0] = p[1]
    else:
        p[0] = Node('echo', [p[2]])

def p_assign(p):
    '''assign : PHPVAR PHPEQUAL expr'''
    p[0] = Node('assign', [p[1], p[3]])

def p_expr(p):
    '''expr : fact
            | expr PLUSMINUS fact'''
    if len(p) == 2:
        p[0] = p[1]
    else:
        p[0] = Node(p[2], [p[1], p[3]])

def p_fact(p):
    '''fact : term
            | fact DIVMUL term'''
    if len(p) == 2:
        p[0] = p[1]
    else:
        p[0] = Node(p[2], [p[1], p[3]])

def p_term(p):
    '''term : arg
            | PHPOPEN expr PHPCLOSE'''
    if len(p) == 2:
        p[0] = p[1]
    else:
        p[0] = p[2]

def p_func(p):
    '''func : PHPFUNC PHPOPEN args PHPCLOSE'''
    p[0] = Node('func', [p[1], p[3]])

def p_args(p):
    '''args :
            | expr
            | args PHPCOMA expr'''
    if len(p) == 1:
        p[0] = Node('args', [])
    elif len(p) == 2:
        p[0] = Node('args', [p[1]])
    else:
        p[0] = p[1].add_parts([p[3]])

def p_arg(p):
    '''arg : string
           | phpvar
           | PHPNUM
           | func'''
    p[0] = Node('arg', [p[1]])

def p_phpvar(p):
    '''phpvar : PHPVAR'''
    p[0] = Node('var', [p[1]])

def p_string(p):
    '''string : PHPSTRING str PHPSTRING'''
    p[0] = p[2]

def p_str(p):
    '''str :
           | STR
           | str phpvar'''
    if len(p) == 1:
        p[0] = Node('str', [''])
    elif len(p) == 2:
        p[0] = Node('str', [p[1]])
    else:
        p[0] = p[1].add_parts([p[2]])

def p_error(p):
    print 'Unexpected token:', p

parser = yacc.yacc()

def build_tree(code):
    return parser.parse(code)



Основной файлик, откуда вызываем все
# coding=utf8

from parser import build_tree

data = '''
<?php
$val = 5;
$result = substr( "foobar", 2*(7-$val) ); /* comment */
echo "это наш результат: ", $result;
'''

result = build_tree(data)
print result



Вывод синтаксического дерева
line:
	assign:
		$val
		arg:
			5
	assign:
		$result
		arg:
			func:
				substr
				args:
					arg:
						str:
							foobar
					*:
						arg:
							2
						-:
							arg:
								7
							arg:
								var:
									$val
	echo:
		args:
			arg:
				str:
					это наш результат: 
			arg:
				var:
					$result



Заключение



Если есть какие-то замечания или пожелания, с удовольствием допишу/изменю пост.

Код статьи здесь

Ну и если интересно с чего я вдруг про ply решил написать — pyfox. Делаю потихонечьку обработку css на python. Вернее парсер css написан, но вот прогулка по dom еще полностью не реализована (не все псевдоселекторы работают). Сейчас конкретно проблема с nth-child. Не на уровне css а на уровне эффективного матчинга — в dom нету такого свойства как номер среди соседей, а считать предыдущих соседей не хочется. Видимо придется HTMLParser кастомизировать. Может кто решит присоединитсья ко мне? Всех welcome :)
Виталий Дятлов @xytop
карма
15,0
рейтинг 0,0
Реклама помогает поддерживать и развивать наши сервисы

Подробнее
Спецпроект

Самое читаемое Разработка

Комментарии (28)

  • 0
    ply – один из лучших парсеров, с которыми я работал. Отмечу, что у рубишников есть rly, почти полный порт ply на ruby, с еще более навороченным синтаксисом (благо ruby в плане DSL потолерантнее питона). Он, правда помедленее ply, так как был допилен до уровня, достаточного чтобы я использовал его в своем проекте, но Yacc-составляющаая полноценная.
  • +5
    На фото у всех шорты мокрые, так ли страшен этот ply?
  • +1
    Интересная штука!
    А может ли это использоваться вот в таком примере:
    <?php
    $foo=6;
    syntax(var, ":=", val, {
    var=val
    });//объявили новый синтаксис
    //используем
    $foo := 10;
    

    То есть в процессе парсинга грамматика может изменяться.
    • +1
      Нет. Грамматика определяется во время компилирования и не изменяется во время выполнения. Такое наверное можно сделать только руками, используя LL(1), и то, будет порождать множество неоднозначностей.
    • +1
      Вспоминается Katahdin.
      • 0
        Да, прикольный язык. О нём даже статья была на хабре. Хотя мне нужно compile-time изменений грамматики.
  • +1
    Спасибо, замечательная статья! Правда есть грамматики, для которых PLY не очень подходит, так что я перешел на простой самопальный парсер :)
    Если будете продолжать писать, хотелось бы услышать про обработку ошибок.
    • +1
      хотелось бы услышать про обработку ошибок

      Ну вот есть t_error и p_error — через них все и делается. В питоне ведь свойства классов динамические, так что чем больше инфы вы походу парсинга будете запихивать в t.lexer и p.parser, тем лучше будет происходить обработка.
      • 0
        Я в курсе про t_error и p_error, но по-человечески их завести так и не вышло. Так и не понял, как сказать красиво, например, что не закрыта какая-нибудь скобка или еще что. Как в gcc в общем.
        А не подходит он для грамматики LOGO, например. Там вот это:

        PRINT THING "VAR
        

        означает это:
        PRINT (THING "VAR)
        

        Так же, как и PRINT ADD 5 THING "VAR. То есть значение имеет количество аргументов у функций.
        • 0
          По поводу скобок и всего такого, можно делать вставки в грамматику, хотя конечно не самый изящный вариант:
          def p_func(p):
              '''func : expect_funcname funcname expect_lbrace LBRACE expect_body body expect_rbrace RBRACE'''
              pass
          
          def p_expect_funcname(p): stack.push('expect funcname')
          def p_expect_lbrace(p): stack.push('expect lbrace')
          



          По поводу logo да, там разве что только лексер оттуда брать…
          • 0
            Ну это выглядит как костыль, сами понимаете.
            • 0
              Костыль, да. Но теоретически… в любых других парсерах это должно делаться примерно так же, ну т.е. мы должны как-то дать знать сами себе что ожидаем.
    • 0
      Для каких например грамматик может не подходить ply?
      • НЛО прилетело и опубликовало эту надпись здесь
  • +1
    Расскажите, пожалуйста, справляется ли PLY со следующими часто встречающимися проблемами парсеров и если да, то как?

    1. В статье написано, что PLY сам сортирует таблицу лексем, чтобы «побеждал длиннейший». В парсере языка C++ была проблема, что код вложенных шаблонных типов (TypeA<TypeB<TypeC>>) разбирался некорректно: две закрывающие треугольные скобки подряд воспринимались за один оператор сдвига (>>) и это приводило к ошибке синтаксиса.
    2. Есть ли возможность разбирать грамматики со значащими отступами, как в самом Python?
    3. Можно ли кастомизировать сообщения об ошибках? Чтобы выдавалось не нечто вроде «Ожидается: идентификатор, или число, или строка, или ...», а более подходящие по контексту: «Возможно, пропущена скобка».
    • 0
      код вложенных шаблонных типов (TypeA<TypeB>) разбирался некорректно: две закрывающие треугольные скобки подряд воспринимались за один оператор сдвига (>>)
      Т.е. что-то типа этого?

      string<allocator<char>> varname = "foo";
      


      Можно просто не включать в лексер оператор '>>' и обрабатывать его в парсере, как последовательность '>'. Но тогда грамматика усложнится за счет того что придется обрабатывать ненужные пробельные символы.

      2. Думаю что можно и с отступами разбирать. Посчитать их количество перед началом первого блока с отступами и считать это длиной оступа по умолчанию. В парсере мы просто считаем количество пробелов перед блоком, а затем делим на отступ по умолчанию и принимаем решение принадлежит ли блок родителю. Ну и действуем соответствующе

      3. Можно. Уже ответил выше как. Нужно вставлять действия посередине.
    • 0
      В стандарте С++ уделено внимание этой неоднозначности и решение только одно — ставить пробел, т.е. проблемный код не является валидным для С++. Но вообще С++ нельзя распарсить LALR(1) парсером, это не проблема PLY
      • +1
        Теперь, с С++11, код без пробела между двумя закрывающими > в шаблонах является валидным.
      • +1
        Указание «ставить пробел» в стандарте — это классический пример ситуации «это не баг, это фича» :)
  • +1
    Кто-нибудь из тех, кто шарит в ply, объясните мне, нафига не использовать именные пространства, а вместо этого использовать префиксы к названиям переменных и функций?
  • 0
    Зачем вы потеряли PHP_END?
    • 0
      Ну, во первых, он по спецификации не обязателен, во вторых он мне был не нужен потому что парсер кроме пыхтачка ничего не понимает.А если бы понимал, то не было бы строгого условия начинать код с <?php (можно ведь текст и вначале файла пихануть какой-то).
      • 0
        Но если хотите, могу написать через день-два, как расширить парсер на использование php как вставок… функции, классы. Хотя на самом деле это уже не будет относиться к ply напрямую, а больше к построению граматики.
  • 0
    Понавешали на девушку — больше всех держит.
    Парсер PHP на Питоне, конечно, тооонко))
    • 0
      Ну… учитывая что есть возможность транслировать python в нативный C, это не кажется оверхедом. Если я правильно помню, то facebook делал интерпретатор php тоже на python (не hiphop, а позже какой-то там), и добились значительного ускорения.
      • 0
        Я имел ввиду, что это тонкий намёк на то, что пора переходить на python:-)
  • +2
    съинтерпретируется

    Wut? Сынтерпретируется же :-)
  • 0
    Если не трудно, покажите примеры ветвлений.

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