Для этой версии, тем не менее, мы сохраним тот же самый примитивный старый код, который делает новую подпрограмму тривиальной:
   {–}
   { 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. Довольно просто включить вызов обработчика ошибок если типы двух операндов несовместимы.
   В общем случае мы можем рассматривать каждый одиночный оператор как обрабатываемый отдельной процедурой, в зависимости от типа двух операндов. Это просто, хотя и утомительно, реализовать просто создав таблицу переходов с типами операндов как индексами. В Паскале эквивалентная операция включала бы вложенные операторы Case. Некторые из вызываемых процедур могли бы тогда быть простыми подпрограммами обработки ошибок, в то время как другие могли бы выполнять любые виды преобразований, необходимые нам. При добавлении нами типов, число процедур будет возрастать в геометрической прогрессии, но это все равно не неприемлемо большое число процедур.
   Сдесь же мы свернули такую таблицу переходов в гораздо меньшее количество процедур, просто используя симметрию и другие упрощающие правила.

Приводить или не приводить

   В случае, если до вас еще не дошло, уверен дойдет, что TINY и KISS возможно не будут строго типизированными языками, так как я разрешил автоматическое смешивание и преобразование почти любых типов. Что поднимает следующий вопрос:
   Это действительно то, что мы хотим сделать?
   Ответ зависит от того, какого рода язык вам нужен и как вы хотели чтобы он себя вел. Мы не обращались к проблеме того, когда разрешить или когда запретить использование операций, включающих различные типы данных. Другими словами, какова должна быть семантика нашего компилятора? Хотим ли мы выполнять автоматическое преобразование типов для всех случаев, в некоторых случаях или не выполнять совсем?
   Давайте приостановимся здесь, чтобы подумать об этом немного больше. В этом нам поможет небольшой исторический обзор.
   Fortran II поддерживал только два простых типа данных: Integer и Real. Он разрешал неявное преобразование типов между real и integer типами во время присваивания, но не в выражениях. Все элементы данных (включая литеральные константы) справа оператора присваивания должны были быть одинакового типа. Это довольно сильно облегчало дела... гораздо проще, чем то, что мы делали здесь.
   Это было изменено в Fortran IV для поддержки «смешанной» арифметики. Если выражение имело любые real элементы, все они преобразовывались в real и само выражение было real. Для полноты, предоставлялись функции для явного преобразования из одного типа в другой, чтобы вы могли привести выражение в любой тип.
   Это вело к двум вещам: код, который был проще для написания и код, который был менее эффективен. Из-за это неаккуратные программисты должны были писать выражения с простыми константами типа 0 и 1, которые компилятор должен был покорно компилировать для преобразования во время выполнения. Однако, система работала довольно хорошо, что показывало что неявное преобразование типов – Хорошая Вещъ.
   Си – также слабо типизированный язык, хотя он поддерживает большее количество типов. C не будет жаловаться, если вы например попытаетесь прибавить символ к целому числу. Частично, в этом помогает соглашение Си о переводе каждого символа в число когда оно загружается или передается в списке параметров. Это совсем немного упрощает преобразование. Фактически, в подмножестве компиляторов Си, которые не поддерживают длинные или с плавающей точкой числа мы возвращаемся к нашей первой нехитрой попытке: каждая переменная получает одинаковое представление как только загружается в регистр. Жизнь становится значительно проще!
   Предельным языком в направлении автоматического преобразования типов является PL/I. Этот язык поддерживает большое количество типов данных, и вы можете свободно смешивать их все. Если неявное преобразование Fortran казалось хорошим, то таковое в PL/I было бы Небесами, но оно скорее оказалось Адом! Проблема состояла в том, что с таким большим количеством типов данных должно сущестовать большое количество различных преобразований и, соответственно, большое количество правил того, как смешиваемые операнды должны преобразовываться. Эти правила стали настолько сложными, что никто не мог запомнить какие они! Множество ошибок в программах на PL/I имели отношение к непредвиденным и нежелательным преобразованиям типов. Слишком хорошо тоже нехорошо!
   Паскаль, с другой стороны, является языком, который «строго типизирован», что означает, что вы вообще не можете смешивать типы даже если они отличаются только именем, хотя они и имеют тот же самый базовый тип! Никлаус Вирт сделал Паскаль строго типизированным чтобы помочь программисту избежать проблем и эти ограничения действительно защитили многих программистов от самих себя, потому что компилятор предохранял его от глупых ошибок. Лучше находить ошибки при компиляции, чем на этапе отладки. Те же самые ограничения могут также вызвать расстройства когда вам действительно нужно смешивать типы и они заставляют бывших C-программистов лезть на стену.
   Даже в этом случае, Паскаль разрешает некоторые неявные преобразования. Вы можете присвоить целое значение вещественному. Вы можете также смешивать целые и вещественные типы в выражениях типа Real. Целые числа будут автоматически приведены к вещественным, как и в Fortran. (и с теми же самыми скрытыми накладными расходами во время выполнения).
   Вы не можете, однако, преобразовывать наоборот из вещественного в целое без применения явной функции преобразования Trunc. Теория здесь в том, что так как числовое значение вещественного числа обязательно будет изменено при преобразованиии (дробная часть будет потеряна), это не должно быть сделано в «секрете» от вас.
   В духе строгого контроля типов Паскаль не позволит вам смешивать Char и Integer переменные без применения явных функций приведения Chr и Ord.
   Turbo Pascal также включает типы Byte, Word и LongInt. Первые два в основном то же самое, что и беззнаковое целое. В Turbo они могут быть свободно смешаны с переменными типа Integer и Turbo автоматически выполнит преобразование. Однако существуют проверки времени выполнения, предохряняющие вас от переполнения или иного способа получения неправильного ответа. Заметьте, что вы все еще не можете смешивать типы Byte и Char, даже при том, что они имеют то же самое внутреннее представление.
   Пределом среди строго типизированных языков является Ada, который не рязрешает никаких невных преобразований типов вообще, и также не разрешает смешанную арифметику. Позиция Jean Ichbiah в том, что преобразования стоят времени выполнения и вам нельзя позволить платить такую цену на скрытый манер. Вынуждая программиста явно запрашивать преобразование типов вы делаете более очевидным то, что здесь могут быть вовлечены затраты.
   Я использовал другой язык со строгим контролем типов, небольшой восхитительный язык, названный Whimsical, от Джона Спрея. Хотя Whimsical предназначен быть языком системного программирования, он также требует каждый раз явного преобразования. В нем никогда не выполняются никакие автоматические преобразования, даже те, которые поддерживаются в Паскале.
   Такой подход имеет некоторые преимущества: компилятор никогда не должен предполагать, что делать: программист всегда говорит ему точно, что он хочет. В результате, появляется почти однозначное соответствие между исходным кодом и компилированным кодом, и компилятор Джона производил очень компактный код.
   С другой стороны, я иногда находил явные преобразования болезненными. Если я хочу, например, прибавить единицу к символу, или сложить ее с маской, необходимо выполнить массу преобразований. Если я сделаю это неправильно единственным сообщением об ошибке будет «Типы не совместимы». Как это случается, специфическая реализация Джоном этого языка в его компиляторе не сообщает вам точно, какие типы несовместимы... он только сообщает вам, в какой строке произошла ошибка.
   Я должен признать, что большинство моих ошибок с этим компилятором были ошибками такого типа, и я потратил много времени с компилятором Whimsical, пытаясь всего-лишь выяснить в каком месте строки я ее допустил. Единственный реальный способ исправить ошибку это продолжать исправления до тех пор, пока что-нибудь не заработает.
   Так что мы должны сделать в TINY и KISS? Для первого я имею ответ: TINY будет поддерживать только типы Char и Integer и мы будем использовать прием C непосредственно переводя Char в Integer. Это означает, что компилятор TINY будет значительно проще, чем то, что мы уже сделали. Необходимость преобразования типов в выражении спорна, так как ни одно из них не требуется! Так как длинное слово не будет поддерживаться, нам также будут не нужны подпрограммы MUL32 и DIV32, ни логики для выяснения когда их вызывать. Мне это нравится!
   KISS, с другой стороны будет поддерживать тип Long.
   Должен ли он поддерживать и знаковую и беззнаковую арифметику? Ради простоты я предпочел бы нет. Это добавляет совсем немного сложности в преобразования типов. Даже Никлаус Вирт удалили беззнаковые числа (Cardinal) из его нового языка Оберон, с тем аргументом, что 32-разрядного целого числа в любом случае должно быть достаточно всем.