Джек Креншоу - Давайте создадим компилятор!
Возможно более удивительно то что символ TAB не имеет никакого эффекта; наша строка «инструкций» начинается с первой колонки так же как и фальшивая метка... Правильно: WinCRT не поддерживает табуляцию. У нас проблема.
Есть несколько способов, с помощью которых мы можем решить эту проблему. Один из вариантов того что мы можем сделать – просто игнорировать ее. Каждый ассемблер, который я когда либо использовал, резервируют колонку 1 для меток и взбунтуется когда увидит, что в ней начинаются инструкции. Так что, по крайней мере, мы должны сдвинуть инструкции на одну колонку чтобы сделать ассемблер счастливым. Это достаточно просто сделать: просто измените в процедуре Emit строку:
Write(TAB, s);
на
Write(' ', s);
Я должен признать что сталкивался с этой проблемой раньше и находил себя меняющим свое мнение так часто как хамелеон меняет цвет. Для наших целей, 99% которых будет проверка выходного кода при выводе на CRT, было бы хорошо видеть аккуратно сгруппированный «объектный» код. Строка:
SUB1: MOVE #4,D0
просто выглядит более опрятно, чем отличающийся, но функционально идентичный код:
SUB1:
MOVE #4,D0
В тестовой версии моего кода я включил более сложную версию процедуры PostLabel, которая позволяет избежать размещения меток на раздельных строках, задерживая печать метки чтобы она оказалась на той же самой строке, что и связанная инструкция. Не позднее чем час назад, моя версия модуля Output предоставляла полную поддержку табуляции с использованием внутренней переменной счетчика столбцов и подпрограммы для ее управления. Я имел некоторый довольно изящный код для поддержки механизма табуляции с минимальным увеличением кода. Было ужасное искушение показать вам эту «красивую» версию, единственно чтобы покрасоваться элегантностью.
Однако, код «элегантной» версии был значительно более сложным и большим. После этого у меня появилась вторая мысль. Несмотря на наше желание видеть красивый вывод, неизбежный факт то, что две версии MAIN: фрагменты кода, показанные выше функционально идентичны; ассемблер, который является конечной целью кода, не интересует какую версию он получает, за исключением того, что красивая версия будет содержать больше символов, следовательно будет использовать больше дискового пространства и дольше ассемблироваться. Но красивая версия не только генерирует больше кода, но дает больший выходной файл, с гораздо большим количество пустых символов чем минимально необходимо. Когда вы посмотрите на это с такой стороны, то не трудно будет решить какой подход использовать, не так ли?
То что наконец решило для меня этот вопрос было напоминанием считаться с моей первой заповедью: KISS. Хотя я был довольно горд всеми своими изящными приемчиками для реализации табуляции, я напомнил себе, что перефразируя сенатора Барри Голдватера, элегантность в поисках сложности не является достоинством. Другой мудрый человек однажды написал: «Любой идиот может разработать Роллс-Ройс. Требуется гений, чтобы разработать VW». Так что изящная, дружественная табуляции версия Output в прошлом, и то, что вы видите, это простая компактная VW версия.
Модуль ERROR
Наш следующий набор подпрограмм обрабатывает ошибки. Чтобы освежить вашу память мы возьмем подход, заданный Borland в Turbo Pascal – останавливаться на первой ошибке. Это не только значительно упрощает наш код, полностью устраняя назойливую проблему восстановления после ошибок, но это также имеет намного больший смысл, по моему мнению, в интерактивной среде. Я знаю, что это может быть крайней позицией, но я считаю практику сообщать обо всех ошибках в программе анахронизмом, пережитком со времен пакетной обработки. Пришло время прекратить такую практику. Так вот.
В нашем оригинальном Cradle мы имели две процедуры обработки ошибок: Error, которая не останавливалась, и Abort, которая останавливалась. Но я не думаю, что мы когда-либо найдем применение процедуре, которая не останавливается, так что в новом, тощем и скромном модуле Errors, показанном ниже, процедура Error занимает место Abort.
{–}
unit Errors;
{–}
interface
procedure Error(s: string);
procedure Expected(s: string);
{–}
implementation
{–}
{ Write error Message and Halt }
procedure Error(s: string);
begin
WriteLn;
WriteLn(^G, 'Error: ', s, '.');
Halt;
end;
{–}
{ Write «<something> Expected» }
procedure Expected(s: string);
begin
Error(s + ' Expected');
end;
end.
{–}
Как обычно, вот программа для проверки:
{–}
program Test;
uses WinCRT, Input, Output, Errors;
begin
Expected('Integer');
end.
{–}
Вы заметили, что строка «uses» в нашей основной программе становится длиннее? Это нормально. В конечной версии основная программа будет вызывать процедуры только из нашего синтаксического анализатора, так что раздел uses будет иметь только пару записей. Но сейчас возможно самое лучшее включить все модули, чтобы мы могли протестировать процедуры в них.
Лексический и синтаксический анализ
Классическая архитектура компилятора основана на отдельных модулях для лексического анализатора, который предоставляет лексемы языка, и синтаксического анализатора, который пытается определить смысл токенов как синтаксических элементов. Если вы еще не забыли что мы делали в более ранних главах, вы вспомните, что мы не делали ничего подобного. Поскольку мы используем предсказывающий синтаксический анализатор, мы можем почти всегда сказать, какой элемент языка следует дальше, всего-лишь исследуя предсказывающий символ. Следовательно, нам не нужно предварительно выбирать токен, как делал бы сканер.
Но даже хотя здесь и нет функциональной процедуры, названной «Scanner», все еще имеет смысл отделить функции лексического анализа от функций синтаксического анализа. Так что я создал еще два модуля, названных, достаточно удивительно, Scanner и Parser. Модуль Scanner содержит все подпрограммы, известные как распознаватели. Некоторые из них, такие как IsAlpha, являются чисто булевыми подпрограммами, которые оперируют только предсказывающим символом. Другие подпрограммы собирают токены, такие как идентификаторы и числовые константы. Модуль Parser будет содержать все подпрограммы, составляющие синтаксический анализатор с рекурсивным спуском. Общим правилом должно быть то, что модуль Parser содержит всю специфическую для языка информацию; другими словами, синтаксис языка должен полностью содержаться в Parser. В идеальном мире это правило должно быть верным в той степени, что мы можем изменять компилятор для компиляции различных языков просто заменяя единственный модуль Parser.
На практике, дела почти никогда не бывают такими чистыми. Все есть небольшая «утечка» синтаксических правил также и в сканер. К примеру, правила составления допустимого идентификатора или константы могут меняться от языка к языку. В некоторых языках правила о комментариях разрешают им быть отфильтрованными в сканере, в то время как другие не разрешают. Так что на практике оба модуля вероятно придут к тому, что будут иметь языко-зависимые компоненты, но изменения, необходимые для сканнера, должны быть относительно тривиальными.
Теперь вспомните, что мы использовали две версии подпрограмм лексического анализатора: одна, которая поддерживала только односимвольные токены, которую мы использовали в ряде наших тестов, и другая, которая предоставляет полную поддержку многосимвольных токенов. Теперь, когда мы разделяем нашу программу на модули, я не ожидаю многого от использования односимвольной версии, но не потребуется многого, чтобы предусмотреть их обе. Я создал две версии модуля Scanner. Первая, названная Scanner1, содержит односимвольную версию подпрограмм распознавания:
{–}
unit Scanner1;
{–}
interface
uses Input, Errors;
function IsAlpha(c: char): boolean;
function IsDigit(c: char): boolean;
function IsAlNum(c: char): boolean;
function IsAddop(c: char): boolean;
function IsMulop(c: char): boolean;
procedure Match(x: char);
function GetName: char;
function GetNumber: char;
{–}
implementation
{–}
{ Recognize an Alpha Character }
function IsAlpha(c: char): boolean;
begin
IsAlpha := UpCase(c) in ['A'..'Z'];
end;
{–}
{ Recognize a Numeric Character }
function IsDigit(c: char): boolean;
begin
IsDigit := c in ['0'..'9'];
end;
{–}
{ Recognize an Alphanumeric Character }
function IsAlnum(c: char): boolean;
begin
IsAlnum := IsAlpha(c) or IsDigit(c);
end;
{–}
{ Recognize an Addition Operator }
function IsAddop(c: char): boolean;
begin
IsAddop := c in ['+','-'];
end;
{–}
{ Recognize a Multiplication Operator }
function IsMulop(c: char): boolean;
begin
IsMulop := c in ['*','/'];
end;
{–}
{ Match One Character }
procedure Match(x: char);
begin
if Look = x then GetChar
else Expected('''' + x + '''');
end;
{–}
{ Get an Identifier }
function GetName: char;
begin
if not IsAlpha(Look) then Expected('Name');
GetName := UpCase(Look);
GetChar;
end;
{–}
{ Get a Number }
function GetNumber: char;
begin
if not IsDigit(Look) then Expected('Integer');
GetNumber := Look;
GetChar;
end;
end.
{–}
Следующий фрагмент кода основной программы обеспечивает хорошую проверку лексического анализатора. Для краткости я включил здесь только выполнимый код; остальное тоже самое. Не забудьте, тем не менее, добавить имя Scanner1 в раздел «uses»: