Конечно, в действительности мы не анализировали правильный синтаксис для объявления данных, так как он включает список переменных. Наша версия разрешает только одну переменную. Это также легко исправить.
   БНФ для <var-list> следующая:
   <var-list> ::= <ident> (, <ident>)*
   Добавление этого синтаксиса в Decl дает новую версию:
   {–}
   { Parse and Translate a Data Declaration }
   procedure Decl;
   var Name: char;
   begin
   Match('v');
   Alloc(GetName);
   while Look = ',' do begin
   GetChar;
   Alloc(GetName);
   end;
   end;
   {–}
   ОК, теперь откомпилируйте этот код и испытайте его. Попробуйте ряд строк с объявлениями VAR, попробуйте список из нескольких переменных в одной строке и комбинации этих двух. Работает?

Инициализаторы

   Пока мы работали с объявлениями данных, меня беспокоила одна вещь – то, что Pascal не позволяет инициализировать данные в объявлении. Эта возможность по общему признанию является своего рода излишеством, и ее может не быть в языке, который считается минимальным языком. Но ее также настолько просто добавить, что было бы позором не сделать этого. БНФ становится:
   <var-list> ::= <var> ( <var> )*
   <var> ::= <ident> [ = <integer> ]
   Измените Alloc как показано ниже:
   {–}
   { Allocate Storage for a Variable }
   procedure Alloc(N: char);
   begin
   Write(N, ':', TAB, 'DC ');
   if Look = '=' then begin
   Match('=');
   WriteLn(GetNum);
   end
   else
   WriteLn('0');
   end;
   {–}
   Вот оно: инициализатор в шесть дополнительных строк Pascal.
   Испытайте эту версию TINY и проверьте, что вы действительно можете задавать начальное значение перменных.
   Ей богу, он начинает походить на настоящий компилятор! Конечно, он все еще ничего не делает, но выглядит хорошо, не так ли?
   Перед тем как оставить этот раздел я должен подчеркнуть, что мы использовали две версии GetNum. Одна, более ранняя, возвращала символьное значение, одиночную цифру. Другая принимала многозначное целое число и возвращала целочисленное значение. Любая из них будет работать здесь, так как WriteLn поддерживает оба типа. Но нет никакой причины ограничивать себя одноразрядными значениями, так что правильной версией для использования будет та, которая возвращает целое число. Вот она:
   {–}
   { Get a Number }
   function GetNum: integer;
   var Val: integer;
   begin
   Val := 0;
   if not IsDigit(Look) then Expected('Integer');
   while IsDigit(Look) do begin
   Val := 10 * Val + Ord(Look) – Ord('0');
   GetChar;
   end;
   GetNum := Val;
   end;
   {–}
   Строго говоря, мы должны разрешить выражения в поле данных инициализатора, или, по крайней мере, отрицательные значения. Сейчас давайте просто разрешим отрицательные значения изменив код для Alloc следующим образом:
   {–}
   { Allocate Storage for a Variable }
   procedure Alloc(N: char);
   begin
   if InTable(N) then Abort('Duplicate Variable Name ' + N);
   ST[N] := 'v';
   Write(N, ':', TAB, 'DC ');
   if Look = '=' then begin
   Match('=');
   If Look = '-' then begin
   Write(Look);
   Match('-');
   end;
   WriteLn(GetNum);
   end
   else
   WriteLn('0');
   end;
   {–}
   Теперь у вас есть возможность инициализировать переменные отрицательными и/или многозначными значениями.

Таблица идентификаторов

   Существует одна проблема с компилятором в его текущем состоянии: он ничего не делает для сохранения переменной когда мы ее объявляем. Так что компилятор совершенно спокойно распределит память для нескольких переменных с тем же самым именем. Вы можете легко убедиться в этом набрав строку типа
   pvavavabe.
   Здесь мы объявили переменную A три раза. Как вы можете видеть, компилятор бодро принимает это и генерирует три идентичных метки. Не хорошо.
   Позднее, когда мы начнем ссылаться на переменные, компилятор также будет позволять нам ссылаться на переменные, которые не существуют. Ассемблер отловит обе эти ошибки, но это совсем не кажется дружественным поведением – передавать такую ошибку ассемблеру. Компилятор должен отлавливать такие вещи на уровне исходного языка.
   Так что даже притом, что нам не нужна таблица идентификаторов для записи типов данных, мы должны установить ее только для того, чтобы проверять эти два условия. Так как пока мы все еще ограничены односимвольными именами переменных таблица идентификаторов может быть тривиальной. Чтобы предусмотреть ее сначала добавьте следующее объявление в начало вашей программы:
   var ST: array['A'..'Z'] of char;
   и вставьте следующую функцию:
   {–}
   { Look for Symbol in Table }
   function InTable(n: char): Boolean;
   begin
   InTable := ST[n] <> ' ';
   end;
   {–}
   Нам также необходимо инициализировать таблицу пробелами. Следующие строки в Init сделают эту работу:
   var i: char;
   begin
   for i := 'A' to 'Z' do
   ST[i] := ' ';
   ...
   Наконец, вставьте следующие две строки в начало Alloc:
   if InTable(N) then Abort('Duplicate Variable Name ' + N);
   ST[N] := 'v';
   Это должно все решить. Теперь компилятор будет отлавливать двойные объявления. Позднее мы также сможем использовать InTable при генерации ссылок на переменные.

Выполнимые утверждения

   К этому времени мы можем генерировать пустую программу, которая имеет несколько объявленных переменных и возможно инициализированных. Но пока мы не генерировали ни строки выполнимого кода.
   Верите ли вы или нет, но мы почти имеем пригодный для использования компилятор! Отсутствует только выполнимый код, который должен входить в основную программу. Но этот код – это только операции присваивания и операторы управления... все вещи, которые мы сделали раньше. Так что у нас не должно занять слишком много времени предусмотреть также и их.
   БНФ определение, данное раньше для основной программы, включало операторный блок, который мы пока что игнорировали:
   <main> ::= BEGIN <block> END
   Сейчас мы можем рассматривать блок просто как серию операций присваивания:
   <block> ::= (Assignment)*
   Давайте начнем с добавления синтаксического анализатора для блока. Мы начнем с процедуры-заглушки для операции присваивания:
   {–}
   { Parse and Translate an Assignment Statement }
   procedure Assignment;
   begin
   GetChar;
   end;
   {–}
   { Parse and Translate a Block of Statements }
   procedure Block;
   begin
   while Look <> 'e' do
   Assignment;
   end;
   {–}
   Измените процедуру Main чтобы она вызывала Block как показано ниже:
   {–}
   { Parse and Translate a Main Program }
   procedure Main;
   begin
   Match('b');
   Prolog;
   Block;
   Match('e');
   Epilog;
   end;
   {–}
   Эта версия все еще не генерирует никакого кода для «операций присваивания»... все что она делает это съедает символы до тех пор, пока не увидит "e", означающее «END». Но она устанавливает основу для того, что следует дальше.
   Следующий шаг, конечно, – это расширение кода для операций присваивания. Это то, что мы делали много раз до этого, поэтому я не буду задерживаться на этом. На этот раз, однако, я хотел бы работать с генерацией кода немного по-другому. До настоящего времени мы всегда просто вставляли Emits, которые генерируют выходной код в соответствии с подпрограммами синтасического анализа. Немного неструктурно, возможно, но это кажется самым простым способом и помогает видеть, какой код должен быть выдан для каждой конструкции.
   Однако, я понимаю, что большинство из вас используют компьютер 80x86, так что от кода, сгенерированного для 68000 вам мало пользы. Некоторые из вас спрашивали меня, что если бы машинозависимый код мог бы быть собран в одном месте, то было бы проще перенастроить его на другой ЦПУ. Ответ конечно да.
   Чтобы сделать это вставьте следующие подпрограммы «генерации кода»:
   {–}
   { Clear the Primary Register }
   procedure Clear;
   begin
   EmitLn('CLR D0');
   end;
   {–}
   { Negate the Primary Register }
   procedure Negate;
   begin
   EmitLn('NEG D0');
   end;
   {–}
   { Load a Constant Value to Primary Register }
   procedure LoadConst(n: integer);
   begin
   Emit('MOVE #');
   WriteLn(n, ',D0');
   end;
   {–}
   { Load a Variable to Primary Register }
   procedure LoadVar(Name: char);
   begin
   if not InTable(Name) then Undefined(Name);
   EmitLn('MOVE ' + Name + '(PC),D0');
   end;
   {–}
   { Push Primary onto Stack }
   procedure Push;
   begin
   EmitLn('MOVE D0,-(SP)');
   end;
   {–}
   { Add Top of Stack to Primary }
   procedure PopAdd;
   begin
   EmitLn('ADD (SP)+,D0');
   end;
   {–}
   { Subtract Primary from Top of Stack }
   procedure PopSub;
   begin
   EmitLn('SUB (SP)+,D0');
   EmitLn('NEG D0');
   end;
   {–}
   { Multiply Top of Stack by Primary }
   procedure PopMul;
   begin
   EmitLn('MULS (SP)+,D0');
   end;
   {–}
   { Divide Top of Stack by Primary }
   procedure PopDiv;
   begin
   EmitLn('MOVE (SP)+,D7');
   EmitLn('EXT.L D7');
   EmitLn('DIVS D0,D7');
   EmitLn('MOVE D7,D0');
   end;
   {–}
   { Store Primary to Variable }
   procedure Store(Name: char);
   begin
   if not InTable(Name) then Undefined(Name);
   EmitLn('LEA ' + Name + '(PC),A0');
   EmitLn('MOVE D0,(A0)')
   end;
   {–}
   Приятная особенность такого подхода, конечно, в том что мы можем перенастроить компилятор на новый ЦПУ просто переписав эти процедуры «генератора кода». Кроме того, позднее мы обнаружим что можем улучшить качество кода немного подправляя эти процедуры без необходимости изменения компилятора.
   Обратите внимание, что и LoadVar и Store проверяют таблицу идентификаторов чтобы удостовериться, что переменная определена. Обработчик ошибки Undefined просто вызывает Abort:
   {–}
   { Report an Undefined Identifier }
   procedure Undefined(n: string);
   begin
   Abort('Undefined Identifier ' + n);
   end;
   {–}
   Итак, теперь мы наконец готовы начать обработку выполнимого кода. Мы сделаем это заменив пустую версию процедуры Assignment.
   Мы проходили этот путь много раз прежде, так что все это должно быть вам знакомо. Фактически, если бы не изменения, связанные с генерацией кода, мы могли бы просто скопировать процедуры из седьмой части. Так как мы сделали некоторые изменения я не буду их просто копировать, но мы пройдем немного быстрее, чем обычно.
   БНФ для операций присваивания:
   <assignment> ::= <ident> = <expression>
   <expression> ::= <first term> ( <addop> <term> )*
   <first term> ::= <first factor> <rest>
   <term> ::= <factor> <rest>
   <rest> ::= ( <mulop> <factor> )*
   <first factor> ::= [ <addop> ] <factor>
   <factor> ::= <var> | <number> | ( <expression> )
   Эта БНФ также немного отличается от той, что мы использовали раньше... еще одна «вариация на тему выражений». Эта специфичная версия имеет то, что я считаю лучшей обработкой унарного минуса. Как вы увидите позднее, это позволит нам очень эффективно обрабатывать отрицательные константы. Здесь стоит упомянуть, что мы часто видели преимущества «подстраивания» БНФ по ходу дела, с цель сделать язык легким для анализа. То, что вы видете здесь, немного другое: мы подстраиваем БНФ для того, чтобы сделать генерацию кода более эффективной! Это происходит впервые в этой серии.
   Во всяком случае, следующий код реализует эту БНФ:
   {–}
   { Parse and Translate a Math Factor }
   procedure Expression; Forward;
   procedure Factor;
   begin
   if Look = '(' then begin
   Match('(');
   Expression;
   Match(')');
   end
   else if IsAlpha(Look) then
   LoadVar(GetName)
   else
   LoadConst(GetNum);
   end;
   {–}
   { Parse and Translate a Negative Factor }
   procedure NegFactor;
   begin
   Match('-');
   if IsDigit(Look) then
   LoadConst(-GetNum)
   else begin
   Factor;
   Negate;
   end;
   end;
   {–}
   { Parse and Translate a Leading Factor }
   procedure FirstFactor;
   begin
   case Look of
   '+': begin
   Match('+');
   Factor;
   end;
   '-': NegFactor;
   else Factor;
   end;
   end;
   {–}
   { Recognize and Translate a Multiply }
   procedure Multiply;
   begin
   Match('*');
   Factor;
   PopMul;
   end;
   {–}
   { Recognize and Translate a Divide }
   procedure Divide;
   begin
   Match('/');
   Factor;
   PopDiv;
   end;
   {–}
   { Common Code Used by Term and FirstTerm }
   procedure Term1;
   begin
   while IsMulop(Look) do begin
   Push;
   case Look of
   '*': Multiply;
   '/': Divide;
   end;
   end;
   end;
   {–}
   { Parse and Translate a Math Term }
   procedure Term;
   begin
   Factor;
   Term1;
   end;
   {–}
   { Parse and Translate a Leading Term }
   procedure FirstTerm;
   begin
   FirstFactor;
   Term1;
   end;
   {–}
   { Recognize and Translate an Add }
   procedure Add;
   begin
   Match('+');
   Term;
   PopAdd;
   end;
   {–}
   { Recognize and Translate a Subtract }
   procedure Subtract;
   begin
   Match('-');
   Term;
   PopSub;
   end;
   {–}
   { Parse and Translate an Expression }
   procedure Expression;
   begin
   FirstTerm;
   while IsAddop(Look) do begin
   Push;
   case Look of
   '+': Add;
   '-': Subtract;
   end;
   end;
   end;
   {–}
   { Parse and Translate an Assignment Statement }
   procedure Assignment;
   var Name: char;
   begin
   Name := GetName;
   Match('=');
   Expression;
   Store(Name);
   end;
   {–}
   ОК, если вы вставили весь этот код, тогда откомпилируйте и проверьте его. Вы должны увидеть приемлемо выглядящий код, представляющий собой законченную программу, которая будет ассемблироваться и выполняться. У нас есть компилятор!

Булева логика

   Следующий шаг также должен быть вам знаком. Мы должны добавить булевы выражения и операторы отношений. Снова, так как мы работали с ними не один раз, я не буду подробно разбирать их за исключением моментов, в которых они отличаются от того, что мы делали прежде. Снова, мы не будем просто копировать их из других файлов потому что я немного изменил некоторые вещи. Большинство изменений просто включают изоляцию машинозависимых частей как мы делали для арифметических операций. Я также несколько изменил процедуру NotFactor для соответствия структуре FirstFactor. Наконец я исправил ошибку в объектном коде для операторов отношений: в инструкции Scc я использовал только младшие 8 бит D0. Нам нужно установить логическую истину для всех 16 битов поэтому я добавил инструкцию для изменения младшего байта.
   Для начала нам понадобятся несколько подпрограмм распознавания:
   {–}
   { Recognize a Boolean Orop }
   function IsOrop(c: char): boolean;
   begin
   IsOrop := c in ['|', '~'];
   end;
   {–}
   { Recognize a Relop }
   function IsRelop(c: char): boolean;
   begin
   IsRelop := c in ['=', '#', '<', '>'];
   end;
   {–}
   Также нам понадобятся несколько подпрограмм генерации кода:
   {–}
   { Complement the Primary Register }
   procedure NotIt;
   begin
   EmitLn('NOT D0');
   end;
   {–}
   .
   .
   .
   {–}
   { AND Top of Stack with Primary }
   procedure PopAnd;
   begin
   EmitLn('AND (SP)+,D0');
   end;
   {–}
   { OR Top of Stack with Primary }
   procedure PopOr;
   begin
   EmitLn('OR (SP)+,D0');
   end;
   {–}
   { XOR Top of Stack with Primary }
   procedure PopXor;
   begin
   EmitLn('EOR (SP)+,D0');
   end;
   {–}
   { Compare Top of Stack with Primary }
   procedure PopCompare;
   begin
   EmitLn('CMP (SP)+,D0');
   end;
   {–}
   { Set D0 If Compare was = }
   procedure SetEqual;
   begin
   EmitLn('SEQ D0');
   EmitLn('EXT D0');
   end;
   {–}
   { Set D0 If Compare was != }
   procedure SetNEqual;
   begin
   EmitLn('SNE D0');
   EmitLn('EXT D0');
   end;
   {–}
   { Set D0 If Compare was > }
   procedure SetGreater;
   begin
   EmitLn('SLT D0');
   EmitLn('EXT D0');
   end;
   {–}
   { Set D0 If Compare was < }
   procedure SetLess;
   begin
   EmitLn('SGT D0');
   EmitLn('EXT D0');
   end;
   {–}
   Все это дает нам необходимые инструменты. БНФ для булевых выражений такая:
   <bool-expr> ::= <bool-term> ( <orop> <bool-term> )*
   <bool-term> ::= <not-factor> ( <andop> <not-factor> )*
   <not-factor> ::= [ '!' ] <relation>
   <relation> ::= <expression> [ <relop> <expression> ]
   Зоркие читатели могли бы заметить, что этот синтаксис не включает нетерминал «bool-factor» используемый в ранних версиях. Тогда он был необходим потому, что я также разрешал булевы константы TRUE и FALSE. Но не забудьте, что в TINY нет никакого различия между булевыми и арифметическими типами... они могут свободно смешиваться. Так что нет нужды в этих предопределенных значениях... мы можем просто использовать -1 и 0 соответственно.
   В терминологии C мы могли бы всегда использовать определения:
   #define TRUE -1
   #define FALSE 0
   (Так было бы, если бы TINY имел препроцессор.) Позднее, когда мы разрешим объявление констант, эти два значения будут предопределены языком.
   Причина того, что я заостряю на этом ваше внимание, в том что я пытался использовать альтернативный путь, который заключался в использовании TRUE и FALSE как ключевых слов. Проблема с этим подходом в том, что он требует лексического анализа каждого имени переменной в каждом выражении. Как вы помните, я указал в главе 7, что это значительно замедляет компилятор. Пока ключевые слова не могут быть в выражениях нам нужно выполнять сканирование только в начале каждого нового оператора... значительное улучшение. Так что использование вышеуказанного синтаксиса не только упрощает синтаксический анализ, но также ускоряет сканирование.
   Итак, если мы удовлетворены синтаксисом, представленным выше, то соответствующий код показан ниже:
   {–}
   { Recognize and Translate a Relational «Equals» }
   procedure Equals;
   begin
   Match('=');
   Expression;
   PopCompare;
   SetEqual;
   end;
   {–}
   { Recognize and Translate a Relational «Not Equals» }
   procedure NotEquals;
   begin
   Match('#');
   Expression;
   PopCompare;
   SetNEqual;
   end;
   {–}
   { Recognize and Translate a Relational «Less Than» }
   procedure Less;
   begin
   Match('<');
   Expression;
   PopCompare;
   SetLess;
   end;
   {–}
   { Recognize and Translate a Relational «Greater Than» }
   procedure Greater;
   begin
   Match('>');
   Expression;
   PopCompare;
   SetGreater;
   end;
   {–}
   { Parse and Translate a Relation }
   procedure Relation;
   begin
   Expression;
   if IsRelop(Look) then begin
   Push;
   case Look of
   '=': Equals;
   '#': NotEquals;
   '<': Less;
   '>': Greater;
   end;
   end;
   end;
   {–}
   { Parse and Translate a Boolean Factor with Leading NOT }
   procedure NotFactor;
   begin
   if Look = '!' then begin
   Match('!');
   Relation;
   NotIt;
   end
   else
   Relation;
   end;
   {–}
   { Parse and Translate a Boolean Term }
   procedure BoolTerm;
   begin
   NotFactor;
   while Look = '&' do begin
   Push;
   Match('&');
   NotFactor;
   PopAnd;
   end;
   end;
   {–}
   { Recognize and Translate a Boolean OR }
   procedure BoolOr;
   begin
   Match('|');
   BoolTerm;
   PopOr;
   end;
   {–}
   { Recognize and Translate an Exclusive Or }
   procedure BoolXor;
   begin
   Match('~');
   BoolTerm;
   PopXor;
   end;
   {–}
   { Parse and Translate a Boolean Expression }
   procedure BoolExpression;
   begin
   BoolTerm;
   while IsOrOp(Look) do begin
   Push;
   case Look of
   '|': BoolOr;
   '~': BoolXor;
   end;
   end;
   end;
   {–}
   Чтобы связать все это вместе не забудьте изменить обращение к Expression в процедурах Factor и Assignment на вызов BoolExpression.
   Хорошо, если вы набрали все это, откомпилируйте и погоняйте эту версию. Сначала удостоверьтесь, что вы все еще можете анализировать обычные арифметические выражения. Затем попробуйте булевские. Наконец удостоверьтесь, что вы можете присваивать результат сравнения. Попробуйте к примеру:
   pvx,y,zbx=z>ye.
   что означает
   PROGRAM
   VAR X,Y,Z
   BEGIN
   X = Z > Y
   END.
   Видите как происходит присваивание булевского значения X?

Управляющие структуры

   Мы почти дома. Имея булевы выражения легко добавить управляющие структуры. Для TINY мы разрешим только две из них, IF и WHILE:
   <if> ::= IF <bool-expression> <block> [ ELSE <block>] ENDIF
   <while> ::= WHILE <bool-expression> <block> ENDWHILE
   Еще раз позвольте мне разъяснить решения, подразумевающиеся в этом синтаксисе, который сильно отличается от синтаксиса C или Pascal. В обоих этих языках «тело» IF или WHILE расценивается как одиночный оператор. Если вы предполагаете использовать блок из более чем одного оператора вы должны создать составной утверждение использую BEGIN-END (в Pascal) или '{}' (в C). В TINY (и KISS) нет таких вещей как составное утверждение... одиночное или множественное, они являются в этом языке просто блоками.
   В KISS все управляющие структуры имеют явные и уникальные ключевые слова, выделяющие операторный блок поэтому не может быть никакой путаницы где он начинается и заканчивается. Это современный подход, используемый в таких уважаемых языках, как Ada и Modula-2 и он полностью устраняет проблему «висячих else».
   Обратите внимание, что я мог бы использовать то же самое ключевое слово END для завершения всех конструкций, как это сделано в Pascal. (Закрывающая '}' в C служит той же самой цели.) Но это всегда вело к неразберихе, вот почему программисты на Pascal предпочитают писать так:
   end { loop }
   или end { if }
   Как я объяснил в пятой части, использование уникальных терминальных ключевых слов увеличивает размер списка ключевых слов и, следовательно, замедляет лексический анализ, но в данном случае это кажется небольшой ценой за дополнительную подстраховку. Лучше обнаруживать ошибки во время компиляции, чем во время выполнения.
   Одна последняя мысль: каждая из двух конструкций выше имеют нетерминалы
   <bool-expression> и <block>,
   расположенные рядом без разделяющих ключевых слов. В Паскале мы ожидали бы в этом месте ключевые слова THEN и DO.
   Я не вижу проблем в том, чтобы опустить эти ключевые слова, и синтаксический анализатор также не будет иметь проблем, при условии, что мы не сделаем ошибок в bool-expression. С другой стороны, если мы включим эти дополнительные ключевые слова мы получили бы еще один уровень подстраховки за малые деньги, и с этим у меня также нет проблем. Примите правильное решение каким путем пойти.
   ОК, после этого небольшого объяснения давайте продолжим. Как обычно нам понадобятся несколько новых подпрограмм генерации кода. Они генерируют код для условных и безусловных переходов:
   {–}
   { Branch Unconditional }
   procedure Branch(L: string);
   begin
   EmitLn('BRA ' + L);
   end;
   {–}
   { Branch False }
   procedure BranchFalse(L: string);
   begin
   EmitLn('TST D0');
   EmitLn('BEQ ' + L);
   end;
   {–}
   Исключая изоляцию подпрограмм генератора кода, код для анализа управляющих конструкций такой же, как вы видели прежде:
   {–}
   { Recognize and Translate an IF Construct }
   procedure Block; Forward;
   procedure DoIf;
   var L1, L2: string;
   begin
   Match('i');
   BoolExpression;
   L1 := NewLabel;
   L2 := L1;
   BranchFalse(L1);
   Block;
   if Look = 'l' then begin
   Match('l');
   L2 := NewLabel;
   Branch(L2);
   PostLabel(L1);
   Block;
   end;
   PostLabel(L2);
   Match('e');
   end;
   {–}
   { Parse and Translate a WHILE Statement }
   procedure DoWhile;
   var L1, L2: string;
   begin
   Match('w');
   L1 := NewLabel;
   L2 := NewLabel;
   PostLabel(L1);
   BoolExpression;
   BranchFalse(L2);
   Block;
   Match('e');
   Branch(L1);
   PostLabel(L2);
   end;
   {–}
   Чтобы связать все это вместе нам нужно только изменить процедуру Block чтобы распознавать ключевые слова IF и WHILE. Как обычно мы расширим определение блока так:
   <block> ::= ( <statement> )*
   где
   <statement> ::= <if> | <while> | <assignment>
   Соответствующий код:
   {–}
   { Parse and Translate a Block of Statements }
   procedure Block;
   begin
   while not(Look in ['e', 'l']) do begin
   case Look of
   'i': DoIf;
   'w': DoWhile;
   else Assignment;
   end;
   end;
   end;
   {–}
   Добавьте подпрограммы, которые я дал, откомпилируйте и протестируйте их. У вас должна быть возможность анализировать односимвольные версии любых управляющих конструкции. Выглядит довольно хорошо!
   Фактически, за исключением односимвольного ограничения, мы получили практически полную версию TINY. Я назову его TINY Version 0.1.

Лексический анализ

   Конечно, вы знаете, что будет дальше: Мы должны преобразовать программу так, чтобы она могла работать с многосимвольными ключевыми словами, переводами строк и пробелами. Мы только что прошли все это в седьмой главе. Мы будем использовать метод распределенного сканера, который я показал вам в этой главе. Фактическая реализация немного отличается, потому что различается способ, которым я обрабатываю переводы строк.
   Для начала, давайте просто разрешим пробелы. Для этого необходимо только добавить вызовы SkipWhite в конец трех подпрограмм GetName, GetNum и Match. Вызов SkipWhite в Init запускает помпу в случае если есть ведущие пробелы.
   Затем мы должны обрабатывать переводы строк. Это в действительности двухшаговый процесс так как обработка переносов с односимвольными токенами отличается от таковой для многосимвольных токенов. Мы можем устранить часть работы сделав оба шага одновременно, но я чувствую себя спокойней, работая последовательно.
   Вставьте новую процедуру:
   {–}
   { Skip Over an End-of-Line }
   procedure NewLine;
   begin
   while Look = CR do begin
   GetChar;
   if Look = LF then GetChar;
   SkipWhite;
   end;
   end;
   {–}
   Заметьте, что мы видели эту процедуру раньше в виде процедуры Fin. Я изменил имя, так как новое кажется более соответствующим фактическому назначению. Я также изменил код чтобы учесть множественные переносы и строки только с пробелами.
   Следующим шагом будет вставка вызовов NewLine везде, где мы посчитаем перенос допустимым. Как я подчеркивал ранее, этот момент может очень различаться для разных языков. В TINY я решил разрешить их практически в любом месте. Это означает, что нам нужно вызывать NewLine в начале (не в конце как с SkipWhite) процедур GetName, GetNum и Match.
   Для процедур, которые имеют циклы While, таких как TopDecl, нам нужен вызов NewLine в начале процедуры и в конце каждого цикла. Таким способом мы можем быть уверены, что NewLine вызывается в начале каждого прохода через цикл.
   Если вы все это сделали, испытайте программу и проверьте, что она действительно обрабатывает пробелы и переносы.
   Если это так, тогда мы готовы работать с многосимвольными токенами и ключевыми словами. Для начала, добавьте дополнительные объявления (скопированные почти дословно из главы 7):
   {–}
   { Type Declarations }
   type Symbol = string[8];
   SymTab = array[1..1000] of Symbol;
   TabPtr = ^SymTab;
   {–}
   { Variable Declarations }
   var Look : char; { Lookahead Character }
   Token: char; { Encoded Token }
   Value: string[16]; { Unencoded Token }
   ST: Array['A'..'Z'] of char;
   {–}
   { Definition of Keywords and Token Types }
   const NKW = 9;
   NKW1 = 10;
   const KWlist: array[1..NKW] of Symbol =
   ('IF', 'ELSE', 'ENDIF', 'WHILE', 'ENDWHILE',
   'VAR', 'BEGIN', 'END', 'PROGRAM');
   const KWcode: string[NKW1] = 'xilewevbep';
   {–}
   Затем добавьте три процедуры, также из седьмой главы:
   {–}
   { Table Lookup }
   function Lookup(T: TabPtr; s: string; n: integer): integer;
   var i: integer;
   found: Boolean;
   begin
   found := false;
   i := n;
   while (i > 0) and not found do
   if s = T^[i] then
   found := true
   else