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

АДДИТИВНЫЕ ВЫРАЖЕНИЯ


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

Хорошо, что мы уже имеем модель для работы с этими более сложными выражениями. Все, что мы должны сделать, это удостовериться, что все процедуры, вызываемые Expression, (Term, Factor и т.д.) всегда возвращают идентификатор типа. Если мы сделаем это, то структура программы едва ли вообще изменится.

Первый шаг прост: мы должны переименовать нашу существующую версию Expression в Term, как мы делали много раз раньше и создать новую версию Expression:

{---------------------------------------------------------------}

{ Parse and Translate an Expression }

function Expression: char;

var Typ: char;

begin

if IsAddop(Look) then

      Typ := Unop

   else

      Typ := Term;

   while IsAddop(Look) do begin



      Push(Typ);

      case Look of

       '+': Typ := Add(Typ);

       '-': Typ := Subtract(Typ);

      end;

   end;

   Expression := Typ;

end;

{--------------------------------------------------------------}

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

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

Для этой версии, тем не менее, мы сохраним тот же самый примитивный старый код, который делает новую подпрограмму тривиальной:


{---------------------------------------------------------------}

{ Process a Term with Leading Unary Operator }

function Unop: char;

begin

   Clear;

   Unop := 'W';

end;

{---------------------------------------------------------------}

Процедура Push - это подпрограмма генерации кода, которая теперь имеет параметр, указывающий тип:

{---------------------------------------------------------------}

{ Push Primary onto Stack }

procedure Push(Size: char);

begin

   Move(Size, 'D0', '-(SP)');

end;

{---------------------------------------------------------------}

Теперь давайте взглянем на функции Add и Subtract. В более старых версиях этих подпрограмм мы позволяем им вызывать подпрограммы генерации кода PopAdd и PopSub. Мы продолжим делать это, что делает сами функции чрезвычайно простыми:

{---------------------------------------------------------------}

{ Recognize and Translate an Add }

function Add(T1: char): char;

begin

   Match('+');

   Add := PopAdd(T1, Term);

end;

{-------------------------------------------------------------}

{ Recognize and Translate a Subtract }

function Subtract(T1: char): char;

begin

   Match('-');

   Subtract := PopSub(T1, Term);

end;

{---------------------------------------------------------------}

Но простота обманчива, поскольку мы переложили всю логику на PopAdd и PopSub, которые больше не являются просто подпрограммами генерации кода. Они также должны теперь заботиться о необходимых преобразованиях типов.

Какие это преобразования? Простые: оба аргумента должны иметь тот же самый размер  и результат также такой размер. Меньший из двух параметров должен быть "приведен" до размера большего.

Но это представляет небольшую проблему. Если переводимый параметр - второй (т.е. в основном регистре D0) мы в отличной форме. Если же нет, мы в затруднении: мы не можем изменить размер данных, которые уже затолкнуты в стек.

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



Альтернативой является назначение вторичного регистра, в качестве которого я выбрал R7. (Почему не R1? Потому, что для других регистров у меня есть планы на будущее.)

Первый шаг в этой новой структуре - представить процедуру Pop, аналогичную Push. Эта процедура будет всегда выталкивать верхний элемент стека в D7:

{---------------------------------------------------------------}

{ Pop Stack into Secondary Register }

procedure Pop(Size: char);

begin

   Move(Size, '(SP)+', 'D7');

end;

{---------------------------------------------------------------}

Общая идея состоит в том, что все "Pop-Op" подпрограммы могут вызывать ее. Когда это сделано, мы будем иметь оба операнда в регистрах, поэтому мы можем перевести любой нужный нам. Для работы процедуре Convert необходим другой аргумент, имя регистра:

{---------------------------------------------------------------}

{ Convert a Data Item from One Type to Another }

procedure Convert(Source, Dest: char; Reg: String);

begin

   if Source <> Dest then begin

      if Source  = 'B' then

         EmitLn('AND.W #$FF,' + Reg);

      if Dest = 'L' then

         EmitLn('EXT.L ' + Reg);

   end;

end;

{---------------------------------------------------------------}

Следующая функция выполняет преобразование, но только если текущий тип T1 меньше по размеру, чем желаемый тип T2. Это функция, возвращающая конечный тип, позволяющий нам знать, что она решила:

{---------------------------------------------------------------}

{ Promote the Size of a Register Value }

function Promote(T1, T2: char; Reg: string): char;

var Typ: char;

begin

   Typ := T1;

   if T1 <> T2 then

      if (T1 = 'B') or ((T1 = 'W') and (T2 = 'L')) then begin

         Convert(T1, T2, Reg);



         Typ := T2;

      end;

   Promote := Typ;

end;

{---------------------------------------------------------------}

Наконец, следующая функция приводит два регистра к одному типу:

{---------------------------------------------------------------}

{ Force both Arguments to Same Type }

function SameType(T1, T2: char): char;

begin

   T1 := Promote(T1, T2, 'D7');

   SameType := Promote(T2, T1, 'D0');

end;

{---------------------------------------------------------------}

Эти новые подпрограммы дают нам заряд, необходимы нам чтобы разложить PopAdd и PopSub:

{---------------------------------------------------------------}

{ Generate Code to Add Primary to the Stack }

function PopAdd(T1, T2: char): char;

begin

   Pop(T1);

   T2 := SameType(T1, T2);

   GenAdd(T2);

   PopAdd := T2;

end;

{---------------------------------------------------------------}

{ Generate Code to Subtract Primary from the Stack }

function PopSub(T1, T2: char): char;

begin

   Pop(T1);

   T2 := SameType(T1, T2);

   GenSub(T2);

   PopSub := T2;

end;

{---------------------------------------------------------------}

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

Обратите внимание на две новые подпрограммы генерации кода GenAdd и GenSub. Они являются остаточной формой оригинальных PopAdd и PopSub. Т.е. они являются чистыми генераторами кода, производящими сложение и вычитание регистров:

{---------------------------------------------------------------}

{ Add Top of Stack to Primary }

procedure GenAdd(Size: char);

begin

   EmitLn('ADD.' + Size + ' D7,D0');

end;



{---------------------------------------------------------------}

{ Subtract Primary from Top of Stack }

procedure GenSub(Size: char);

begin

   EmitLn('SUB.' + Size + ' D7,D0');

   EmitLn('NEG.' + Size + ' D0');

end;

{---------------------------------------------------------------}

ОК, я соглашусь с вами: я выдал вам множество подпрограмм с тех пор, как мы в последний раз протестировали код. Но вы должны признать, что каждая новая подпрограмма довольно проста и ясна. Если вам (как и мне) не нравится тестировать так много новых подпрограмм одновременно все в порядке. Вы можете заглушить подпрограммы типа Convert, Promote и SameType так как они не считывают входной поток. Вы не получите корректный код, конечно, но программа должна работать. Затем постепенно расширяйте их.

При тестировании программы не забудьте, что  вы сначала должны объявить некоторые переменные а затем начать "тело" программы с "B" в верхнем регистре (для BEGIN). Вы должны обнаружить, что синтаксический анализатор обрабатывает любые аддитивные выражения. Как только все подпрограммы преобразования будет введены, вы должны увидеть, что генерируется правильный код и код для преобразования типов вставляется в нужных местах. Попробуйте смешивать переменные различных размеров а также литералы. Удостоверьтесь, что все работает правильно. Как обычно, хорошо было бы попробовать некоторые ошибочные выражения и посмотреть, как компилятор обрабатывает их.


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