355 500 произведений, 25 200 авторов.

Электронная библиотека книг » Джек Креншоу » Давайте создадим компилятор! » Текст книги (страница 18)
Давайте создадим компилятор!
  • Текст добавлен: 21 сентября 2016, 15:19

Текст книги "Давайте создадим компилятор!"


Автор книги: Джек Креншоу



сообщить о нарушении

Текущая страница: 18 (всего у книги 24 страниц)

Что неправильно?

Тут вы могли бы подумать: Уверен, здесь должно быть что-то большее чем несколько сохранений и восстановлений из стека. Для передачи параметров здесь должно быть что-то большее чем тут есть.

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

Самая явная оплошность в том, что он неправильный! Если вы оглянетесь на код для вызова процедур, вы увидите, что вызывающая подпрограмма помещает каждый фактический параметр в стек перед тем, как она вызывает процедуру. Процедура использует эту информацию, но она не изменяет указатель стека. Это означает, что содержимое все еще остается там когда мы возвращаемся. Кто-то должен очистить стек или мы скоро окажемся в очень трудной ситуации!

К счастью, это легко исправить. Все, что мы должны сделать – это увеличить указатель стека когда мы закончим.

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

Я предпочитаю разрешить очистку в вызывающей программе, так что вызываемая процедура должна только выполнить возврат. Также это кажется немного более сбалансированным так как именно вызывающая программа первой «засорила» стек. Но это означает, что вызывающая программа должна запоминать сколько элементов помещено в стек. Чтобы сделать проще, я изменил процедуру 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.)

Это все что требуется. Попробуйте и посмотрите как вам это нравится.

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

Все еще остается только одна небольшая проблема:

У нас нет способа возвратить результат в вызывающую программу!

Но это, конечно, не ограничение генерируемого нами кода, а ограничение, свойственное протоколу передачи по значению. Обратите внимание, что мы можем использовать формальные параметры любым способом внутри процедуры. Мы можем вычислять для них новое значение, использовать их как счетчики циклов (если бы мы имели циклы!) и т.д. Так что код делает то, что предполагается. Чтобы решить эту последнюю проблему мы должны рассмотреть альтернативный протокол.

Передача по ссылке

Это просто теперь, когда мы уже имеем механизм. Мы только должны внести несколько изменений в генерацию кода. Вместо помещения значения в стек, мы должны помещать адрес. Оказывается, 68000 имеет инструкцию PEA которая как раз делает это.

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

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

FOO(X, Y)

оттранслировать в:

PEA X(PC) ; Сохранить адрес X

PEA Y(PC) ; Сохранить адрес Y

BSR FOO ; Вызвать FOO

Это просто вопрос небольших изменений в Param:

{–}

{ Process an Actual Parameter }

procedure Param;

begin

EmitLn('PEA ' + GetName + '(PC)');

end;

{–}

(Обратите внимание, что при передачей по ссылке мы не можем использовать выражения в списке параметров, поэтому Param может просто непосредственно считывать имя).

На другой стороне, ссылки на формальные параметры должны получить один уровень косвенности:

FOO: LINK A6,#0

MOVE.L 12(A6),A0 ; Извлечь адрес A

MOVE (A0),D0 ; Извлечь A

MOVE D0,-(SP) ; Сохранить

MOVE.L 8(A6),A0 ; Извлечь адрес B

MOVE (A0),D0 ; Извлечь B

ADD (SP)+,D0 ; Добавить A

MOVE.L 12(A6),A0 ; Извлечь адрес A

MOVE D0,(A0) : Сохранить A

UNLK A6

RTS

Все это может быть обработано с изменениями в LoadParam and StoreParam:

{–}

{ Load a Parameter to the Primary Register }

procedure LoadParam(N: integer);

var Offset: integer;

begin

Offset := 8 + 4 * (NumParams – N);

Emit('MOVE.L ');

WriteLn(Offset, '(A6),A0');

EmitLn('MOVE (A0),D0');

end;

{–}

{ Store a Parameter from the Primary Register }

procedure StoreParam(N: integer);

var Offset: integer;

begin

Offset := 8 + 4 * (NumParams – N);

Emit('MOVE.L ');

WriteLn(Offset, '(A6),A0');

EmitLn('MOVE D0,(A0)');

end;

{–}

Для правильного расчета, мы также должны изменить одну строку в ParamList:

ParamList := 4 * N;

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

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

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

Это, фактически, будет одним из различий между TINY и KISS. В следующей версии TINY мы будем использовать передачу по ссылке для всех параметров. KISS будет поддерживать оба метода.

Локальные переменные

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

И снова здесь мы стоим перед выбором: статическое или динамическое хранение?

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

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

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

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

Альтернативой является динамическое хранение, при котором память распределяется в стеке точно также как и для переданных параметров. Для это мы уже имеем готовый механизм. Фактически, те же самые подпрограммы, которые работают с переданными (по значению) параметрами в стеке, могут так же легко работать и с локальными переменными... генерируемый код тот же самый. Назначение смещения в инструкции 68000 LINK сейчас такое: мы можем использовать его для регулировки указателя стека при выделении места для локальных переменных. Динамическое хранение, конечно, по существу поддерживает рекурсию.

Когда я впервые начал планировать TINY, я должен признаться имел предубеждение в пользу статического хранения. Просто потому, что старые FORTRAN программы были чрезвычайно эффективны... ранние компиляторы FORTRAN производили качественный код, который и сейчас редко сопоставим с современными компиляторами. Даже сегодня данная программа, написанная на FORTRAN вероятно превзойдет ту же самую программу написанную на C или Pascal, иногда с большим отрывом. (Вот так! Что вы скажете на это заявление!)

Я всегда полагал, что причина имела отношение к двум основным различиям между реализациями Фортрана и другими языками: статическое хранение и передача по ссылке. Я знаю, что динамическое хранение поддерживает рекурсию, но мне всегда казалось немного странным желание мириться с более медленным кодом, который в 95% случаев не нуждается в рекурсии, только для того чтобы получить эту возможность когда она понадобится. Идея состоит в том, что со статическим хранением вы можете использовать не косвенную а абсолютную адресацию, которая должна привести к более быстрому коду.

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

MOVE 8(A6),D0

имеет тоже самое время выполнения, что и

MOVE X(PC),D0.

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

Так как такое использование локальных переменных так хорошо соответствует схеме передачи параметров по значению, мы будем использовать эту версию транслятора для иллюстрации (я надеюсь вы сохранили копию!).

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

Давайте начнем с создания новой переменной Base:

var Base: integer;

Мы будем использовать эту переменную вместо NumParams для вычисления смещения стека. Это подразумевает изменение двух ссылок на NumParams в LoadParam и StoreParam:

{–}

{ Load a Parameter to the Primary Register }

procedure LoadParam(N: integer);

var Offset: integer;

begin

Offset := 8 + 2 * (Base – 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 * (Base – N);

Emit('MOVE D0,');

WriteLn(Offset, '(A6)');

end;

{–}

Идея состоит в том, что значение Base будет заморожено после того, как мы обработаем формальные параметры и не будет увеличиваться дальше когда новые, локальные, переменные будут вставлены в таблицу идентификаторов. Об этом позаботится код в конце FormalList:

{–}

{ Process the Formal Parameter List of a Procedure }

procedure FormalList;

begin

Match('(');

if Look <> ')' then begin

FormalParam;

while Look = ',' do begin

Match(',');

FormalParam;

end;

end;

Match(')');

Fin;

Base := NumParams;

NumParams := NumParams + 4;

end;

{–}

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

Все что мы должны сделать дальше – это установить семантику объявления локальных переменных в синтаксическом анализаторе. Подпрограммы очень похожи на Decl и TopDecls:

{–}

{ Parse and Translate a Local Data Declaration }

procedure LocDecl;

var Name: char;

begin

Match('v');

AddParam(GetName);

Fin;

end;

{–}

{ Parse and Translate Local Declarations }

function LocDecls: integer;

var n: integer;

begin

n := 0;

while Look = 'v' do begin

LocDecl;

inc(n);

end;

LocDecls := n;

end;

{–}

Заметьте, что LocDecls является функцией, возвращающей число локальных переменных в DoProc.

Затем мы изменим DoProc для использования этой информации:

{–}

{ Parse and Translate a Procedure Declaration }

procedure DoProc;

var N: char;

k: integer;

begin

Match('p');

N := GetName;

if InTable(N) then Duplicate(N);

ST[N] := 'p';

FormalList;

k := LocDecls;

ProcProlog(N, k);

BeginBlock;

ProcEpilog;

ClearParams;

end;

{–}

(Я сделал пару изменений, которые не были в действительности необходимы. Кроме небольшой реорганизации я переместил вызов Fin в FormalList а также в LocDecls. Не забудьте поместить его в конец FormalList.)

Обратите внимание на изменения при вызове ProcProlog. Новый параметр – это число слов (не байт) для распределения памяти. Вот новая версия ProcProlog:

{–}

{ Write the Prolog for a Procedure }

procedure ProcProlog(N: char; k: integer);

begin

PostLabel(N);

Emit('LINK A6,#');

WriteLn(-2 * k)

end;

{–}

Сейчас должно работать. Добавьте эти изменения и посмотрите как они работают.

Заключение

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

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

Это будет следующая глава, которая появится ближе к Форуму.

Увидимся.

Типы

Введение

В последней главе (Часть 13, Процедуры) я упомянул, что в ней и в следующей главе мы рассмотрим две возможности, которые помогут отделить игрушечный язык от настоящего, пригодного к использованию. В ней мы рассмотрели вызовы процедур. Многие из вас терпеливо ждали, начиная с Августа'89 когда я выдам вторую. Хорошо, вот она.

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

Несколько предупреждений: Во-первых, есть некоторые типы, которые я не буду охватывать в этой главе. Здесь мы будем говорить только о простых, встроенных типах. Мы даже не будем работать с массивами, указателями или строками, я охвачу их в следующих нескольких главах.

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

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

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


    Ваша оценка произведения:

Популярные книги за неделю