Опубликован: 08.04.2009 | Уровень: для всех | Доступ: платный
Лекция 15:

Контекстно-свободные грамматики

15.3.4. Написать LL(1)-грамматику для того же языка.

Решение.

\begin{align*}
   \hbox{\texttt{K}}&\to\hbox{\texttt{\# K}}\\
   \hbox{\texttt{K}}&\to
\end{align*}

Как говорят, "леворекурсивное" правило заменено на "праворекурсивное".

Следующая задача показывает, что для LL(1)-грамматики существует не более одного возможного продолжения левого вывода.

15.3.5. Пусть дано выводимое в LL(1)-грамматике слово X, в котором выделен самый левый нетерминал K: X=AKS, где A - слово из терминалов, S - слово из терминалов и нетерминалов. Пусть существуют два различных правила грамматики с нетерминалом K в левой части, и мы применили их к выделенному в X нетерминалу K, затем продолжили вывод и в конце концов получили два слова из терминалов, начинающихся на A. Доказать, что в этих словах за началом A идут разные буквы. (Здесь к числу букв мы относим EOI.)

Решение. Эти буквы принадлежат направляющим множествам различных правил.

15.3.6. Доказать, что если слово выводимо в LL(1)-грамматике, то его левый вывод единствен.

Решение. Предыдущая задача показывает, что на каждом шаге левый вывод продолжается однозначно.

15.3.7. Грамматика называется леворекурсивной, если из некоторого нетерминала K выводится слово, начинающееся с K, но не совпадающее с ним. Доказать, что леворекурсивная грамматика, в которой из каждого нетерминала выводится хотя бы одно непустое слово из терминалов и для каждого нетерминала существует вывод (начинающийся с начального нетерминала), в котором он встречается, не является LL(1)-грамматикой.

Решение. Пусть из K выводится KU, где K - нетерминал, а U - непустое слово. Можно считать, что это левый вывод (другие нетерминалы можно не заменять). Рассмотрим вывод

K \leadsto KU \leadsto KUU \leadsto \ldots
(знак \leadsto обозначает несколько шагов вывода) и левый вывод K \leadsto A, где A - непустое слово из терминалов. На каком-то шаге второй вывод отклоняется от первого, а между тем по обоим путям может быть получено слово, начинающееся на A (в первом случае это возможно, так как сохраняется нетерминал K, который может впоследствии быть заменен на A ). Это противоречит возможности однозначного определения правила, применяемого на очередном шаге поиска левого вывода. (Однозначность выполняется для выводов из начального нетерминала, и надо воспользоваться тем, что K по предположению встречается в таком выводе.)

Таким образом, к леворекурсивным грамматикам (кроме тривиальных случаев) LL(1)-метод неприменим. Их приходится преобразовывать к эквивалентным LL(1)-грамматикам - или пользоваться другими методами распознавания.

15.3.8. Используя сказанное, построить алгоритм проверки выводимости слова из терминалов в LL(1)-грамматике.

Решение. Мы следуем описанному выше методу поиска левого вывода, храня лишь часть слова, находящуюся правее уже прочитанной части входного слова. Другими словами, мы храним слово S из терминалов и нетерминалов, обладающее такими свойствами (прочитанную часть входа обозначаем через A ):

  1. слово AS выводимо в грамматике;
  2. любой левый вывод входного слова проходит через стадию AS

Эти свойства вместе будем обозначать "(И)".

Вначале A пусто, а S состоит из единственного символа - начального нетерминала.

Если в некоторый момент S начинается на терминал t и {\texttt{t}} = {\texttt{Next}}, то можно выполнить команду Move и удалить символ t, являющийся начальным в S, поскольку при этом AS не меняется.

Если S начинается на терминал t и {\texttt{t}}\neq{\texttt{Next}}, то входное слово невыводимо - ибо по условию любой его вывод должен проходить через AS. (Это же справедливо и в случае {\texttt{Next}} =
{\texttt{EOI}}.)

Если S пусто, то из условия (И) следует, что входное слово выводимо тогда и только тогда, когда {\texttt{Next}} =
{\texttt{EOI}}.

Остается случай, когда S начинается с некоторого нетерминала K. По доказанному выше все левые выводы из S слов, начинающихся на символ Next, начинаются с применения к S одного и того же правила - того, для которого Next принадлежит направляющему множеству. Если таких правил нет, то входное слово невыводимо. Если такое правило есть, то нужно применить его к первому символу слова S - при этом свойство (И) не нарушится. Приходим к такому алгоритму:

s := пустое слово;
error := false;
{error => входное слово невыводимо;}
{not error => (И)}
while (not error) and not ((Next=EOI) and (S пусто))
| |  do begin
| if (S начинается на терминал, равный Next) then begin
| | Move; удалить из S первый символ;
| end else if (S начинается на терминал, не равный Next)
| |   then begin
| | error := true;
| end else if (S пусто) and (Next <> EOI) then begin
| | error := true;
| end else if (S начинается на нетерминал и Next входит в
| |    направляющее множество одного из правил для этого
| |    нетерминала) then begin
| | применить это правило
| end else if (S начинается на нетерминал и Next не входит
| |  в направляющее множество ни одного из правил для этого
| |  нетерминала) then begin
| | error := true;
| end else begin
| | {так не бывает}
| end;
end;
{входное слово выводимо <=> not error}

Алгоритм заканчивает работу, поскольку при появлении терминала в начале слова S происходит чтение со входа или остановка, а бесконечный цикл сменяющих друг друга нетерминалов в начале S означал бы, что грамматика леворекурсивна. (А мы можем предполагать, согласно предыдущей задаче, что это не так: нетерминалы, не встречающиеся в выводах, а также нетерминалы, из которых не выводится непустого слова, несложно удалить из грамматики.)

Замечания.

  • Приведенный алгоритм использует S как стек (все действия производятся с левого конца).
  • Действия двух последних вариантов внутри цикла не приводят к чтению очередного символа со входа, поэтому их можно заранее предвычислить для каждого нетерминала и каждого символа Next. После этого на каждом шаге цикла будет читаться очередной символ входа.
  • При практической реализации удобно составить таблицу, в которой записаны варианты действий в зависимости от входного символа и первого символа S, и небольшую программу, выполняющую действия в соответствии с этой таблицей.

15.3.9. При проверке того, относится ли данная грамматика к типу LL(1), необходимо вычислить Послед(T) и Нач(T) для всех нетерминалов T. Как это сделать?

Решение.Пусть, например, в грамматике есть правило K\to L\,M\,N. Тогда

{\setlength{\tabcolsep}{0pt}

%%%%%\hbox to\textwidth{\hss

\begin{tabular}{rcll}

{Нач}\,(L) & \subset {Нач}\,(K),\quad & & \\
{Нач}\,(M) & \subset {Нач}\,(K),\quad &&
   \text{если из L выводимо пустое слово,}\\
{Нач}\,(N) & \subset {Нач}\,(K),\quad &&
   \text{если из L и M выводимо пустое слово,}\\
%
{Послед}\,(K) & \subset {Послед}\,(N),\quad && \\
{Послед}\,(K) & \subset {Послед}\,(M),\quad &&
   \text{если из N выводимо пустое слово,}\\
{Послед}\,(K) & \subset {Послед}\,(L),\quad &&
   \text{если из M и N выводимо пустое слово,}\\
%
{Нач}\,(N) & \subset {Послед}\,(M),\quad && \\
{Нач}\,(M) & \subset {Послед}\,(L),\quad && \\
{Нач}\,(N) & \subset {Послед}\,(L),\quad &&
   \text{если из M выводимо пустое слово.}
%%%%%\end{tabular}\hss}
\end{alignat*}
Подобные правила позволяют постепенно шаг за шагом порождать множества Нач( T ), а затем и Послед( T ), для всех терминалов и нетерминалов T. При этом началом служит
{EOI} \in {Послед}\,(K)
для начального нетерминала K и
z \in {Нач}\,(z)
для любого терминала z. Порождение заканчивается, когда применение правил перестает давать новые элементы множеств Нач(T) и Послед(T).

Татьяна Новикова
Татьяна Новикова
Россия, Пошатово
Artem Bardakov
Artem Bardakov
Россия