Давайте создадим компилятор! - Джек Креншоу
Шрифт:
Интервал:
Закладка:
{–}
{ Add a New Parameter to Table }
procedure AddParam(Name: char);
begin
if IsParam(Name) then Duplicate(Name);
Inc(NumParams);
Params[Name] := NumParams;
end;
{–}
Наконец, нам понадобятся некоторые подпрограммы генерации кода:
{–}
{ Load a Parameter to the Primary Register }
procedure LoadParam(N: integer);
var Offset: integer;
begin
Offset := 4 + 2 * (NumParams – N);
Emit('MOVE ');
WriteLn(Offset, '(SP),D0');
end;
{–}
{ Store a Parameter from the Primary Register }
procedure StoreParam(N: integer);
var Offset: integer;
begin
Offset := 4 + 2 * (NumParams – N);
Emit('MOVE D0,');
WriteLn(Offset, '(SP)');
end;
{–}
{ Push The Primary Register to the Stack }
procedure Push;
begin
EmitLn('MOVE D0,-(SP)');
end;
{–}
(Последнюю подпрограмму мы уже видели прежде, но ее не было в этой остаточной версии программы.)
После этих приготовлений мы готовы работать с семантикой процедур со списками вызовов (помните, что код для работы с синтаксисом уже на месте).
Давайте начнем с обработки формальных параметров. Все что мы должны сделать – добавить каждый параметр в таблицу идентификаторов параметров:
{–}
{ Process a Formal Parameter }
procedure FormalParam;
begin
AddParam(GetName);
end;
{–}
Теперь, что делать с формальными параметрами, когда они появляются в теле процедуры? Это требует немного больше работы. Мы должны сначала определить, что это формальный параметр. Чтобы сделать это, я написал модифицированную версию TypeOf:
{–}
{ Get Type of Symbol }
function TypeOf(n: char): char;
begin
if IsParam(n) then
TypeOf := 'f'
else
TypeOf := ST[n];
end;
{–}
(Обратите внимание, что так как TypeOf теперь вызывает IsParam, возможно будет необходимо изменить ее местоположение в программе.)
Мы также должны изменить AssignOrProc для работы с этим новым типом:
{–}
{ Decide if a Statement is an Assignment or Procedure Call }
procedure AssignOrProc;
var Name: char;
begin
Name := GetName;
case TypeOf(Name) of
' ': Undefined(Name);
'v', 'f': Assignment(Name);
'p': CallProc(Name);
else Abort('Identifier ' + Name + ' Cannot Be Used Here');
end;
end;
{–}
Наконец, код для обработки операции присваивания и выражения должен быть расширен:
{–}
{ Parse and Translate an Expression }
{ Vestigial Version }
procedure Expression;
var Name: char;
begin
Name := GetName;
if IsParam(Name) then
LoadParam(ParamNumber(Name))
else
LoadVar(Name);
end;
{–}
{ Parse and Translate an Assignment Statement }
procedure Assignment(Name: char);
begin
Match('=');
Expression;
if IsParam(Name) then
StoreParam(ParamNumber(Name))
else
StoreVar(Name);
end;
{–}
Как вы можете видеть, эти процедуры обработают каждое встретившееся имя переменной или как формальный параметр или как глобальную переменную, в зависимости от того, появляется ли оно в таблице идентификаторов параметров. Запомните, что мы используем только остаточную форму Expression. В конечной программе изменения, показанные здесь, должны быть добавлены в Factor а не Expression.
Осталось самое простое. Мы должны только добавить семантику в фактический вызов процедуры, что мы можем сделать с помощъю одной новой строки кода:
{–}
{ Process an Actual Parameter }
procedure Param;
begin
Expression;
Push;
end;
{–}
Так вот. Добавьте эти изменения в вашу программу и испытайте ее. Попробуйте объявить одну или две процедуры, каждая со списком формальных параметров. Затем сделайте какие-нибудь присваивания, используя комбинации глобальных и формальных параметров. Вы можете вызывать одну процедуру из другой, но вы не можете объявлять вложенные процедуры. Вы можете даже передавать формальные параметры из одной процедуры в другую. Если бы мы имели здесь полный синтаксис языка, вы могли бы также читать и выводить формальные параметры или использовать их в сложных выражениях.
Что неправильно?
Тут вы могли бы подумать: Уверен, здесь должно быть что-то большее чем несколько сохранений и восстановлений из стека. Для передачи параметров здесь должно быть что-то большее чем тут есть.
Вы были бы правы. Фактически, код, который мы здесь генерируем, оставляет желать лучшего в нескольких случаях.
Самая явная оплошность в том, что он неправильный! Если вы оглянетесь на код для вызова процедур, вы увидите, что вызывающая подпрограмма помещает каждый фактический параметр в стек перед тем, как она вызывает процедуру. Процедура использует эту информацию, но она не изменяет указатель стека. Это означает, что содержимое все еще остается там когда мы возвращаемся. Кто-то должен очистить стек или мы скоро окажемся в очень трудной ситуации!
К счастью, это легко исправить. Все, что мы должны сделать – это увеличить указатель стека когда мы закончим.
Должны ли мы делать это в вызывающей программе или в вызываемой процедуре? Некоторые люди позволяют вызываемой процедуре очищать стек, так как требуется генерировать меньше кода на вызов и так как процедура, в конце концов, знает сколько параметров она получила. Но это означает, что она должна что-то делать с адресом возврата чтобы не потерять его.
Я предпочитаю разрешить очистку в вызывающей программе, так что вызываемая процедура должна только выполнить возврат. Также это кажется немного более сбалансированным так как именно вызывающая программа первой «засорила» стек. Но это означает, что вызывающая программа должна запоминать сколько элементов помещено в стек. Чтобы сделать проще, я изменил процедуру 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 }