Добавил:
Upload Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:
Лекции по информатике / Раздел07(Трансляция, компиляция и интерпретация).doc
Скачиваний:
197
Добавлен:
09.05.2015
Размер:
291.84 Кб
Скачать

Формальные языки и грамматики. Простейший компилятор.

Алфавит  это любое множество символов. Понятие символа не определяется. Цепочка символов 0,1,2 записывается как «012» (или 012). Другие обозначения:

xR  цепочка x с символами в обратном порядке

xn  цепочка x, повторенная n раз

x*  цепочка x, повторенная 0 или более раз

x+  цепочка x, повторенная 1 или более раз

xy  сцепление (конкатенация) цепочек x и y

|x|  длина (число символов) цепочки x

e  пустая цепочка

Цепочку из одного символа будем обозначать самим символом. Буквы x,y,z,u,v,w,t будем применять для обозначения цепочек. Множество всех цепочек из элементов множества E естественно обозначить через E*. Язык - это подмножество E*. Примеры языков: Си, русский  { 0 1 | n >= 0 }.

Язык программирования можно задать:

 перечислив все цепочки;

 написав программу-распознаватель, которая получает на вход цепочку символов и выдает ответ «да», если цепочка принадлежит языку и «нет» в противном случае;

 с помощью механизма порождения  грамматики.

Чтобы задать грамматику, требуется указать:

 множество символов алфавита (или терминальных символов) E. Будем обозначать их строчными символами алфавита и цифрами;

 множество нетерминальных символов (или метасимволов), не пересекающееся с E со специально выделенным начальным символом S. Будем обозначать их прописными буквами;

 множество правил вывода, определяющих правила подстановки для цепочек. Каждое правило состоит из двух цепочек (например, x и y), причем x должна содержать по крайней мере один нетерминал; и означает, что цепочку x в процессе вывода можно заменить на y. Вывод цепочек языка начинается с нетерминала S. Правило грамматики будем записывать в виде x : y. (Также употребляется запись x ::= y или x -> y)

Более строго, определим понятие выводимой цепочки:

 S  выводимая цепочка;

 если xyz  выводимая цепочка и в грамматике имеется правило y:t, то xtz  выводимая цепочка;

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

Примеры:

а) S : e б) S : e

S : 0S1 S : (S)

S : SS

Для сокращения записи принято использовать символ «или» - «|».

Короткая форма записи предыдущих примеров:

а) S : e | 0S1 б) S : e | (S) | SS

Более сложный пример:

в) S : aSBC | abC

CB : BC

bB : bb

cC : cc

bC : bc

n n n

Эта грамматика порождает язык a b c .

Грамматики в свою очередь образуют т.н. метаязык. Выше была описана «академическая» форма записи метаязыка. Существуют различные способы записи синтаксических правил, что в основном определяется условными обозначениям и ограничениями на структуру правил, принятыми в используемых метаязыках. Метаязыки используются для задания грамматики языков программирования со времен Алгола 60. Кратко рассмотрим основные вехи становления и развития метаязыков. Во всех случаях будем определять идентификатор.

Метаязык Хомского

Метаязык Хомского вышел из недр математической логики. Он имеет следующую систему обозначений:

  • символ “” отделяет левую часть правила от правой (читается как «порождает» и «это есть»);

  • нетерминалы обозначаются буквой А с индексом, указывающим на его номер;

  • терминалы - это символы используемые в описываемом языке;

  • каждое правило определяет порождение одной новой цепочки, причем один и тот же нетерминал может встречаться в нескольких правилах слева.

Описание идентификатора на метаязыке Хомского будет выглядеть следующим образом:

1. A1 A

23. A1  W

45. A1  s

2. A1  B

24. A1  X

46. A1  t

3. A1  C

25. A1  Y

47. A1  u

4. A1  D

26. A1  Z

48. A1  v

5. A1  E

27. A1  a

49. A1  w

6. A1  F

28. A1  b

50. A1  x

7. A1  G

29. A1  c

51. A1  y

8. A1  H

30. A1  d

52. A1  z

9. A1  I

31. A1  e

53. A2  0

10. A1  J

32. A1  f

54. A2  1

11. A1  K

33. A1  g

55. A2  2

12. A1  L

34. A1  h

56. A2  3

13. A1  M

35. A1  i

57. A2  4

14. A1  N

36. A1  j

58. A2  5

15. A1  O

37. A1  k

59. A2  6

16. A1  P

38. A1  l

60. A2  7

17. A1  Q

39. A1  m

61. A2  8

18. A1  R

40. A1  n

62. A2  9

19. A1  S

41. A1  o

63. A3  A1

20. A1  T

42. A1  p

64. A3  A3A1

21. A1  U

43. A1  q

65. A3  A3A2

22. A1  V

44. A1  r

 

Метаязык Хомского-Щутценберже

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

  • символ “=” отделяет левую часть правила от правой (вместо символа “”);

  • нетерминалы обозначаются буквой А с индексом, указывающим на его номер;

  • терминалы - это символы используемые в описываемом языке;

  • каждое правило определяет порождение нескольких альтернативных цепочек, отделяемых друг от друга символом “+”, что позволяет, при желании, использовать в левой части только разные нетерминалы.

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

  1. A1=A+B+C+D+E+F+G+H+I+J+K+L+M+N+O+P+Q+R+S+T+ U+V+W+X+Y+Z+a+b+c+d+e+f+g+h+i+j+k+l+m+n+o+p+q+ r+s+t+u+v+w+x+y+z

  2. A2=0+1+2+4+5+6+7+8+9

  3. A3=A1+A3A1+A3A2

Бэкуса-Наура формы (БНФ)

Метаязыки Хомского и Хомского-Щутценберже использовались в математической литературе при описании простых абстрактных языков. Метаязык, предложенный Бэкусом и Науром, впервые использовался для описания синтаксиса реального языка программирования Алгол 60. Наряду с новыми обозначениями метасимволов, в нем использовались содержательные обозначения нетерминалов. Это сделало описание языка нагляднее и позволило в дальнейшем широко использовать данную нотацию для описания реальных языков программирования. Были использованы следующие обозначения:

  • символ «::=« отделяет левую часть правила от правой;

  • нетерминалы обозначаются произвольной символьной строкой, заключенной в угловые скобки «<« и «>«;

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

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

Пример описания идентификатора с использованием БНФ:

  1. <буква> :: = А|В|С|D|E|F|G|H|I|J|K|L|M|N|O|P|Q|R|S|T|U|V| W|X|Y|Z|a|b|c|d|e|f|g|h|i|j|k|l|m|n|o|p|q|r|s|t|u|v|w|x|y|z

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

  3. <идентификатор> ::= <буква> | <идентификатор><буква> | <идентификатор><цифра>

Правила можно задавать и раздельно:

  1. <идентификатор> :: = <буква>

  2. <идентификатор> :: = <идентификатор> <буква>

  3. <идентификатор> :: = <идентификатор> <цифра>

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

<число> : <цифра> | <цифра> <число>

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

Расширенные Бэкуса-Наура формы (РБНФ)

Метаязыки, представленные выше, позволяют описывать любой синтаксис. Однако, для повышения удобства и компактности описания, целесообразно вести в язык дополнительные конструкции. В частности, специальные метасимволы были разработаны для описания необязательных цепочек, повторяющихся цепочек, обязательных альтернативных цепочек. Существуют различные расширенные формы метаязыков, незначительно отличающиеся друг от друга. Их разнообразие зачастую объясняется желанием разработчиков языков программирования по-своему описать создаваемый язык. К примерам таких широко известных метаязыков можно отнести: метаязык PL/I, метаязык Вирта, используемый при описании Модулы-2, метаязык Кернигана-Ритчи, описывающий Си. Зачастую такие языки называются расширенными формами Бэкуса-Наура (РБНФ).

В частности, РБНФ, используемые Виртом, имеют следующие особенности:

  • Квадратные скобки «[« и «]» означают, что заключенная в них синтаксическая конструкция может отсутствовать;

  • фигурные скобки «{« и «}» означают ее повторение (возможно, 0 раз);

  • круглые скобки «(« и «)» используются для ограничения альтернативных конструкций;

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

Если нетерминал состоит из нескольких смысловых слов, то они должны быть написаны слитно. В этом случае для повышения удобства в восприятии фразы целесообразно каждое ее слово начинать с заглавной буквы или разделять слова во фразах символом подчеркивания. Терминальные символы изображаются словами, написанными буквами латинского алфавита (зарезервированные слова) или цепочками знаков, заключенными в кавычки. Синтаксическим правилам предшествует знак «$» в начале строки. Каждое правило оканчивается знаком «.» (точка). Левая часть правила отделяется от правой знаком «=« (равно), а альтернативы - вертикальной чертой «|». Этот вариант РБНФ и будет использоваться для описания синтаксиса языков в лабораторной работе. В соответствии с данными правилами синтаксис идентификатора будет выглядеть следующим образом:

$ буква = «A»|»B»|»C»|»D»|»E»|»F»|»G»|»H»|»I»|»J»|»K»|»L»|»M»|»N»|»O»|»P»|»Q»|»R»| «S»|»T»|»U»|»V»|»W»|»X»|»Y»|»Z»|»a»|»b»|»c»|»d»|»e»|»f»|»g»|»h»|»i»|»j»|»k»|»l»|»m»|»n»| «o»|»p»|»q»|»r»|»s»|»t»|»u»|»v»|»w»|»x»|»y»|»z».

$ цифра = «0»|»1»|»2»|»3»|»4»|»5»|»6»|»7»|»8»|»9».

$ идентификатор = буква {буква | цифра}.

Диаграммы Вирта

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

Рисунок 1  Графические примитивы, используемые при построении диаграмм Вирта

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

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

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

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

  • альтернативы в правилах задаются ветвлением дуг, а итерации - их слиянием;

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

Пример описания идентификатора с использованием диаграмм Вирта представлен на рисунке 2.

Рисунок 2  Описание идентификатора с использованием диаграмм Вирта

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

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

Рассмотрим язык простейших арифметических формул:

Опишем грамматику математической формулы с использованием метаязыка Бэкуса-Наура:

<формула> : (<формула>) | <число> | <формула><знак><формула>

<знак> : + | *

Почему «3+5*2» является формулой? Приведем последовательность преобразований цепочек (так называемый «разбор» или «вывод»):

Сокращенно наличие вывода (цепочки преобразований) будем за писывать в виде <формула>::3+5*2. Большинство грамматик допускают несколько различных выводов для одной и той же цепочки из языка. Постройте другой вывод для цепочки «3+5*2»  упражнение.

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

Изобразим выполняемые замены цепочек в виде т.н. «дерева разбора» (или дерева вывода). По традиции дерево изображается «вверх ногами»:

Нарисованное дерево имеет ветви (линии) и узлы (помечены терминалами и нетерминалами), из которых растут ветви. Конечные узлы (терминалы) называются листьями. Понятия «поддерево», «корень дерева», видимо, не нуждаются в определении.

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

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

<формула> : <терм> | <терм><знак><формула>

<терм> : (<формула>) | <число>

<знак> : + | *

Как изменится дерево разбора? Дерево разбора определяет некоторую структуру цепочки языка. Так, мы видим, что подцепочка «3+5» является «формулой». Это противоречит нашим (интуитивным) понятиям о смысле исходной формулы: 3+5 в отличие от 5*2 не является подвыражением. Мы можем учесть приоритет операций, изменив грамматику:

<формула> : <терм> | <формула> + <терм>

<терм> : <элемент> | <терм> * <элемент>

<элемент> : (<формула>) | <число>

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

Примеры:

2+3 2 3 +

2*3+4 2 3 * 4 +

2*(3+4) 2 3 4 + *

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

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

CompForm() {

CompForm()

...

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

Если устранить левую рекурсию:

<формула> : <терм> | <терм><плюс минус><формула>

<терм> : <элемент> | <элемент><умножить разделить><терм>

<плюс минус> : + | -

<умножить разделить> : * | /

то описанная проблема исчезнет, рекурсивный компилятор можно будет написать, однако появятся новые трудности (какое дерево разбора будет соответствовать цепочке «5-3-2»?).

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

Наиболее просто решить эту проблему можно, добавив в метаязык НФБН символы итерации {} «повторить 0 или более раз». С применением новых обозначений грамматика легко запишется без левой рекурсии:

<формула> : <терм> { <плюс минус> <формула> }

<терм> : <элемент> { <умножить разделить> <элемент> }

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

char *infix, *postfix; /* указатели на входную и выход-

ную цепочки */

CompForm() { /* компилировать формулу */

register char sign;

CompTerm();

while ( (sign = *infix) == '+' || sign == '-' ) {

++infix;

CompTerm();

*postfix++ = sign;

*postfix++ = ' ';

}

}

CompTerm() { /* компилировать терм */

register char sign;

CompEl();

while ( (sign = *infix) == '*' || sign == '/' ) {

++infix;

CompEl();

*postfix++ = sign;

*postfix++ = ' ';

}

}

CompEl () { /* компилировать элемент */

if ( *infix == '(' ) {

++infix;

CompForm();

if ( *infix++ != ')' ) error();

} else {

if ( !isdigit(*infix) ) error();

while ( isdigit( *infix ) ) *postfix++ = *infix++;

*postfix++ = ' ';

}

}

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

Функция error в компиляторе служит для вывода сообщения о том, что предъявленная цепочка не входит в язык арифметических формул. Если компилятор при получении на вход цепочки не выдает сообщения об ошибке, говорят, что эта цепочка допущена.

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

30