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

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

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

На языке 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)


  1. delvin-fil
    10.12.2016 14:54

    Познавательно!


  1. to_climb
    10.12.2016 17:50
    +7

    Зачем у класса Repeat заведёно значение root?
    А вообще, без примера использования статья выглядит незаконченной.


    1. Arhin
      10.12.2016 20:03

      Спасибо за Ваш комментарий. На счет Вашего вопроса: создаем дерево с корнем root и добавляем, в качестве детей, последовательно все разобранные значения. Если же Repeat ни разу не сработает, то возвращается пустое дерево с корнем root.
      В следующей статье я планировал описать разработку лексера и пример разбора математических выражений с помощью этой библиотеки.


  1. icoz
    10.12.2016 21:56
    +1

    Интересно. Только бы побольше теории. А то статья выглядит написанной для тех, кто в теме.
    По сути похоже на описание библиотеки construct. Загляните в код. Очень занимательно.


  1. john999
    11.12.2016 13:23

    Спасибо за статью.



  1. ndv79
    13.12.2016 01:16
    +1

    Boost.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 даже не рискну это воспроизвести — не силён в головоломках.


  1. JTProg
    13.12.2016 01:16

    Автор, спасибо за статью! Довольно любопытно было почитать. Теперь с нетерпением жду продолжения с живыми примерами использования.