%{
int nchar, nword, nline;
%}
%%
\n {nline++; nchar++;}
[^ \t\n]+ {nword++, nchar += yyleng;}
{nchar++;}
%%
int main(void) {
yylex();
printf («%d\t % d\t % d\n», nchar, nword, nline);
return 0;
}
В целом подсистема LEX для систем UNIX включает следующие файлы:
/usr/ccs/bin/lex;lex.yy.c;/usr/ccs/lib/lex/ncform;/usr/lib/libl.a;/usr/lib/libl.so.В каталоге /usr/ccs/lib/lex имеется файл-заготовка ncform, который LEX используется для построения ЛА. Этот файл является уже готовой программой лексического анализа. Но в нем не определены действия, которые необходимо выполнять при распознавании лексем, отсутствуют и сами лексемы, не сформированы рабочие массивы и т.д. С помощью команды lex файл ncform достраивается. В результате мы получаем файл со стандартным именем lex.yy.c. Если LEX-программа размещена в файле program.l, то для получения ЛА с именем program необходимо выполнить следующий набор команд:
lex program.lcc lex.yy.c – ll – o programЕсли имя входного файла для команды lex не указано, то будет использоваться файл стандартного ввода. Флаг – ll требуется для подключения /usr/ccs/lib/libl.a – библиотеки LEX. Если необходимо получить самостоятельную программу, как в данном случае, подключение библиотеки обязательно, поскольку из нее подключается главная функция main. В противном случае, если имеется необходимость включить ЛА в качестве функции в другую программу (например, в программу синтаксического анализа), эту библиотеку необходимо вызвать уже при сборке. Тогда, если main определен в вызывающей ЛА программе, редактор связей не будет подключать раздел main из библиотеки LEX.
Общий формат вызова команды lex:
lex [-ctvn – V – Q [y|n]] [file]Флаги:
– c – включает фазу генерации Си-файла (устанавливается по умолчанию);
– t – поместить результат в стандартный файл вывода, а не в файл lex.yy.c;
– v – вывести размеры внутренних таблиц;
– n – не выводить размеры таблиц (устанавливается по умолчанию);
– V – вывести информацию о версии LEX в стандартный файл ошибок;
– Q – вывести (Qy) либо не выводить (Qn, устанавливается по умолчанию) информацию о версии в файл lex.yy.c.
Грамматики для yacc описываются с помощью Формы Бэкуса-Наура (Backus Naur Form, BNF)
Эту технику была ввели John Backus и Peter Naur и использовали ее, чтобы описать ALGOL60. Грамматика BNF может быть использована для описания контекстно-свободных языков. Большинство конструкций современного программирования могут быть представлены в BNF.
Например, грамматика для выражения, которое умножает и складывает числа может быть представлена следующим образом:
E -> E + E
E -> E * E
E -> id
Были специфицированы 3 правила продукции. Термы, которые могут быть с левой стороны правил продукции(lhs) продукции, такие как E (expression), являются нетерминальными символами. Термы, такие как id (identifier) являются терминальными символами (токенами, возвращаемыми lex) и могут быть только с правой стороны правил продукции(rhs).
Эта грамматика определяет, что выражение может быть суммой двух выражений, произведением двух выражений или идентификатором. Можно использовать эту грамматику, чтобы сгенерировать выражения:
E -> E * E (r2)
-> E * z (r3)
-> E + E * z (r1)
-> E + y * z (r3)
-> x + y * z (r3)
На каждом шаге мы расширяем терм, заменяем левую часть продукции соответствующей правой частью. Числа справа показывают какое правило применяется. Чтобы распознать выражение, необходима обратная операция. Кроме того, что необходимо начинать с единичного нетерминального (начального символа), и генерировать выражение из грамматики. Это известно под термином восходящий (bottom-up) анализ и использование стека для хранения термов. Вот некоторый пример в обратном порядке:
1. x + y * zshift
2 x. + y * zreduce(r3)
3 E. + y * z shift
4 E +. y * z shift
5 E + y. * z reduce(r3)
6 E + E. * z shift
7 E + E *. z shift
8 E + E * z. reduce(r3)
9 E + E * E. reduce(r2) emit multiply
10 E + E. reduce(r1) emit add
11 E. accept
Каждый файл спецификаций состоит из трех секций: объявления, (грамматические) правила, и программы. Секции разделяются символами двойного процента «%%». Другими словами, полный файл спецификации выглядит как:
описания
%%
правила
%%
программы
Секция объявлений может быть пуста. Более того, если секция программ опущена, то вторая метка «%%» также может быть опущена; таким образом, минимальная разрешенная спецификация Yacc есть:
%%
правила
Пример простейшей программы на YACC'e:
%token name
%start e
%%
e: e '+' m | e '-' m | m;
m: m '*' t | m '/' t | t;
t: name | ' (' e ')';
%%
Секция правил содержит информацию о том, что символ name является лексемой (терминалом) грамматики, а символ e – ее начальным нетерминалом.
Грамматика записана обычным образом – идентификаторы обозначают терминалы и нетерминалы, символьные константы типа '+' '-' – терминалы. Символы: |; – принадлежат метаязыку и читаются «есть по определению», «или» и «конец правила» соответственно.
Каждому правилу можно поставить в соответствие некое действие, которое будет выполняться всякий раз, как это правило будет распознано. Действия могут возвращать значения и могут пользоваться значениями, возвращенными предыдущими действиями. Более того, лексический анализатор может возвращать значения для токенов (дополнительно), если хочется. Действие – это обычный оператор языка Си, который может выполнять ввод, вывод, вызывать подпрограммы и изменять глобальные переменные.
Действия, состоящие из нескольких операторов, необходимо заключать в фигурные скобки. Например:
A: ' (' B ')'{hello (1, «abc»);}и
XXX: YYY ZZZ{printf («amessage\n»); flag = 25;}являются грамматическими правилами с действиями.
Чтобы обеспечить связь действий с анализатором, используется спецсимвол «доллар» ($). Чтобы вернуть значение, действие обычно присваивает его псевдопеременной $$. Например, действие, которое не делает ничего, но возвращает единицу:
{$$ = 1;}Чтобы получить значения, возвращенные предыдущими действиями и лексическим анализатором, действие может использовать псевдопеременные $1, $2 и т.д., которые соответствуют значениям, возвращенным компонентами правой части правила, считая слева направо. Таким образом, если правило имеет вид:
A: B C D;то $2 соответствует значению, возвращенному нетерминалом C, a $3 – нетерминалом D. Более конкретный пример:
expr: ' (' expr ')';Значением, возвращаемым этим правилом, обычно является значение выражения в скобках, что может быть записано так:
expr: ' (' expr ')'{$$ = $2;}По умолчанию, значением правила считается значение, возвращенное первым элементом ($1). Таким образом, если правило не имеет действия, Yacc автоматически добаляет его в виде $$=$1;, благодаря чему для правила вида
A: B;обычно не требуется самому писать действие.
В вышеприведенных примерах все действия стояли в конце правил, но иногда желательно выполнить что-либо до того, как правило будет полностью разобрано. Для этого Yacc позволяет записывать действия не только в конце правила, но и в его середине. Значение такого действия доступно действиям, стоящим правее, через обычный механизм:
A: B{$$ = 1;}C{x = $2; y = $3;};В результате разбора иксу (x) присвоится значение 1, а игреку (y) – значение, возвращенное нетерминалом C. Действие, стоящее в середине правила, считается за его компоненту, поэтому x=$2 присваивает X-у значение, возвращенное действием $$=1;
Для действий, находящихся в середине правил, Yacc создает новый нетерминал и новое правило с пустой правой частью и действие выполняется после разбора этого нового правила. На самом деле Yacc трактует последний пример как
NEW_ACT: /* empty */ /* НОВОЕ ПРАВИЛО */{$$ = 1;};A: B NEW_ACT C{x = $2; y = $3;};В большинстве приложений действия не выполняют ввода / вывода, а конструируют и обрабатывают в памяти структуры данных, например дерево разбора. Такие действия проще всего выполнять вызывая подпрограммы для создания и модификации структур данных. Предположим, что существует функция node, написанная так, что вызов node (L, n1, n2) создает вершину с меткой L, ветвями n1 и n2 и возвращает индекс свежесозданной вершины. Тогда дерево разбора может строиться так:
expr: expr '+' expr{$$ = node ('+', $1, $3);}Программист может также определить собственные переменные, доступные действиям. Их объявление и определение может быть сделано в секции объявлений, заключенное в символы%{и%}. Такие объявления имеют глобальную область видимости, благодаря чему доступны как действиям, так и лексическому анализатору. Например:
%{int variable = 0;%}Такие строчки, помещенные в раздел объявлений, объявляют переменную variable типа int и делают ее доступной для всех действий. Все имена внутренних переменных Yacca начинаются c двух букв y, поэтому не следует давать своим переменным имена типа yymy.
Программист, использующий Yacc должен написать сам (или создать при помощи программы типа Lex) внешний лексический анализатор, который будет читать символы из входного потока (какого – это внутреннее дело лексического анализатора), обнаруживать терминальные символы (токены) и передавать их синтаксическому анализатору, построенному Yaccом (возможно вместе с неким значением) для дальнейшего разбора. Лексический анализатор должен быть оформлен как функция с именем yylex, возвращающая значение типа int, которое представляет собой тип (номер) обнаруженного во входном потоке токена. Если есть желание вернуть еще некое значение (например номер в группе), оно может быть присвоено глобальной, внешней (по отношению к yylex) переменной по имени yylval.
Лексический анализатор и Yacc должны использовать одинаковые номера токенов, чтобы понимать друг друга. Эти номера обычно выбираются Yaccом, но могут выбираться и человеком. В любом случае механизм define языка Си позволяет лексическому анализатору возвращать эти значения в символическом виде. Предположем, что токен по имени DIGIT был определен в секции объявлений спецификации Yaccа, тогда соответствующий текст лексического анализатора может выглядеть так: