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

 

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

· проверку соблюдения во входной программе семантических соглашений вход­ного языка;

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Дополнение внутреннего представления программы операторами и действиями, предусмотренными семантикой входного языка, неявно связано с преобразова­нием типов операндов в выражениях и при передаче параметров в процедуры и функции. Если вернуться к рассмотренному выше элементарному оператору языка Pascal: а:= b+с; то можно отметить, что здесь выполняются две операции: одна операция сложе­ния (или конкатенации, в зависимости от типов операндов) и одна операция присвоения результата. Соответствующим образом должен быть порожден и код результирующей программы.

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

 

var

a: real;

b: integer;

с: double;

 

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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