Дан формальный контекстно-свободный грамматический фрагмент, генерирующий арифметические выражения с рекурсией влево; предложите метод преобразования грамматики для построения эффективного LL(1)-парсера, проанализируйте возможную неоднозначность и опишите, как построить AST, сохраняющий информацию о приоритете операций
Начнём с проблемы и затем предложу конкретное преобразование, анализ неоднозначности и способ построения AST. 1) Проблема (пример исходной грамматики) G0:E→E+E∣E∗E∣(E)∣idG_0:\quad E \to E + E \mid E * E \mid (E) \mid idG0:E→E+E∣E∗E∣(E)∣id
Эта грамматика леворекурсивна и неоднозначна: при выражении a+b∗ca+b*ca+b∗c нет правила приоритета, а при a−b−ca-b-ca−b−c невозможно однозначно задать ассоциативность. 2) Преобразование для LL(1) — убрать левую рекурсию и ввести уровни приоритета Обычно вводят нетерминалы для уровней приоритета (E — сумма, T — произведение, F — фактор). Без левой рекурсии: E→T E′E′→+ T E′∣− T E′∣εT→F T′T′→∗ F T′∣/ F T′∣εF→(E)∣id∣number∣−F(унарный минус)
\begin{aligned} E &\to T\ E'\\ E' &\to +\ T\ E' \mid -\ T\ E' \mid \varepsilon\\ T &\to F\ T'\\ T' &\to *\ F\ T' \mid /\ F\ T' \mid \varepsilon\\ F &\to (E)\mid id\mid number\mid -F\quad(\text{унарный минус}) \end{aligned} EE′TT′F→TE′→+TE′∣−TE′∣ε→FT′→∗FT′∣/FT′∣ε→(E)∣id∣number∣−F(унарныйминус)
Этот приём устраняет левую рекурсию и кодирует приоритет ∗/ * / ∗/ выше чем +− + - +−. Для правой ассоциативных операторов (например, возведение в степень \(^\)) используют правую рекурсию: P→B P∣B
P \to B\ ^\ P \mid B P→BP∣B
(чтобы получить правую ассоциативность). 3) Анализ неоднозначности и LL(1)-корректность - Неоднозначность исходной грамматики снимается: разделение на уровни приоритета однозначно определяет дерево (если вы однозначно задаёте ассоциативность: E' реализует левую ассоциативность через свёртку слева). - Для LL(1) нужно, чтобы альтернативы имели раздельные FIRST (и при наличии пустой альтернативы — FIRST ∩ FOLLOW = ∅). В приведённой грамматике FIRST-альтернативы для E′E'E′ — {+,−,ε}\{+,-,\varepsilon\}{+,−,ε} и для T′T'T′ — {∗,/,ε}\{*,/,\varepsilon\}{∗,/,ε}. Конфликтов нет. Унарный минус в FFF распознаётся на позициях начала фактора, а бинарный минус — в E′E'E′ после TTT, поэтому они не конфликтуют. - При добавлении новых операторов нужно проверять FIRST/FOLLOW для конфликтов и при необходимости делать левое факторирование. 4) Построение AST, сохраняющего приоритет и ассоциативность - Структура AST: листья — числа/идентификаторы; узлы — бинарные операторы с двумя поддеревьями, унарные — один ребёнок. - Рекомендуемый приём реализации для левоассоциативных уровней — итеративная свёртка (точно соответствует LL(1)-реализации с правой рекурсией E′E'E′), псевдокод для parseE: parseE(): left = parseT() while nextToken in {+, -}: op = nextToken; consume(op) right = parseT() left = new BinOp(op, left, right) return left parseT() аналогично для {*, /}: left = parseF() while nextToken in {*, /}: op = nextToken; consume(op) right = parseF() left = new BinOp(op, left, right) return left parseF(): if nextToken == '-': consume('-') child = parseF() return new UnaryOp('-', child) if nextToken == '(': consume('(') node = parseE() consume(')') return node if nextToken is number or id: return new Leaf(token) - Этот алгоритм создаёт левую ассоциативность естественным образом: для a−b−ca-b-ca−b−c получится дерево BinOp(′−′, BinOp(′−′, a,b),c)BinOp('-',\,BinOp('-',\,a, b), c)BinOp(′−′,BinOp(′−′,a,b),c). - Для правой ассоциативных операторов (например, \(a^b^c\)) парсер-функция должна использовать рекурсивный разбор правой рекурсии: parsePower(): left = parseBase() if nextToken == '^': consume('^') right = parsePower() return new BinOp('^', left, right) return left 5) Дополнительные замечания - Если нужны разные приоритеты/ассоциативности, добавьте соответствующие уровни нетерминалов; это автоматически сохранит приоритет в форме дерева (глубина отражает приоритет). - Перед реализацией проверьте FIRST/FOLLOW для каждой альтернативы чтобы гарантировать LL(1). При конфликте примените левое факторирование или перестройте грамматику. - Семантические действия (или построение узлов в AST) можно привязать к каждой продукции в генераторе парсера (например, ANTLR, hand-written recursive-descent). Конец.
1) Проблема (пример исходной грамматики)
G0:E→E+E∣E∗E∣(E)∣idG_0:\quad E \to E + E \mid E * E \mid (E) \mid idG0 :E→E+E∣E∗E∣(E)∣id Эта грамматика леворекурсивна и неоднозначна: при выражении a+b∗ca+b*ca+b∗c нет правила приоритета, а при a−b−ca-b-ca−b−c невозможно однозначно задать ассоциативность.
2) Преобразование для LL(1) — убрать левую рекурсию и ввести уровни приоритета
Обычно вводят нетерминалы для уровней приоритета (E — сумма, T — произведение, F — фактор). Без левой рекурсии:
E→T E′E′→+ T E′∣− T E′∣εT→F T′T′→∗ F T′∣/ F T′∣εF→(E)∣id∣number∣−F(унарный минус) \begin{aligned}
E &\to T\ E'\\
E' &\to +\ T\ E' \mid -\ T\ E' \mid \varepsilon\\
T &\to F\ T'\\
T' &\to *\ F\ T' \mid /\ F\ T' \mid \varepsilon\\
F &\to (E)\mid id\mid number\mid -F\quad(\text{унарный минус})
\end{aligned}
EE′TT′F →T E′→+ T E′∣− T E′∣ε→F T′→∗ F T′∣/ F T′∣ε→(E)∣id∣number∣−F(унарный минус) Этот приём устраняет левую рекурсию и кодирует приоритет ∗/ * / ∗/ выше чем +− + - +−. Для правой ассоциативных операторов (например, возведение в степень \(^\)) используют правую рекурсию:
P→B P∣B P \to B\ ^\ P \mid B
P→B P∣B (чтобы получить правую ассоциативность).
3) Анализ неоднозначности и LL(1)-корректность
- Неоднозначность исходной грамматики снимается: разделение на уровни приоритета однозначно определяет дерево (если вы однозначно задаёте ассоциативность: E' реализует левую ассоциативность через свёртку слева).
- Для LL(1) нужно, чтобы альтернативы имели раздельные FIRST (и при наличии пустой альтернативы — FIRST ∩ FOLLOW = ∅). В приведённой грамматике FIRST-альтернативы для E′E'E′ — {+,−,ε}\{+,-,\varepsilon\}{+,−,ε} и для T′T'T′ — {∗,/,ε}\{*,/,\varepsilon\}{∗,/,ε}. Конфликтов нет. Унарный минус в FFF распознаётся на позициях начала фактора, а бинарный минус — в E′E'E′ после TTT, поэтому они не конфликтуют.
- При добавлении новых операторов нужно проверять FIRST/FOLLOW для конфликтов и при необходимости делать левое факторирование.
4) Построение AST, сохраняющего приоритет и ассоциативность
- Структура AST: листья — числа/идентификаторы; узлы — бинарные операторы с двумя поддеревьями, унарные — один ребёнок.
- Рекомендуемый приём реализации для левоассоциативных уровней — итеративная свёртка (точно соответствует LL(1)-реализации с правой рекурсией E′E'E′), псевдокод для parseE:
parseE():
left = parseT()
while nextToken in {+, -}:
op = nextToken; consume(op)
right = parseT()
left = new BinOp(op, left, right)
return left
parseT() аналогично для {*, /}:
left = parseF()
while nextToken in {*, /}:
op = nextToken; consume(op)
right = parseF()
left = new BinOp(op, left, right)
return left
parseF():
if nextToken == '-':
consume('-')
child = parseF()
return new UnaryOp('-', child)
if nextToken == '(':
consume('(')
node = parseE()
consume(')')
return node
if nextToken is number or id:
return new Leaf(token)
- Этот алгоритм создаёт левую ассоциативность естественным образом: для a−b−ca-b-ca−b−c получится дерево BinOp(′−′, BinOp(′−′, a,b),c)BinOp('-',\,BinOp('-',\,a, b), c)BinOp(′−′,BinOp(′−′,a,b),c).
- Для правой ассоциативных операторов (например, \(a^b^c\)) парсер-функция должна использовать рекурсивный разбор правой рекурсии:
parsePower():
left = parseBase()
if nextToken == '^':
consume('^')
right = parsePower()
return new BinOp('^', left, right)
return left
5) Дополнительные замечания
- Если нужны разные приоритеты/ассоциативности, добавьте соответствующие уровни нетерминалов; это автоматически сохранит приоритет в форме дерева (глубина отражает приоритет).
- Перед реализацией проверьте FIRST/FOLLOW для каждой альтернативы чтобы гарантировать LL(1). При конфликте примените левое факторирование или перестройте грамматику.
- Семантические действия (или построение узлов в AST) можно привязать к каждой продукции в генераторе парсера (например, ANTLR, hand-written recursive-descent).
Конец.