Лекции по построению компилятора на Pascal

       

ЧТО НЕПРАВИЛЬНО?


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

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

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

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

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

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

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

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

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

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

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


Содержание раздела