ВВЕДЕНИЕ
В последней главе я показал вам основную идею нисходящей разработки компилятора. Я показал вам первые несколько шагов этого процесса для компиляторов Pascal и C, но я остановился далеко от его завершения. Причина была проста: если мы собираемся построить настоящий, функциональный компилятор для какого-нибудь языка, я предпочел бы сделать это для KISS, языка, который я определил в этой обучающей серии.
В этой главе мы собираемся сделать это же для подмножества KISS, которое я решил назвать TINY.
Этот процесс по существу будет аналогичен выделенному в главе 9, за исключением одного заметного различия. В той главе я предложил вам начать с полного БНФ описания языка. Это было бы прекрасно для какого-нибудь языка типа Pascal или C, определения которого устоялись. В случае же с TINY, однако, мы еще не имеем полного описания... мы будем определять язык по ходу дела. Это нормально. Фактически, это предпочтительней, так как мы можем немного подстраивать язык по ходу дела для сохранения простоты анализа.
Так что в последующей разработке мы фактически будем выполнять нисходящую разработку и языка и его компилятора. БНФ описание будет расти вместе с компилятором.
В ходе этого будет принят ряд решений, каждое из которых будет влиять на БНФ и, следовательно, характер языка. В каждой решающей точке я попытаюсь не забывать объяснять решение и разумное обоснование своего выбора. Если вам случится придерживаться другого мнения и вы предпочтете другой вариант, вы можете пойти своим путем. Сейчас вы имеет базу для этого. Я полагаю важно отметить, что ничего из того, что мы здесь делаем не подчинено каким-либо жесткими правилами. Когда вы разрабатываете свой язык вы не должны стесняться делать это своим способом.
Многие из вас могут сейчас спросить: зачем нужно начинать с самого начала? У нас есть работающее подмножество KISS как результат главы 7 (лексический анализ). Почему бы просто не расширить его как нужно? Ответ тройной. Прежде всего, я сделал несколько изменений для упрощения программы... типа изоляции процедур генерации кода, в результате чего мы можем более легко выполнять преобразование для различных машин. Во-вторых, я хочу, чтобы вы увидели что разработка действительно может быть выполнена сверху вниз как это подчеркнуто в последней главе. Наконец, нам всем нужна практика. Каждый раз, когда я прохожу через эти упражнения, я начинаю понимать немного больше, и вы будете тоже.
ПОДГОТОВКА
Много лет назад существовали языки, называемые Tiny BASIC, Tiny Pascal и Tiny C, каждый из которых был подмножеством своего полного родительского языка. Tiny BASIC, к примеру, имел только одно-символьные имена переменных и глобальные переменные. Он поддерживал только один тип данных. Звучит знакомо? К этому моменту мы имеем почти все инструменты, необходимые для создания компилятора подобного этому.
Однако язык, называемый Tiny-такой-то все же несет некоторый багаж, унаследованный от своего родительского языка. Я часто задавался вопросом, хорошая ли это идея. Согласен, язык, основанный на каком-то родительском языке, будет иметь преимущество знакомости, но может также существовать некоторый особенный синтаксис, перенесенный из родительского языка, который может приводить к появлению ненужной сложности в компиляторе. (Нигде это не является большей истиной, чем в Small C).
Я задавался вопросом, насколько маленьким и простым может быть создан компилятор и при этом все еще быть полезным, если он разрабатывался из условия быть легким и для использования и для синтаксического анализа. Давайте выясним. Этот язык будет называться просто "TINY". Он является подмножеством KISS, который я также еще полностью не определил, что по крайней мере делает нас последовательными (!). Я полагаю вы могли бы назвать его TINY KISS. Но это открывает целую кучу проблем, так что давайте просто придерживаться имени TINY.
Главные ограничения TINY будут возникать из-за тех вещей, которые мы еще не рассмотрели, таких как типы данных. Подобно своим кузенам Tiny C и Tiny BASIC, TINY будет иметь только один тип данных, 16-разрядное целое число. Первая версия, которую мы разработаем, не будет также иметь вызовов процедур и будет использовать одно-символьные имена переменных, хотя, как вы увидите, мы можем удалить эти ограничения без особых усилий.
Язык, который я придумал, разделит некоторые хорошие особенности Pascal, C и Ada. Получив урок из сравнения компиляторов Pascal и C в предыдущей главе, TINY все же будет иметь преимущественно вкус Паскаля. Везде, где возможно, структура языка будет ограничена ключевыми словами или символами, так что синтаксический анализатор будет знать, что происходит без догадок.
Другое основное правило: Я хотел бы чтобы в течение всей разработки компилятор производил настоящий выполнимый код. Даже если его не может быть слишком много в самом начале, но по крайней мере он должен быть корректным.
Наконец, я буду использовать пару ограничений Pascal, которые имеют смысл: Все данные и процедуры должны быть объявлены перед тем, как они используются. Это имеет большой смысл, даже если сейчас единственным типом данных, который мы будем использовать, будет слово. Это правило, в свою очередь, означает, что единственное приемлемое место для размещения выполнимого кода основной программы - в конце листинга.
Определение верхнего уровня будет аналогично Pascal:
<program> ::= PROGRAM <top-level decl> <main> '.'
Мы уже достигли решающей точки. Моей первой мыслью было сделать основной блок необязательным. Кажется бессмысленным писать "программу" без основной программы, но это имеет смысл, если мы разрешим множественные модули, связанные вместе. Фактически я предполагаю учесть это в KISS. Но тогда мы столкнемся с кучей проблем, которые я предпочел бы сейчас не затрагивать. Например, термин "PROGRAM" в действительности становится неправильно употребляемым. MODULE из Modula-2 или UNIT из Turbo Pascal были бы более подходящими. Во-вторых, как насчет правил видимости? Нам необходимо соглашение для работы с видимостью имен в модулях. На данный момент лучше просто сохранить простоту и совершенно игнорировать эту идею.
Также необходимо определиться с требованием, чтобы основная программа была последней. Я играл с идеей сделать ее размещение нефиксированным как в C. Характер SK*DOS, ОС под которую я компилирую, позволяет сделать это очень просто. Но это в действительности не имеет большого смысла принимая во внимание Pascal-подобное требование, что все данные и процедуры должны быть объявлены прежде чем они используются. Так как основная программа может вызывать только те процедуры, которые уже были объявлены, единственное местоположение, имеющее смысл - в конце, a la Pascal.
По данной выше БНФ давайте напишем синтаксический анализатор, который просто распознает скобки:
Code: |
{ Parse and Translate a Program } procedure Prog; begin Match('p'); Header; Prolog; Match('.'); Epilog; end; |
Процедура Header просто выдает инициализационный код, необходимый ассемблеру:
Code: |
{ Write Header Info } procedure Header; begin WriteLn('WARMST', TAB, 'EQU $A01E'); end; |
Процедуры Prolog и Epilog выдают код для идентификации основной программы и для возвращения в ОС:
Code: |
{ Write the Prolog } procedure Prolog; begin PostLabel('MAIN'); end;
{ Write the Epilog } procedure Epilog; begin EmitLn('DC WARMST'); EmitLn('END MAIN'); end; |
Основная программа просто вызывает Prog и затем выполняет проверку на чистое завершение:
{ Main Program }
begin
Init;
Prog;
if Look <> CR then Abort('Unexpected data after ''.''');
end.
Сейчас TINY примет только одну "программу" - пустую:
PROGRAM . (или 'p.' в нашей стенографии).
Заметьте, тем не менее, что компилятор генерирует для этой программы корректный код. Она будет выполняться и делать то, что можно ожидать от пустой программы, т.е. ничего кроме элегантного возвращения в ОС.
Один из моих любимых бенчмарков для компиляторов заключается в компиляции, связывании и выполнении пустой программы для любого языка. Вы можете многое узнать о реализации измеряя предел времени, необходимый для компиляции тривиальной программы. Также интересно измерить количество полученного кода. Во многих компиляторах код может быть довольно большим, потому что они всегда включают целую run-time библиотеку независимо от того, нуждаются они в ней или нет. Ранние версии Turbo Pascal в этом случае производили объектный файл 12К. VAX C генерирует 50К!
Самые маленькие пустые программы какие я видел, получены компиляторами Модула-2 и они занимают примерно 200-800 байт.
В случае TINY у нас еще нет run-time библиотеки, так что объектный код действительно крошечный (tiny): два байта. Это стало рекордом, и вероятно останется таковым, так как это минимальный размер, требуемый ОС.
Следующим шагом будет обработка кода для основной программы. Я буду использовать блок BEGIN из Pascal:
<main> ::= BEGIN <block> END
Здесь мы снова приняли решение. Мы могли бы потребовать использовать объявление вида "PROCEDURE MAIN", подобно C. Я должен допустить, что это совсем неплохая идея... Мне не особенно нравится подход Паскаля так как я предпочитаю не иметь проблем с определением местоположения основной программы в листинге Паскаля. Но альтернатива тоже немного неудобна, так как вы должны работать с проверкой ошибок когда пользователь опустит основную программу или сделает орфографическую ошибку в ее названии. Здесь я использую простой выход.
Другое решение проблемы "где расположена основная программа" может заключаться в требовании имени для программы и заключения основной программы в скобки:
BEGIN <name>
END <name>
аналогично соглашению Модула-2. Это добавляет в язык немного "синтаксического сахара". Подобные вещи легко добавлять и изменять по вашим симпатиям если вы сами проектируете язык.
Для синтаксического анализа такого определения основного блока измените процедуру Prog следующим образом:
Code: |
{ Parse and Translate a Program } procedure Prog; begin Match('p'); Header; Main; Match('.'); end;
и добавьте новую процедуру:
{ Parse and Translate a Main Program } procedure Main; begin Match('b'); Prolog; Match('e'); Epilog; end; |
Теперь единственной допустимой программой является программа:
PROGRAM BEGIN END. (или 'pbe.')
Разве мы не делаем успехи??? Хорошо, как обычно это становится лучше. Вы могли бы попробовать сделать здесь некоторые преднамеренные ошибки подобные пропуску 'b' или 'e' и посмотреть что случится. Как всегда компилятор должен отметить все недопустимые входные символы.
ОБЪЯВЛЕНИЯ
Очевидно на следующем шаге необходимо решить, что мы подразумеваем под объявлением. Я намереваюсь иметь два вида объявлений: переменных и процедур/функций. На верхнем уровне разрешены только глобальные объявления, точно как в C.
Сейчас здесь могут быть только объявления переменных, идентифицируемые по ключевому слову VAR (сокращенно "v").
<top-level decls> ::= ( <data declaration> )*
<data declaration> ::= VAR <var-list>
Обратите внимание, что так как имеется только один тип переменных, нет необходимости объявлять этот тип. Позднее, для полной версии KISS, мы сможем легко добавить описание типа.
Процедура Prog становится:
Code: |
{ Parse and Translate a Program } procedure Prog; begin Match('p'); Header; TopDecls; Main; Match('.'); end;
Теперь добавьте две новые процедуры:
{ Process a Data Declaration } procedure Decl; begin Match('v'); GetChar; end;
{ Parse and Translate Global Declarations } procedure TopDecls; begin while Look <> 'b' do case Look of 'v': Decl; else Abort('Unrecognized Keyword ''' + Look + ''''); end; end; |
Заметьте, что на данный момент Decl - просто заглушка. Она не генерирует никакого кода и не обрабатывает список... каждая переменная должна быть в отдельном утверждении VAR.
ОК, теперь у нас может быть любое число объявлений данных, каждое начинается с "v" вместо VAR, перед блоком BEGIN. Попробуйте несколько вариантов и посмотрите, что происходит.
ОБЪЯВЛЕНИЯ И ИДЕНТИФИКАТОРЫ
Это выглядит довольно хорошо, но мы все еще генерируем только пустую программу. Настоящий ассемблер должен выдавать директивы ассемблера для распределения памяти под переменные. Пришло время действительно получить какой-нибудь код.
С небольшим дополнительным кодом это легко сделать в процедуре Decl. Измените ее следующим образом:
Code: |
{ Parse and Translate a Data Declaration } procedure Decl; var Name: char; begin Match('v'); Alloc(GetName); end; |
Процедура Alloc просто выдает команду ассемблеру для распределения памяти:
Code: |
{ Allocate Storage for a Variable } procedure Alloc(N: char); begin WriteLn(N, ':', TAB, 'DC 0'); end; |
Погоняйте программу. Попробуйте входную последовательность, которая объявляет какие-нибудь переменные, например:
pvxvyvzbe.
Видите, как распределяется память? Просто, да? Заметьте также, что точка входа "MAIN" появляется в правильном месте.
Кстати, "настоящий" компилятор имел бы также таблицу идентификаторов для записи используемых переменных. Обычно, таблица идентификаторов необходима для записи типа каждой переменной. Но так как в нашем случае все переменные имеют один и тот же тип, нам не нужна таблица идентификаторов. Оказывается, мы смогли бы находить идентификатор даже без различия типов, но давайте отложим это пока не возникнет такая необходимость.
Конечно, в действительности мы не анализировали правильный синтаксис для объявления данных, так как он включает список переменных. Наша версия разрешает только одну переменную. Это также легко исправить.
БНФ для <var-list> следующая:
<var-list> ::= <ident> (, <ident>)*
Добавление этого синтаксиса в Decl дает новую версию:
Code: |
{ 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 как показано ниже:
Code: |
{ 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 поддерживает оба типа. Но нет никакой причины ограничивать себя одноразрядными значениями, так что правильной версией для использования будет та, которая возвращает целое число. Вот она:
Code: |
{ 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 следующим образом:
Code: |
{ 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;
и вставьте следующую функцию:
Code: |
{ 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)*
Давайте начнем с добавления синтаксического анализатора для блока. Мы начнем с процедуры-заглушки для операции присваивания:
Code: |
{ 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 как показано ниже:
Code: |
{ Parse and Translate a Main Program } procedure Main; begin Match('b'); Prolog; Block; Match('e'); Epilog; end; |
Эта версия все еще не генерирует никакого кода для "операций присваивания"... все что она делает это съедает символы до тех пор, пока не увидит "e", означающее "END". Но она устанавливает основу для того, что следует дальше.
Следующий шаг, конечно, - это расширение кода для операций присваивания. Это то, что мы делали много раз до этого, поэтому я не буду задерживаться на этом. На этот раз, однако, я хотел бы работать с генерацией кода немного по-другому. До настоящего времени мы всегда просто вставляли Emits, которые генерируют выходной код в соответствии с подпрограммами синтаксического анализа. Немного неструктурно, возможно, но это кажется самым простым способом и помогает видеть, какой код должен быть выдан для каждой конструкции.
Однако, я понимаю, что большинство из вас используют компьютер 80x86, так что от кода, сгенерированного для 68000 вам мало пользы. Некоторые из вас спрашивали меня, что если бы машиннозависимый код мог бы быть собран в одном месте, то было бы проще перенастроить его на другой ЦПУ. Ответ конечно да.
Чтобы сделать это вставьте следующие подпрограммы "генерации кода":
Code: |
{---------------------------------------------------------------} { 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:
Code: |
{ 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> )
Эта БНФ также немного отличается от той, что мы использовали раньше... еще одна "вариация на тему выражений". Эта специфичная версия имеет то, что я считаю лучшей обработкой унарного минуса. Как вы увидите позднее, это позволит нам очень эффективно обрабатывать отрицательные константы. Здесь стоит упомянуть, что мы часто видели преимущества "подстраивания" БНФ по ходу дела, с цель сделать язык легким для анализа. То, что вы видите здесь, немного другое: мы подстраиваем БНФ для того, чтобы сделать генерацию кода более эффективной! Это происходит впервые в этой серии.
Во всяком случае, следующий код реализует эту БНФ:
Code: |
{---------------------------------------------------------------} { 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 битов поэтому я добавил инструкцию для изменения младшего байта.
Для начала нам понадобятся несколько подпрограмм распознавания:
Code: |
{ 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; |
Также нам понадобятся несколько подпрограмм генерации кода:
Code: |
{---------------------------------------------------------------} { 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, что это значительно замедляет компилятор. Пока ключевые слова не могут быть в выражениях нам нужно выполнять сканирование только в начале каждого нового оператора... значительное улучшение. Так что использование вышеуказанного синтаксиса не только упрощает синтаксический анализ, но также ускоряет сканирование.
Итак, если мы удовлетворены синтаксисом, представленным выше, то соответствующий код показан ниже:
Code: |
{---------------------------------------------------------------} { 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. С другой стороны, если мы включим эти дополнительные ключевые слова мы получили бы еще один уровень подстраховки за малые деньги, и с этим у меня также нет проблем. Примите правильное решение, каким путем пойти.
ОК, после этого небольшого объяснения давайте продолжим. Как обычно нам понадобятся несколько новых подпрограмм генерации кода. Они генерируют код для условных и безусловных переходов:
Code: |
{---------------------------------------------------------------} { 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; |
Исключая изоляцию подпрограмм генератора кода, код для анализа управляющих конструкций такой же, как вы видели прежде:
Code: |
{---------------------------------------------------------------} { 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>
Соответствующий код:
Code: |
{ 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 запускает помпу в случае если есть ведущие пробелы.
Затем мы должны обрабатывать переводы строк. Это в действительности двух шаговый процесс так как обработка переносов с одно-символьными токенами отличается от таковой для много символьных токенов. Мы можем устранить часть работы сделав оба шага одновременно, но я чувствую себя спокойней, работая последовательно.
Вставьте новую процедуру:
Code: |
{ 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):
Code: |
{ 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'; |
Затем добавьте три процедуры, также из седьмой главы:
Code: |
{ 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 dec(i); Lookup := i; end;
. .
{ Get an Identifier and Scan it for Keywords } procedure Scan; begin GetName; Token := KWcode[Lookup(Addr(KWlist), Value, NKW) + 1]; end;
. .
{ Match a Specific Input String } procedure MatchString(x: string); begin if Value <> x then Expected('''' + x + ''''); end; |
Теперь мы должны сделать довольно много тонких изменений в оставшихся процедурах. Сначала мы должны изменить функцию GetName на процедуру, снова как в главе 7:
Code: |
{ Get an Identifier } procedure GetName; begin NewLine; if not IsAlpha(Look) then Expected('Name'); Value := ''; while IsAlNum(Look) do begin Value := Value + UpCase(Look); GetChar; end; SkipWhite; end; |
Обратите внимание, что эта процедура оставляет свой результат в глобальной строковой переменной Value.
Затем, мы должны изменить каждую обращение к GetName чтобы отразить ее новую форму. Они происходят в Factor, Assignment и Decl:
Code: |
{---------------------------------------------------------------} { Parse and Translate a Math Factor } procedure BoolExpression; Forward; procedure Factor; begin if Look = '(' then begin Match('('); BoolExpression; Match(')'); end else if IsAlpha(Look) then begin GetName; LoadVar(Value[1]); end else LoadConst(GetNum); end;
. .
{ Parse and Translate an Assignment Statement } procedure Assignment; var Name: char; begin Name := Value[1]; Match('='); BoolExpression; Store(Name); end; {---------------------------------------------------------------} . .
{ Parse and Translate a Data Declaration } procedure Decl; begin GetName; Alloc(Value[1]); while Look = ',' do begin Match(','); GetName; Alloc(Value[1]); end; end; |
(Заметьте, что мы все еще разрешаем только одно-символьные имена переменных поэтому мы используем здесь простое решение и просто используем первый символ строки.)
Наконец, мы должны внести изменения, позволяющие использовать Token вместо Look как символа для проверки и вызывать Scan в подходящих местах. По большей части это включает удаление вызовов Match, редкие замены вызовов Match на вызовы MatchString, и замену вызовов NewLine на вызовы Scan. Вот затронутые подпрограммы:
Code: |
{---------------------------------------------------------------} { Recognize and Translate an IF Construct } procedure Block; Forward; procedure DoIf; var L1, L2: string; begin BoolExpression; L1 := NewLabel; L2 := L1; BranchFalse(L1); Block; if Token = 'l' then begin L2 := NewLabel; Branch(L2); PostLabel(L1); Block; end; PostLabel(L2); MatchString('ENDIF'); end;
{ Parse and Translate a WHILE Statement } procedure DoWhile; var L1, L2: string; begin L1 := NewLabel; L2 := NewLabel; PostLabel(L1); BoolExpression; BranchFalse(L2); Block; MatchString('ENDWHILE'); Branch(L1); PostLabel(L2); end;
{ Parse and Translate a Block of Statements } procedure Block; begin Scan; while not(Token in ['e', 'l']) do begin case Token of 'i': DoIf; 'w': DoWhile; else Assignment; end; Scan; end; end;
{ Parse and Translate Global Declarations } procedure TopDecls; begin Scan; while Token <> 'b' do begin case Token of 'v': Decl; else Abort('Unrecognized Keyword ' + Value); end; Scan; end; end;
{ Parse and Translate a Main Program } procedure Main; begin MatchString('BEGIN'); Prolog; Block; MatchString('END'); Epilog; end;
{ Parse and Translate a Program } procedure Prog; begin MatchString('PROGRAM'); Header; TopDecls; Main; Match('.'); end;
{ Initialize } procedure Init; var i: char; begin for i := 'A' to 'Z' do ST[i] := ' '; GetChar; Scan; end; |
Это должно работать. Если все изменения сделаны правильно, вы должны теперь анализировать программы, которые выглядят как программы. (Если вы не сделали всех изменений, не отчаивайтесь. Полный листинг конечной формы дан ниже.)
Работает? Если да, то мы почти дома. Фактически, с несколькими небольшими исключениями, мы уже получили компилятор, пригодный для использования. Имеются еще несколько областей, требующих усовершенствования.
МНОГОСИМВОЛЬНЫЕ ИМЕНА ПЕРЕМЕННЫХ
Одна из них - ограничение, требующее использования одно-символьных имен переменных. Теперь, когда мы можем обрабатывать много символьные ключевые слова, это ограничение начинает казаться произвольным и ненужным. И действительно это так. В основном, единственное его достоинство в том, что он позволяет получить тривиально простую реализацию таблицы идентификаторов. Но это просто удобство для создателей компиляторов и оно должно быть уничтожено.
Мы уже делали этот шаг прежде. На этот раз, как обычно, я сделаю это немного по-другому. Я думаю подход, примененный здесь, сохранит простоту настолько, насколько это возможно.
Естественным путем реализации таблицы идентификаторов на Pascal является объявление переменной типа запись и создание таблицы идентификаторов как массива таких записей. Здесь, однако, нам в действительности пока не нужно поле типа (существует пока что только один разрешенный тип), так что нам нужен только массив символов. Это имеет свое преимущество, потому что мы можем использовать существующую процедуру Lookup для поиска в таблице идентификаторов также как и в списке ключевых слов. Оказывается, даже когда нам нужны больше полей, мы все равно можем использовать тот же самый подход, просто сохраняя другие поля в отдельных массивах.
Вот изменения, которые необходимо сделать. С начала добавьте новую типизированную константу:
NEntry: integer = 0;
Затем измените определение таблицы идентификаторов как показано ниже:
const MaxEntry = 100;
var ST : array[1..MaxEntry] of Symbol;
(Обратите внимание, что ST не объявлен как SymTab. Это объявление липовое, чтобы заставить Lookup работать. SymTab заняли бы слишком много памяти и поэтому фактически никогда не объявляется).
Затем мы должны заменить InTable.
Code: |
{ Look for Symbol in Table } function InTable(n: Symbol): Boolean; begin InTable := Lookup(@ST, n, MaxEntry) <> 0; end; |
Нам также необходима новая процедура AddEntry, которая добавляет новый элемент в таблицу:
Code: |
{ Add a New Entry to Symbol Table } procedure AddEntry(N: Symbol; T: char); begin if InTable(N) then Abort('Duplicate Identifier ' + N); if NEntry = MaxEntry then Abort('Symbol Table Full'); Inc(NEntry); ST[NEntry] := N; SType[NEntry] := T; end; |
Эта процедура вызывается из Alloc:
Code: |
{ Allocate Storage for a Variable } procedure Alloc(N: Symbol); begin if InTable(N) then Abort('Duplicate Variable Name ' + N); AddEntry(N, 'v'); . . . |
Наконец, мы должны изменить все подпрограммы, которые в настоящее время обрабатывают имена переменных как одиночный символ. Они включают LoadVar и Store (просто измените тип с char на string) и Factor, Assignment и Decl (просто измените Value[1] на Value).
Последняя вещь: измените процедуру Init для очистки массива как показано ниже:
Code: |
{ Initialize } procedure Init; var i: integer; begin for i := 1 to MaxEntry do begin ST[i] := ''; SType[i] := ' '; end; GetChar; Scan; end; |
Это должно работать. Испытайте ее и проверьте, что вы действительно можете использовать много символьные имена переменных.
СНОВА ОПЕРАТОРЫ ОТНОШЕНИЙ
У нас осталось последнее одно-символьное ограничение - ограничение операторов отношений. Некоторые из операторов отношений действительно состоят из одиночных символов, но другие требуют двух. Это '<=' и '>='. Я также предпочитаю Паскалевское '<>' для "не равно" вместо '#'.
Как вы помните, в главе 7 я указал, что стандартный способ работы с операторами отношений - включить их в список ключевых слов и позволить лексическому анализатору отыскивать их. Но, опять, это требует выполнение полного анализа выражения, тогда как до этого мы у нас была возможность ограничить использование сканера началом утверждения.
Я упомянул тогда, что мы все же можем избежать неприятностей с этим, так как много символьных операторов отношений немного и они ограничены в применении. Было бы легко обрабатывать их просто как специальные случаи и поддерживать их специальным способом.
Требуемые изменения влияют только на подпрограммы генерации кода и процедуры Relation и ее друзей. С начала, нам понадобятся еще две подпрограммы генерации кода:
Code: |
{---------------------------------------------------------------} { Set D0 If Compare was <= } procedure SetLessOrEqual; begin EmitLn('SGE D0'); EmitLn('EXT D0'); end; {---------------------------------------------------------------} { Set D0 If Compare was >= } procedure SetGreaterOrEqual; begin EmitLn('SLE D0'); EmitLn('EXT D0'); end; {---------------------------------------------------------------} |
Затем измените подпрограммы анализа отношений как показано ниже:
Code: |
{---------------------------------------------------------------} { Recognize and Translate a Relational "Less Than or Equal" } procedure LessOrEqual; begin Match('='); Expression; PopCompare; SetLessOrEqual; end; {---------------------------------------------------------------} { Recognize and Translate a Relational "Not Equals" } procedure NotEqual; begin Match('>'); Expression; PopCompare; SetNEqual; end; {---------------------------------------------------------------} { Recognize and Translate a Relational "Less Than" } procedure Less; begin Match('<'); case Look of '=': LessOrEqual; '>': NotEqual; else begin Expression; PopCompare; SetLess; end; end; end; {---------------------------------------------------------------} { Recognize and Translate a Relational "Greater Than" } procedure Greater; begin Match('>'); if Look = '=' then begin Match('='); Expression; PopCompare; SetGreaterOrEqual; end else begin Expression; PopCompare; SetGreater; end; end; {---------------------------------------------------------------} |
Это все, что требуется. Теперь вы можете обрабатывать все операторы отношений. Попробуйте.
ВВОД/ВЫВОД
Теперь у нас есть полный, работающий язык, за исключением одного небольшого смущающего факта: у нас нет никакого способа получить или вывести данные. Нам нужны подпрограммы ввода/вывода.
Современное соглашение, установленное в C и продолженное в Ada и Modula-2, состоит в том, чтобы вывести I/O операторы из самого языка и просто включить их в библиотеку подпрограмм. Это было бы прекрасно, за исключением того, что мы пока не имеем никаких средств поддержки подпрограмм. В любом случае, с этим подходом вы столкнетесь с проблемой переменной длины списка параметров. В Паскале I/O операторы встроены в язык, поэтому это единственные операторы, для которых список параметров может иметь переменное число элементов. В C мы примиряемся с клуджами типа scanf и printf и должны передавать количество параметров в вызываемую процедуру. В Ada и Modula-2 мы должны использовать неудобный (и медленный!) способ отдельного вызова для каждого аргумента.
Так что я думаю, что предпочитаю Паскалевский подход встраивания подпрограмм ввода/вывода, даже если мы не нуждаемся в этом.
Как обычно, для этого нам нужны еще несколько подпрограмм генерации кода. Они, оказывается, самые простые из всех, потому что все, что мы делаем это вызываем библиотечные процедуры для выполнения работы.
Code: |
{---------------------------------------------------------------} { Read Variable to Primary Register } procedure ReadVar; begin EmitLn('BSR READ'); Store(Value); end; {---------------------------------------------------------------} { Write Variable from Primary Register } procedure WriteVar; begin EmitLn('BSR WRITE'); end; |
Идея состоит в том, что READ загружает значение из входного потока в D0, а WRITE выводит его оттуда.
Эти две процедуры представляют собой нашу первую встречу с потребностью в библиотечных процедурах... компонентах Run Time Library (RTL). Конечно кто-то (а именно мы) должен написать эти подпрограммы, но они не являются непосредственно частью компилятора. Я даже не буду беспокоиться о том, чтобы показать здесь эти подпрограммы, так как они очевидно очень ОС-зависимы. Я просто скажу, что для SK*DOS они особенно просты... почти тривиальны. Одна из причин, по которым я не буду показывать их здесь в том, что вы можете добавлять новые виды возможностей, например приглашение в READ или возможность пользователю повторить ошибочный ввод.
Но это действительно отдельный от компилятора проект, так что теперь я буду подразумевать что библиотека, называемая TINYLIB.LIB, существует.
Так как нам теперь нужно загружать ее, мы должны добавить ее загрузку в процедуру Header:
Code: |
{ Write Header Info } procedure Header; begin WriteLn('WARMST', TAB, 'EQU $A01E'); EmitLn('LIB TINYLIB'); end; |
Она возьмет на себя эту часть работы. Теперь нам также необходимо распознавать команды ввода и вывода. Мы можем сделать это добавив еще два ключевых слова в наш список:
Code: |
{ Definition of Keywords and Token Types } const NKW = 11; NKW1 = 12; const KWlist: array[1..NKW] of Symbol = ('IF', 'ELSE', 'ENDIF', 'WHILE', 'ENDWHILE', 'READ', 'WRITE', 'VAR', 'BEGIN', 'END', 'PROGRAM'); const KWcode: string[NKW1] = 'xileweRWvbep'; |
(Обратите внимание, что здесь я использую кода в верхнем регистре чтобы избежать конфликта с 'w' из WHILE.)
Затем нам нужны процедуры для обработки оператора ввода/вывода и его списка параметров:
Code: |
{ Process a Read Statement } procedure DoRead; begin Match('('); GetName; ReadVar; while Look = ',' do begin Match(','); GetName; ReadVar; end; Match(')'); end;
{ Process a Write Statement } procedure DoWrite; begin Match('('); Expression; WriteVar; while Look = ',' do begin Match(','); Expression; WriteVar; end; Match(')'); end; |
Наконец, мы должны расширить процедуру Block для поддержки новых типов операторов:
Code: |
{ Parse and Translate a Block of Statements } procedure Block; begin Scan; while not(Token in ['e', 'l']) do begin case Token of 'i': DoIf; 'w': DoWhile; 'R': DoRead; 'W': DoWrite; else Assignment; end; Scan; end; end;
|
На этом все. Теперь у нас есть язык!
ЗАКЛЮЧЕНИЕ
К этому моменту мы полностью определили TINY. Он не слишком значителен... в действительности игрушечный компилятор. TINY имеет только один тип данных и не имеет подпрограмм... но это законченный, пригодный для использования язык. Пока что вы не имеете возможности написать на нем другой компилятор или сделать что-нибудь еще очень серьезное, но вы могли бы писать программы для чтения входных данных, выполнения вычислений и вывода результатов. Не слишком плохо для игрушки.
Более важно, что мы имеем твердую основу для дальнейшего развития. Я знаю, что вы будете рады слышать это: в последний раз я начал с создания синтаксического анализатора заново... с этого момента я предполагаю просто добавлять возможности в TINY пока он не превратится в KISS. Ох, будет время, когда нам понадобится попробовать некоторые вещи с новыми копиями Cradle, но как только мы разузнаем как они делаются, они будут встроены в TINY.
Какие это будут возможности? Хорошо, для начала нам понадобятся подпрограммы и функции. Затем нам нужна возможность обрабатывать различные типы, включая массивы, строки и другие структуры. Затем нам нужно работать с идеей указателей. Все это будет в следующих главах.
Увидимся.
В справочных целях полный листинг TINY версии 1.0 показан ниже:
Code: |
program Tiny10;
{ Constant Declarations } const TAB = ^I; CR = ^M; LF = ^J; LCount: integer = 0; NEntry: integer = 0;
{ 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 }
const MaxEntry = 100; var ST : array[1..MaxEntry] of Symbol; SType: array[1..MaxEntry] of char;
{ Definition of Keywords and Token Types } const NKW = 11; NKW1 = 12; const KWlist: array[1..NKW] of Symbol = ('IF', 'ELSE', 'ENDIF', 'WHILE', 'ENDWHILE', 'READ', 'WRITE', 'VAR', 'BEGIN', 'END', 'PROGRAM'); const KWcode: string[NKW1] = 'xileweRWvbep';
{ Read New Character From Input Stream } procedure GetChar; begin Read(Look); end;
{ Report an Error } procedure Error(s: string); begin WriteLn; WriteLn(^G, 'Error: ', s, '.'); end;
{ Report Error and Halt } procedure Abort(s: string); begin Error(s); Halt; end;
{ Report What Was Expected } procedure Expected(s: string); begin Abort(s + ' Expected'); end;
{ Report an Undefined Identifier } procedure Undefined(n: string); begin Abort('Undefined Identifier ' + n); end;
{ Recognize an Alpha Character } function IsAlpha(c: char): boolean; begin IsAlpha := UpCase(c) in ['A'..'Z']; end;
{ Recognize a Decimal Digit } 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 Addop } function IsAddop(c: char): boolean; begin IsAddop := c in ['+', '-']; end;
{ Recognize a Mulop } function IsMulop(c: char): boolean; begin IsMulop := c in ['*', '/']; end;
{ 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;
{ Recognize White Space } function IsWhite(c: char): boolean; begin IsWhite := c in [' ', TAB]; end;
{ Skip Over Leading White Space } procedure SkipWhite; begin while IsWhite(Look) do GetChar; end;
{ Skip Over an End-of-Line } procedure NewLine; begin while Look = CR do begin GetChar; if Look = LF then GetChar; SkipWhite; end; end;
{ Match a Specific Input Character } procedure Match(x: char); begin NewLine; if Look = x then GetChar else Expected('''' + x + ''''); SkipWhite; end;
{ 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 dec(i); Lookup := i; end;
{ Locate a Symbol in Table } { Returns the index of the entry. Zero if not present. } function Locate(N: Symbol): integer; begin Locate := Lookup(@ST, n, MaxEntry); end;
{ Look for Symbol in Table } function InTable(n: Symbol): Boolean; begin InTable := Lookup(@ST, n, MaxEntry) <> 0; end;
{ Add a New Entry to Symbol Table } procedure AddEntry(N: Symbol; T: char); begin if InTable(N) then Abort('Duplicate Identifier ' + N); if NEntry = MaxEntry then Abort('Symbol Table Full'); Inc(NEntry); ST[NEntry] := N; SType[NEntry] := T; end;
{ Get an Identifier } procedure GetName; begin NewLine; if not IsAlpha(Look) then Expected('Name'); Value := ''; while IsAlNum(Look) do begin Value := Value + UpCase(Look); GetChar; end; SkipWhite; end;
{ Get a Number } function GetNum: integer; var Val: integer; begin NewLine; 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;
{ Get an Identifier and Scan it for Keywords } procedure Scan; begin GetName; Token := KWcode[Lookup(Addr(KWlist), Value, NKW) + 1]; end;
{ Match a Specific Input String } procedure MatchString(x: string); begin if Value <> x then Expected('''' + x + ''''); end;
{ Output a String with Tab } procedure Emit(s: string); begin Write(TAB, s); end;
{ Output a String with Tab and CRLF } procedure EmitLn(s: string); begin Emit(s); WriteLn; end;
{ Generate a Unique Label } function NewLabel: string; var S: string; begin Str(LCount, S); NewLabel := 'L' + S; Inc(LCount); end;
{ Post a Label To Output } procedure PostLabel(L: string); begin WriteLn(L, ':'); end;
{---------------------------------------------------------------} { Clear the Primary Register } procedure Clear; begin EmitLn('CLR D0'); end;
{---------------------------------------------------------------} { Negate the Primary Register } procedure Negate; begin EmitLn('NEG D0'); end;
{---------------------------------------------------------------} { Complement the Primary Register } procedure NotIt; begin EmitLn('NOT 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: string); 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;
{---------------------------------------------------------------} { 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;
{---------------------------------------------------------------} { Set D0 If Compare was <= } procedure SetLessOrEqual; begin EmitLn('SGE D0'); EmitLn('EXT D0'); end;
{---------------------------------------------------------------} { Set D0 If Compare was >= } procedure SetGreaterOrEqual; begin EmitLn('SLE D0'); EmitLn('EXT D0'); end;
{---------------------------------------------------------------} { Store Primary to Variable } procedure Store(Name: string); begin if not InTable(Name) then Undefined(Name); EmitLn('LEA ' + Name + '(PC),A0'); EmitLn('MOVE D0,(A0)') end;
{---------------------------------------------------------------} { 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;
{---------------------------------------------------------------} { Read Variable to Primary Register } procedure ReadVar; begin EmitLn('BSR READ'); Store(Value[1]); end;
{ Write Variable from Primary Register } procedure WriteVar; begin EmitLn('BSR WRITE'); end;
{ Write Header Info } procedure Header; begin WriteLn('WARMST', TAB, 'EQU $A01E'); end;
{ Write the Prolog } procedure Prolog; begin PostLabel('MAIN'); end;
{ Write the Epilog } procedure Epilog; begin EmitLn('DC WARMST'); EmitLn('END MAIN'); end;
{---------------------------------------------------------------} { Parse and Translate a Math Factor } procedure BoolExpression; Forward; procedure Factor; begin if Look = '(' then begin Match('('); BoolExpression; Match(')'); end else if IsAlpha(Look) then begin GetName; LoadVar(Value); end 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;
{---------------------------------------------------------------} { Recognize and Translate a Relational "Equals" } procedure Equal; begin Match('='); Expression; PopCompare; SetEqual; end;
{---------------------------------------------------------------} { Recognize and Translate a Relational "Less Than or Equal" } procedure LessOrEqual; begin Match('='); Expression; PopCompare; SetLessOrEqual; end;
{---------------------------------------------------------------} { Recognize and Translate a Relational "Not Equals" } procedure NotEqual; begin Match('>'); Expression; PopCompare; SetNEqual; end;
{---------------------------------------------------------------} { Recognize and Translate a Relational "Less Than" } procedure Less; begin Match('<'); case Look of '=': LessOrEqual; '>': NotEqual; else begin Expression; PopCompare; SetLess; end; end; end;
{---------------------------------------------------------------} { Recognize and Translate a Relational "Greater Than" } procedure Greater; begin Match('>'); if Look = '=' then begin Match('='); Expression; PopCompare; SetGreaterOrEqual; end else begin Expression; PopCompare; SetGreater; end; end;
{---------------------------------------------------------------} { Parse and Translate a Relation }
procedure Relation; begin Expression; if IsRelop(Look) then begin Push; case Look of '=': Equal; '<': 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;
{ Parse and Translate an Assignment Statement } procedure Assignment; var Name: string; begin Name := Value; Match('='); BoolExpression; Store(Name); end;
{---------------------------------------------------------------} { Recognize and Translate an IF Construct } procedure Block; Forward;
procedure DoIf; var L1, L2: string; begin BoolExpression; L1 := NewLabel; L2 := L1; BranchFalse(L1); Block; if Token = 'l' then begin L2 := NewLabel; Branch(L2); PostLabel(L1); Block; end; PostLabel(L2); MatchString('ENDIF'); end;
{ Parse and Translate a WHILE Statement } procedure DoWhile; var L1, L2: string; begin L1 := NewLabel; L2 := NewLabel; PostLabel(L1); BoolExpression; BranchFalse(L2); Block; MatchString('ENDWHILE'); Branch(L1); PostLabel(L2); end;
{ Process a Read Statement } procedure DoRead; begin Match('('); GetName; ReadVar; while Look = ',' do begin Match(','); GetName; ReadVar; end; Match(')'); end;
{ Process a Write Statement } procedure DoWrite; begin Match('('); Expression; WriteVar; while Look = ',' do begin Match(','); Expression; WriteVar; end; Match(')'); end;
{ Parse and Translate a Block of Statements } procedure Block; begin Scan; while not(Token in ['e', 'l']) do begin case Token of 'i': DoIf; 'w': DoWhile; 'R': DoRead; 'W': DoWrite; else Assignment; end; Scan; end; end;
{ Allocate Storage for a Variable } procedure Alloc(N: Symbol); begin if InTable(N) then Abort('Duplicate Variable Name ' + N); AddEntry(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;
{ Parse and Translate a Data Declaration } procedure Decl; begin GetName; Alloc(Value); while Look = ',' do begin Match(','); GetName; Alloc(Value); end; end;
{ Parse and Translate Global Declarations } procedure TopDecls; begin Scan; while Token <> 'b' do begin case Token of 'v': Decl; else Abort('Unrecognized Keyword ' + Value); end; Scan; end; end;
{ Parse and Translate a Main Program } procedure Main; begin MatchString('BEGIN'); Prolog; Block; MatchString('END'); Epilog; end;
{ Parse and Translate a Program } procedure Prog; begin MatchString('PROGRAM'); Header; TopDecls; Main; Match('.'); end;
{ Initialize } procedure Init; var i: integer; begin for i := 1 to MaxEntry do begin ST[i] := ''; SType[i] := ' '; end; GetChar; Scan; end;
{ Main Program } begin Init; Prog; if Look <> CR then Abort('Unexpected data after ''.'''); end. |
Просьба писать ваши замечания, наблюдения и все остальное,
что поможет улучшить предоставляемую информацию на этом сайте.
ВСЕ КОММЕНТАРИИ МОДЕРИРУЮТСЯ ВРУЧНУЮ, ТАК ЧТО СПАМИТЬ БЕСПОЛЕЗНО!