Современная электронная библиотека ModernLib.Net

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

ModernLib.Net / Программирование / Креншоу Джек / Давайте создадим компилятор! - Чтение (стр. 19)
Автор: Креншоу Джек
Жанр: Программирование

 

 


      Да, действительно. Все, что нам нужно сделать – выполнить преобразование с другого конца... т.е. мы выполняем преобразование на выходе, когда данные сохраняются, а не на входе.
      Но запомните, часть присваивания, отвечающая за хранение, в значительной степени независима от загрузки данных, о которой заботится процедура Expression. Вообще, выражение может быть произвольно сложным, поэтому как может процедура Assignment знать, какой тип данных оставлен в регистре D0?
      Снова, ответ прост: Мы просто спросим об этом процедуру Expression! Ответ может быть возвращен как значение функции.
      Все это требует изменения некоторых процедур, но эти изменения, как и сам метод, совсем простые. Прежде всего, так как мы не требуем чтобы LoadVar выполнял всю работу по преобразованию, давайте возвратимся к простой версии:
      {–}
      { Load a Variable to Primary Register }
      procedure LoadVar(Name, Typ: char);
      begin
      Move(Typ, Name + '(PC)', 'D0');
      end;
      {–}
      Затем, давайте добавим новую процедуру, которая будет выполнять преобразование из одного типа в другой:
      {–}
      { Convert a Data Item from One Type to Another }
      procedure Convert(Source, Dest: char);
      begin
      if Source <> Dest then begin
      if Source = 'B' then
      EmitLn('AND.W #$FF,D0');
      if Dest = 'L' then
      EmitLn('EXT.L D0');
      end;
      end;
      {–}
      Затем, мы должны реализовать логику, требуемую для загрузки и сохранения переменной любого типа. Вот подпрограммы для этого:
      {–}
      { Load a Variable to the Primary Register }
      function Load(Name: char): char;
      var Typ : char;
      begin
      Typ := VarType(Name);
      LoadVar(Name, Typ);
      Load := Typ;
      end;
      {–}
      { Store a Variable from the Primary Register }
      procedure Store(Name, T1: char);
      var T2: char;
      begin
      T2 := VarType(Name);
      Convert(T1, T2);
      StoreVar(Name, T2);
      end;
      {–}
      Обратите внимание, что Load является функцией, которая не только выдает код для загрузки, но также возвращает тип переменной. Таким образом, мы всегда знаем, с каким типом данных мы работаем. Когда мы выполняем Store, мы передаем ей текущий тип переменной в D0. Так как Store также знает тип переменной назначения, она может выполнить преобразование необходимым образом.
      Вооруженная всеми этими новыми подпрограммами, реализация нашего элементарного присваивания по существу тривиальна. Процедура Expression теперь становится функцией возвращающей тип выражения в процедуру Assignment:
      {–}
      { Parse and Translate an Expression }
      function Expression: char;
      begin
      Expression := Load(GetName);
      end;
      {–}
      { Parse and Translate an Assignment Statement }
      procedure Assignment;
      var Name: char;
      begin
      Name := GetName;
      Match('=');
      Store(Name, Expression);
      end;
      {–}
      Снова, заметьте как невероятно просты эти две подпрограммы. Мы изолировали всю логику типа в Load и Store и хитрость с передачей типа делает остальную работу чрезвычайно простой. Конечно, все это для нашего специального, тривиального случая с Expression. Естественно, для общего случая это будет более сложно. Но теперь вы смотрите на финальную версию процедуры Assignment!
      Все это выглядит как очень простое и ясное решение, и действительно это так. Откомпилируйте эту программу и выполните те же самые тесты, что и ранее. Вы увидите, что все типы данных преобразованы правильно и здесь немного, если вообще есть, зря потраченных инструкций. Только преобразование «байт-длинное слово» использует две инструкции когда можно было бы использовать одну, и мы могли бы легко изменить Convert для обработки этого случая.
      Хотя мы в этом случае не рассматривали переменные без знака, я думаю вы можете видеть, что мы могли бы легко исправить процедуру Convert для работы и с этими типами. Это «оставлено как упражнение для студента».

Литеральные аргументы

      Зоркие читатели могли бы отметить, однако, что мы еще даже не имеем правильной формы простого показателя, потому что мы не разрешаем загрузку литеральных констант, только переменных. Давайте исправим это сейчас.
      Для начала нам понадобится функция GetNum. Мы уже видели ее несколько версий, некоторые возвращают только одиночный символ, некоторые строку, а некоторые целое число. Та, которая нам здесь нужна будет возвращать длинное целое, так что она может обрабатывать все, что мы ей подбросим. Обратите внимание, что здесь не возвращается никакой информации о типах: GetNum не интересуется тем, как будет использоваться число:
      {–}
      { Get a Number }
      function GetNum: LongInt;
      var Val: LongInt;
      begin
      if not IsDigit(Look) then Expected('Integer');
      Val := 0;
      while IsDigit(Look) do begin
      Val := 10 * Val + Ord(Look) – Ord('0');
      GetChar;
      end;
      GetNum := Val;
      SkipWhite;
      end;
      {–}
      Теперь, когда работаем с литералами, мы имеем одну небольшую проблему. С переменными мы знаем какого типа они должны быть потому что они были объявлены с таким типом. Мы не имеем такой информации о типе для литералов. Когда программист говорит «-1», означает ли это байт, слово или длинное слово? Мы не имеем никаких сведений. Очевидным способом было бы использование наибольшего возможного типа, т.е. длинного слова. Но это плохая идея, потому что когда мы примемся за более сложные выражения, мы обнаружим, что это заставит каждое выражение включающее литералы, также переводить в длинное.
      Лучшим подходом было бы выбрать тип, основанный на значении литерала, как показано далее:
      {–}
      { Load a Constant to the Primary Register }
      function LoadNum(N: LongInt): char;
      var Typ : char;
      begin
      if abs(N) <= 127 then
      Typ := 'B'
      else if abs(N) <= 32767 then
      Typ := 'W'
      else Typ := 'L';
      LoadConst(N, Typ);
      LoadNum := Typ;
      end;
      {–}
      (Я знаю, знаю, база числа не является в действительности симметричной. Вы можете хранить -128 в одиночном байте и -32768 в слове. Но это легко исправить и не стоит затраченного времени или дополнительной сложности возиться с этим сейчас. Стоящая мысль.)
      Заметьте, что LoadNum вызывает новую версию подпрограммы генерации кода LoadConst, которая имеет дополнительный параметр для определения типа:
      {–}
      { Load a Constant to the Primary Register }
      procedure LoadConst(N: LongInt; Typ: char);
      var temp:string;
      begin
      Str(N, temp);
      Move(Typ, '#' + temp, 'D0');
      end;
      {–}
      Теперь мы можем изменить процедуру Expression для использования двух возможных видов показателей:
      {–}
      { Parse and Translate an Expression }
      function Expression: char;
      begin
      if IsAlpha(Look) then
      Expression := Load(GetName)
      else
      Expression := LoadNum(GetNum);
      end;
      {–}
      (Вау, это, уверен, не причинило слишком большого вреда! Всего несколько дополнительных строк делают всю работу.)
      ОК, соберите этот код в вашу программу и испытайте ее. Вы увидите, что она теперь работает и для переменных и для констант как допустимых выражений.

Аддитивные выражения

      Если вы следовали за этой серией с самого начала, я уверен вы знаете, что будет дальше. Мы расширим форму выражения для поддержки сначала аддитивных выражений, затем мультипликативных, а затем общих выражений со скобками.
      Хорошо, что мы уже имеем модель для работы с этими более сложными выражениями. Все, что мы должны сделать, это удостовериться, что все процедуры, вызываемые 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). Вы должны обнаружить, что синтаксический анализатор обрабатывает любые аддитивные выражения. Как только все подпрограммы преобразования будет введены, вы должны увидеть, что генерируется правильный код и код для преобразования типов вставляется в нужных местах. Попробуйте смешивать переменные различных размеров а также литералы. Удостоверьтесь, что все работает правильно. Как обычно, хорошо было бы попробовать некоторые ошибочные выражения и посмотреть, как компилятор обрабатывает их.

Почему так много процедур?

      К этому моменту вы можете подумать, что я зашел слишком далеко в смысле глубоко вложенных процедур. В этом несомненно есть большие накладные расходы. Но в моем безумии есть смысл. Как в случае с UnOp, я заглядываю вперед на время, когда мы захотим генерировать лучший код. С таким способом организации кода мы можем достичь этого без значительных изменений в программе Например, в случаях, где значение помещенное в стек не должно преобразовываться, все же лучше использовать инструкцию «вытолкнуть и сложить». Если мы решим проверять такие случаи, мы можем включить дополнительные тесты в PopAdd и PopSub не изменяя что-либо еще.

Мультипликативные выражения

      Процедуры для работы с мультипликативными операторами почти такие же. Фактически, на первом уровне они почти идентичны, так что я просто покажу их здесь без особых фанфар. Первая – наша общая форма для Factor, которая включает подвыражения в скобках:
      {–}
      { Parse and Translate a Factor }
      function Expression: char; Forward;
      function Factor: char;
      begin
      if Look = '(' then begin
      Match('(');
      Factor := Expression;
      Match(')');
      end
      else if IsAlpha(Look) then
      Factor := Load(GetName)
      else
      Factor := LoadNum(GetNum);
      end;
      {–}
      { Recognize and Translate a Multiply }
      Function Multiply(T1: char): char;
      begin
      Match('*');
      Multiply := PopMul(T1, Factor);
      end;
      {–}
      { Recognize and Translate a Divide }
      function Divide(T1: char): char;
      begin
      Match('/');
      DIvide := PopDiv(T1, Factor);
      end;
      {–}
      { Parse and Translate a Math Term }
      function Term: char;
      var Typ: char;
      begin
      Typ := Factor;
      while IsMulop(Look) do begin
      Push(Typ);
      case Look of
      '*': Typ := Multiply(Typ);
      '/': Typ := Divide(Typ);
      end;
      end;
      Term := Typ;
      end;
      {–}
      Эти подпрограммы соответствуют аддитивным почти полностью. Как и прежде, сложность изолирована в PopMul и PopDiv. Если вам захочется протестировать программу прежде чем мы займемся ими, вы можете написать их пустые версии, аналогичные PopAdd и PopSub. И снова, код не будет корректным в данный момент, но синтаксический анализатор должен обрабатывать выражения произвольной сложности.

Умножение

      Если вы убедились, что сам синтаксический анализатор работает правильно, мы должны выяснить, что необходимо сделать для генерации правильного кода. С этого места дела становятся немного труднее так как правила более сложные.
      Давайте сперва возьмем случай умножения. Эта операция аналогична «addops» в том, что оба операнда должны быть одного и того же размера. Она отличается в трех важных отношениях:
      Тип произведения обычно не такой же как тип двух операндов. Для произведения двух слов мы получаем в результате длинное слово.
      68000 не поддерживает умножение 32 x 32, так что необходим вызов подпрограммы для программного умножения.
      Он также не поддерживает умножение 8 x 8, поэтому байтовые операнды должны быть переведены до слова.
      Действия, которые мы должны выполнить, лучше всего показывает следующая таблица:
      T1
      T2 B W L
      B Преобразовать D0 в W
      Преобразовать D7 в W
      MULS
      Result = W Преобразовать D0 в W
      MULS
      Result = L Преобразовать D0 в L
      JSR MUL32
      Result = L
      W Преобразовать D7 в W
      MULS
      Result = L MULS
      Result = L Преобразовать D0 в L
      JSR MUL32
      Result = L
      L Преобразовать D7 в L
      JSR MUL32
      Result = L Преобразовать D7 в L
      JSR MUL32
      Result = L JSR MUL32
      Result = L
      Эта таблица показывает действия, предпринимаемые для каждой комбинации типов операндов. Есть три вещи, на которые необходимо обратить внимание: во-первых, мы предполагаем, что существует библиотечная подпрограмма MUL32, которая выполняет 32 x 32 умножение, оставляя 32-битное (не 64) произведение. Если в процессе этого происходит переполнение мы игнорируем его и возвращаем только младшие 32 бита.
      Во-вторых, заметьте, что таблица симметрична. Наконец, обратите внимание, что произведение это всегда длинное слово, за исключением случая когда оба операнда байты. (Стоит заметить, между прочим, что это означает что результатом многих выражений будет длинное слово, нравится нам это или нет. Возможно идея перевода всех их заранее не была уж такой возмутительной, в конце концов!)
      Теперь ясно, что мы должны будем генерировать различный код для 16-разрядного и 32-разрядного умножения. Для этого лучше всего иметь отдельные подпрограммы генерации кода для этих двух случаев:
      {–}
      { Multiply Top of Stack by Primary (Word) }
      procedure GenMult;
      begin
      EmitLn('MULS D7,D0')
      end;
      {–}
      { Multiply Top of Stack by Primary (Long) }
      procedure GenLongMult;
      begin
      EmitLn('JSR MUL32');
      end;
      {–}
      Исследование кода ниже для PopMul должно убедить вас, что условия в таблице выполнены:
      {–}
      { Generate Code to Multiply Primary by Stack }
      function PopMul(T1, T2: char): char;
      var T: char;
      begin
      Pop(T1);
      T := SameType(T1, T2);
      Convert(T, 'W', 'D7');
      Convert(T, 'W', 'D0');
      if T = 'L' then
      GenLongMult
      else
      GenMult;
      if T = 'B' then
      PopMul := 'W'
      else
      PopMul:= 'L';
      end;
      {–}
      Как вы можете видеть, подпрограмма начинается совсем как PopAdd. Два аргумента приводятся к тому же самому типу. Два вызова Convert заботятся о случаях, когда оба операнда – байты. Сами данные переводятся до слова, но подпрограмма помнит тип чтобы назначать корректный тип результату. В заключение мы вызываем одну из двух подпрограмм генерации кода и затем назначаем тип результата. Не слишком сложно, действительно.
      Я полагаю, что сейчас вы уже тестируете программу. Попробуйте все комбинации размеров операндов.

Деление

      Случай с делением совсем не так симметричен. У меня также есть для вас некоторые плохие новости:
      Все современные 16-разрядные процессоры поддерживают целочисленное деление. Спецификации изготовителей описывают эту операцию как 32 x 16 бит деление, означающее, что вы можете разделить 32-разрядное делимое на 16-разрядный делитель. Вот плохая новость:
      Они вам лгут!!!
      Если вы не верите в это, попробуйте разделить любое большое 32-разрядное число (это означает, что оно имеет ненулевые биты в старших 16 разрядах) на целое число 1. Вы гарантированно получите исключение переполнения.
      Проблема состоит в том, что эта команда в действительности требует, чтобы получаемое частное вписывалось в 16-разрядный результат. Этого не случится, если делитель достаточно большой. Когда любое число делится на единицу, частное будет конечно тем же самым, что и делимое.
      С начала времен (ну во всяком случае компьютерных) архитекторы ЦПУ предусматривали этот маленький подводный камень в схеме деления. Это обеспечивает некоторую симметрию, так как это своего рода инверсия способа каким работает умножение. Но так как единица – это совершенно допустимое (и довольно частое) число для использования в качестве делителя, делению, реализованному аппаратно, требуется некоторая помощь от программистов.
      Подразумевает следующее:
      Тип частного всегда должен быть того же самого типа, что и делимое. Он независим от делителя.
      Несмотря на то, что ЦПУ поддерживает деление длинного слова, аппаратно предоставленной инструкции можно доверить только делимые байт и слово. Для делимых типа длинное слово нам необходима другая библиотечная подпрограмма, которая может возвращать длинный результат.
      Это похоже на работу для другой таблицы, для суммирования требуемых действий:
      T1
      T2 B W L
      B Преобразовать D0 в W
      Преобразовать D7 в L
      DIVS
      Result = B Преобразовать D0 в W
      Преобразовать D7 в L
      DIVS
      Result = W Преобразовать D0 в L
      JSR DIV32
      Result = L
      W Преобразовать D7 в L
      DIVS
      Result = B Преобразовать D7 в L
      DIVS
      Result = W Преобразовать D0 в L
      JSR DIV32
      Result = L
      L Преобразовать D7 в L
      JSR DIV32
      Result = B Преобразовать D7 в L
      JSR DIV32
      Result = W JSR DIV32
      Result = L
      (Вы можете задаться вопросом, почему необходимо выполнять 32-разрядное деление, когда делимое, скажем, всего лишь байт. Так как число битов в результате может быть только столько, сколько и в делимом, зачем беспокоиться? Причина в том, что если делитель – длинное слово и в нем есть какие-либо установленные старшие разряды, результат деления должен быть равен нулю. Мы не смогли бы получить его, если мы используем только младшее слово делителя)
      Следующий код предоставляет корректную функцию для PopDiv:
      {–}
      { Generate Code to Divide Stack by the Primary }
      function PopDiv(T1, T2: char): char;
      begin
      Pop(T1);
      Convert(T1, 'L', 'D7');
      if (T1 = 'L') or (T2 = 'L') then begin
      Convert(T2, 'L', 'D0');
      GenLongDiv;
      PopDiv := 'L';
      end
      else begin
      Convert(T2, 'W', 'D0');
      GenDiv;
      PopDiv := T1;
      end;
      end;
      {–}
      Две подпрограммы генерации кода:
      {–}
      { Divide Top of Stack by Primary (Word) }
      procedure GenDiv;
      begin
      EmitLn('DIVS D0,D7');
      Move('W', 'D7', 'D0');
      end;
      {–}
      { Divide Top of Stack by Primary (Long) }
      procedure GenLongDiv;
      begin
      EmitLn('JSR DIV32');
      end;
      {–}
      Обратите внимание, мы предполагаем, что DIV32 оставляет результат (длинное слово) в D0.
      ОК, установите новые процедуры деления. Сейчас у вас должна быть возможность генерировать код для любого вида арифметических выражений. Погоняйте ее!

Завершение

      Наконец-то, в этой главе мы узнали как работать с переменными (и литералами) различных типов. Как вы можете видеть, это не было слишком сложно. Фактически, в каком-то отношении большая часть кода выглядит даже еще проще, чем это было в более ранних программах. Только операторы умножения и деления требуют небольших размышлений и планирования.
      Основная идея, которая облегчила нам жизнь, – идея преобразования процедур типа Expression в функции, возвращающие тип результата. Как только это было сделано, мы смогли сохранить ту же самую общую структуру компилятора.
      Я не буду притворяться, что мы охватили каждый одиночный аспект этой проблемы. Я удобно проигнорировал беззнаковую арифметику. Из того, что мы сделали, я думаю вы можете видеть, что их включение не добавляет никаких дополнительных проблем, просто дополнительные проверки.
      Я так же игнорировал логические операторы And, Or и т.д. Оказывается, их довольно легко обрабатывать. Все логические операторы – побитовые операции, так что они симметричны и, следовательно, работают в том же самом режиме, что и PopAdd. Однако, имеется одно отличие: если необходимо расширить длину слова для логической переменной, расширение должно быть сделано как число без знака. Числа с плавающей точкой, снова, являются простыми для обработки... просто еще несколько процедур, которые будут добавлены в run-time библиотеку или, возможно, инструкции для математического сопроцессора.
      Возможно более важно, что я также отделил проблему контроля соответствия типов, в противоположность преобразованию. Другими словами, мы разрешили операции между переменными всех комбинаций типов. Вообще, это не будет верным... конечно вы не захотите прибавить целое число, например, к строке. Большинство языков также не позволят вам смешивать символьные и целочисленные переменные.
      Снова, в действительности в этом случае нет никаких новых проблем для рассмотрения. Мы уже проверяем типы двух операндов... в основном эти проверки выполняются в процедурах типа SameType. Довольно просто включить вызов обработчика ошибок если типы двух операндов несовместимы.

  • Страницы:
    1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23