Привет, Хабр!
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 как инструмент изучения предметной области». Запись по ссылке
Nikeware
На ваши "открытые уроки" записываться не буду. Не надейтесь!
p.s. Лучше бы показали как сами таблицы парсинга строятся.