FLEX(1)
ИМЯ
flex,lex - ЛЕКСИЧЕСКИЙ АНАЛИЗАТОР
СИНТАКСИС
flex [-bcdfhilnpstvwBFILTV78+ -C[aefFmr] -Pprefix -Sskele-
ton] [filename ...]
Введение
flex(1) - это генератор программ, предназначенный для лексической обработки символьных входных данных. Он принимает проблемно-ориентированную спецификацию на высоком уровне и формирует
программу на языке СИ, которая работает с регулярными выражениями. Регулярные выражения определяются пользователем в ключевой
спецификации, выдаваемой программе lex. Система lex распознает
эти выражения и разделяет входные данные на блоки в соответствии
с ними. На границах между блоками исполняются фрагменты программ, разработанные пользователем. исходный файл lex объединяет
регулярные выражения и фрагменты программ. По мере того, как выражения появляются на входе программы, составленной lex, соответствующий фрагмент подается на выполнение.
Пользователь вводит добавочные фрагменты программ, необходимые для комплектования его задания, включая коды, написанные
другими генераторами. Программа, распознающая выражения, формируется из фрагментов программ пользователя на языке СИ. lex -
это не полный язык, а только генератор, представляющий новую
возможность, которая дополняет возможности языка СИ.
lex превращает выражения и команды пользователя (называемые
в этой главе источником) в программу на языке СИ, имеющую название yylex. Программа yylex распознает выражения во входном потоке (называемые в этой главе входными) и осуществляет специальные
операции для каждого выражения, как только оно встретится.
Рассмотрим программу, удаляющую из стандартного файла ввода
все пробелы и знаки табуляции в конце строк. Для этого требуются
только следующие строки:
%%
[\t]+$;
Программа содержит разграничитель %%, выделяющий начало
фрагмента и одно правило. Это правило содержит регулярное выражение, которое выбирает один или более экземпляров пробелов или
знаков табуляции (для наглядности пишется \t, в соответствии с
правилами языка СИ) до конца строки. Скобки указывают на знаковый класс знака табуляции или пробела, + обозначает один или
несколько предыдущих пунктов, и знак $ указывает на конец строки. Никаких действий не определяется, поэтому программа, созданная lex, будет просто игнорировать эти символы. Все остальное
будет копироваться. Чтобы преобразовать любую цепочку пробелов и
знаков табуляции в единственный пробел, добавьте еще одно правило:
%%
[\t]+$;
[\t]+ printf("");
Программа, созданная по этому источнику, проверяет сразу
два правила: контролирует окончание строки пробелов и знаков табуляции и проверяет, имеется ли знак новой строки, затем выполняет действие, предписанное правилом.
Регулярные выражения lex
Регулярное выражение определяет набор строк, которые должны
быть сопоставлены. Оно содержит текстовые символы (соответствующие символам в сравниваемых строках) и знаки операторов символы
(указывающие на повторение, удаление и другие возможности). Буквы алфавита и цифры всегда являются текстовыми символами. Таким образом, регулярное выражение:
integer
подбирает литерную цепочку integer, где бы она ни появилась, а
выражение
a57D
ищет литерную цепочку a57D. Существуют следующие знаки операторов:
" \ [] ^ - ? . * + I () $ / {} % <>
Если какой-либо из этих знаков используются дословно, то
это необходимо отмечать косой чертой (\). Группа знаков выделяется кавычками ("). Оператор кавычек указывает на то, что все,
содержащееся между двумя выделяющими кавычками, воспринимается
как символы текста. Таким образом
xyz"++"
подбирает символьную цепочку xyz++, где бы они ни встретилась.
Выделяемую часть символьной цепочки можно заключать в кавычки.
Безобидно, но излишне выделять обычную текстовую литеру. Выражение
"xyz++"
эквивалентно предыдущему. Таким образом, заключая в кавычки каждый не алфавитно-цифровой символ, используемый как текстовая литера, вы избавляетесь от необходимости помнить список текущих
знаков операторов.
Знак оператора также может быть преобразован в текстовый
символ, если перед ним поставить обратную косую черту (\):
xyz\+\+
Это менее читабельный эквивалент рассмотренного выше выражения. Механизм заключения в кавычки может быть использован также для того, чтобы вставить в выражение пробел. Обычно пробелы и
знаки табуляции заканчивают правило. Любой пробел, не заключенный в скобки, должен браться в кавычки. СИ распознает несколько
обычных знаков с обратной косой чертой (\):
\n знак новой строки
\t знак табуляции
\b возврат на один знак назад с его удалением
\\ обратная косая черта
Поскольку знаки новой строки в выражениях запрещены, надо
использовать \n. Не требуется избегать использования знаков табуляции и возврата. Заметим, что любой символ всегда является
текстовым. Исключение из этого составляют знаки табуляции, пробелы, знаки новой строки и знаки операторов, рассмотренные выше.
Вызов lex
Компилирование исходной программы lex осуществляется в две
стадии. Сначала исходная программа преобразуется в модуль на
универсальном языке программирования. Затем программа (обычно
с использованием библиотечных подпрограмм lex) транслируется и
загружается. Созданная программа находится в файле с именем lex.
yy.c. Библиотека ввода/вывода определяется в теминах стандартной
библиотеки СИ. Доступ к библиотеке осуществляется с помощью флага загрузки -ll. Вот пример характерного набора команд:
lex source
cc lex.yy.c -ll
Полученная программа помещается в файл a.out для последующего использования. Как применять lex вместе с yacc, описано в
разделе "lex и yacc" этой главы и в главе 6 "YACC: компилятор
компиляторов". Хотя подпрограммы ввода-вывода lex по умолчанию
используют стандартную библиотеку СИ, производимые lex программы
этого не делают. Если даны собственные варианты input(),
output(), unput(), библиотека может не использоваться.
Описание классов символов
Классы символов задаются с помощью квадратных скобок [ и ].
Конструкция
[abc]
выбирает один из символов a, b или c. Внутри квадратных скобок
большинство значений операторов игнорируется. Только три знака
рассматриваются как специальные: обратная косая черта (\), тире
(-) и символ (^). Символ тире задает перечисления. Например:
[a-z0-9<>_]
задает класс символов, содержащие все строчные буквы, цифры, угловые скобки и знак подчеркивания. Перечисления могут задаваться
в любом порядке.
Использование тире между парами символов, не являющихся одновременно заглавныим буквами, сточными буквами или числами, делает их зависимыми от реализации и вызывает предупреждающее сообщение. Если желательно включить тире в класс символов, то оно должно быть первым или последним. Таким образом:
[-+0-9]
подбирает все цифры и знаки плюс и минус. Знак оператора (^)
должен находиться в первой позиции после левой скобки. Он указывает на то, что результирующая строка будетдополнительной к набору символов компьютера. Таким образом:
[^abc]
подбирает все символы, кроме a,b,c, включая все специальные и
управляющие знаки, а
[^a-zA-Z]
это любой символ, не являющийся буквой. Косая черта (\) обеспечивает механизм отмены специального значения внутри класса символов, заключенного в скобки. Таким образом, символы могут быть введены литерально, если перед ними ставить косую черту.
Описание произвольного символа
Точка (.) определяет класс всех символов, кроме символа новой строки. Возможен переход в восьмеричную систему, но это исключает мобильность программы. Например:
[\40-\176]
подбираетет все печатаеные символы в коде ASCII, с восьмеричного
40 (пробел) до восьмеричного 176 (тильда).
Описание необязательных выражений
Оператор вопроса (?) указывает на необязательный элемент
выражения. Таким образом
ab?c
подбирает ac или abc. Заметим, что значение оператора знака вопроса в данном случае отличается от его значения в командном процессоре.
Описание повторяющихся выражениий
Повторение классов обозначаются операторами звездочки (*) и
плюса (+). Например:
a*
подбирает любую цепочку последовательных символов a, включая
пустую, в то время как a+ подбирает один и более экземпляров a.
Например:
[a-z]+
подбирает все цепочки строчных букв, и
[A-Za-z][A-Za-z0-9]*
подбирает все алфавитно-цифровые строки с начальной буквой. Заметим, что это типичное выражение для распознавания идентификаторов в языках программирования.
Описание чередования и группировки
Оператор вертикальной черты (|) определяет чередование.
Например:
(ab|cd)
подбирает ab или cd. Заметим, что круглые скобки используются
для группировки, хотя они не являются необходимыми на внешнем
уровне. Например:
ab|cd
эквивалентно предыдущему примеру. Круглые скобки используются
для более сложных выражений типа:
(ab|cd+)?(ef)*
которое подбирает такие литерные цепочки, как abefef, efefef,
cdef и cddd, но не abc, abcd или abcdef.
Описание зависимости от контекста
lex распознает только малую часть окружающего контекста.
Для этого используются два простейших оператора: знак (^) и знак
доллара ($). Если (^) - первый знак выражения, то выражение сопоставляется только в начале строки (после символа новой строки
или в начале входного потока). Это не может противоречить другому значению символа (^), дополнению класса символов, так как дополнение применяется только внутри скобок. Если самым последним
символом является зкак доллара, то выражение сопоставляется
только в конце строки (если за ним сразу же следует знак новой
строки). Этот оператор является частным случаем оператора косой
черты (/), который указывает на последующий контекст. Выражение
ab/cd
подбирает строку ab, если за ней идет cd. Таким образом:
ab$
это то же самое, что
ab/\n
Левый контекст управляется из lex путем описания стартовых
условий, которые объясняются в разделе "Описание зависимости от левого контекста". Если правило должно выполняться только
в случае нахождения автоматического интерпретатора lex в стартовой позиции x, то оно должно быть заключено в угловые скобки:
<x>
Если мы считаем что стартовое условие ONE находится в начале строки, то оператор (^) будет эквивалентен
<ONE>
Стартовые условия будут детально описаны в этой главе.
Описание повторения выражений
Фигурные скобки ({ }) описывают или повторения (если они
содержат число), или определение расширения (если оно содержит
имя). Например:
{digit}
ищет встроенную строку символов с именем digit и вставляет ее в
данное место выражения.
Описание определений
Определения задаются в первой части ввода lex, перед правилами.
a{1,5}
наоборот, ищет от 1 до 5 экземпляров символа a.
Наконец, начальный знак процента (%) является специальным,
поскольку он разграничивает сегменты источника lex.
Описание действий
Когда выражение сопоставляется с образцом текста на вводе,
lex выполняет соответствующие действия. Этот раздел описывает
некоторые особенности lex, которые помогают при описании действий. Заметим, что существует действие по умолчанию, которое копирует входные данные на выход. Это выполняется для всех символов, которые в противном случае не сопоставляются. Таким образом, пользователь lex, который желает произвести полный ввод без
какого-либо вывода, должен составить правила, подбирающие все
данные. Если lex используется совместно с yacc, то это считается
нормальной ситуацией. Вы можете рассматривать эти действия, как
заменяющие копирование входных данных в выходные. Таким образом,
правило простого копирования может быть опущено.
Самое простое, что можно сделать, это проигнорировать ввод.
Описание в качестве действия пустого оператора языка СИ приводит
именно к этому результату. Часто используется правило
[\t\n];
которое вызывает игнорирование трех символов (пробел, знак табуляции, знак новой строки).
Чтобы не описывать действие, можно использовать символ повтора |, который указывает, что действие для данного правила задает и действие для следующего. Предыдущий пример можно было бы
записать следующим образом:
" " |
"\t" |
"\n" ;
Это приведет к тому же результату, только представлено в
другом виде. Кавычки вокруг \n и \t не обязательны.
Для задания более сложных действий часто нужно знать текущий текст, сопоставляемый выражениям типа
[a-z]+
lex хранит этот текст во внешнем символьном массиве с именем yytext. Так, чтобы напечатать найденное имя, используется правило типа
[a-z]+printf("%s",yytext);
которое печатает символьную строку, хранящуюся в yytext. Функция
языка СИ printf(2) позволяет печатать формальные параметры и
данные. В этом случае форматом является print string, где знак
процента (%) указывает на преобразование данных, s обозначает
тип символьной строки. Данными являются символы из yytext. Это
переводит подобранную строку в выходные данные. Это действие
настолько заурядно, что может быть написано через ECHO. Например:
[a-z]+ ECHO;
аналогично предыдущему примеру. Так как действие по умолчанию
печатает только найденные символы, то может возникнуть вопрос:
зачем нужно правило, описывающее только действие по умолчанию?
Такие правила нужны, чтобы избежать сопоставления каких-то других, нежелательных правил. Например, если есть правило, которое
сопоставляет read, то оно будет сопоставлять экземпляры read,
заключенные в bread или readjust. Чтобы избежать этого, требуется правило
[a-z]+
Этот вопрос будет подробнее разобран далее.
Иногда бывает удобным знать конец найденных данных, поэтому
lex подсчитывает число сопоставленных символов в переменной
yyleng. Чтобы подсчитать и число слов и число символов, следует
написать:
[a-zA-Z]+ {words++;chars+=yyleng;}
Будет произведен подсчет символов в словах, а результат будет передан переменной chars. Обращение к последнему символу сопоставляемой строки может произведено следующим образом:
yytext[yyleng-1]
Иногда lex может решить, что правило не обнаружило соответствующий набор символов. Существуют две программы, помогающие
в этой ситуации. Во-первых, может быть вызвана подпрограмма
yymore(), предписывающая присоединить следующее вводимое выражение к концу текущего ввода. Обычно следующая вводимая символьная
строка затирает текущий элемент в yytext. Во-вторых, может быть
вызвана подпрограмма yyless(n), определяющая, что не все символы, подбираемые текущим выражением, требуются прямо сейчас. Ар
гумент n указывает число символов, содержащихся в yytext. После
дующие символы, которые уже были сопоставлены, возвращаются
обратно на ввод. Это обеспечивает способ просмотра данных, ана
логичный задаваемому оператором (/), но в другой форме.
Например, рассмотрим язык, который определяет группу симво
лов, заключенных в кавычки, как символьную строку. Если в эту
строку надо включить знак кавычек, то ему должна предшествовать
обратная косая черта (\). Регулярные выражения, используемые для
такого сопоставления, довольно сложны, поэтому предпочтительнее
ввести:
\"[^"]* {
if(yytext[yyleng-1]==`\\`)
yymore();
else
...normal user processing
}
При этом, если встретится
"abc\"def"
то будут сопоставляться первые пять символов
"abc\
Затем, вызов yymore() перенесет следующую часть символьной
строки
"def
в конец. Заметим, что последний знак кавычки, завершающий строку, будет представлен в коде, соответствующем обычной обработке.
Функция yyless() в различных обстоятельствах может быть использована для повторной обработки текста. Рассмотрим проблему
определения неоднозначного выражения =-a в старом варианте СИ.
Допустим, что это требуется представить как =- a для последующей
печати. Можно применить следующее правило:
=-[a-zA-Z] {
printf("Operator(=-)ambiguous\n");
yyless(yyleng-1);
...action for=-...
}
Будет печататься сообщение, после каждого оператора буква
будет возвращаться во входной поток, а оператор будет трактоваться как =-.
Наоборот, может потребоваться трактовать это как = -a; чтобы добиться этого, уберите знак минус и букву для ввода. Интерпретация будет выполнена следующими строками:
=-[a-zA-Z {
printf("Operator(=-)ambiguous\n");
yyless(yyleng-2);
...action for=...
}
Заметим, что выражения в этих случаях могут быть записаны
как
=-/[A-Za-z]
в первом случае, и
=/-[A-Za-z]
во втором. В правилах, действия которых описаны выше, не требуется возврата. Для раскрытия неопределенности нет необходимости
в распознавании всего идентификатора. Тем не менее, в случае =-3
лучше использовать правило
=-/[^\t\n]
В дополнение к этим программам, lex также разрешает доступ
своим собственным подпрограммам ввода/вывода:
- input(), которая возвращает следующий вводимый символ;
- output(с), которая выводит символ c;
- unput(с), которая помещает символ c обратно во входной
поток, чтобы затем считать его с помощью input().
По умолчанию эти подпрограммы рассматриваются как макроопределения, но пользователь может заменить их собственными версиями. Эти подпрограммы устанавливают взаимосвязь между внешними
файлами и внутренними символами, поэтому они все должны сохраняться или видоизменяться согласованно. Они могут быть переопределены, чтобы вызвать перемещение выходных или входных данных в
другие области, включая другие программы или оперативную память.
Но используемые наборы символов во всех подпрограммах должны
быть согласованы. Нулевое значение, возвращаемое input(), должно
означать конец файла. Связь между unput() и input() должна сохраняться, иначе просмотр вперед не будет осуществляться. lex
вообще не будет выполнять просмотр вперед, если ему это не предписано. Но каждое провило, содержащее косую черту (/) или оканчивающееся одним из следующих символов подразумевает просмотр:
+ * ? $
Просмотр вперед также необходим для сопоставления выражения, которое является префиксом другого выражения. Ниже рассматривается набор символов, часто используемых lex. Стандартная библиотека lex содержит 100 резервных копий символов.
Еще одна подпрограмма библиотеки lex, которую иногда желательно переопределить, - это yywrap(), вызываемая каждый раз,
когда lex достигает конца файла. Если yywrap() обращает 1, lex
продолжает обычную работу до конца ввода. Иногда, тем не менее,
удобно принять дополнительные входные данные из нового источника. В этом случае пользователь должен вызвать подпрограмму
yywrap(), которая принимает новые входные данные и возвращает 0.
Это заставит lex продолжить обработку. По умолчанию yywrap()
всегда возвращает 1.
Эта подпрограмма также удобна для печати в конце программы
таблиц, кратких пояснений и т.д. Заметим, что нельзя написать
обычное правило которое распознает конец файла. Единственный
сделать это - использовать yywrap(). В действительности, если не
установлена собственная версия input(), то файлом, содержащим
пустые указатели, нельзя управлять, поскольку значение 0, возвращаемое input(), воспринимается как конец файла.
Обработка неоднозначностей источника
lex может обрабатывать неоднозначные спецификации. Если в
текущем вводе сопоставляется более одного выражения, lex поступает следующим образом:
- предпочитается самое длинное сопоставление;
- предпочитается первое из правил, сопоставляющих одинаковое число символов.
Предположим, например, что даны слаедующие правила:
integer keyword action...;
[a-z]+ identifier action...;
Если на входе intergers, это воспринимается как идентификатор, потому что
[a-z]+
сопоставляет восемь символов, в то время, как
integer
сопоставляет только 7. Если на входе integer, оба правила сопоставляют 7 символов, выбирается правило клавиатуры, поскольку оно
задано первым. Что-либо более короткое (например, int) не сопоставляет выражение integer, поэтому используется интерпретация
данных как идентификатора.
Принцип предпочтения наиболее длинного сопоставления делает
некоторые конструкции, такие как
.*
опасными. Например,
`.*`
могло бы показаться хорошим способом распознавать символьную
строку в кавычках. Но это воспринимается программой как приглашение просматривать далеко вперед в поисках последней кавычки.
Представленное на входе выражение
`first`quoted string here,`second`here
подбирает
`first`quoted string here,`second`
Но это, возможно, не то, что требуется. Более приемлемо
следующее правило:
`[^~\n]*`
которое, при описанном выше вводе, остановится после `first`
Последствия подобных ошибок смягчаются тем, что оператор точки
(.) не сопоставляется знаку новой строки. Поэтому таким выражениям всегда сопоставляется не более одной строки. Не пытайтесь
применять выражение типа
[.\n]+
или их эквиваленты: программа, созданная lex, будет пытаться
считать весь входной файл, что вызовет переполнение внутреннего
буфера.
Заметим, что lex обычно разделяет входной поток, не исследуя все возможные сопоставления каждого выражения. Это значит,
что каждый символ рассматривается только один раз. Предположим,
что надо сосчитать все she и he во входном тексте. Это могут
сделать несколько правил:
she s++;
he h++;
\n |
. ;
где два последних правила игнорируют все, кроме he и she. Следует помнить, что точка (.) не подбирает знак новой строки. Так
как she включает he, то lex не будет распзнавать экземпляры he
внутри she: обработав she, он больше к этому месту не возвращается.
Иногда пользователь хочет изменить свое решение. Действие
REJECT означает переход к выполнению другой альтернативы. Это
вызывает исполнение правила, которое было вторым после исполняемого. Позиция указателя ввода, соответственно, перемещается. Допустим, что пользователь действительно хочет сосчитать все экземпляры he. Для этого можно применить строки:
she {s++;REJECT;}
he {h++;REJECT;}
\n |
. ;
Эти правила - измененный вариант предыдущих примеров. Но их
действие такое же. После подсчета каждого выражения, оно отклоняется. После каждого присваивания будет подсчитываться следующее выражение. В этом примере пользователь мог бы заметить, что
she включает he, но не наоборот, и опустить действие REJECT относительно he. В других случаях нельзя указать, какой входной
символ будет в обоих классах.
Рассмотрим два правила:
a[bc]+{...;REJECT;}
a[cd]+{...;REJECT;}
Если вводится ab, то сопоставляется только первое правило,
если ad - только второе. Входная символьная строка accb сопоставляется первому правилу (для четырех символов), и второму -
для трех. Наоборот, ввод accd сопоставляется второму правилу для
четырех символов, затем первому для трех.
REJECT полезно всегда, когда целью lex является не разделение входного потока, а обнаружение всех экземпляров определенных
элементов входных данных. Эти элементы могут перекрываться или
содержать в себе друг друга. Допустим, что требуется числовая
таблица входных данных; числа перекрываются аналогично тому, как
слово "the" содержит th и he. Следует определить двумерный массив digram. Тогда подойдет такой источник:
%%
[a-z][a-z] {digram[yytext[0]yytext[1]]++;REJECT;}
. ;
\n ;
где REJECT необходим для выборки пары букв, начинающейся с любого символа, а не любого одного символа.
Следует помнить, что REJECT не просматривает входные данные
повторно. Наоборот, он запоминает результаты предыдущего анализа. Это значит, что если найдено правило с последующим контекстом и исполнялось REJECT, то для изменения символов во входном
потоке нельзя использовать input(). Это единственное ограничение,
накладываемое на возможности обработки еще не просматривавшегося
ввода.
Описание зависимости от левого контекста
Иногда необходимо иметь несколько наборов лексических правил и обращаться к ним в различное время в процессе ввода. Например, препроцессор компилятора должен различать операторы препроцессора и анализировать их отдельно от других операторов. Для
этого надо учитывать предыдущий контекст. Существует несклько
способов решения таких проблем. Оператор (^) - это оператор
предшествующего контекста. Он распознает непосредственно предшествующий левый контекст после того, как оператор знака доллара
($) распознает правый контекст. Принципы обработки прилегающего
левого контекста могли бы быть расширены для обеспечения возможностей, аналогичных обеспечиваемым анализои прилегающего правого
контекста. Но вряд ли это так же полезно, поскольку нужный левый
контекст часто появляется раньше например, в начале строки.
Этот раздел описывает три способа работы с различными контекстами:
- Использование флагов, если при переходе от одной среды к
другой меняются всего несколько правил.
- Использование в правилах стартовых условий.
- Использование нескольких лексических анализаторов, работающих совместно.
В каждом случае существуют правила, которые обнаруживают
необходимость изменения среды, в которой анализируется входной
текст, и устанавливают некоторые параметры для учета изменения.
Это может быть флаг, явно проверяемый программой действия пользователя. Такой флаг наиболее просто разрешает проблему, так как
lex вообще не привлекается. Тем не менее, есть еще более удобный
способ - заставить lex запомнить флаги в качестве начальных условийначальных условий правил. Каждое правило может быть ассоциировано с определенным стартовым условием. Оно будет распознаваться только тогда, когда lex будет находиться в этом стартовом
условии. Текущее стартовое условие может быть изменено в любой
момент. Наконец, если наборы правил для различных сред неодинаковы, то самый лучший способ достигнуть ясности - это написать
несколько различных лексических анализаторов и переключатся с
одного на другой.
Допустим, имеется следующая задача: скопировать входные
данные в выходные, заменяя в каждой строке, начинающейся с буквы
a, слово magic на first; заменяя magic на second в каждой строке, начинающейся с буквы b; и заменяя magic на third в каждой
строке, начинающейся с буквы c. Все остальные слова и строки оставляются без изменений.
Эта задача настолько проста, что лучше всего воспользоваться эфлогом:
int flag;
%%
^a {flag=`a`;ECHO;}
^b {flag=`b`;ECHO;}
^c {flag=`c`;ECHO;}
\n {flag=0;ECHO;}
magic {
switch (flag)
{
case`a`:printf("first");break;
case`b`:printf("second");break;
case`c`:printf("third");break;
default:ECHO;break;
}
}
Чтобы решить эту же задачу при помощи стартовых условий,
каждое стартовое условие должно быть представлено lex в секции
определений строкой
% Start name1 name2...
где условия могут именоваться в любом порядке. Слово Start может
быть заменено сокращением s или S. Об условиях можно упомянуть в
начале программы, используя угловые скобки. Например, правило:
<name1> expression
распознается, только когда lex находится в стартовом условии
name1. Чтобы войти в стартовое условие, надо выполнить оператор
действия
BEGIN name1;
который меняет стартовое условие на name1. Для возврата в прежнее положение используйте оператор
BEGIN 0
который устанавливает начальные условия интерпретатора lex. Правило может действовать в нескольких стартовых условиях. Например:
<name1,name2,name3>
является разрешенным префиксом. Любое правило, начинающееся не с
префиксной операции, всегда активно.
Пример, аналогичный предыдущему, может быть записан так:
% START AA BB CC
%%
^a {ECHO;BEGIN AA;}
^b {ECHO;BEGIN BB;}
^c {ECHO;BEGIN CC;}
\n {ECHO;BEGIN 0;}
<AA>magic printf("first");
<BB>magic printf("second");
<CC>magic printf("third");
Здесь логика точно такая же, как и в предыдущем варианте,
но работает lex, а не код, предоставленный пользователем.
Описание определений источника
Вспомним формат источника lex:
{ определения }
%%
{ правила }
%%
{ подпрограммы пользователя }
Пока что были описаны только провила. Вам понадобятся дополнительные опции хотя бы для определения используемых в вашей
программе переменнных и для самого lex. Это можно сделать или в
секции определений или в секции правил.
Помните, что lex превращает правила в программу. Любой источник, не перехваченный lex, копируется в созданную программу.
Существуют три класса подобных случаев:
- Любая строка, которая не является частью правила или
действия lex, начинающегося с пробела или знака табуляции, копируется в программу, созданную lex. Подобный вход из источника,
предшествующий первому ограничителю %%, будет внешним по отношению к любой функции в коде программы; если он появляется сразу
после первого ограничителя %%, то он будет помещен в соответствующее место для определений в функции, написанной lex и содержащей действия. Этот материал должен выглядеть как фрагменты
программ и должен предшествовать первому правилу lex.
Как побочный эффект сказанного выше, строки, начинающиеся с
пробела или знака табуляции и содержащие комментарии, переходят
в генерируемую программу. Это можно использовать в источнике lex
или в генерируемом коде. Комментарии должны быть записаны по
правилам языка СИ.
- Все заключенное между строками, содержащими только %{ и
%}, копируется аналогично тому, как описано выше.Ограничители
отбрасываются. Этот формат допускает ввод текста типа операторов
препроцессора, который должен начинаться в первом столбце, и копирование строк, не являющиеся строками програмн.
- Все расположенное после третьего ограничителя %% копируется после вывода lex, невзирая на формат.
Определения, предназначенные для lex, задаются до первого
ограничителя %%. Любая строка этой секции, не заключенная между
%{ и %} начинающаяся в первом столбце, рассматривается как определение замещающих строк lex. Формат этих строк следующий:
имя перевод
Это вызывает сопоставление строки, заданной как "перевод",
с соответствующим именем. Имя и перевод должны разделяться, по
крайней мере, одним пробелом или знаком табуляции, причем имя
должно начинаться с буквы. Перевод может быть указан в правиле в
формате {name}. Например, использование {D} для цифр и {E} для
порядка экспоненты может сократить правила распознавания чисел:
D [0-9]
E [DEde][-+]?{D}+
%%
{D}+ printf("inreger");
{D}+"."{D}*({E})? |
{D}*"."{D}+({E})? |
{D}+{E} printf("real");
Первые два правила используются для вещественных чисел; в
обоих необходима десятичная точка и десятичная часть. В первом
правиле необходима хотя бы одна цифра перед десятичной точкой;
во втором - хотя бы одна цифра после десятичной точки. Чтобы успешно разрешить проблемы, возникающие в языке FORTRAN при применении выражений вида 35.Q.I, которые не содержат целых чисел,
помимо обычных правил для целых чисел используется контекс тно-зависимое правило:
[0-9]+/"."EQ printf("integer");
Секция определений может содержать также другие команды,
включая таблицу символов, список стартовых условий, соглашения о
размерах массивов по умолчанию, содержащиеся внутри самого lex
для исходных программ большого размера. Эти возможности описываются в разделе "Формат источника".
lex и yacc
Если вы используете lex совместно с yacc, помните: все написанное lex заносится в программу с именем yylex(); это имя
потребуется анализатору yacc. Обычно принимаемая по умолчанию
главная программа библиотеки lex вызывает эту подпрограмму. Но
если yacc загружен и используется его главная программа, yacc
будет вызывать yylex(). В этом случае все правила lex будут
оканчиваться:
return (token);
которое возвращает значение соответствующей лексемы.
Простой способ получить доступ к именам лексем yacc - оттранслировать выходной файл lex как часть выходного файла yacc,
поместив в последнюю секцию входных данных yacc следующую строку:
#include "lex.yy.c"
Предположим, грамматика имеет имя good, а лексические правила - имя better, тогда нужная последовательность команд XENIX
может быть такой:
yacc good
lex better
cc y.tab.c-ly-ll
Библиотеку yacc(-ly) следует загрузить до загрузки библиотеки lex, чтобы получить главную программу, вызывающую анализатор yacc. Генерация программ lex и yacc может быть осуществлена
в любом порядке.
В качестве простой задач, рассмотрим копировани входного
файла с прибавлением 3 к каждому положительному числу, кратному
7. Вот пример исходной программы lex, показывающей как это делать:
%%
int k
[0-9]+ {
k=atoi(yytext);
if(k%7==0)
printf("%d",k+3);
else
printf("%d",k);
}
Правило [0-9] узнает строки цифр; atoi (см. atof(2)) - преобразует цифры в двоичные и накапливает результат в k. Оператор
остатка (%) используется для проверки, является ли k кратным 7;
если да, то к нему добавляется 3. Можно показать, что эта программа будет искажать входные данные типа 49.63 или X7
Более того, она увеличивает абсолютное значение всех отрицательных чисел, кратных семи. Чтобы избежать этого, надо добавить несколько правил к имеющимся, как это показано ниже:
%% int k;
-?[0-9]+ {
k=atoi(yytext);
printf("%d",k%7==0?k+3:k);
}
-?[0-9.]+ ECHO;
[A-Za-z][A-Za-z0-9]+ ECHO;
Числовые строки, содержащие десятичные точки или начинающиеся с букв, будут отбираться одним из двух последних правил и
изменяться не будут. if-else для экономии места замещается условным оператором СИ. Конструкция a?b:c означает: если a, то b,
иначе c.
Следующая программа производит статистическую обработку. Она строит гистограмму длин слов, считая словом строку
букв..
int lengs[100];
%%
[a-z]+ lengs[yyleng]++;
. |
\n ;
%%
yywrap()
{
int i;
printf("Length No. words\n");
for(i=0;i<100;i++)
if(lengs[i]>0)
printf("%5d%10d\n",i,lengs[i]);
return(1);
}
Эта программа накапливает гистограмму, не выводя ничего в
процессе работы. По окнчании ввода она печатает таблицу. Последняя команда return(1) означает, что lex должен выполнить цикл.
Если yywrap() выдает нуль ("ложь"), это значит, что имеются еще
входные данные и следует продолжать ввод и обработку. Если
yywrap() никогда не получит значение "истина", программа впадет
в бесконечный цикл.
В качестве примера большего обьема, приведем части программы, предназначенной для преобразования чисел двойной точности
FORTRANа в одинарную. Так как FORTRAN не различает заглавные и
строчные буквы, эта подпрограмма начинается с определения набора
классов, включающих оба варианта каждой буквы:
a [aA]
b [bB]
c [cC]
. .
. .
. .
z [zZ]
Дополнительный класс распознает пробел:
w[\t]*
Первое правило переводит double precision в real или DOUBLE
PRECISION в REAL.
{d}{o}{u}{b}{l}{e}{W}{p}{r}{e}{c}{i}{s}{i}{o}{n}{
printf(yytext[0]=='d'?"real":"REAL");
}
Во всей программе соблюдается осторожность, чтобы сохранить
исходный текст программы. Для выбора правильных форм ключевых
слов используется условный оператор.. Следующее правило копирует
указания карты продолжения, чтобы не спутать их с константами:
^" "[^0] ECHO;
В обычном выражении пробел заключается в кавычки. Это распознается как начало строки, затем ставится пять пробелов, затем
любой символ, кроме пробела или нуля. Здесь символ (^) выступает
в двух значениях. Приводим пример программы, которая заменяет
константы двойной точности на обычные константы с плавающей точкой:
[0-9]+{W}{d}{W}[+-}?{W}[0-9]+ |
[0-9]+{W}"."{W}{d}{W}[+-]?{W}[0-9]+ |
"."{W}[0-9]+{W}{d}{W}[+-]?{W}[0-9]+ {
/*convert constants*/
for(p=yytext;*p!=0;p++)
{
if(*p=='d'||*p=='D')
*p+='e'-'d';
ECHO;
}
После нахождения константы с плавающей точкой, она обрабатывается в цикле for с целью найти букву "d" или "D". Затем
программа добавляет "'e'-'d'", что переводит ее в следующую букву алфавита. Модифицированная константа выводится снова, теперь
уже одинарной точности. Далее приводится ряд имен, которые должны быть преобразованы с целью удалить начальную "d". При испльзовании массива yytext подобное действие удовлетворяет всем именам (здесь приводится только часть довольно длинного списка).
{d}{s}{i}{n} |
{d}{c}{o}{s} |
{d}{s}{q}{r}{t} |
{d}{a}{t}{a}{n} |
...
{d}{f}{l}{o}{a}{t}printf("%s",yytext+1);
Другой список имен изменяет начальную d на начальную a:
{d}{l}{o}{g} |
{d}{l}{o}{g}10 |
{d}{m}{i}{n}1 |
{d}{m}{a}{x}1 {
yytext[0]+='a'-'d';
ECHO;
}
Еще одна подпрограмма изменяет начальную d на r:
{d}1{m}{a}{c}{h}{
yytext[0]+='r'-'d';
ECHO;
}
Чтобы избежать восприятия таких имен, как имен dsinx, в качестве экземпляров dsin, некоторые конечные правила принимают
более длинные слова как идентификаторы и копируют некоторые оставшиеся символы:
[A-Za-z][A-Za-z0-9]* |
[0-9]+ |
\n |
. ECHO;
Заметим, что эта программа не является полной; она не справится с проблемами пробелов в FORTRAN и с использованием ключевых
слов в качестве идентификаторов.
Описание наборов символов
Программы, созданные lex, осуществляют символьный ввод/вывод только с помощью подпрограмм input(), output(), unput(). Таким образом, символьное представление в этих подпрограммах допускается lex и используется для возврата значений в yytext. Для
внутреннего использования символ представляется как малое целое.
Если используется стандартная библиотека, он имеет значение,
равное целому значению двоичного кода, представляющего символ на
главной ЭВМ. Обычно буква a представляется в той же форме, что и
символьная константа:
'a'
Если эта интерпретация изменяется добавлением подпрограмм
ввода-вывода, транслирующих символы, то об этом надо информировать lex с помощью таблицы перекодировки. Эта таблица должна находиться в секции определений и должна отделяться строками, содержащими только %T. Таблица состоит из строк следующего вида:
{целое} {строка символов}
которые указывают значения, соответствующие каждому символу.
Например:
%T
1 Aa
2 Bb
...
26 Zz
27 \n
28 +
29 -
30 0
31 1
...
39 9
%T
Эта таблица устанавливает соответствие заглавных и строчных
букв целым числам от 1 до 26, знака новой строки - 27, плюса (+)
и минуса (-) - 28 и 29, и цифр - числам от 30 до 39. Заметьте
избегание знака новой строки. Если таблица задается, то каждый
символ, который может появиться в правилах или в корректных
входных данных, должен быть в нее включен. нельзя пользоваться
значением 0 и значениями, выходящими за пределеы набора кодов,
обеспечиваемого аппаратной частью.
Формат источника
Стандартная форма файла источника lex:
{определения}
%%
{правила}
%%
{подпрограммы пользователя}
Cекция определений содержит комбинацию следующих элементов:
- Определения в форме "имя пробел перевод".
- Включаемый код в форме "пробел код".
- Включаемый код в форме
%{
код
%}
- Стартовые условия, задаваемые в форме:
%S имя1 имя2
- Таблицы наборов символов в форме:
%T
число пробел строка-символов
%T
- Изменения размеров внутреннего массива в форме:
%x nnn
где nnn - десятичное целое число, представляющее размер массива;
x выбирает значение одного из следующих параметров:
Буква | параметр
|
---|
p | позиции
|
n | состояния
|
e | вершины дерева
|
a | переходы
|
k | упакованные классы символов
|
o | размер выходного массива
|
В секции правил строки имеют вид:
выражение действие
где действие может быть продолжено на следующих строках, если
использовать фигурные скобки для обозначения границ.
Регулярные выражения в lex используют следующие операторы:
x | символ "x"
|
"x" | "x", даже если x - оператор
|
\x | "x", даже если х - оператор
|
[xy] | символ x или y
|
[x-z] | символы x,y или z
|
[^x] | любой знак, кроме x
|
. | любой знак, кроме знака новой строки
|
^x | x в начале строки
|
<y>x | x, если lex находится в стартовом условии y
|
x$ | x в конце строки
|
x? | необязательный x
|
x* | 0,1,2,... экземпляров x
|
x+ | 1,2,3,... экземпляров x
|
x|y | x или y
|
(x) | x
|
x/y | x, но только если за ним следует y
|
{xx} | перевод xx из секции определений
|
x{m,n} | от m до n вхождений x
|