ВВЕДЕНИЕ В предыдущих главах мы изучили
многие из методов, необходимых для создания полноценного компилятора. Мы
разработали операции присваивания (с булевыми и арифметическими выражениями),
операторы отношений и управляющие конструкции. Мы все еще не обращались
к вопросу вызова процедур и функций, но даже без них мы могли бы в принципе
создать мини-язык. Я всегда думал, что было бы забавно просто посмотреть,
насколько маленьким можно было бы построить язык, чтобы он все еще оставался
полезным. Теперь мы уже почти готовы сделать это. Существует проблема:
хотя мы знаем, как анализировать и транслировать конструкции, мы все еще
совершенно не знаем, как сложить их все вместе в язык.
ВЕРХНИЙ УРОВЕНЬ Одна из самых больших ошибок
людей при нисходящем проектировании заключается в неправильном выборе истинной
вершины. Они думают, что знают какой должна быть общая структура проекта
и поэтому они продолжают и записывают ее.
begin
Конечно, я соглашусь с вами,
что это не слишком большая подсказка о том, что расположено на следующем
уровене, но я все равно запишу это просто для того, чтобы почувствовать,
что я действительно начинаю с вершины.
СТРУКТУРА ПАСКАЛЯ Большинство книг по Pascal включают БНФ определение языка. Вот несколько первых строк одного из них: <program> ::= <program-header> <block> '.' <program-header> ::= PROGRAM <ident> <block> ::= <declarations> <statements> Мы можем написать подпрограммы
распознавания для работы с каждым из этих элементов подобно тому, как мы
делали это прежде. Для каждого из них мы будем
использовать знакомые нам односимвольные токены, затем понемногу расширяя
их. Давайте начнем с первого распознавателя: непосредственно программы.
{--------------------------------------------------------------}
procedure Prog;
Процедуры Prolog и Epilog выполняют
все, что необходимо для связи программы с операционной системой так чтобы
она могла выполняться как программа. Само собой разумеется, эта часть будет
очень ОС-зависима. Помните, что я выдаю код для 68000, работающий под ОС,
которую я использую - SK*DOS. Я понимаю, что большинство из вас использует
PC и вы предпочли бы увидеть что-нибудь другое, но я слишком далеко зашел,
чтобы что-то сейчас менять!
{--------------------------------------------------------------}
procedure Prolog;
{--------------------------------------------------------------}
procedure Epilog(Name: char);
Как обычно добавьте этот код и испытайте "компилятор". В настоящее время существует только одна допустимая входная последовательность: px. (где х - это любая одиночная буква, имя программы). Хорошо, как обычно наша первая
попытка не очень впечатляет, но я уверен к настоящему времени вы знаете,
что дальше станет интересней. Есть одна важная вещь, которую следует отметить:
на выходе получается работающая, законченная и выполнимая программа (по
крайней мере после того, как она будет ассемблирована).
РАСШИРЕНИЕ Чтобы расширить компилятор мы должны просто работать с возможностями языка последовательно. Я хочу начать с пустой процедуры, которая ничего не делает, затем добавлять детали в пошаговом режиме. Давайте начнем с обработки блока в соответствии с его PDL выше. Мы можем сделать это в два этапа. Сначала добавьте пустую процедуру: {--------------------------------------------------------------}
procedure DoBlock(Name: char);
и измените Prog следующим образом: {--------------------------------------------------------------}
procedure Prog;
Это конечно не должно изменить поведения программы, и не меняет. Но сейчас определение Prog закончено и мы можем перейти к расширению DoBlock. Это получается прямо из его БНФ определения: {--------------------------------------------------------------}
procedure DoBlock(Name: char);
Процедура PostLabel была определена
в главе по ветвлениям. Скопируйте ее в вашу копию Cradle.
ОБЪЯВЛЕНИЯ БНФ для объявлений в Pascal такая: <declarations> ::= ( <label
list> |
(Заметьте, что я использую более либеральное определение,
используемое в Turbo Pascal. В определении стандартного Pascal каждая из
этих частей должна следовать в определенном порядке относительно других).
{--------------------------------------------------------------}
procedure Declarations;
Конечно, нам нужны процедуры-заглушки для каждого из этих типов объявлений. На этот раз они не могут быть совсем пустыми процедурами, так как иначе мы останемся с бесконечным циклом While. По крайней мере каждая подпрограмма распознавания должна съедать символ, который вызывает ее. Вставьте следующие процедуры: {--------------------------------------------------------------}
procedure Labels;
{--------------------------------------------------------------}
procedure Constants;
{--------------------------------------------------------------}
{--------------------------------------------------------------}
procedure Variables;
{--------------------------------------------------------------}
procedure DoProcedure;
{--------------------------------------------------------------}
procedure DoFunction;
Теперь испытайте компилятор используя
несколько характерных входных последовательностей. Вы можете смешивать
объявления любым образом, каким вам нравится пока последним символом в
программе не будет ".", указывающий на конец программы. Конечно, ни одно
из этих объявлений фактически ничего не объявляет, так что вам не нужны
(и вы не можете использовать) любые символы, кроме тех, которые обозначают
ключевые слова.
<statements> ::= <compound statement> <compound statement> ::= BEGIN <statement>(';' <statement>) END Заметьте, что утверждение может начинаться с любого идентификатора, исключая END. Так что первая пустой формой процедуры Statements будет: {--------------------------------------------------------------}
procedure Statements;
Сейчас компилятор примет любое
число объявлений, сопровождаемое блоком BEGIN основной программы. Сам этот
блок может содержать любые символы (за исключением END), но они должны
присутствовать.
'pxbe.' Испытайте ее. Также попробуйте
некоторые ее комбинации. Сделайте некоторые преднамеренные ошибки и посмотрите
что произойдет.
<statement> ::= <simple statement> | <structured statement> <simple statement> ::= <assignment> | <procedure call> | null <structured statement> ::=
<compound statement> |
Это начинает выглядеть знакомыми.
Фактически вы уже прошли через процесс синтаксического анализа и генерации
кода и для операций присваивания и для управляющих структур. Это место,
где верхний уровень встречается с нашим восходящим методом из предыдущих
уроков. Конструкции будут немного отличаться от тех, которые мы использовали
для KISS, но в этих различиях нет ничего, чего бы вы не смогли сделать.
СТРУКТУРА СИ Язык C совсем другой вопрос,
как вы увидите. Книги по C редко включают БНФ определения языка. Возможно
дело в том, что этот язык очень сложен для описания в БНФ.
1. Определение языка управляет структурой компилятора.
Что работает для одного языка может быть бедствием для другого. Это очень
плохая идея попытаться принудительно встроить данную структуру в компилятор.
Скорее вы должны позволить БНФ управлять структурой, как мы делали здесь.
Программа на C имеет меньше структур, чем ее аналог на Pascal. На верхнем уровне все в C является статическим объявлением или данных или функций. Мы можем зафиксировать эту мысль так: <program> ::= ( <global declaration> )* <global declaration> ::= <data declaration> | <function> В Small C функции могут иметь только тип по умолчанию int, который не объявлен. Это делает входную программу легкой для синтаксического анализа: первым токеном является или "int", "char" или имя функции. В Small C команды препроцессора также обрабатываются компилятором соответствующе, так что синтаксис становится: <global declaration> ::= '#'
<preprocessor command> |
Хотя мы в действительности больше заинтересованы здесь в полном C , я покажу вам код, соответствующий структуре верхнего уровня Small C. {--------------------------------------------------------------}
procedure Prog;
Обратите внимание, что я должен
был использовать ^Z чтобы указать на конец исходного кода. C не имеет ключевого
слова типа END или "." для индикации конца программы.
<program> ::= ( <top-level decl> )* <top-level decl> ::= <function def> | <data decl> <data decl> ::= [<class>] <type> <decl-list> <function def> ::= [<class>] [<type>] <function decl> Теперь вы можете увидеть проблему: первые две части обьявлений для данных и функций могут быть одинаковыми. Из-за неоднозначности в этой грамматике выше, она является неподходящей для рекурсивного синтаксического анализатора. Можем ли мы преобразовать ее в одну из подходящих? Да, с небольшой работой. Предположим мы запишем ее таким образом: <top-level decl> ::= [<class>] <decl> <decl> ::= <type> <typed decl> | <function decl> <typed decl> ::= <data list> | <function decl> Мы можем написать подпрограмму
синтаксичесого анализа для определений классов и типов и позволять им отложить
их сведения и продолжать выполнение даже не зная обрабатывается ли функция
или объявление данных.
{--------------------------------------------------------------}
begin
{--------------------------------------------------------------} На первый раз просто сделайте
три процедуры-заглушки которые ничего не делают, а только вызывают GetChar.
var Class: char; и измените GetClass {--------------------------------------------------------------}
Procedure GetClass;
Здесь я использовал три одиночных
символа для представления трех классов памяти "auto", "extern" и "static".
Это не единственные три возможных класса... есть также "register" и "typedef",
но это должно дать вам представление. Заметьте, что класс по умолчанию
"auto".
{--------------------------------------------------------------}
procedure GetType;
Обратите внимание, что вы должны
добавить еще две глобальные переменные Sign и Typ.
{--------------------------------------------------------------}
procedure TopDecl;
(Заметьте, что так как мы уже прочитали имя, мы должны
передать его соответствующей подпрограмме.)
{--------------------------------------------------------------}
procedure DoFunc(n: char);
{--------------------------------------------------------------}
procedure DoData(n: char);
Так как мы еще далеки от получения
выполнимого кода, я решил чтобы эти две подпрограммы только сообщали нам,
что они нашли.
|