Добавил:
Upload Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:
Glava14 r.doc
Скачиваний:
0
Добавлен:
01.07.2025
Размер:
548.35 Кб
Скачать

Этапы семантического анализа

Семантический анализатор выполняет следующие основные действия:

--- проверка соблюдения во входной программе семантических соглашений вход

ного языка;

--- дополнение внутреннего представления программы в компиляторе оператов;

рами и действиями, неявно предусмотренными семантикой входного языка;

--- проверка элементарных семантических (смысловых) норм языков програм мирования, напрямую не связанных с входным языком.

Проверка соблюдения во входной программе семантических соглашений входного языка заключается в сопоставлении входных цепочек программы с требованиями семантики входного языка программирования. Каждый язык программирования имеет четко заданные и специфицированные семантические соглашения, которые не могут быть проверены на этапе синтаксического разбора. Именно их в первую очередь проверяет семантический анализатор.

Примерами таких соглашений являются следующие требования:

--- каждая метка, на которую есть ссылка, должна один раз присутствовать в программе;

--- каждый идентификатор должен быть описан один раз, и ни один идентификатор не может быть описан более одного раза (с учетом блочной структуры описаний);

--- все операнды в выражениях и операциях должны иметь типы, допустимые для данного выражения или операции;

--- типы переменных в выражениях должны быть согласованы между собой;

--- при вызове процедур и функций число и типы фактических параметров должны быть согласованы с числом и типами формальных параметров.

Это только примерный перечень такого рода требований. Конкретный состав требований, которые должен проверять семантический анализатор, жестко связан с семантикой входного языка (например, некоторые языки допускают не описывать идентификаторы определенных типов). Варианты реализаций такого рода семантических анализаторов детально рассмотрены в [6, т. 2, 74].

Например, если мы возьмем оператор языка Pascal, имеющий вид:

то с точки зрения синтаксического разбора это будет абсолютно правильный оператор. Однако мы не можем сказать, является ли этот оператор правильным с точки зрения входного языка (Pascal), пока не проверим семантические требования для всех входящих в него лексических элементов. Такими элементами здесь являются идентификаторы a, b и с. He зная, что они собой представляют, мы не можем не только окончательно утверждать правильность приведенного выше оператора, но и понять его смысл. Фактически необходимо знать описание этих идентификаторов.

B том случае, если хотя бы один из них не описан, имеет место явная ошибка. Если это числовые переменные и константы, то мы имеем дело с оператором сложения, если же это строковые переменные и константы — с оператором конкатенации строк. Кроме того, идентификатор а, например, ни в коем случае не может быть константой — иначе нарушена семантика оператора присваивания. Также невозможно, чтобы одни из идентификаторов были числами, а другие — строками, или, скажем, идентификаторами массивов или структур — такое сочетание аргументов для операции сложения недопустимо. И это только некоторая часть соглашений, которые должен проверить компилятор с точки зрения семантики входного языка (в данном примере — Pascal).

Следует еще отметить, что от семантических соглашений зависит не только правильность оператора, но и его смысл. Действительно, операции алгебраического сложения и конкатенации строк имеют различный смысл, хотя и обозначаются в рассмотренном примере одним знаком операции — «+». Следовательно, от семантического анализатора зависит также и код результирующей программы.

Если какое-либо из семантических требований входного языка не выполняется, то компилятор выдает сообщение об ошибке и процесс компиляции на этом, как правило, прекращается.

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

Если вернуться к рассмотренному выше элементарному оператору языка Pascal:

то можно отметить, что здесь выполняются две операции: одна операция сложения (или конкатенации, в зависимости от типов операндов) и одна операция присвоения результата. Соответствующим образом должен быть порожден и код результирующей программы.

Однако не все так очевидно просто. Допустим, что где-то перед рассмотренным оператором мы имеем описание его операндов в виде:

из этого описания следует, что а — вещественная переменная языка Pascal, b — целочисленная переменная, с — вещественная переменная с двойной точностью. Тогда смысл рассмотренного оператора с точки зрения входной программы существенным образом меняется, поскольку в языке Pascal нельзя напрямую выполнять операции над операндами различных типов. Существуют правила преобразования типов, принятые для данного языка. Кто выполняет эти преобразования?

Это может сделать разработчик программы — но тогда преобразования типов в явном виде будут присутствовать в тексте входной программы (в рассмотренном примере это не так). B другом случае это делает код, порождаемый компилятором, когда преобразования типов в явном виде в тексте программы не присутствуют, но неявно предусмотрены семантическими соглашениями языка. Для этого в составе библиотек функций, доступных компилятору, должны быть функции преобразования типов (более подробно о библиотеках функций см. в разделе «Принципы функционирования систем программирования», глава 15). Вызовы этих функций как раз и будут встроены в текст результирующей программы для удовлетворения семантических соглашений о преобразованиях типов во входном языке, хотя в тексте программы в явном виде они не присутствуют. Чтобы это произошло, эти функции должны быть встроены и во внутреннее представление программы в компиляторе. За это также отвечает семантический анализатор.

C учетом предложенных типов данных, в рассмотренном примере будут не две, а четыре операции: преобразование целочисленной переменной b в формат вещественных чисел с двойной точностью; сложение двух вещественных чисел с двойной точностью; преобразование результата в вещественное число с одинарной точностью; присвоение результата переменной с. Количество операций возросло вдвое, причем добавились два вызова весьма нетривиальных функций преобразования типов. Разработчик программы должен помнить об этом, если хочет добиваться высокой эффективности результирующего кода.

Преобразование типов — это только один вариант операций, неявно добавляемых компилятором в код программы на основе семантических соглашений. Другим примером такого рода операций могут служить операции вычисления адреса, когда происходит обращение к элементам сложных структур данных. Существуют и другие варианты такого рода операций (преобразование типов — только самый распространенный пример).

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

Проверка элементарных смысловых норм языков программирования, напрямую не связанных с входным языком, — это сервисная функция, которую предоставляют большинство современных компиляторов. Эта функция обеспечивает проверку компилятором некоторых соглашений, применимых к большинству современных языков программирования, выполнение которых связано со смыслом как всей входной программы в целом, так и отдельных ее фрагментов.

Примерами таких соглашений являются следующие требования:

--- каждая переменная или константа должна хотя бы один раз использоваться в программе;

--- каждая переменная должна быть определена до ее первого использования при любом ходе выполнения программы (первому использованию переменной должно всегда предшествовать присвоение ей какого-либо значения);

--- результат функции должен быть определен при любом ходе ее выполнения;

--- каждый оператор в исходной программе должен иметь возможность хотя бы один раз выполниться;

--- операторы условия и выбора должны предусматривать возможность хода выполнения программы по каждой из своих ветвей;

--- операторы цикла должны предусматривать возможность завершения цикла.

Конечно, это только примерный перечень основных соглашений. Конкретный состав проверяемых соглашений зависит от семантики языка. Однако, в отличие от семантических требований языка, строго проверяемых семантическим анализатором, выполнение данных соглашений не является обязательным. Поэтому то, какие конкретно соглашения будут проверяться и как они будут обрабатываться, зависит от качества компилятора, от функций, заложенных в него разработчиками. Простейший компилятор вообще может не выполнять этот этап семантического анализа и не проверять ни одного такого соглашения, даже если это и возможно с точки зрения семантики входного языка1.

1 Конечно, современные компиляторы, создаваемые известными фирмами-разработчиками, стремятся выполнить проверку максимально возможного числа такого рода соглашений (насколько это позволяет входной язык). Эта функция обычно преподносится как одно из достоинств того или иного компилятора и способствует завоеванию им хороших позиций на рынке.

Необязательность соглашений такого типа накладывает еще одну особенность на их обработку в семантическом анализаторе: их несоблюдение не может трактоваться как ошибка. Даже если компилятор полностью уверен в своей «правоте», тот факт, что какое-то из указанных соглашений не соблюдается, не должен приводить к прекращению компиляции входной программы. Обычно факт обнаружения несоблюдения такого рода соглашений трактуется компилятором как «предупреждение» (warning). Компилятор выдает пользователю сообщение об обнаружении несоблюдения одного из требований, не прерывая сам процесс компиляции, — то есть он просто обращает внимание пользователя на то или иное место в исходной программе. To, как реагировать на «предупреждение» (вносить изменения в исходный код или проигнорировать этот факт), — это уже забота и ответственность разработчика программы.

Необязательность указанных соглашений объясняется тем, о чем уже говорилось выше (см. раздел «Языки и цепочки символов. Способы задания языков», глава 9), — ни один компилятор не способен полностью понять и оценить смысл исходной программы. A поскольку смысл программы доступен только человеку (для плохо написанной программы — только ее разработчику, а в другом случае — некоторому кругу лиц), то он и долженнести ответственность за семантические соглашения1.

Задача проверки семантических соглашений входного языка во многом связана с проблемой верификации программ. Эта проблема детально рассмотрена в [1, 25, 46, 51].

Рассмотрим в качестве примера функцию, представляющую собой фрагмент входной программы на языке C:

Практически любой современный компилятор с этого языка обнаружит в данном месте входной программы массу «неточностей». Например, переменная с описана, ей присваивается значение, но она нигде не используется; значение переменной b, присвоенное в операторе b=0, тоже никак не используется; наконец, услов-

1 Обычно хорошим стилем считается построить программу так, чтобы она компилировалась «без предупреждений», — то есть так, чтобы компилятор не обнаруживал в ней ни одного несоблюдения соглашений о смысле операторов входного языка. B большинстве случаев это возможно. Тем не менее практически все современные компиляторы позволяют отключить в процессе компиляции программы проверку того или иного соглашения, вплоть до полного исключения всех возможных вариантов такого рода проверки. Эти соглашения входят в состав «опций» компиляторов. Следует отметить, что отключать можно только необязательные соглашения — ни одну проверку семантических требований языка отключить невозможно.

ный оператор лишен смысла, так как всегда предусматривает ход выполнения только по одной своей ветке (и значит, оператор c=a+b; никогда выполнен не будет). Скорее всего, компилятор выдаст еще одно предупреждение, характерное именно для языка C, — в операторе if(b=l) присвоение стоит в условии (это не запрещено ни синтаксисом, ни семантикой языка, но является очень распространенной смысловой ошибкой в C). B принципе смысл (а точнее, бессмысленность) этого фрагмента будет правильно воспринят и обработан компилятором.

Однако если взять аналогичный по смыслу, но синтаксически более сложный фрагмент программы, то картина будет несколько иная:

Здесь уже компилятор вряд ли сможет выяснить порядок изменения значения переменных и выполнение условий в данном фрагменте из двух функций (обе они сами по себе независимо вполне осмысленны!). Единственное предупреждение, которое, скорее всего, получит в данном случае разработчик, — это то, что функция f_test не всегда корректно возвращает результат (отсутствует оператор return перед концом функции). И то это предупреждение на самом деле не будет соответствовать истинному положению вещей.

B целом проверка дополнительных семантических соглашений является весьма полезной функцией компиляторов, но ответственность за смысл исходнойпрограммы всегда по-прежнему остается на ее разработчике.

Соседние файлы в предмете [НЕСОРТИРОВАННОЕ]