Дан контекстно‑свободный грамматический фрагмент, который порождает амбигуитет при разборе арифметических выражений с операциями + и * без приоритета: S -> S + S | S * S | (S) | id — преобразуйте грамматику в эквивалентную без неоднозначности, приведите алгоритм парсинга и обсудите, в каких случаях необходима более мощная модель (например, LR vs LL)
Коротко и по делу. 1) Преобразование грамматики в однозначную (задать приоритет и ассоциативность: ∗*∗ выше, обе — левосторонние): E→E+T∣TE \to E + T \mid TE→E+T∣TT→T∗F∣FT \to T * F \mid FT→T∗F∣FF→(E)∣idF \to (E) \mid idF→(E)∣id Эта грамматика однозначна: она задаёт приоритет ∗*∗ над +++ и лево‑ассоциативность обоих операторов. Для удобства парсинга верхнего уровня (убрать левую рекурсию) эквивалентная LL‑форма: E→T E′E \to T\;E'E→TE′E′→+ T E′∣εE' \to +\;T\;E' \mid \varepsilonE′→+TE′∣εT→F T′T \to F\;T'T→FT′T′→∗ F T′∣εT' \to *\;F\;T' \mid \varepsilonT′→∗FT′∣εF→(E)∣idF \to (E) \mid idF→(E)∣id 2) Алгоритм парсинга - Для LL(1) (рекурсивный спуск, predictive parsing) можно реализовать функции: parseE(): parseT() parseEprime() parseEprime(): if (current == '+') { consume('+'); parseT(); parseEprime(); } else return parseT(): parseF() parseTprime() parseTprime(): if (current == '*') { consume('*'); parseF(); parseTprime(); } else return parseF(): if (current == '(') { consume('('); parseE(); consume(')'); } else if (current == id) consume(id); else error (Лексемы и ошибки — стандартно.) - Для LR‑парсеров (LR(0)/SLR(1)/LALR(1)/LR(1)) можно взять исходную (лево-рекурсивную) форму E→E+E∣E∗E∣(E)∣idE \to E + E \mid E * E \mid (E) \mid idE→E+E∣E∗E∣(E)∣id
но лучше использовать однозначную с разделением уровней (первые правила выше). LR‑парсер автоматически управляет левой рекурсией и ассоциативностью; при конфликтах применяют декларации приоритета/ассоциативности или переписывают грамматику. 3) Когда нужна более мощная модель (LR vs LL) - LL(1) (рекурсивный спуск) прост и прозрачен, но требует: - отсутствие левой рекурсии (надо её устранять), - отсутствие неоднозначных выборов без достаточного lookahead, - грамматика должна быть LL(1) (или преобразуема в неё). - LR (особенно LALR(1) / LR(1)) мощнее: - поддерживает левую рекурсию напрямую, - допускает больше конструкций (непрямое/жёсткое факторирование, более сложные разветвления), - лучше справляется с грамматиками реальных языков (C, Java) и с конфликтами, которые трудно устранить для LL(1). Примеры, когда нужен LR: - грамматика не может быть преобразована в LL(1) без существенной потери структуры, - конструкции с «развешенным» контекстом (dangling else — хотя его часто решают приоритетом), - необходимость точного анализа без ручного переписывания множества правил. Вывод: для арифметики достаточно LL(1)‑грамы (представленной выше) и рекурсивного спуска; для сложных языков и грамматик с леворекурсией/конфликтами предпочтителен LR‑парсер.
1) Преобразование грамматики в однозначную (задать приоритет и ассоциативность: ∗*∗ выше, обе — левосторонние):
E→E+T∣TE \to E + T \mid TE→E+T∣T T→T∗F∣FT \to T * F \mid FT→T∗F∣F F→(E)∣idF \to (E) \mid idF→(E)∣id
Эта грамматика однозначна: она задаёт приоритет ∗*∗ над +++ и лево‑ассоциативность обоих операторов.
Для удобства парсинга верхнего уровня (убрать левую рекурсию) эквивалентная LL‑форма:
E→T E′E \to T\;E'E→TE′ E′→+ T E′∣εE' \to +\;T\;E' \mid \varepsilonE′→+TE′∣ε T→F T′T \to F\;T'T→FT′ T′→∗ F T′∣εT' \to *\;F\;T' \mid \varepsilonT′→∗FT′∣ε F→(E)∣idF \to (E) \mid idF→(E)∣id
2) Алгоритм парсинга
- Для LL(1) (рекурсивный спуск, predictive parsing) можно реализовать функции:
parseE():
parseT()
parseEprime()
parseEprime():
if (current == '+') { consume('+'); parseT(); parseEprime(); }
else return
parseT():
parseF()
parseTprime()
parseTprime():
if (current == '*') { consume('*'); parseF(); parseTprime(); }
else return
parseF():
if (current == '(') { consume('('); parseE(); consume(')'); }
else if (current == id) consume(id);
else error
(Лексемы и ошибки — стандартно.)
- Для LR‑парсеров (LR(0)/SLR(1)/LALR(1)/LR(1)) можно взять исходную (лево-рекурсивную) форму
E→E+E∣E∗E∣(E)∣idE \to E + E \mid E * E \mid (E) \mid idE→E+E∣E∗E∣(E)∣id но лучше использовать однозначную с разделением уровней (первые правила выше). LR‑парсер автоматически управляет левой рекурсией и ассоциативностью; при конфликтах применяют декларации приоритета/ассоциативности или переписывают грамматику.
3) Когда нужна более мощная модель (LR vs LL)
- LL(1) (рекурсивный спуск) прост и прозрачен, но требует:
- отсутствие левой рекурсии (надо её устранять),
- отсутствие неоднозначных выборов без достаточного lookahead,
- грамматика должна быть LL(1) (или преобразуема в неё).
- LR (особенно LALR(1) / LR(1)) мощнее:
- поддерживает левую рекурсию напрямую,
- допускает больше конструкций (непрямое/жёсткое факторирование, более сложные разветвления),
- лучше справляется с грамматиками реальных языков (C, Java) и с конфликтами, которые трудно устранить для LL(1).
Примеры, когда нужен LR:
- грамматика не может быть преобразована в LL(1) без существенной потери структуры,
- конструкции с «развешенным» контекстом (dangling else — хотя его часто решают приоритетом),
- необходимость точного анализа без ручного переписывания множества правил.
Вывод: для арифметики достаточно LL(1)‑грамы (представленной выше) и рекурсивного спуска; для сложных языков и грамматик с леворекурсией/конфликтами предпочтителен LR‑парсер.