Теория языков 6

advertisement
Теория языков
программирования и
методы трансляции
Тема №6
Семантический анализ
Вопросы
Некоторые характеристики языков программирования не являются
контекстно-свободными, следовательно, их нельзя определить с
помощью контекстно-свободных грамматик. В данной теме
рассматриваются аспекты семантического анализа, в частности,
анализируются не-контекстно-свободные аспекты языков
программирования.
• Характеристики языка, не являющиеся контекстно-свободными.
• Усовершенствование контекстно-свободного синтаксического
анализатора путем введения протабулированных действий для
проверки неконтекстно-свободных характеристик языков
программирования.
• Практические методы реализации таблиц символов и типов.
Семантический анализ.
Не-контекстно-свободные
характеристики языков
Каждая программа данного языка будет иметь, по меньшей мере,
одно синтаксическое дерево (и по меньшей мере, одно левостороннее и
правостороннее порождение), которое может быть использовано для
отображения ее порождения. В то же время не каждое синтаксическое дерево,
которое можно сгенерировать грамматикой языка, соответствует корректной
программе.
main () {
int first, second;
first = 4;
second = 5;
printf(“%d”, first + second)
}
main () {
first = 4;
second = 5;
printf(“%d”, first + second)
}
Семантический анализ.
Не-контекстно-свободные
характеристики языков
При этом первая программа компилируется и выполняется, а
вторая приводит к появлению сообщений об ошибках.
Первая программа корректна в С, а вторая — нет. Другими
словами, появление в программе переменных подразумевает, что в какомто месте программы должны быть их объявления. Следовательно,
существуют ограничения на способ производства порождения. Контекстносвободные грамматики не имеют механизма определения таких
ограничений, следовательно, не могут применяться для точного
определения, что составляет программу на С. В то же время контекстносвободные грамматики можно использовать для определения
расширенного множества всех программ на С, расширенного множества
всех корректных программ, а также расширенного множества всех
программ, некорректных вследствие неконтекстно-свободных сбоев,
подобных отсутствию объявления переменных.
Семантический анализ.
Не-контекстно-свободные
характеристики языков
Проиллюстрируем другую категорию не-контекстно-свободных
дефектов на следующем примере.
main() {
int first;
int second[5] = {3,4,6,1,8};
first = second;
printf(“%d”, first);
}
Очевидно, что проблема этой программы заключается в
несовместимости типов по обе стороны оператора присваивания:
first = second;
В целом, язык С достаточно терпим к так называемым ошибкам
типов. Ни один из следующих операторов присваивания не породит
сообщения об ошибке.
int p = 4.3;
real x = 2;
int x = ‘a’;
int x = NULL;
Семантический анализ.
Не-контекстно-свободные
характеристики языков
В таких зыках как Ada и ALGOL 68, типы трактуются строже,
поэтому неявные преобразования либо вообще отсутствуют, либо их
очень мало. Это связано с тем, что при таком подходе в процессе
компиляции будет обнаружено большинство программных ошибок.
Такие языки называются языками со строгим контролем типов.
Другие языки имеют не статические, а динамические типы, когда тип
величины неизвестен в процессе компиляции и должен определяться
уже во время выполнения программы. Это означает, что во время
выполнения программы также должны осуществляться преобразования
типов, для чего следует сгенерировать код еще в процессе компиляции.
Семантический анализ.
Не-контекстно-свободные
характеристики языков
На следующем примере иллюстрируется еще один тип неконтекстно-свободных дефектов, которые могут возникнуть в программе.
int bigger(int no1, int no2) {
if(nol > no2) return nol;
else return no2;
}
main() {
int first, second;
first = 4;
second = 5;
second = bigger (first);
printf ("%d", second);
}
Функция bigger определена с двумя параметрами, но из main она
вызывается только с одним параметром, что приводит к выдаче следующей
ошибки в процессе компиляции.
Error. Too few parameters in call to ‘bigger’ in function main
Семантический анализ.
Не-контекстно-свободные
характеристики языков
Типы параметров в вызове функции также должны соответствовать
типам в объявлении. В языке С, разумеется, изменение типов между int,
float и char осуществляется автоматически. В то же время несоответствие
между параметром типа int в описании функции и массивом чисел типа int,
используемым в качестве данного параметра функции, будет ошибочным.
Подобного рода ошибки могут возникнуть в индексах массивов, как это
показано на следующем примере.
main() {
int number;
int matrix [3][2] = {{4,5},{8,9}, {11,12}};
number = matrix [1,1,1];
printf (“%d”, number);
}
Здесь к массиву matrix, определенному как двумерный, обращаются
как к трехмерному массиву.
Семантический анализ.
Не-контекстно-свободные
характеристики языков
Несколько иная разновидность контекстно-свободных дефектов
возникает в связи с определенными в языке правилам области видимости.
Проиллюстрируем это на следующем примере.
int p = 7;
void fun1() {
int p = 4;
printf (“локальное р = %d\n”,p); {
int p = 11;
printf(“более локальное р = %d\n”, p);
}
}
void fun2() {
printf(“глобальное р =
}
main() {
fun1();
fun2();
}
%d\n”, p)
Семантический анализ.
Не-контекстно-свободные
характеристики языков
Ошибки времени компиляции, связанные с рассмотренными выше
примерами, не могут выявляться программой синтаксического анализа,
основанной исключительно на контекстно-свободной грамматике. Иначе
говоря, синтаксический анализатор, построенный с помощью YACC, не
сможет обнаружить ни одной из подобных ошибок, обратившись к “пустой”
позиции (соответствующей синтаксической ошибке) в созданной YАСС
таблице LALR(1)-анализа. Таким образом, для обнаружения этих ошибок
потребуются дополнительные проверки. Тот факт, что данные типы ошибок
не приводят к появлению ошибочной записи в таблице синтаксического
анализа, значительно упрощает восстановление после них, поскольку для
продолжения анализа не нужно делать каких-то предположений об ошибках
программиста. Операция, выявляющая ошибки, может просто сообщать о
проблеме и продолжить синтаксический анализ. Обычно программисту
проще произвести подробную диагностику на предмет наличия неконтекстно-свободных ошибок, чем найти контекстно-свободные ошибки.
Семантический анализ.
Не-контекстно-свободные
характеристики языков
Далее рассмотрим природу действий, которые нужно добавить к
программе синтаксического анализа для обнаружения ошибок типов и
области видимости. В принципе, эти типы дефектов легко обнаружить,
поскольку вся необходимая информация считывается анализатором до
места появления ошибки. Если же информация, требуемая для
обнаружения дефекта, находится после места его проявления,
потребуется дополнительный проход или проходы. Впрочем, чтобы
требуемая информация была доступна, следует использовать таблицы с
информацией о типах и области видимости.
Семантический анализ.
Таблицы компилятора.
•
•
•
•
В процессе компиляции анализатору требуются две основные таблицы.
Таблица символов.
Таблица типов.
Кроме того, необходимыми являются следующие таблицы.
Таблица функций.
Таблица меток.
Семантический анализ.
Таблицы символов.
Основная задача таблицы символов – установить соответствие между
переменной и ее типом. С таблицей символов связаны следующие две
основные операции.
• Соответствующая определяющему вхождению переменной, например,
int х. Имя переменной и ее тип помещаются в таблицу символов.
• Соответствующая применимому вхождению переменной, например,
х = 5; Исследуется таблица символов для нахождения типа переменной.
Сложность таблицы символов и процедур работы с таблицей зависят от:
• языка реализации;
• важности эффективной компиляции.
Семантический анализ.
Таблицы символов.
Необходимо отметить, что неверным будет предположение о том,
что только одна переменная в программе может быть представлена
идентификатором х, поскольку, в общем случае, в программе может
находиться произвольное количество переменных с именем х. Таким
образом, для каждого применимого вхождения переменной х определяется
позиция таблицы символов, соответствующая подходящему
определяющему вхождению переменной х.
Семантический анализ.
Таблицы символов.
Форма таблицы символов, требуемой для анализа одной функции
языка С, является простой. Рассмотрим схему функции С.
void scopes()
{int a, b, c /*уровень 0*/
…
{ int a, b /*уровень 1a*/
…
}
{ float c, d /*уровень 1b*/
{ int m /*уровень 2*/
…
}
}
}
Таблицу символов можно представить с помощью растущих вверх
стеков, если поиск в ней определяющего вхождения идентификатора
осуществляется сверху вниз, и позиции удаляются из стека после выхода
переменной из области видимости.
Семантический анализ.
Таблицы символов.
Используя в качестве иллюстрации приведенную выше схему
функции, покажем состояния стека на различных этапах анализа.
Изначально таблица символов пуста:
После обработки первых трех объявлений таблица имеет
следующий вид.
Семантический анализ.
Таблицы символов.
После обработки объявлений уровня 1а таблица имеет такой вид.
Поскольку поиск в таблице символов осуществляется сверху вниз,
будут идентифицированы позиции, соответствующие наиболее недавним
(или наиболее глубоким) определяющим вхождениям a или b. После
прохождения области видимости, соответствующей объявлениям 1а,
позиции, соответствующие этим объявлениям, должны удаляться из
таблицы символов, таким образом, таблица вернется к прежнему виду.
Семантический анализ.
Таблицы символов.
В это же время значение указателя стека уменьшается до значения,
имевшегося перед обработкой объявлений уровня 1a. Чтобы сделать это,
требуется поддерживать массив указателей стека. После обработки
объявлений уровня 1b стек принимает следующий вид.
Семантический анализ.
Таблицы символов.
После обработки объявлений уровня 2 таблица имеет такой вид.
После прохождения области видимости объявлений уровня 2 стек
возвращается к прежнему значению.
Семантический анализ.
Таблицы символов.
После прохождения области видимости объявлений уровня 1b он вновь
имеет следующий вид.
После выхода из функции таблица символов снова становится пустой
Семантический анализ.
Таблицы символов.
Существует несколько моментов, на которые стоит обратить
внимание. Во-первых, для глобально объявленных переменных необходим
нижний (или внешний) уровень стека, существующий во время всего
процесса компиляции. Отметим также, что при рассмотрении операций
таблицы символов внешняя переменная (для которой память выделяется в
другом исходном файле) трактуется так же, как и глобальная.
С точки зрения таблицы символов статическая переменная
трактуется так же, как и автопеременная (подобная рассмотренным в
примере), хотя при распределении памяти работа с этими переменными
достаточно отличается.
На практике таблица символов может иметь более двух полей.
Например, дополнительное поле может использоваться для указания того,
относится ли идентификатор к переменным или к константам. Еще одно
роле можно использовать для хранения констант или адресов переменных
времени компиляции, хотя значение этого поля будет неизвестным до
момента распределения памяти.
Семантический анализ.
Таблицы символов.
Если линейный поиск оказывается неэффективным для нахождения
определяющего вхождения идентификатора, более эффективные алгоритмы
поиска могут дать более сложные структуры данных, такие как бинарные
деревья для различных уровней стека. Методы, рассмотренные в связи с
лексическим и синтаксическим анализом, требуют времени,
пропорционального длине программы, К сожалению, того же нельзя сказать
для не-контекстно-свободного анализа, который часто называют статическим
семантическим анализом. Чем больше программа, тем больше некоторые ее
таблицы и тем больше времени будет занимать поиск в них. Это означает, что
полное время компиляции может быть нелинейной функцией размера
программы и для длинных программ может оказаться несоразмерно большим.
Семантический анализ.
Таблицы символов.
Стековое представление таблицы символов, подходящее для языка С,
будет неадекватным для языков с более сложными правилами обзора,
например, для языка Ada. Рассмотрим следующий фрагмент программы на
языке Ada.
procedure main is
x: integer;
procedure inner is
x:character;
begin x := ‘A’;
put(x);
put(main.x);
end inner;
begin x:= 4;
inner;
put(x);
end main;
Семантический анализ.
Таблицы символов.
Видно, что объявленная в процедуре main целая переменная х не
является полностью скрытой внутри процедуры inner, и доступ к ней
можно Получить посредством обращения main.x. Очевидно, что этот
факт должен отражаться в таблице символов для реализации языка Ada,
возможно, посредством присваивания имен разделам таблицы символов.
Еще одной интересной особенностью Ada, касающейся
структуры таблицы символов, является оператор use, например,
use stack
Здесь stack — имя пакета, содержащего процедуры добавления
и удаления элементов из стека, имена которых оператор use делает
видимыми, не показывая при этом подробностей реализации стека.
Семантический анализ.
Таблицы символов.
Из правил языка Ada следует:
• оператор use может сделать идентификатор непосредственно
видимым, только если тот не является непосредственно видимым
при отсутствии этого оператора;
• идентификатор, ставший непосредственно видимым при
использовании оператора use, должен объявляться в одном и только
одном пакете, указанном в операторе use.
Учитывая эти правила, необходимо вставить корректную
реализацию оператора use в видимую часть сегмента таблицы
символов, указанную в операторе use “вверху” таблицы символов.
После того, как завершится область видимости оператора use, данную
часть таблицы символов можно будет удалить.
Семантический анализ.
Таблицы символов.
Типы соотносятся не только с переменными и константами; каждый
элемент выражения имеет соответствующий ему тип, в том числе это
относится:
• к литералам, таким как 3, 23.4, true;
• к выражениям, таким как 3 + 4.
Типы литералов обычно определяются при лексическом анализе.
Например (предполагается использование языка С), 3, очевидно, имеет тип
int, а 23.4 – float. Типы выражений определяются по типам их компонентов.
Поскольку 23.4 и 34.2 имеют тип float, то выражение
23.4 + 34.2
также будет иметь тип float. Подобным образом выражение
23.4 + 5
будет иметь тип float.
Семантический анализ.
Таблицы символов.
Это можно объяснить двумя способами, в зависимости от
рассматриваемого языка.
• Переменная типа float плюс переменная типа int дает переменную типа
float.
• Сложение определено только для переменных одного типа, поэтому перед
сложением целое 5 должно преобразовываться в тип float.
В большинстве языков программирования имеет место неявное изменение
типов. Реже встречаются языки, подобные Ada, в которых большинство
изменений типов должно быть явным.
В языке С явно заданные изменения называются приведением типов,
примером подобного может служить следующее выражение.
x = float(m)
Здесь значение m (предполагаемое целым) преобразовывается в тип float
перед присвоением его значения переменной х.
Семантический анализ.
Таблицы символов.
В языках со статическими типами, например С, все типы
известны во время компиляции, и это относится к типам
выражений, идентификаторам и литералам. При этом не важно,
насколько сложным является выражение: его тип может
определяться во время компиляции за определенное количество
шагов, исходя из типов его составляющих. Фактически, это
позволяет производить контроль типов во время компиляции и
находить заранее (в процессе компиляции, а не во время
выполнения программы) многие программные ошибки.
Семантический анализ.
Таблицы типов.
В компиляторе должен существовать способ уникального представления
каждого типа конкретной программы. Если исходный язык содержит только
конечное число типов, для представления разрешенных типов можно
использовать различные целые числа. Некоторые ранние языки, такие как
FORTRAN, подобное позволяли, однако, более поздние языки в общем
случае уже нельзя рассматривать так просто. При рассмотрении подходящего
представления типов в программе необходимо принять во внимание
следующие факторы.
• Высокая структурированность и рекурсивная природа многих типов.
• Общие операции, которые компилятор должен будет производить
над, типами.
Семантический анализ.
Таблицы типов.
Общими операциями над типами в С являются следующие.
• Нахождение типа поля элементов struct или union.
• Нахождение типа элемента массива.
• Нахождение типа результата функции.
Основные типы, такие как int, float и char, могут представляться в С
посредством целых чисел, а составные типы, например, array и union,
могут представляться как структуры.
Семантический анализ.
Таблицы типов.
Например, тип typedef
typedef struct {
int day;
int mth;
int year;
} dob;
можно представить с помощью следующей структуры
Семантический анализ.
Таблицы типов.
А тип typedef
typedef long int[9][19] matrix;
можно представить с помощью следующей структуры
Семантический анализ.
Таблицы типов.
Этот способ представления позволяет сравнительно легко
выполнять обычные операции над типами. Остается всего лишь определить
массив (таблицу типов), который отображает имена типов в указатели на
структуры типов. Поскольку используемые в программе имена типов, как и
другие имена, имеют области видимости, в таблице также должна
существовать возможность отображения областей видимости подобно тому,
как это сделано в таблице символов. В простых случаях достаточной
является стековая структура таблицы.
Семантический анализ.
Другие таблицы.
•
•
Число других таблиц, необходимых в процессе компиляции,
определенным образом зависит от компилируемого языка. Обычно
используются следующие таблицы.
Таблица функций.
Таблица меток.
Некоторым образом функция подобна типу, с ней соотнесены подтипы,
т.е. типы параметров и результатов функции. В процессе генерации кода
ей также выделяется адрес, а вся информация об этом хранится в
таблице времени компиляции, обзор которой производится согласно
соответствующим правилам языка.
Семантический анализ.
Другие таблицы.
Управляющие структуры в языках высокого уровня должны быть
представлены в целевом коде посредством переходов (условных или иных) и
меток. Таким образом, целевые версии исходных программ будут содержать
как метки, определенные пользователем, так и метки, определенные
компилятором. Структура многих языков позволяет использовать таблицу
стекового типа для связывания определяющего вхождения метки и
применимого вхождения метки. Из этого следует, что, по-видимому, в
процессе компиляции (и во время выполнения) придется использовать
несколько стеков. Управление стеками, в смысле выделения каждому из них
достаточной памяти, становится сложным, если число стеков больше двух,
так что значительные преимущества дает возможность объединения
множества стеков в единую стековую структуру. К счастью, правила обзора
многих языков такие возможности предоставляют.
Download