Лекции по построению компилятора на Pascal

         

ОБЪЯВЛЕНИЕ ПРОЦЕДУРЫ


Если вы удовлетворены, как работает наша маленькая программа, тогда пришло время поработать с процедурами. Так как мы еще не говорили о параметрах мы начнем с рассмотрения только таких процедур которые не имеют списка параметров.

Для начала, давайте рассмотрим простую программу с процедурой и подумаем о коде который мы хотели бы увидеть для нее сгенерированным:

    PROGRAM FOO;

    .

    .

    PROCEDURE BAR;                     BAR:

    BEGIN                                   .

    .                                       .

    .                                       .

    END;                                    RTS

    BEGIN { MAIN PROGRAM }             MAIN:

    .                                       .

    .                                       .




    FOO;                                    BSR BAR

    .                                       .

    .                                       .

    END.                                    END MAIN

Здесь я показал конструкции высокоуровневого языка слева и желаемый ассемблерный код справа. Прежде всего заметьте, что здесь несомненно нам не нужно генерировать много кода! Для большей части процедуры и основной программы наши существующие конструкции позаботятся о генерируемом коде.

Ключ к работе с телом процедуры - понимание того, что хотя процедура может быть очень длинной, ее объявление в действительности не отличается от объявления переменной. Это просто еще один вид объявлений. Мы можем записать БНФ:

    <declaration> ::= <data decl> | <procedure>

Это означает, что можно легко изменить TopDecl для работы с процедурами. Как насчет синтаксиса процедуры? Хорошо, вот предлагаемый синтаксис, который по существу такой же, как и в Pascal:

    <procedure> ::= PROCEDURE <ident> <begin-block>

Здесь практически не требуется никакой генерации кода., кроме генерации внутри блока begin. Мы должны только выдать метку в начале процедуры и RTS в конце.



Вот требуемый код:

{--------------------------------------------------------------}

{ Parse and Translate a Procedure Declaration }

procedure DoProc;

var N: char;

begin

     Match('p');

     N := GetName;

     Fin;

     if InTable(N) then Duplicate(N);

     ST[N] := 'p';

     PostLabel(N);

     BeginBlock;

     Return;

end;

{--------------------------------------------------------------}

Обратите внимание, что я добавил новую подпрограмму генерации кода Return, которая просто выдает инструкцию RTS. Создание этой подпрограммы "оставлено как упражнение студенту".

Для завершения этой версии добавьте следующую строку в оператор Case в DoBlock.

    'p': DoProc;

Я должен упомянуть, что эта структура для объявлений и БНФ, которая управляет ей, отличается от стандартного Паскаля. В определении Паскаля от Дженсена и Вирта  объявления переменных и, фактически, все виды объявлений, должны следовать в определенном порядке, т.е. метки, константы, типы, переменные, процедуры и основная программа. Чтобы следовать такой схеме, мы должны разделить два объявления и написать в основной программе что-нибудь вроде:

    DoVars;

    DoProcs;

    DoMain;

Однако, большинство реализаций Паскаля, включая Turbo, не требуют такого порядка и позволяют вам свободно перемешивать различные объявления до тех пор пока вы не попробуете обратиться к чему-то прежде, чем это будет объявлено. Хотя это может быть больше эстетическим удовлетворением объявлять все глобальные переменные на вершине программы, конечно не будет никакого вреда от того, чтобы разрешить расставлять их в любом месте. Фактически, это может быть даже немного полезно, в том смысле, что это даст нам возможность  выполнять небольшое элементарное скрытие информации. Переменные, к которым нужно обращаться только из основной программы, к примеру, могут быть объявлены  непосредственно перед ней и будут таким образом недоступны для процедур.



Испытайте эту новую версию. Заметьте, что мы можем объявить так много процедур, как захотим (до тех пор, пока не исчерпаем одно-символьные имена!) и все метки и RTS появятся в правильных местах.

Здесь стоит заметить, что я не разрешаю вложенные процедуры. В TINY все процедуры должны быть объявлены на глобальном уровне, так же как и в C. На Форуме Компьютерных Языков на CompuServe по этому поводу возникла порядочная дискуссия. Оказывается, существует значительная расплата сложностью которая должна быть заплачена за роскошь использования вложенных процедур. Более того, эта расплата платится во время выполнения, так как должен быть добавлен дополнительный код, который будет выполняться каждый раз когда процедура вызывается. Я также охотно верю что вложение это не очень хорошая идея просто на том основании, что я видел слишком много злоупотреблений этой возможностью. Прежде, чем сделать следующий шаг, также стоит обратить внимание на то, что "основная программа" в ее текущем состоянии незавершена, так как она не имеет метки и утверждения END. Давайте исправим эту небольшую оплошность:

{--------------------------------------------------------------}

{ Parse and Translate a Main Program }

procedure DoMain;

begin

     Match('b');

     Fin;

     Prolog;

     DoBlock;

     Epilog;

end;

{--------------------------------------------------------------}

.

.

.

{--------------------------------------------------------------}

{ Main Program }

begin

     Init;

     TopDecls;

     DoMain;

end.

{--------------------------------------------------------------}

Обратите внимание, что DoProc и DoMain не совсем симметричны. DoProc использует вызов BeginBlock тогда как DoMain нет. Это из-за того, что начало процедуры определяется по ключевому слову  PROCEDURE (в данном случае сокращенно 'p'), тогда как основная программа не имеет никакого другого ключевого слова кроме непосредственно BEGIN.



И это ставит интересный вопрос: почему?

Если мы посмотрим на структуры C программы, мы обнаружим, что все функции совсем одинаковы, за исключением того, что основная программа идентифицируется по своему имени "main". Так как функции C могут появляться в любом порядке, основная программа так же может быть в любом месте модуля компиляции.

В Паскале наоборот, все переменные и процедуры должны быть объявлены прежде чем они используются, что означает, что нет никакого смысла помещать что-либо после основной программы... к ней никогда нельзя будет обратиться. "Основная программа" не идентифицирована вообще, кроме того, что эта часть кода следует после глобального BEGIN. Другими словами если это не что-нибудь еще, это должна быть основная программа.

Это приводит к немалой путанице для начинающих программистов, а для больших программ на Паскале иногда вообще трудно найти начало основной программы.     Это ведет к соглашениям типа идентификации ее в комментариях:

    BEGIN { of MAIN }

Это всегда казалось мне немного клуджем. Возникает вопрос: Почему обработка основной программы должна так отличаться от обработки процедур? Теперь, когда мы осознали, что объявление процедур это просто... часть глобальных объявлений... не является ли основная программа также просто еще одним объявлением?

Ответ - да, и обрабатывая ее таким способом мы можем упростить код и сделать его значительно более ортогональным. Я предлагаю использовать для идентификации основной программы явное ключевое слово PROGRAM (Заметьте, что это означает, что мы не можем начать с него файл, как в Паскале). В этом случае наша БНФ становится:

    <declaration> ::= <data decl> | <procedure> | <main program>

    <procedure> ::= PROCEDURE <ident> <begin-block>

    <main program> ::= PROGRAM <ident> <begin-block>

Код также смотрится намного лучше, по крайней мере в том смысле, что DoMain и DoProc выглядят более похоже:



{--------------------------------------------------------------}

{ Parse and Translate a Main Program }

procedure DoMain;

var N: char;

begin

     Match('P');

     N := GetName;

     Fin;

     if InTable(N) then Duplicate(N);

     Prolog;

     BeginBlock;

end;

{--------------------------------------------------------------}

.

.

.

{--------------------------------------------------------------}

{ Parse and Translate Global Declarations }

procedure TopDecls;

begin

     while Look <> '.' do begin

      case Look of

            'v': Decl;

            'p': DoProc;

            'P': DoMain;

          else Abort('Unrecognized Keyword ' + Look);

          end;

          Fin;

     end;

end;

{--------------------------------------------------------------}

{ Main Program }

begin

     Init;

     TopDecls;

     Epilog;

end.

{--------------------------------------------------------------}

Так как объявление основной программы теперь внутри цикла TopDecl, возникают некоторые трудности. Как мы можем гарантировать, что она - последняя в файле? И выйдем ли мы когда либо из цикла? Мой ответ на второй вопрос, как вы можете видеть, - в том, чтобы вернуть нашего старого друга точку. Как только синтаксический анализатор увидит ее дело сделано.

Ответ на первый вопрос: он зависит от того, насколько мы хотим защищать программиста от глупых ошибок. В коде, который я показал, нет ничего, предохраняющего программиста от добавления кода после основной программы... даже другой основной программы. Код просто не будет доступен. Однако, мы могли бы обращаться к нему через утверждение FORWARD, которое мы предоставим позже. Фактически, многие программисты на ассемблере любят использовать область сразу после программы для объявления больших, неинициализированных блоков данных, так что действительно может быть некоторый смысл не требовать, чтобы основная программа была последней. Мы оставим все как есть.

Если мы решим, что должны дать программисту немного больше помощи чем сейчас, довольно просто добавить некоторую логику, которая выбросит нас из цикла как только основная программа будет обработана. Или мы могли бы по крайней мере сообщать об ошибке если кто-то попытается вставить две основных.


Содержание раздела