Некоторые специфические свойства
Кроме сохранения распознанной цепочки в yytext[], lex автоматически подсчитывает число сопоставленных символов и сохраняет его в переменной yyleng. Можно использовать эту переменную, чтобы обратиться к любому отдельному символу, уже помещенному в массив yytext[]. Напомним, что в языке C элементы массива нумеруются, начиная с 0, поэтому, чтобы распечатать третью цифру в распознанном целом числе (если таковая имеется), следует написать:
[0-9]+ { if (yyleng > 2) printf("%c", yytext[2]); }
Чтобы разрешить неоднозначности, к которым может приводить совокупность заданных пользователем спецификаций, lex следует нескольким соглашениям. Подобные неоднозначности возникают, например, в ситуациях, когда цепочка символов (скажем, зарезервированное слово), может сопоставляться с шаблонами из двух правил. В примере лексического анализатора, описанном ниже в разделе Совместное использование lex и yacc, зарезервированное слово end может сопоставляться как с шаблоном из второго правила, так и с шаблоном из правила (седьмого) для идентификаторов.
Примечание
Если имеет место сопоставление с двумя или более специ- фикациями, выполняется действие, относящееся к первой из них.
Поместив правило для end и других зарезервированных слов перед правилом для идентификаторов, мы гарантируем, что зарезервированные слова будут распознаваться должным образом.
Другая потенциальная проблема может возникнуть в случаях, когда один из распознаваемых шаблонов является началом другого. Например, два последних правила в упомянутом лексическом анализаторе служат для распознавания операций > и >=. Может возникнуть предположение, что в случае, когда текст содержит цепочку >=, лексический анализатор, обнаружив символ >, выполнит действие для шаблона >, но не станет читать следующий символ и не выполнит действие для >=.
Примечание
Сопоставляется самая длинная из подходящих цепочек символов и выполняется соответствующее действие.
В данном случае распознается операция >= и выполняется ассоциированное с ней действие. Другой пример: данное правило позволяет различить в C-программах операции + и ++.
Наконец, еще одна трудность возникает в случае, когда анализатор должен прочитать символы вне искомой цепочки, поскольку не может быть уверенным в том, что в самом деле обнаружил ее, не прочитав дополнительные символы. Такие случаи доказывают важность завершающего контекста. Классический пример - оператор DO
в Фортране. При анализе оператора
DO 50 k = 1, 20, 1
нельзя быть уверенным в том, что первая единица - это начальное значение управляющей переменной k, пока не прочитана первая запятая, поскольку оператор
DO50k = 1
есть просто оператор присваивания. (Напомним, что в Фортране пробелы игнорируются.) Для обработки ситуаций, требующих опере- жающего просмотра, используется символ / (не путать с символом \), который обозначает, что следующая за ним последовательность является завершающим контекстом и ее не следует сохранять в yytext[], поскольку она не входит в саму лексему. Правило для распознавания DO в Фортране можно записать так:
DO/[0-9 ]*[a-zA-Z0-9 ]+=[a-zA-Z0-9 ]+, printf("нашли DO");
Различные версии Фортрана налагают различные ограничения на длину идентификаторов, в том числе имен управляющих переменных цикла. Чтобы упростить пример, в правиле описывается имя произвольной длины. (На самом деле сделано еще несколько упрощающих предположений.)
Для обозначения специального завершающего контекста - конца строки - lex использует знак операции $. (Это эквивалентно \n). Ниже приведено правило, которое игнорирует все пробелы и табуляции в конце строки:
[ \t]+$ ;
С другой стороны, если нужно сопоставить шаблон непременно с начала строки, lex предлагает операцию ^ (это пример начального контекста). Например, в утилите форматирования nroff требуется, чтобы строка никогда не начиналась с пробела, поэтому для проверки входных данных nroff можно использовать такое правило:
^[ ] printf("ошибка: удалите начальные пробелы");
Наконец, для выполнения некоторых действий может потребоваться прочитать еще один символ, возвратить символ обратно для последующего повторного прочтения или вывести символ на устройство вывода. lex предоставляет для этих целей три функции - input(), unput(c) и output(c) соответственно. Один из способов пропустить все символы между двумя специальными символами, скажем, между парой двойных кавычек, заключается в использовании input():
\" while (input() != '"');
После того, как найдена первая двойная кавычка, сгенерированная программа a.out будет продолжать чтение всех последующих символов без попыток сопоставления с шаблонами, пока не найдет вторую двойную кавычку.
Для выполнения специальных операций ввода/вывода можно переписать функции input(), unput(), и output(), используя стандартные для языка C средства вода/вывода. Эти и другие определенные программистом функции следует поместить в секцию подпрограмм. В таком случае новые подпрограммы заменят стандартные. Стандартная функция input(), в действительности, эквивалентна getchar(), а стандартная функция output() - putchar().
В lex'е имеется ряд подпрограмм, которые предоставляют возможность многократной обработки цепочек символов. Это функции yymore() и yyless(n) и действие REJECT. Повторим, что текст, сопоставленный с некоторой спецификацией, помещается в массив yytext[]. В общем случае, после того как выполнено действие для данной спецификации, символы в yytext[] замещаются последующими символами входного потока, образующими следующую сопоставляемую цепочку. Функция yymore(), напротив, гарантирует, что последующие распознаваемые символы будут добавляться после тех, которые уже содержатся в yytext[]. Тем самым в случае, когда значимыми оказываются как одна цепочка символов, так и другая, более длинная, включающая первую, появляется возможность выполнить сначала одно действие, а потом другое, ассоциированное с длинной цепочкой.
В качестве примера рассмотрим словарь, в котором чередуются английские слова и их русские эквиваленты. Слова (как английские, так и русские) разделяются запятыми; запятые стоят также в начале и конце словаря. Требуется распечатать содержимое словаря в виде пар (английское слово, русский эквивалент). К цели ведет следующее правило:
,[^,]* { if (flag == 0) { flag = 1; yymore(); } else { flag = 0; yytext [yyleng] = (char) 0; printf ("%s\n", &yytext[1]); } }
Предполагается, что переменная flag описана (и снабжена нулевым начальным значением) в секции определений; она нужна, чтобы отличить цепочку символов, завершающуюся перед второй запятой (английское слово), от цепочки, завершающейся перед третьей запятой (русский эквивалент).
Функция yyless(n) позволяет переустановить указатель конца обработанных символов на символ, оказавшийся n-м в массиве yytext[]. Предположим, что мы занимаемся расшифровкой кода и уловка заключается в том, чтобы обрабатывать только половину символов в последовательности, заканчивающейся определенным символом, например, прописной или строчной буквой z. Достаточно воспользоваться правилом
[a-yA-Y]+[Zz] { yyless(yyleng/2); ...обработка первой половины цепочки...}
Наконец, действие REJECT упрощает обработку цепочек символов в случаях, когда они перекрываются или одна является частью другой. Выполнение REJECT заключается в немедленном переходе к следующему правилу без изменения содержимого yytext[]. Например, для подсчета числа вхождений в тексте как цепочки символов snapdragon, так и ее подцепочки dragon, можно использовать правило
snapdragon { countflowers++; REJECT; } dragon countmonsters++;
В следующем примере, иллюстрирующем частичное перекрытие двух шаблонов, подсчитывается число вхождений цепочек comedian и diana, причем правильно обрабатываются даже такие входные цепочки, как comediana...:
comedian { comiccount++; REJECT; } diana princesscount++;
Отметим, что действия здесь могут быть существенно сложнее, чем просто увеличение счетчика. Как всегда, счетчики и другие необходимые переменные описываются в секции определений, с которой начинается lex-спецификация.