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

2.8 Семантический анализ

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

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

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

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

Таблицы компилятора

В процессе компиляции анализатору требуются таблицы:

– таблица символов;

– таблица типов;

– таблица функций;

– таблица меток.

Основная задача таблицы символов – установление соответствия между переменной и ее типом. Операции, связанные с таблицей символов:

– операция, соответствующая определяющему вхождению переменной. В таблицу символов помещаются имя переменной и ее тип;

– операция, соответствующая применимому вхождению переменной. Исследуется таблица символов для нахождения типа переменной.

Сложность таблицы символов и процедур работы с таблицей зависят от языка реализации и важности эффективной компиляции.

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

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

Изначально таблица символов пуста:

После обработки первых трех объявлений таблица имеет вид

После обработки объявлений уровня 1а таблица примет вид

Поскольку поиск в таблице символов осуществляется сверху вниз, будут идентифицированы позиции, соответствующие последним определяющим вхождениям а или b. После прохождения области видимости, соответствующей объявлениям 1а, позиции, соответствующие этим объявлениям, должны удаляться из таблицы символов. Таким образом, таблица вернется к прежнему виду

В это же время значение указателя стека уменьшается до значения, имевшегося перед обработкой объявлений уровня 1а. Чтобы сделать это, требуется поддерживать массив указателей стека. После обработки объявлений уровня 1b стек принимает вид

После обработки объявлений уровня 2 таблица имеет следующий вид:

После прохождения области видимости объявлений уровня 2 стек возвращается к прежнему значению

После прохождения области видимости объявлений уровня 1b он вновь примет следующий вид:

После выхода из функции таблица символов снова становится пустой:

Для глобально объявленных переменных необходим нижний или внешний уровень стека, существующий во время всего процесса компиляции. При рассмотрении операций таблицы символов внешняя переменная, для которой память выделяется в другом исходном файле, трактуется так же, как и глобальная.

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

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

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

Стековое представление таблицы символов, подходящее для языка С, будет неадекватным для языков с более сложными правилами обзора (язык Ада).

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

– высокая структурированность и рекурсивная природа многих типов;

– общие операции, которые компилятор должен будет производить над типами.

Общие операции над типами в языке С:

– нахождение типа поля элементов struct или union;

– нахождение типа элемента массива;

– нахождение типа результата функции.

Основные типы (int, float и char) могут представляться в языке С посредством целых чисел, а составные типы (array и union) могут представляться как структуры. Этот способ представления позволяет выполнять обычные операции над типами. Остается всего лишь определить массив (таблицу типов), который отображает имена типов в указатели на структуры типов. Поскольку используемые в программе имена типов имеют области видимости, в таблице должна существовать возможность отображения областей видимости подобно тому, как это сделано в таблице символов. В простых случаях достаточной является стековая структура таблицы.

Число таблиц, необходимых в процессе компиляции, зависит от компилируемого языка. В процессе генерации кода функции выделяется адрес, а вся информация об этом хранится в таблице времени компиляции, обзор которой производится согласно соответствующим правилам языка.

Управляющие структуры в языках высокого уровня должны быть представлены в целевом коде посредством переходов и меток. Целевые версии исходных программ будут содержать как метки, определенные пользователем, так и метки, определенные компилятором. Структура многих языков позволяет использовать таблицу стекового типа для связывания определяющего вхождения метки и применимого вхождения метки. В процессе компиляции и во время выполнения придется использовать несколько стеков. Управление стеками, в смысле выделения каждому из них достаточной памяти, становится сложным, если число стеков больше двух, поэтому значительные преимущества дает объединение множества стеков в единую стековую структуру.