ВВЕДЕНИЕ В последней главе мы изучили методы, используемые для синтаксического анализа и трансляции математических выражений в общей форме. Мы закончили созданием простого синтаксического анализатора, поддерживающего выражения произвольной сложности с двумя ограничениями:
ПЕРЕМЕННЫЕ Большинство выражений, который мы встречаем на практике, включают переменные, например: b * b + 4 * a * c Ни один компилятор нельзя считать достаточно хорошим,
если он не работает с ними. К счастью, это тоже очень просто сделать.
<factor> ::= <number> | (<expression>) "|" заменяет "или", означая, что любая из этих форм является
допустимой. Запомните, также, что у нас нет проблемы в определении каждой
их них… предсказывающим символом в одном случае является левая скобка "("
и цифра – в другом.
<factor> ::= <number> | (<expression>) | <variable> И снова, здесь нет неоднозначности: если предсказывающий
символ – буква, то это переменная, если цифра то число. Когда мы транслируем
число, мы просто генерируем код для загрузки числа, как промежуточных данных,
в D0. Сейчас мы делаем то же самое, только для переменной.
MOVE X(PC),D0 где X, конечно, имя переменной. Вооружившись этим, изменим текущую версию процедуры Factor следующим образом: {---------------------------------------------------------------}
procedure Expression; Forward; procedure Factor;
Я уже отмечал, как легко добавлять расширения в синтаксический
анализатор благодаря способу его структурирования. Вы можете видеть, что
это все еще остается действительным и сейчас. На этот раз это стоило нам
всего двух дополнительных строк кода. Заметьте так же, как структура if-then-else
точно соответствует синтаксическому уравнению БНФ.
ФУНКЦИИ Есть еще только один распространенный
вид показателей, поддерживаемый большинством языков: вызов функции. В действительности,
нам пока слишком рано иметь дела с функциями, потому что мы еще не обращались
к вопросу передачи параметров. Более того, "настоящий" язык должен включать
механизм поддержки более чем одного типа, одним из которых должен быть
тип функции. Мы не имеем также и этого. Но все же я хотел бы работать с
функциями сейчас по двум причинам. Во-первых, это позволит нам превратить
компилятор во что-то очень близкое к конечному виду и, во вторых, это раскроет
новую проблему, о которой очень стоит поговорить.
X(). Так как мы пока не работаем со списками параметров, для
вызова функций не нужно ничего дополнительно, и необходимо только выдавать
BSR (вызов) вместо MOVE.
{---------------------------------------------------------------}
procedure Expression; Forward; procedure Factor;
и вставим перед ней новую процедуру {---------------------------------------------------------------}
procedure Ident;
Откомпилируйте и протестируйте эту версию. Обрабатывает
ли она все правильные выражения и корректно отмечает неправильные?
ПОДРОБНЕЕ ОБ ОБРАБОТКЕ ОШИБОК Имеется еще одна важная проблема,
которую стоит отметить: обработка ошибок. Обратите внимание, что хотя синтаксический
анализатор правильно отбрасывает (почти) каждое некорректное выражение,
которое мы ему подбросим, со значимым сообщением об ошибке, в действительности
мы не слишком много поработали для того, чтобы это происходило. Фактически
во всей программе (от Ident до Expression) есть только два вызова подпрограммы
обработки ошибок Expected. Но даже они не являются необходимыми… если вы
посмотрите снова на процедуры Term и Expression, то увидите, что эти утверждения
не выполнятся никогда. Я поместил их сюда ранее для небольшой подстраховки,
но сейчас они более не нужны. Почему бы не удалить их сейчас?
1+2 <space> 3+4 Видите, как пробел был обработан как признак завершения? Чтобы заставить компилятор правильно отмечать это, добавьте строку if Look <> CR then Expected('Newline'); в основную программу, сразу после вызова Expression. Это отлавливает все левое во входном потоке. Не забудьте определить CR в разделе const: CR = ^M; Как обычно откомпилируйте программу и проверьте, что она делает то, что нужно. ПРИСВАИВАНИЕ Итак, к этому моменту мы имеем
синтаксический анализатор, работающий очень хорошо. Я хотел бы подчеркнуть,
что мы получили это, используя всего 88 строк выполнимого кода, не считая
того, что было в Cradle. Откомпилированный объектный файл занял 4752 байта.
Неплохо, учитывая то, что мы не слишком старались сохранять размеры как
исходного так и объектного кода. Мы просто придерживались принципа KISS.
<Ident> = <Expression> Мы находимся на расстоянии вздоха от возможности анализировать операции присваивания, так что давайте сделаем этот последний шаг. Сразу после процедуры Expression добавьте следующую новую процедуру: {--------------------------------------------------------------}
procedure Assignment;
Обратите внимание снова, что код полностью соответствует
БНФ. И заметьте затем, что проверка ошибок была безболезненна и обработана
GetName и Match.
МНОГОСИМВОЛЬНЫЕ ТОКЕНЫ. В этой серии я тщательно ограничивал
все, что мы делаем, односимвольными токенами, все время уверяя вас, что
не составит проблемы расширить их до многосимвольных. Я не знаю, верили
вы мне или нет… я действительно не обвинил бы вас, если бы вы были немного
скептичны. Я буду продолжать использовать этот подход и в следующих главах,
потому что это позволит избежать сложности. Но я хотел бы поддержать эту
уверенность и показать вам, что это действительно легко сделать. В процессе
этого мы также предусмотрим обработку вложенных пробелов. Прежде чем вы
сделаете следующие несколько изменений, сохраните текущую версию синтаксического
анализатора под другим именем. Я буду использовать ее в следующей главе
и мы будем работать с односимвольной версией.
{--------------------------------------------------------------}
function IsAlNum(c: char): boolean;
Добавьте эту функцию в анализатор. Я поместил ее сразу
после IsDigit. Вы можете также включить ее как постоянного члена в Cradle.
{--------------------------------------------------------------}
function GetName: string;
Аналогично измените GetNum следующим образом: {--------------------------------------------------------------}
function GetNum: string;
Достаточно удивительно, что это фактически все необходимые изменения! Локальная переменная Name в процедурах Ident и Assignment были первоначально объявлены как "char" и теперь должны быть объявлены как string[8]. (Ясно, что мы могли бы сделать длину строки больше, если бы захотели, но большинство ассемблеров в любом случае ограничивают длину.) Внесите эти изменения и затем откомпилируйте и протестируйте. Сейчас вы верите, что это просто? ПРОБЕЛЫ Прежде, чем мы оставим этот синтаксический
анализатор на некоторое время, давайте обратимся к проблеме пробелов. На
данный момент, синтаксический анализатор выразит недовольство (или просто
завершит работу) на одиночном символе пробела, вставленном где-нибудь во
входном потоке. Это довольно недружелюбное поведение. Так что давайте немного
усовершенствуем анализатор, избавившись от этого последнего ограничения.
{--------------------------------------------------------------}
function IsWhite(c: char): boolean;
Нам также нужна процедура, "съедающая" символы пробела до тех пор, пока не найдет отличный от пробела символ: {--------------------------------------------------------------}
procedure SkipWhite;
Сейчас добавьте вызовы SkipWhite в Match, GetName и GetNum как показано ниже: {--------------------------------------------------------------}
procedure Match(x: char);
{--------------------------------------------------------------}
function GetName: string;
{--------------------------------------------------------------}
function GetNum: string;
(Обратите внимание, как я немного реорганизовал Match
без изменения функциональности.)
{--------------------------------------------------------------}
procedure Init;
Внесите эти изменения и повторно
откомпилируйте программу. Вы обнаружите, что необходимо переместить Match
ниже SkipWhite чтобы избежать сообщение об ошибке от компилятора Pascal.
Протестируйте программу как всегда, чтобы удостовериться, что она работает
правильно.
{--------------------------------------------------------------}
{--------------------------------------------------------------}
const TAB = ^I;
{--------------------------------------------------------------}
var Look: char; { Lookahead Character } {--------------------------------------------------------------}
procedure GetChar;
{--------------------------------------------------------------}
procedure Error(s: string);
{--------------------------------------------------------------}
procedure Abort(s: string);
{--------------------------------------------------------------}
procedure Expected(s: string);
{--------------------------------------------------------------}
function IsAlpha(c: char): boolean;
{--------------------------------------------------------------}
function IsDigit(c: char): boolean;
{--------------------------------------------------------------}
function IsAlNum(c: char): boolean;
{--------------------------------------------------------------}
function IsAddop(c: char): boolean;
{--------------------------------------------------------------}
function IsWhite(c: char): boolean;
{--------------------------------------------------------------}
procedure SkipWhite;
{--------------------------------------------------------------}
procedure Match(x: char);
{--------------------------------------------------------------}
function GetName: string;
{--------------------------------------------------------------}
function GetNum: string;
{--------------------------------------------------------------}
procedure Emit(s: string);
{--------------------------------------------------------------}
procedure EmitLn(s: string);
{---------------------------------------------------------------}
procedure Ident;
{---------------------------------------------------------------}
procedure Expression; Forward; procedure Factor;
{--------------------------------------------------------------}
procedure Multiply;
{-------------------------------------------------------------}
procedure Divide;
{---------------------------------------------------------------}
procedure Term;
{--------------------------------------------------------------}
procedure Add;
{-------------------------------------------------------------}
procedure Subtract;
{---------------------------------------------------------------}
procedure Expression;
{--------------------------------------------------------------}
procedure Assignment;
{--------------------------------------------------------------}
procedure Init;
{--------------------------------------------------------------}
begin
Теперь синтаксический анализатор
закончен. Он получил все возможности, которые мы можем разместить в однострочном
"компиляторе". Сохраните его в безопасном месте. В следующий раз мы перейдем
к новой теме, но мы все рано будем некоторое время говорить о выражениях.
В следующей главе я планирую рассказать немного об интерпретаторах в противоположность
компиляторам и показать вам как немного изменяется структура синтаксического
анализатора в зависимости от изменения характера принимаемых действий.
Информация, которую мы рассмотрим, хорошо послужит нам позднее, даже если
вы не интересуетесь интерпретаторами. Увидимся в следующий раз.
|