Добавил:
Upload Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:
СПО юнита 2.doc
Скачиваний:
38
Добавлен:
17.11.2019
Размер:
5.82 Mб
Скачать

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

На этапе лексического анализа происходит формирование языковых символов из последовательностей знаков. Например, в языке С содержится шесть типов символов:

– ключевые слова (const, char, if, else, typedef);

– идентификаторы (sum, main, printf);

– константы (28, 3.141529, 017 (восьмеричная система));

– строковые литералы ("Katherine", "bannockburn");

– операторы (+, -, ++, », /=, &&);

– знаки пунктуации ({, ], ...).

Типы символов формируются лексическим анализатором в процессе лексического анализа.

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

Помимо распознавания символов языка, лексический анализатор выполняет и другие задачи:

– удаление комментариев;

– введение номеров строк;

– вычисление констант.

Распознавание символов

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

64 const char typedef >> +.

Причина – каждый отдельный символ полностью корректен. То, что данная последовательность не составляет начала какой-либо программы, будет обнаружено синтаксическим анализатором. Лексический анализатор не придает значения области видимости переменных, т.е. он не различает использование идентификатора sum для представления двух различных переменных в различных функциях. Где бы ни появлялся идентификатор, для лексического анализатора он является одним и тем же. В фазе лексического анализа он даже не будет определен как переменная в отличие, например, от имени функции.

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

letter (letter  digit)*.

Подобным образом можно представить и действительное число

(+ | – ) digit*.digit digit*.

Возможные ситуации и способы их представления в программе:

– необязательные знаки ("+", "–"). Если их нет – это не ошибка, а переход к считыванию следующего символа;

– обязательные знаки (десятичная точка и одна цифра после нее). Если их нет – вызывается функция error;

– знаки, которые могут появиться нуль или большее число раз – инициируется цикл while для проверки каждого знака без обращения к функции error.

Инструментальное средство Lex

Конструирование устройств распознавания символов из регулярных выражений или конечных автоматов, принимающих эти выражения, может быть автоматизировано с помощью соответствующих инструментальных средств. Наиболее известным для этих целей инструментом является Lex, используемый совместно с генератором программ синтаксического анализа YACC в среде Unix. В настоящее время существует общедоступный эквивалент Lex, именуемый Flex. Кроме того, имеются версии для других сред, таких как Windows и Macintosh. Изначально для Lex необходимо было записывать действия, сопровождающие анализ, на языке С. Современные версии Lex позволяют использовать и другие языки (Турбо Паскаль и C++).

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

– форма записи Lex допускает более эффективное представление некоторых типов символов;

– в определенных обстоятельствах форма записи Lex pacширяет выразительную силу обозна-чений регулярных выражений.

Чтобы определить идентификатор в обозначениях Lex, нужно определить нетерминалы letter и digit:

– letter [a-z];

– digit [0-9].

Это называется определениями в Lex. Отметим, что необязательно перечислять каждый знак в диапазоне a–z или 0–9. Теперь можно определить идентификатор:

identifier {letter}({letter}|{digit})*.

Здесь | и * означают то же, что и в регулярных выражениях, а фигурные скобки применяются для обособления уже определенных величин. Если какое-то действие должно выполняться всякий раз, когда встречается идентификатор, то оно выражается в виде следующего правила:

{identifier} {printf("идентификатор опознан\n");}.

При каждом распознавании идентификатора {identifier} выполняется простой оператор.

Общий вид входа, ожидаемого Lex:

определения

%%

правила

%%

пользовательские функции

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

Обозначения, используемые на входе Lex:

а – представляет отдельный знак;

"а" – представляет а, если а – знак, используемый в системе обозначений;

a b – представляет а или b;

a? – представляет нуль или одно вхождение а;

a* – представляет нуль или более вхождений а;

a+ – представляет одно или более вхождений а;

a{m,n} – представляет от m до n вхождений а;

[a-z] – представляет набор знаков (алфавит);

[а-z A-Z] – представляет набор знаков (больший);

[ а-z] – представляет дополнение первого набора знаков;

{name} – представляет регулярное выражение, определенное идентификатором name;

а – представляет а в начале строки;

а$ – представляет а в конце строки;

ab\xy – представляет ab, следующее перед ху.

Cлова языка распознаются как таковые, а не как идентификаторы. Для поиска соответствия Lex прежде всего обращается к разделу правил, и поэтому важно, чтобы именно там были определены слова языка. Double распознается как идентификатор, а первые две его буквы не распознаются как слово языка do. Для сопоставления Lex подыскивает более длинное слово, и только в случае, если два слова имеют одинаковую длину, анализатор выбирает первое.

Лексические затруднения

Существуют языковые характеристики, которые усложняют создание лексических анализаторов. Большинство из таких характеристик принадлежит к одному из следующих классов:

– слова языка доступны для использования в качестве идентификаторов;

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

Языки Фортран и PL/1 допускают использование слов языка в качестве идентификаторов, определяемых пользователем. Преимущество такого подхода заключается в том, что пользователю языка не нужно знать все слова языка перед написанием программы. Язык Кобол содержит более 100 слов, при этом ни одно из них не может быть переопределено пользователем. Возможность использования слов языка в качестве идентификаторов приводит к возникновению трудностей в лексическом анализаторе и, возможно, затрудняет чтение программы человеком. Рассмотрим, к примеру, фрагмент кода на языке Фортран

IF(I) = 1.

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

IF(I) 1, 2, 3.

Фортран допускает использование слов языка в качестве идентификаторов, определяемых пользователем. Он также не придает значения пробелам внутри идентификаторов и в отличие от большинства языков не использует пробелы для разделения символов. Выражение

DO 7 I = 1, 5

является началом оператора DO. В то же время, пока не считан знак ",", начало строки может представлять идентификатор

DO 7 I.

Чтобы избежать подобной неоднозначности, лексический анализатор для языка Фортран должен уметь выполнять некоторый предпросмотр, и именно поэтому Lex предлагает средства сопоставления строк. Предпросмотр в Фортране обычно ограничен и никогда не простирается далее текущего оператора.

В языке PL/1 слова языка также не резервируются. Вследствие более структурированной природы языка (по сравнению с Фортраном) для устранения локальных неоднозначностей может требоваться произвольный объем предпросмотра. Рассмотрим следующее выражение:

IF (I) = THEN + THEN;

Оно представляет оператор присваивания. В то же время, выражение

IF (I) = THEN + THEN + THEN;

является оператором IF. Подобным образом

DO WHILE (P = 0);

является оператором (do while), a

DO WHILE(P) = 0...

является оператором DO с управляющей переменной while(p).