Функциональные языки программирования позволяют описывать функции высших порядков, которые принимают в качестве аргументов и возвращают как результат другие функции.
Парсер-комбинаторы – известная техника создания парсеров, которая использует возможности функциональных языков программирования для динамического построения более сложных парсеров из простых по правилам некоторой грамматики.
На языке Python классические парсер-комбинаторы можно описать следующим образом. Определение парсеров будет осуществляется посредством описания классов. Каждый класс будет переопределять метод __call__, который и будет выполнять всю работу.
На вход парсеры будут принимать линейную последовательность некоторых данных (это может быть набор символов, токенов и т. п.) и начальную позицию, с которой следует начать разбор.
В качестве результата разбора будет возвращаться объект типа Res, который, в случае успеха, будет содержать часть разобранного AST (abstract syntax tree) и следующую позицию во входной последовательности, иначе – позицию элемента, который вызвал ошибку.
Определение класса Res:
class Res:
def __init__(self, subtree, pos):
self.subtree = subtre
self.pos = pos
def __str__(self):
return '(' + ', '.join((str(self.subtree), str(self.pos))) + ')'
def __bool__(self):
return self.subtree != None
Определение класса Tree:
class Tree:
def __init__(self, root = None, children = None):
self.root = root
if children == None:
self.children = []
else:
self.children = children
def __str__(self):
r = str(self.root)
c = ', '.join(str(c) for c in self.children)
if c:
r = '[' + r + ', ' + c + ']'
return r
Опишем базовый класс, от которого будут наследоваться все парсеры:
import abc
class Parser(metaclass = abc.ABCMeta):
@abc.abstractmethod
def __call__(self):
pass
def __lshift__(self, other): # переопределение оператора <<
return Concat(self, other, 1)
def __rshift__(self, other): # переопределение оператора >>
return Concat(self, other, 0)
def __or__(self, other): # переопределение оператора |
return Alt(self, other)
Парсер Atom принимает один элемент и сопоставляет его с элементом во входной последовательности. В случае успеха возвращает лист, ассоциированный с этим элементом.
class Atom(Parser):
def __init__(self, token):
self.token = token
def __call__(self, tokens, pos = 0):
if pos != len(tokens) and self.token == tokens[pos]:
return Res(Tree(tokens[pos]), pos + 1)
return Res(None, pos)
Парсер Concat принимает на вход два парсера. Сначала применяется левый парсер, затем – правый. Если он отрабатывает успешно, результат будет содержать лево- или право-ассоциативное дерево. Если один из них не разобрал свою часть последовательности, то вся комбинация возвращает неудачу.
class Concat(Parser):
def __init__(self, left, right, F = 0):
self.left = left
self.right = right
self.F = F # если F == 0 строиться лево-ассоциативное дерево,
# иначе – право-ассоциативное
def __call__(self, tokens, pos = 0):
left_res = self.left(tokens, pos)
if left_res:
right_res = self.right(tokens, left_res.pos)
if right_res:
if self.F == 0:
right_res.subtree.children.insert(0, left_res.subtree)
return right_res
left_res.subtree.children.append(right_res.subtree)
return Res(left_res.subtree, right_res.pos)
return right_res
return left_res
Опишем парсер альтернативы. Парсер Alt принимает на вход два парсера. Он отрабатывает успешно, если успешно отработал левый или правый парсер.
class Alt(Parser):
def __init__(self, left, right):
self.left = left
self.right = right
def __call__(self, tokens, pos = 0):
left_res = self.left(tokens, pos)
if left_res:
return left_res
right_res = self.right(tokens, pos)
if right_res:
return right_res
if left_res.pos > right_res.pos:
return left_res
return right_res
Опишем опциональный парсер Opt. Если его аргумент отработал успешно, то возвращает результат, иначе все равно возвращает успех, но с заданным значением по умолчанию.
class Opt(Parser):
def __init__(self, parser, default = None):
self.parser = parser
def __call__(self, tokens, pos = 0):
res = self.parser(tokens, pos)
if res:
return res
return Res(Tree(default), pos)
Парсер повторения Repeat работает пока не “сломается”. Если аргумент не сработал ни разу, это тоже считается успехом.
class Repeat(Parser):
def __init__(self, root, parser, F = 0):
self.root = root
self.parser = parser
self.F = F
def __call__(self, tokens, pos = 0):
tree = Tree(self.root)
while True:
res = self.parser(tokens, pos)
if not res:
break
if self.F == 0:
tree.children.append(res.subtree)
else:
tree.children.insert(0, res.subtree)
pos = res.pos
return Res(tree, pos)
Парсер Prog последовательно применяет, преданные ему, парсеры и возвращает результат указанного (по умолчанию последнего).
class Prog(Parser):
def __init__(self, parser, *others, N = -1):
self.parser = parser
self.others = others
self.N = N
def __call__(self, tokens, pos = 0):
i = 1 + len(self.others) + self.N if self.N < 0 else self.N
if i < 0 or i > len(self.others):
raise IndexError
res = self.parser(tokens, pos)
if not res:
return res
t = res
error = 0
j = 1
for parser in self.others:
res = parser(tokens, res.pos)
if not res:
error = 1
break
if j == i:
t = res
j += 1
if error:
return res
return Res(t.subtree, res.pos)
Парсер Lazy используется для описания рекурсивных парсеров. Он принимает на вход функцию без аргументов, которая возвращает парсер. Это связано с тем, что на момент описания парсер еще не определен и не может ссылаться на себя непосредственно.
class Lazy(Parser):
def __init__(self, parser_func):
self.parser_func = parser_func
self.parser = None
def __call__(self, tokens, pos = 0):
if not self.parser:
self.parser = self.parser_func()
return self.parser(tokens, pos)
Парсер LExp в некоторых случаях позволяет обойти левую рекурсию (к примеру, при разборе лево-ассоциативных операторов). Принимает на вход три парсера: для разбора левого элемента, разделителя и правого элемента. При отсутствии очередного разделителя возвращает результат.
class LExp(Parser):
def __init__(self, first, sep, parser):
self.first = first
self.sep = sep
self.parser = parser
def __call__(self, tokens, pos = 0):
left_res = self.first(tokens, pos)
if not left_res:
return left_res
error = 0
while True:
sep_res = self.sep(tokens, left_res.pos)
if not sep_res:
break
right_res = self.parser(tokens, sep_res.pos)
if not right_res:
error = 1
break
sep_res.subtree.children.append(right_res.subtree)
sep_res.subtree.children.insert(0, left_res.subtree)
left_res = Res(sep_res.subtree, right_res.pos)
if error:
return right_res
return left_res
Описанные выше, парсер-комбинаторы предоставляют удобный и гибкий инструментарий для создания парсеров. Конечно они не сравнятся с такими известными библиотеками, как Boost.Spirit, но для новичка написание собственной библиотеки парсеров позволит лучше разобраться в процессе парсинга, что зачастую вызывает недоумение.
Комментарии (7)
to_climb
10.12.2016 17:50+7Зачем у класса Repeat заведёно значение root?
А вообще, без примера использования статья выглядит незаконченной.Arhin
10.12.2016 20:03Спасибо за Ваш комментарий. На счет Вашего вопроса: создаем дерево с корнем root и добавляем, в качестве детей, последовательно все разобранные значения. Если же Repeat ни разу не сработает, то возвращается пустое дерево с корнем root.
В следующей статье я планировал описать разработку лексера и пример разбора математических выражений с помощью этой библиотеки.
icoz
10.12.2016 21:56+1Интересно. Только бы побольше теории. А то статья выглядит написанной для тех, кто в теме.
По сути похоже на описание библиотеки construct. Загляните в код. Очень занимательно.
ndv79
13.12.2016 01:16+1Boost.Spirit та ещё гадость. Проще писать самому, да и код получается более читабельный. Пример парсинга конструкции типа __attribute__ ((reqd_work_group_size(x,y,z))) на чистом Си:
bool attribute(KernelDeclaration & kd) { auto was = current; int x,y,z; if (sterm("__attribute__") && sterm("(") && sterm("(") && sterm("reqd_work_group_size") && sterm("(") && sint(x) && sterm(",") && sint(y) && sterm(",") && sint(z) && sterm(")") && sterm(")") && sterm(")")) { kd.requiredWorkgroupSize[0] = x; kd.requiredWorkgroupSize[1] = y; kd.requiredWorkgroupSize[2] = z; return true; } current = was; return false; }
Где sterm(t) пропускает пробелы и комментарии, после чего считывает данный терминал. На Boost.Spirit даже не рискну это воспроизвести — не силён в головоломках.
JTProg
13.12.2016 01:16Автор, спасибо за статью! Довольно любопытно было почитать. Теперь с нетерпением жду продолжения с живыми примерами использования.
delvin-fil
Познавательно!