ВВЕДЕНИЕ В пятой части этой серии мы рассмотрели
управляющие конструкции и разработали подпрограммы синтаксического анализа
для трансляции их в объектный код. Мы закончили с хорошим, относительно
богатым набором конструкций.
ПЛАН Мы собираемся подойти к этой главе немного по-другому, чем к любой другой. В других главах мы начинали немедленно с экспериментов, используя компилятор Pascal, выстраивая синтаксические анализаторы от самых элементарных начал до их конечных форм, не тратя слишком много времени на предварительное планирование. Это называется кодированием без спецификации и обычно к нему относятся неодобрительно. Раньше мы могли избегать планирования, потому что правила арифметики довольно хорошо установлены... мы знаем, что означает знак "+" без необходимости подробно это обсуждать. То же самое относится к ветвлениям и циклам. Но способы, которыми языки программирования реализуют логику, немного отличаются от языка к языку. Поэтому прежде, чем мы начнем серьезное кодирование, лучше мы сперва примем решение что же мы хотим. И способ сделать это находится на уровне синтаксических правил БНФ (грамматики). ГРАММАТИКА Некоторое время назад мы реализовали синтаксические уравнения БНФ для арифметических выражений фактически даже не записав их все в одном месте. Пришло время сделать это. Вот они: <expression> ::= <unary
op> <term> [<addop> <term>]*
(Запомните, преимущества этой грамматики в том, что она
осуществляет такую иерархию приоритетов операторов, которую мы обычно ожидаем
для алгебры.)
<expression>
::= <term> [<addop> <term>]*
Это возлагает обработку унарного минуса на Factor, которому
он в действительности и принадлежит.
<b-expression>::= <b-term>
[<orop> <b-term>]*
Заметьте, что в этой грамматике
оператор AND аналогичен "*", а OR (и исключающее OR) - "+". Оператор NOT
аналогичен унарному минусу. Эта иерархия не является абсолютным стандартом...
некоторые языки, особенно Ada, обрабатывают все логические операторы как
имеющие одинаковый уровень приоритета... но это кажется естественным.
a * -b или еще хуже: a - -b не разрешены. В булевой алгебре наоборот, выражение a AND NOT b имеет точный смысл и показанный синтаксис учитывает это. ОПЕРАТОРЫ ОТНОШЕНИЙ Итак, предполагая что вы захотите
принять грамматику, которую я показал здесь, мы теперь имеем синтаксические
правила и для арифметики и для булевой алгебры. Сложность возникает когда
мы должны объединить их. Почему мы должны сделать это? Ну, эта тема возникла
из-за необходимости обрабатывать "предикаты" (условия), связанные с управляющими
операторами такими как IF. Предикат должен иметь логическое значение, то
есть он должен быть оценен как TRUE или FALSE. Затем ветвление выполняется
или не выполняется в зависимости от этого значения. Тогда то, что мы ожидаем
увидеть происходящим в процедуре Condition, будет вычисление
булевого выражения.
IF a AND NOT b THEN .... Но более часто мы видим, что булева алгебра появляется в таком виде: IF (x >= 0) and (x <= 100) THEN... Здесь два условия в скобках являются
булевыми выражениями, но индивидуальные сравниваемые термы: x, 0
и 100 являются числовыми по своей природе. Операторы отношений >= и <=
являются катализаторами, с помощью которых булевские и арифметические компоненты
объединяются вместе.
<relation> ::= <expression> <relop> <expression>, где выражения, о которых мы говорим здесь - старого числового типа, а операторы отношений это любой из обычных символов: =, <> (или !=), <, >, <= и >= Если вы подумаете об этом немного, то согласитесь, что так как этот вид предиката имеет логическое значение, TRUE или FALSE, это в действительности просто еще один вид показателя. Поэтому мы можем расширить определение булевого показателя следующим образом: <b-factor> ::=
<b-literal>
Вот эта связь! Операторы отношений
и отношения, которые они определяют, служат для объединения двух типов
алгебры. Нужно заметить, что это подразумевает иерархию, в которой арифметическое
выражение имеет более высокий приоритет, чем булевский показатель и, следовательно,
чем все булевы операторы. Если вы выпишите уровни приоритета для всех операторов,
вы прийдете к следующему списку:
Если мы захотим принять столько уровней приоритета, эта грамматика кажется приемлемой. К несчастью, она не будет работать! Грамматика может быть великолепной в теории, но она может совсем не иметь смысла в практике нисходящего синтаксического анализатора. Чтобы увидеть проблему рассмотрите следующий фрагмент кода: IF ((((((A + B + C) < 0 ) AND.... Когда синтаксический анализатор анализирует этот код он знает, что после того, как он рассмотрит токен IF следующим должно быть булево выражение. Поэтому он может приступить к началу вычисления такого выражения. Но первое выражение в примере является арифметическим выражением A + B + C. Хуже того, в точке, в которой анализатор прочитал значительную часть входной строки: IF ((((((A , он все еще не имеет способа узнать с каким видом выражения
он имеет дело. Так не пойдет, потому что мы должны иметь две различных
программы распознавания для этих двух случаев. Ситуация может быть обработана
без изменения наших определений но только если мы захотим принять произвольное
количество возвратов (backtracking) чтобы избавить наш путь от неверных
предположений. Ни один из создателей компиляторов в здравом уме не согласился
бы на это.
ИСПРАВЛЕНИЕ ГРАММАТИКИ Проблема, с которой мы столкнулись,
возникает потому, что наше определение и арифметических и булевых показателей
позволяет использовать выражения в скобках. Так как определения рекурсивны,
мы можем закончить с любым числом уровней скобок и синтаксический анализатор
не может знать с каким видом выражения он имеет дело.
Заметьте, что имеется только один набор синтаксических правил, применимый к обоим видам операторов. Тогда согласно этой грамматике выражения типа: x + (y AND NOT z) DIV 3 являются совершенно допустимыми. И, фактически, они таковыми
являются... настолько, насколько синтаксический анализатор в этом заинтересован.
Паскаль не позволяет смешивать арифметические и логические переменные,
и подобные вещи скорее перехватываются на семантическом уровне, когда придет
время генерировать для них код, чем на синтаксическом уровне.
IF (c >= 'A') and (c <= 'Z') then ... скобки обязательны. Я никогда не мог понять раньше почему, и ни мой компилятор, ни любой человек также не объясняли этого достаточно хорошо. Но сейчас мы все можем видеть, что оператор "and", имеющий приоритет как у оператора умножения, имеет более высокий приоритет, чем у операторов отношения, поэтому без скобок выражение эквивалентно: IF c >= ('A' and c) <= 'Z' then что не имеет смысла.
<b-expression> ::= <b-term>
[<orop> <b-term>]*
Эта грамматика приводит к тому
же самому набору семи уровней, которые я показал ранее. Действительно,
это почти таже самая грамматика... я просто исключил заключенное в скобки
b-выражение как возможный b-показатель и добавил отношение как допустимую
форму b-показателя.
СИНТАКСИЧЕСКИЙ АНАЛИЗАТОР Теперь, когда мы прошли через
процесс принятия решений, мы можем поспешить с разработкой синтаксического
анализатора. Вы делали это со мной несколько раз до этого, поэтому вы знаете
последовательность: мы начнем с новой копии Cradle и будем добавлять процедуры
одна за другой. Так что давайте сделаем это.
{--------------------------------------------------------------}
function IsBoolean(c: char): Boolean;
{--------------------------------------------------------------}
function GetBoolean: Boolean;
Внесите эти подпрограммы в вашу программу. Вы можете протестировать их, добавив в основную программу оператор печати: WriteLn(GetBoolean); Откомпилируйте программу и протестируйте
ее. Как обычно пока не очень впечатляет но скоро будет.
{---------------------------------------------------------------}
procedure BoolExpression;
Добавьте эту процедуру в ваш
анализатор и вызовите ее из основной программы (заменив оператор печати,
который вы только что там поместили). Как вы можете видеть, мы все еще
не имеем значительной части синтаксического анализатора, но выходной код
начинает выглядеть более реалистичным.
<b-expression> ::= <b-term> [<orop> <b-term>]* Я предпочитаю Паскалевскую версию "orop" - OR и XOR. Но так как мы сохраняем односимвольные токены, я буду кодировать их знаками '|' и '~'. Следующая версия BoolExpression - почти полная копия арифметической процедуры Expression: {--------------------------------------------------------------}
procedure BoolOr;
{--------------------------------------------------------------}
procedure BoolXor;
{---------------------------------------------------------------}
procedure BoolExpression;
Обратите внимание на новую процедуру IsOrOp, которая также является копией, на этот раз IsAddOp: {--------------------------------------------------------------}
function IsOrop(c: char): Boolean;
ОК, переименуйте старую версию
BoolExpression в BoolTerm, затем наберите код, представленный выше. Откомпилируйте
и протестируйте эту версию. К этому моменту выходной код начинает выглядеть
довольно хорошим. Конечно, нет большого смысла от булевой алгебры над постоянными
значениями, но скоро мы расширим булевы типы, с которыми мы работаем.
{---------------------------------------------------------------}
procedure BoolTerm;
Теперь мы почти дома. Мы транслируем сложные булевые выражения, хотя только и для постоянных значений. Следующий шаг - учесть NOT. Напишите следующую процедуру: {--------------------------------------------------------------}
procedure NotFactor;
И переименуйте предыдущую процедуру
в BoolFactor. Теперь испытайте компилятор. К этому времени синтаксический
анализатор должен обрабатывать любое булевое выражение, которое вы позаботитесь
ему подкинуть. Работает? Отлавливает ли он неправильно сформированные выражения?
{--------------------------------------------------------------}
procedure BoolFactor;
Вы могли бы задаться вопросом,
когда я собираюсь предоставить булевские переменные и булевские выражения
в скобках. Отвечаю: никогда. Помните, ранее мы убрали их из грамматики.
Прямо сейчас я собираюсь кодировать грамматику, которую мы уже согласовали.
Сам компилятор не может видеть разницы между булевыми переменными или выражениями
и арифметическими переменными или выражениями... все это будет обрабатываться
в Relation в любом случае.
{---------------------------------------------------------------}
procedure Relation;
ОК, наберите этот код и испытайте
его. Все старые дела все еще должны работать... у вас должна быть возможность
генерировать код для AND, OR и NOT. Кроме того, если вы наберете любой
алфавитный символ, вы должны получить небольшой заменитель <Relation>,
где должен быть булев показатель. Вы получили это? Отлично, тогда давайте
перейдем к полной версии Relation.
<relation> ::= | <expression> [<relop> <expression] Так как у нас появился новый вид операторов, нам также понадобится новая логическая функция для ее распознавания. Эта функция показана ниже. Из-за ограничения в один символ, я придерживаюсь четырех операторов, которые могут быть закодированы такими символами ("не равно" закодировано как "#"). {--------------------------------------------------------------}
function IsRelop(c: char): Boolean;
Теперь вспомните, что мы используем
нуль или -1 в регистре D0 для представления логического значения и также
то, что операторы цикла ожидают, что будет установлен соответствующий флаг.
При реализации всего этого для 68000 все становится немного сложным.
{---------------------------------------------------------------}
procedure Equals;
{---------------------------------------------------------------}
procedure NotEquals;
{---------------------------------------------------------------}
procedure Less;
{---------------------------------------------------------------}
procedure Greater;
{---------------------------------------------------------------}
procedure Relation;
Теперь этот вызов Expression выглядит знакомым! Вот где редактор вашей системы оказывается полезным. Мы уже генерировали код для Expression и его близнецов на предыдущих уроках. Теперь вы можете скопировать их в ваш файл. Не забудьте использовать односимвольную версию. Просто чтобы быть уверенным, я продублировал арифметические процедуры ниже. Если вы наблюдательны, вы также увидите, что я их немного изменил чтобы привести в соответствие с последней версией синтаксиса. Эти изменения не являются необходимыми, так что вы можете предпочесть оставить все как есть до тех пор, пока не будете уверены, что все работает. {---------------------------------------------------------------}
procedure Ident;
{---------------------------------------------------------------}
procedure Expression; Forward; procedure Factor;
{---------------------------------------------------------------}
procedure SignedFactor;
{--------------------------------------------------------------}
procedure Multiply;
{-------------------------------------------------------------}
procedure Divide;
{---------------------------------------------------------------}
procedure Term;
{---------------------------------------------------------------}
procedure Add;
{---------------------------------------------------------------}
procedure Subtract;
{---------------------------------------------------------------}
procedure Expression;
Теперь вы получили что-то... синтаксический анализатор, который может обрабатывать и арифметику и булеву алгебру и их комбинации через использование операторов отношений. Я советую вам сохранить копию этого синтаксического анализатора в безопасном месте для будущих обращений, потому что на нашем следующем шаге мы собираемся разделить его. ОБЪЕДИНЕНИЕ С УПРАВЛЯЮЩИМИ КОНСТРУКЦИЯМИ Сейчас давайте возвратимся назад
к файлу который мы создали ранее и который выполняет синтаксический анализ
управляющих конструкций. Помните небольшие фиктивные процедуры Condition
и Expression? Теперь вы знаете, что в них должно находиться!
ia=bxlye что означает "IF a=b X ELSE Y ENDIF".
ДОБАВЛЕНИЕ ПРИСВАИВАНИЙ Раз у нас уже есть подпрограммы
для выражений, мы могли бы также заменить "блоки" настоящими операциями
присваивания. Мы уже делали это прежде, поэтому это не будет слишком трудно.
Прежде, чем сделать этот шаг, однако, мы должны исправить кое-что
еще.
{--------------------------------------------------------------}
procedure Fin;
{--------------------------------------------------------------} Теперь добавьте два вызова Fin в процедуру Block следующим образом: {--------------------------------------------------------------}
procedure Block(L: string);
Теперь вы обнаружите, что можете
использовать многострочные "программы". Единственное ограничение в том,
что вы не можете отделять токены IF или WHILE от их предикатов.
{--------------------------------------------------------------}
procedure Assignment;
С этими изменениями у вас теперь
должна быть возможность писать сносные, реалистично выглядящие программы,
подчиненные только нашему ограничению односимвольными токенами. Первоначально
я также намеревался избавить вас и от этого ограничения. Однако, это потребует
довольно больших изменений того, что мы сделали к этому моменту. Нам нужен
настоящий лексический анализатор и это требует некоторых структурных изменений.
Это небольшие изменения, которые потребуют чтобы мы выбросили все, что
мы сделали к этому времени... при желании это может быть сделано в действительности
с минимальными изменениями. Но необходимо такое желание.
|