Будь умным!


У вас вопросы?
У нас ответы:) SamZan.net

Способы задания языков Цепочки символов

Работа добавлена на сайт samzan.net: 2015-07-05

Поможем написать учебную работу

Если у вас возникли сложности с курсовой, контрольной, дипломной, рефератом, отчетом по практике, научно-исследовательской и любой другой работой - мы готовы помочь.

Предоплата всего

от 25%

Подписываем

договор

Выберите тип работы:

Скидка 25% при заказе до 21.5.2024

PAGE  30

  1.  Формальные языки и грамматики
    1.  Языки и цепочки символов. Способы задания языков
      1.  Цепочки символов. Операции над цепочками символов

Цепочкой символов (или строкой ) называют произвольную последовательность символов, записанных один за другим. Понятие символа (или буквы) является базовым в теории формальных языков и не нуждается в определении.

Далее цепочки символов будем обозначать греческими буквами: α, β, γ

Итак, цепочка — это последовательность, в которую могут входить любые допустимые символы. Строка, которую вы сейчас читаете, является примером цепочки, допустимые символы в которой — строчные и заглавные русские буквы, знаки препинания и символ пробела. Но цепочка — это необязательно некоторая осмысленная последовательность символов. Последовательность «аввв.-аагрьь ,.лл» — тоже пример цепочки символов.

Для цепочки символов важен состав и количество символов в ней, а также порядок символов в цепочке. Один и тот же символ может произвольное число раз входить в цепочку. Поэтому цепочки «а» и «аа», а также «аб» и «ба» — это различные цепочки символов. Цепочки символов  α  и  β  равны (совпадают), α  =  β, если они имеют один и тот же состав символов, одно и то же их количество и одинаковый порядок следования символов в цепочке.

Количество символов в цепочке называют длиной цепочки. Длина цепочки символов α   обозначается как | α |. Очевидно, что если α  = β. то и |α|  = |β|.

Основной операцией над цепочками символов является операция конкатенации (объединения или сложения) цепочек.

Конкатенация (сложение, объединение) двух цепочек символов — это дописывание второй цепочки в конец первой. Конкатенация цепочек α  и  β  обозначается как αβ  . Выполнить конкатенацию цепочек просто: например, если α  = «аб», а β  = «вг», то αβ  = «абвг».

Так как в цепочке важен порядок символов, то очевидно, что операция конкатенации не обладает свойством коммутативности, то есть в общем случае α  и  β  такие, что αββα. Также очевидно, что конкатенация обладает свойством ассоциативности, то есть (αβ)γ=α(βγ).

Можно выделить еще две операции над цепочками.

Обращение цепочки — это запись символов цепочки в обратном порядке. Обращение цепочки  α обозначается как α R. Если α = «абвг», то α R  = «гвба». Для операции обращения справедливо следующее равенство  R = R  R.

Итерация (повторение) цепочки n раз, где n  N, n > 0 — это конкатенация цепочки самой с собой n раз. Итерация цепочки n раз обозначается как n.  Для операции повторения справедливы следующие равенства : 1 = , 2 = , 3 = , ... и т. д.

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

Для пустой цепочки справедливы следующие равенства:

  1.  || = 0
  2.  : =   = 
  3.  R=
  4.   n>=0: n=
  5.  : 0 =   
    1.  Понятие языка. Формальное определение языка

В общем случае язык — это заданный набор символов и правил, устанавливающих способы комбинации этих символов между собой для записи осмысленных текстов. Основой любого естественного или искусственного языка является алфавит, определяющий набор допустимых символов языка.

Алфавит — это счетное множество допустимых символов языка. Будем обозначать это множество символом V. Интересно, что согласно формальному определению, алфавит не обязательно должен быть конечным (перечислимым) множеством, но реально все существующие языки строятся на основе конечных алфавитов.

Цепочка символов является цепочкой над алфавитом V: (V), если в нее входят только символы, принадлежащие множеству символов V. Для любого алфавита V пустая цепочка  может как являться, так и не являться цепочкой (V). Это условие оговаривается дополнительно.

Если V — некоторый алфавит, то:

  •  V+ — множество всех цепочек над алфавитом V без ;
  •  V* — множество всех цепочек над алфавитом V, включая .

Справедливо равенство: V" = V+  {}.

Языком L над алфавитом V: L(V) называется некоторое счетное подмножество цепочек конечной длины из множества всех цепочек над алфавитом V. Из этого определения следуют два вывода: во-первых, множество цепочек языка не обязано быть конечным; во-вторых, хотя каждая цепочка символов, входящая в язык, обязана иметь конечную длину, эта длина может быть сколь угодно большой и формально ничем не ограничена.

Все существующие языки подпадают под это определение. Большинство реальных естественных и искусственных языков содержат бесконечное множество цепочек. Также в большинстве языков длина цепочки ничем не ограничена (например, этот длинный текст — пример цепочки символов русского языка). Цепочку символов, принадлежащую заданному языку, часто называют предложением языка, а множество цепочек символов некоторого языка L(V) — множеством предложений этого языка.

Для любого языка L(V) справедливо: L(V) V*.

Язык L(V) включает в себя язык L'(V): L'(V) L(V), если  L(V): L'(V). Множество цепочек языка L'(V) является подмножеством множества цепочек языка L(V) (или эти множества совпадают). Очевидно, что оба языка должны строится на основе одного и того же алфавита.

Два языка L(V) и L'(V) совпадают (эквивалентны): L'(V) = L(V), если L'(V) L(V) и L(V) L'(V); или, что то же самое:  L'(V): L(V) и  L(V): L’(V). Множества допустимых цепочек символов для эквивалентных языков должны быть равны.

Два языка L(V) и L'(V) почти эквивалентны: L'(V)  L(V), если L'(V){}= L(V){}. Множества допустимых цепочек символов почти эквивалентных языков могут различаться только на пустую цепочку символов.

  1.  Способы задания языков

Итак, каждый язык — это множество цепочек символов над некоторым алфавитом. Но кроме алфавита язык предусматривает и задание правил построения допустимых цепочек, поскольку обычно далеко не все цепочки над заданным алфавитом принадлежат языку. Символы могут объединяться в слова или лексемы — элементарные конструкции языка, на их основе строятся предложения — более сложные конструкции. И те и другие в общем виде являются цепочками символов, но предусматривают некоторые правила построения. Таким образом, необходимо указать эти правила, или, строго говоря, задать язык.

Язык задать можно тремя способами:

1. Перечислением всех допустимых цепочек языка.

2. Указанием способа порождения цепочек языка (заданием грамматики языка).

3. Определением метода распознавания цепочек языка.

Первый из методов является чисто формальным и на практике не применяется, так как большинство языков содержат бесконечное число допустимых цепочек и перечислить их просто невозможно. Трудно себе представить, чтобы появилась возможность перечислить, например, множество всех правильных текстов на русском языке или всех правильных программ на языке Pascal. Иногда, для чисто формальных языков, можно перечислить множество входящих в них цепочек, прибегнув к математическим определениям множеств. Однако этот метод уже стоит ближе ко второму способу.

Например, запись L({0,1}) - {0n1n, n > 0} задает язык над алфавитом V = {0,1}, содержащий все последовательности с чередующимися символами 0 и 1, начинающиеся с 0 и заканчивающиеся 1. Видно, что пустая цепочка символов в этот язык не входит. Если изменить условие в этом определении с n > 0 на n>=0, то получим почти эквивалентный язык L'({0,1}), содержащий пустую цепочку.

Второй способ предусматривает некоторое описание правил, с помощью которых строятся цепочки языка. Тогда любая цепочка, построенная с помощью этих правил из символов алфавита языка, будет принадлежать заданному языку. Например, с правилами построения цепочек символов русского языка вы долго и упорно знакомились в средней школе.

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

  1.  Синтаксис и семантика языка

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

Синтаксис языка — это набор правил, определяющий допустимые конструкции языка. Синтаксис определяет «форму языка» — задает набор цепочек символов, которые принадлежат языку. Чаще всего синтаксис языка можно задать в виде строгого набора правил, но полностью это утверждение справедливо только для чисто формальных языков. Даже для большинства языков программирования набор заданных синтаксических конструкций нуждается в дополнительных пояснениях, а синтаксис языков естественного общения вполне соответствует общепринятому мнению о том, что «исключения только подтверждают правило».

Например, любой окончивший среднюю школу может сказать, что строка «3+2» является арифметическим выражением, а «З 2 +» — не является. Правда, не каждый задумается при этом, что он оперирует синтаксисом алгебры.

Семантика языка — это раздел языка, определяющий значение предложений языка. Семантика определяет «содержание языка» — задает значение для всех допустимых цепочек языка. Семантика для большинства языков определяется неформальными методами (отношения между знаками и тем, что они обозначают, изучаются семиотикой). Чисто формальные языки лишены какого-либо смысла. Возвращаясь к примеру, приведенному выше, и используя семантику алгебры, мы можем сказать, что строка «З + 2» есть сумма чисел 3 и 2, а также то, что «3 + 2 = 5» — это истинное выражение. Однако изложить любому ученику синтаксис алгебры гораздо проще, чем ее семантику, хотя в данном случае семантику как раз можно определить формально.

Лексика — это совокупность слов (словарный запас) языка. Слово или лексическая единица (лексема) языка — это конструкция, которая состоит из элементов алфавита языка и не содержит в себе других конструкций. Иначе говоря, лексическая единица может содержать только элементарные символы и не может содержать других лексических единиц.

Лексическими единицами (лексемами) русского языка являются слова русского языка, а знаки препинания и пробелы представляют собой разделители, не образующие лексем. Лексическими единицами алгебры являются числа, знаки математических операций, обозначения функций и неизвестных величин. В языках программирования лексическими единицами являются ключевые слова, идентификаторы, константы, метки, знаки операций; в них также существуют и разделители (запятые, скобки, точки с запятой и т. д.).

  1.  Особенности языков программирования

Языки программирования занимают некоторое промежуточное положение между формальными и естественными языками. С формальными языками их объединяют строгие синтаксические правила, на основе которых строятся предложения языка. От языков естественного общения в языки программирования перешли лексические единицы, представляющие основные ключевые слова (чаще всего это слова английского языка, но существуют языки программирования, чьи ключевые слова заимствованы из русского и других языков). Кроме того, из алгебры языки программирования переняли основные обозначения математических операций, что также делает их более понятными человеку.

Для задания языка программирования необходимо решить три вопроса:

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

Только первые два вопроса полностью или частично удается решить с помощью теории формальных языков.

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

Второй вопрос решается в теории формальных языков только частично. Для всех языков программирования существуют правила, определяющие синтаксис языка, но как уже было сказано, их недостаточно для того, чтобы строго определить все возможные синтаксические конструкции. Дополнительные ограничения накладываются семантикой языка. Эти ограничения оговариваются в неформальной форме для каждого отдельного языка программирования. К таким ограничениям можно отнести необходимость предварительного описания переменных и функций, необходимость соответствия типов переменных и констант в выражениях, формальных и фактических параметров в вызовах функций и др.

Отсюда следует, что практически все языки программирования, строго говоря, не являются формальными языками. И именно поэтому во всех трансляторах, кроме синтаксического разбора и анализа предложений языка, дополнительно предусмотрен семантический анализ.

Третий вопрос в принципе не относится к теории формальных языков, поскольку, как уже было сказано, такие языки лишены какого-либо смысла. Для ответа на него нужно использовать другие подходы. В качестве таких подходов можно указать следующие:

  •  изложить смысл программы, написанной на языке программирования, на другом языке, более понятном тому, кому адресована программа;
  •  использовать для проверки смысла некоторую «идеальную машину», которая предназначена для выполнения программ, написанных на данном языке.

Все, кто писал программы, так или иначе обязательно использовали первый подход. Комментарии в хорошей программе — это и есть изложение ее смысла. Построение блок-схемы, а равно любое другое описание алгоритма программы — это тоже способ изложить смысл программы на другом языке (например, языке графических символов блок-схем алгоритмов, смысл которого, в свою очередь, изложен в соответствующем ГОСТе). Да и документация к программе — тоже способ изложения ее смысла. Но все эти способы ориентированы на человека, которому они, конечно же, более понятны. Однако не существует пока универсального способа проверить, насколько описание соответствует программе.

Машина же понимает только один язык — язык машинных команд. Но изложить программу на языке машинных команд — задача слишком трудоемкая для человека, как раз для ее решения и создаются трансляторы.

Второй подход используется при отладке программы. Оценку же результатов выполнения программы при отладке выполняет человек. Любые попытки поручить это дело машине лишены смысла вне контекста решаемой задачи.

Например, предложение в программе на языке Pascal вида: i:=0; while i=0 do i:=0; может быть легко оценено любой машиной как бессмысленное. Но если в задачу входит обеспечить взаимодействие с другой параллельно выполняемой программой или, например, просто проверить надежность и долговечность процессора или какой-то ячейки памяти, то это же предложение покажется уже не лишенным смысла.

Некоторые успехи в процессе проверки осмысленности программ достигнуты в рамках систем автоматизации разработки программного обеспечения (CASE-системы). Их подход основан на проектировании сверху вниз — от описания задачи на языке, понятном человеку, до перевода ее в операторы языка программирования. Но такой подход выходит за рамки возможностей трансляторов, поэтому здесь рассматриваться не будет.

Однако разработчикам компиляторов, так или иначе, приходится решать вопрос о смысле программ. Во-первых, компилятор должен все-таки преобразовать исходную программу в последовательность машинных команд, а для этого ему необходимо иметь представление о том, какая последовательность команд соответствует той или иной части исходной программы. Обычно такие последовательности сопоставляются базовым конструкциям входного языка (далее будет рассмотрено, как это можно сделать). Здесь используется первый подход к изложению смысла программы. Во-вторых, многие современные компиляторы позволяют выявить сомнительные с точки зрения смысла места в исходной программе — такие, как недостижимые операторы, неиспользуемые переменные, неопределенные результаты функций и т. п. Обычно компилятор указывает такие места в виде дополнительных предупреждений, которые разработчик может принимать или не принимать во внимание. Для достижения этой цели компилятор должен иметь представление о том, как программа будет выполняться — используется второй подход. Но в обоих случаях осмысление исходной программы закладывает в компилятор его создатель (или коллектив создателей) — то есть человек, который руководствуется неформальными методами (чаще всего, описанием входного языка). В теории формальных языков вопрос о смысле программ не решается.

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

  1.  Определение грамматики. Форма Бэкуса—Наура
    1.  Понятие о грамматике языка

Грамматика — это описание способа построения предложений некоторого языка. Иными словами, грамматика — это математическая система, определяющая язык.

Фактически, определив грамматику языка, мы указываем правила порождения цепочек символов, принадлежащих этому языку. Таким образом, грамматика — это генератор цепочек языка. Она относится ко второму способу определения языков — порождению цепочек символов.

Грамматику языка можно описать различными способами; например, грамматика русского языка описывается довольно сложным набором правил, которые изучают в начальной школе. Но для многих языков (и для синтаксической части языков программирования в том числе) допустимо использовать формальное описание грамматики, построенное на основе системы правил (или продукций).

Правило (или продукция) — это упорядоченная пара цепочек символов ( ). В правилах очень важен порядок цепочек, поэтому их чаще записывают в виде  (или =). Такая запись читается как « порождает » или « по определению есть ».

Грамматика языка программирования содержит правила двух типов: первые (определяющие синтаксические конструкции языка) довольно легко поддаются формальному описанию; вторые (определяющие семантические ограничения языка) обычно излагаются в неформальной форме. Поэтому любое описание (или общепринятый стандарт) языка программирования обычно состоит из двух частей: вначале формально излагаются правила построения синтаксических конструкций, а потом на естественном языке дается описание семантических правил. Естественный язык понятен человеку, пользователю, который будет писать программы на языке программирования; для компилятора же семантические ограничения необходимо излагать в виде алгоритмов проверки правильности программы (речь, как уже было сказано выше, не идет о смысле программ, а только лишь о семантических ограничениях на исходный текст). Такой проверкой в компиляторе занимается семантический анализатор — специально для этого разработанная часть компилятора.

Далее, говоря о грамматиках языков программирования, будем иметь в виду только правила построения синтаксических конструкций языка. Однако следует помнить, что грамматика любого языка программирования в общем случае не ограничивается только этими правилами.

Язык, заданный грамматикой G, обозначается как L(G).

Две грамматики G и G' называются эквивалентными, если они определяют один и тот же язык: L(G) = L(G'). Две грамматики G и G' называются почти эквивалентными, если заданные ими языки различаются не более чем на пустую цепочку символов: L(G) {} = L(G') {}.

  1.  Формальное определение грамматики. Форма Бэкуса—Наура

Для полного формального определения грамматики кроме правил порождения цепочек языка необходимо задать также алфавит языка.

Формально грамматика G определяется как четверка G(VT,VN,P,S), где:

  •  VT — множество терминальных символов;
  •  VN — множество нетерминальных символов: VN  VT = ;
  •  P — множество правил (продукций) грамматики вида , где V+, V*;
  •  S — целевой (начальный) символ грамматики SVN.

Множество V = VN  VT называют полным алфавитом грамматики G.

Рассмотрим множества VN и VT. Множество терминальных символов VT содержит символы, которые входят в алфавит языка, порождаемого грамматикой. Как правило, символы из множества VT встречаются только в цепочках правых частей правил, если же они встречаются в цепочке левой части правила, то обязаны быть и в цепочке правой его части. Множество нетерминальных символов VN содержит символы, которые определяют слова, понятия, конструкции языка. Каждый символ этого множества может встречаться в цепочках как левой, так и правой частей правил грамматики, но он обязан хотя бы один раз быть в левой части хотя бы одного правила. Правила грамматики строятся так, чтобы в левой части каждого правила был хотя бы один нетерминальный символ.

Эти два множества не пересекаются: каждый символ может быть либо терминальным, либо нетерминальным. Ни один символ в алфавите грамматики не может быть нетерминальным и терминальным одновременно. Целевой символ грамматики — это всегда нетерминальный символ.

Во множестве правил грамматики может быть несколько правил, имеющих одинаковые правые части, вида:   n. Тогда эти правила объединяют вместе и записывают в виде: ||n|. Одной строке в такой записи соответствует сразу n правил.

Такую форму записи правил грамматики называют формой Бэкуса—Наура. Форма Бэкуса—Наура предусматривает, как правило, также, что нетерминальные символы берутся в угловые скобки: < >. Иногда знак «—>» в правилах грамматики заменяют на знак «::=» (что характерно для старых монографий), но это всего лишь незначительные модификации формы записи, не влияющие на ее суть.

Ниже приведен пример грамматики для целых десятичных чисел со знаком:

G({0,1,2,3,4,5,6,7,8,9,-,+},{<число>,<чс>,<цифра>},Р,<число>)

Р:

<число> —> <чс> | +<чc> | -<чс>

<чс> -> <цифра> | <чс><цифра>

<цифра> ->0|1|2|3|4|5|6|7|8|9

Рассмотрим составляющие элементы грамматики G:

  •  множество терминальных символов VT содержит двенадцать элементов: десять десятичных цифр и два знака;
  •  множество нетерминальных символов VN содержит три элемента: символы <число>, <чс> и <цифра>;
  •  множество правил содержит 15 правил, которые записаны в три строки (то есть имеются только три различных правых части правил);
  •  целевым символом грамматики является символ <число>.

Следует отметить, что символ <чс> — это бессмысленное сочетание букв русского языка, но это обычный нетерминальный символ грамматики, такой же, как и два других. Названия нетерминальных символов не обязаны быть осмысленными, это сделано просто для удобства понимания правил грамматики человеком. В принципе в любой грамматике можно полностью изменить имена всех нетерминальных символов, не меняя при этом языка, заданного грамматикой, — точно также, например, в программе на языке Pascal можно изменить имена идентификаторов, и при этом не изменится смысл программы.

Для терминальных символов это неверно. Набор терминальных символов всегда строго соответствует алфавиту языка, определяемого грамматикой.

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

G'({0,1,2,3,4,5,6,7,8,9,-,+},(S,T,F},P,S)

Р:

S -> Т | +Т | -Т 

Т -> F | TF

F-> 0|1|2|3|4|5|6|7|8|9

Здесь изменилось только множество нетерминальных символов. Теперь VN = {S,T,F}. Язык, заданный грамматикой, не изменился — грамматики G и G' эквивалентны.

  1.  Принцип рекурсии в правилах грамматики

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

Возможность пользоваться конечным набором правил достигается в такой форме записи грамматики за счет рекурсивных правил. Рекурсия в правилах грамматики выражается в том, что один из нетерминальных символов определяется сам через себя. Рекурсия может быть непосредственной (явной) — тогда символ определяется сам через себя в одном правиле, либо косвенной (неявной) — тогда то же самое происходит через цепочку правил.

В рассмотренной выше грамматике G непосредственная рекурсия присутствует в правиле: <чс>-><чс><цифра>, а в эквивалентной ей грамматике G' — в правиле: T->TF.

Чтобы рекурсия не была бесконечной, для участвующего в ней нетерминального символа грамматики должны существовать также и другие правила, которые определяют его, минуя самого себя, и позволяют избежать бесконечного рекурсивного определения (в противном случае этот символ в грамматике был бы просто не нужен). Такими правилами являются <чс>-><цифра> — в грамматике G и T->F — в грамматике G'.

В теории формальных языков более ничего сказать о рекурсии нельзя. Но, чтобы полнее понять смысл рекурсии, можно прибегнуть к семантике языка — в рассмотренном выше примере это язык целых десятичных чисел со знаком. Рассмотрим его смысл.

Если попытаться дать определение тому, что же является числом, то начать можно с того, что любая цифра сама по себе есть число. Далее можно заметить, что любые две цифры — это тоже число, затем — три цифры и т. д. Если строить определение числа таким методом, то оно никогда не будет закончено (в математике разрядность числа ничем не ограничена). Однако можно заметить, что каждый раз, порождая новое число, мы просто дописываем цифру справа (поскольку привыкли писать слева направо) к уже написанному ряду цифр. А этот ряд цифр, начиная от одной цифры, тоже в свою очередь является числом. Тогда определение для понятия «число» можно построить таким образом: «число — это любая цифра, либо другое число, к которому справа дописана любая цифра». Именно это и составляет основу правил грамматик G и G' и отражено во второй строке правил в правилах <чс>-><цифра> | <чс><цифра> и T->F|TF. Другие правила в этих грамматиках позволяют добавить к числу знак (первая строка правил) и дают определение понятию «цифра» (третья строка правил). Они элементарны и не требуют пояснений.

Принцип рекурсии (иногда его называют «принцип итерации», что не меняет сути) — важное понятие в представлении о формальных грамматиках. Так или иначе, явно или неявно рекурсия всегда присутствует в грамматиках любых реальных языков программирования. Именно она позволяет строить бесконечное множество цепочек языка, и говорить об их порождении невозможно без понимания принципа рекурсии. Как правило, в грамматике реального языка программирования содержится не одно, а целое множество правил, построенных с помощью рекурсии.

  1.  Другие способы задания грамматик

Форма Бэкуса—Наура — удобный с формальной точки зрения, но не всегда доступный для понимания способ записи формальных грамматик. Рекурсивные определения хороши для формального анализа цепочек языка, но не удобны с точки зрения человека. Например, то, что правила <чс>—><цифра>|<чс><цифра> отражают возможность для построения числа дописывать справа любое число цифр, начиная от одной, неочевидно и требует дополнительного пояснения.

Но при создании языка программирования важно, чтобы его грамматику понимали не только те, кому предстоит создавать компиляторы для этого языка, но и пользователи языка — будущие разработчики программ. Поэтому существуют другие способы описания правил формальных грамматик, которые ориентированы на большую понятность человеку:

  •  запись правил грамматик с использованием метасимволов;
  •  запись правил грамматик в графическом виде.

  1.  Классификация языков и грамматик

Выше упоминались различные типы грамматик, но не было указано, как и по какому принципу они подразделяются на типы. Для человека языки бывают простые и сложные, но это субъективное мнение, которое зависит от личности человека.

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

  1.  Классификация грамматик. Четыре типа грамматик по Хомскому

Формальные грамматики классифицируются по структуре их правил. Если все без исключения правила грамматики удовлетворяют некоторой заданной структуре, то ее относят к определенному типу. Достаточно иметь в грамматике одно правило, не удовлетворяющее требованиям структуры правил, и она уже не попадает в заданный тип.

По классификации Хомского выделяют четыре типа грамматик.

  1.  Тип 0: грамматики с фразовой структурой

На структуру их правил не накладывается никаких ограничений: для грамматики вида G(VT,VN,P,S), V = VN VT правила имеют вид: ->, где V+,  V*.

Это самый общий тип грамматик. В него подпадают все без исключения формальные грамматики, но часть из них, к общей радости, может быть также отнесена и к другим классификационным типам. Дело в том, что грамматики, которые относятся только к типу 0 и не могут быть отнесены к другим типам, являются самыми сложными по структуре.

Практического применения грамматики, относящиеся только к типу 0, не имеют.

  1.  Тип 1: контекстно-зависимые (КЗ) и неукорачивающие грамматики

В этот тип входят два основных класса грамматик.

Контекстно-зависимые грамматики G(VT,VN,P,S), V = VNVT имеют правила вида: A2->2, где 12V*, A VN, V+

Неукорачивающие грамматики G(VT,VN,P,S), V = VNVT  имеют правила вида:

->, где , V+, ||>=||

Структура правил КЗ-грамматик такова, что при построении предложений заданного ими языка один и тот же нетерминальный символ может быть заменен на ту или иную цепочку символов в зависимости от того контекста, в котором он встречается. Именно поэтому эти грамматики называют «контекстно-зависимыми». Цепочки и 2  в правилах грамматики обозначают контекст ( — левый контекст, а 2 — правый контекст), в общем случае любая из них (или даже обе) может быть пустой. Говоря иными словами, значение одного и того же символа может быть различным в зависимости от того, в каком контексте он встречается.

Неукорачивающие грамматики имеют такую структуру правил, что при построении предложений языка, заданного грамматикой, любая цепочка символов может быть заменена на цепочку символов не меньшей длины. Отсюда и название «неукорачивающие».

Доказано, что эти два класса грамматик эквивалентны. Это значит, что для любого языка, заданного контекстно-зависимой грамматикой, можно построить неукорачивающую грамматику, которая будет задавать эквивалентный язык, и наоборот: для любого языка, заданного неукорачивающей грамматикой, можно построить контекстно-зависимую грамматику, которая будет задавать эквивалентный язык.

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

  1.  Тип 2: контекстно-свободные (КС) грамматики

Контекстно-свободные (КС) грамматики G(VT,VN,P,S), V = VNVT имеют правила вида: A->, где  A VN, V+.

Такие грамматики также иногда называют неукорачивающими контекстно-свободными (НКС) грамматиками (видно, что в правой части правила у них должен всегда стоять как минимум один символ).

Существует также почти эквивалентный им класс грамматик — укорачивающие контекстно-свободные (УКС) грамматики G(VT,VN,P,S), V = VNVT ,правила которых могут иметь вид: A->, где  A VN, V*.

Разница между этими двумя классами грамматик заключается лишь в том, что в УКС-грамматиках в правой части правил может присутствовать пустая цепочка (), а в НКС-грамматиках — нет. Отсюда ясно, что язык, заданный НКС-грамматикой, не может содержать пустой цепочки. Доказано, что эти два класса грамматик почти эквивалентны. В дальнейшем, когда речь будет идти о КС-грамматиках, уже не будет уточняться, какой класс грамматики (УКС или НКС) имеется в виду, если возможность наличия в языке пустой цепочки не имеет принципиального значения.

КС-грамматики широко используются при описании синтаксических конструкций языков программирования. Синтаксис большинства известных языков программирования основан именно на КС-грамматиках, поэтому в данном курсе им уделяется большое внимание.

Внутри типа КС-грамматик кроме классов НКС и УКС выделяют еще целое множество различных классов грамматик, и все они относятся к типу 2. Далее, когда КС-грамматики и КС-языки будут рассматриваться более подробно, на некоторые из этих классов грамматик и их характерные особенности будет обращено особое внимание.

  1.  Тип 3: регулярные грамматики

К типу регулярных относятся два эквивалентных класса грамматик: леволинейные и праволинейные.

Леволинейные грамматики G(VT,VN,P,S), V = VNVT могут иметь правила двух видов: А->В или А->, где A,BVN, VT*.

В свою очередь, праволинейные грамматики G(VT,VN,P,S), V = VNVT могут иметь правила тоже двух видов: А-> В или А->, где A,BVN, VT*.

Эти два класса грамматик эквивалентны и относятся к типу регулярных грамматик.

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

Типы грамматик соотносятся между собой особым образом. Из определения типов 2 и 3 видно, что любая регулярная грамматика является КС-грамматикой, но не наоборот. Также очевидно, что любая грамматика может быть отнесена и к типу 0, поскольку он не накладывает никаких ограничений на правила. В то же время существуют укорачивающие КС-грамматики (тип 2), которые не являются ни контекстно-зависимыми, ни неукорачивающими (тип 1), поскольку могут содержать правила вида «А->», недопустимые в типе 1. В целом можно сказать, что сложность грамматики обратно пропорциональна тому максимально возможному номеру типа, к которому может быть отнесена грамматика. Грамматики, которые относятся только к типу 0, являются самыми сложными, а грамматики, которые можно отнести к типу 3 — самыми простыми.

  1.  Классификация языков

Языки классифицируются в соответствии с типами грамматик, с помощью которых они заданы. Причем, поскольку один и тот же язык в общем случае может быть задан сколь угодно большим количеством грамматик, которые могут относиться к различным классификационным типам, то для классификации самого языка среди всех его грамматик всегда выбирается грамматика с максимально возможным классификационным типом. Например, если язык L может быть задан грамматиками G1 и G2, относящимися к типу 1 (контекстно-зависимые), грамматикой G3, относящейся к типу 2 (контекстно-свободные), и грамматикой G4, относящейся к типу 3 (регулярные), то сам язык должен быть отнесен к типу 3 и является регулярным языком.

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

Сложность языка убывает с возрастанием номера классификационного типа языка. Самыми сложными являются языки типа 0, самыми простыми — языки типа 3.

Согласно классификации грамматик, существуют также четыре типа языков.

  1.  Тип 0: языки с фразовой структурой

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

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

Далее языки с фразовой структурой рассматриваться не будут.

  1.  Тип 1: контекстно-зависимые (КЗ) языки

Тип 1 — второй по сложности тип языков. В общем случае время на распознавание предложений языка, относящегося к типу 1, экспоненциально зависит от длины исходной цепочки символов.

Языки и грамматики, относящиеся к типу 1, применяются в анализе и переводе текстов на естественных языках. Распознаватели, построенные на их основе, позволяют анализировать тексты с учетом контекстной зависимости в предложениях входного языка (но они не учитывают содержание текста, поэтому в общем случае для точного перевода с естественного языка все же требуется вмешательство человека). На основе таких грамматик может выполняться автоматизированный перевод с одного естественного языка на другой, ими могут пользоваться сервисные функции проверки орфографии и правописания в языковых процессорах.

В компиляторах КЗ-языки не используются, поскольку языки программирования имеют более простую структуру, поэтому здесь они подробно не рассматриваются.

  1.  Тип 2: контекстно-свободные (КС) языки

КС-языки лежат в основе синтаксических конструкций большинства современных языков программирования, на их основе функционируют некоторые довольно сложные командные процессоры, допускающие управляющие команды цикла и условия. Эти обстоятельства определяют распространенность данного класса языков.

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

Однако среди КС-языков существует много классов языков, для которых эта зависимость линейна. Многие языки программирования можно отнести к одному из таких классов.

КС-языки подробно рассматриваются в главе «Контекстно-свободные языки» данного учебного пособия.

  1.  Тип 3: регулярные языки

Регулярные языки — самый простой тип языков. Наверное, поэтому они являются самым распространенным и широко используемым типом языков в области вычислительных систем. Время на распознавание предложений регулярного языка линейно зависит от длины исходной цепочки символов.

Как уже было сказано выше, регулярные языки лежат в основе простейших конструкций языков программирования (идентификаторов, констант и т. п.), кроме того, на их основе строятся многие мнемокоды машинных команд (языки ассемблеров), а также командные процессоры, символьные управляющие команды и другие подобные структуры.

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

  1.  Примеры классификации языков и грамматик

Классификация языков идет от простого к сложному. Если мы имеем дело с регулярным языком, то можно утверждать, что он также является и контекстно-свободным, и контекстно-зависимым, и даже языком с фразовой структурой. В то же время известно, что существуют КС-языки, которые не являются регулярными, и существуют КЗ-языки, которые не являются ни регулярными, ни контекстно-свободными.

Далее приводятся примеры некоторых языков указанных типов.

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

G({0,1,2,3,4,5,6,7,8,9,-,+},(S,T,F},P,S)

Р:

S -> Т | +Т | -Т 

Т -> F | TF

F-> 0|1|2|3|4|5|6|7|8|9

По структуре своих правил данная грамматика G относится к контекстно-свободным грамматикам (тип 2). Конечно, ее можно отнести и к типу 0, и к типу 1, но максимально возможным является именно тип 2, поскольку к типу 3 эту грамматику отнести никак нельзя: строка T->F | TF содержит правило T->TF, которое недопустимо для типа 3, и, хотя все остальные правила этому типу соответствует, одного несоответствия достаточно.

  1.  Распознаватели. Задача разбора
    1.  Общая схема распознавателя

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

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

В общем виде распознаватель можно отобразить в виде условной схемы, представленной на рис. 9.5.

Рис. 9.5. Условная схема распознавателя

Следует подчеркнуть, что представленный рисунок — всего лишь условная схема, отображающая работу алгоритма распознавателя. Ни в коем случае не стоит искать подобного устройства в составе компьютера. Распознаватель, являющийся частью компилятора, представляет собой часть программного обеспечения компьютера.

Как видно из рисунка, распознаватель состоит из следующих основных компонентов:

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

Распознаватель работает с символами своего алфавита — алфавита распознавателя. Алфавит распознавателя конечен. Он включает в себя все допустимые символы входных цепочек, а также некоторый дополнительный алфавит символов, которые могут обрабатываться УУ и храниться в рабочей памяти распознавателя.

В процессе своей работы распознаватель может выполнять некоторые элементарные операции, такие как чтение очередного символа из входной цепочки, сдвиг входной цепочки на заданное количество символов (вправо или влево), доступ к рабочей памяти для чтения или записи информации, преобразование информации в памяти, изменение состояния УУ. То, какие конкретно операции должны выполняться в процессе работы распознавателя, определяется в УУ.

Распознаватель работает по шагам или тактам. В начале такта, как правило, считывается очередной символ из входной цепочки, и в зависимости от этого символа УУ определяет, какие действия необходимо выполнить. Вся работа распознавателя состоит из последовательности тактов. В начале каждого такта состояние распознавателя определяется его конфигурацией. В процессе работы конфигурация распознавателя меняется.

Конфигурация распознавателя определяется следующими параметрами:

  •  содержимое входной цепочки символов и положение считывающей головки в ней;
  •  состояние УУ;
  •  содержимое внешней памяти.

Для распознавателя всегда задается определенная конфигурация, которая считается начальной конфигурацией. В начальной конфигурации считывающая головка обозревает первый символ входной цепочки, УУ находится в заданном начальном состоянии, а внешняя память либо пуста, либо содержит строго определенную информацию.

Кроме начального состояния для распознавателя задается одна или несколько конечных конфигураций. В конечной конфигурации считывающая головка, как правило, находится за концом исходной цепочки (часто для распознавателей вводят специальный символ, обозначающий конец входной цепочки).

Распознаватель допускает входную цепочку символов , если, находясь в начальной конфигурации и получив на вход эту цепочку, он может проделать последовательность шагов, заканчивающуюся одной из его конечных конфигураций.

Формулировка «может проделать последовательность шагов» более точна, чем прямое указание «проделает последовательность шагов», так как для многих распознавателей при одной и той же входной цепочке символов из начальной конфигурации могут быть допустимы различные последовательности шагов, не все из которых ведут к конечной конфигурации.

Язык, определяемый распознавателем, — это множество всех цепочек, которые допускает распознаватель.

Далее в главах этого пособия рассмотрены конкретные типы распознавателей для различных типов языков. Но все, что было сказано здесь, относится ко всем без исключения типам распознавателей для всех типов языков.

  1.  Виды распознавателей

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

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

Односторонние распознаватели допускают перемещение считывающей головки по ленте входных символов только в одном направлении. Это значит, что на каждом шаге работы распознавателя считывающая головка может либо переместиться по ленте символов на некоторое число позиций в заданном направлении, либо остаться на месте. Поскольку все языки программирования подразумевают нотацию чтения исходной программы «слева направо», то так же работают и все распознаватели. Поэтому, когда говорят об односторонних распознавателях, то прежде всего имеют в виду левосторонние, которые читают исходную цепочку слева направо и не возвращаются назад к уже прочитанной части цепочки.

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

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

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

В противном случае распознаватель называется недетерминированным. Недетерминированный распознаватель может иметь такую допустимую конфигурацию, для которой существует некоторое конечное множество конфигураций, возможных на следующем шаге работы. Достаточно иметь хотя бы одну такую 'конфигурацию, чтобы распознаватель был недетерминированным.

По видам внешней памяти распознаватели бывают следующих типов:

  •  распознаватели без внешней памяти;
  •  распознаватели с ограниченной внешней памятью;
  •  распознаватели с неограниченной внешней памятью.

У распознавателей без внешней памяти эта память полностью отсутствует. В процессе их работы используется только конечная память устройства управления, доступ к внешней памяти не выполняется.

Для распознавателей с ограниченной внешней памятью размер внешней памяти ограничен в зависимости от длины исходной цепочки символов. Эти ограничения могут налагаться некоторой зависимостью объема памяти от длины цепочки — линейной, полиномиальной, экспоненциальной и т. д. Кроме того, для таких распознавателей может быть указан способ организации внешней памяти — стек, очередь, список и т. п.

Распознаватели с неограниченной внешней памятью предполагают, что для их работы может потребоваться внешняя память неограниченного объема (как правило, вне зависимости от длины входной цепочки). У таких распознавателей предполагается память с произвольным методом доступа.

Вместе эти три составляющих позволяют организовать общую классификацию распознавателей. Например, в этой классификации возможен такой тип: «двусторонний недетерминированный распознаватель с линейно ограниченной стековой памятью».

Тип распознавателя в классификации определяет сложность создания такого распознавателя, а следовательно, сложность разработки соответствующего программного обеспечения для компилятора. Чем выше в классификации стоит распознаватель, тем сложнее создавать алгоритм, обеспечивающий его работу. Разрабатывать двусторонние распознаватели сложнее, чем односторонние. Можно заметить, что недетерминированные распознаватели по сложности выше детерминированных. Зависимость затрат на создание алгоритма от типа внешней памяти также очевидна.

  1.  Классификация распознавателей по типам языков

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

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

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

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

Такой алгоритм распознавателя уже может быть реализован в программном обеспечении компьютера — зная длину входной цепочки, всегда можно сказать, за какое максимально возможное время будет принято решение о принадлежности цепочки данному языку и какие вычислительные ресурсы для этого потребуются. Однако экспоненциальная зависимость времени разбора от длины цепочки существенно ограничивает применение распознавателей для контекстно-зависимых языков. Как правило, такие распознаватели применяются для автоматизированного перевода и анализа текстов на естественных языках, когда временные ограничения на разбор текста несущественны (следует также напомнить, что, поскольку естественные языки более сложны, чем контекстно-зависимый тип, то после такой обработки часто требуется вмешательство человека). В компиляторах для анализа текстов на различных языках программирования контекстно-зависимые распознаватели не применяются, поскольку скорость работы компилятора имеет существенное значение, а синтаксический разбор текста программы можно выполнять в рамках более простого, контекстно-свободного типа языков.

Поэтому в рамках этого учебного пособия контекстно-зависимые языки также не рассматриваются.

Для контекстно-свободных языков (тип 2) распознавателями являются односторонние недетерминированные автоматы с магазинной (стековой) внешней памятью — МП-автоматы. При простейшей реализации алгоритма работы такого автомата он имеет экспоненциальную сложность, однако путем некоторых усовершенствований алгоритма можно добиться полиномиальной (кубической) зависимости времени, необходимого па разбор входной цепочки, от длины этой цепочки. Следовательно, можно говорить о полиномиальной сложности распознавателя для КС-языков.

Среди всех КС-языков можно выделить класс детерминированных КС-языков, распознавателями для которых являются детерминированные автоматы с магазинной (стековой) внешней памятью — ДМП-автоматы. Эти языки обладают свойством однозначности — доказано, что для любого детерминированного КС-языка всегда можно построить однозначную грамматику. Кроме того, для таких языков существует алгоритм работы распознавателя с квадратичной сложностью. Поскольку эти языки являются однозначными, именно они представляют наибольший интерес для построения компиляторов.

Более того, среди всех детерминированных КС-языков существуют такие классы языков, для которых возможно построить линейный распознаватель — распознаватель, у которого время принятия решения о принадлежности цепочки языку имеет линейную зависимость от длины цепочки. Синтаксические конструкции практически всех существующих языков программирования могут быть отнесены к одному из таких классов языков. Это обстоятельство очень важно для разработки современных быстродействующих компиляторов. Поэтому в главе, посвященной КС-языкам, в первую очередь будет уделено внимание именно таким классам этих языков.

Тем не менее, следует помнить, что только синтаксические конструкции языков программирования допускают разбор с помощью распознавателей КС-языков. Сами языки программирования, как уже было сказано, не могут быть полностью отнесены к типу КС-языков, поскольку предполагают некоторую контекстную зависимость в тексте исходной программы (например, такую, как необходимость предварительного описания переменных). Поэтому кроме синтаксического разбора практически все компиляторы предполагают дополнительный семантический анализ текста исходной программы. Этого можно было бы избежать, если построить компилятор на основе контекстно-зависимого распознавателя, но скорость работы такого компилятора была бы недопустима низка, поскольку время разбора в таком варианте будет экспоненциально зависеть от длины исходной программы. Комбинация из распознавателя КС-языка и дополнительного семантического анализатора является более эффективной с точки зрения скорости разбора исходной программы.

Для регулярных языков (тип 3) распознавателями являются односторонние недетерминированные автоматы без внешней памяти — конечные автоматы (КА). Это очень простой тип распознавателя, который всегда предполагает линейную зависимость времени на разбор входной цепочки от ее длины. Кроме того, конечные автоматы имеют важную особенность: любой недетерминированный КА всегда может быть преобразован в детерминированный. Это обстоятельство существенно упрощает разработку программного обеспечения, обеспечивающего функционирование распознавателя.

Простота и высокая скорость работы распознавателей определяют широкую область применения регулярных языков.

В компиляторах распознаватели на основе регулярных языков используются для лексического анализа текста исходной программы — выделения в нем простейших конструкций языка, таких как идентификаторы, строки, константы и т. п. Это позволяет существенно сократить объем исходной информации и упрощает синтаксический разбор программы. Более подробно взаимодействие лексического и синтаксического анализаторов текста программы рассмотрено дальше, в главе, посвященной структуре компилятора. На основе распознавателей регулярных языков функционируют ассемблеры — компиляторы с языков ассемблера (мнемокода) в язык машинных команд.

Кроме компиляторов регулярные языки находят применение еще во многих областях, связанных с разработкой программного обеспечения вычислительных систем. На их основе функционируют многие командные процессоры как в системном, так и в прикладном программном обеспечении. Для регулярных языков существуют развитые, математически обоснованные механизмы, которые позволяют облегчить создание распознавателей. Они положены в основу существующих разнообразных программных средств, которые позволяют автоматизировать этот процесс.

  1.  Задача разбора (постановка задачи)

Грамматики и распознаватели — два независимых метода, которые реально могут быть использованы для определения какого-либо языка. Однако при разработке компилятора для некоторого языка программирования возникает задача, которая требует связать между собой эти методы задания языков.

Разработчики компилятора всегда имеют дело с уже определенным языком программирования. Грамматика для синтаксических конструкций этого языка известна. Она, как правило, четко описана в стандарте языка, и хотя форма описания может быть произвольной, ее всегда можно преобразовать к требуемому виду (например, к форме Бэкуса—Наура или к форме описания с использованием метасимволов). Задача разработчиков заключается в том, чтобы построить распознаватель для заданного языка, который затем будет основой синтаксического анализатора в компиляторе.

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

Задача разбора в общем виде может быть решена не для всех типов языков. Но как было сказано выше, разработчиков компиляторов интересуют, прежде всего, контекстно-свободные и регулярные языки. Для данных типов языков доказано, что задача разбора для них разрешима. Более того, для них найдены формальные методы ее решения. Описанию и обоснованию именно методов решения задачи разбора и будет посвящена большая часть материала последующих глав.

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

Кроме того, если входная цепочка символов не принадлежит заданному языку — исходная программа содержит ошибку, — разработчику программы не интересно просто узнать сам факт наличия ошибки. В данном случае задача разбора также расширяется: распознаватель в составе компилятора должен не только установить факт присутствия ошибки во входной программе, но и по возможности определить тип ошибки и то место в цепочке символов, где она встречается.

  1.  Конечные автоматы
    1.  Определение конечного автомата

Конечным автоматом (КА) называют пятерку следующего вида:

M(Q,V,,q0,F),

где

  •  Q — конечное множество состояний автомата;
  •  V — конечное множество допустимых входных символов (алфавит автомата);
  •  — функция переходов, отображающая VQ (декартово произведение множеств) в множество подмножеств Q: R(Q), то есть (a,q) = R, aV, q Q, R  Q;
  •  q0 — начальное состояние автомата Q, q0Q;
  •  F — непустое множество конечных состояний автомата, FQ, F.

КА называют полностью определенным, если в каждом его состоянии существует функция перехода для всех возможных входных символов, то есть

aV, qQ (a,q) = R,R Q.

Работа конечного автомата представляет собой последовательность шагов (или тактов). На каждом шаге работы автомат находится в одном из своих состояний Q,(в текущем состоянии), на следующем шаге он может перейти в другое состояние или остаться в текущем состоянии. То, в какое состояние автомат перейдет на следующем шаге работы, определяет функция переходов . Она зависит не только от текущего состояния, но и от символа из алфавита V, поданного на вход автомата. Когда функция перехода допускает несколько следующих состояний автомата, то КА может перейти в любое из этих состояний. В начале работы автомат всегда находится в начальном состоянии q0. Работа КА продолжается до тех пор, пока на его вход поступают символы из входной цепочки V+.

Видно, что конфигурацию КА на каждом шаге работы можно определить в виде (q,,n), где q — текущее состояние автомата, qQ; — цепочка входных символов, V+;    n — положение указателя в цепочке символов, n N {0}, n  || (N — множество натуральных чисел). Конфигурация автомата на следующем шаге — это (q',,n+l), если         q' (a,q) и символ a V находится в позиции n+1 цепочки . Начальная конфигурация автомата: (q0,,0); заключительная конфигурация автомата: (f,,n), fQ, n = ||, она является конечной конфигурацией, если f  F.

КА  M(Q,V,,q0,F) принимает цепочку символов V+, если, получив на вход эту цепочку, он из начального состояния q0 может перейти в одно из конечных состояний f  F. В противном случае КА не принимает цепочку символов.

Язык L(M), заданный КА M(Q,V,,q0,F), — это множество всех цепочек символов, которые принимаются этим автоматом. Два КА эквивалентны, если они задают один и тот же язык.

Таким образом, КА является распознавателем для формальных языков. Далее будет показано, что КА — это распознаватели для регулярных языков.

КА часто представляют в виде диаграммы или графа переходов автомата.

Граф переходов КА — это направленный помеченный граф, с символами состояний КА в вершинах, в котором есть дуга (p,q)  p,q   Q , помеченная символом aV, если в КА определена (а,р) и q   (a,p). Начальное и конечные состояния автомата на графе состояний помечаются специальным образом (начальное состояние — дополнительной пунктирной линией, конечное состояние — дополнительной сплошной линией).

Рассмотрим конечный автомат

M({H, A, B, S},{a, b},, H,{S}); : (Н, b) = В, (В, а) = А, (A,b) = {B,S}.

Ниже на рис. 10.1 приведен пример графа состояний для этого КА.

Рис. 10.1. Граф переходов недетерминированного конечного автомата

Для моделирования работы КА его удобно привести к полностью определенному виду, чтобы исключить ситуации, из которых нет переходов по входным символам. Для этого в КА добавляют еще одно состояние, которое можно условно назвать «ошибка». На это состояние замыкают все неопределенные переходы, а все переходы из самого состояния «ошибка» замыкают на него же.

Если преобразовать подобным образом рассмотренный выше автомат М, то получим полностью определенный автомат: M({H, A, B, E, S},{a, b},, H,{S}); ; (Н, a) = E, (Н, b) = В, (В, а) = А, (В, b) = E, (A, a) = {E}, (A,b) = {B,S}, (E, a) = {E}, (E, b) = {E}, (S, a) = {E}, (S, b) = {E}.

Состояние Е как раз соответствует состоянию «ошибка». Граф переходов этого КА представлен на рис. 10.2.

Рис. 10.2. Граф переходов полностью определенного недетерминированного конечного автомата

  1.  Детерминированные и недетерминированные конечные автоматы

Конечный автомат M(Q,V,,q0,F) называют детерминированным конечным автоматом (ДКА), если в каждом из его состояний для любого входного символа функция перехода содержит не более одного состояния: a V, q Q: либо (a,q) = {r}, r Q, либо (a,q) = .

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

ДКА может быть задан в виде пятерки:

M(Q,V,,q0,F)

где Q — конечное множество состояний автомата; V — конечное множество допустимых входных символов; — функция переходов, отображающая VQ в множество Q: (a,q) = r, a V, q,rQ; q0 начальное состояние автомата Q, q0Q , F — непустое множество конечных состояний автомата, F Q , F.

Если функция переходов ДКА определена для каждого состояния автомата, то автомат называется полностью определенным ДКА: a V, q Q: либо (a,q) = r, r Q.

Моделировать работу ДКА существенно проще, чем работу произвольного КА.

Доказано, что для любого КА можно построить эквивалентный ему ДКА. Поэтому используемый произвольный КА стремятся преобразовать в ДКА. При построении компиляторов чаще всего используют полностью определенный ДКА.

  1.  Контекстно-свободные языки
    1.  Распознаватели КС-языков. Автоматы с магазинной памятью
      1.  Определение МП-автомата

Контекстно-свободными (КС) называются языки, определяемые грамматиками типа G(VT,VN,P,S), в которых правила Р имеют вид: А-> , где A VN и V*, V=VT VN.

Распознавателями КС-языков служат автоматы с магазинной памятью (МП-автоматы).

В общем виде МП-автомат можно определить следующим образом:

R(Q,V,Z, ,q0, z0,F),

где Q — множество состояний автомата; V — алфавит входных символов автомата; Z — специальный конечный алфавит магазинных символов автомата (обычно он включает в себя алфавиты терминальных и нетерминальных символов грамматики), VZ;  — функция переходов автомата, которая отображает множество Q(V{})Z на конечное множество подмножеств P(Q  Z*); q0Q — начальное состояние автомата; z0Z — начальный символ магазина; FQ — множество конечных состояний.

МП-автомат в отличие от обычного КА имеет стек (магазин), в который можно помещать специальные «магазинные» символы (обычно это терминальные и нетерминальные символы грамматики языка). Переход из одного состояния в другое зависит не только от входного символа, но и от одного или нескольких верхних символов стека. Таким образом, конфигурация автомата определяется тремя параметрами: состоянием автомата, текущим символом входной цепочки (положением указателя в цепочке) и содержимым стека.

МП-автомат условно можно представить в виде схемы на рис. 11.1.

Рис. 11.1. Общая условная схема автомата с магазинной памятью (МП-автомата)

Конфигурация МП-автомата описывается в виде тройки (q, ) Q V* Z*, которая определяет текущее состояние автомата q, цепочку еще непрочитанных символов на входе автомата и содержимое магазина (стека) . Вместо в конфигурации можно указать пару ( n), где V* — вся цепочка входных символов, a n N{0}, n  0 — положение считывающего указателя в цепочке.

Тогда один такт работы автомата можно описать в виде (q,a,z) (q', , ),    если (q', )(q,a,z), где q,q' Q, aV{}, V*, z Z {}, Z*. При выполнении такта (перехода) из стека удаляется верхний символ, соответствующий условию перехода, и добавляется цепочка, соответствующая правилу перехода. Первый символ цепочки становится верхушкой стека. Допускаются переходы, при которых входной символ игнорируется (и тем самым он будет входным символом при следующем переходе). Эти переходы (такты) называются переходами (-тактами). Аналогично, автомат не обязательно должен извлекать символ из стека — когда z=, этого не происходит.

МП-автомат называется недетерминированным, если из одной и той же его конфигурации возможен более чем один переход.

Начальная конфигурация МП-автомата, очевидно, определяется как (q0, ,z0), V*. Множество конечных конфигураций автомата — (q, , ), q F, Z*.

МП-автомат допускает (принимает) цепочку символов, если, получив эту цепочку на вход, он может перейти в одну из конечных конфигураций, — когда при окончании цепочки автомат находится в одном из конечных состояний, а стек содержит некоторую определенную цепочку. Тогда входная цепочка принимается (после окончания цепочки автомат может сделать произвольное количество -переходов). Иначе цепочка символов не принимается.

Язык, определяемый МП-автоматом, — это множество всех цепочек символов, которые допускает данный автомат. Язык, определяемый МП-автоматом R, обозначается как L(R). Два МП-автомата называются эквивалентными, если они определяют один и тот же язык. Если два МП-автомата R1 и R2 определяют один и тот же язык, это записывается как L(R1) = L(R2).

МП-автомат допускает цепочку символов с опустошением магазина, если при окончании разбора цепочки автомат находится в одном из конечных состояний, а стек пуст — конфигурация (q, , ), q F. Если язык задан МП-автоматом R, который допускает цепочки с опустошением стека, это обозначается так: L(R). Для любого МП-автомата всегда можно построить эквивалентный ему МП-автомат, допускающий цепочки заданного языка с опустошением стека. То есть МП-автомата R: МП-автомат R', такой что L(R) = L (R').

Кроме обычного МП-автомата существует также понятие расширенного МП-автомата.

Расширенный МП-автомат может заменять цепочку символов конечной длины в верхней части стека на другую цепочку символов конечной длины. В отличие от обычного МП-автомата, который на каждом такте работы может изымать из стека только один символ, расширенный МП-автомат может изымать за один такт сразу некоторую цепочку символов, находящуюся на вершине стека. Функция переходов для расширенного МП-автомата отображает множество Q (V{})Z* на конечное множество подмножеств P(QZ*).

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

  1.  Детерминированные МП-автоматы

МП-автомат называется детерминированным, если из одной и той же его конфигурации возможно не более одного перехода в следующую конфигурацию.

Формально для детерминированного МП-автомата R(Q,V,Z, ,q0, z0,F), функция переходов может q Q, aV,  z Z иметь один из следующих трех видов:

1. (q,a,z) содержит один элемент: (q,a,z) = {(q',)}, Z* и (q, ,z) = .

2. (q,a,z) = и (q, ,z) содержит один элемент: (q, ,z)  = {(q', )}, Z*.

3. (q,a,z) =  и (q, ,z) =.

Класс ДМП-автоматов и соответствующих им языков значительно уже, чем весь класс МП-автоматов и КС-языков. В отличие от обычного конечного автомата, не для каждого МП-автомата можно построить эквивалентный ему ДМП-авто-мат. Иными словами, в общем случае невозможно преобразовать недетерминированный МП-автомат в детерминированный.

Детерминированные МП-автоматы (ДМП-автоматы) определяют очень важный класс среди всех КС-языков, называемый детерминированными КС-языками. Доказано, что все языки, принадлежащие к классу детерминированных КС-языков, могут быть построены с помощью однозначных КС-грамматик. Поскольку однозначность — это важное и обязательное требование в грамматике любого языка программирования, ДМП-автоматы представляют особый интерес для создания компиляторов. Синтаксический распознаватель цепочек любого языка программирования может быть построен на основе ДМП-автомата.

Кроме того, доказано, что для любого ДМП-автомата всегда можно построить эквиьалентный ему ДМП-автомат, который будет учитывать входную цепочку до конца — не допускать бесконечной последовательности -переходов по завершении цепочки. Это значительно облегчает моделирование работы ДМП-автоматов.

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

  1.  Распознаватели КС-языков с возвратом
    1.  Принципы работы распознавателей с возвратом

Распознаватели с возвратом — это самый примитивный тип распознавателей для КС-языков. Логика их работы основана на моделировании недетерминированного МП-автомата.

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

В первом варианте на каждом шаге работы алгоритм должен запоминать все возможные следующие состояния МП-автомата, выбирать одно из них, переходить в это состояние и действовать так до тех пор, пока либо не будет достигнуто конечное состояние автомата, либо автомат не перейдет в такую конфигурацию, когда следующее состояние будет не определено. Если достигнуто одно из конечных состояний — входная цепочка принята, работа алгоритма завершается. В противном случае алгоритм должен вернуть автомат на несколько шагов назад, когда еще был возможен выбор одного из набора следующих состояний, выбрать другой вариант и промоделировать поведение автомата с этим условием. Алгоритм завершается с ошибкой, когда все возможные варианты работы автомата будут перебраны и ни одно из возможных конечных состояний не было достигнуто.

Во втором варианте алгоритм моделирования МП-автомата должен на каждом шаге работы при возникновении неоднозначности с несколькими возможными следующими состояниями автомата запускать новую свою копию для обработки каждого из этих состояний. Алгоритм завершается, если хотя бы одна из выполняющихся его копий достигнет одно из конечных состояний. При этом работа всех остальных копий алгоритма прекращается. Если ни одна из копий алгоритма не достигла конечного состояния МП-автомата, то алгоритм завершается с ошибкой.

Второй вариант реализации алгоритма связан с управлением параллельными процессами в вычислительных системах, поэтому сложен в реализации. Кроме того, на каждом шаге работы МП-автомата альтернатив следующих состояний может быть много, а количество возможных параллельно выполняющихся процессов в операционных системах ограничено, поэтому применение второго варианта алгоритма осложнено. По этим причинам большее распространение получил первый вариант алгоритма, который предусматривает возврат к ранее запомненным состояниям МП-автомата — отсюда и название «разбор с возвратами».

Следует отметить, что, хотя МП-автомат является односторонним распознавателем, алгоритм моделирования его работы предусматривает возврат назад, к уже прочитанной части цепочки символов, чтобы исключить недетерминизм в поведении автомата (который невозможно промоделировать).

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

Чтобы избежать таких ситуаций, алгоритмы разбора с возвратами строят не для произвольных МП-автоматов, а для МП-автоматов, удовлетворяющим некоторым заданным условиям. Как правило, эти условия связаны с тем, что МП-автомат должен строиться на основе грамматики заданного языка только после того, как она подвергнется некоторым преобразованиям. Поскольку преобразования грамматик сами по себе не накладывают каких-либо ограничений на входной класс КС-языков (в результате преобразования мы всегда получаем эквивалентную грамматику), то они и не ограничивают применимости алгоритмов разбора с возвратами — эти алгоритмы применимы для любого КС-языка, заданного произвольной КС-грамматикой или МП-автоматом.

Алгоритмы разбора с возвратами обладают экспоненциальными характеристиками. Это значит, что вычислительные затраты алгоритмов экспоненциально зависят от длины входной цепочки символов: , VT*, n = ||. Конкретная зависимость определяется вариантом реализации алгоритма.

Доказано, что в общем случае при первом варианте реализации для произвольной КС-грамматики G(VT,VN,P,S) время выполнения данного алгоритма Тэ будет иметь экспоненциальную зависимость от длины входной цепочки, а необходимый объем памяти Мэ — линейную зависимость от длины входной цепочки:

Тэ = О(еn) и Мэ = O(n). При втором варианте реализации, наоборот, время выполнения данного алгоритма Тэ будет иметь линейную зависимость от длины входной цепочки, а необходимый объем памяти Мэ — экспоненциальную зависимость от длины входной цепочки: Тэ = О(n) и Мэ = O(еn).

Экспоненциальная зависимость вычислительных затрат от длины входной цепочки существенно ограничивает применимость алгоритмов разбора с возвратами. Они тривиальны в реализации, но имеют неудовлетворительные характеристики, поэтому могут использоваться только для простых КС-языков с малой длиной входных предложений языка.

Возможность использовать эти алгоритмы в реальных компиляторах весьма сомнительна, поскольку длина входной цепочки может достигать нескольких тысяч и даже десятков тысяч символов. Очевидно, что время работы алгоритма при экспоненциальной зависимости требуемых вычислительных ресурсов от длины входной цепочки символов будет в таком варианте явно неприемлемым даже на самых современных компьютерах.

Для многих классов КС-языков существуют более эффективные алгоритмы распознавания, поэтому алгоритмы разбора с возвратами применяются редко.

Далее рассмотрены два основных варианта таких алгоритмов.

  1.  Нисходящий распознаватель с возвратом
    1.  Принцип работы нисходящего распознавателя с подбором альтернатив

Этот распознаватель моделирует работу МП-автомата с одним состоянием    q: R({q}, V,Z, ,q,S,{q}). Автомат распознает цепочки КС-языка, заданного КС-грамматикой G(VT,VN,P,S). Входной алфавит автомата содержит терминальные символы грамматики: V = VT, а алфавит магазинных символов строится из терминальных и нетерминальных символов грамматики: Z = VTVN.

Начальная конфигурация автомата определяется так: (q, ,S) — автомат пребывает в своем единственном состоянии q, считывающая головка находится в начале входной цепочки символов VT*, в стеке лежит символ, соответствующий целевому символу грамматики S.

Конечная конфигурация автомата определяется так: (q, ,) — автомат пребывает в своем единственном состоянии q, считывающая головка находится за концом входной цепочки символов, стек пуст.

Функция переходов МП-автомата строится на основе правил грамматики:

  1.  (q, )(q, ,A), AVN, (VTVN)*, если правило А—> содержится во множестве правил Р грамматики G: А—>   Р.
  2.  (q, )(q, a, a),  aVT

Этот МП-автомат уже был рассмотрен выше.

Работу данного МП-автомата можно неформально описать следующим образом:

если на верхушке стека автомата находится нетерминальный символ А, то его можно заменить на цепочку символов к, если в грамматике языка есть правило А-> , не сдвигая при этом считывающую головку автомата (этот шаг работы называется «подбор альтернативы»); если же на верхушке стека находится терминальный символ а, который совпадает с текущим символом входной цепочки, то этот символ можно выбросить из стека и передвинуть считывающую головку на одну позицию вправо (этот шаг работы называется «выброс»). Данный МП-автомат может быть недетерминированным, поскольку при подборе альтернативы в грамматике языка может оказаться более одного правила вида А—>, следовательно, тогда функция (q, ,A) будет содержать более одного следующего состояния — у автомата будет несколько альтернатив.

Данный МП-автомат строит левосторонние выводы для грамматики G(VT,VN, P,S). Для моделирования такого автомата необходимо, чтобы грамматика G(VT, VN,P,S) не была леворекурсивной (в противном случае, очевидно, автомат может войти в бесконечный цикл). Поскольку, как было доказано выше, произвольную КС-грамматику всегда можно преобразовать к нелеворекурсивному виду, то этот алгоритм применим для любой КС-грамматики, следовательно, им можно распознавать цепочки любого КС-языка.

Рассмотренный МП-автомат строит левосторонние выводы и читает цепочку входных символов слева направо. Поэтому для него естественным является построение дерева вывода сверху вниз. Такой распознаватель называется нисходящим.

Решение о том, выполнять ли на каждом шаге работы МП-автомата выброс или подбор альтернативы, принимается однозначно. Моделирующий алгоритм должен обеспечивать выбор одной из возможных альтернатив и хранение информации о том, какие альтернативы на каком шаге уже были выбраны, чтобы иметь возможность вернуться к этому шагу и подобрать другие альтернативы. Такой алгоритм разбора называется алгоритмом с подбором альтернатив.

  1.  Реализация алгоритма распознавателя с подбором альтернатив

Существует масса способов реализации алгоритма, моделирующего работу этого МП-автомата. Рассмотрим один из примеров реализации алгоритма нисходящего распознавателя с возвратом.

Для работы алгоритма используется МП-автомат, построенный на основе исходной грамматики G(VT,VN,P,S). Для удобства работы все правила из множества Р в грамматике G представим в виде A->1|2|…|n, то есть пронумеруем все возможные альтернативы для каждого нетерминального символа AVN. Входная цепочка символов имеет вид =a1a2…an, || =  n. В алгоритме используется также еще дополнительное состояние автомата b (от «back» — «назад»), которое сигнализирует о выполнении возврата к уже прочитанной части входной цепочки. Для хранения уже выбранных альтернатив используется дополнительный стек L2, который может содержать следующую информацию:

символы aVT входного языка автомата;

символы вида Aj, где A VN — это означает, что среди всех возможных правил для символа А была выбрана альтернатива с номером j.

В итоге алгоритм работает с двумя стеками: l1 — стек МП-автомата и L2 — стек возвратов. Оба они представлены в виде цепочек символов. Символы в цепочку стека l1 помещаются слева, а в цепочку стека L2 — справа. В целом состояние алгоритма на каждом шаге определяется четырьмя параметрами: (Q, i, L1, L2), где Q — текущее состояние автомата (q или b); i — положение считывающей головки во входной цепочке символов                          (1 < i <= n+1); L1 — содержимое стека МП-автомата; L2 — содержимое дополнительного стека.

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

Алгоритм предусматривает циклическое выполнение следующих шагов.

Шаг 1 (Разрастание), (q, i, А, ) —> (q, i, 1, A1), если A->1 — это первая из всех возможных альтернатив для символа А.

Шаг 2 (Успешное сравнение), (q, i, a, ) -> (q, i+1, , а), если а = аi , aVT.

Шаг 3 (Завершение). Если состояние соответствует (q, n+1, , ), то разбор завершен, алгоритм заканчивает работу, иначе (q, i, , ) -> (b, i, , ), когда in+1.

Шаг 4 (Неуспешное сравнение), (q, i, а, ) —> (b, i, а, ), если а ai, aVT.

Шаг 5 (Возврат по входу), (b, i, , а) —> (q, i-1, a, )), aVT.

Шаг 6 (Другая альтернатива). Исходное состояние (b, i, j, Aj), действия:

  •  перейти в состояние (q, i, j+1, Aj+1), если еще существует альтернатива A—>j+1 для символа A VN;
  •  сигнализировать об ошибке и прекратить выполнение алгоритма, если AS и не существует больше альтернатив для символа S;
  •  иначе перейти в состояние (q, i, А, ).

В случае успешного завершения алгоритма цепочку вывода можно построить на основе содержимого стека L2, полученного в результате выполнения алгоритма. Цепочка вывода строится следующим образом: поместить в цепочку номер правила m, соответствующий альтернативе А—>j , если в стеке содержится символ Aj; все символы aVT, содержащиеся в стеке L2, игнорируются.

Этот алгоритм может быть напрямую использован для построения распознавателей. Следует помнить, что для применения этого алгоритма исходная грамматика не должна быть леворекурсивной. Если это условие не удовлетворяется, то грамматику предварительно надо преобразовать к нелеворекурсивному виду.

Рассмотрим в качестве примера грамматику G ({+.-,/,*, a, b}, {S, R, T, F, E}, P, S) с правилами (это нелеворекурсивная грамматика):

Р:

S -> Т | TR

R -> +T | -Т | +TR | -TR

Т -> Е | EF

F -> *Е | /Е | *EF | /EF

Е -> (S) | а | b

Проследим разбор цепочки а+(а*b) из языка этой грамматики с помощью алгоритма нисходящего распознавателя с возвратами. Работу алгоритма будем представлять в виде последовательности его состояний, взятых в скобки {} (фигурные скобки используются, чтобы не путать их с круглыми скобками, предусмотренными в правилах грамматики). Альтернативы будем нумеровать слева направо, правила — слева направо и сверху вниз. Для пояснения каждый шаг работы сопровождается номером шага алгоритма, который был применен для перехода в очередное состояние (записывается перед состоянием через символ «:» — двоеточие).

Алгоритм работы нисходящего распознавателя с возвратами при разборе цепочки а+(а*b) будет выполнять следующие шаги:

На основании полученной цепочки номеров альтернатив S2T1E2R1T1E1S1T2E2F1E3

построим последовательность номеров примененных правил: 2, 7, 14, 3, 7, 13, 1, 8, 14, 9, 15. Получаем левосторонний вывод: S => TR => ER => aR => a+T => a+E => a+(S) => a+(T) => a+(EF) => a+(aF) => a+(a*E) => a+(a*b). Соответствующее ему дерево вывода приведено на рис. 11.2.

Рис. 11.2. Дерево вывода для грамматики без левых рекурсий

Из приведенного примера очевиден недостаток алгоритма нисходящего разбора с возвратами — значительная временная емкость: для разбора достаточно короткой входной цепочки (всего 7 символов) потребовалось 68 шагов работы алгоритма. Такого результата и следовало ожидать, исходя из экспоненциальной зависимости необходимых для работы алгоритма вычислительных ресурсов от длины входной цепочки. Это существенный недостаток данного алгоритма.

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

Еще одно преимущество алгоритма — его универсальность. На его основе можно распознавать входные цепочки языка, заданного любой КС-грамматикой, достаточно лишь привести ее к нелеворекурсивному виду (а это можно сделать с любой грамматикой, см. раздел «Преобразование КС-грамматик. Приведенные грамматики»). Интересно, что грамматика даже не обязательно должна быть однозначной — для неоднозначной грамматики алгоритм найдет один из возможных ле-носторонних выводов.

Сам по себе алгоритм разбора с подбором альтернатив, использующий возвраты, • не находит применения в реальных компиляторах. Однако его основные принципы лежат в основе многих нисходящих распознавателей, строящих левосторонние выводы и работающих без использования возвратов. Методы, позволяющие строить такие распознаватели для некоторых классов КС-языков, рассмотрены далее. Эти распознаватели будут более эффективны в смысле необходимых вычислительных ресурсов, но алгоритмы их работы уже более сложны, кроме того, они не являются универсальными.

  1.  Распознаватель на основе алгоритма «сдвиг-свертка»
    1.  Принцип работы восходящего распознавателя по алгоритму «сдвиг-свертка»

Этот распознаватель строится на основе расширенного МП-автомата с одним состоянием q: R({q},V,Z,,q,S,{q}). Автомат распознает цепочки КС-языка, заданного КС-грамматикой G(VT,VN,P,S). Входной алфавит автомата содержит терминальные символы грамматики: V = VT; а алфавит магазинных символов строится из терминальных и нетерминальных символов грамматики: Z = VTVN.

Начальная конфигурация автомата определяется так: (q,) — автомат пребывает в своем единственном состоянии q, считывающая головка находится в начале входной цепочки символов VT*, стек пуст.

Конечная конфигурация автомата определяется так: (q, S) — автомат пребывает в своем единственном состоянии q, считывающая головка находится за концом входной цепочки символов, в стеке лежит символ, соответствующий целевому символу грамматики S.

Функция переходов МП-автомата строится на основе правил грамматики:

1. (q,A) (q, ), AVN, (VTVN)*, если правило А-> содержится во множестве правил Р грамматики G: А-> Р.

2. (q, a)(q, a, ) aVT.

Неформально работу этого расширенного автомата можно описать так: если на верхушке стека находится цепочка символов , то ее можно заменить на нетерминальный символ А, если в грамматике языка существует правило вида А—>,   не сдвигая при этом считывающую головку автомата (этот шаг работы называется «свертка»); с другой стороны, если считывающая головка автомата обозревает некоторый символ входной цепочки а, то его можно поместить в стек, сдвинув при этом головку на одну позицию вправо (этот шаг работы называется «сдвиг» или «перенос»). Сам алгоритм, моделирующий работу такого расширенного автомата, называется алгоритмом «сдвиг-свертка» или «перенос-свертка» (по названиям основных действий алгоритма).

Данный расширенный МП-автомат строит правосторонние выводы для грамматики. Для моделирования такого автомата необходимо, чтобы грамматика G(VT, VN, P, S) не содержала -правил и цепных правил (иначе, автомат может войти в бесконечный цикл из сверток). Поскольку произвольную КС-грамматику всегда можно преобразовать к виду без -правил и цепных правилБ о этот алгоритм применим для любой КС-граматики, следовательно, им можно распознавать цепочки любого КС-языка.

Этот расширенный МП-автомат строит правосторонние выводы и читает цепочку входных символов слева направо. Поэтому для него естественным является построение дерева вывода снизу вверх. Такой распознаватель называется восходящим.

Данный расширенный МП-автомат потенциально имеет больше неоднозначностей, чем рассмотренный выше МП-автомат, основанный на алгоритме подбора альтернатив. На  каждом шаге работы автомата надо решать следующие вопросы:

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

Чтобы промоделировать работы этого расширенного МП-автомата, надо на каждом шаге запоминать все предпринятые действия, чтобы иметь возможность вернуться к уже сделанному шагу и выполнить эти же действия по-другому. Этот процесс должен повторяться до тех пор, пока не будут перебраны все возможные варианты.

  1.  Табличные распознаватели для КС-языков
    1.  Общие принципы работы табличных распознавателей

Табличные распознаватели используют для построения цепочки вывода КС-грамматики другие принципы, нежели МП-автоматы. Как и МП-автоматы, они получают на вход цепочку входных символов = а1а2an, VT*, | | = n, a построение вывода основывают на правилах заданной КС-грамматики G(VT,VN,P,S). Принцип их работы заключается в том, что искомая цепочка вывода строится не сразу — сначала на основе входной цепочки порождается некоторое промежуточное хранилище информации объема n*n (промежуточная таблица), а потом уже на его основе строится вывод.

Табличные алгоритмы обладают полиномиальными характеристиками требуемых вычислительных ресурсов в зависимости от длины входной цепочки. Для произвольной КС-грамматики G(VT,VN,P,S) время выполнения алгоритма Тэ имеет кубическую зависимость от длины входной цепочки, а необходимый объем памяти Мэ — квадратичную зависимость от длины входной цепочки: , VT*, n= ||: Тэ = O(n3) и Мэ= О(n2). Квадратичная зависимость объема необходимой памяти от длины входной цепочки напрямую связана с использованием промежуточного хранилища данных. 

Табличные распознаватели универсальны — они могут быть использованы для распознавания цепочек, порожденных с помощью произвольной КС-грамматики (возможно, саму грамматику первоначально потребуется привести к заданному виду, но это не ограничивает универсальности алгоритмов). Кроме того, табличные распознаватели — это самые эффективные с точки зрения требуемых вычислительных ресурсов универсальные алгоритмы для распознавания цепочек КС-языков.

  1.  Принципы построения распознавателей КС-языков без возвратов

Выше были рассмотрены различные универсальные распознаватели для КС-языков — то есть распознаватели, позволяющие выполнить разбор цепочек для любого КС-языка (заданного произвольной КС-грамматикой). Они универсальны, но имеют неудовлетворительные характеристики. Распознаватели с возвратами имеют экспоненциальную зависимость требуемых для выполнения алгоритма разбора вычислительных ресурсов от длины входной цепочки символов, а табличные распознаватели — полиномиальную. Для практического применения в реальных компиляторах такие характеристики являются неудовлетворительными.

К сожалению, универсальных распознавателей с лучшими характеристиками для КС-языков построить не удается. Среди универсальных распознавателей лучшими по эффективности являются табличные.

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

Для многих классов КС-грамматик (и соответствующих им классов КС-языков) можно построить распознаватели, имеющие лучшие характеристики, чем рассмотренные выше распознаватели с возвратами и табличные. Эти распознаватели уже не будут универсальными — они будут применимы только к заданному классу КС-языков с соответствующими ограничениями, зато они будут иметь лучшие характеристики.

Далее будут рассмотрены некоторые из таких распознавателей. Все они имеют линейные характеристики — линейную зависимость необходимых для выполнения алгоритма разбора вычислительных ресурсов от длины входной цепочки. Для каждого распознавателя рассматривается класс КС-грамматик, с которым он связан. Это значит, что он может принимать только входные цепочки из КС-языков, заданных такими грамматиками. Всегда описываются ограничения, налагаемые на правила грамматики, или дается алгоритм проверки принадлежности произвольной КС-грамматики к заданному классу.

Однако следует всегда помнить, что проблема преобразования КС-грамматик алгоритмически неразрешима. Если какая-то грамматика не принадлежит к требуемому классу КС-грамматик, это еще не значит, что заданный ею язык не может быть описан грамматикой такого класса. Иногда удается выполнить преобразования и привести исходную грамматику к требуемому виду. Но, к сожалению, этот процесс не формализован, не поддается алгоритмизации и требует участия человека. Чаще всего такую работу вынужден выполнять разработчик компилятора (правда, выполняется она только один раз для синтаксических конструкций каждого языка программирования).

Существуют два принципиально разных класса распознавателей. Первый — нисходящие распознаватели, которые порождают цепочки левостороннего вывода и строят дерево вывода сверху вниз. Второй — восходящие распознаватели, которые порождают цепочки правостороннего вывода и строят дерево вывода снизу вверх. Названия «нисходящие» и «восходящие» связаны с порядком построения дерева вывода. Как правило, все распознаватели читают входную цепочку символов слева направо, поскольку предполагается именно такая нотация в написании исходного текста программ.

Нисходящие распознаватели используют модификации алгоритма с подбором альтернатив. При их создании применяются методы, которые позволяют однозначно выбрать одну и только одну альтернативу на каждом шаге работы МП-автомата (шаг «выброс» в этом автомате всегда выполняется однозначно). Алгоритм подбора альтернатив без модификаций был рассмотрен выше.

Восходящие распознаватели используют модификации алгоритма «сдвиг-свертка» (или «перенос-свертка», что то же самое). При их создании применяются методы, которые позволяют однозначно выбрать между выполнением «сдвига» («переноса») или «свертки» на каждом шаге работы расширенного МП-автомата, а при выполнении свертки однозначно выбрать правило, по которому будет производиться свертка. Алгоритм «сдвиг-свертка» без модификаций был рассмотрен выше.

  1.  Классы КС-языков и грамматик
    1.  Нисходящие распознаватели КС-языков без возвратов
      1.  Левосторонний разбор по методу рекурсивного спуска

Стремление улучшить алгоритм нисходящего разбора заключается в первую очередь в определении метода, по которому на каждом шаге алгоритма можно было бы однозначно выбрать одну из всего множества возможных альтернатив. В таком случае алгоритм моделирования работы МП-автомата не требовал бы возврата на предыдущие шаги и за счет этого обладал бы линейными характеристиками от длины входной цепочки. В случае неуспеха выполнения такого алгоритма входная цепочка однозначно не принимается, повторные итерации разбора не выполняются.

Наиболее очевидным методом выбора одной из множества альтернатив является выбор ее на основании символа VT, обозреваемого считывающей головкой автомата на каждом шаге его работы (находящегося справа от положения текущей головки во входной цепочке символов). Поскольку в процессе нисходящего разбора именно этот символ должен появиться на верхушке магазина для продвижения считывающей головки автомата на один шаг (условие (q,a,a) = {(q, )}, aVT в функции переходов МП-автомата), то разумно искать альтернативу, где он присутствует в начале цепочки, стоящей в правой части правила грамматики.

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

  1.  Алгоритм разбора по методу рекурсивного спуска

В реализации этого алгоритма для каждого нетерминального символа AVN грамматики G(VN,VT,P,S) строится процедура разбора, которая получает на вход цепочку символов а и положение считывающей головки в цепочке i. Если для символа А в грамматике G определено более одного правила, то процедура разбора ищет среди них правило вида       А а, aVT, (VNVT)*, первый символ правой части которого совпадал бы с текущим символом входной цепочки а = i. Если такого правила не найдено, то цепочка не принимается. Иначе (если найдено правило А—>а или для символа А в грамматике G существует только одно правило А—>), то запоминается номер правила, и когда а = i, то считывающая головка передвигается (увеличивается i), а для каждого нетерминального символа в цепочке рекурсивно вызывается процедура разбора этого символа.

Название метода происходит из реализации алгоритма, которая заключается в последовательности рекурсивных вызовов процедур разбора. Для начала разбора входной цепочки нужно вызвать процедуру для символа S с параметром i = 1.

Условия применимости метода можно получить из описания самого алгоритма—в грамматике G(VN,VT,P,S) AVN возможны только два варианта правил:

А->, (VNVT)* и это единственное правило для А;

A->a11| a22|…| ann , i: ai VT, i (VNVT)* и если ij, то аiaj.

Этим условиям удовлетворяет незначительное количество реальных грамматик. Это достаточные, но не необходимые условия. Если грамматика не удовлетворяет этим условиям, еще не значит, что заданный ею язык не может распознаваться с помощью метода рекурсивного спуска. Возможно, над грамматикой просто необходимо выполнить ряд дополнительных преобразований.

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

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

1. Исключение -правил.

2. Исключение левой рекурсии.

3. Добавление новых нетерминальных символов.

Например:

если правило: A->a1| a2|… |an|b11| b22|…| bmm|,

то заменяем его на два: А->аА'| b11| b22|…| bmm|, и A'->1| 2|… |n.

4. Замена нетерминальных символов в правилах на цепочки их выводов.

Например:

если имеются правила:

A->B1| B2|… |Bn|b11| b22|…| bmm|,

B1->11| 12|… |1k,

Bn->n1| n2|… |np

заменяем первое правило на

A->11| 12|… |1k|…|n1| n2|… |np| b11| b22|…| bmm|.

В целом алгоритм рекурсивного спуска эффективен и прост в реализации, но имеет очень ограниченную применимость.

  1.  Пример реализации метода рекурсивного спуска

Дана грамматика G({a,b,c},(A,B,C,S},P,S):

Р:

S -> аА | bВ

А -> а | bА | сС

В -> b | аВ | сС

С -> АаВb

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

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

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

  •  цепочка входных символов;
  •  положение указателя (считывающей головки МП-автомата) во входной цепочке;
  •  массив для записи номеров примененных правил;
  •  порядковый номер очередного правила в массиве.

Результатом работы каждой процедуры может быть число, отличное от нуля («истина»), или 0 («ложь»). В первом случае входная цепочка символов принимается распознавателем, во втором случае — не принимается. Для удобства реализации в том случае, если цепочка принимается распознавателем, будем возвращать текущее положение указателя в цепочке. Кроме того, потребуется еще одна дополнительная процедура для ведения записей в массиве последовательности правил (назовем ее WriteRules).

Теперь для распознавания входной цепочки необходимо иметь целочисленный массив Rules достаточного объема для хранения номеров правил. Тогда работа распознавателя заключается в вызове процедуры proc_S(Str,0,Ru1es,&N), где Str — это входная цепочка символов, N — переменная для запоминания количества примененных правил (первоначально N = 0). Затем требуется обработка полученного результата: если результат на 1 превышает длину цепочки — цепочка принята, иначе — цепочка не принята. В первом случае в массиве Rules будем иметь последовательность номеров правил грамматики, необходимых для вывода цепочки, а в переменной N — количество этих правил. На основе этой цепочки можно легко построить дерево вывода.

Объем массива Rules заранее не известен, так как заранее не известно количество шагов вывода. Чтобы избежать проблем с недостаточным объемом статического массива, приведенные выше процедуры распознавателя можно модифицировать так, чтобы они работали с динамическим распределением памяти (изменив процедуру WriteRules и тип параметра piRul в вызовах остальных процедур). На логику работы распознавателя это никак не повлияет. Следует помнить также, что метод рекурсивного спуска основан на рекурсивном вызове множества процедур, что при значительной длине входной цепочки символов может потребовать соответствующего объема стека вызовов для хранения адресов процедур, их параметров и локальных переменных.

Из приведенного примера видно, что алгоритм рекурсивного спуска удобен и прост в реализации. Главным препятствием в его применении является то, что класс грамматик, допускающих разбор на основе этого алгоритма, сильно ограничен.

  1.  Определение LL(k)-грамматики

Логическим продолжением идеи, положенной в основу метода рекурсивного спуска, является предложение использовать для выбора одной из множества альтернатив не один, а несколько символов входной цепочки. Однако напрямую переложить алгоритм выбора альтернативы для одного символа на такой же алгоритм для цепочки символов не удастся — два соседних символа в цепочке на самом деле могут быть выведены с использованием различных правил грамматики, поэтому неверным будет напрямую искать их в одном правиле. Тем не менее, существует класс грамматик, основанный именно на этом принципе — выборе одной альтернативы из множества возможных на основе нескольких очередных символов в цепочке. Это так называемые LL(k)-грамматики. Правда, алгоритм работы распознавателя для них не так очевидно прост, как рассмотренный выше алгоритм рекурсивного спуска.

Грамматика обладает свойством LL(k), k > 0, если на каждом шаге вывода для однозначного выбора очередной альтернативы МП-автомату достаточно знать символ на верхушке стека и рассмотреть первые k символов от текущего положения считывающей головки во входной цепочке символов.

Грамматика называется LL(k) -грамматикой, если она обладает свойством LL(k) для некоторого k > 0. (Требование k > 0, безусловно, является разумным — для принятия решения о выборе той или иной альтернативы МП-автомату надо рассмотреть хотя бы один символ входной цепочки. Если представить себе LL-грамматику с k = 0, то в такой грамматике вывод совсем не будет зависеть от входной цепочки. В принципе такая грамматика возможна, но в ней будет всего одна-единственная цепочка вывода. Поэтому практическое применение языка, заданного такого рода грамматикой, представляется весьма сомнительным.)

Название «LL(k)» несет определенный смысл. Первая литера «L» происходит от слова «left» и означает, что входная цепочка символов читается в направлении слева направо. Вторая литера «L» также происходит от слова «left» и означает, что при работе распознавателя используется левосторонний вывод. Вместо «k» в названии класса грамматики стоит некоторое число, которое показывает, сколько символов надо рассмотреть, чтобы однозначно выбрать одну из множества альтернатив. Так, существуют LL(1)-грамматики, LL(2)-грамматики и другие классы.

В совокупности все LL(k)-грамматики для всех k>0 образуют класс LL-грамматик.

На рис. 12.1 схематично показано частичное дерево вывода для некоторой LL(k)-грамматики. В нем обозначает уже разобранную часть входной цепочки , которая построена на основе левой части дерева у. Правая часть дерева х — это еще не разобранная часть, а А — текущий нетерминальный символ на верхушке стека МП-автомата. Цепочка х представляет собой незавершенную часть цепочки вывода, содержащую как терминальные, так и нетерминальные символы. После завершения вывода символ А раскрывается в часть входной цепочки , а правая часть дерева х преобразуется в часть входной цепочки . Свойство LL(k) предполагает, что однозначный выбор альтернативы для символа А может быть сделан на основе k первых символов цепочки , являющейся частью входной цепочки .

Алгоритм разбора входных цепочек для LL(k)-грамматики носит название «k-предсказывающего алгоритма». Принципы его выполнения во многом соответствуют функционированию МП-автомата с той разницей, что на каждом шаге работы этот алгоритм может просматривать k символов вперед от текущего положения считывающей головки автомата.

Рис. 12.1. Схема построения дерева вывода для LL(k)-грамматики

Для LL(k)-грамматик известны следующие полезные свойства:

  •  всякая LL(k)-грамматика для любого k>0 является однозначной;
  •  существует алгоритм, позволяющий проверить, является ли заданная грамматика LL(k)-грамматикой для строго определенного числа k.

Кроме того, известно, что все грамматики, допускающие разбор по методу рекурсивного спуска, являются подклассом LL(1) -грамматик. То есть любая грамматика, допускающая разбор по методу рекурсивного спуска, является LL(1)-грамматикой (но не наоборот!).

Есть, однако, неразрешимые проблемы для произвольных КС-грамматик:

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

Это несколько ограничивает применимость LL(k)-грамматик, поскольку не всегда для произвольной КС-грамматики можно очевидно найти число k, для которого она является LL(k)-грамматикой, или узнать, существует ли вообще для нее такое число k.

Для LL(k)-грамматики при k>l совсем не обязательно, чтобы все правые части правил грамматики для каждого нетерминального символа начинались с k различных терминальных символов. Принципы распознавания предложений входного языка такой грамматики накладывают менее жесткие ограничения на правила грамматики, поскольку k соседних символов, по которым однозначно выбирается очередная альтернатива, могут встречаться и в нескольких правилах грамматики (эти условия рассмотрены ниже). Грамматики, у которых все правые части правил для всех нетерминальных символов начинаются с k различных терминальных символов, носят название «сильно LL(k)-грамматик». Метод построения распознавателей для них достаточно прост, алгоритм разбора очевиден, но, к сожалению, такие грамматики встречаются крайне редко.

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

Поскольку все LL(k) -грамматики используют левосторонний нисходящий распознаватель, основанный на алгоритме с подбором альтернатив, очевидно, что они не могут допускать левую рекурсию. Поэтому никакая леворекурсивная грамматика не может быть LL-грамматикой. Следовательно, первым делом при попытке преобразовать грамматику к виду LL-грамматики необходимо устранить в ней левую рекурсию (соответствующий алгоритм был рассмотрен выше).

Класс LL-грамматик широк, но все же он недостаточен для того, чтобы покрыть все возможные синтаксические конструкции в языках программирования (к ним относим все детерминированные КС-языки). Известно, что существуют детерминированные КС-языки, которые не могут быть заданы LL(k)-грамматикой ни для каких k. Однако LL-грамматики удобны для использования, поскольку позволяют построить распознаватели с линейными характеристиками (линейной зависимостью требуемых для работы алгоритма распознавания вычислительных ресурсов от длины входной цепочки символов).

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

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

  •  FIRST(k,) — множество терминальных цепочек, выводимых из (VTVN)*, укороченных до k символов;
  •  FOLLOW(k,A) — множество укороченных до k символов терминальных цепочек, которые могут следовать непосредственно за AVN в цепочках вывода.

Формально эти два множества могут быть определены следующим образом:

FIRST(k, ) = {VT* | либо ||  k и =>*, либо ||>k и а=>*х, x(VT VN)*}, (VTVN)*, k>0.

FOLLOW(k,A) ={VT* | S=>*A и FIRST(,k), VT*}, AVN, k>0.

Очевидно, что если имеется цепочка терминальных символов VT*, то FIRST(k,) — это первые k символов цепочки .

Доказано, что грамматика G(VT,VN,P,S) является LL(k )-грамматикой тогда и только тогда, когда выполняется следующее условие: А->  Р и А-> Р (): FIRST(k,)  FIRST(k, ) = для всех цепочек со таких, что S =>* A.

Иначе говоря, если существуют две цепочки вывода:

S =>* A => z =>* 

S =>* A => t =>* 

то из условия FIRST(k,) = FIRST(k,) следует, что z = t.

На основе этих двух множеств строится алгоритм работы распознавателя для LL(k)-грамматик, который представляет собой k-предсказывающий алгоритм для МП-автомата, заданного так: R({q},VT,Z, ,q,S,{q}), где Z = VTVN, а S - целевой символ грамматики G. Функция переходов автомата строится на основе управляющей таблицы М, которая отображает множество (Z{}) VT*k на множество, состоящее из следующих элементов:

  •  пар вида (, i), где — цепочка символов, помещаемая автоматом на верхушку стека, а i — номер правила вида А->, AVN, Z*;
  •  «выброс»;
  •  «допуск»;
  •  «ошибка».

Конфигурацию распознавателя можно отобразить в виде конфигурации МП-автомата с дополнением цепочки , в которую помещаются номера примененных правил. Поскольку автомат имеет только одно состояние, его в конфигурации можно не указывать. Если считать, что Х — это символ на верхушке стека автомата, — непрочитанная автоматом часть входной цепочки символов, а = FIRST(k,), то работу алгоритма распознавателя можно представить следующим образом:

  •  (, Х, ) (, , i), Z*, если M(X,) = (,i);
  •  (, Х, ) (’, , ), если X = aVT и = a’, M(a,) = «выброс»;
  •  (, , ) — завершение работы, при этом М(, ) = «допуск»;
  •  иначе — «ошибка».

Цепочка = FIRST(k, ) носит в работе автомата название «аванцепочка».

Таким образом, для создания алгоритма распознавателя языка, заданного произвольной LL(k)-грамматикой, достаточно уметь построить управляющую таблицу М. Управляющую таблицу М, а также множества FIRST и FOLLOW можно получить на основе правил исходной грамматики.

При k= 1 все существенно проще. Методы построения для LL(l)-грамматик, а также проверки принадлежности грамматики к классу LL(l)-грамматик рассмотрены ниже.

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

Для LL(l)-грамматик алгоритм работы распознавателя предельно прост. Он заключается всего в двух условиях, проверяемых на шаге выбора альтернативы. Исходными данными для этих условий являются символ aVT, обозреваемый считывающей головкой МП-автомата (текущий символ входной цепочки), и символ AVN, находящийся на верхушке стека автомата.

Эти условия можно сформулировать так:

  •  необходимо выбрать в качестве альтернативы правило А->х, если a  FIRST(l,x);
  •  необходимо выбрать в качестве альтернативы правило А->, если a  FOLLOW(l,A).

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

Работа автомата на шаге «выброса» остается без изменений.

Кроме того, чтобы убедиться, является ли заданная грамматика G(VT,VN,P,S) LL(1)-грамматикой, необходимо и достаточно проверить следующее условие:

для каждого символа AVN, для которого в грамматике существует более одного правила вида А -> 1||…|n, должно выполняться требование

FIRST(l, iFOLLOW(l,A)) FIRST(l, jFOLLOW(l,A)) =  i j, ni>0, nj>0.

Очевидно, что если для символа AVN отсутствует правило вида А->, то согласно этому требованию все множества FIRST(l, 1), FIRST(1, 2), .... FIRST(l, n) должны попарно не пересекаться, если же присутствует правило А->, то они не должны также пересекаться со множеством FOLLOW(1,A). Отсюда видно, что LL(1)-грамматика не может содержать для одного и того же нетерминального символа AVN двух правил, начинающихся с одного и того же терминального символа.

Условие, накладываемое на правила LL(1)-грамматики, является довольно жестким. Очень немногие реальные грамматики могут быть отнесены к классу LL(1)-грамматик. Например, даже довольно простая грамматика G({a},{S}, {S->a|aS}, S) не удовлетворяет этому условию (хотя она является LL(2)-грамматикой и даже регулярной праволинейной грамматикой).

Иногда удается преобразовать правила грамматики так, чтобы они удовлетворяли требованию LL(1)-грамматик. Например, приведенная выше грамматика может быть преобразована к виду G'({a},{S,A}, {S->aA, A-> |S}, S). В такой форме она уже является LL(1)-грамматикой (это можно проверить). Но формального метода преобразовать произвольную КС-грамматику к виду LL(1)-грамматики или убедиться в том, что такое преобразование невозможно, не существует. Первое преобразование правил грамматики, которое можно рекомендовать, — устранение левой рекурсии. Второе преобразование носит название «левая факторизация», оно уже было упомянуто выше при знакомстве с методом рекурсивного спуска. Это преобразование заключается в следующем: если для символа AVN существует ряд правил

A-> a1| a2|… |an|1|2|…|m, i: i(VTVN)*, j: j(VTVN)*, aVT

и ни одна цепочка символов  j не начинается с символа а, тогда во множество нетерминальных символов грамматики VN добавляется новый символ А', а правила для А и А' записываются следующим образом: A-> aA’|1|2|…|m и A’-> 1| 2|… |n. Левую факторизацию можно применять к правилам грамматики несколько раз с целью исключить для каждого нетерминального символа правила, начинающиеся с одних и тех же терминальных символов. Однако применение этих двух преобразований отнюдь не гарантирует, что произвольную КС-грамматику удастся привести к виду LL(1)-грамматики.

Для того чтобы запрограммировать работу МП-автомата, выполняющего разбор входных цепочек символов языка, заданного LL(1)-грамматикой, надо научиться строить множества символов FIRST(l, x) и FOLLOW(1, A). Для множества FIRST(l, x) все очевидно, если цепочка х начинается с терминального символа, если же она начинается с нетерминального символа В (х = By, x  (VTVN)+, y (VTVN)*), то FIRST(l, x) = FIRST(1,B). Следовательно, для LL(1)-грамматик остается только найти алгоритм построения множеств FIRST(1,B) и FOLLOW(1,A) для всех нетерминальных символов A, BVN.

Исходными данными для этих алгоритмов служат правила грамматики.

  1.  Алгоритм построения множества FIRST(1,A)

Алгоритм строит множества FIRST(1,A) сразу для всех нетерминальных символов грамматики G(VT,VN,P,S), AVN. Для выполнения алгоритма надо предварительно преобразовать исходную грамматику G(VT,VN,P,S) в грамматику G'(VT,VN',P',S'), не содержащую -правил (см. алгоритм преобразования в разделе «Преобразование КС-грамматик, Приведенные грамматики», глава 11). На основании полученной грамматики G' и выполняется построение множеств FIRST(l, A) для всех A VN (если A VN, то согласно алгоритму преобразования также справедливо AVN'). Множества строятся методом последовательного приближения. Если в результате преобразования грамматики G в грамматику G' множество VN' содержит новый символ S', то при построении множества FIRST(1, A) он не учитывается.

Алгоритм состоит из нескольких шагов.

Шаг 1. Для всех AVN: FIRST0(l, A) = {X | А->Х  P, X (VTVN), (VTVN)*} (первоначально вносим во множество первых символов для каждого нетерминального символа А все символы, стоящие в начале правых частей правил для этого символа А); i:=0.

Шаг 2. Для всех AVN: FIRSTi+1(1, A) = FIRSTi(1, A)  FIRSTi(1,B), для всех нетерминальных символов B (FIRSTi(l,A)  VN).

Шаг 3. Если  AVN: FIRSTi+1(l,A)  FIRSTi(1,A), то i:=i+l и вернуться к шагу 2, иначе перейти к шагу 4.

Шаг 4. Для всех AVN: FIRST(1,A) = FIRSTi(1,A) \ VN (исключаем из построенных множеств все нетерминальные символы).

  1.  Алгоритм построения множества FOLLOW(1,A)

Алгоритм строит множества FOLLOW(1,A) сразу для всех нетерминальных символов грамматики G(VT,VN,P,S), AVN. Для выполнения алгоритма предварительно надо построить все множества FIRST(1,A),  AVN. Множества строятся методом последовательного приближения. Алгоритм состоит из нескольких шагов.

Шаг 1. Для всех AVN: FOLLOW0(l,A) = {X | В->АХ P, BVN, X (VTVN), (VTVN)*} (первоначально вносим  во множество последующих символов для каждого нетерминального символа А все символы, которые в правых частях правил встречаются непосредственно за символом A); i:=0.

Шаг 2. FOLLOW0(l, S) = FOLLOW0(l, S) {} (вносим пустую цепочку во множество последующих символов для целевого символа S — это означает, что в конце разбора за целевым символом цепочка кончается, иногда для этой цели используется специальный символ конца цепочки: k).

Шаг 3. Для всех AVN: FOLLOWi(1,А) = FOLLOWi(1,A)  FIRST(1,B), для всех нетерминальных символов B (FOLLOWi(l,A)  VN).

Шаг 4. Для всех AVN: FOLLOW"i (1,А) = FOLLOWi(1,A)   FOLLOWi(1,B), для всех нетерминальных символов B (FOLLOW'i(l,A)  VN) и существует правило В->.

Шаг 5. Для всех A VN: FOLLOWi+1(l,A) = FOLLOW"i(1,A)   FOLLOW"i(1,B), для всех нетерминальных символов B VN, если существует правило В->А, (VTVN)*.

Шаг 6. Если  AVN: FOLLOWi+1(l,A)  FOLLOWi(1,A), то i:=i+l и вернуться к шагу 3, иначе перейти к шагу 7.

Шаг 7. Для всех AVN: FOLLOW(l, A) = FOLLOWi(1,A) \ VN (исключаем из построенных множеств все нетерминальные символы).

  1.  Восходящие распознаватели КС-языков без возвратов
    1.  Определение LR(k)-грамматики

Восходящие распознаватели выполняют построение дерева вывода снизу вверх. Результатом их работы является правосторонний вывод. Функционирование таких распознавателей основано на модификациях алгоритма «сдвиг-свертка» (или «перенос-свертка»).

Идея состоит в том, чтобы модифицировать этот алгоритм таким образом, чтобы на каждом шаге его работы можно было однозначно дать ответ на следующие вопросы:

что следует выполнять: сдвиг (перенос) или свертку;

какую цепочку символов  выбрать из стека для выполнения свертки;

какое правило выбрать для выполнения свертки (в том случае, если существует несколько правил вида A1->, A2->, … An->).

Тогда восходящий алгоритм распознавания цепочек КС-языка не требовал бы выполнения возвратов, поскольку сам язык мог бы быть задан детерминированным расширенным МП-автоматом. Конечно, как уже было сказано, это нельзя сделать в общем случае, для всех КС-языков (поскольку даже сам класс детерминированных КС-языков более узкий, чем весь класс КС-языков). Но, вероятно, среди всех КС-языков можно выделить такой класс (или классы), для которых подобная реализация распознающего алгоритма станет возможной.

В первую очередь можно использовать тот же самый подход, который был положен в основу определения LL(k)-грамматик. Тогда мы получим другой класс КС-грамматик, который носит название LR(k)-грамматик.

КС-грамматика обладает свойством LR(k), k  0, если на каждом шаге вывода для однозначного решения вопроса о выполняемом действии в алгоритме «сдвиг-свертка» («перенос-свертка») расширенному МП-автомату достаточно знать содержимое верхней части стека и рассмотреть первые k символов от текущего положения считывающей головки автомата во входной цепочке символов.

Грамматика называется LR(k)-грамматикой, если она обладает свойством LR(k) для некоторого k0.

Название «LR(k)», как и рассмотренное выше «LL(k)», также несет определенный смысл. Первая литера «L» также обозначает порядок чтения входной цепочки символов: слева— направо. Вторая литера «R» происходит от слова «right» и, по аналогии с LL(k), означает, что в результате работы распознавателя получается правосторонний вывод. Вместо «k» в названии грамматики стоит число, которое показывает, сколько символов входной цепочки надо рассмотреть, чтобы принять решение о действии на каждом шаге алгоритма «сдвиг-свертка». Так, существуют LR(0)-грамматики, LR(1)-грамматики и другие классы.

В совокупности все LR(k)-грамматики для всех k  0 образуют класс LR-грамматик.

На рис. 12.4 схематично показано частичное дерево вывода для некоторой LR(k)-грамматики. В нем обозначает уже разобранную часть входной цепочки , на основе которой построена левая часть дерева . Правая часть дерева х — это еще не разобранная часть, а А — это нетерминальный символ, к которому на очередном шаге будет свернута цепочка символов z, находящаяся на верхушке стека МП-автомата. В эту цепочку уже входит прочитанная, но еще не разобранная часть входной цепочки . Правая часть дерева х будет построена на основе части входной цепочки . Свойство LR(k) предполагает, что однозначный выбор действия, выполняемого на каждом шаге алгоритма «сдвиг-свертка», может быть сделан на основе цепочки и k первых символов цепочки , являющихся частью входной цепочки . Этим очередным действием может быть свертка цепочки z к символу А или перенос первого символа из цепочки и добавление его к цепочке z.

Рис. 12.4. Схема построения дерева вывода для LR(k)-грамматики

Рассмотрев схему построения дерева вывода для LR(k)-грамматики на рис. 12.4 и сравнив ее с приведенной выше на рис. 12.1 схемой для LL(k)-грамматики, можно предположить, что класс LR-грамматик является более широким, чем класс LL-грамматик. Основанием для такого предположения служит тот факт что на каждом шаге работы распознавателя LR(k)-грамматики обрабатывается больше информации, чем на шаге работы распознавателя LL(k)-грамматики. Действительно, для принятия решения на каждом шаге алгоритма распознавания LL(k)-грамматики используются первые k символов из цепочки , а для принятия решения на шаге распознавания LR(k)-грамматики — вся цепочка и еще первые k символов из цепочки . Очевидно, что во втором случае можно проанализировать больший объем информации и, таким образом, построить вывод для более широкого класса КС-языков.

Для LR(k)-грамматик известны следующие полезные свойства:

  •  всякая LR(k)-грамматика для любого k >= 0 является однозначной;
  •  существует алгоритм, позволяющий проверить, является ли заданная грамматика LR(k)-грамматикой для строго определенного числа k.

Есть, однако, неразрешимые проблемы для произвольных КС-грамматик (они аналогичны таким же проблемам для других классов КС-грамматик):

  •  не существует алгоритма, который бы мог проверить, является ли заданная КС-грамматика LR(k)-грамматикой для некоторого произвольного числа k;
  •  не существует алгоритма, который бы мог преобразовать (или доказать, что преобразование невозможно) произвольную КС-грамматику к виду LR(k)-грамматики для некоторого k.

Кроме того, для LR-грамматик доказано еще одно очень интересное свойство — класс LR-грамматик полностью совпадает с классом детерминированных КС-языков. То есть, во-первых, любая LR(k)-грамматика задает детерминированный КС-язык (это очевидно следует из однозначности всех LR-грамматик), а во-вторых, для любого детерминированного КС-языка можно построить LR-грамматику, задающую этот язык.

В принципе класс LR-грамматик очень удобен для построения распознавателей детерминированных КС-языков (а все языки программирования, безусловно, относятся к этому классу). Но тот факт, что для каждого детерминированного КС-языка существует задающая его LR-грамматика, еще ни о чем не говорит, поскольку из-за неразрешимости проблемы преобразования отсутствует алгоритм, который позволил бы эту грамматику построить всегда. Данный детерминированный КС-язык может быть изначально задан грамматикой, которая не относится к классу LR-грамматик. В таком случае совсем не очевидно, что для этого языка удастся построить распознаватель на основе LR-грамматики, потому что в общем случае нет алгоритма, который бы позволил эту грамматику получить, хотя и известно, что она существует. То, что проблема не разрешима в общем случае, совсем не означает, что ее не удастся решить в конкретной ситуации. И здесь факт существования LR-грамматики для каждого детерминированного КС-языка играет важную роль — всегда есть смысл в каждом конкретном случае пытаться построить такую грамматику.

  1.  Принципы построения распознавателей для LR(k)-грамматик

Для того чтобы формально определить LR(k) свойство для КС-грамматик, введем понятие пополненной КС-грамматики. Грамматика G' является пополненной грамматикой, построенной на основании исходной грамматики G(VT,VN,P,S), если выполняются следующие условия:

  •  грамматика G' совпадает с грамматикой G, если целевой символ S не встречается нигде в правых частях правил грамматики G;
  •  грамматика G' строится как грамматика G'(VT,VN{S'},P{S'->S},S'), если целевой символ S встречается в правой части хотя бы одного правила из множества Р в исходной грамматике G.

Фактически пополненная КС-грамматика строится таким образом, чтобы ее целевой символ не встречался в правой части ни одного правила. Если нужно, то в исходную грамматику G для этого добавляется новый терминальный символ S', который становится целевым символом, и новое правило S'->S. Очевидно, что пополненная грамматика G' эквивалентна исходной грамматике G, то есть L(G’) = L(G).

Теперь рассмотрим формальное определение LR(k) свойства.

Если для произвольной КС-грамматики G в ее пополненной грамматике G' для двух произвольных цепочек вывода из условий:

1. S' =>* aAw => w

2. S' =>* Вх => у

3. FIRST(k, w) = FIRST(k, y)

следует, что Aw = Bx (то есть = , А = В и х = у), то доказано, что грамматика G обладает LR(k) свойством. Очевидно, что тогда и пополненная грамматика G' также обладает LR(k) свойством.

  1.  Грамматики предшествования (основные принципы)

Еще одним распространенным классом КС-грамматик, для которых возможно построить восходящий распознаватель без возвратов, являются грамматики предшествования. Так же как и распознаватель рассмотренных выше LR-грамматик, распознаватель для грамматик предшествования строится на основе алгоритма «сдвиг-свертка» («перенос-свертка.

Принцип организации распознавателя входных цепочек языка, заданного грамматикой предшествования, основывается на том, что для каждой упорядоченной пары символов в грамматике устанавливается некоторое отношение, называемое отношением предшествования. В процессе разбора входной цепочки расширенный МП-автомат сравнивает текущий символ входной цепочки с одним из символов, находящихся на верхушке стека автомата. В процессе сравнения проверяется, какое из возможных отношений предшествования существует между этими двумя символами. В зависимости от найденного отношения выполняется либо сдвиг (перенос), либо свертка. При отсутствии отношения предшествования между символами алгоритм сигнализирует об ошибке.

Задача заключается в том, чтобы иметь возможность непротиворечивым образом определить отношения предшествования между символами грамматики. Если это возможно, то грамматика может быть отнесена к одному из классов грамматик предшествования.

Существует несколько видов грамматик предшествования. Они различаются по тому, какие отношения предшествования в них определены и между какими типами символов (терминальными или нетерминальными) могут быть установлены эти отношения. Кроме того, возможны незначительные модификации функционирования самого алгоритма «сдвиг-свертка» в распознавателях для таких грамматик (в основном на этапе выбора правила для выполнения свертки, когда возможны неоднозначности).

Выделяют следующие виды грамматик предшествования:

  •  простого предшествования;
  •  расширенного предшествования;
  •  слабого предшествования;
  •  смешанной стратегии предшествования;
  •  операторного предшествования.

Далее будут рассмотрены два наиболее простых и распространенных типа — грамматики простого и операторного предшествования.

  1.  Грамматики простого предшествования

Грамматикой простого предшествования называют такую приведенную КС-грамматику  (КС-грамматика называется приведенной, если она не содержит циклов, бесплодных и недостижимых символов и -правил) G(VN,VT,P,S), V = VTVN, в которой:

1. Для каждой упорядоченной пары терминальных и нетерминальных символов выполняется не более чем одно из трех отношений предшествования:

  •  Вi =• Bj ( Bi, Bj V), если и только если правило             A->xBiBjy  P, где AVN, x,y V*;
  •  Вi <• Bj ( Bi, Bj V), если и только если   правило            A->xBiDy  P и вывод D=>*Sjz, где A,D VN, x,y,z V*;
  •  Вi •> Bj ( Bi, Bj V), если и только если   правило            A->xCBjy  P и вывод C=>*zBi, или   правило A->xCDy  P и выводы C=>*zBi, и D=>*Bjw, где A,C,D  VN, x,y,z,w  V*.

2. Различные правила в грамматике имеют разные правые части (то есть в грамматике не должно быть двух различных правил с одной и той же правой частью).

Отношения  =•, <• и •> называют отношениями предшествования для символов. Отношение предшествования единственно для каждой упорядоченной пары символов. При этом между какими-либо двумя символами может и не быть отношения предшествования — это значит, что они не могут находиться рядом ни в одном элементе разбора синтаксически правильной цепочки. Отношения предшествования зависят от порядка, в котором стоят символы, и в этом смысле их нельзя путать со знаками математических операций - они не обладают ни свойством коммутативности, ни свойством ассоциативности. Например, если известно, что Bi •> Bj, то не обязательно выполняется Bj <• Вi, (поэтому знаки предшествования иногда помечают специальной точкой: -•, <•, •>).

Для грамматик простого предшествования известны следующие полезные свойства:

  •  всякая грамматика простого предшествования является однозначной;
  •  легко проверить, является или нет произвольная КС-грамматика грамматикой простого предшествования (для этого достаточно проверить рассмотренные выше свойства грамматик простого предшествования или воспользоваться алгоритмом построения матрицы предшествования, который рассмотрен далее).

Метод предшествования основан на том факте, что отношения предшествования между двумя соседними символами распознаваемой строки соответствуют трем следующим вариантам:

  •  Вi <• Вi+1, если символ Bi+1 — крайний левый символ некоторой основы (это отношение между символами можно назвать «предшествует основе» или просто «предшествует»);
  •  Вi •> Bi+1, если символ Вi — крайний правый символ некоторой основы (это отношение между символами можно назвать «следует за основой» или просто «следует»);
  •  Вi =• Bi+1, если символы Вi, и Bi+1 принадлежат одной основе (это отношение между символами можно назвать «составляют основу»).

Исходя из этих соотношений, выполняется разбор строки для грамматики предшествования.

Суть принципа такого разбора можно пояснить на рис. 12.5. На нем изображена входная цепочка символов  в тот момент, когда выполняется свертка цепочки . Символ а является последним символом подцепочки , а символ b первым символом подцепочки . Тогда, если в грамматике удастся установить непротиворечивые отношения предшествования, то в процессе выполнения разбора по алгоритму «сдвиг-свертка» можно всегда выполнять сдвиг до тех пор, пока между символом на верхушке стека и текущим символом входной цепочки существует отношение <• или =•. А как только между этими символами будет обнаружено отношение •>, так сразу надо выполнять свертку. Причем для выполнения свертки из стека надо выбирать все символы, связанные отношением =. То, что все различные правила в грамматике предшествования имеют различные правые части, гарантирует непротиворечивость выбора правила при выполнении свертки.

Таким образом, установление непротиворечивых отношений предшествования между символами грамматики в комплексе с несовпадающими правыми частями различных правил дает ответы на все вопросы, которые надо решить для организации работы алгоритма «сдвиг-свертка» без возвратов.

Рис. 12.5. Отношения между символами входной цепочки в грамматике предшествования

На основании отношений предшествования строят матрицу предшествования грамматики. Строки матрицы предшествования помечаются первыми (левыми) символами, столбцы — вторыми (правыми) символами отношений предшествования. В клетки матрицы на пересечении соответствующих столбца и строки помещаются знаки отношений. При этом пустые клетки матрицы говорят о том, что между данными символами нет ни одного отношения предшествования.

Матрицу предшествования грамматики сложно построить, опираясь непосредственно на определения отношений предшествования. Удобнее воспользоваться двумя дополнительными множествами — множеством крайних левых и множеством крайних правых символов относительно нетерминальных символов грамматики G(VN,VT,P,S), V = VTVN. Эти множества определяются следующим образом:

  •  L(A) = {X |  A=>*Xz}, AVN, XV, zV* — множество крайних левых символов относительно нетерминального символа А (цепочка z может быть и пустой цепочкой);
  •  R(A) = {X |  A=>*zX}, AVN, XV, zV* — множество крайних правых символов относительно нетерминального символа А.

Иными словами, множество крайних левых символов относительно нетерминального символа А — это множество всех крайних левых символов в цепочках, которые могут быть выведены из символа А. Аналогично, множество крайних правых символов относительно нетерминального символа А — это множество всех крайних правых символов в цепочках, которые могут быть выведены из символа А.

Тогда отношения предшествования можно определить так:

  •  Вi =• Bj ( Bi, Bj V), если правило A->xBiBjy  P, где AVN, x,y V*;
  •  Вi <• Bj ( Bi, Bj V), если правило A->xBiDy  P  и            Bj L(D), где A,D VN, x,y V*;
  •  Вi •> Bj ( Bi, Bj V), если правило A->xCBjy  P и  Bi R(С) или   правило A->xCDy  P и Bi R(С), Bj L(D), где            A,С, D VN, x,y V*;

Такое определение отношений удобнее на практике, так как не требует построения выводов, а множества L(A) и R(A) могут быть построены для каждого нетерминального символа AVN грамматики G(VN,VT,P,S), V = VTVN по очень простому алгоритму.

Шаг 1. AVN:

R0(A) = {X | A->yX, XV, yeV*}, L0(A) = {X | A->Xy, XV, yV*}, i := 1.

Для каждого нетерминального символа А ищем все правила, содержащие А в левой части. Во множество L(A) включаем самый левый символ из правой части правил, а во множество R(A) — самый крайний правый символ из правой части. Переходим к шагу 2.

Шаг 2.  AVN:

Ri(А) - Ri-1(A) Ri-1(B),  В (Ri-1(A) VN),

Li(A) = Li-1(A) Li-1(B), B (Li-1(A) VN).

Для каждого нетерминального символа А: если множество L(A) содержит нетерминальные символы грамматики А', А", ..., то его надо дополнить символами, входящими в соответствующие множества L(A'), L(A"), ... и не входящими в L(A). Ту же операцию надо выполнить для R(A).

Шаг 3. Если  AVN: Ri(A)  Ri-1(A) или Li(A) Li-1(A), то i:=i+l и вернуться к шагу 2, иначе построение закончено: R(A) = Ri(A) и L(A) = Li(A).

Если на предыдущем шаге хотя бы одно множество L(A) или R(A) для некоторого символа грамматики изменилось, то надо вернуться к шагу 2, иначе построение закончено.

После построения множеств L(A) и R(A) по правилам грамматики создается матрица предшествования. Матрицу предшествования дополняют символами н и к (начало и конец цепочки). Для них определены следующие отношения предшествования:

н<• X,  aV, если  S=>*Xy, где SVN, yV* или (с другой стороны) если XL(S);

к  >X,  aV, если  S=>*yX, где SVN, y V* или (с другой стороны) если XR(S).

Здесь S — целевой символ грамматики.

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




1. Тема- Налогообложение строительных организаций Выполнила студентка гр
2. кинорежиссер родился 18 июня 1902 года
3. Електричне освітлення і опромінення Спеціальність 5
4.  ОПРЕДЕЛЕНИЕ ПЕРИОДИЧНОСТИ ТО И КР Периодичность ежедневного обслуживания ЕОс равна среднесуточн
5. Руководством по борьбе с пылью в угольных шахтах и Инструкцией по определению запыленности воздуха в рудни
6. Брендинг и маркетинговые коммуникации
7. тема 7 Назначение наказания
8. Синтез системы автоматического регулирования радиального перемещения каретки
9. Тема- Принцип ldquo;невидимой рукиrdquo; Выполнила- студентка I курса 6 группыочнозаочной формы об
10. Два основных понимания всемирной истории- унитарно-стадиальное и плюрально-циклическое
11. Свободные цены ' это- договорные лимитирующие регулируемые фиксированные 2
12. Процессуальные особенности рассмотрения и разрешения дел о взыскании алиментов
13. темам в которых присутствует субъект он сам себе может ставить цель и сам пытается ее достичь
14. Сколько ведется журналов поездных телефонограмм на этой станции для каждого примыкающего перегона э1
15. Классификация программ
16. 36 20090731 Роберт Орешник ПИЯВКА комедия в 2 действиях место де
17. энергетический и сырьевой придаток Запада обрекающую на ускоренное вымирание более половины населения стр
18. Курсовая работа- Роль психологических аспектов педагогического процесса в формировании будущего специалиста
19. Плавлення й кристалізація Демонстрації 6 хв 1
20. Введение4