G1-2

advertisement
Лекция 1. 7.09.09
Введение
В прежние времена под языками подразумевалось исключительно
средство общения между людьми, т.е. имелись в виду только естественные
языки – русский, немецкий, английский и пр. В начале ХХ века это
представление претерпело серьёзные изменения и в настоящее время под
языком понимается всякое средство общения, состоящее из знаковой
системы, множества смыслов этой системы и имеющее установленное
соответствие между последовательностями знаков и смыслами. Особенно
широкий (и все более увеличивающийся) класс составляют языки
программирования, изучению которых и посвящен наш курс.
Стремительное развитие вычислительной техники сделало
возможной компьютерную обработку текстов, относящихся к самым
различным языкам – естественным языкам, языкам формул, языкам
программирования.
Существует великое множество программных средств такой
обработки. Это и различные редакторы с самыми разными возможностями,
и архиваторы, и трансляторы для языков программирования или
программы-переводчики для естественных языков.
При обработке текстов возникает ряд задач. Это задачи, связанные с
проблемой задания языка, или генерации его цепочек, задачи определения
принадлежности некоторого множества слов заданному языку, задачи
идентификации цепочек. Для их решения разработано множество методов,
причем некоторые из них работают только с определенным классом
языков (например, с языками программирования), другие же являются
универсальными.
В нашем курсе будут рассматриваться различные методы решения
задач генерации и распознавания цепочек языка, основные элементы
теории перевода текстов с одного языка на другой, схема трансляции
программ и основные этапы трансляции.
1
Глава 1 Формальные языки и грамматики
1.1 Языки и цепочки символов
1.1.1 Цепочки символов и операции над ними
Сначала дадим все необходимые определения и введём некоторые
понятия. Большинство из них достаточно очевидны, но всё же
рекомендуется с ними ознакомиться.
 Под алфавитом  будем понимать непустое конечное множество
символов.
 Цепочка, или строка – это последовательность символов некоторого
алфавита, при этом символы в строке могут повторяться. Для цепочки
важен её состав, порядок символов и их количество.
 Длиной цепочки является количество символов в ней. Например,
цепочка x = ’abcab’ имеет длину |x| = 5.
 Две цепочки  и  совпадают (равны): =, если они имеют один и тот
же состав и порядок символов и их количество. Например, =’aabc’;
=‘abac‘ – цепочки различны, т.к. порядок символов различен, хотя состав
и количество символов в этих цепочках совпадают.
 Цепочка, не содержащая символов, называется пустой цепочкой,
обозначается через  или .
 Любая последовательно взятая часть цепочки является её подцепочкой,
для цепочки ‘xy’ подцепочка ‘x’ является префиксом, подцепочка ‘y’ –
суффиксом. Следует заметить, что пустая цепочка является как
суффиксом, так и префиксом любой цепочки. Сама цепочка также является
своим префиксом и суффиксом. Собственный суффикс (префикс) – это
суффикс (префикс), не совпадающий со всей цепочкой. Например, для
цепочки =’aabc’ собственными префиксами будут являться цепочки ’a’,
’aa’, ’aab’; собственными суффиксами – цепочки ’c’, ’bc’, ’abc’. Цепочки
’ab’ и ’b’ – просто подцепочки исходной цепочки .
Над цепочками можно выделить следующие операции:
 Конкатенацией, или сцеплением (сложением) цепочек, называется
строка, полученная их объединением, например: x = ’abcab’, y = ’cde’,
тогда конкатенация цепочек x и y будет иметь вид z = x·y = ’abcabcde’.
Свойства: Так как в цепочке важен порядок символов, очевидно, что
операция сложения не коммутативна, т.е. в общем случае  ,.
Конкатенация ассоциативна, т.е. ,, () = ().
 Обращение цепочки x – это запись её символов в обратном порядке,
обозначается xR. Например, для цепочки x = ’abcab’ её обращение
xR = ’bacba’. Свойство операции обращения: , ()R= RR.
 Итерация цепочки n раз – это её сцепление (повторение) n раз,
2
обозначается n. Например, если =’ab’, то 3 = ’ababab’.
Для пустой цепочки  справедливы следующие равенства:
1. || = 0;
2. :  =  = ;
3. R = ;
4. n0: n = ;
5. : 0 = .
1.1.2 Понятие языка. Способы задания языков
В общем случае язык – это заданный набор символов и правил,
устанавливающих способы комбинации этих символов между собой для
записи осмысленных текстов. Основой любого языка является алфавит,
определяющий набор допустимых символов.
 Цепочка символов  является цепочкой над алфавитом Σ: (Σ), если в
неё входят только символы этого алфавита.
 Если  – некоторый алфавит, то:
 + – множество всех цепочек над алфавитом  без пустой цепочки .
 * – множество всех цепочек над алфавитом , включая .
 Языком L над алфавитом Σ: L(Σ) – называют некоторое счётное
подмножество цепочек конечной длины из множества всех цепочек над
этим алфавитом. Множество цепочек языка не обязано быть конечным и
каждая цепочка может иметь сколь угодно большую длину.
Большая часть следующих утверждений основана на теории множеств.
 Для любого языка L(Σ) справедливо L(Σ)  Σ*.
 Язык L(Σ) включает в себя язык L(Σ): L(Σ)  L(Σ), если L(Σ)
L(Σ). Легко видеть, что это обычное определение включения множеств.
 Два языка совпадают (эквивалентны): L(Σ) = L(Σ), если L(Σ)  L(Σ) и
L(Σ)  L(Σ). И это определение тоже соответствует понятию равенства
множеств.
 Конкатенацией (объединением) языков L1 и L2 называют язык L,
состоящий из всевозможных сцеплений цепочек языков L1 и L2:
L = L1·L2 = {x·y | x  L1, y  L2}.
 Замыкание Клини, или итерация языка L, обозначается L* и
определяется рекурсивно: 1) L0 = {λ}, 2) Ln = L·Ln-1 для n > 0, 3) L* =  Ln
для всех n  0.
 L+ = L*\{λ}.
 Язык L обладает суффиксным (префиксным) свойством, если никакая
цепочка языка не является суффиксом (префиксом) другой цепочки.
Примеры
1. Для языка L={01,11} замыканием Клини будет бесконечное
3
множество цепочек вида L*={λ,01,11,0101,1111,0111,1101, 010101,
010111, 011101, 110101, 110111, 111101, 011111, 111111, …}.
2. Язык L={01,010,111,100} не обладает префиксным свойством,
т.к. цепочка ‘01’ является префиксом цепочки ‘010’. В то же время он
обладает суффиксным свойством, т.к. в нём нет ни одной цепочки,
которая была бы суффиксом какой-то другой цепочки.
Язык состоит из бесконечного множества цепочек символов над
некоторым алфавитом, или слов. Но не любая цепочка символов над этим
алфавитом принадлежит языку, т.к. существуют правила построения
допустимых цепочек для каждого конкретного языка. Указать эти правила
– значит задать язык. Существуют три основных способа задания языка:
1. Перечисление всех допустимых цепочек языка (способ формальный, на
практике нереализуем, т.к. в общем случае множество цепочек языка
бесконечно и перечислить их невозможно).
2. Указание способа порождения цепочек языка (задание грамматики
языка) – применение т.н. генератора.
3. Задание метода распознавания цепочек языка – использование
распознавателя.
Генераторы и распознаватели являются основными инструментами
задания бесконечного языка конечными средствами. Существует
определенная классификация языков по их типу, и для каждого класса
языков имеются эквивалентные способы задания с помощью генераторов и
распознавателей. Далее рассмотрим эти способы более подробно.
В любом языке можно выделить его синтаксис и семантику. Кроме
того, трансляторы имеют дело также с лексическими конструкциями
(лексемами), которые задаются лексикой языка. Дадим определения этих
понятий.
Синтаксис языка – это набор правил, определяющий допустимые
конструкции языка. Чаще всего синтаксис языка можно задать в виде
строгого набора правил, но полностью это справедливо только для
формальных языков.
Семантика языка – это раздел, определяющий значение
предложений языка. Семантика определяет «содержание языка», т.е. задаёт
значение всех допустимых цепочек языка. Для большинства языков
семантика определяется неформальными методами.
Лексика – это словарный запас языка. Лексическая единица
(лексема) – конструкция, которая состоит из элементов алфавита языка и
не содержит в себе других конструкций. Т.е. лексема может содержать в
себе только элементарные символы алфавита и не может содержать других
лексем. Например, лексемами русского языка являются слова, а пробелы и
знаки препинания представляют собой разделители. Лексемами алгебры
являются числа, знаки математических операций, обозначения функций и
4
т.п. В языках программирования лексемы – это ключевые слова,
идентификаторы, константы, метки, знаки операций.
1.1.3 Особенности языков программирования
Языки программирования занимают промежуточное место между
формальными и естественными языками. С формальными языками их
объединяют строгие синтаксические правила, на основе которых строятся
предложения языка. От естественных языков в языки программирования
перешли лексические единицы, представляющие основные ключевые
слова (чаще это английские слова, но бывают и слова других языков).
Для задания языка программирования необходимо решить три
основных вопроса:
1. определить множество допустимых символов языка;
2. определить множество правильных программ языка;
3. задать смысл каждой правильной программы.
С помощью теории формальных языков удается решить (полностью
или частично) только первые два вопроса.
Первый вопрос решается легко. Алфавит языка и представляет собой
множество его допустимых символов. Для языков программирования это,
как правило, тот набор символов, который можно ввести с клавиатуры.
Второй вопрос в теории формальных языков решается только
частично. Для всех языков программирования существуют синтаксические
правила, но их недостаточно для строгого определения всех возможных
синтаксических
конструкций.
Дополнительные
ограничения
накладываются семантикой языка и неформально оговариваются для
каждого отдельного языка программирования. Это, например,
необходимость предварительного описания переменных и функций,
необходимость соответствия типов переменных и констант в выражениях,
формальных и фактических параметров в описаниях и вызовах процедур и
функций, и т.п.
Отсюда следует, что практически все языки программирования,
строго говоря, не являются формальными языками. Поэтому во всех
трансляторах, кроме синтаксического разбора и анализа предложений
языка, дополнительно предусмотрен семантический анализ.
Третий вопрос вообще не относится к теории формальных языков.
Для ответа на него нужно использовать другие подходы, например:
1. изложить
смысл
программы,
написанной
на
языке
программирования, на другом языке, более понятном тому, кому
адресована программа;
2. использовать для проверки смысла некоторую «идеальную
машину», которая предназначена для проверки программ, написанных на
5
данном языке.
Любой разработчик программ так или иначе использует первый
подход – это комментарии в программе, построение её блок-схемы,
разработка документации или любое другое описание алгоритма. Все эти
способы ориентированы на человека, но универсального способа
проверить, насколько описание в действительности соответствует
программе, пока не существует. Для изложения программы на языке,
понятном машине – в машинных командах – существуют трансляторы.
Второй подход используется при отладке программы; оценку
результатов выполнения программы при отладке осуществляет человек.
Разработчикам компиляторов так или иначе приходится решать
вопрос о смысле программ.
Во-первых, компилятору для преобразования исходной программы в
последовательность машинных команд необходимо иметь представление о
том, какая последовательность команд должна соответствовать той или
иной части программы. Обычно такие последовательности сопоставляют
базовым конструкциям входного языка – используется первый подход к
изложению смысла программы.
Во-вторых, многие современные компиляторы позволяют выявить
сомнительные с точки зрения смысла места в исходной программе –
например, недостижимые операторы, неиспользуемые переменные,
неопределенные результаты функций и т.п. Обычно компилятор указывает
такие места в виде предупреждений, на которые разработчик может
обращать или не обращать внимание. Для этого компилятор должен иметь
представление о том, как программа будет выполняться, т.е. используется
второй подход.
Но в любом случае осмысление исходной программы закладывает в
компилятор его создатель, который руководствуется неформальными
методами – как правило, описанием входного языка. В теории формальных
языков вопрос о смысле программ не решается.
Итак, возможности трансляторов по проверке осмысленности
исходной программы практически равны нулю, поэтому семантические
ошибки остаются на совести авторов программ.
Выше, когда шла речь о различных способах задания языков, как
наиболее реальные были выделены второй и третий способ, а именно:
задание языка с помощью генераторов и распознавателей. В качестве
генераторов в общем случае могут использоваться грамматики, а для более
узкого класса языков – регулярные выражения. В качестве
распознавателей, как правило, применяются автоматы различных типов (в
зависимости от класса языка). В первой главе будет рассмотрен способ
задания языков с помощью грамматик.
6
1.2 Определение грамматики
1.2.1 Понятие грамматики и формальное определение.
Форма Бэкуса-Наура
Грамматика в общем представляет собой описание способа построения
цепочек языка. Например, для естественных языков это набор правил
построения различных выражений и предложений. Для многих языков (в том
числе
для
языков
программирования)
возможно
использовать
формализованное описание – некую математическую систему, описывающую
язык. Грамматика задаёт правила порождения цепочек языка, следовательно,
является генератором – реализует второй способ задания языка.
Формальное описание грамматики построено на основе системы правил,
или продукций. Правило представляет собой упорядоченную пару цепочек
символов (,), которую обычно записывают как    и читают как «
порождает ».
Грамматика языка программирования содержит правила двух типов:
первые (определяющие синтаксические конструкции языка) легко поддаются
формальному описанию; вторые (определяющие семантические ограничения
языка) обычно излагаются в неформальном виде. Поэтому любое описание
языка программирования обычно состоит из двух частей: сначала формально
излагаются правила построения синтаксических конструкций, затем на
естественном языке даётся описание семантических правил. Для компилятора
семантические ограничения должны быть представлены в виде алгоритмов
проверки правильности программы.
Замечание: Говоря далее о грамматиках языков программирования,
будем иметь в виду только правила построения синтаксических конструкций
языка.
Формально грамматика G определяется как четвёрка G(VT,VN,P,S), где
VT – множество терминальных символов (символы алфавита языка);
VN – множество нетерминальных символов (вспомогательные символы, ими
могут быть слова, понятия, конструкции языка); причём множества
терминальных и нетерминальных символов не пересекаются: VTVN=;
V=VTVN – полный алфавит грамматики G; P – множество правил вида
  , где V+, V*; SVN – начальный (целевой) символ грамматики G,
с которого начинается процесс порождения цепочек языка.
Нетерминальные символы участвуют в процессе порождения цепочек
языка, но в окончательном виде цепочки языка состоят только из символов
терминального алфавита.
В зависимости от вида правил грамматики классифицируются по типам.
Эту классификацию рассмотрим далее. Но существуют некоторые общие
принципы построения правил любой грамматики:
1. Каждый нетерминальный символ может встречаться в цепочках как
левой, так и правой частей правил, но он обязан быть хотя бы один раз в
7
левой части хотя бы одного правила.
2. В левой части каждого из правил грамматики есть хотя бы один
нетерминальный символ.
Во множестве правил грамматики может быть несколько правил,
имеющих одинаковые левые части, например: 1, 2, …, n. Тогда
эти правила записываются в одну строку: 12…n.
Рассмотренная форма записи правил грамматики называется формой
Бэкуса-Наура. В ней, как правило, нетерминальные символы заключаются в
угловые скобки <>.
Пример: грамматика порождения целых десятичных чисел со знаком.
G({0,1,2,3,4,5,6,7,8,9,–,+}, {<число>,<чс>,<цифра>}, P, <число>).
P:
<число>  <чс>+<чс>–<чс>
<чс>  <цифра><чс><цифра>
<цифра>  0123456789
В данной грамматике множество нетерминальных символов содержит 3
элемента (символы <число>,<чс>,<цифра>), множество терминальных
символов – 12 элементов (10 цифр и два знака), множество P – 15 правил,
записанных в три строки. Начальный символ грамматики – <число>.
В грамматике можно изменить названия нетерминальных символов без
какого-то изменения языка, заданного ею. Терминальные символы изменять
нельзя! Они соответствуют алфавиту задаваемого языка. При изменении
множества терминальных символов изменится и алфавит языка.
Рассмотренную в примере грамматику для целых десятичных чисел со
знаком можно переписать в виде:
G({0,1,2,3,4,5,6,7,8,9,–,+}, {S,T,F}, P, S).
P:
S  T+T–T
T  FTF
F  0123456789.
Формальные грамматики определяют бесконечное множество цепочек
языка с помощью конечного набора правил. Это становится возможным
благодаря использованию рекурсивных правил, в которых нетерминал
выражается сам через себя. Рекурсия может быть непосредственной (явной),
когда символ определяется сам через себя в одном правиле; или косвенной
(неявной), когда переопределение происходит через цепочку правил.
В грамматиках G и G явная рекурсия присутствует во второй части
второго правила: <чс>  <чс><цифра>, T  TF.
Чтобы рекурсия не была бесконечной, участвующий в ней
нетерминальный символ должен обязательно присутствовать в левой части
другого правила, где он определяется, минуя самого себя. В грамматиках G и
G это достигается в первой части второго правила: <чс>  <цифра>, T  F.
Явно или неявно, рекурсия всегда присутствует в грамматиках любых
реальных языков программирования. Как правило, в грамматиках реального
языка программирования присутствует не одно, а множество правил,
построенных с помощью рекурсии.
8
Лекция 2. 10.09.09
1.2.2 Другие способы задания грамматик
Кроме формы Бэкуса-Наура, существуют и другие способы записи
правил грамматик, а именно запись с использованием метасимволов и
графическое представление.
Запись правил грамматик с использованием метасимволов
предполагает, что в строке правила могут присутствовать особые символы
– т.н. метасимволы, которые трактуются специальным образом. Обычно в
этой роли используются () круглые, [] квадратные и {} фигурные скобки, а
также «,» запятые и «” ”»кавычки. Они имеют следующий смысл:
 ( ) – в данном месте правила может стоять только одна из всех
перечисленных внутри них цепочек;
 [ ] – указанная внутри них цепочка может быть в правиле грамматики
один раз или ни одного раза;
 {} – указанная внутри них цепочка может встречаться любое
количество раз (в том числе сколь угодно много или ни одного);
 , – разделяет цепочки символов внутри круглых скобок;
 “ ” – используются в том случае, когда какой-то из метасимволов
нужно включить в цепочку символов языка.
Правила рассмотренной выше грамматики G порождения целых
десятичных чисел со знаком в записи с применением метасимволов будут
иметь следующий вид:
<число>  [(+, –)] <цифра>{<цифра>}
<цифра>  (0,1,2,3,4,5,6,7,8,9)
Эти правила означают, что «число есть цепочка символов, которая
может начинаться со знака + или – (причём этот знак может и
отсутствовать), дальше должна обязательно содержать одну цифру, за
которой может следовать (хотя и не обязательно) последовательность
цифр любой длины».
Грамматика стала более понятной, кроме того, удалось полностью
избежать рекурсии, заменив её символом итерации {}. Эта форма наиболее
употребима для одного из типов грамматик, а именно – для регулярных
грамматик, которые будут рассмотрены ниже.
При записи правил в графическом виде вся грамматика
представляется в виде набора специальным образом построенных
диаграмм. Эта форма доступна только для грамматик контекстносвободных и регулярных типов, но этого достаточно, чтобы её можно было
использовать для задания известных языков программирования.
В такой форме записи каждому нетерминальному символу
соответствует диаграмма, построенная в виде направленного графа. Граф
9
имеет следующие типы вершин:
 точка входа (не обозначена – из неё начинается входная дуга графа);
 нетерминальный символ – на диаграмме обозначен прямоугольником, в
который вписано обозначение символа;
 цепочка терминальных символов – обозначается овалом, кругом или
скругленным прямоугольником;
 узловая точка – обозначена закрашенным кружком;
 точка выхода – в нее входит выходная дуга графа.
Каждая диаграмма имеет только одну точку входа и одну точку
выхода, но сколько угодно вершин других типов. Вершины соединяются
направленными дугами, из входной точки дуги могут только выходить, в
выходную точку – только входить. Все остальные вершины должны иметь
как минимум по одной выходной и одной входной дуге.
Чтобы
построить
цепочку
символов,
соответствующую
нетерминальному символу грамматики, нужно рассмотреть диаграмму для
этого символа. Начав движение от точки входа, нужно двигаться по дугам
до точки выхода, помещая при этом все встречающиеся по пути
нетерминальные символы или терминальные цепочки в результирующую
цепочку. Через любую вершину графа можно пройти один раз, ни разу или
сколь угодно много раз, в зависимости от пути движения. Если
результирующая цепочка содержит нетерминальные символы, нужно в
свою очередь рассмотреть соответствующие им диаграммы, до тех пор,
пока не будет получена цепочка, состоящая полностью из терминальных
символов. Для получения цепочки заданного языка процесс порождения
необходимо начинать с диаграммы начального символа грамматики.
Описанная ранее грамматика G порождения целых десятичных чисел
со знаком в графическом виде будет выглядеть следующим образом:
Цифра:
0
1
2
+
3
цифра
4
–
5
6
7
8
При этом начальным символом является <число>.
9
Данный способ в основном применяется в литературе при изложении
грамматик языков программирования. Он удобен для пользователей –
разработчиков, но практического применения в компиляторах не имеет.
Число:
10
1.3 Классификация языков и грамматик
От того, к какому типу относится тот или иной язык
программирования, зависит сложность распознавателя для этого языка.
Для некоторых типов языков в принципе невозможно построить
компилятор, который анализировал бы исходные тексты на этих языках за
приемлемое время на основе ограниченных вычислительных ресурсов.
1.3.1 Классификация грамматик по Хомскому
Формальные грамматики классифицируются по структуре их правил.
Если все без исключения правила грамматики удовлетворяют
определённой структуре, то её относят к заданному типу. Если хотя бы
одно правило грамматики не удовлетворяет требованиям структуры, то она
не попадает в заданный тип. Если правила грамматики соответствуют
структуре нескольких типов, то она будет отнесена по классификации к
самому простому из них.
Пусть грамматика обозначена как G(VT,VN,P,S), V=VTVN. В
соответствии с иерархией Хомского выделяют 4 типа грамматик.
1. Тип 0 – грамматики с фразовой структурой, или без ограничений
На структуру их правил не накладывается никаких ограничений, т.е.
правила имеют вид:   , где V+, V*. Это самый общий тип
грамматик. Грамматики, которые относятся только к этому и не могут
быть отнесены ни к какому другому типу, являются самыми сложными.
2. Тип 1 – Контекстно-зависимые (КЗ) и неукорачивающие
грамматики
К этому типу относятся два основных класса грамматик.
Контекстно-зависимые
грамматики
имеют
правила
вида
*
+
1A2  12, где 1,2V , AVN, V .
Неукорачивающие грамматики имеют правила вида   , где
,V+, .
В КЗ-грамматиках при построении предложений заданного языка
один и тот же нетерминальный символ может быть заменен различными
терминальными цепочками в зависимости от контекста, в котором он
встречается. Цепочки 1 и 2 в правилах обозначают контекст: 1 – левый
контекст, 2 – правый контекст. В общем случае они могут быть пустыми.
В неукорачивающих грамматиках при построении предложений языка
цепочка символов заменяется на цепочку не меньшей длины.
Эти два класса грамматик эквивалентны.
При построении компиляторов такие грамматики не применяются,
поскольку языки программирования имеют более простую структуру и
могут быть построены с помощью грамматик других типов.
11
3. Тип 2 – Контекстно-свободные (КС) грамматики
Контекстно-свободные (КС) грамматики имеют правила вида A  , где
AVN, V+. В правой части у них стоит всегда хотя бы один символ. Их
отличие от предыдущих типов состоит в том, что левая часть правил должна
состоять ровно из одного нетерминального символа. Такие грамматики еще
называют неукорачивающими контекстно-свободными (НКС) грамматиками.
Существует почти эквивалентный им класс укорачивающих контекстносвободных (УКС) грамматик, отличие которого в том, что он допускает
пустую цепочку, т.е. правила имеют вид A  , где AVN, V*. В
дальнейшем, если возможность наличия в языке пустой цепочки не имеет
принципиального значения, будем говорить просто о КС-грамматиках. КСграмматики широко используются при описании синтаксических
конструкций языков программирования.
4. Тип 3 – Регулярные грамматики
В правой части правил грамматик этого типа может присутствовать не
более одного нетерминального символа, причём он должен быть
расположен во всех правилах одной грамматики с одной и той же стороны
от цепочки терминалов, а требования к левой части правил совпадают с
предыдущим типом. К этому типу относятся два эквивалентных класса
грамматик: леволинейные и праволинейные (их название определяется
местоположением нетерминального символа в правой части правил
относительно терминальной цепочки). Для любой праволинейной
грамматики можно построить эквивалентную ей леволинейную, задающую
тот же язык, и наоборот.
Леволинейные грамматики могут иметь правила двух видов: A  B,
или A  , где A,BVN, VT*. Нетерминальный символ в правой части
правил размещается слева от цепочки терминалов.
Праволинейные грамматики имеют правила тоже двух видов: A  B,
или A  , где A,BVN, VT*. Соответственно нетерминальный символ
находится справа от терминальных.
Регулярные грамматики используются при описании простейших
конструкций языков программирования: идентификаторов, констант,
строк, комментариев и т.д. Они очень просты и удобны в использовании,
поэтому в компиляторах на их основе строятся функции лексического
анализа входного языка.
Из определения типов видно, что любая регулярная грамматика
является также КС-грамматикой, или любая грамматика может быть
отнесена к типу 0. В то же время существуют УКС-грамматики, которые
не относятся к типу 1, поскольку могут содержать правила вида A  ,
недопустимые в этом типе. В общем, сложность грамматики обратно
пропорциональна тому максимально возможному номеру типа, к которому
может быть отнесена эта грамматика. Самыми простыми являются
грамматики типа 3, самыми сложными – типа 0.
12
1.3.2 Классификация языков
Языки классифицируются согласно иерархии Хомского в соответствии с
типами грамматик, с помощью которых они заданы, причем из всех
эквивалентных грамматик, задающих один и тот же язык, выбирается
грамматика с максимально возможным номером, т.е. самая простая.
Сложность
языков
соответствует
сложности
грамматик.
От
классификационного типа языка зависит сложность его распознавателя.
Тип 0 – языки с фразовой структурой
Это самые сложные языки, для распознавания которых требуются
вычислители, равномощные машине Тьюринга. Для такого языка невозможно
построить компилятор, который выполнил бы разбор за ограниченное время
на основе ограниченных вычислительных ресурсов.
Практически все естественные языки относятся к этому типу. Одно и то
же слово в естественном языке может иметь различный смысл в зависимости
от контекста и играть различную роль в предложении. Такие языки далее
рассматриваться не будут.
1.
Тип 1 – контекстно-зависимые (КЗ) языки
В общем случае время на распознавание языка типа 1 экспоненциально
зависит от длины исходной цепочки символов.
Языки и грамматики этого типа используются в переводе текстов на
естественных языках. Распознаватели, построенные на их основе, позволяют
анализировать тексты с учётом контекстной зависимости в предложениях
входного языка, хотя в общем случае для точного перевода всё же требуется
вмешательство человека. Такие грамматики могут использоваться в
сервисных функциях проверки орфографии в языковых процессорах.
Однако языки программирования имеют более простую структуру,
поэтому в компиляторах КЗ-языки не применяются.
2.
Тип 2 – контекстно-свободные (КС) языки
КС-языки лежат в основе большинства современных языков
программирования, на их основе работают некоторые командные
процессоры, допускающие управляющие команды цикла и условия.
В общем случае время на распознавание предложений языка этого типа
полиномиально зависит от длины цепочки символов (это кубическая или
квадратичная зависимость в зависимости от класса языка). Но среди КСязыков существует много классов, для которых эта зависимость линейна, и
многие языки программирования можно отнести к одному из таких классов.
3.
Тип 3 – регулярные языки
Это самый простой тип языков, и они являются наиболее широко
распространенным типом, используемым в вычислительных системах. Время
на распознавание цепочек языка линейно зависит от их длины. Поэтому
иногда эти языки ещё называют линейными. Для работы с такими языками
используются регулярные множества и выражения, конечные автоматы.
КС-языки и регулярные языки будут рассматриваться подробно.
4.
13
Лекция 3. 14.09.09
1.4 Вывод и выводимость
1.4.1 Цепочки вывода. Сентенциальная форма
Выводом называется процесс порождения цепочек языка на основе
правил определяющей язык грамматики.
Цепочка 12 называется непосредственно выводимой из цепочки
12 в грамматике G(VT,VN,P,S), V=VTVN, 1,,2V*, V+, если в
грамматике G существует правило     P. Непосредственная
выводимость обозначается . Т.е. цепочка  непосредственно
выводима из цепочки  в том случае, если можно взять несколько
символов в цепочке , заменить их на другие символы согласно правилу
грамматики и получить при этом цепочку . В формальном определении
любая из цепочек 1, 2 (или обе) может быть пустой. В пределе вся
цепочка  может быть заменена на цепочку , тогда в грамматике должно
существовать правило     P.
Цепочка  называется выводимой из цепочки  – обозначается  * ,
если выполняется одно из двух следующих условий:
  непосредственно выводима из :   ;
 , такая, что:  выводима из  и  непосредственно выводима из 
( *  и   ).
Это рекурсивное определение выводимости цепочки. Т.е.  выводима
из цепочки , если она либо непосредственно выводима, либо можно
построить последовательность непосредственно выводимых цепочек от  к
:   1  2  …  i  i+1 …  n  . Такая последовательность
непосредственно выводимых цепочек называется выводом или цепочкой
вывода, а каждый переход – шагом вывода. Если  непосредственно
выводима из  (  ), то имеется всего один шаг вывода.
Если вывод содержит два или более шагов, то говорят, что 
нетривиально выводима из :  + . Если количество шагов вывода
известно, его можно указать у знака выводимости цепочек:  4 
означает, что  выводима из  за 4 шага. Запись  0  означает, что
цепочки равны. Целесообразно при записи цепочки вывода на каждом
шаге над знаком выводимости указывать номер правила, согласно
которому выполнен этот шаг.
Грамматика, в которой для любой цепочки порождаемого языка
существует единственная цепочка вывода, называется однозначной.
Вывод называется законченным, если из полученной цепочки нельзя
сделать более ни одного шага, т.е. если полученная цепочка пустая или
содержит только терминальные символы грамматики: VT*. Цепочка,
полученная в результате законченного вывода, называется конечной
14
цепочкой вывода.
Цепочка символов V* называется сентенциальной формой
грамматики G(VT,VN,P,S), если она выводима из целевого символа
грамматики S: S * . Если цепочка получена в результате законченного
вывода, она называется конечной сентенциальной формой.
Вывод называется левосторонним, если в нём на каждом шаге вывода
правило грамматики применяется к самому левому нетерминальному
символу в цепочке. Аналогично определяется правосторонний вывод.
Если символы заменяются в произвольном порядке, вывод нельзя отнести
ни к какому из типов. Для КС - грамматик для любой сентенциальной
формы всегда можно построить левосторонний или правосторонний
вывод. Для грамматик более сложных типов это не всегда возможно
(структура правил не всегда позволяет заменять крайний левый или
крайний правый нетерминальные символы в цепочке).
Рассмотрим снова пример грамматики целых десятичных чисел со
знаком.
G ({0,1,2,3,4,5,6,7,8,9,–,+}, {S,T,F}, P, S).
P:
S  T+T–T
T  FTF
F  0123456789.
Построим в ней несколько цепочек вывода, причём в качестве начала
будем брать не только целевой символ, но и другие символы или их
сочетания.
(1)
(2)
(3)
(4)
(5)
(6)
S  –T  –TF  –FF  –1F  –19 (левосторонний);
T  TF  T0  TF0  T30  F30  730; (правосторонний)
S  T  TF  TFF  T6F  F6F  F63  263; (–)
S  T  TF  T3  TF3  T63  F63  263; (правосторонний);
S  T  TF  TFF  T8F  F8F (не законченный);
TFT  TFTF  TFFF  TFF0  TF10  T210  F210  1210 (правост.)
Здесь (1) – (4) – конечные сентенциальные формы, поскольку цепочка
в (2) может быть получена из целевого символа грамматики (хотя в
этом примере и получена из нетерминала T); (5) – просто сентенциальная
форма (не конечная). В выводе (6) в явном виде не присутствует
сентенциальная форма, хотя цепочка 1210 и является конечной
сентенциальной формой. Для того, чтобы это подтвердить, достаточно
построить другой вывод этой цепочки из целевого символа. А цепочка
TFT не является сентенциальной формой, т.к. её невозможно получить из
целевого символа. Все выводы, за исключением (5), являются
законченными. На примере (3), (4) видно, что одна и та же цепочка может
быть получена посредством разных выводов. Выводы (2), (4), (6) –
правосторонние, (1) – левосторонний, (3), (5) – ни то, ни другое.
15
1.4.2 Дерево вывода
Деревом вывода грамматики G называется дерево (граф), которое
соответствует некоторой цепочке вывода и удовлетворяет следующим
условиям:

каждая вершина дерева обозначается символом грамматики AV;

корнем дерева является вершина, обозначенная целевым символом
грамматики S;

листьями дерева являются вершины, обозначенные терминальными
символами грамматики или символом ;

если некоторый узел обозначен символом AVN, а связанные с ним
узлы – символами b1, b2,…, bnn > 0, 0 < i  n, biV{}, то в
грамматике существует правило A  b1b2…bn P.
По структуре правил дерево вывода всегда можно построить для
грамматик типа 2 и 3. Для строго формализованного построения дерева
удобнее пользоваться левосторонним либо правосторонним выводом.
Дерево вывода можно построить двумя способами: сверху вниз (обычно
левосторонний вывод) и снизу вверх (обычно правосторонний).
1.
2.
3.
4.
5.
Алгоритм построения дерева сверху вниз:
целевой символ грамматики помещается в вершину дерева;
в грамматике выбирается необходимое правило и целевой символ
раскрывается на несколько символов первого уровня;
среди всех концевых вершин дерева выбирается крайняя (левая для
левостороннего вывода, правая – для правостороннего вывода)
вершина, обозначенная нетерминальным символом;
для неё выбирается нужное правило и она снова раскрывается на
несколько вершин следующего уровня;
если все концевые вершины обозначены терминальными символами,
процесс закончен. Иначе – возврат на шаг 3.
При построении дерева снизу вверх процесс построения начинается с
листьев, в качестве которых выбираются терминальные символы цепочки
языка. Они образуют последний уровень дерева; далее в грамматике
выбирается соответствующее правило, согласно которому один или
несколько символов цепочки могут быть «свёрнуты» в нетерминальный
символ – (при левостороннем или правостороннем выводе это крайние
символы в слое дерева) они соединяются с новой вершиной; и так до тех
пор, пока все вершины не окажутся соединены в корневой вершине.
Поскольку компилятор читает программу сверху вниз и слева
направо, для построения дерева сверху вниз обычно используется
левосторонний вывод.
16
Пример: Рассмотрим деревья вывода для
сентенциальных форм (1) и (2). Подробно рассмотрим
построение для формы (1).
S
Дерево строится сверху вниз. Номера шагов
обозначены цифрами. Процесс начинается с корня (он
помечен целевым символом), далее при выводе
применялось правило S  –T (первый шаг),
следовательно, в следующем уровне будут две
вершины («–» и «Т»). Вершина «–» помечена
терминальным символом, значит, это лист. Вершина
«Т» представляет собой нетерминал и вновь
раскрывается по правилу T  TF согласно ранее
построенному выводу (второй шаг). Поскольку вывод
левосторонний, каждый раз в цепочке вывода
очередное правило применяется к крайнему левому
нетерминалу. Поэтому следующим заменяется
нетерминал Т, как самый левый в цепочке вывода
(она в настоящий момент имеет вид –TF), и это
третий шаг… Процесс продолжается до тех пор, пока
все вершины не получат в качестве меток
терминальные символы.
T
2
T
F
3
5
F
9
4
T
1
1
T
F
2
3
T
F
5
0
4
F
3
6
Для формы (2) построение отличается тем, что в
качестве начального символа вместо целевого S взят
нетерминал Т, а вывод использован правосторонний.
Построение
дерева
снизу вверх
является более неоднозначным процессом.
Рассмотрим его для вывода формы (4). При
этом будем двигаться по цепочке вывода
от её конца к началу. Сначала строим
листья «2», «6» и «3». Последним шагом
было правило F  2. Оно позволяет
заменить терминальный символ ‘2’ на ‘F’
(схема 1). Следующие правила T  F и
F  6 (схема 2). Далее по правилу T  TF
нетерминалы T и F сворачиваются в T
(схема 3). И наконец после применения
правил F  3, T  TF и S  T получается
итоговое дерево (схема 4).
1
–
7
F
T
2
F
F
2
6
1
2
S
T
T
3
F
F
2
6
3
3
T
T
6
4
T
3
F
F
F
2
6
3
17
1.5 Распознаватели. Задача разбора
1.5.1 Общая схема распознавателя
В числе прочих задач компилятор должен определить
принадлежность некоторого текста к конкретному языку. В отношении
исходной программы компилятор выступает в роли распознавателя, а
человек, создавший программу – в роли генератора цепочек этого языка.
Распознаватель – это специальный алгоритм, позволяющий для
некоторой цепочки символов определить, принадлежит ли она заданному
языку. Это один из способов задания языка.
Распознаватель входит в состав компилятора и является частью
программного обеспечения компьютера.
Условная схема распознавателя имеет следующий вид:
a1
a2
a3
…
an
входная лента
входная головка
УУ с конечной
памятью
рабочая
память
Основные компоненты распознавателя:

входная лента – линейная последовательность клеток, или ячеек,
каждая из которых содержит ровно один символ входного алфавита;

входная (считывающая) головка обозревает одну входную ячейку; на
каждом шаге работы может сдвигаться на одну ячейку вправо, влево
или оставаться на месте;

устройство управления (УУ), которое координирует работу
распознавателя, имеет некоторое множество состояний и конечную
память;

внешняя (рабочая) память может хранить некоторую информацию в
процессе работы распознавателя и может иметь неограниченный
объем.
Алфавит распознавателя конечен; он включает в себя все допустимые
символы входных цепочек, а также некоторый дополнительный алфавит
символов, которые могут обрабатываться УУ и храниться в рабочей
памяти распознавателя.
В процессе своей работы распознаватель может выполнять некоторые
элементарные операции, такие как чтение входного символа, сдвиг
головки, доступ к рабочей памяти для чтения или записи информации,
изменение состояния УУ.
Работа распознавателя состоит из последовательности шагов, или
18
тактов. То, каким должен быть этот такт, определяется текущим входным
символом, состоянием УУ и символом, извлеченным из памяти. Итак,
Такт состоит из следующих моментов:

входная головка распознавателя сдвигается на одну ячейку вправо,
влево или остается на месте;

в память помещается некоторая информация;

изменяется состояние УУ.
В процессе работы распознавателя происходит смена конфигураций.
Конфигурация распознавателя (мгновенное описание) определяется
следующими параметрами:

состояние УУ;

содержимое входной ленты и положение считывающей головки в ней;

содержимое внешней памяти.
Конфигурация называется начальной, если УУ находится в начальном
состоянии, входная головка обозревает самый левый символ на входной
ленте, а память имеет заранее установленное начальное содержимое.
Конфигурация называется заключительной, если УУ находится в
одном из множества заключительных состояний, а входная головка
обозревает правый концевой маркер или сошла с ленты.
Распознаватель допускает входную цепочку символов, если, находясь
в начальной конфигурации, в которой данная цепочка записана на входной
ленте, он может проделать конечную последовательность шагов,
заканчивающуюся одной из его заключительных конфигураций.
Некоторые виды распознавателей могут из начальной конфигурации
проделать различные последовательности шагов, из которых, может быть,
лишь некоторые (или даже одна) приведут к заключительной
конфигурации. В таком случае входная цепочка является допущенной.
Язык, определяемый распознавателем – это множество всех цепочек,
которые допускает этот распознаватель.
19
Лекция 4. 21.09.09
1.5.2 Виды распознавателей и их классификация
Распознаватели
классифицируют
в
зависимости
от
вида
составляющих их компонентов: считывающего устройства, устройства
управления и внешней памяти.
По видам считывающего устройства распознаватели могут быть
двусторонними и односторонними. Односторонние распознаватели
допускают перемещение считывающей головки по ленте только в одном
направлении, обычно слева направо (такой распознаватель называется
левосторонним). Такие распознаватели не возвращаются назад к уже
прочитанной части цепочки. Двусторонние распознаватели допускают
перемещение считывающей головки в любом направлении.
По видам устройства управления распознаватели бывают
детерминированные и недетерминированные. Распознаватель является
детерминированным, если для любой допустимой конфигурации
существует не более одной следующей конфигурации. Если для любой
допустимой конфигурации единственно возможна ровно одна следующая
конфигурация, то такой распознаватель является детерминированным
полностью определённым, иначе он неполностью определённый. Для
недетерминированного распознавателя на некотором шаге возможно
совершить несколько альтернативных переходов в различные
конфигурации. При этом может оказаться, что только одна из возможных
последовательностей шагов приводит в заключительную конфигурацию.
По видам внешней памяти распознаватели бывают следующих типов:

распознаватели без внешней памяти;

распознаватели с ограниченной внешней памятью;

с неограниченной внешней памятью.
Распознаватели без внешней памяти моделируются конечными
автоматами и используют в процессе работы только конечную память УУ.
Размер внешней памяти распознавателей второго типа ограничен. Эти
ограничения могут носить характер некоторой функциональной
зависимости от длины исходной цепочки символов – линейной,
полиномиальной, экспоненциальной и т.п. Кроме того, может быть указан
способ организации внешней памяти – стек, очередь, список и т.п.
Например, широко распространены распознаватели с памятью
магазинного типа, которая организована по стековому принципу.
В распознавателях последнего типа предполагается, что для их работы
может потребоваться внешняя память неограниченного объема, вне
зависимости от длины входной цепочки. У таких распознавателей
используется память с произвольным методом доступа.
Все три рассмотренных составляющих организуют общую
20
классификацию распознавателей. Например, в ней возможен такой тип:
«односторонний
детерминированный
полностью
определённый
распознаватель с линейно ограниченной стековой памятью». Тип
распознавателя в классификации определяет его сложность в целом.
Сложность распознавателя напрямую связана с типом языка, цепочки
которого он должен определять.
Например, для языков типа 1 (КЗ) распознавателями являются
двусторонние недетерминированные автоматы с линейно ограниченной
внешней памятью. Такой алгоритм уже может быть реализован в ПО
компьютера. Но экспоненциальная зависимость времени разбора от длины
входной цепочки существенно ограничивает применение распознавателей
для КЗ-языков. Такие распознаватели, как правило, применяются для
автоматизированного перевода текстов на естественных языках, когда
временные ограничения на разбор текста несущественны.
Для КС-языков распознавателями являются односторонние
недетерминированные автоматы с магазинной (стековой) внешней
памятью. Кроме того, среди всего множества КС-языков можно выделить
подкласс детерминированных языков, для которых распознавателями
являются детерминированные автоматы с магазинной памятью – ДМПА.
Этот класс языков наиболее интересен для построения компиляторов.
Для языков программирования более целесообразно строить
компиляторы на основе КС-языков, дополняя их семантическим
анализатором, чем использовать в качестве основы КЗ-языки – такая
комбинация имеет более высокую скорость работы.
Для регулярных языков распознавателями являются односторонние
недетерминированные распознаватели без внешней памяти – конечные
автоматы (КА). Кроме того, любой недетерминированный КА всегда
может быть преобразован в детерминированный (ДКА). В компиляторах
распознаватели на основе регулярных языков используются для
лексического анализа текста исходной программы. Регулярные языки
находят применение также во многих областях, связанных с разработкой
ПО вычислительных систем.
21
1.5.3 Задача разбора
Грамматики и распознаватели – два независимых метода, которые
реально могут быть использованы для определения языка. Однако при
разработке компилятора для некоторого языка программирования
возникает задача, которая требует связать между собой эти два метода.
Разработчики компилятора имеют дело с конкретным языком
программирования, т.е. грамматика языка уже известна. Задача
разработчиков состоит в построении распознавателя данного языка,
который явится основой синтаксического анализатора в компиляторе.
Итак, задача разбора формулируется следующим образом: на основе
имеющейся грамматики некоторого языка необходимо построить
распознаватель для этого языка. Заданная грамматика и распознаватель
должны быть эквивалентны, т.е. определять один и тот же язык.
В общем виде эта задача может быть решена не для всех типов
языков, хотя для КС и регулярных языков задача разбора разрешима и
существуют некоторые формальные методы её решения.
Компилятор должен не просто дать ответ, принадлежит ли входная
цепочка данному языку, но и определить её смысл. Фактически работа
распознавателя в составе компилятора заключается в построении дерева
разбора, которое затем используется для генерации кода. Кроме того, если
исходная цепочка не принадлежит к заданному языку, компилятор должен
не просто установить факт ошибки, но и по возможности определить её
тип и местоположение.
22
Лекция 4 (продолжение). 21.09.09
Глава 2 Регулярные языки
2.1
Регулярные множества и регулярные выражения
2.1.1 Определение и свойства регулярных выражений
Проблема генерирования бесконечных языков посредством конечных
описаний решается различными способами. Одним из них является
использование регулярных выражений для порождения бесконечных
цепочек языка.
Регулярное множество и регулярное выражение для некоторого
алфавита V определяется рекурсивно следующим образом:

0 – регулярное выражение, обозначает  регулярное множество;

 – регулярное выражение, обозначает регулярное множество {};

aV a – регулярное выражение, обозначает регулярное множество
{a};

если p и q – произвольные регулярные выражения, обозначающие
регулярные множества P и Q, то p+q, pq, p* – регулярные выражения,
обозначающие соответственно регулярные множества PQ, PQ, P*;

ничто другое регулярным выражением и регулярным множеством не
является.
Иными словами, регулярные множества – это цепочки символов над
заданным алфавитом, построенные с использованием операций
объединения, конкатенации и замыкания.
Все регулярные языки представляют собой регулярные множества.
Два регулярных выражения  и  эквивалентны:  = , если они
обозначают одно и то же множество.
Каждое регулярное выражение обозначает одно и только одно
регулярное множество, но для одного регулярного множества может
существовать сколько угодно задающих его регулярных выражений.
При записи регулярных выражений используются круглые скобки, как
для обычных арифметических выражений. При отсутствии скобок
операции выполняются слева направо с учетом приоритета. Наивысшим
приоритетом обладает операция итерации, затем конкатенации, потом –
объединение множеств.
Например, язык, представляющий собой множество всех цепочек из
нулей и единиц произвольной длины, может быть описан эквивалентными
регулярными выражениями α1 и α2: α1 = (0+1)*, α2 = (0*1*)*. α1 = α2 .
Пусть , ,  – регулярные выражения. Тогда свойства регулярных
выражений можно записать в виде следующих формул:
23
11. 0*=

6. 
(+)()+
7. **
12. 0=0=0
*
*
*
()()
8. + = +=
13. 0+=+0=
*
*
*
(+)+
9.   
14. ==
* *
*
(+)=+
10. ( ) 
Все эти свойства доказываются с применением аппарата теории
множеств, т.к. регулярные выражения – это обозначения для
соответствующих множеств.
1.
2.
3.
4.
5.
2.1.2 Уравнения с регулярными коэффициентами
Простейшие уравнения с регулярными коэффициентами будут
выглядеть следующим образом: X=X+; X=X+, где ,V* –
регулярные выражения над алфавитом V, а XV. Два вида записи
уравнений (правосторонняя и левосторонняя запись) связаны с тем, что
для регулярных выражений операция конкатенации не обладает свойством
коммутативности. Обе записи равноправны.
Решениями таких уравнений будут регулярные множества. Это
значит, что, если взять РМ, являющееся решением уравнения, обозначить
его соответствующим РВ и подставить в уравнение, то получится
тождественное равенство.
Решением первого уравнения является регулярное множество,
обозначенное *. Если подставить его вместо переменной X в уравнение,
получим X+=(*)+=6 (* )+=13(* )+=5(* +)=1* =X.
Аналогично, для второго уравнения решение будет иметь вид *.
Подставив его в уравнение, также получим тождество.
Указанные решения уравнений не всегда являются единственными.
Например, если регулярное выражение  в первом уравнении обозначает
множество, которое содержит пустую цепочку, то решением уравнения
может быть любое множество, обозначенное выражением X=*(+), где 
может обозначать произвольное множество (в том числе не регулярное).
Однако указанные решения – наименьшие возможные для данных
уравнений. Они называются наименьшей подвижной точкой.
Из рассмотренных уравнений можно формировать систему уравнений
с регулярными коэффициентами. Например, в правосторонней записи
такая система будет иметь вид:
X1=10+11X1+12X2+…+1nXn;
X2=20+21X1+22X2+…+2nXn;
…
Xi=i0+i1X1+i2X2+…+inXn;
…
Xn=n0+n1X1+n2X2+…+nnXn;
В данной системе все коэффициенты ij – регулярные выражения над
алфавитом V, а переменные не входят в этот алфавит: XiV  i.
24
Системы уравнений с регулярными коэффициентами решаются
методом последовательных подстановок. Рассмотрим метод решения для
правосторонней записи. Алгоритм работает с переменной номера шага i.
Алгоритм решения:
1. Положить i:=1.
2. Если i=n, то перейти к шагу 4, иначе записать i-е уравнение в виде:
Xi=iXi+i, где i=ii, i=i0+i i+1 Xi+1+…+i n Xn, решить уравнение и
получить Xi=i*i. Затем подставить это выражение во все уравнения для
переменных Xi+1,…, Xn.
3. Увеличить i на единицу и вернуться к шагу 2.
4. После всех подстановок уравнение для Xn будет иметь вид:
Xn=nXn+, где n=nn, а  – регулярное выражение над алфавитом V*, т.е.
не содержит переменных Xi. Тогда можно найти решение Xn=n*.
5. Уменьшить i на единицу. Если i=0, то алгоритм завершен.
6. Взять найденное решение для Xi=iXi+i, где i=ii,
i=i0+i i+1 Xi+1+…+i n Xn и подставить в него найденные окончательные
решения для переменных Xi+1,…, Xn. Получим окончательное решение для
Xi. Вернуться к шагу 5.
Система уравнений с регулярными коэффициентами всегда имеет
решение, но оно не всегда единственно. Рассмотренный алгоритм всегда
находит решение, которое является наименьшей подвижной точкой
системы уравнений.
2.2 Конечные автоматы и грамматики
2.2.1 Автоматные грамматики
Среди регулярных грамматик можно выделить отдельный класс –
автоматные грамматики. Они могут быть леволинейными и
праволинейными.
Леволинейные автоматные грамматики G(VT, VN, P, S) могут иметь
правила двух видов: A  Bt или A  t, где A, BVN, tVT.
Праволинейные автоматные грамматики могут тоже иметь правила
двух видов: A  tB или A  t, где A,BVN, tVT.
Классы обычных регулярных грамматик и автоматных почти
эквивалентны (с точностью до правила S  ). В общем случае в
автоматных грамматиках разрешается правило вида S  , где S – целевой
символ грамматики, который не должен встречаться в правых частях
других правил грамматики. Тогда язык, задаваемый автоматной
грамматикой G, будет включать в себя пустую цепочку: L(G). В таком
случае классы регулярных и автоматных грамматик становятся
эквивалентными.
Как следует из определения, в правилах автоматных грамматик в
25
отличие от регулярных вместо цепочки терминальных символов
присутствует только один терминальный символ. Любая автоматная
грамматика является регулярной, обратное же справедливо не всегда.
Существует алгоритм, позволяющий преобразовать регулярную
грамматику к автоматному виду. Он очень полезен, т.к. упрощает задачу
построения распознавателей для регулярных языков. Кратко его идея
состоит в том, что при наличии в правой части правила регулярной
грамматики цепочки терминальных символов длины n необходимо ввести
n–1 дополнительный нетерминальный символ и записать для каждого из
них соответствующие правила, т.е. разбить стоящую справа терминальную
цепочку на символы и разнести их по разным правилам.
Пример. Рассмотрим регулярную грамматику и приведём её к
автоматному виду. G({0,1},{S,A},P,S), S  001A, A  0A | 1A | 11. Эта
грамматика задаёт язык с алфавитом {0,1}, все цепочки которого
начинаются с цепочки ‘001’, а заканчиваются цепочкой‘11’.
В первом правиле присутствует цепочка из трёх терминальных
символов, значит, потребуется добавить два нетерминала. Пусть это
будут B и C. Первое правило примет вид: S  0B, B  0C, C  1A. 2
правила для A останутся без изменения: A  0A | 1A, а третье правило
A  11 тоже придётся разбить на два путём добавления ещё одного
нетерминала, например, D: A  1D, D  1. Итак, теперь грамматика
будет содержать 5 нетерминалов и 7 правил: G({0,1},{S,A,B,C,D},P,S),
S  0B, B  0C, C  1A, A  0A | 1A | 1D, D  1.
2.2.2 Определение конечного автомата
С автоматной грамматикой тесно связано понятие конечного
автомата. Автоматная грамматика позволяет сгенерировать все цепочки
некоторого регулярного языка, а конечный автомат – распознать этот язык.
Конечным автоматом (КА) называют пятёрку вида: M(Q,V,,q0,F), где

Q – конечное множество состояний УУ автомата;

V – алфавит автомата (конечное множество допустимых символов);

 – функция переходов, отображающая QV в множество подмножеств
множества Q: aV, qQ (q,a)={R, | RQ};

q0 – начальное состояние автомата: q0Q;

F – множество конечных (заключительных) состояний: FQ, F.
КА называется полностью определённым, если в каждом его
состоянии существует функция переходов для всех возможных входных
символов: aV, qQ (q,a)={R, RQ}.
Работа автомата представляет собой последовательность тактов
(или шагов). На каждом шаге работы автомат может остаться в том же
состоянии или перейти в другое. Поведение автомата на каждом такте
определяется функцией переходов, которая зависит от текущего состояния
26
и входного символа. Если функция переходов допускает несколько
переходов в следующее состояние, то КА может перейти в любое из них, и
такой КА является недетерминированным (НКА). Конечный автомат
является детерминированным (ДКА) если множество (q,a) содержит не
более одного состояния aV, qQ. Если (q,a) всегда содержит ровно
одно состояние, то M является полностью определённым (ПО).
В начале работы автомат находится в начальном состоянии q0. Работа
автомата продолжается до тех пор, пока на его вход поступают символы
входной цепочки.
Мгновенным описанием (МО), или конфигурацией автомата M
называется пара (q, w), где qQ – состояние УУ, wV* – неиспользованная
часть входной цепочки (символ, обозреваемый считывающей головкой и
все символы справа от него). Тогда пара (q0, w) называется начальной
конфигурацией для цепочки w, а конфигурация (q, ) является
заключительной, или допускающей, если qF (q – одно из заключительных
состояний автомата).
Представим такт автомата M бинарным отношением ├─M на
множестве конфигураций. Тогда: если (q,a) содержит p: p(q,a), то для
обозначения такта записывают (q,aw)├─M (p,w) для всех цепочек wV*.
Здесь q,pQ, aV, т.е. a является символом входного алфавита.
Рефлексивное и транзитивное замыкание отношения ├─ обозначается
через ├─*.
Цепочка w допускается автоматом M, если (q0,w)├─*(q,) для
некоторого qF, т.е. получив на вход эту цепочку, автомат из начальной
конфигурации может перейти в заключительную.
Язык, допускаемый (определяемый) автоматом M, представляет
собой множество L(M)={wwV* и  qF(q0,w)├─*(q,)}. Два автомата
эквивалентны, если они задают один и тот же язык.
КА является распознавателем для регулярных языков.
Функция переходов КА может быть задана таблицей или графом
переходов (диаграммой).
В таблице принято в качестве заголовков строк брать состояния
автомата, а в качестве заголовков столбцов – символы алфавита. Если в
ячейке пересечения строки p и столбца a стоит состояние q, то это
означает, что при прочтении символа a автомат переходит из состояния p в
q. Тогда в случае детерминированного полностью определённого автомата
в каждой ячейке таблицы будет находиться ровно одно состояние.
Граф переходов КА – это направленный помеченный граф, вершины
которого обозначены состояниями автомата, а дуги – символами входного
алфавита. При этом дуга (p,q)p,qQ обозначена символом aV, если в
a
КА определена (p,a) и q(p,a).
p
q
27
Лекция 5. 24.09.09
Начальное и конечное состояния на графе помечаются специальным
образом, как правило, начальное – дополнительной пунктирной линией, а
конечное – двойным кружком.
Для моделирования работы КА удобно, чтобы в нём были заданы все
возможные переходы для всех символов входного алфавита. Если это не
так, то автомат можно привести к полностью определённому виду путем
ввода дополнительного состояния «ошибка», на которое следует замкнуть
все неопределённые переходы, а все переходы из этого нового состояния
замкнуть на него же.
Рассмотрим два примера конечных автоматов – ДКА и НКА.
1. Пусть M=({p,q,r},{0,1},,p,{r}) – ДКА, где функция переходов
задана таблицей:
Автомат M допускает все цепочки из
{0,1}*, содержащие два стоящих рядом
0
1
состояние
нуля. Попав в состояние r, автомат из
p
{q}
{p}
него уже не выходит.
q
{r}
{p}
Пусть w=’01001’.
r
{r}
{r}
Рассмотрим последовательность тактов для данной цепочки w.
(p,01001) ├─ (q,1001) ├─ (p,001) ├─ (q,01) ├─ (r,1) ├─ (r,).
Поскольку rF, это означает, что ‘01001’L(M).
2. Построим НКА, допускающий цепочки в алфавите {1,2,3}, у
которых последний символ цепочки уже появлялся в ней ранее (т.е.,
например, цепочка ‘1232’ должна быть допущена, а ‘122113’ – нет).
Введем состояния: q0 – нейтральное, qi делает предположение, что
последний символ цепочки совпадает с индексом состояния, qf –
заключительное состояние. Из состояния q0 автомат может перейти в qa,
если наблюдает символ ”a”, или остаться в q0. Находясь в qa и наблюдая
символ ”a”, автомат может перейти в заключительное состояние qf или
остаться в qa. Из qf автомат никуда не переходит.
Формально такой автомат определим следующим образом:
M=({q0,q1,q2,q3,qf},{1,2,3},,q0,{qf}), функция переходов задана
таблицей:
вход
состояние
1
2
3
q0
{q0,q1} {q0,q2} {q0,q3}
Рассмотрим
для
примера
q1
{q1,qf}
{q1}
{q1}
цепочку w=’12321’. Процесс
q2
{q2}
{q2,qf}
{q2}
порождения
конфигураций
q3
{q3}
{q3}
{q3,qf}
будет выглядеть следующим
qf
образом:



вход
28
(q0,12321) ├─ (q0,2321) ├─ (q0,321) ├─ (q0,21) ├─ (q0,1) ├─ (q0,)
(q1,)
(q2,1) ├─ (q2,)
(q3,21) ├─ (q3,1) ├─ (q3,)
(q2,321) ├─ (q2,21) ├─ (q2,1) ├─ (q2,)
(qf,1)
(q1,2321) ├─ (q1,321) ├─ (q1,21) ├─ (q1,1) ├─ (q1,)
(qf,)
Поскольку (q0,12321)├─*(qf,), то ‘12321’L(M).
Построение ДКА, эквивалентного заданному НКА
Моделировать работу ДКА значительно проще, чем НКА. Доказано,
что для любого НКА можно построить эквивалентный ему ДКА. Для этого
существует специальный алгоритм, идея которого состоит в следующем:

В качестве состояний нового ДКА рассматриваются все состояния
исходного автомата, а также всевозможные сочетания этих состояний по
два, по три, …, по n, если в исходном автомате было n состояний.

Для всех новых состояний строится функция переходов, в которой для
каждого состояния qiqj появляется переход в новое состояние,
представляющий собой объединение всех прежних переходов из qi и qj.

Начальное состояние нового автомата остается тем же, а множество
конечных состояний нового автомата будут состоять из всех сочетаний, в
которых присутствовало qf – конечное состояние исходного автомата.
После построения ДКА из него удаляют все недостижимые состояния
(состояния, переход в которые невозможен при любой входной цепочке).
В итоге построен ДКА, эквивалентный заданному НКА. При этом
если исходный автомат имел n состояний, то в наихудшем случае
построенный ДКА будет иметь (2n–1) состояний.
Рассмотрим на примере построение ДКА, эквивалентного заданному
НКА.
1. Пусть M=({p,q,r},{0,1},,p,{r}) – НКА, где функция переходов 
0,1
задана графом:
1
1
p
q
r
Недетерминированный автомат M допускает
вход
*
все цепочки из {0,1} , заканчивающиеся двумя состояние 0
1
единицами: (0+1)*11.
p
{p} {p,q}
Представим
функцию
переходов
в
q
–
{r}
табличном виде (таблица справа).
r
–
–
29
вход
0
1
состояние
p
{p} {pq}
q
–
{r}
r
–
–
pq
{p} {pqr}
pr
{p} {pq}
qr
–
{r}
pqr
{p} {pqr}
Далее создадим все возможные новые
состояния, которые могут получиться в результате
сочетаний исходных состояний, и занесём их в
таблицу. Новые состояния будем записывать в виде
{pq}. Тогда из состояния p по символу 1 переход
будет происходить в такое состояние pq. В таблице
присутствуют недостижимые состояния q, r, pr и qr,
которые можно удалить без изменения результата.
Можно было упростить процесс и сразу заносить в новую таблицу
только те состояния, которые действительно могут возникнуть. Тогда
состояния pr и qr в ней бы не появились. Заключительным состоянием
исходного автомата было состояние r, следовательно, в новом автомате
это будет состояние pqr. Если бы, к примеру, состояние pr было
достижимым, то оно тоже являлось бы заключительным, так как автомат
может иметь несколько заключительных состояний. После удаления
недостижимых состояний таблица примет вид:
вход
вход
Для удобства переобозначим
1
0
1
состояние 0
состояние
состояния: заменим p на A, pq
p
{p} {pq} на B, pqr – на C. Тогда
A
{A} {B}
pq
{p} {pqr} начальным состоянием будет A,
B
{A} {C}
pqr
{p} {pqr} конечным C.
C
{A} {C}
В итоге полученный детерминированный автомат, эквивалентный
исходному НКА, будет иметь вид: M=({A,B,C},{0,1},,A,{C}), граф
функции переходов :
0
1
1
A
0
C
B
1
0
Минимизация КА
Многие КА возможно минимизировать, т.е. построить эквивалентный
КА с минимально возможным числом состояний. Для этого существует
алгоритм минимизации автомата. Дадим необходимые определения.
Пусть q1 и q2 – состояния автомата M, на вход подается цепочка
символов w длины k0: wV*, wk, (q1,w) ├─* (q3,), (q2,w) ├─* (q4,).
Если одно из состояний (q3 или q4) входит в F, а другое – нет, то говорят,
что цепочка w различает состояния q1 и q2. Если же для любой входной
цепочки w длины k она не различает состояния q1 и q2, то говорят, что они
являются k-эквивалентными или k-неразличимыми. Множество Kэквивалентных состояний составляет класс эквивалентности R(k).
Очевидно, что множества F и Q\F являются 0-эквивалентными,
записывается этот факт следующим образом: R(0)={F,Q\F}.
Для построения минимального автомата строятся классы
эквивалентности R(n).
30
Алгоритм построения классов эквивалентности:
1. Полагаем n:=0, строится R(0)= {F,Q\F}.
2. n:=n+1. Строится R(n) на основании R(n–1): в класс эквивалентности
R(n) входят те состояния, которые по одинаковым символам переходят в
n–1-эквивалентные состояния: R(n):={rij(n): {qijQ: aV (qij,a) rj(n–1)}
i,jN}.
3. Если R(n)= R(n–1), то работа заканчивается. Иначе переход на шаг 2.
Алгоритм минимизации автомата:
1. Исключить все недостижимые состояния.
2. Построить классы эквивалентности.
3. Каждый из этих классов становится состоянием нового автомата.
4. Функция переходов строится на основании функции переходов
исходного автомата и новых состояний.
Рассмотрим пример. Пусть ДКА имеет вид: M = ({q0, q1, q2, q3, q4, q5},
{0,1}, , q0, {q4, q5}), а функция переходов задана графом:
q0
0
1
1 1
1
q2
0
q1
q3
0
0
q4
0,1 0,1
q5
Требуется минимизировать данный автомат.
Недостижимых состояний нет. Построим классы эквивалентности:
R(0)={{q0,q1,q2,q3}, {q4,q5}}. Из состояния q2 по 0 происходит переход в q3,
а из q1 – в q4. Состояния q3 и q4 находятся в разных классах
эквивалентности R(0)  q1 и q2 будут входить в разные классы R(1).
Аналогично рассуждая про остальные состояния, получим:
R(1)={{q4,q5},{q0,q2},{q1,q3}}.
Аналогично: R(2)={{q4,q5}, {q0,q2}, {q1,q3}}.  В новом автомате 3
состояния. Обозначим их по номеру одного из состояний каждого класса.
M’ = {q0,q1,q4}, {0,1}, ’, q0, {q4}). Функция переходов будет иметь вид:
1
q0
1
0
0,1
q1
q4
0
Данный автомат принимает цепочки, состоящие из нулей и единиц, в
которых обязательно присутствуют два подряд расположенных нуля.
31
2.3 Особенности регулярных языков
2.3.1 Способы задания регулярных языков
Регулярные грамматики, конечные автоматы и регулярные множества
(и обозначающие их регулярные выражения) представляют собой три
различных способа задания регулярных языков.
Утверждение
1. Язык является РМ тогда и только тогда, когда он задан леволинейной
(праволинейной) грамматикой. Язык может быть задан леволинейной
(праволинейной) грамматикой тогда и только тогда, когда он является
регулярным множеством.
2. Язык является РМ тогда и только тогда, когда он задан с помощью
конечного автомата. Язык распознается с помощью конечного автомата
тогда и только тогда, когда он является РМ.
Все эти три способа эквивалентны. Существуют алгоритмы,
позволяющие для языка, заданного одним из способов, построить другой
способ, определяющий этот же язык (см. список литературы).
Например, для нахождения регулярного выражения для языка,
заданного праволинейной грамматикой, необходимо построить и решить
систему уравнений с регулярными коэффициентами.
В теории языков программирования наиболее важную роль играет
эквивалентность КА и регулярных грамматик, поскольку такие
грамматики используются для определения лексических конструкций
языков программирования. Создав на основе известной грамматики
автомат, получаем распознаватель для данного языка. Таким образом
удается решить задачу разбора для лексических конструкций языка.
Для построения КА на основе известной регулярной грамматики её
необходимо привести к автоматному виду. Множество состояний автомата
будет соответствовать множеству нетерминальных символов грамматики.
2.3.2 Свойства регулярных языков
Регулярные множества замкнуты относительно операций пересечения,
объединения, дополнения, итерации, конкатенации, изменения имен
символов и подстановки цепочек вместо символов.
Для регулярных языков разрешимы многие проблемы, неразрешимые
для других типов языков. Например, следующие проблемы являются
разрешимыми независимо от того, каким из способов задан язык:
Проблема эквивалентности: Даны два регулярных языка L1(V) и
L2(V). Необходимо установить, являются ли они эквивалентными.
Проблема принадлежности цепочки языку. Дан регулярный язык
L(V), цепочка символов V*. Требуется проверить, принадлежит ли эта
32
цепочка языку.
Проблема пустоты языка. Дан регулярный язык L(V). Необходимо
проверить, является ли этот язык пустым, т.е. найти хотя бы одну цепочку
, L(V).
Иногда бывает необходимо доказать, является ли некоторый язык
регулярным. Если возможно задать этот язык одним из рассмотренных
способов, то он является регулярным. Но если такой способ найти не
удается, неизвестно, является язык регулярным или нет.
Для регулярных языков выполняется т.н. лемма о разрастании
регулярных языков. Она часто используется для доказательства (от
противного) того факта, что некоторый язык не является регулярным. Для
такого доказательства предполагают, что язык является регулярным, в
таком случае лемма должна выполняться. Если в результате получается
противоречие, это доказывает, что предположение было неверным,
следовательно, язык регулярным не является.
Лемма о разрастании регулярных языков формулируется следующим
образом. Если дан регулярный язык и достаточно длинная цепочка
символов, принадлежащая этому языку, то в ней можно найти непустую
подцепочку, которую можно повторить сколь угодно много раз, и все
полученные таким образом цепочки также будут принадлежать
рассматриваемому регулярному языку.
Формально лемма записывается так. Если дан регулярный язык L, то 
константа p>0, такая, что если L и p, то цепочку  можно записать
в виде , где 0<p, и тогда =i, L i0.
Пример. Рассмотрим язык L={anbnn>0}. Докажем, что он не является
регулярным, используя для этого лемму о разрастании языков.
Пусть этот язык регулярный, тогда для него должна выполняться
лемма о разрастании. Возьмем некоторую цепочку этого языка = anbn и
запишем ее в виде . Если a+ или b+, то тогда цепочка i не
принадлежит языку для любого i, что противоречит условиям леммы.
Если же a+b+, то цепочка 2 также не принадлежит языку L.
Получили противоречие, следовательно, язык не является регулярным.
33
Download