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

 

     13.1. Контекстно-свободные грамматики. Общий алгоритм  разбора.

 

     Чтобы  определить  то,  что  называют  контекстно-свободной

грамматикой (КС-грамматикой), надо:

     (а) указать конечное множество A, называемое алфавитом; его

элементы  называют символами; конечные последовательности симво-

лов называют словами (в данном алфавите);

     (б) разделить все символы алфавита A на две группы:  терми-

нальные ("окончательные") и нетерминальные ("промежуточные");

     (в)  выбрать среди нетерминальных символов один, называемый

начальным;

     (г) указать конечное число правил грамматики, каждое из ко-

торых должно иметь вид

     K -> X

где K - некоторый нетерминальный символ, а X - слово (в него мо-

гут входить и терминальные, и нетерминальные символы).

 

     Пусть  фиксирована  КС-грамматика  (мы часто будем опускать

приставку "КС-", так как других грамматик у нас не будет). Выво-

дом в этой грамматике называется последовательность  слов  X[0],

X[1],..., X[n], в которой X[0] состоит из одного символа, и этот

символ  - начальный, а X[i+1] получается из X[i] заменой некото-

рого нетерминального символа K на слово X по  одному  из  правил

грамматики.  Слово, составленное из терминальных символов, назы-

вается выводимым, если существует вывод, который  им  кончается.

Множество всех выводимых слов (из терминальных символов) называ-

ется языком, порождаемым данной грамматикой.

     В  этой  и следующих главах мы будем ходить вокруг да около

такого вопроса: дана КС-грамматика; построить алгоритм,  который

по любому слову проверяет, выводимо ли оно в этой грамматике.

 

     Пример 1.   Алфавит:

 

            ( ) [ ] E

 

(четыре  терминальных  символа  и один нетерминальный символ E).

Начальный символ: e.

Правила:

                E -> (E)

                E -> [E]

                E -> EE

                E ->

 

(в последнем правиле справа стоит пустое слово).

 

     Примеры выводимых слов:

 

                     (пустое слово)

                ()

                ([])

                ()[([])]

                [()[]()[]]

 

     Примеры невыводимых слов:

 

                (

                )(

                (]

                ([)]

 

Эта грамматика встречалась в разделе 00 (где выводимость  в  ней

проверялась с помощью стека).

 

     Пример 2. Другая грамматика, порождающая тот же язык:

 

Алфавит: ( ) [ ] T E

 

Правила:

           E ->

           E -> TE

           T -> (E)

           T -> [E]

 

Начальным символом во всех приводимых далее примерах будем  счи-

тать  символ,  стоящий  в  левой части первого правила (в данном

случае это символ T), не оговаривая этого особо.

 

     Для каждого нетерминального символа можно рассмотреть  мно-

жество всех слов из терминальных символов, которые из него выво-

дятся (аналогично тому, как это сделано для начального символа в

определении выводимости в грамматике). Каждое правило грамматики

можно  рассматривать  как свойство этих множеств. Покажем это на

примере только что приведенной грамматики. Пусть SetT и  SetE  -

множества  слов (из скобок), выводимых из нетерминалов T и E со-

ответственно.  Тогда  правилам  грамматики  соответствуют  такие

свойства:

 

E ->            SetE содержит пустое слово

E -> TE         если слово A принадлежит SetT,

                слово B принадлежит

                SetE, то слово AB принадлежит SetE

T -> [E]        если A принадлежит

                SetE, то слово [A] принадлежит SetT

T -> (E)        если A принадлежит

                SetE, то слово (A) принадлежит SetT

 

Сформулированные  свойства множеств SetE, SetT не определяют эти

множества однозначно (например, они остаются верными, если в ка-

честве SetE и SetT взять множество всех слов). Однако можно  до-

казать,  что  множества,  задаваемые грамматикой, являются мини-

мальными среди удовлетворяющих этим условиям.

 

     13.1.1.  Сформулируйте точно и докажите это утверждение для

произвольной контекстно-свободной грамматики.

 

     13.1.2. Постройте грамматику, в которой выводимы слова

     (а) 00..0011..11 (число нулей равно числу единиц);

     (б) 00..0011..11 (число нулей вдвое больше числа единиц);

     (в) 00..0011..11 (число нулей больше числа единиц);

(и только они).

 

     13.1.3.  Доказать, что не существует КС-грамматики, в кото-

рой были бы выводимы слова вида  00..0011..1122..22,  в  которых

числа нулей, единиц и двоек равны, и только они.

     Указание. Докажите следующую лемму о произвольной  КС-грам-

матике:  для  любого  достаточно  длинного слова F, выводимого в

этой грамматике,  существует  такое  его  представление  в  виде

ABCDE,  что  любое  слово  вида AB..BCD..DE, где B и D повторены

одинаковое число раз, также выводимо  в  этой  грамматике.  (Это

можно  установить,  найдя  нетерминальный  символ, оказывающийся

своим собственным "наследником" в процессе вывода.)

 

     Нетерминальный символ можно рассматривать как "родовое имя"

для выводимых из него слов. В следующем примере для  наглядности

в   качестве   нетерминальных  символов  использованы  фрагменты

русских слов, заключенные в  угловые  скобки.    точки  зрения

грамматики каждое такое слово - один символ!)

 

     Пример 3. Алфавит:

 

терминалы: + * ( ) x

нетерминалы: <выр>, <оствыр>, <слаг>, <остслаг>, <множ>

правила:

 

    <выр>     -> <слаг> <оствыр>

    <оствыр>  -> + <выр>

    <оствыр>  ->

    <слаг>    -> <множ> <остслаг>

    <остслаг> -> * <слаг>

    <остслаг> ->

    <множ>    -> x

    <множ>    -> ( <выр> )

 

Согласно этой грамматике, выражение  (<выр>)  -  это  последова-

тельность  слагаемых  (<слаг>), разделенных плюсами, слагаемое -

это последовательность множителей (<множ>), разделенных звездоч-

ками (знаками умножения), а множитель - это либо буква  x,  либо

выражение в скобках.

 

     13.1.4. Приведите пример другой грамматики, задающей тот же

язык.

 

     Ответ. Вот один из вариантов:

    <выр> -> <выр> + <выр>

    <выр> -> <выр> * <выр>

    <выр> -> x

    <выр> -> ( <выр> )

 

Эта  грамматика  хоть и проще, но в некоторых отношениях хуже, о

чем мы еще будем говорить.

 

     13.1.5. Дана произвольная КС-грамматика. Построить алгоритм

проверки принадлежности задаваемому ей языку, работающий полино-

миальное время (т.е. число действий не превосходит  полинома  от

длины проверяемого слова; полином может зависеть от грамматики).

 

     Решение. Заметим, что требование полиномиальности исключает

возможность решения, основанном на переборе всех возможных выво-

дов.  Тем не менее полиномиальный алгоритм существует. Поскольку

практического значения он не  имеет  (используемые  на  практике

КС-грамматики  обладают дополнительными свойствами, позволяющими

строить более эффективные алгоритмы), мы изложим лишь общую схе-

му решения.

 

     (1) Пусть в грамматике есть нетерминалы K1,...,Kn. Построим

новую грамматику с нетерминалами K1',...,Kn' так, чтобы выполня-

лось такое свойство: из Ki' выводятся (в новой грамматике) те же

слова, что из Ki в старой, за исключением пустого слова, которое

не выводится.

     Чтобы выполнить такое преобразование грамматики, надо выяс-

нить, из каких нетерминалов исходной грамматики выводится пустое

слово,  а  затем каждое правило заменить на совокупность правил,

получающихся, если в правой части опустить какие-либо из  нетер-

миналов, из которых выводится пустое слово, а у остальных поста-

вить штрихи. Например, если в исходной грамматике было правило

 

     K -> L M N,

 

причем  из L и N выводится пустое слово, а из M нет, то это пра-

вило надо заменить на правила

 

     K'-> L'M'N'

     K'->   M'N'

     K'-> L'M'

     K'->   M'

 

     (2) Итак, мы свели дело к грамматике, где ни из одного  не-

терминала не выводится пустое слово. Теперь устраним "циклы" ви-

да

     K -> L

     L -> M

     M -> N

     N -> K

(в правой части каждого правила один символ, и эти символы обра-

зуют  цикл  произвольной  длины): это легко сделать, отождествив

все входящие в цикл нетерминалы.

 

     (3) Теперь проверка принадлежности какого-либо слова языку,

порожденному  грамматикой,  может  выполняться  так: для каждого

подслова проверяемого слова и для каждого нетерминала  выясняем,

порождается ли это подслово этим нетерминалом. При этом подслова

проверяются  в порядке возрастания длин, а нетерминалы - в таком

порядке, чтобы при наличии правила K -> L нетерминал L проверял-

ся раньше нетерминала K. (Это возможно в  силу  отсутствия  цик-

лов.) Поясним этот процесс на примере.

     Пусть в грамматике есть правила

        K -> L

        K -> M N L

и других правил, содержащих K в левой части, нет. Мы  хотим  уз-

нать,  выводится  ли  данное слово A из нетерминала K. Это будет

так в одном из случаев: (1) если A выводится из L;  (2)  если  A

можно разбить на непустые слова B, C, D, для которых B выводится

из  M,  C выводится из N, а D выводится из L. Вся эта информация

уже есть (слова B, C, D короче A, а L рассмотрен до K).

     Легко  видеть, что число действий этого алгоритма полиноми-

ально. Степень полинома зависит от числа нетерминалов  в  правых

частях  правил и может быть понижена, если грамматику преобразо-

вать к форме, в которой правая часть каждого правила содержит  1

или  2  нетерминала (это легко сделать, вводя новые нетерминалы:

например, правило K -> LMK можно заменить на K -> LN и N ->  MK,

где N - новый нетерминал).

 

     13.1.6. Рассмотрим грамматику с  единственным  нетерминалом

K, нетерминалами 1, 2, 3 и правилами

 

     K -> 0

     K -> 1 K

     K -> 2 K K

     K -> 3 K K K

 

Как  проверить  выводимость слова в этой грамматике, читая слово

слева направо? (Число действий при прочтении одной буквы  должно

быть ограничено.)

 

     Решение.  Хранится целая переменная n, инвариант: слово вы-

водимо <-> непрочитанная часть представляет  собой  конкатенацию

(соединение) n выводимых слов.

 

     13.1.7. Тот же вопрос для грамматики

 

          K -> 0

          K -> K 1

          K -> K K 2

          K -> K K K 3

 

     13.2. Метод рекурсивного спуска.

 

     В отличие от алгоритма предыдущего раздела (представляющего

чисто теоретический интерес), алгоритмы на  основе  рекурсивного

спуска  часто используются на практике. Этот метод применим, од-

нако, далеко не ко всем грамматикам. Мы обсудим необходимые  ог-

раничения позднее.

     Идея  метода рекурсивного спуска такова. Для каждого нетер-

минала K мы строим процедуру ReadK, которая - в применении к лю-

бому входному слову x - делает две вещи:

     (1) находит наибольшее начало z слова x, которое может быть

началом выводимого из K слова;

     (2) сообщает, является ли найденное слово z выводимым из K.

 

     Прежде чем описывать этот метод более подробно, договоримся

о том, как процедуры получают сведения о входном слове и как со-

общают о результатах своей работы. Мы  предполагаем,  что  буквы

входного  слова  поступают к ним по одной, т.е. имеется граница,

отделяющая "прочитанную" часть от  "непрочитанной".  Будем  счи-

тать, что есть функция (без параметров)

 

     Next: Symbol

 

дающая  первый  непрочитанный  символ.  Ее значениями могут быть

терминальные символы, а также специальный  символ  EOI  (End  Of

Input  -  конец входа), означающий, что все слово уже прочитано.

Вызов  этой функции, естественно, не сдвигает границы между про-

читанной и непрочитанной частью - для этого есть процедура Move,

которая  сдвигает  границу  на один символ. (Она применима, если

Next <> EOI.) Пусть, наконец, имеется булевская переменная b.

 

     Теперь мы можем сформулировать наши требования к  процедуре

ReadK. Они состоят в следующем:

     (1)  ReadK  прочитывает  из  оставшейся  части слова макси-

мальное начало A, являющееся началом некоторого слова, выводимо-

го из K;

     (2) значение b становится истинным или ложным в зависимости

от того, является ли A выводимым из K или лишь невыводимым нача-

лом выводимого (из K) слова.

 

     Для удобства введем такую терминологию: выводимое из K сло-

во будем называть K-словом, а любое начало любого выводимого  из

K  слова - K-началом. Требования (1) и (2) вместе будем выражать

словами "ReadK корректна для K".

 

     Начнем с рассмотрения частного случая. Пусть правило

        K -> L M

является единственным правилом грамматики, содержащим K в  левой

части, пусть L, M - нетерминалы и ReadL, ReadM - корректные (для

них) процедуры.

     Рассмотрим такую процедуру:

 

     procedure ReadK;

     begin

     | ReadL;

     | if b then begin

     | | ReadM;

     | end;

     end;

 

     13.2.1.  Привести  пример, когда эта процедура будет некор-

ректной для K.

     Ответ. Пусть из L выводится любое слово вида 00..00, а из M

выводится лишь слово 01. Тогда из K выводится  слово  00001,  но

процедура ReadK этого не заметит.

 

     Укажем достаточноые условия корректности  процедуры  ReadK.

Для  этого нам понадобятся некоторые обозначения. Пусть фиксиро-

ваны КС-грамматика и некоторый  нетерминал  N  этой  грамматики.

Рассмотрим  N-слово A, которое имеет собственное начало B, также

являющееся N-словом (если такие есть). Для любой пары таких слов

A и B рассмотрим терминальный символ, идущий в A непосредственно

за B. Множество всех таких терминалов обозначим  Посл(N).  (Если

никакое N-слово не является собственным началом другого N-слова,

то множество Посл(N) пусто.)

 

     13.2.2. Указать (а) Посл(E) для примера 1;  (б)  Посл(E)  и

Посл(T)  для примера 2; (в) Посл(<слаг>) и Посл(<множ>) для при-

мера 3.

     Ответ.  (а)  Посл(e)  =  {  [, ( }. (б) Посл(e) = { [, ( };

Посл(t) пусто (никакое t-слово не является началом другого). (в)

Посл(<слаг>) = {*}; Посл(<множ>) пусто.

 

Кроме  того,  для  каждого  нетерминала N обозначим через Нач(N)

множество всех терминалов, являющихся первыми  буквами  непустых

N-слов.  Это  обозначение  - вместе с предыдущим - позволит дать

достаточное условие корректности процедуры ReadK в описанной вы-

ше ситуации.

 

     13.2.3.  Доказать,  что  если  Посл  (L)  не пересекается с

Нач(M) и множество всех M-слов непусто, то ReadK корректна.

 

     Решение. Рассмотрим два случая. (1) Пусть после ReadL  зна-

чение  переменной  b  ложно. В этом случае ReadM читает со входа

максимальное M-начало A, не являющееся  M-словом.  Оно  является

K-началом (здесь важно, что множество L-слов непусто.). Будет ли

оно максимальным K-началом среди начал входа? Если нет, то A яв-

ляется  началом  слова BC, где B есть L-слово, C есть M-начало и

BC - более длинное начало входа, чем A. Если B длиннее A, то A -

не максимальное начало входа, являющееся L-началом, что противо-

речит корректности ReadL. Если B = A, то A было бы  L-словом,  а

это  не так. Значит, B короче A, C непусто и первый символ слова

C следует в A за последним символом слова B, т.е. Посл(L)  пере-

секается с Нач(M). Противоречие. Итак, A максимально. Из сказан-

ного  следует  также,  что  A не является K-словом. Корректность

процедуры ReadK в этом случае проверена.

     (2) Пусть после ReadL значение переменной b истинно.  Тогда

прочитанное  процедурой  ReadK  начало входа имеет вид AB, где A

есть L-слово, а B есть M-начало. Тем  самым  AB  есть  K-начало.

Проверим его максимальность. Пусть C есть большее K-начало. Тог-

да  либо  C есть L-начало (что невозможно, так как A было макси-

мальным L-началом), либо C = A'B', где A' - L-слово, B' -  M-на-

чало.  Если  A'  короче A, то B' непусто и начинается с символа,

принадлежащего и Нач(M), и  Посл(L),  что  невозможно.  Если  A'

длиннее  A,  то  это противоречит тому, что A было максимальным.

Итак, A' = A. Но в этом случае B' есть продолжение B, что проти-

воречит корректности ReadM. Итак, AB  -  максимальное  K-начало.

Остается  проверить  правильность  выдаваемого  процедурой ReadK

значения переменной b. Если оно истинно, то это  очевидно.  Если

оно  ложно,  то B не есть M-слово, и надо проверить, что AB - не

K-слово. В самом деле, если бы выполнялось AB = A'B', где  A'  -

L-слово,  B' - M-слово, то A' не может быть длиннее A (ReadL чи-

тает максимальное слово), A' не может быть  равно  A  (тогда  B'

равно  B  и  не  является  M-словом) и A' не может быть короче A

(тогда первый символ B' принадлежит и Нач(M), и Посл(L)). Задача

решена.

 

     Перейдем теперь к другому частному случаю. Пусть в КС-грам-

матике есть правила

        K -> L

        K -> M

        K -> N

и других правил с левой частью K нет.

 

     13.2.4.  Считая, что ReadL, ReadM и ReadN корректны (для L,

M и N) и что множества Нач(L), Нач(M) и Нач(N) не  пересекаются,

написать процедуру, корректную для K.

 

     Решение. Схема процедуры такова:

 

     procedure ReadK;

     begin

     | if (Next принадлежит Нач(L)) then begin

     | | ReadL;

     | end else if (Next принадлежит Нач(M)) then begin

     | | ReadM;

     | end else if (Next принадлежит Нач(N)) then begin

     | | ReadN;

     | end else begin

     | | b := true или false  в зависимости от того,

     | |      выводимо ли пустое слово из K или нет

     | end;

     end;

 

Докажем, что ReadK корректно реализует K. Если Next не принадле-

жит ни одному из множеств Нач(L), Нач(M), Нач(N),то пустое слово

является наибольшим началом входа,  являющимся  K-началом.  Если

Next  принадлежит  одному  (и,  следовательно, только одному) из

этих множеств, то максимальное начало входа, являющееся  K-нача-

лом, непусто и читается соответствующей процедурой.

 

     13.2.5. Используя сказанное, составьте процедуру  распозна-

вания  выражений для грамматики (уже рассматривавшейся в примере

3):

 

    <выр>     -> <слаг> <оствыр>

    <оствыр>  -> + <выр>

    <оствыр>  ->

    <слаг>    -> <множ> <остслаг>

    <остслаг> -> * <слаг>

    <остслаг> ->

    <множ>    -> x

    <множ>    -> ( <выр> )

 

     Решение. Эта грамматика не полностью подпадает под рассмот-

ренные частные случаи: в правых частях есть комбинации  термина-

лов и нетерминалов

        + <выр>

и группы из трех символов

        ( <выр> )

В  грамматике есть также несколько правил с одной левой частью и

с правыми частями разного рода, например

    <оствыр>  -> + <выр>

    <оствыр>  ->

Эти ограничения не являются принципиальными. Так, правило типа

        K -> L M N

можно  было бы заменить на два правила K -> LQ и Q -> MN, терми-

нальные символы в правой части - на нетерминалы    едиственным

правилом  замены на соответствующие терминалы). Несколько правил

с одной левой частью и разнородными правыми также можно свести к

уже разобранному случаю: например,

 

        K -> L M N

        K -> P Q

        K ->

 

можно заменить на правила

 

        K  -> K1

        K  -> K2

        K  -> K3

        K1 -> L M N

        K2 -> P Q

        K3 ->

 

Но  мы  не будем этого делать - а сразу же запишем то, что полу-

чится, если подставить описания процедур для новых  терминальных

символов в места их использования. Например, для правила

        K -> L M N

это дает процедуру

 

        procedure ReadK;

        begin

        | ReadL;

        | if b then begin ReadM; end;

        | if b then begin ReadN; end;

        end;

 

Для  ее  корректности  надо,  чтобы  Посл(L)  не  пересекалось с

Нач(MN) (которое равно Нач(M), если из  M  не  выводится  пустое

слово,  и  равно объединению Нач(M) и Нач(N), если выводится), а

также чтобы Посл(M) не пересекалось с Нач(N).

     Аналогичным образом правила

        K -> L M N

        K -> P Q

        K ->

приводят к процедуре

 

        procedure ReadK;

        begin

        | if (Next принадлежит Нач(LMN)) then begin

        | | ReadB;

        | | if b then begin ReadM; end;

        | | if b then begin ReadN; end;

        | end else if (Next принадлежит Нач(PQ)) then begin

        | | ReadP;

        | | if b then begin ReadQ; end;

        | end else begin

        | | b := true;

        | end;

        end;

Читая  приведенную  далее  программу, полезно иметь в виду соот-

ветствие между русскими и английскими словами:

 

        ВЫРажение               EXPRession

        ОСТаток ВЫРажения       REST of EXPRession

        СЛАГаемое               ADDitive term

        ОСТаток СЛАГаемого      REST of ADDitive term

        МНОЖитель               MULTiplier

 

     procedure ReadSymb (c: Symbol);

     | b := (Next = c);

     | if b then begin Move; end;

     end;

 

     procedure ReadExpr;

     | ReadAdd;

     | if b then begin ReadRestExpr; end;

     end;

 

     procedure ReadRestExpr;

     | if Next = '+' then begin

     | | ReadSymb ('+');

     | | if b then begin ReadExpr; end;

     | end else begin

     | | b := true;

     | end;

     end;

 

     procedure ReadAdd;

     | ReadMult;

     | if b then begin ReadRestAdd; end;

     end;

 

     procedure ReadRestAdd;

     | if Next = '*' then begin

     | | ReadSymb ('*');

     | | if b then begin ReadAdd; end;

     | end else begin

     | | b := true;

     | end;

     end;

 

     procedure ReadMult;

     | if Next = 'x' then begin

     | | ReadSymb ('x');

     | end else if Next = '(' then begin

     | | ReadSymb ('(');

     | | if b then begin ReadExpr; end;

     | | if b then begin ReadSymb (')'); end;

     | end else begin

     | | b := false;

     | end;

     end;

 

Осталось  обсудить проблемы, связанные с взаимной рекурсивностью

этих процедур (одна использует другую и наоборот). В паскале это

допускается, только требуется дать предварительное описание про-

цедур  ("forward").  Как всегда для рекурсивных процедур, помимо

доказательства того, что каждая процедура работает  правильно  в

предположении,  что  используемые в ней вызовы процедур работают

правильно, надо доказать отдельно, что работа завершается.  (Это

не  очевидно: если бы в грамматике было правило K -> KK, то из K

ничего не выводится, Посл(K) и Нач(K) пусты,  но  написанная  по

нашим канонам процедура

 

     procedure ReadK;

     begin

     | ReadK;

     | if b then begin

     | | ReadK;

     | end;

     end;

 

не заканчивает работы.)

     В   даннном  случае  процедуры  ReadRestExpr,  ReadRestAdd,

ReadMult либо завершаются, либо  уменьшают  длину  непрочитанной

части  входа. Поскольку любой цикл вызовов включает одну из них,

то зацикливание невозможно. Задача решена.

 

     13.2.6. Пусть в грамматике имеются два правила  с  нетерми-

иналом K в левой части, имеющих вид

        K -> LK

        K ->

по   которым  K-слово  представляет  собой  конечную  последова-

тельность L-слов, причем множества Посл(L) и  Нач(K)    данном

случае  равное Нач(L)) не пересекаются. Используя корректную для

L процедуру ReadL, написать корректную для K процедуру ReadK, не

используя рекурсии. Предполагается, что пустое слово не выводимо

из L.

 

     Решение. По нашим правилам следовало бы написать

 

     procedure ReadK;

     begin

     | if (Next принадлежит Нач (L)) then begin

     | | ReadL;

     | | if b then begin ReadK; end;

     | end else begin

     | | b := true;

     | end;

     end;

 

завершение работы гарантируется тем, что пустое слово не выводи-

мо из L (и, следовательно, перед рекурсивным вызовом длина  неп-

рочитанной части уменьшается).

     Эта рекурсивная процедура эквивалентна нерекурсивной:

 

     procedure ReadK;

     begin

     | b := true;

     | while b and (Next принадлежит Нач (L)) do begin

     | | ReadL;

     | end;

     end;

 

Формально можно проверить эту эквивалентность так. Завершаемость

в обоих случаях ясна. Достаточно проверить поэтому, что тело ре-

курсивной  процедуры эквивалентно нерекурсивной в предположении,

что ее рекурсивный вызов эквивалентен вызову нерекурсивной  про-

цедуры. Подставим:

 

     if (Next принадлежит Нач (K)) then begin

     | ReadL;

     | if b then begin

     | | b := true;

     | | while b and (Next принадлежит Нач (L)) do begin

     | | | ReadL;

     | | end;

     | end;

     end else begin

     | b := true;

     end;

 

Первую команду b := true можно выкинуть (в этом месте  и  так  b

истинно). Вторую команду можно перенести в начало:

 

     b := true;

     if (Next принадлежит Нач (K)) then begin

     | ReadL;

     | if b then begin

     | | while b and (Next принадлежит Нач (L)) do begin

     | | | ReadL;

     | | end;

     | end;

     end;

 

Теперь  внутренний  if  можно выкинуть (если b ложно, цикл while

все равно не выполняется) и добавить в условие внешнего if усло-

вие b (которое все равно истинно).

 

     b := true;

     if b and (Next принадлежит Нач (L)) then begin

     | ReadL;

     | while b and (Next принадлежит Нач (A)) do begin

     | | ReadL;

     | end;

     end;

 

что эквивалентно приведенной выше  нерекурсивной  процедуре  (из

которой вынесена первая итерация цикла).

 

     13.2.7.  Доказать корректность приведенной выше нерекурсив-

ной программы непосредственно, без ссылок на рекурсивную.

 

     Решение. Рассмотрим  наибольшее  начало  входа,  являющееся

K-началом.  Оно  представляется  в виде конкатенации (последова-

тельного приписывания) нескольких непустых L-слов  и,  возможно,

одного  непустого  L-начала,  не являющегося L-словом. Инвариант

цикла: прочитано несколько из них; b <=> (последнее  прочитанное

является L-словом).

     Сохранение  инварианта:  если осталось последнее слово, это

очевидно; если осталось несколько, то  за  первым  B-словом  (из

числа  оставшихся)  идет  символ из Нач(B), и потому это слово -

максимальным началом входа, являющееся B-началом.

 

     На  практике  при  записи грамматики используют сокращения.

Если правила для какого-то нетерминала K имеют вид

     K -> L K

     K ->

(т.е. K-слова - это последовательности L-слов), то  этих  правил

не  пишут, а вместо K пишут L в фигурных скобках. Несколько пра-

вил с одной левой частью и разными правыми записывают  как  одно

правило,  разделяя альтернативные правые части вертикальной чер-

той.

     Например, рассмотренная выше  грамматика  для  <выр>  может

быть записана так:

 

    <выр>     -> <слаг> { + <слаг> }

    <слаг>    -> <множ> { * <множ> }

    <множ>    -> x | ( <выр> )

 

     13.2.8. Написать процедуру,  корректно  для  <выр>,  следуя

этой  грамматике  и используя цикл вместо рекурсии, где можно.

 

     Решение.

 

     procedure ReadSymb (c: Symbol);

     | b := (Next = c);

     | if b then begin Move; end;

     end;

 

     procedure ReadExpr;

     begin

     | ReadAdd;

     | while b and (Next = '+') do begin

     | | Move;

     | | ReadAdd;

     | end;

     end;

 

     procedure ReadAdd;

     begin

     | ReadMult;

     | while b and (Next = '*') do begin

     | | Move;

     | | ReadMult;

     | end;

     end;

 

     procedure ReadMult;

     begin

     | if Next = 'x' do begin

     | | Move;

     | end else if Next = '(' then begin

     | | Move;

     | | ReadExpr;

     | | if b then begin ReadSymb (')'); end;

     | end else begin

     | | b := false;

     | end;

     end;

 

     13.3. Алгоритм разбора для LL(1)-грамматик.

 

     В этом разделе мы рассморим еще один метод проверки выводи-

мости в КС-грамматике, называемый  по  традиции  LL(1)-разбором.

Вот его идея в одной фразе: можно считать, что в процессе вывода

мы  всегда  заменяем самый левый нетерминал и нужно лишь выбрать

одно из правил; если нам повезет с грамматикой, то выбрать  пра-

вило  можно, глядя на первый символ выводимого из этого нетерми-

нала слова. Говоря более формально, дадим такое

     Определение. Левым выводом (слова в грамматике)  называется

вывод,  в котором на каждом шаге замене подвергается самый левый

из нетерминалов.

 

     13.3.1.  Для  каждого  выводимого слова (из терминалов) су-

ществует его левый вывод.

 

     Решение. Различные нетерминалы заменяются независимо;  если

в  процессе вывода появилось слово ..K..L.., где K, L - нетерми-

налы, то замены K и L можно производить в любом порядке. Поэтому

можно перестроить вывод так, чтобы стоящий левее нетерминал  за-

менялся  раньше. (Формально говоря, надо доказывать индукцией по

длине вывода такой факт: если из некоторого нетерминала K  выво-

дится некоторое

слово A, то существует левый вывод A из K.)

 

     13.3.2. В грамматике с 4 правилами

 

        (1) E ->

        (2) E -> T E

        (3) T -> ( E )

        (4) T -> [ E ]

 

найти  левый  вывод  слова  A  =  [()([])]  и  доказать,  что он

единствен.

 

     Решение. На первом шаге можно применить только правило (2):

        E -> TE

Что будет дальше с T? Так как слово A начинается на "[", то  мо-

жет примениться только правило (4):

        E -> TE -> [E]E

Первое  E должно замениться на TE (иначе вторым символом была бы

скобка "]"):

        E -> TE -> [E]E -> [TE]E

и T должно заменяться по (3):

        E -> TE -> [E]E -> [TE]E -> [(E)E]E

Далее первое E должно замениться на пустое слово (иначе  третьей

буквой слова будет "(" или "[" - только на эти символы может на-

чинаться слово, выводимое из T):

        E -> TE -> [E]E -> [TE]E -> [(E)E]E -> [()E]E

и далее

  ...  ->  [()TE]E -> [()(E)E]E -> [()(TE)E]E -> [()([E]E)E]E ->

        -> [()([]E)E]E -> [()([])E]E -> [()([])]E -> [()([])].

 

     Что требуется от грамматики, чтобы такой метод поиска лево-

го вывода был применим? Пусть, например, на очередном шаге самым

левым  нетерминалом  оказался  нетерминал K, т.е. мы имеем слово

вида AKU, где A - слово из терминалов, а U - слово из терминалов

и нетерминалов. Пусть в грамматике есть правила

     K -> L M N

     K -> P Q

     K -> R

Нам надо выбрать одно из них. Мы будем пытаться сделать этот вы-

бор,  глядя  на  первый символ той части входного слова, которая

выводится из KU.

     Рассмотрим множество Нач(LMN) тех терминалов, с которых на-

чинаются непустые слова, выводимые из LMN. (Это множество  равно

Нач(L),  объединенному с Нач(M), если из L выводится пустое сло-

во, а также с Нач(N), если из L и из M выводится пустое  слово.)

Чтобы  описанный  метод  был  применим,  надо,  чтобы  Нач(LMN),

Нач(PQ) и Нач(R) не пересекались. Но этого мало. Ведь может быть

так, например, что из LMN будет выведено пустое слово, а из сло-

ва U будет выведено слово, начинающееся  на  букву  из  Нач(PQ).

Следующие определения учитывают эту проблему.

 

     Напомним,  что определение выводимости в КС-грамматике было

дано только для слова из терминалов. Оно очевидным образом обоб-

щается на случай слов из терминалов и нетерминалов. Можно  также

говорить о выводимости одного слова (содержащего терминалы и не-

терминалы)  из  другого. (Если говорится о выводимости слова без

указания того, откуда оно выводится, то  всегда  подразумевается

выводимость  в грамматике, т.е. выводимость из начального нетер-

минала.)

     Для каждого слова X  из  терминалов  и  нетерминалов  через

Нач(X) обозначаем множество всех терминалов, с которых начинают-

ся непустые слова из терминалов, выводимые из X. (В случае, если

из  любого  нетерминала выводится хоть одно слово из терминалов,

не играет роли, рассматриваем ли мы при определении Нач(X) слова

только из терминалов или любые слова. Мы будем предполагать  да-

лее, что это условие выполнено.)

    Для каждого нетерминала K  через  Послед(K)  обозначим  мно-

жество  терминалов, которые встречаются в выводимых словах сразу

же за K. Кроме того, в Послед(K) включается символ EOI, если су-

ществует выводимое слово, оканчивающееся на K.

     Для каждого правила

        K -> V

(где  K - нетерминал, V - слово, содержащее терминалы и нетерми-

налы) определим множество "направляющих  терминалов",  обознача-

емое Напр(K->V). По определению оно равно Нач(V), к которому до-

бавлено Послед(K), если из V выводится пустое слово.

 

     Определение.  Грамматика называется LL(1)-грамматикой, если

для любых правил K->V и K->W с одинаковыми левыми  частями  мно-

жества Напр(K->V) и Напр(K->W) не пересекаются.

 

     13.3.3. Является ли грамматика

          K -> K #

          K ->

(выводимыми   словами   являются   последовательности    диезов)

LL(1)-грамматикой?

 

     Решение. Нет: символ # принадлежит множествам  направляющих

символов для обоих правил (для второго - поскольку # принадлежит

Послед(K)).

 

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

 

     Решение.

          K -> # K

          K ->

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

сивное".

 

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

ствует не более одного возможного продолжения левого вывода.

 

     13.3.5. Пусть дано выводимое в LL(1)-грамматике слово X,  в

котором  выделен  самый левый нетерминал К: X=AKS, где A - слово

из терминалов, S - слово из терминалов и нетерминалов. Пусть су-

ществуют два различных правила грамматики с нетерминалом K в ле-

вой части, и мы применили их к выделенному в  X  нетерминалу  K,

затем  продолжили  вывод  и в конце концов получили два слова из

терминалов, начинающихся на A. Доказать, что в  этих  словах  за

началом A идут разные буквы.

 

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

личных правил.

 

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

ке, то его левый вывод единствен.

 

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

левый вывод продолжается однозначно.

 

     13.3.7.  Грамматика называется леворекурсивной, если из не-

которого нетерминала K выводится слово, начинающееся с K, но  не

совпадающее  с  ним. Доказать, что леворекурсивная грамматика, в

которой из каждого нетерминала выводится хотя бы  одно  непустое

слово  из  терминалов и для каждого нетерминала существует вывод

(начинающийся с начального нетерминала), в котором он встречает-

ся, не является LL(1)-грамматикой.

 

     Решение. Пусть из K выводится KU, где K - нетерминал, а U -

непустое слово. Можно считать, что это левый вывод  (другие  не-

терминалы  можно не заменять). Рассмотрим вывод K --> KU --> KUU

->... (знак --> обозначает несколько шагов вывода) и левый вывод

K -> A, где A - непустое слово из терминалов. На  каком-то  шаге

второй  вывод отклоняется от первого, а между тем по обоим путям

может быть получено слово, начинающееся на A    первом  случае

это  возможно,  так  как сохраняется нетерминал K, который может

впоследствии быть заменен на A).  Это  противоречит  возможности

однозначного определения правила, применяемого на очередном шаге

поиска  левого вывода. (Oднозначность выполняется для выводов из

начального нетерманала, и надо воспользоваться  тем,  что  K  по

предположению встречается в таком выводе.)

 

     Таким образом, к леворекурсивным грамматикам (кроме  триви-

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

зовывать к эквивалентным LL(1)-грамматикам  -  или  пользоваться

другими методами распознавания.

 

     13.3.8.  Используя  сказанное,  построить алгоритм проверки

выводимости слова из терминалов в LL(1)-грамматике, не являющей-

ся леворекурсивной.

 

     Решение.  Мы  следуем  описанному выше методу поиска левого

вывода, храня лишь часть слова, находящуюся правее уже прочитан-

ной части входного слова. Другими словами, мы храним слово S  из

терминалов и нетерминалов, обладающее таким свойством (прочитан-

ную часть входа обозначаем через A):

 

    | (1) слово AS выводимо в грамматике;

(И) | (2) любой левый вывод входного слова проходит через стадию

    |     AS

 

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

чального нетерминала.

     Если  в  некоторый  момент S начинается на терминал t и t =

Next, то можно выполнить команду Move и удалить символ t,  явля-

ющийся начальным в S, поскольку при этом AS не меняется.

     Если S начинается на терминал t и t не равно Next, то вход-

ное  слово  невыводимо  -  ибо по условию любой его вывод должен

проходить через AS. (Это же справедливо и в случае Next = EOI.)

     Если S пусто, то из условия (И) следует, что входное  слово

выводимо тогда и только тогда, когда Next = EOI.

     Остается случай, когда S начинается с некоторого нетермина-

ла K. По доказанному выше все левые выводы из  S  слов,  начина-

ющихся на символ Next, начинаются с применения к T одного и того

же  правила  - того, для которого 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;

   end;

   {входное слово выводимо <=> not error}

 

Алгоритм заканчивает работу, поскольку при появлении нетерминала

в начале слова S происходит чтение со  входа  или  остановка,  а

бесконечный  цикл сменяющих друг друга терминалов в начале S оз-

начал бы, что грамматика леворекурсивна.

 

     Замечания.  1.  Приведенный  алгоритм использует S как стек

(все действия производятся с левого конца).

     2. Действия двух последних вариантов внутри цикла не приво-

дят к чтению очередного символа со входа, поэтому их можно зара-

нее предвычислить для  каждого  нетерминала  и  каждого  символа

Next.  После этого на каждом шаге цикла будет читаться очередной

символ входа.

     3. При практической реализации удобно составить таблицу,  в

которой  записаны  варианты  действий  в зависимости от входного

символа и первого символа S, и небольшую программу,  выполняющую

действия в соответствии с этой таблицей.

Hosted by uCoz