Добавил:
Upload Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:
СПО юнита 2.doc
Скачиваний:
38
Добавлен:
17.11.2019
Размер:
5.82 Mб
Скачать

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

Контексно-свободная грамматика – грамматика, все продукции которой содержат в левой части единственный символ.

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

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

Грамматика определяется как следующая четверка чисел:

Vt, Vn, P, S,

где Vt – алфавит, символы которого называют терминальными символами или терминалами; Vn – алфавит с нетерминальными символами или нетерминалами. Vt и Vn не имеют общих символов, т.е. Vt Vn = 0, а V определяется как Vt  Vn; Р – множество продукций или правил, каждый элемент которого состоит из пары (, ), где  – левая часть продукции,  – правая часть продукции, а сама продукция записывается следующим образом:

  

Здесь  принадлежит V (строки, состоящие из одного или более символов V), а  принадлежит V (строки, состоящие из нуля или более символов V).

S принадлежит Vn и называется символом предложения, или аксиомой грамматики. С него начинается генерация любой строки языка.

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

Поскольку пустая строка также принадлежит языку, то в набор Р входит продукция

S  

Строка ххууу генерируется следующим образом:

S => xS => xxS => ххуВ => ххууВ = ххууу

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

*

S => хууу

Это означает, что хууу порождается из S за нуль или более шагов.

Выражение

+

S => хууу

означает, что хууу порождается из S за один или более шагов.

Будем использовать строчные буквы или слова, состоящие только из строчных букв, для обозначения терминалов грамматики, а заглавные буквы или слова из заглавных букв – для обозначения нетерминалов. Символы предложений обозначаются буквой S, хотя это обозначение не является обязательным. Греческие буквы используются для обозначения строк терминалов и/или нетерминалов. Во всех случаях, когда будут использоваться другие обозначения, это будет оговариваться.

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

Введем понятие иерархии Хомского грамматик / языков. Хомский определил четыре класса грамматик, которые назвал грамматиками 0-го, ..., 3-го типов. Грамматики 0-го типа, или рекурсивно перечислимые грамматики, определяются как все грамматики, которые соответствуют данному выше определению без ограничений на типы продукций. Данные грамматики – это наиболее общий класс, а грамматики других типов могут быть получены путем наложения ограничений на возможные формы продукций грамматик 0-го типа.

Грамматики 0-го типа эквивалентны машинам Тьюринга в том смысле, что для любой данной грамматики 0-го типа существует машина Тьюринга, которая допускает, и только допускает, все предложения, сгенерированные данной грамматикой. И наоборот, для данной машины Тьюринга существует грамматика 0-го типа, которая генерирует точно все предложения, допускаемые машиной Тьюринга.

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

  

длина строки , исчисляемая в количестве символов, которое она может содержать, была не больше длины строки .

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

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

В контекстно-свободных грамматиках удобно разрешить продукцию S  . Хотя строго эта продукция не разрешена даже в контекстно-зависимых грамматиках. Это позволяет включить в язык пустую строку. В некоторых грамматиках будут встречаться продукции, в которых пустые строки генерируют другие нетерминалы. Грамматики 2-го типа эквивалентны магазинным автоматам.

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

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

Регулярная грамматика – грамматика, яляющаяся праволинейной. Леволинейная грамматика, которая определяется по аналогии с праволинейной, также по определению относится к регулярным.

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

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

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

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

Иерархия Хомского является включающей, так что все грамматики 3-го типа являются грамматиками 2-го, все грамматики 2-го типа являются грамматиками 1-го, а все грамматики 1-го типа – грамматиками 0-го типа.

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

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

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

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

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

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

– множество токенов, представляющих собой терминальные символы или терминалы;

– множество нетерминальных символов;

– множество продукций, каждая из которых состоит из нетерминала, называемого левой частью продукции, стрелки и последовательности токенов и/или нетерминалов, называемых правой частью продукции;

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

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

Продукция называется продукцией нетерминала, если он записан в левой части. Строка токенов является последовательностью из нуля или нескольких токенов. Пустая строка – строка, содержащая нуль токенов и записываемая как .

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

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

Если S1 и S2 являются инструкциями, а E – выражением, то

if E then S1, else S2

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

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

Контекстно-свободная грамматика состоит из терминалов, нетерминалов, стартового символа и продукций:

терминалы – базовые символы, из которых формируются строки. Слово "токен" является синонимом слова "терминал", когда речь идет о грамматиках языков программирования. Ключевые слова if, then, else являюется терминалами;

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

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

– продукции грамматики определяют способ, которым терминалы и нетерминалы могут объединяться для создания строк. Каждая продукция состоит из нетерминала, за которым следует стрелка (или символ : : =), и строка нетерминалов и терминалов.

Соглашения по обозначениям

Соглашения по обозначениям при записи грамматик:

1. Символы, являющиеся терминалами:

– строчные буквы из начала алфавита (а, b, с);

– символы операторов (+, –);

– символы пунктуации (запятые, скобки);

– цифры 0, 1, ..., 9;

– строки, выделенные полужирным шрифтом (id или if).

2. Символы, являющиеся нетерминалами:

– прописные буквы из начала алфавита (А, В, С);

– буква S, которая обычно означает стартовый символ;

– имена из строчных букв, выделенные курсивом (stmt или ехрr).

3. Прописные буквы из конца алфавита, такие как X, У, Z, представляют грамматические символы, т.е. либо терминалы, либо нетерминалы.

4. Строчные буквы из конца алфавита, такие как u, v, ..., z, обозначают строки терминалов.

5. Строчные греческие буквы, такие как , ,  представляют строки грамматических символов. Таким образом, в общем виде продукция может быть записана как А  , в которой одиночный нетерминал А располагается слева от стрелки (в левой части продукции), а строка грамматических символов  – справа от стрелки (в правой части продукции).

6. Если А  1, А  2, ..., А  к представляют собой продукции с А в левой части, то можем записать А  12…к. Мы называем 1, 2, …, к альтернативами А.

7. Если иное не указано явно, левая часть первой продукции является стартовым символом.

Можно механически преобразовать недетерминированный конечный автомат в грамматику, порождающую тот же язык, что и распознаваемый НКА. Для каждого состояния i в НКА создается нетерминальный символ Аi. Если состояние i имеет переход в состояние j по символу a, вводим в грамматику соответствующую продукцию Аi → aAJ. Если состояние i переходит в состояние j для входа , то добавляем продукцию Аi  AJ. Для заключительного состояния i вводим продукцию Аi → . Если же i – начальное состояние, то Аi становится стартовым символом грамматики.

Причины использования регулярных выражений для определения лексической структуры языка:

– лексические правила языка зачастую достаточно просты, и для их описания не нужен такой мощный инструмент, как грамматика;

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

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

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

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

Ограничения контекстно-свободных грамматик

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

var x : integers;

begin х : = ' ? '

является некорректным, поскольку х может принимать только целые значения. Подобным образом, если переменная р объявлена как

procedure p (i, j; integer);

ее нельзя вызвать следующим образом:

Р(3, 4, 5),

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

var A[1…10] of integer;

запись

А[2, 3] := 0

будет ошибочной.

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

– отсутствие наглядности и неинтуитивная природа грамматик 0-го типа. В качестве иллюстрации можно рассмотреть грамматику G3;

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

Из второго пункта следует, что программа синтаксического анализа, основанная на грамматике 0-го типа, будет обладать теми же плюсами и минусами, что и машина Тьюринга.

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

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

Программы синтаксического анализа включают следующие элементы:

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

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

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

Определим значения выражений, которые задаются грамматикой со следующими продукциями:

  1. <ехрr> ::= <ехрr> "+" <term>

  2. <expr> ::= <term>

  3. <term> :: = <term> "*" <factor>

  4. <term> :=> <factor>

  5. <factot> ::= constant

  6. <factor> ::= “(“<ехрr>”)”

Здесь терминалы взяты в скобки, чтобы показать, что они являются действительным представлением терминалов, и отличить от имени терминала (constant), которое представляет набор действительных представлений.

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

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

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

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

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

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

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

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

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

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

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

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

Синтаксически управляемые определения

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

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

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