ВВЕДЕНИЕ В четырех первых главах этой
серии мы сконцентрировали свое внимание на синтаксическом анализе математических
выражений и операций присваивания. В этой главе мы остановимся на новой
и захватывающей теме: синтаксическом анализе и трансляции управляющих конструкций
таких как, например, операторы IF.
ПЛАН Далее мы снова начнем с пустого
Cradle и, как мы делали уже дважды до этого, будем строить программу последовательно.
Мы также сохраним концепцию односимвольных токенов, которая так хорошо
служила нам до настоящего времени. Это означает, что "код" будет выглядеть
немного забавным с "i" вместо IF, "w" вместо WHILE и т.д. Но это поможет
нам узнать основные понятия не беспокоясь о лексическом анализе.
Не бойтесь... в конечном счете мы увидим что-то похожее на "настоящий" код.
{--------------------------------------------------------------}
procedure Other;
Теперь включим ее вызов в основную программу таким образом: {--------------------------------------------------------------}
begin
Запустите программу и посмотрите,
что вы получили. Не очень захватывающе, не так ли? Но не зацикливайтесь
на этом, это только начало, результат будет лучше.
<program> ::= <block> END <block> ::= [ <statement> ]* Это означает, что программа определена
как блок, завершаемый утверждением END. Блок, в свою очередь, состоит из
нуля или более операторов. Пока у нас есть только один вид операторов.
{--------------------------------------------------------------}
procedure DoProgram;
Обратите внимание, что я выдаю
ассемблеру команду "END", что своего рода расставляет знаки препинания
в выходном коде и заставляет чувствовать, что мы анализируем здесь законченную
программу.
{--------------------------------------------------------------}
procedure Block;
(Из формы процедуры вы видите, что мы собираемся постепенно
ее расширять!)
НЕМНОГО ОСНОВ Прежде чем мы начнем определять различные управляющие конструкции, мы должны положить немного более прочное основание. Во-первых, предупреждаю: я не буду использовать для этих конструкций тот же самый синтаксис с которым вы знакомы по Паскалю или Си. К примеру синтаксис Паскаль для IF такой: IF <condition> THEN <statement> (где <statement>, конечно, может быть составным.)
IF ( <condition> ) <statement> Вместо этого я буду использовать нечто более похожее на Ada: IF <condition> <block> ENDIF Другими словами, конструкция IF имеет специфический символ
завершения. Это позволит избежать висячих else Паскаля и Си и также предотвращает
необходимость использовать скобки {} или begin-end. Синтаксис, который
я вам здесь показываю, фактически является синтаксисом языка KISS, который
я буду детализировать в следующих главах. Другие конструкции также будут
немного отличаться. Это не должно быть для вас большой проблемой. Как только
вы увидите, как это делается, вы поймете, что в действительности не имеет
большого значения, какой конкретный синтаксис используется. Как только
синтаксис определен, включить его в код достаточно просто.
IF <condition> A ENDIF B... должен быть переведен в:
Если условие не выполнено то переход на L
Ясно, что нам понадобятся несколько
процедур, которые помогут нам работать с этими переходами. Ниже я определил
две из них. Процедура NewLabel генерирует уникальные метки. Это сделано
с помощью простого способа называть каждую метку 'Lnn', где nn - это номер
метки, начинающийся с нуля. Процедура PostLabel просто выводит метки в
соответствующем месте.
{--------------------------------------------------------------}
function NewLabel: string;
{--------------------------------------------------------------}
procedure PostLabel(L: string);
Заметьте, что мы добавили новую глобальную переменную LCount, так что вы должны изменить раздел описания переменных в начале программы, следующим образом: var Look : char;
{ Lookahead Character }
Также добавьте следующий дополнительный инициализирующий код в Init: LCount := 0; (Не забудьте сделать это, иначе ваши метки будут выглядеть
действительно странными!).
IF: Сначала получить условие
и выдать код для него. Затем создать уникальную метку и выдать переход
если условие ложно.
Эти действия могут быть показаны очень кратко, если мы запишем синтаксис таким образом: IF
Это пример синтаксически-управляемого
перевода. Мы уже делали все это... мы просто никогда прежде не записывали
это таким образом. Содержимое фигурных скобок представляет собой действия,
которые будут выполняться. Хорошо в этом способе представления то, что
он не только показывает что мы должны распознать, но также и действия,
которые мы должны выполнить и в каком порядке. Как только мы получаем такой
синтаксис, код возникает почти сам собой.
BEQ <=> Переход если
ложь
По природе вещей большинство ветвлений, которые мы увидим, будут BEQ... мы будем обходить вокруг кода, который должен выполняться когда условие истинно. ОПЕРАТОР IF После этого небольшого пояснения
метода мы наконец готовы начать программирование синтаксического анализатора
для условного оператора. Фактически, мы уже почти сделали это! Как обычно
я буду использовать наш односимвольный подход, с символом "i" вместо "IF"
и "e" вместо "ENDIF" (также как и END... это двойственная природа не вызывает
никакого беспорядка). Я также пока полностью пропущу символ для условия
ветвления, который мы все еще должны определить.
{--------------------------------------------------------------}
procedure Block; Forward; procedure DoIf;
Добавьте эту подпрограмму в вашу программу и измените Block так, чтобы он ссылался на нее как показано ниже: {--------------------------------------------------------------}
procedure Block;
Обратите внимание на обращение к процедуре Condition. В конечном итоге мы напишем подпрограмму, которая сможет анализировать и транслировать любое логическое условие которое мы ей дадим. Но это уже тема для отдельной главы (фактически следующей). А сейчас давайте просто заменим ее макетом, который выдает некоторый текст. Напишите следующую подпрограмму: {--------------------------------------------------------------}
Procedure Condition;
Вставьте эту процедуру в вашу программу как раз перед DoIf. Теперь запустите программу. Испробуйте строку типа: aibece Как вы можете видеть, синтаксический анализатор, кажется, распознает конструкцию и вставляет объектный код в правильных местах. Теперь попробуйте набор вложенных IF: aibicedefe Он начинает все более походить
на настоящий, не так ли?
IF <condition> <block> [ ELSE <block>] ENDIF Сложность возникает просто потому,
что здесь присутствует необязательное условие, которого нет в других конструкциях.
<condition>
Это приводит нас к следующей синтаксически управляемой схеме перевода: IF
Сравнение этого со случаем IF без ELSE дает нам понимание того, как обрабатывать обе эти ситуации. Код ниже выполняет это. (Обратите внимание, что использую "l" вместо "ELSE" так как "e" имеет другое назначение): {--------------------------------------------------------------}
procedure DoIf;
Вы получили его. Законченый анализатор/транслятор
в 19 строк кода.
aiblcede Работает? Теперь, только для того, чтобы убедиться, что мы ничего не испортили и случай с IF без ELSE тоже будет обрабатываться, введите aibece Теперь испробуйте несколько вложенных IF. Испытайте что-нибудь на ваш выбор, включая несколько неправильных утверждений. Только запомните, что 'e' не является допустимым оператором "other". ОПЕРАТОР WHILE Следующий вид оператора должен быть простым, так как мы уже имеем опыт. Синтаксис, который я выбрал для оператора WHILE следующий: WHILE <condition> <block> ENDWHILE Знаю, знаю, мы действительно
не нуждаемся в отдельных видах ограничителей для каждой конструкции... вы
можете видеть, что фактически в нашей односимвольной версии 'e' используется
для всех из них. Но я также помню множество сессий отладки в Паскале, пытаясь
отследить своенравный END который по мнению компилятора я хотел поместить
где-нибудь еще. По своему опыту знаю, что специфичные и уникальные ключевые
слова, хотя они и добавляются к словарю языка, дают небольшую защиту от
ошибок, которая стоит дополнительной работы создателей компиляторов.
L1: <condition>
Как и прежде, сравнение этих двух представлений дает нам действия, необходимые на каждом этапе: WHILE
{ L1 = NewLabel;
Код выходит непосредственно из синтаксиса: {--------------------------------------------------------------}
procedure DoWhile;
Так как мы получили новый оператор, мы должны добавить его вызов в процедуру Block: {--------------------------------------------------------------}
procedure Block;
Никаких других изменений не требуется.
ОПЕРАТОР LOOP Мы могли бы остановиться на этом
и иметь работающий язык. Много раз было показано, что языка высокого уровня
всего с двумя конструкциями IF и WHILE достаточно для написания структурного
кода. Но раз уж мы начали, то давайте немного расширим репертуар.
LOOP <block> ENDLOOP Синтаксически управляемый перевод: LOOP
{ L = NewLabel;
Соответствующий код показан ниже. Так как мы уже использовали "l" для ELSE на этот раз я использовал последнюю букву "p" как "ключевое слово". {--------------------------------------------------------------}
procedure DoLoop;
После того, как вы вставите эту подпрограмму, не забудьте добавить строчку в Block для ее вызова. REPEAT-UNTIL Имеется одна конструкция, которую я взял напрямую из Паскаля. Синтаксис: REPEAT <block> UNTIL <condition> и синтаксически-управляемый перевод: REPEAT
{ L = NewLabel;
Как обычно, код вытекает отсюда довольно легко: {--------------------------------------------------------------}
procedure DoRepeat;
Как и прежде, мы должны добавить вызов DoRepeat в Block. Хотя на этот раз есть различия. Я решил использовать "r" вместо REPEAT (естественно), но я также решил использовать "u" вместо UNTIL. Это означает, что "u" должен быть добавлен к множеству символов в условии while. Это символы, которые сигнализируют о выходе из текущего блока... символы "follow", на жаргоне разработчиков компиляторов. {--------------------------------------------------------------}
procedure Block;
ЦИКЛ FOR Цикл FOR очень удобен, но он
тяжел для трансляции. Не столько потому, что сама конструкция трудна... в
конце концов это всего лишь цикл... но просто потому, что она трудна для
реализации на ассемблере. Как только код придуман, трансляция достаточно
проста.
FOR <ident> = <expr1> TO <expr2> <block> ENDFOR Сложность трансляции цикла "FOR" зависит от выбранного вами способа его реализации, от пути, которым вы решили определять правила обработки ограничений. Рассчитывается ли expr2 каждый раз при прохождении цикла, например, или оно обрабатывается как постоянное ограничение? Всегда ли вы проходите цикл хотя бы раз, как в Fortran, или нет. Все становится проще, если вы приверженец точки зрения что эта конструкция эквивалентна: <ident> = <expr1>
Заметьте, что с этим определением цикла <block> не
будет выполнен вообще если <expr1> изначально больше чем <expr2>.
<ident> ; получить имя счетчика цикла
Ничего себе! Это же куча кода...
строка, содержащая <block> кажется совсем потерявшейся. Но это лучшее
из того, что я смог придумать. Я полагаю, чтобы вам помочь, вы должны иметь
в виду что в действительности это всего лишь шестнадцать слов, в конце
концов. Если кто-нибудь сможет оптимизировать это лучше, пожалуйста дайте
мне знать.
{--------------------------------------------------------------}
procedure DoFor;
Так как в этой версии синтаксического анализатора у нас нет выражений, я использовал тот же самый прием что и для Condition и написал подпрограмму: {--------------------------------------------------------------}
Procedure Expression;
Испытайте его. Снова, не забудьте добавить вызов в Block. Так как у нас нет возможности ввода для фиктивной версии Expression, типичная входная строка будет выглядеть так: afi=bece Хорошо, генерируется много кода, не так ли? Но, по крайней мере, это правильный код. ОПЕРАТОР DO Из-за всего этого мне захотелось
иметь более простую версию цикла FOR. Причина появления всего этого кода
выше состоит в необходимости иметь счетчик цикла, доступный как переменная
внутри цикла. Если все, что нам нужно это считающий цикл, позволяющий нам
выполнить что-то определенное число раз, но не нужен непосредственный доступ
к счетчику, имеется более простое решение. Процессор 68000 имеет встроенную
команду "уменьшить и переход если не ноль", которая является идеальной
для подсчета. Для полноты давайте добавим и эту конструкцию. Это будет
наш последний цикл.
DO
Это гораздо проще! Цикл будет выполняться <expr> раз. Вот код: {--------------------------------------------------------------}
procedure Dodo;
Я думаю вы согласитесь, что это гораздо проще, чем классический цикл FOR. Однако, каждая конструкция имеет свое назначение. ОПЕРАТОР BREAK Ранее я обещал вам оператор BREAK
для сопровождения цикла LOOP. Им я в некотором роде горд. На первый взгляд
BREAK кажется действительно сложным. Моим первым подходом было просто использовать
его как дополнительный ограничитель в Block и разделить все циклы на две
части точно также как я сделал это для ELSE оператора IF. Но, оказывается,
это не работает, потому что оператор BREAK редко находится на том же самом
уровне, что и сам цикл. Наиболее вероятное место для BREAK - сразу после
IF, что приводило бы к выходу из конструкции IF, а не из окружающего цикла.
Неправильно. BREAK должен выходить из внутреннего LOOP даже если он вложен
в несколько уровней IF.
{--------------------------------------------------------------}
procedure DoLoop;
Заметьте, что теперь DoLoop имеет
две метки а не одну. Вторая дает команде BREAK адрес перехода Если
в цикле нет BREAK, то мы зря потратили метку и немного загромоздили код,
но не нанесли никакого вреда.
{--------------------------------------------------------------}
procedure Block(L: string);
Снова заметьте, что все что Block
делает с меткой это передает ее в DoIf и DoBreak. Циклы не нуждаются в
ней, потому что они в любом случае передают свою собственную метку.
{--------------------------------------------------------------}
procedure Block(L: string); Forward; procedure DoIf(L: string);
Здесь единственное, что изменяется,
это добавляется параметр у процедуры Block. Оператор IF не меняет уровень
вложенности цикла, поэтому DoIf просто передает метку дальше. Независимо
от того, сколько уровней вложенности IF мы имеем, будет использоваться
та же самая метка.
{--------------------------------------------------------------}
procedure DoBreak(L: string);
{--------------------------------------------------------------} { Parse and Translate a Program } procedure DoProgram;
Этот код позаботится почти обо
всем. Испытайте его, посмотрите, сможете ли вы "сломать" ("break") его
(каламбур). Аккуратней однако. К настоящему времени мы использовали
так много букв, что трудно придумать символ, который не представляет сейчас
какое либо зарезервированное слово. Не забудьте, перед тем, как вы протестируете
программу, вы должны будете исправить каждый случай появления Block в других
циклах для включения нового параметра. Сделайте это точно так же, как я
сделал это для LOOP.
{--------------------------------------------------------------}
procedure Dodo;
Две дополнительные инструкции SUBQ и ADDQ заботятся о сохранении стека в правильной форме. ЗАКЛЮЧЕНИЕ К этому моменту мы создали ряд
управляющих конструкций... в действительности более богатый набор чем предоставляет
почти любой другой язык программирования. И, за исключением цикла FOR,
это было довольно легко сделать. Но даже этот цикл был сложен только потому,
что сложность заключалась в ассемблере.
{--------------------------------------------------------------}
{--------------------------------------------------------------}
const TAB = ^I;
{--------------------------------------------------------------}
var Look : char;
{ Lookahead Character }
{--------------------------------------------------------------}
procedure GetChar;
{--------------------------------------------------------------}
procedure Error(s: string);
{--------------------------------------------------------------}
procedure Abort(s: string);
{--------------------------------------------------------------}
procedure Expected(s: string);
{--------------------------------------------------------------}
procedure Match(x: char);
{--------------------------------------------------------------}
function IsAlpha(c: char): boolean;
{--------------------------------------------------------------}
function IsDigit(c: char): boolean;
{--------------------------------------------------------------}
function IsAddop(c: char): boolean;
{--------------------------------------------------------------}
function IsWhite(c: char): boolean;
{--------------------------------------------------------------}
procedure SkipWhite;
{--------------------------------------------------------------}
function GetName: char;
{--------------------------------------------------------------}
function GetNum: char;
{--------------------------------------------------------------}
function NewLabel: string;
{--------------------------------------------------------------}
procedure PostLabel(L: string);
{--------------------------------------------------------------}
procedure Emit(s: string);
{--------------------------------------------------------------} { Output a String with Tab and CRLF } procedure EmitLn(s: string);
{--------------------------------------------------------------}
procedure Condition;
{--------------------------------------------------------------}
procedure Expression;
{--------------------------------------------------------------}
procedure Block(L: string); Forward; procedure DoIf(L: string);
{--------------------------------------------------------------}
procedure DoWhile;
{--------------------------------------------------------------}
procedure DoLoop;
{--------------------------------------------------------------}
procedure DoRepeat;
{--------------------------------------------------------------}
procedure DoFor;
{--------------------------------------------------------------}
procedure Dodo;
{--------------------------------------------------------------}
procedure DoBreak(L: string);
{--------------------------------------------------------------}
procedure Other;
{--------------------------------------------------------------}
procedure Block(L: string);
{--------------------------------------------------------------} { Parse and Translate a Program } procedure DoProgram;
{--------------------------------------------------------------} { Initialize } procedure Init;
{--------------------------------------------------------------}
begin
|