- •Лекции по построению компилятора на Pascal Автор неизвестен Оглавление
- •1. Введение введение
- •2. Синтаксический анализ выражений начало
- •Одиночные цифры
- •Выражения с двумя цифрами
- •Общая форма выражения
- •Использование стека
- •Умножение и деление
- •Круглые скобки
- •Унарный минус
- •Слово об оптимизации
- •3. Снова выражения введение
- •Переменные
- •Функции
- •Подробнее об обработке ошибок
- •Присваивание
- •Многосимвольные токены.
- •Пробелы
- •4. Интерпретаторы введение
- •Интерпретатор
- •Немного философии
- •5. Управляющие конструкции введение
- •Немного основ
- •Оператор if
- •Оператор while
- •Оператор loop
- •Цикл for
- •Оператор do
- •Оператор break
- •Заключение
- •6. Булевы выражения введение
- •Грамматика
- •Операторы отношений
- •Исправление грамматики
- •Синтаксический анализатор
- •Объединение с управляющими конструкциями
- •Добавление присваиваний
- •7. Лексический анализ введение
- •Лексический анализ
- •Конечные автоматы и альтернативы
- •Эксперименты по сканированию
- •Конечные автоматы
- •Новые строки
- •Операторы
- •Списки, запятые и командные строки.
- •Становится интересней
- •Возвращение символа
- •Распределенные сканеры против централизованных
- •Объединение сканера и парсера
- •Пара комментариев:
- •Заключение
- •8. Немного философии введение
- •Дорога домой
- •Почему это так просто?
- •Здесь нет ничего сложного!
- •Заключение
- •9. Вид сверху введение
- •Верхний уровень
- •Структура паскаля
- •Расширение
- •Объявления
- •Структура си
- •10. Представление "tiny" введение
- •Подготовка
- •Объявления
- •Объявления и идентификаторы
- •Инициализаторы
- •Выполнимые утверждения
- •Булева логика
- •Управляющие структуры
- •Лексический анализ
- •Многосимвольные имена переменных
- •Снова операторы отношений
- •Ввод/вывод
- •Заключение
- •11. Пересмотр лексического анализа введение
- •Предпосылка
- •Проблема
- •Решение
- •Исправление компилятора
- •Заключение
- •12. Разное введение
- •Точки с запятой
- •Синтаксический сахар
- •Работа с точками с запятой
- •Компромисс
- •Комментарии
- •Односимвольные разделители
- •Многосимвольные разделители
- •Односторонние комментарии
- •Заключение
- •13. Процедуры введение
- •Последнее отклонение
- •Основа для экспериментов
- •Объявление процедуры
- •Вызов процедуры
- •Передача параметров
- •Семантика параметров
- •Передача по значению
- •Что неправильно?
- •Передача по ссылке
- •Локальные переменные
- •Заключение
- •14. Типы введение
- •Что будет дальше?
- •Добавление записей
- •Распределение памяти
- •Объявление типов
- •Присваивания
- •Трусливый выход
- •Более приемлемое решение
- •Литеральные аргументы
- •Аддитивные выражения
- •Почему так много процедур?
- •Мультипликативные выражения
- •Умножение
- •Деление
- •Завершение
- •Приводить или не приводить
- •Заключение
- •15. Назад в будущее введение
- •Новое начало, старое направление
- •Начинаем заново?
- •Модуль input
- •Модуль output
- •Модуль error
- •Лексический и синтаксический анализ
- •Модуль scanner
- •Решения, решения
- •Синтаксический анализ
- •16. Конструирование модулей введение
- •Совсем как классический?
- •Расширение синтаксического анализатора
- •Термы и выражения
- •Присваивания
- •Булева алгебра
Что неправильно?
Тут вы могли бы подумать: Уверен, здесь должно быть что-то большее чем несколько сохранений и восстановлений из стека. Для передачи параметров здесь должно быть что-то большее чем тут есть.
Вы были бы правы. Фактически, код, который мы здесь генерируем, оставляет желать лучшего в нескольких случаях.
Самая явная оплошность в том, что он неправильный! Если вы оглянетесь на код для вызова процедур, вы увидите, что вызывающая подпрограмма помещает каждый фактический параметр в стек перед тем, как она вызывает процедуру. Процедура использует эту информацию, но она не изменяет указатель стека. Это означает, что содержимое все еще остается там когда мы возвращаемся. Кто-то должен очистить стек или мы скоро окажемся в очень трудной ситуации!
К счастью, это легко исправить. Все, что мы должны сделать - это увеличить указатель стека когда мы закончим.
Должны ли мы делать это в вызывающей программе или в вызываемой процедуре? Некоторые люди позволяют вызываемой процедуре очищать стек, так как требуется генерировать меньше кода на вызов и так как процедура, в конце концов, знает сколько параметров она получила. Но это означает, что она должна что-то делать с адресом возврата чтобы не потерять его.
Я предпочитаю разрешить очистку в вызывающей программе, так что вызываемая процедура должна только выполнить возврат. Также это кажется немного более сбалансированным так как именно вызывающая программа первой "засорила" стек. Но это означает, что вызывающая программа должна запоминать сколько элементов помещено в стек. Чтобы сделать проще, я изменил процедуру ParamList на функцию, возвращающую количество помещенных байт:
{--------------------------------------------------------------}
{ Process the Parameter List for a Procedure Call }
function ParamList: integer; var N: integer; begin N := 0; Match('('); if Look <> ')' then begin Param; inc(N); while Look = ',' do begin Match(','); Param; inc(N); end; end; Match(')'); ParamList := 2 * N; end;
{--------------------------------------------------------------}
Процедура CallProc затем использует его для очистки стека:
{--------------------------------------------------------------}
{ Process a Procedure Call }
procedure CallProc(Name: char); var N: integer; begin N := ParamList; Call(Name); CleanStack(N); end;
{--------------------------------------------------------------}
Здесь я создал еще одну подпрограмму генерации кода:
{--------------------------------------------------------------}
{ Adjust the Stack Pointer Upwards by N Bytes }
procedure CleanStack(N: integer); begin if N > 0 then begin Emit('ADD #'); WriteLn(N, ',SP'); end; end;
{--------------------------------------------------------------}
ОК, если вы добавили этот код в ваш компилятор, я думаю вы убедитесь, что стек теперь под контролем.
Следующая проблема имеет отношение к нашему способу адресации относительно указателя стека. Это работает отлично на наших простых примерах, так как с нашей элементарной формой выражений никто больше не засоряет стек. Но рассмотрим другой пример, такой простой как:
PROCEDURE FOO(A, B)
BEGIN A = A + B
END
Код, сгенерированный нехитрым синтаксическим анализатором, мог бы быть:
FOO: MOVE 6(SP),D0 ; Извлечь A
MOVE D0,-(SP) ; Сохранить его MOVE 4(SP),D0 ; Извлечь B ADD (SP)+,D0 ; Добавить A MOVE D0,6(SP) : Сохранить A
RTS
Это было бы неправильно. Когда мы помещаем первый аргумент в стек, смещения для двух формальных параметров больше не 4 и 6, я 6 и 8. Поэтому вторая выборка вернула бы снова A а не B.
Но это не конец света. Я думаю, вы можете видеть, что все, что мы должны делать - изменять смещение каждый раз, когда мы помещаем в стек и что фактически и делается если ЦПУ не имеет поддержки других методов.
К счастью, все же, 68000 имеет такую поддержку. Поняв, что этот ЦПУ мог бы использоваться со многими компиляторами языков высокого уровня, Motorola решила добавить прямую поддержку таких вещей.
Проблема, как вы можете видеть в том, что когда процедура выполняется, указатель стека скачет вверх и вниз, и поэтому использование его как ссылки для доступа к формальным параметрам становится неудобным. Решение состоит в том, чтобы вместо него определить и использовать какой-то другой регистр. Этот регистр обычно устанавливается равным подлинному указателю стека и называется указателем кадра.
Команда LINK из набора инструкций 68000 позволяет вам объявить такой указатель кадра и установить его равным указателю стека и все это в одной команде. Фактически, она делает даже больше чем это. Так как этот регистр может использоваться для чего-то еще в вызывающей процедуре, LINK также помещает текущее значение регистра в стек. Вы можете также добавить значение к указателю стека чтобы создать место для локальных переменных.
В дополнение к LINK есть UNLK, которая просто восстанавливает указатель стека и выталкивает старое значение обратно в регистр.
С использованием этих двух команд код для предыдущего примера станет:
FOO: LINK A6,#0
MOVE 10(A6),D0 ; Извлечь A MOVE D0,-(SP) ; Сохранить его MOVE 8(A6),D0 ; Извлечь B ADD (SP)+,D0 ; Добавить A MOVE D0,10(A6) : Сохранить A UNLK A6
RTS
Исправить компилятор для генерации этого кода намного проще чем объяснить. Все, что нам нужно сделать - изменить генерацию кода в DoProc. Так как из-за этого код становится немного больше одной строки, я создал новые процедуры, схожие с процедурами Prolog и Epilog, вызываемыми DoMain:
{--------------------------------------------------------------}
{ Write the Prolog for a Procedure }
procedure ProcProlog(N: char); begin PostLabel(N); EmitLn('LINK A6,#0'); end;
{--------------------------------------------------------------} { Write the Epilog for a Procedure }
procedure ProcEpilog; begin EmitLn('UNLK A6'); EmitLn('RTS'); end;
{--------------------------------------------------------------}
Процедура DoProc теперь просто вызывает их:
{--------------------------------------------------------------}
{ Parse and Translate a Procedure Declaration }
procedure DoProc; var N: char; begin Match('p'); N := GetName; FormalList; Fin; if InTable(N) then Duplicate(N); ST[N] := 'p'; ProcProlog(N); BeginBlock; ProcEpilog; ClearParams; end;
{--------------------------------------------------------------}
В заключение, мы должны изменить ссылки на SP в процедурах LoadParam и StoreParam:
{--------------------------------------------------------------}
{ Load a Parameter to the Primary Register }
procedure LoadParam(N: integer); var Offset: integer; begin Offset := 8 + 2 * (NumParams - N); Emit('MOVE '); WriteLn(Offset, '(A6),D0'); end;
{--------------------------------------------------------------} { Store a Parameter from the Primary Register }
procedure StoreParam(N: integer); var Offset: integer; begin Offset := 8 + 2 * (NumParams - N); Emit('MOVE D0,'); WriteLn(Offset, '(A6)'); end;
{--------------------------------------------------------------}
(Заметьте, что вычисление Offset изменяется чтобы учесть дополнительное сохранение A6.)
Это все что требуется. Попробуйте и посмотрите как вам это нравится.
К этому моменту мы генерируем некоторый относительно хороший код для процедур и вызовов процедур. С ограничениями, что нет никаких локальных переменных (пока) и не разрешено вложение процедур этот код именно то что нам нужно.
Все еще остается только одна небольшая проблема:
У нас нет способа возвратить результат в вызывающую программу!
Но это, конечно, не ограничение генерируемого нами кода, а ограничение, свойственное протоколу передачи по значению. Обратите внимание, что мы можем использовать формальные параметры любым способом внутри процедуры. Мы можем вычислять для них новое значение, использовать их как счетчики циклов (если бы мы имели циклы!) и т.д. Так что код делает то, что предполагается. Чтобы решить эту последнюю проблему мы должны рассмотреть альтернативный протокол.