Привет, Хабр!
LR-парсеры – это инструмент для анализа и синтаксического разбора языков программирования. LR в данном контексте означает Left-to-right, слева направо и Rightmost derivation, правое разложения. LR парсеры используют метод снизу вверх, который отличается от более известных LL-парсеров, работающих сверху вниз.
Одна из основных фич LR-парсеров - способность обрабатывать большую часть контекстно-свободных грамматик.
Типы LR-парсеров
LR(0) парсеры
LR(0) парсеры — это базовый тип LR-парсера, который использует нулевое заглядывание вперед, т.е принимает решения о переходах и редукциях, не анализируя следующие символы входной строки.
LR(0) парсеры являются базой для понимания более сложных парсеров. Они используют стек для хранения состояния и очереди для хранения входных символов.
Поскольку они не используют заглядывание вперед, их способность разрешать двусмысленности в грамматике ограничена.
Основная проблема LR(0) парсеров — это конфликты shift/reduce и reduce/reduce, которые могут возникать из-за недостатка информации о следующих символах.
Рассмотрим грамматику:
S → A A → aA | b
Для строки "aab", LR(0) парсер будет выполнять следующие шаги:
Shift: перемещение
aв стек.Shift: перемещение второго
aв стек.Reduce: преобразование
aAвA.Shift: перемещение
bв стек.Reduce: преобразование
AвS.
SLR (Simple LR) парсеры
SLR парсеры представляют собой апдейт версию LR(0) парсеров, в которых используется односимвольное заглядывание вперед для разрешения двусмысленностей.
SLR парсеры решают многие конфликты, присутствующие в LR(0) парсерах, путем использования дополнительных правил перехода.
аблицы переходов и действий для SLR парсеров меньше, чем у более сложных LR(1) парсеров, что делает их более производительней с точки зрения памяти.
Для той же грамматики:
S → A A → aA | b
SLR парсер будет использовать заглядывание вперед для определения, какое правило применять в случае двусмысленности.
LALR (Look-Ahead LR) парсеры
LALR парсеры — это еще один шаг в эволюции LR-парсеров, представляющий собой компромисс между мощностью и эффективностью.
LALR парсеры способны обрабатывать большинство грамматик, которые могут обрабатывать полные LR(1) парсеры, но с меньшими затратами на память.
Таблицы LALR парсеров более компактны, чем таблицы полных LR(1) парсеров.
Рассмотрим грамматику:
S → E E → E + T | T T → T * F | F F → ( E ) | id
LALR парсер будет использовать заглядывание вперед для корректного парсинга выражений, разрешая двусмысленности при помощи компактных таблиц переходов.
Canonical LR(1) парсеры
Canonical LR(1) парсеры представляют собой наиболее мощный тип LR-парсеров, способный обрабатывать любые контекстно-свободные грамматики без ограничений.
ти парсеры могут обрабатывать любые грамматики, которые может обрабатывать любая LR-парсерная техника.
Таблицы переходов и действий для LR(1) парсеров значительно больше, чем у других типов LR-парсеров.
Для той же грамматики:
S → E E → E + T | T T → T * F | F F → ( E ) | id
Canonical LR(1) парсер будет создавать полные таблицы переходов и действий для каждой возможной комбинации состояний и символов, обеспечивая максимально точный парсинг.
Из чего состоят LR-парсерсы и алгоритм их работы
Основные компоненты LR-парсера
Стек:
Стек используется для хранения состояний и символов грамматики. В процессе анализа парсер перемещает символы и состояния между стеком и входным буфером.Входной буфер:
Входной буфер содержит строку, которая должна быть разобрана. Строка канчивается специальным символом конца ($).Action table:
Таблица действий определяет действия парсера (shift, reduce, accept, или error) на основе текущего состояния и символа на вершине стека.Goto table:
Таблица переходов определяет новое состояние, в которое парсер должен перейти после выполнения reduce-операции, на основе текущего состояния и символа, к которому была применена редукция.
Шаги выполнения алгоритма
Алгоритм LR-парсера состоит из повторяющихся шагов, включающих операции shift и reduce:
Shift:
Перемещение следующего символа из входного буфера в стек.
Обновление состояния парсера в соответствии с таблицей действий.
Reduce:
Применение правила грамматики к символам на вершине стека.
Замена правой части правила на левую часть в стеке.
Переход в новое состояние согласно таблице переходов.
Рассмотрим пример грамматики и выполнение шагов парсинга:
Грамматика:
E → E + T | T T → T * F | F F → ( E ) | id
Входная строка: id + id * id
Шаги выполнения:
Shift:
idперемещается в стек, состояние обновляется.Reduce:
idпреобразуется вF, затемFвT.Shift:
+перемещается в стек.Shift: следующий
idперемещается в стек, состояние обновляется.Reduce:
idпреобразуется вF, затемFвT, иTвE.Shift:
*перемещается в стек.Shift: следующий
idперемещается в стек, состояние обновляется.Reduce:
idпреобразуется вF, затемFвT.Reduce:
T * Fпреобразуется вT.Reduce:
E + Tпреобразуется вE.Accept: строка успешно разобрана.
Примеры кода
LR(0) парсер
class LR0Parser: def __init__(self, grammar, parsing_table): self.grammar = grammar self.parsing_table = parsing_table def parse(self, input_string): stack = [0] input_string = input_string.split() + ['$'] cursor = 0 while True: state = stack[-1] lookahead = input_string[cursor] action = self.parsing_table[state][lookahead] if action.startswith('s'): stack.append(lookahead) stack.append(int(action[1:])) cursor += 1 elif action.startswith('r'): production = self.grammar[int(action[1:])] for _ in range(2 * len(production[1])): stack.pop() state = stack[-1] stack.append(production[0]) stack.append(self.parsing_table[state][production[0]]) elif action == 'accept': print("Parsing completed successfully!") return True else: print("Error: Invalid syntax") return False grammar = [ ('S', ['A']), ('A', ['a', 'A']), ('A', ['b']) ] parsing_table = { 0: {'a': 's2', 'b': 's3', 'S': 1, 'A': 4}, 2: {'a': 's2', 'b': 's3', 'A': 5}, 3: {'$': 'r2'}, 4: {'$': 'accept'}, 5: {'a': 's2', 'b': 's3', 'A': 6}, 6: {'$': 'r1'} } parser = LR0Parser(grammar, parsing_table) parser.parse('a a b')
SLR парсер
class SLRParser: def __init__(self, grammar, parsing_table, follow_set): self.grammar = grammar self.parsing_table = parsing_table self.follow_set = follow_set def parse(self, input_string): stack = [0] input_string = input_string.split() + ['$'] cursor = 0 while True: state = stack[-1] lookahead = input_string[cursor] action = self.parsing_table[state].get(lookahead, None) if action is None: print("Error: Invalid syntax") return False elif action.startswith('s'): stack.append(lookahead) stack.append(int(action[1:])) cursor += 1 elif action.startswith('r'): production = self.grammar[int(action[1:])] for _ in range(2 * len(production[1])): stack.pop() state = stack[-1] stack.append(production[0]) stack.append(self.parsing_table[state][production[0]]) elif action == 'accept': print("Parsing completed successfully!") return True else: print("Error: Invalid syntax") return False grammar = [ ('S', ['E']), ('E', ['E', '+', 'T']), ('E', ['T']), ('T', ['T', '*', 'F']), ('T', ['F']), ('F', ['(', 'E', ')']), ('F', ['id']) ] parsing_table = { 0: {'id': 's5', '(': 's4', 'E': 1, 'T': 2, 'F': 3}, 1: {'+': 's6', '$': 'accept'}, 2: {'+': 'r2', '*': 's7', '$': 'r2'}, 3: {'+': 'r4', '*': 'r4', '$': 'r4'}, 4: {'id': 's5', '(': 's4', 'E': 8, 'T': 2, 'F': 3}, 5: {'+': 'r6', '*': 'r6', '$': 'r6'}, 6: {'id': 's5', '(': 's4', 'T': 9, 'F': 3}, 7: {'id': 's5', '(': 's4', 'F': 10}, 8: {')': 's11', '+': 's6'}, 9: {'+': 'r1', '*': 's7', '$': 'r1'}, 10: {'+': 'r3', '*': 'r3', '$': 'r3'}, 11: {'+': 'r5', '*': 'r5', '$': 'r5'} } follow_set = { 'S': ['$'], 'E': ['$', '+', ')'], 'T': ['$', '+', '*', ')'], 'F': ['$', '+', '*', ')'] } parser = SLRParser(grammar, parsing_table, follow_set) parser.parse('id + id * id')
LALR парсер
class LALRParser: def __init__(self, grammar, parsing_table): self.grammar = grammar self.parsing_table = parsing_table def parse(self, input_string): stack = [0] input_string = input_string.split() + ['$'] cursor = 0 while True: state = stack[-1] lookahead = input_string[cursor] action = self.parsing_table[state].get(lookahead, None) if action is None: print("Error: Invalid syntax") return False elif action.startswith('s'): stack.append(lookahead) stack.append(int(action[1:])) cursor += 1 elif action.startswith('r'): production = self.grammar[int(action[1:])] for _ in range(2 * len(production[1])): stack.pop() state = stack[-1] stack.append(production[0]) stack.append(self.parsing_table[state][production[0]]) elif action == 'accept': print("Parsing completed successfully!") return True else: print("Error: Invalid syntax") return False grammar = [ ('E', ['E', '+', 'T']), ('E', ['T']), ('T', ['T', '*', 'F']), ('T', ['F']), ('F', ['(', 'E', ')']), ('F', ['id']) ] parsing_table = { 0: {'id': 's5', '(': 's4', 'E': 1, 'T': 2, 'F': 3}, 1: {'+': 's6', '$': 'accept'}, 2: {'+': 'r2', '*': 's7', '$': 'r2'}, 3: {'+': 'r4', '*': 'r4', '$': 'r4'}, 4: {'id': 's5', '(': 's4', 'E': 8, 'T': 2, 'F': 3}, 5: {'+': 'r6', '*': 'r6', '$': 'r6'}, 6: {'id': 's5', '(': 's4', 'T': 9, 'F': 3}, 7: {'id': 's5', '(': 's4', 'F': 10}, 8: {')': 's11', '+': 's6'}, 9: {'+': 'r1', '*': 's7', '$': 'r1'}, 10: {'+': 'r3', '*': 'r3', '$': 'r3'}, 11: {'+': 'r5', '*': 'r5', '$': 'r5'} } parser = LALRParser(grammar, parsing_table) parser.parse('id + id * id')
Canonical LR(1) парсер
class LR1Parser: def __init__(self, grammar, parsing_table): self.grammar = grammar self.parsing_table = parsing_table def parse(self, input_string): stack = [0] input_string = input_string.split() + ['$'] cursor = 0 while True: state = stack[-1] lookahead = input_string[cursor] action = self.parsing_table[state].get(lookahead, None) if action is None: print("Error: Invalid syntax") return False elif action.startswith('s'): stack.append(lookahead) stack.append(int(action[1:])) cursor += 1 elif action.startswith('r'): production = self.grammar[int(action[1:])] for _ in range(2 * len(production[1])): stack.pop() state = stack[-1] stack.append(production[0]) stack.append(self.parsing_table[state][production[0]]) elif action == 'accept': print("Parsing completed successfully!") return True else: print("Error: Invalid syntax") return False grammar = [ ('E', ['E', '+', 'T']), ('E', ['T']), ('T', ['T', '*', 'F']), ('T', ['F']), ('F', ['(', 'E', ')']), ('F', ['id']) ] parsing_table = { 0: {'id': 's5', '(': 's4', 'E': 1, 'T': 2, 'F': 3}, 1: {'+': 's6', '$': 'accept'}, 2: {'+': 'r2', '*': 's7', '$': 'r2'}, 3: {'+': 'r4', '*': 'r4', '$': 'r4'}, 4: {'id': 's5', '(': 's4', 'E': 8, 'T': 2, 'F': 3}, 5: {'+': 'r6', '*': 'r6', '$': 'r6'}, 6: {'id': 's5', '(': 's4', 'T': 9, 'F': 3}, 7: {'id': 's5', '(': 's4', 'F': 10}, 8: {')': 's11', '+': 's6'}, 9: {'+': 'r1', '*': 's7', '$': 'r1'}, 10: {'+': 'r3', '*': 'r3', '$': 'r3'}, 11: {'+': 'r5', '*': 'r5', '$': 'r5'} } parser = LR1Parser(grammar, parsing_table) parser.parse('id + id * id'
Каждый из парсеров обрабатывает строку, следуя правилам грамматики, и осуществляет операции shift и reduce для построения дерева разбора или обнаружения ошибок в синтаксисе.
В завершение напомню о ближайших открытых уроках:
13 июня, «Этапы, задачи и виды проектирования»: разберем, как грамотно определить подход к проектированию информационной системы и программного обеспечения. Записаться по ссылке
19 июня, «Event storming как инструмент изучения предметной области». Запись по ссылке
