Постройте формальную грамматику (контекстно‑свобо́дную или регулярную) для описания простого языка арифметических выражений с унарным минусом, скобками и операциями +, -, *, /; объясните, как преобразовать эту грамматику в парсер (рекурсивный‑нисходящий или табличный) и укажите возможные проблемы леворекурсии или неоднозначности
Грамматика (контекстно‑свободная, LL(1)-совместимая, учитывает унарный минус и приоритеты): Expr → Term Expr’Expr’ → ′+′ Term Expr’ ∣ ′−′ Term Expr’ ∣ ϵTerm → Factor Term’Term’ → ′∗′ Factor Term’ ∣ ′/′ Factor Term’ ∣ ϵFactor → ′−′ Factor ∣ ′(′ Expr ′)′ ∣ NUMBER
\begin{aligned} &\text{Expr} \;\to\; \text{Term}\ \text{Expr'}\\ &\text{Expr'} \;\to\; '+'\ \text{Term}\ \text{Expr'} \;|\; '-'\ \text{Term}\ \text{Expr'} \;|\; \epsilon\\ &\text{Term} \;\to\; \text{Factor}\ \text{Term'}\\ &\text{Term'} \;\to\; '*'\ \text{Factor}\ \text{Term'} \;|\; '/'\ \text{Factor}\ \text{Term'} \;|\; \epsilon\\ &\text{Factor} \;\to\; '-'\ \text{Factor} \;|\; '('\ \text{Expr}\ ')'\;|\; \text{NUMBER} \end{aligned} Expr→TermExpr’Expr’→′+′TermExpr’∣′−′TermExpr’∣ϵTerm→FactorTerm’Term’→′∗′FactorTerm’∣′/′FactorTerm’∣ϵFactor→′−′Factor∣′(′Expr′)′∣NUMBER Пояснения: - Приоритеты заданы структурой: Factor\text{Factor}Factor (унарный минус, скобки, число) имеет наибольший приоритет, затем ∗,/*,/∗,/, затем +,−+,-+,−. - Унарный минус реализован правилом Factor→′−′ Factor\text{Factor}\to '-'\ \text{Factor}Factor→′−′Factor, что делает его правооперавативным: −−a- -a−−a парсится как −(−a)-(-a)−(−a). - ϵ\epsilonϵ — пустая продукция. Как преобразовать в парсер 1) Рекурсивный нисходящий (recursive‑descent, прямой код): - Для каждой нетерминальной функции написать процедуру, использующую текущее токен/лексему. - Пример псевдокода (очень сжато): parseExpr(): parseTerm() while currentToken in {'+','-'}: op = consume() parseTerm() buildBinary(op) parseTerm(): parseFactor() while currentToken in {'*','/'}: op = consume() parseFactor() buildBinary(op) parseFactor(): if currentToken == '-': consume() node = parseFactor() return makeUnary('-', node) elif currentToken == '(': consume(); node = parseExpr(); expect(')'); return node elif currentToken is NUMBER: return makeNumber(consume()) - Эта версия использует устранённую левую рекурсию (см. ниже) и является простым LL(1)-парсером при корректной лексеме NUMBER и разделении токенов. 2) Табличный LL(1)-парсер: - Построить FIRST/FOLLOW для нетерминалов и таблицу разборов; таблица будет однозначна для предложенной грамматики (при корректном лексическом разборе). - Алгоритм: стек с начальным символом Expr\text{Expr}Expr и входной поток токенов; применять продукцию по таблице. Возможные проблемы - Леворекурсия: простая запись вида Expr→Expr+Term\text{Expr}\to\text{Expr} + \text{Term}Expr→Expr+Term является лево-рекурсивной и не подходит для рекурсивного нисходящего парсера. Решение — факторизация/преобразование в форму с хвостовой продукцией (как в приведённой грамматике с Expr’\text{Expr'}Expr’ и Term’\text{Term'}Term’). - Неоднозначность: грамматика вида Expr→Expr ′+′ Expr ∣ Expr ′∗′Expr ∣ …\text{Expr}\to\text{Expr}\ '+'\ \text{Expr}\ |\ \text{Expr}\ '*' \text{Expr}\ |\ \dotsExpr→Expr′+′Expr∣Expr′∗′Expr∣… без явного задания приоритетов/ассоциативности будет неоднозначна. В приведённой грамматике приоритеты и левосторонняя ассоциативность для +,−+,-+,− и ∗,/*,/∗,/ закодированы структурой (она не неоднозначна). - Унарный vs бинарный минус: конфликт решается грамматикой контекстно (в позициях начала факторa минус трактуется как унарный, в позиции после терма — как бинарный в Expr’\text{Expr'}Expr’). В лексере/парсере надо отличать только токен '-'; грамматика и алгоритм разбора однозначно определяют роль. - LR‑парсер: для bottom‑up парсеров возможны shift/reduce конфликты для грамматик с неявными приоритетами; решается либо явным определением приоритетов/ассоциативности, либо использованием той же факторизованной грамматики. - Error‑recovery и лексические детали (пробелы, целые/вещественные числа, идентификаторы) нужно реализовать отдельно. Итого: использовать предложенную LL(1)-грамматику, реализовать рекурсивный нисходящий парсер по шаблону parseExpr/parseTerm/parseFactor; избегать исходной леворекурсивной формы и неявных приоритетов, чтобы устранить неоднозначности.
Expr → Term Expr’Expr’ → ′+′ Term Expr’ ∣ ′−′ Term Expr’ ∣ ϵTerm → Factor Term’Term’ → ′∗′ Factor Term’ ∣ ′/′ Factor Term’ ∣ ϵFactor → ′−′ Factor ∣ ′(′ Expr ′)′ ∣ NUMBER \begin{aligned}
&\text{Expr} \;\to\; \text{Term}\ \text{Expr'}\\
&\text{Expr'} \;\to\; '+'\ \text{Term}\ \text{Expr'} \;|\; '-'\ \text{Term}\ \text{Expr'} \;|\; \epsilon\\
&\text{Term} \;\to\; \text{Factor}\ \text{Term'}\\
&\text{Term'} \;\to\; '*'\ \text{Factor}\ \text{Term'} \;|\; '/'\ \text{Factor}\ \text{Term'} \;|\; \epsilon\\
&\text{Factor} \;\to\; '-'\ \text{Factor} \;|\; '('\ \text{Expr}\ ')'\;|\; \text{NUMBER}
\end{aligned}
Expr→Term Expr’Expr’→′+′ Term Expr’∣′−′ Term Expr’∣ϵTerm→Factor Term’Term’→′∗′ Factor Term’∣′/′ Factor Term’∣ϵFactor→′−′ Factor∣′(′ Expr ′)′∣NUMBER
Пояснения:
- Приоритеты заданы структурой: Factor\text{Factor}Factor (унарный минус, скобки, число) имеет наибольший приоритет, затем ∗,/*,/∗,/, затем +,−+,-+,−.
- Унарный минус реализован правилом Factor→′−′ Factor\text{Factor}\to '-'\ \text{Factor}Factor→′−′ Factor, что делает его правооперавативным: −−a- -a−−a парсится как −(−a)-(-a)−(−a).
- ϵ\epsilonϵ — пустая продукция.
Как преобразовать в парсер
1) Рекурсивный нисходящий (recursive‑descent, прямой код):
- Для каждой нетерминальной функции написать процедуру, использующую текущее токен/лексему.
- Пример псевдокода (очень сжато):
parseExpr():
parseTerm()
while currentToken in {'+','-'}:
op = consume()
parseTerm()
buildBinary(op)
parseTerm():
parseFactor()
while currentToken in {'*','/'}:
op = consume()
parseFactor()
buildBinary(op)
parseFactor():
if currentToken == '-':
consume()
node = parseFactor()
return makeUnary('-', node)
elif currentToken == '(':
consume(); node = parseExpr(); expect(')'); return node
elif currentToken is NUMBER:
return makeNumber(consume())
- Эта версия использует устранённую левую рекурсию (см. ниже) и является простым LL(1)-парсером при корректной лексеме NUMBER и разделении токенов.
2) Табличный LL(1)-парсер:
- Построить FIRST/FOLLOW для нетерминалов и таблицу разборов; таблица будет однозначна для предложенной грамматики (при корректном лексическом разборе).
- Алгоритм: стек с начальным символом Expr\text{Expr}Expr и входной поток токенов; применять продукцию по таблице.
Возможные проблемы
- Леворекурсия: простая запись вида Expr→Expr+Term\text{Expr}\to\text{Expr} + \text{Term}Expr→Expr+Term является лево-рекурсивной и не подходит для рекурсивного нисходящего парсера. Решение — факторизация/преобразование в форму с хвостовой продукцией (как в приведённой грамматике с Expr’\text{Expr'}Expr’ и Term’\text{Term'}Term’).
- Неоднозначность: грамматика вида Expr→Expr ′+′ Expr ∣ Expr ′∗′Expr ∣ …\text{Expr}\to\text{Expr}\ '+'\ \text{Expr}\ |\ \text{Expr}\ '*' \text{Expr}\ |\ \dotsExpr→Expr ′+′ Expr ∣ Expr ′∗′Expr ∣ … без явного задания приоритетов/ассоциативности будет неоднозначна. В приведённой грамматике приоритеты и левосторонняя ассоциативность для +,−+,-+,− и ∗,/*,/∗,/ закодированы структурой (она не неоднозначна).
- Унарный vs бинарный минус: конфликт решается грамматикой контекстно (в позициях начала факторa минус трактуется как унарный, в позиции после терма — как бинарный в Expr’\text{Expr'}Expr’). В лексере/парсере надо отличать только токен '-'; грамматика и алгоритм разбора однозначно определяют роль.
- LR‑парсер: для bottom‑up парсеров возможны shift/reduce конфликты для грамматик с неявными приоритетами; решается либо явным определением приоритетов/ассоциативности, либо использованием той же факторизованной грамматики.
- Error‑recovery и лексические детали (пробелы, целые/вещественные числа, идентификаторы) нужно реализовать отдельно.
Итого: использовать предложенную LL(1)-грамматику, реализовать рекурсивный нисходящий парсер по шаблону parseExpr/parseTerm/parseFactor; избегать исходной леворекурсивной формы и неявных приоритетов, чтобы устранить неоднозначности.