Добавил:
Upload Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:
УМК по СПО.doc
Скачиваний:
0
Добавлен:
01.07.2025
Размер:
1.79 Mб
Скачать

6.12.Контроль

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

7.Модуль Инструментальные средства для построения трансляторов

7.1.Инструментальные средства для построения компиляторов

Реализация компилятора для настоящего языка программирования всегда является весьма трудоемкой задачей, и поэтому давно предпринимались попытки автоматизировать этот процесс. Чаще всего объектом приложения таких усилий служили лучше всего изученные (и наиболее простые) части компилятора - сканер и парсер. Рассмотрим два инструментальных средства: Lex - предназначен для реализации сканера, Yacc - для построения парсера. Эти программы разработаны в Белловской лаборатории и получили широкую известность благодаря системе UNIX, в состав которой они включены.

Они компилируют описания сканера и парсера соответственно, записанные на специальных языках, в C-программы, реализующие эти компоненты компилятора.

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

7.1.1.Построитель лексических анализаторов Lex

Лексический анализ

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

Весьма полезным инструментом для построения лексических анализаторов является lex(1). Работа лексических анализаторов, порожденных lex'ом, хорошо согласуется с процедурами синтаксического разбора, порожденными yacc'ом. Спецификации лексических анализаторов используют не грамматические правила, а регулярные выражения. lex удобно использовать для порождения довольно хитроумных лексических анализаторов, однако остаются отдельные языки (такие как ФОРТРАН), которые не опираются ни на какое теоретическое обоснование и лексические анализаторы для которых приходится мастерить вручную.

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

Регулярные выражения в Lex-правилах

Регулярные выражения определяют лексему. Регулярное выражение может содержать символы латинского и русского алфавитов в верхнем и нижнем регистрах, другие символы (цифры, знаки препинания и т.д.) и символы-операторы. Рассмотрим метаязык, используемый для описания регулярных выражений LEX'a.

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

Обозначения символов в выражениях

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

"abc"

abc эти последовательности символов идентичны.

Для того, чтобы включить в выражение символы, имеющие специальное значение в метаязыке ( + - * ? ( ) [ ] { } | / \ ^ $ . < > ), следует предварять их знаком \ или употреблять внутри кавычек. Непечатные символы можно задавать как в C.

a "a" \a - три способа задать выражение, состоящее из символа a

.

точка означает любой символ, кроме символа новой строки "\n";

\XXX

Указание символа его восьмеричным кодом (как в Си);

\n

символ новой строки;

\t

символ табуляции;

\b

возврат курсора на один шаг назад;

" "

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

Операторы регулярных выражений

Операторы обозначаются символами-операторами, к ним относятся:

\ ^ ? * + | $ / %

[] {} () <>

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

Например:

abc+ - символ "+" - оператор;

abc\+ - символ "+";

abc"+" - символ "+".

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

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

[abc] означает либо символ "a", либо "b", либо символ "c";

Знак - используется для указания любого символа из лексикографически упорядоченной последовательности:

[A-z] означает любой латинский символ;

[А-Я] любая прописная русская буква;

[+-0-9] все цифры и знаки "+" и "-".

[A-Za-z_] большая или маленькая латинская буква или знак _

[^0-9A-Fa-f] что угодно кроме шестнадцатеричных цифр.

Повторители

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

Оператор * означает любое (в том числе и 0) число вхождений символа или класса символов. Например:

x* любое число вхождений символа "x";

abc* любое число вхождений цепочки "abc";

[A-z]* любое число вхождений любой латинской буквы;

[A-ZА-Яa-zа-я_0-9]* любое вхождение русских и латинских букв, знака подчеркивания и цифр.

Оператор + означает одно и более вхождений. Например:

x+ одно или более вхождений "x";

[0-9]+ одно или более вхождений цифр;

abc+ одно или более вхождений цепочки abc;

[A-z]+ одно или более вхождений любой латинской буквы.

Если прибегнуть к метаязыку (НФБН) для продолжения описания метаязыка LEXа, служащего для описания регулярных выражений, то получим следующие правила. Нетерминал r следует произносить как "описатель регулярного выражения". Правила грамматики указаны в порядке уменьшения приоритетов операций.

r: r? необязательное вхождение r

r: r r конкатенация двух цепочек:

[A-Za-z][A-Za-z0-9]* последовательность букв или цифр, начинающаяся с буквы.

Операторы выбора

Операторы / | ? $ ^ управляют процессом выбора символов.

ab/cd "ab" учитывается только тогда, когда за ним следует "cd".

ab|cd или "ab", или "cd".

x? означает необязательный символ "x".

_?[A-Za-z]* означает, что перед цепочкой любого количества латинских букв может быть необязательный знак подчеркивания.

-?[0-9]+ выделит любое целое число с необязательным минусом впереди.

x$ означает выбрать символ "x", если он является последним в строке. Стоит перед символом "\n"!

abc$ означает выбрать цепочку "abc", если она завершает строку.

^x означает выбрать символ "x", если он является первым символом строки;

^abc означает выбрать цепочку символов "abc", если она начинает строку.

[^A-Z]* означает все символы, кроме прописных латинских букв. Когда символ ^ стоит перед выражением или внутри [], он выполняет операцию дополнение. Внутри квадратных скобок символ ^ должен обязательно стоять первым у открывающей скобки.

^#" "*define оператор препроцессора C

(+|-)?[0-9]+ целая константа со знаком

r : (r) для указания порядка вычисления можно использовать обычные круглые скобки.

Оператор {}

Оператор {} имеет два различных применения:

x{n,m} здесь n и m натуральные, m > n. Означает от n до m вхождений x, например, x{2,7} - от 2 до 7 вхождений x.

r: r{m,n} повторение r от m до n раз

r: r{m} повторение r ровно m раз

r: r{m,} повторение r m или более раз

[A-Za-z] ([A-Za-z0-9]){0,5} - идентификатор фортрана

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

Пример:

БУКВА [A-ZА-Яa-zа-я_]

ЦИФРА [0-9]

ИДЕНТИФИКАТОР {БУКВА}({БУКВА}|{ЦИФРА})*

%%

{ИДЕНТИФИКАТОР} printf("\n%s",yytext);

lex построит лексический анализатор, который будет определять и выводить все "слова" из входного файла. Под словом в данном случае подразумевается идентификатор Си-программы. В этом примере {ИДЕНТИФИКАТОР} будет заменен на {БУКВА}({БУКВА}|{ЦИФРА})*, затем на [A-ZА-Яa-zа-я_]([A-ZА- Яa-zа-я_]|[0-9])*.

yytext - это внешний массив символов программы lex.yy.c, которую строит lex. yytext формируется в процессе чтения входного файла и содержит текст, для которого установлено соответствие какому-либо выражению. Этот массив доступен пользовательским разделам Lex-программы.

Оператор printf выводит каждый идентификатор на новой строке.

Оператор <>. Служебные слова START и BEGIN

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

Начальные условия Lex-программы помещаются в раздел определений, а неактивные правила помечаются соответствующими условиями. Оператор START позволяет указать список начальных условий Lex-программы, а оператор BEGIN позволяет активировать правила, помеченные начальными условиями.

Активные правила имеют следующий синтаксис:

РЕГУЛЯРНОЕ_ВЫРАЖЕНИЕ ДЕЙСТВИЕ

Неактивные правила имеют следующий синтаксис:

<МЕТКА_УСЛОВИЯ>РЕГ_ВЫРАЖЕНИЕ ДЕЙСТВИЕ

ВАЖНО: любое правило должно начинаться с первой позиции строки, пробелы и табуляции недопустимы - они используются как разделители между регулярным выражением и действием в правиле!

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

%START AA BB CC DD

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

Каждое правило, перед которым указан оператор типа "<МЕТКА>", мы будем называть помеченным правилом. Метка формируется так же, как и метка в Си.

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

<МЕТКА1,МЕТКА2,МЕТКА3>x ДЕЙСТВИЕ

Запятая - обязательный разделитель списка меток!

Структура Lex-программы

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

/********************* Программа wc.lex *******************************/

/***************** Секция определений *********************************/

/* NODELIM означает любой символ, кроме разделителей слов */

NODELIM [^" "\t\n]

int l, w, c; /* Число строк, слов, символов */

%% /******************** Секция правил ***********************/

{ l=w=c=0; /* Инициализация */ }

{NODELIM}+ { w++; c+=yyleng; /* Слово */ }

\n { l++; /* Перевод строки */ }

. { c++; /* Остальные символы */ }

%% /******************* Секция программ *****************************/

int main() { yylex(); return 0; }

int yywrap() { /* Вызывается при достижении конца входного файла */

printf( " Lines - %d Words - %d Chars - %d\n", l, w, c );

return( 1 );

}

/****************** Конец программы wc.lex ****************************/

Можно заметить, что LEX-программа состоит из трех секций, отделяемых друг от друга символом %%. Lex-программа включает разделы

опредeлений;

правил;

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

Рассмотрим подробнее способы оформления этих разделов.

Все строки, в которых занята первая позиция, относятся к Lex-программе. Любая строка, не являющаяся частью правила или действия, которая начинается с пробела или табуляции, копируется в сгенерированную программу lex.yy.c - результат работы lex.

Раздел определений Lex-программы

Раздел содержит определения макросимволов (или, если угодно, нетерминалов грамматики регулярных выражений). Каждое описание начинается с первой позиции строки и имеет вид "имя_макросимвола строка". Вместо последовательности {имя_макросимвола}, встреченной после его определения, будет подставлена соответствующая ему строка. Макро полезны для задания сложных выражений, например:

letter [a-zA-Z_#]

digit [0-9]

ident {letter}({letter}|{digit})*

iconst (\+|\-)?{iuconst}

Определения, предназначенные для lex, помещаются перед первым %%. Любая строка этого раздела, не содержащаяся между %{ и %} и начинающаяся в первой колонке, является определением строки подстановки lex. Раздел определений Lex-программы может включать:

начальные условия,

определения,

фрагменты программы пользователя,

таблицы наборов символов,

указатели host-языка,

изменения размеров внутренних массивов,

комментарии в формате host-языка.

НАЧАЛЬНЫЕ УСЛОВИЯ задаются в форме:

%START имя1 имя2 ...

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

ОПРЕДЕЛЕНИЯ задаются в форме:

имя трансляция

В качестве разделителя используется один или более пробелов или табуляций. Пример:

БУКВА [A-ZА-Яa-zа-я_]

ЦИФРА [0-9]

ИДЕНТИФИКАТОР {БУКВА}({БУКВА}|{DIGIT})*

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

ФРАГМЕНТЫ ПРОГРАММЫ ПОЛЬЗОВАТЕЛЯ указываются двумя способами:

в виде "пробел фрагмент";

в виде:

%{

строки

фрагмента

программы

пользователя

%}

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

ТАБЛИЦА НАБОРОВ СИМВОЛОВ задается в виде:

%T

целое_число строка_символов

.........

целое_число строка_символов

%T

Сгенерированная программа lex.yy.c осуществляет ввод-вывод символов посредством библиотечных функций lex с именами input, output, unput. Таким образом, lex помещает в yytext символы в представлении, используемом в этих библиотечных функциях. Для внутреннего использования символ представляется целым числом, значение которого образовано набором битов, представляющих символ в конкретной ЭВМ. Пользователю предоставляется возможность менять представление символов (целых констант) с помощью таблицы наборов символов. Если таблица символов присутствует в разделе определений, то любой символ, появляющийся либо во входном потоке, либо в правилах, должен быть определен в таблице символов. Символам нельзя назначать число 0 и число, большее числа, выделенного для внутреннего представления символов конкретной ЭВМ.

Пример:

%T

1 Aa

2 Bb

3 Cc

...

26 Zz

27

28 +

29 -

30 0

31 1

...

39 9

%T

В этом примере символы верхнего и нижнего регистров переводятся в числа 1-26, символ новой строки в 27, "+" и "-" переводятся в числа 28 и 29, а цифры - в числа 30-39.

ИЗМЕНЕНИЯ РАЗМЕРА ВНУТРЕННИХ МАССИВОВ задаются в форме:

%x число

число - новый размер массива;

x - одна из букв:

p

позиции

n

состояния

E

узлы дерева

A

упакованные переходы

K

упакованные классы символов

O

массив выходных элементов

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

Ниже перечислены размеры таблиц, которые устанавливаются по умолчанию:

p

позиций

1500

N

состояний

300

E

узлов

600

A

упакованных переходов

1500

K

упакованных классов символов

1000

O

выходных элементов

1500

Для того чтобы определить, каковы размеры таблиц и насколько они заняты, можно использовать флаг -v, например:

% lex -v source.l

33/600 узлов(%e)

97/1500 позиций(%p)

17/300 состояний(%n)

2551 переходов

18/1000 упакованных классов символов(%k)

41/1500 упакованных переходов(%a)

68/1500 выходных элементов(%o)

%

Здесь показано сообщение, которое выводит lex по флагу -v. Число перед символом "/" указывает сколько элементов массива занято, а число за символом "/" указывает установленный размер массива.

КОММЕНТАРИИ в разделе определений задаются в форме host-языка и должны начинаться не с первой колонки строки.

Детализация структуры Lex-программы

Раздел правил

Все, что указано после первой пары %% и до конца Lex-программы или до второй пары %%, если она указана, относится к разделу правил. Раздел правил может содержать правила и фрагменты программ. Правила имеют вид "регулярное выражение { действие }". Действие представляют собой последовательность операторов языка C, выполняемые при успешном распознавании регулярного выражения. Выражение записывается с начала строки. Фигурная скобка, начинающая действие, должна находится в той же строке, что и регулярное выражение, действие может продолжаться на нескольких строках.

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

%{

строки

фрагмента

программы

%}

Например:

%%

%{

#include file.h

%}

Здесь строка "#include file.h" станет строкой функции yylex().

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

Активное правило имеет вид:

ВЫРАЖЕНИЕ ДЕЙСТВИЕ

Неактивное правило имеет вид:

<МЕТКА>ВЫРАЖЕНИЕ ДЕЙСТВИЕ

или

<СПИСОК_МЕТОК>ВЫРАЖЕНИЕ ДЕЙСТВИЕ

где СПИСОК_МЕТОК имеет вид:

метка1,метка2,...

В качестве первого правила раздела правил может быть правило вида:

BEGIN МЕТКА;

В этом правиле отсутствует ВЫРАЖЕНИЕ, и первым действием в разделе правил будет активизация помеченных правил. Для возвращения автомата в исходное состояние можно использовать действие:

BEGIN 0;

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

Действия в правилах Lex-программы

Действие можно представлять либо как оператор lex, например, "BEGIN МЕТКА;", либо как оператор Си. Если имеется необходимость выполнить достаточно большой набор преобразований, то действие оформляют как блок Си-программы (он начинается открывающей фигурной скобкой и завершается закрывающей фигурной скобкой), содержащий необходимые фрагменты.

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

Область действия переменных, объявленных внутри блока, распространяется только на этот блок. Внешними переменными для всех действий будут являться только те переменные, которые объявлены в разделе определений Lex-программы.

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

[ \t\n] ;

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

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

" " |

\t |

\n ;

Результат будет тот же, что и в примере, указанном выше.

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

[A-Z]+ printf("%s",yytext);

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

[A-Z]+ ECHO;

Результат действия этого правила будет аналогичен результату предыдущего примера. В выходном файле lex.yy.c ECHO определено как макроподстановка:

#define ECHO fprintf(yyout, "%s",yytext);

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

[A-Z]+ printf("%c",yytext[yyleng-1]);

В этом примере будет выводится последний символ слова, соответствующего регулярному выражению [A-Z]+. Рассмотрим еще один пример:

[A-Z]+ {

число_слов ++;

число_букв += yyleng;}

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

Порядок действия активных правил

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

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

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

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

Рассмотрим пример:

...

[Мм][Аа][Йй] ECHO;

[А-Яа-я]+ ECHO;

...

Слово "Май" распознают оба правила, однако, выполнится первое из них, так как и первое, и второе правило распознали лексему одинакового размера (3 символа). Если во входном потоке будет, допустим, слово "майский", то первые 3 символа удовлетворяют первому правилу, а все 7 символов удовлетворяют второму правилу, следовательно, выполнится второе правило, так как ему удовлетворяет более длинная последовательность символов.

Условные правила

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

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

Переключение сканера из одного состояния в другое осуществляется оператором BEGIN имя_состояния; возвращение в исходное осуществляется оператором BEGIN 0.

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

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

%Start out_com in_com

%%

{ BEGIN out_com; }

"/*" { putchar( '\n' ); ECHO; BEGIN in_com;}

.|\n { }

"*/" { ECHO; BEGIN out_com; }

.|\n { ECHO; }

%%

int main() { yylex(); return 0; }

int yywrap() { putchar( '\n' ); return( 1 ); }

Правило ".|\n ;" используется для того, чтобы пропустить (не выводить) все цепочки символов, которые не соответствуют регулярному выражению.

Построенный сканер содержит два состояния: out_com – вне комментария и in_com - внутри комментария. В первом состоянии он игнорирует все символы, кроме начала комментария "/*". Встретив его, сканер переводит строчку, выводит начало комментария и переключается во второе состояние. Оно отличается от первого тем, что "остальные" символы копируются в стандартный вывод до тех пор пока не встретится конец комментария "*/".

Раздел программ пользователя

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

Комментарии Lex-программы

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

Примеры Lex-программ

%Start COMMENT

/* Программа записывает в стандартный файл

вывода комментарии Си-программы. Обратите

внимание на то, что здесь строки комментариев

указаны не с первой позиции строки! */

КОММ_НАЧАЛО "/*"

КОММ_КОНЕЦ "*/"

%%

{КОММ_НАЧАЛО} {

ECHO;

BEGIN COMMENT;}

[0* ;

<COMMENT>[^*]* ECHO;

<COMMENT>[^/] ECHO;

<COMMENT>{КОММ_КОНЕЦ} {

ECHO;

printf("0);

/* Здесь приведен пример использования

комментариев в разделе правил Lex-программы.

Обратите внимание на то, что комментарий указан

внутри блока, определяющего действие правила. */

BEGIN 0;}

%%

/* Здесь приведен пример комментариев

в разделе программ пользователя. */

lex построит лексический анализатор, который выделяет комментарии в Си-программе и записывает их в стандартный файл вывода. Программа начинается с ключевого слова START, которое указано после символа %. Ключевое слово START можно указать и так: Start, или S, или s . За ключевым словом START указана метка начального условия COMMENT.

Оператор "<COMMENT>x" означает - x, если анализатор находится в начальном условии COMMENT.

Oператор "BEGIN COMMENT;" переводит анализатор в начальное условие COMMENT (смотрите первое правило раздела правил этой Lex-программы). После этого анализатор уже находится в новом состоянии и теперь разбор входного потока символов будет осуществляется и теми правилами, которые начинаются оператором "<COMMENT>". Например, правило

<COMMENT>[^*]* ECHO;

выполняется только тогда, когда во входном потоке символов будет обнаружено начало комментариев ("/*"). В этом случае анализатор записывает в стандартный файл вывода любое число (в том числе и ноль) символов, отличных от символа "*". Оператор "BEGIN 0;" переводит анализатор в исходное состояние.

Рассмотрим пример с несколькими начальными условиями:

%START AA BB CC

БУКВА [A-ZА-Яa-zа-я_]

ЦИФРА [0-9]

ИДЕНТИФИКАТОР {БУКВА}({БУКВА}|{ЦИФРА})*

%%

^# BEGIN AA;

^[ \t]*main BEGIN BB;

^[ \t]*{ИДЕНТИФИКАТОР} BEGIN CC;

\t ;

\n BEGIN 0;

<AA>define {

printf("Определение.\n"); }

<AA>include

printf("Включение.\n"); }

<AA>ifdef {

printf("Условная компиляция.\n"); }

<BB>[^\,]*","[^\,]*")" {

printf("main с аргументамии.\n"); }

<BB>[^\,]*")" {

printf("main без аргументов.\n"); }

<CC>":"/[ \t] {

printf("Метка.\n"); }

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

В результате работы lex мы получим лексический анализатор, который будет распознавать в Си-программе строки препроцессора Cи-компилятора, выделять функцию main, распознавая, с аргументами она или без них, распознавать метки. Лексический анализатор не выводит ничего, кроме сообщений о выделенных лексемах.

Функции yyless и yymore

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

yymore()

В некоторых случаях возникает необходимость использовать не все символы распознанной последовательности в yytext, а только необходимое их число. Для этой цели используется функция yyless. Формат ее вызова:

yyless(n)

где n указывает, что в данный момент необходимы только n символов строки в yytext. Остальные найденные символы будут возвращены во входной поток.

Пример использования фунцкии yymore:

...

\"[^"]* {

if( yytext[yyleng - 1] == '\\') {

yymore();

}

else {

/* здесь должна быть часть программы,

обрабатывающая закрывающую кавычку. */

}

}

...

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

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

Пример использования фунции yyless:

...

=-[A-ZА-Яa-zа-я] {

printf("Oператор (=-) двусмысленный.\n");

yyless(yyleng - 2);

/* здесь необходимо указать действия для случая "=-" */

}

...

В этом примере разрешается двусмысленность выражения "=-буква" в языке Си. Это выражение можно рассматривать как "=- буква" (равносильно "-=") или "= -буква".

Предположим, что желательно эту ситуацию рассматривать как "= -буква" и выводить предупреждение. Указанное в примере правило распознает эту ситуацию и выводит предупреждение. Затем, в результате вызова "yyless(yyleng - 2);" два символа "-буква" будут возвращены во входной поток, а знак "=" останется в yytext для обработки, как в нормальной ситуации. Таким образом, при продолжении чтения входного потока уже будет обрабатываться цепочка "-буква", что и требовалось.

Флаги Lex

-t

Поместить результат в стандартный файл вывода, а не в файл lex.yy.c;

-v

Вывести размеры внутренних таблиц;

-f

Ускорить работу, не упаковывая таблицы (только для небольших программ);

-n

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

-d

Используется при отладке lex.

Имеется возможность собрать анализатор для диагностики. Для этого необходимо компиляцию файла lex.yy.c осуществлять с подключением разделов диагностики:

cc -d -DLEXDEBUG lex.yy.c

При работе полученного таким образом анализатора будет выводиться диагностика действий. Флаг -d, кроме того, позволяет проверить текст программы lex.yy.c с помощью текстового отладчика cdeb.

Принцип работы сканера

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

Для запуска автомата надо вызвать функцию yylex(), содержащей все действия, описанные в секции правил LEX-программы. Она, в свою очередь вызывает функцию yylook, реализующую собственно конечный автомат. В процессе работы автомат читает символ за символом из потока yyin, назначенного по умолчанию на стандартный ввод. После успешного распознавания одного из выражений происходит возврат в функцию yylex и выполнение соответствующего ему действия. При этом, переменная char yytext[YYLMAX] содержит терминированную нулем строку считанных символов, соответствующую данному регулярному выражению. Переменная int yyleng содержит длину этой строки. Действие может завершаться оператором return, который возвратит код лексемы, вызвавшей yylex функции (например, парсеру).

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

[\n.] { /* Ничего не делать */ }

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

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

7.1.2.Yacc

Yacc предназначен для построения парсера (синтаксического анализатора). Он воспринимает описание контекстно-свободной грамматики языка в виде, близком форме Бэкуса-Наура (НФБН) и строит восходящий LALR(1) распознаватель для данного языка.

Yacc как средство синтаксического анализа

yacc(1) предоставляет универсальные средства для структуризации исходных данных программ. Пользователь yacc'а готовит спецификацию, которая включает:

Множество правил, описывающих составные части исходных данных.

Действия, выполняемые при применении правила.

Определение или описание процедуры нижнего уровня, анализирующей исходные данные.

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

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

Ядром yacc-спецификации является набор грамматических правил. Каждое правило описывает синтаксическую конструкцию и дает ей имя. Грамматическое правило может быть, например, таким:

date : month_name day ',' year

;

где date, month_name и day представляют собой рассматриваемые конструкции; предполагается, что они детализируются в другом месте. В примере запятая заключена в одинарные кавычки. Это означает, что запятая должна встретиться во входном тексте. Двоеточие и точка с запятой служат в правиле всего лишь знаками препинания и при анализе текста не учитываются. При подходящих дополнительных определениях цепочка «July 4, 1776» может сопоставиться с данным правилом.

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

month_name : 'J' 'a' 'n'

;

month_name : 'F' 'e' 'b'

;

. . .

month_name : 'D' 'e' 'c'

;

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

Отдельные знаки, такие как запятая, также можно пропускать через лексический анализатор и считать лексемами.

Спецификации обладают очень большой гибкостью. Довольно легко добавить к нашему примеру правило:

date : month '/' day '/' year

;

допускающее использование во входном тексте конструкции «7/4/1776»

в качестве синонима ранее упоминавшейся цепочки «July 4, 1776»

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

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

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

Основные спецификации

Имена обозначают лексемы или нетерминальные символы. YACC требует, чтобы имена лексем были указаны явно. Хотя лексический анализатор можно включить в файл спецификаций, определение его в отдельном файле, вероятно, более соответствует принципам модульного проектирования. Подобно лексическому анализатору, в файл спецификаций могут быть также включены и другие подпрограммы. Таким образом, каждый файл спецификаций теоретически состоит из трех секций:

  • определений,

  • (грамматических) правил и

  • подпрограмм.

Секции отделяются двумя знаками процента %% (знак% используется в yacc-спецификациях как универсальный управляющий).

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

определения

%%

правила

%%

подпрограммы

Секции определений и подпрограмм являются необязательными. Минимальная допустимая yacc-спецификация - это

%

правила

Пробелы, табуляции и переводы строки, которые встречаются вне имен и зарезервированных слов, игнорируются. Комментарии могут быть везде, где допустимо имя. Они заключаются в "скобки" /*...*/, как в языке C.

Секция правил составляется из одного или большего числа грамматических правил. Грамматическое правило имеет вид:

нтс : тело ;

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

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

Двоеточие и точка с запятой - знаки препинания yacc'а.

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

Литерал - это знак, заключенный в одиночные кавычки, '. Как и в языке C, в качестве управляющего используется знак \, воспринимаются также все принятые в C управляющие последовательности. Например, yacc трактует перечисленные ниже последовательности следующим образом:

'\n' перевод строки

'\r' возврат каретки

'\'' одинарная кавычка '

'\\' обратная наклонная черта \

'\t' табуляция

'\b' забой

'\f' переход к новой странице

'\xxx' xxx в восьмеричной записи

По ряду технических причин символ NULL (\0 или 0) нельзя использовать в грамматических правилах.

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

A : B C D

;

A : E F

;

A : G

;

с использованием вертикальной черты могут быть заданы для yacc'а в виде

A | B C D

| E F

| G

;

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

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

epsilon :

;

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

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

%token имя1 имя2 ...

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

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

%start начальный_символ

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

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

Действия

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

Действие - это произвольный оператор на языке C. Следовательно, в действии можно выполнять операции ввода/вывода, вызывать подпрограммы, изменять значения массивов и переменных. Действие задается одним или несколькими операторами в фигурных скобках, { и }. Например, конструкции

A : '(' B ')'

{

hello (1, "abc");

}

;

и

XXX : YYY ZZZ

{

(void) printf ("сообщение\n");

flag = 25;

}

;

являются грамматическими правилами с действиями.

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

{$$ = 1;}

возвращает значение 1; в сущности, это все, что оно делает.

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

В случае правила

A : B C D

;

$2 принимает значение, возвращенное C, а $3 - значение, возвращенное D.

Пример с правилом

expr : '(' expr ')'

;

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

expr : '(' expr ')'

{

$$ = $2;

}

;

По умолчанию значение правила равно значению первого компонента в нем ($1). Поэтому грамматические правила вида

A : B

;

зачастую не требуют явного указания действия.

В предыдущих примерах действия всегда выполнялись в конце применения правила. Иногда же бывает желательно получить управление до того, как правило применено полностью. yacc позволяет писать действие в середине правила так же, как и в конце. Считается, что такое действие возвращает значение, доступное действиям справа от него посредством обычного механизма $. В свою очередь, оно имеет доступ к значениям, возвращаемым действиями слева от него. Таким образом, в правиле, указанном ниже, результатом действия является присваивание переменной x значения 1, а переменной y - значения, которое возвращает C.

A : B

{

$$ = 1;

}

C

{

x = $2;

y = $3;

}

;

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

$ACT : /* пусто */

{

$$ = 1;

}

;

A : B $ACT C

{

x = $2;

y = $3;

}

;

где $ACT - пустая цепочка.

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

node (L, n1, n2)

создает узел с меткой L и потомками n1 и n2 и возвращает указатель на него. Тогда дерево разбора можно построить при помощи действий вида:

expr : expr '+' expr

{

$$ = node ('+', $1, $3);

}

;

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

%{ int variable = 0; %}

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