Анимация
JavaScript
|
Главная Библионтека значительно более ортогональным. Я предлагаю использовать для идентификации основной программы явное ключевое слово PROGRAM (Заметьте, что это означает, что мы не можем начать с него файл, как в Паскале). В этом случае наша БНФ становится: <declaration> ::= <data decl> j <procedure> j <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, которое мы предоставим позже. Фактически, многие программисты на ассемблере любят использовать область сразу после программы для объявления больших, неинициализированных блоков данных, так что действительно может быть некоторый смысл не требовать, чтобы основная программа была последней. Мы оставим все как есть. Если мы решим, что должны дать программисту немного больше помощи чем сейчас, довольно просто добавить некоторую логику, которая выбросит нас из цикла как только основная программа будет обработана. Или мы могли бы по крайней мере сообщать об ошибке если кто-то попытается вставить две основных. ВЫЗОВ ПРОЦЕДУРЫ Если вы удовлетворены работой программы, давайте обратимся ко второй половине уравнения... вызову. Рассмотрим БНФ для вызова процедуры: <proc call> ::= <identifier> с другой стороны БНФ для операции присваивания: <assignment> ::= <identifier> = <expression> Кажется у нас проблема. Оба БНФ утверждения с правой стороны начинаются с токена <identifier>. Как мы предполагаем узнать, когда мы видим идентификатор, имеем ли мы вызов процедуры или операцию присваивания? Это похоже на случай, когда наш синтаксический анализатор перестает быть предсказывающим и действительно это точно такой случай. Однако, оказывается эту проблему легко решить, так как все, что мы должны сделать - посмотреть на тип идентификатора записанный в таблице идентификаторов. Как мы обнаружили раньше, небольшое локальное нарушение правила предсказывающего синтаксического анализа может быть легко обработано как специальный случай. Вот как это делается: { Parse and Translate an Assignment Statement } procedure Assignment(Name: char); begin Match(=); Expression; StoreVar(Name); end; { Decide if a Statement is an Assignment or Procedure Call } procedure AssignOrProc; var Name: char; begin Name := GetName; case TypeOf(Name) of : Undefined(Name); v: Assignment(Name); p: CallProc(Name); else Abort(Identifier + Name + Cannot Be Used Here); end; end; { Parse and Translate a Block of Statements } procedure DoBlock; begin while not(Look in [e]) do begin AssignOrProc; Fin; end; end; Как вы можете видеть, процедура Block сейчас вызывает AssignOrProc вместо Assignment. Назначение этой новой процедуры просто считать идентификатор, определить его тип и затем вызвать процедуру, соответствующую этому типу. Так как имя уже прочитано, мы должны передать его в эти две процедуры и соответственно изменить Assignment. Процедура CallProc - это просто подпрограмма генерации кода: { Call a Procedure } procedure CallProc(N: char); begin EmitLn(BSR + N); end; Хорошо, к этому моменту у нас есть компилятор, который может работать с процедурами. Стоить отметить, что процедуры могут вызывать процедуры с любой степенью вложенности. Так что, даже хотя мы и не разрешаем вложенные объявления, нет ничего, чтобы удерживало нас от вложенных вызовов, точно так, как мы ожидали бы на любом языке. Мы получили это и это было не слишом сложно, не так ли? Конечно, пока мы можем работать только с процедурами, которые не имеют параметров. Процедуры могут оперировать глобальными переменными по их глобальным именам. Так что к этому моменту мы имеем эквивалент конструкции Бейсика GOSUB. Не слишком плохо... в конце концов масса серъезных программ была написана с применением GOSUBа., но мы можем добиться большего и добьемся. Это следующий шаг. ПЕРЕДАЧА ПАРАМЕТРОВ Снова, все мы знаем основную идею передачи параметров, но давайте просто для надежности разберем ее заново. Вообще, процедуре предоставляется список параметров, например: PROCEDURE FOO(X, Y, Z) В объявлении процедуры параметры называются формальными параметрами и могут упоминаться в теле процедуры по своим именам. Имена, используемые для формальных параметров в действительности произвольны. Учитывается только позиция. В примере выше имя X просто означает "первый параметр" везде, где он используется. Когда процедура вызывается, "фактические параметры" переданные ей, связаны с формальными параметрами на взаимно-однозначном принципе. БНФ для синтаксиса выглядит приблизительно так: <procedure> ::= PROCEDURE <ident> ( <param-list> ) <begin-block> <param list> ::= <parameter> ( , <parameter> )* j null Аналогично, вызов процедуры выглядит так: <proc call> ::= <ident> ( <param-list> ) Обратите внимание, что здесь уже есть неявное решение, встроенное в синтаксис. Некоторые языки, такие как Pascal и Ada разрешают списку параметров быть необязательным. Если нет никаких параметров, вы просто полностью отбрасываете скобки. Другие языки, типа C и Modula-2, требуют скобок даже если список пустой. Ясно, что пример, который мы только что привели, соответствует первой точке зрения. Но, сказать правду, я предпочитаю последний . Для одних процедур решение кажется должно быть в пользу "безсписочного" подхода. Оператор Initialize; , стоящий отдельно, может означать только вызов процедуры. В синтаксических анализаторах, которые мы писали, мы преимущественно использовали процедуры без параметров и было бы позором каждый раз заставлять писать пустую пару скобок. Но позднее мы также собираемся использовать и функции. И так как функции могут 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 [ 70 ] 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 |