Этапы семантического анализа
Семантический анализатор выполняет следующие основные действия:
· проверку соблюдения во входной программе семантических соглашений входного языка;
· дополнение внутреннего представления программы в компиляторе операторами и действиями, неявно предусмотренными семантикой входного языка;
· проверку элементарных семантических (смысловых) норм языков программирования, напрямую не связанных с входным языком.
Проверка соблюдения во входной программе семантических соглашений входного языка заключается в сопоставлении входных цепочек программы с требованиями семантики входного языка программирования. Каждый язык программирования имеет четко заданные и специфицированные семантические соглашения, которые не могут быть проверены на этапе синтаксического разбора. Именно их в первую очередь проверяет семантический анализатор.
Примерами таких соглашений являются следующие требования:
· каждая метка, на которую есть ссылка, должна один раз присутствовать в программе;
· каждый идентификатор должен быть описан один раз, и ни один идентификатор не может быть описан более одного раза (с учетом блочной структуры описаний);
· все операнды в выражениях и операциях должны иметь типы, допустимые для данного выражения или операции;
· типы переменных в выражениях должны быть согласованы между собой;
· при вызове процедур и функций число и типы фактических параметров должны быть согласованы с числом и типами формальных параметров.
Это только примерный перечень такого рода требований. Конкретный состав требований, которые должен проверять семантический анализатор, жестко связан с семантикой входного языка (например, некоторые языки допускают не описывать идентификаторы определенных типов).
Например, если мы возьмем оператор языка Pascal, имеющий вид: а:= b+с; то с точки зрения синтаксического разбора это будет абсолютно правильный оператор. Однако мы не можем сказать, является ли этот оператор правильным с точки зрения входного языка (Pascal), пока не проверим семантические требования для всех входящих в него лексических элементов. Такими элементами здесь являются идентификаторы а, b и с. Не зная, что они собой представляют, мы не можем не только окончательно утверждать правильность приведенного выше оператора, но и понять его смысл. Фактически необходимо знать описание этих идентификаторов.
В том случае, если хотя бы один из них не описан, имеет место явная ошибка. Если это числовые переменные и константы, то мы имеем дело с оператором сложения, если же это строковые переменные и константы – с оператором конкатенации строк. Кроме того, идентификатор а, например, ни в коем случае не может быть константой – иначе нарушена семантика оператора присваивания. Также невозможно, чтобы одни из идентификаторов были числами, а другие – строками, или, скажем, идентификаторами массивов или структур – такое сочетание аргументов для операции сложения недопустимо. И это только некоторая часть соглашений, которые должен проверить компилятор с точки зрения семантики входного языка (в данном примере – Pascal).
Следует еще отметить, что от семантических соглашений зависит не только правильность оператора, но и его смысл. Действительно, операции алгебраического сложения и конкатенации строк имеют различный смысл, хотя и обозначаются в рассмотренном примере одним знаком операции – “+”. Следовательно, от семантического анализатора зависит также и код результирующей программы.
Если какое-либо из семантических требований входного языка не выполняется, то компилятор выдает сообщение об ошибке и процесс компиляции на этом, как правило, прекращается.
Дополнение внутреннего представления программы операторами и действиями, предусмотренными семантикой входного языка, неявно связано с преобразованием типов операндов в выражениях и при передаче параметров в процедуры и функции. Если вернуться к рассмотренному выше элементарному оператору языка Pascal: а:= b+с; то можно отметить, что здесь выполняются две операции: одна операция сложения (или конкатенации, в зависимости от типов операндов) и одна операция присвоения результата. Соответствующим образом должен быть порожден и код результирующей программы.
Однако не все так очевидно просто. Допустим, что где-то перед рассмотренным оператором мы имеем описание его операндов в виде:
var
a: real;
b: integer;
с: double;
Из этого описания следует, что а – вещественная переменная языка Pascal, b – целочисленная переменная, с – вещественная переменная с двойной точностью. Тогда смысл рассмотренного оператора с точки зрения входной программы существенным образом меняется, поскольку в языке Pascal нельзя напрямую выполнять операции над операндами различных типов. Существуют правила преобразования типов, принятые для данного языка.
Эти преобразования может сделать разработчик программы – но тогда преобразования типов в явном виде будут присутствовать в тексте входной программы (в рассмотренном примере это не так). В другом случае это делает код, порождаемый компилятором, когда преобразования типов в явном виде в тексте программы не присутствуют, но неявно предусмотрены семантическими соглашениями языка. Для этого в составе библиотек функций, доступных компилятору, должны быть функции преобразования типов. Вызовы этих функций как раз и будут встроены в текст результирующей программы для удовлетворения семантических соглашений о преобразованиях типов во входном языке, хотя в тексте программы в явном виде они не присутствуют. Чтобы это произошло, эти функции должны быть встроены и во внутреннее представление программы в компиляторе. За это также отвечает семантический анализатор.
С учетом предложенных типов данных в рассмотренном примере будут не две, а четыре операции: преобразование целочисленной переменной b в формат вещественных чисел с двойной точностью; сложение двух вещественных чисел с двойной точностью; преобразование результата в вещественное число с одинарной точностью; присвоение результата переменной с. Количество операций возросло вдвое, причем добавились два вызова весьма нетривиальных функций преобразования типов. Разработчик программы должен помнить об этом, если хочет добиваться высокой эффективности результирующего кода.
Преобразование типов – это только один вариант операций, неявно добавляемых компилятором в код программы на основе семантических соглашений. Другим примером такого рода операций могут служить операции вычисления адреса, когда происходит обращение к элементам сложных структур данных. Существуют и другие варианты такого рода операций (преобразование типов – только самый распространенный пример).
Таким образом, и здесь действия, выполняемые семантическим анализатором, существенным образом влияют на порождаемый компилятором код результирующей программы.
Проверка элементарных смысловых норм языков программирования, напрямую не связанных с входным языком, – это сервисная функция, которую предоставляют большинство современных компиляторов. Эта функция обеспечивает проверку компилятором некоторых соглашений, применимых к большинству современных языков программирования, выполнение которых связано со смыслом как всей входной программы в целом, так и отдельных ее фрагментов.
Примерами таких соглашений являются следующие требования:
· каждая переменная или константа должна хотя бы один раз использоваться в программе;
· каждая переменная должна быть определена до ее первого использования при любом ходе выполнения программы (первому использованию переменной должно всегда предшествовать присвоение ей какого-либо значения);
· результат функции должен быть определен при любом ходе ее выполнения;
· каждый оператор в исходной программе должен иметь возможность хотя бы один раз выполниться;
· операторы условия и выбора должны предусматривать возможность хода выполнения программы по каждой из своих ветвей;
· операторы цикла должны предусматривать возможность завершения цикла.
Конечно, это только примерный перечень основных соглашений. Конкретный состав проверяемых соглашений зависит от семантики языка. Однако, в отличие от семантических требований языка, строго проверяемых семантическим анализатором, выполнение данных соглашений не является обязательным. Поэтому то, какие конкретно соглашения будут проверяться и как они будут обрабатываться, зависит от качества компилятора, от функций, заложенных в него разработчиками. Простейший компилятор вообще может не выполнять этот этап семантического анализа и не проверять ни одного такого соглашения, даже если это и возможно с точки зрения семантики входного языка.
Необязательность соглашений такого типа – еще одна из особенностей при их обработке в семантическом анализаторе: их несоблюдение не может трактоваться как ошибка. Даже если компилятор полностью уверен в своей “правоте”, тот факт, что какое-то из указанных соглашений не соблюдается, не должен приводить к прекращению компиляции входной программы. Обычно факт обнаружения несоблюдения такого рода соглашений трактуется компилятором как “предупреждение” (warning). Компилятор выдает пользователю сообщение об обнаружении несоблюдения одного из требований, не прерывая сам процесс компиляции, т. е. он просто обращает внимание пользователя на то или иное место в исходной программе. То, как реагировать на “предупреждение” (вносить изменения в исходный код или проигнорировать этот факт), – это уже забота и ответственность разработчика программы.
Необязательность указанных соглашений объясняется тем, что ни один компилятор не способен полностью понять и оценить смысл исходной программы. А поскольку смысл программы доступен только человеку, то он и должен нести ответственность за семантические соглашения.