Теория языков программирования и методы трансляции Тема №3 Лексический анализ Вопросы Основные черты типичного лексического анализатора. Построение простого лексического анализатора с помощью регулярных выражений и связанных с ними автоматов. Использование генератора лексических анализаторов Lex для создания лексических анализаторов. Некоторые лексические "проблемы", возникающие в хорошо известных языках программирования. Лексический анализ В этой теме на концептуальном уровне рассматривается, что представляет собой первая фаза компиляции — лексический анализ — основные функции которой состоят в группировке последовательностей знаков исходного кода в символы языка. Лексический анализ На этапе лексического анализа происходит формирование языковых символов из последовательностей знаков. Например, в языке С содержится шесть типов символов. Ключевые слова, например, const, char, if, else, typedef. Идентификаторы, например, sum, main, printf. Константы, например, 28, 3.141529, 017 (восьмеричная система). Строковые литералы, например, "Katherine", "bannockburn". Операторы, например, +, -, ++, --, Знаки пунктуации, например, {, ], ..., ;. Лексический анализ Благодаря сравнительно простой природе символов, их всегда можно представить с помощью регулярных выражений или, эквивалентно, грамматик 3-го типа. Процесс создания лексического анализатора легко автоматизируется, а инструментальные средства для его создания на основе регулярных грамматик (или регулярных выражений) всегда доступны. Лексический анализ Помимо распознавания символов языка, лексический анализатор также выполняет некоторые другие задачи. Удаление комментариев. Введение номеров строк. Вычисление констант. Впрочем, существуют аргументы за то, чтобы последнюю задачу выполнял машинно-зависимый постпроцессор компилятора. Лексический анализ Лексический анализатор всего лишь распознает символы языка для передачи их синтаксическому анализатору. Порядок следования символов для него абсолютно не важен. 64 const char typedef >> + Каждый отдельный символ полностью корректен. То, что данная последовательность не составляет начала (или хотя бы фрагмента) какой-либо программы, должно быть обнаружено синтаксическим анализатором. Лексический анализ Для лексического анализа регулярные выражения представляют собой удобный метод представления символов, таких как идентификаторы и константы. Например, идентификатор может быть представлен следующим образом. letter (letter | digit)* Подобным образом можно представить и действительное число. (+| ‒)digit*.digit digit* Лексический анализ Достаточно легко написать программу распознавания символов. Программа распознавания любого идентификатора может выглядеть следующим образом: main() { char in = getchar(); if(isAlpha(in)) in = getchar(); else error(); while(isAlpha(in) || isDigit(in)) in = getchar(); } Лексический анализ Программа распознавания действительно числа может иметь вид: main() { char in; in = getchar(); if (in == '+' || in == '-') in = getchar(); while (isdigit(in)) in = getchar(); if (in == '.') in = getchar(); else error(); if (isdigit(in)) in = getchar(); else error(); while (isdigit(in)) in = getchar(); printf("ok\n"); } Написать код очень просто — он пишется с такой же скоростью, с какой вы можете набирать! Более того, его создание можно легко автоматизировать. Лексический анализ Вместо регулярных выражений можно использовать соответствующий конечный автомат. Конечный автомат состоит из конечного множества состояний и переходов между ними, которые определяются считываемыми знаками из входной строки. При этом одно состояние определяется как начальное, а одно или более состояний — как конечные. Считается, что конечный автомат принял входную строку, если, начав работу с начального состояния и выполнив соответствующие переходы при считывании каждого знака исходной строки, автомат переходит в конечное состояние, когда строка полностью считана. Лексический анализ Конченый автомат определяется как следующая пятерка элементов. M (K , , , S , F ) K множество состояний; алфавит, на основе которого формируются входные строки; множество переходов; S (S K ) начальное состояние; F ( F K ) множество конечных состояний. Лексический анализ Переходы δ можно определить как таблицу (или графически), и они для каждого состояния будут указывать следующее состояние и все возможные входные знаки. Лексический анализ int main() { int state; char in; state = 1; in = getchar(); while (isAlpha(in)||isDigit(in)) {switch(state) {case 1: if (isAlpha(in)) state = 2; else error(); break; case 2: state = 2; break; } in = getchar(); } return (state == 2); } Лексический анализ Действительное число можно представить с помощью приведенного ниже конечного автомата Лексический анализ int isSign(char sign) { return (sign == ‘+’ || sign==‘-’);} int main() { int state; char in; state = 1; in = getchar(); while(isDigit(in) || isSign(in) || in == ‘.’) {switch(state) {case 1: if(isDigit(in) || isSign(in)) state = 2; else if (in == ‘.’) state = 3; break; case 2: if(isDigit(in)) state = 2; else if (in == ‘.’) state = 3; break; case 3: if(isDigit(in)) state = 4; else error(); break; case 4: if(isDigit(in)) state = 4; else error; break;} in = getchar(); } return (state == 4) } Лексический анализ Для определения символов, поступающих на вход Lex, используется форма записи, весьма подобная применяющейся для регулярных выражений. Отличия касаются двух принципиальных моментов моментов. 1. Форма записи Lex допускает более эффективное представление (с точки зрения числа знаков, используемых в представлении) некоторых типов символов. 2. Кроме того, в определенных обстоятельствах форма записи Lex расширяет выразительную силу обозначений регулярных выражений. Чтобы определить идентификатор в обозначениях Lex, для начала можно определить нетерминалы letter и digit. letter [a-z] digit [0-9] Это называется определениями в Lex. Отметим, что совсем необязательно перечислять каждый знак в диапазоне a-z или 0-9. Далее определим сам идентификатор identifier {letter}({letter}|{digit})* Лексический анализ Если какое-то действие должно выполняться всякий раз, когда встречается идентификатор, то оно выражается в виде следующего правила {identifier} {printf("идентификатор опознан\n");} Полным входом для создания анализатора, распознающего идентификаторы и при этом каждый раз выполняющего приведенную выше команду, будет следующий код Lex. letter [a-z] digit [0-9] identifier {letter}({letter}|{digit})* %% {identifier} {printf("идентификатор опознан \n"};} %% Лексический анализ lex firstlex.l firstlex.c (firstlex.java) Более интересный анализатор можно создать с помощью следующего кода Lex. letter [a-z] digit [0-9] identifier {letter}({letter}|{digit})* %% {identifier} {printf("идентификатор %s yytext, yylineno);} %% в строке %d\n", Лексический анализ Общий вид входа ожидаемого Lex. определения %% правила %% пользовательские функции Здесь вторая часть является обязательной, а остальные используются по мepe необходимости. Различные части должны отделяться посредством (строки, которая в крайнем левом положении содержит символы %%) Лексический анализ Вход Lex для создания анализатора по распознанию и распечатке действительных чисел digit [0-9] realno [+\-]{digit}*\.{digit}+ %% {realno} {printf("действительное число %s в строке %d\n", yytext, yylineno);} В этом примере иллюстрируются два момента 1. 2. Перед знаками ввода, которые являются частью системы обозначений ("-" и "."), необходимо употреблять знак "\" (или брать в двойные кавычки). Следует отметить, что при первом появлении нет нужды выделять подобным образом знак "+", поскольку здесь неоднозначности не имеется. Знак "+" (как часть системы обозначения) используется для указания, что предшествующий знак употребляется один или более раз. Это не увеличивает выразительную силу обозначения по сравнению с регулярными выражениями, но делает запись немного компактнее и легче для понимания. Лексический анализ Система обозначений Lex a представляет отдельный знак; \a представляет а, если а — знак, используемый в системе обозначений (для устранения неоднозначности); “a” также представляет а, если а — знак, используемый в системе обозначений; a|b представляет а или b; a? представляет нуль или одно вхождение а; a* представляет нуль или более вхождений а; a+ представляет одно или более вхождений а; a{m,n} представляет от m до n вхождений а; Лексический анализ [a-z] представляет набор знаков (алфавит); [a-zA-Z] также представляет набор знаков (больший); [^a-z] представляет дополнение первого набора знаков; {name} представляет регулярное выражение, определенное идентификатором name; ^a представляет а в начале строки; a$ представляет а в конце строки; ab\xy представляет ab, следующее перед ху. Лексический анализ digit [0-9] intconst [+\-]?{digit}+ realconst [+\-]?{digit}+\.{digit}+(e[+\-]?{digit}+)? letter [a-zA-Z] identifier {letter}({letter}|{digit})* whitespace [ \t\n] stringch [^’] string ‘{stringch}+’ otherch [^0-9a-zA-Z+\-’ \t\n] othersymb {otherch}+ Лексический анализ %% program printf("опознано слово program\n"); var printf("опознано слово var\n"); begin printf ("опознано слово begin\n); for printf("опознано слово for\n"); to printf("опознано слово to\n"); do printf ("опознано слово do\n"),; end printf("опознано слово end\n"); {intconst} printf("целое число %s в строке %d\n, yytext, yylineno); {realconst} printf("действительное число %s в строке %d\n", yytext, yylineno); {string} printf(“строка %s в строке %d\n", yytext, yylineno); {identifier} printf("идентификатор %s в строке %d\n",yytext, yylineno); {whitespace} ; /*нет действий*/ {othersymb} ; /*нет действий*/ %% Лексический анализ program double (input, output); var i: 1..10; begin writeln('число':10, 'удвоенное число':10); for i:= 1 to 10 do writeln (i:X0, i*i:10); writeln end. опознано слово program идентификатор double в строке 1 идентификатор input в строке 1 идентификатор output в строке 1 опознано слово var идентификатор i в строке 2 целое число 1 в строке 2 целое число 10 в строке 2 опознано слово begin идентификатор writeln в строке 4 строка 'число' в строке 4 целое число 10 в строке 4 Лексический анализ С помощью функции yylex() Lex может вызываться из программы на С. Следующий пример входа Lex показывает, как код С можно интегрировать в анализатор, созданный Lex. %{ int chars = 0, lines = 0; }% %% \n ++lines; . ++chars; %% void main() { yylex(); printf(“Число знаков = %d, число строк = %d”, chars, lines) } Лексический анализ С помощью следующего входа Lex можно получить максимальное и среднее значения длин слов для некоторой части программы. %{ int letters = 0, words = 0, len = 0, length = 0; }% word [a-zA-Z]+ space [ \n] ws {space}+ %% {word} {++word; length = yyleng; letters = letters + length; if(length > len) len = length;} ws ; /*ничего не делать*/ . ; /*ничего не делать*/ %% void main {yylex(); printf(“максимальная длина слова = %d, средняя = %f”, len, letters/words);} Лексический анализ Если необходимо, чтобы анализатор окончил работу в конце первого предложения, то вход Lex можно записать следующим образом. %{ int letters = 0, words = 0, len = 0, length = 0; }% word [a-zA-Z]+ space [ \n] ws {space}+ eos [!?.] %% {word} {++word; length = yyleng; letters = letters + length; if(length > len) len = length;} {eos} yywrap(); ws ; /*ничего не делать*/ . ; /*ничего не делать*/ %% void main {yylex(); printf(“максимальная длина слова = %d, средняя = %f”, len, letters/words);} Вызов yywrap() означает прекращение анализа Лексический анализ Еще одним простым примером является использование Lex для создания инструмента добавления номеров строк в исходный код. Рассмотрим следующий вход для Lex. %{ int lineno = 0; }% line [^\n]*\n %% {line} {printf(“%d %s”, lineno++, yytext);} %% void main() { yylex(); } Выходом будет исходный код, в котором каждая строка (включая пустые) начинается с номера строки, считая от начала исходного кода. Лексический анализ Распознать комментарии в языке обычно нелегко. Проблема заключается в знаках, используемых для обособления комментария, которые, следовательно, не должны появляться внутри комментария. comment “/*”“/”*([^*/]|[^*]“/”|“*”[^/])*“*”*“*/” %% “/*” {char in; for (;;) { while ((in = getchar()) ! = '*'); /* ничего не делать */ while ((in = getchar() =='*'); /* пропустить *'s */ if (in == ‘/’) break; /* окончание комментария*/ }} Лексический анализ Несмотря на общую простоту процесса лексического анализа, существует небольшое количество языковых характеристик, что усложняют создание лексических анализаторов. Большинство из таких характеристик принадлежит к одному из следующих классов. Ключевые слова языка доступны для использования в качестве идентификаторов. Интерпретация некоторых последовательностей знаков является контекстно-зависимой.