ВВЕДЕНИЕ Могло ли действительно пройти четыре
года с тех пор, как я написал четырнадцатую главу этой серии? Действительно
ли возможно, что шесть долгих лет прошли с тех пор как я начал ее? Забавно,
как летит время когда вы весело его проводите, не так ли?
НОВОЕ НАЧАЛО, СТАРОЕ НАПРАВЛЕНИЕ Подобно многим другим вещам, языки
программирования и стили програмирования изменяются со временем. В 1994
году кажется немного анахроничным программировать на Turbo Pascal, когда
остальной мир кажется сходит с ума по C++. Также кажется немного странным
программировать в классическом стиле, когда остальной мир переключился
на объектно-ориентированные методы. Однако, несмотря на четырехлетнюю паузу,
было бы слишком тяжело сейчас переключиться, скажем, на C++ с объектной
ориентацией. Во всяком случае, Pascal все еще не только мощный язык программирования
(фактически больше, чем когда либо), но это и замечательная среда для обучения.
Си - известно трудный для чтения язык... он часто был обвиняем, наряду
с Forth, как "язык только для записи". Когда я программирую на C++ я трачу
по крайней мере 50% своего времени на борьбу с синтаксисом языка а не с
концепциями. Сбивающие с толку "&" или "*" могут не только изменить
функционирование программы, но также и ее правильность. Наоборот, код Паскаля
обычно совершенно ясен и прост для чтения даже если вы не знаете языка.
Что вы видите, то вы почти всегда и получите, и мы можем сконцентрироваться
на концепциях, а не тонкостях реализации. Я сказал в начале, что целью
этой обучающей серии была не генерация самого быстрого в мире компилятора,
а изучение основ технологии компиляции, с наименьшими затратами времени
на борьбу с синтаксисом языка или другими аспектами реализации программного
обеспечения. Наконец, так как многое из того, что мы делаем в этом курсе,
составляет программное экспериментирование, важно иметь компилятор и связанную
с ним среду, который компилирует быстро и без суеты. По моему мнению наиболее
значимым мерилом времени при разработке программного обеспечения является
скорость цикла редактирование/компиляция/тестирование. В этом отделе Turbo
Pascal - король. Скорость компиляции блестяще быстрая, и продолжает становиться
быстрее с каждым выпуском (как им это удается?). Несмотря на крупные усовершенствования
в быстродействии компиляции C за последние годы, даже Borland-овский самый
быстрый компилятор C/C++ все еще не сравним с Turbo Pascal. Далее, редактор,
встроенный в его IDE, средство make, и даже их превосходный умный компоновщик,
все дополняют друг друга чтобы получить замечательную среду для быстрой
разработки. По всем этим причинам, я собираюсь придерживаться Паскаля в
продолжении этой серии. Мы будем использовать Turbo Pascal for Windows,
один из компиляторов, предоставляемый Borland Pascal with Objects, версия
7.0. Если у вас нет этого компилятора не волнуйтесь... ничего из того,
что мы делаем здесь не будет рассчитано на то, что вы имеете последнюю
версию. Использование Windows версии сильно помогает мне, позволяя использовать
Clipboard для копирования кода из редактора компилятора в эти документы.
Она также должна помочь вам по крайней мере копировать код в обратном направлении.
НАЧИНАЕМ ЗАНОВО? Четыре года назад, в Главе 14, я обещал
вам, что наши дни повторного изобретения колеса и написания одних и тех
же программ на каждом уроке, прошли и что с этого момента мы будем придерживаться
более завершенных программ, к которым мы должны просто добавлять новые
возможности. Я все еще собираюсь сдержать это обещание; это одна из основных
целей использования модулей. Однако, из-за прошествия длительного времени
с главы 14, естественно хотелось бы сделать по крайней мере небольшой обзор
и в любом случае мы окажемся перед необходимостью сделать довольно обширные
изменения кода, чтобы выполнить переход к модулям. Кроме того, если откровенно,
после всего этого времени я не могу помнить всех хороших идей, которые
я имел в моей голове четыре года назад. Для меня лучший способ вспомнить
их - заново пройти некоторые шаги, которые привели нас к Главе 14. Так
что я надеюсь, что вы поймете и смиритесь со мной когда мы возвратимся
к своим корням, в некотором смысле, и перестроим ядро программы, распределяя
подпрограммы по различным модулям, и вытащим сами себя назад к точке где
мы были многие луны тому назад. Как всегда бывало, вы увидите все мои ошибки
и смены направлений в реальном режиме времени. Пожалуйста, будьте терпеливы...
мы доберемся до новых вещей раньше чем вы успеете оглянуться.
МОДУЛЬ INPUT Ключевой концепцией, которую мы использовали начиная с первого дня, была идея входного потока с одним предсказывающим символом. Все подпрограммы синтаксического анализа проверяют этот символ, не изменяя его, чтобы решить, что они должны делать дальше. (Сравните этот подход с подходом C/Unix, использующим getchar и unget, и я думаю вы согласитесь, что наш подход проще). Мы начнем нашу экскурсию в будущее перенеся эту концепцию в нашу новую модульную организацию. Первый модуль, соответствующе названный Input, показан ниже: {--------------------------------------------------------------}
{--------------------------------------------------------------}
{--------------------------------------------------------------}
procedure GetChar;
{--------------------------------------------------------------}
Как вы можете видеть, здесь нет ничего
очень заумного и конечно ничего сложного, так как он состоит только из
одной процедуры. Но мы уже можем видеть как использование модулей дает
нам преимущества. Обратите внимание на выполнимый код в блоке инициализации.
Этот код "запускает помпу" входного потока для нас, нечто такое мы всегда
делали раньше вставляя вызовы GetChar в процедуру Init. На этот раз
вызов происходит без каких-либо специальных обращений к ней с нашей стороны,
за исключением самого модуля. Как я предсказывал ранее, этот механизм сделает
нашу жизнь в будущем значительно проще. Я полагаю это одна из наиболее
полезных возможностей Turbo Pascal и я буду сильно на нее полагаться.
{--------------------------------------------------------------}
Обратите внимание на использование
предоставляемого Borland модуля WinCRT. Этот модуль необходим, если вы
предполагаете использовать стандартные подпрограммы ввода/вывода Паскаля
Read, ReadLn, Write и WriteLn, которые мы конечно предполагаем использовать.
Если вы забудете включить этот модуль в раздел "uses" вы получите действительно
причудливое и непонятное сообщение во время выполнения.
МОДУЛЬ OUTPUT Конечно, каждая приличная программа должна выводить результат и наша не исключение. Наши подпрограммы вывода включают функции Emit. Код для соответствующего модуля показан дальше: {--------------------------------------------------------------}
{--------------------------------------------------------------}
{--------------------------------------------------------------}
procedure Emit(s: string);
{--------------------------------------------------------------}
procedure EmitLn(s: string);
end.
(Заметьте, что этот модуль не имеет
раздела инициализации, так что он не требует блока begin.)
{--------------------------------------------------------------}
Увидели ли вы что-либо, что удивило
вас? Вы возможно были удивлены видеть, что вам было необходимо что-то набрать
даже хотя основная программа не требует никакого ввода. Дело в разделе
инициализации модуля Input, который все еще требует поместить что-либо
в предсказывающий символ. Жаль, нет никакого способа выйти из этого, или
скорее, мы не хотим выходить. За исключением простых тестовых случаев,
как этот, нам всегда будет необходим допустимый предсказывающий символ,
так что самое лучшее, что мы можем сделать с этой "проблемой" это... ничего.
Write(TAB, s); на Write(' ', s); Я должен признать что сталкивался с этой проблемой раньше и находил себя меняющим свое мнение так часто как хамелеон меняет цвет. Для наших целей, 99% которых будет проверка выходного кода при выводе на CRT, было бы хорошо видеть аккуратно сгруппированный "объектный" код. Строка: SUB1: MOVE #4,D0 просто выглядит более опрятно, чем отличающийся, но функционально идентичный код: SUB1:
В тестовой версии моего кода я включил
более сложную версию процедуры PostLabel, которая позволяет избежать размещения
меток на раздельных строках, задерживая печать метки чтобы она оказалась
на той же самой строке, что и связанная инструкция. Не позднее чем час
назад, моя версия модуля Output предоставляла полную поддержку табуляции
с использованием внутренней переменной счетчика столбцов и подпрограммы
для ее управления. Я имел некоторый довольно изящный код для поддержки
механизма табуляции с минимальным увеличением кода. Было ужасное искушение
показать вам эту "красивую" версию, единственно чтобы покрасоваться элегантностью.
МОДУЛЬ ERROR Наш следующий набор подпрограмм обрабатывает
ошибки. Чтобы освежить вашу память мы возьмем подход, заданный Borland
в Turbo Pascal - останавливаться на первой ошибке. Это не только значительно
упрощает наш код, полностью устраняя назойливую проблему восстановления
после ошибок, но это также имеет намного больший смысл, по моему мнению,
в интерактивной среде. Я знаю, что это может быть крайней позицией, но
я считаю практику сообщать обо всех ошибках в программе анахронизмом, пережитком
со времен пакетной обработки. Пришло время прекратить такую практику.
Так вот.
{--------------------------------------------------------------}
{--------------------------------------------------------------}
{--------------------------------------------------------------}
procedure Error(s: string);
{--------------------------------------------------------------}
procedure Expected(s: string);
end.
Как обычно, вот программа для проверки: {--------------------------------------------------------------}
begin
Вы заметили, что строка "uses" в нашей основной программе становится длиннее? Это нормально. В конечной версии основная программа будет вызывать процедуры только из нашего синтаксического анализатора, так что раздел uses будет иметь только пару записей. Но сейчас возможно самое лучшее включить все модули, чтобы мы могли протестировать процедуры в них. ЛЕКСИЧЕСКИЙ И СИНТАКСИЧЕСКИЙ АНАЛИЗ Классическая архитектура компилятора
основана на отдельных модулях для лексического анализатора, который предоставляет
лексемы языка, и синтаксического анализатора, который пытается определить
смысл токенов как синтаксических элементов. Если вы еще не забыли что мы
делали в более ранних главах, вы вспомните, что мы не делали ничего подобного.
Поскольку мы используем предсказывающий синтаксический анализатор, мы можем
почти всегда сказать, какой элемент языка следует дальше, всего-лишь исследуя
предсказывающий символ. Следовательно, нам не нужно предварительно выбирать
токен, как делал бы сканер.
{--------------------------------------------------------------}
function IsAlpha(c: char): boolean;
procedure Match(x: char);
{--------------------------------------------------------------}
{--------------------------------------------------------------}
function IsAlpha(c: char): boolean;
{--------------------------------------------------------------}
function IsDigit(c: char): boolean;
{--------------------------------------------------------------}
function IsAlnum(c: char): boolean;
{--------------------------------------------------------------}
function IsAddop(c: char): boolean;
{--------------------------------------------------------------}
function IsMulop(c: char): boolean;
{--------------------------------------------------------------}
procedure Match(x: char);
{--------------------------------------------------------------}
function GetName: char;
{--------------------------------------------------------------}
function GetNumber: char;
end.
Следующий фрагмент кода основной программы обеспечивает хорошую проверку лексического анализатора. Для краткости я включил здесь только выполнимый код; остальное тоже самое. Не забудьте, тем не менее, добавить имя Scanner1 в раздел "uses": Write(GetName);
Этот код распознает все предложения вида: x=0+y где x и y могут быть любыми односимвольными именами переменных и 0 любой цифрой. Код должен отбросить все другие предложения и выдать осмысленное сообщение об ошибке. Если это произошло, тогда вы в хорошей форме и мы можем продолжать. МОДУЛЬ SCANNER Следующая, и намного более важная, версия лексического анализатора, та которая обрабатывает многосимвольные токены, которые должны иметь все настоящие языки. Только две функции, GetName и GetNumber отличаются в этих двух модулях, но только чтобы убедиться, что здесь нет никаких ошибок, я воспроизвел здесь весь модуль. Это модуль Scanner: {--------------------------------------------------------------}
function IsAlpha(c: char): boolean;
procedure Match(x: char);
{--------------------------------------------------------------}
{--------------------------------------------------------------}
function IsAlpha(c: char): boolean;
{--------------------------------------------------------------}
function IsDigit(c: char): boolean;
{--------------------------------------------------------------}
function IsAlnum(c: char): boolean;
{--------------------------------------------------------------}
function IsAddop(c: char): boolean;
{--------------------------------------------------------------}
function IsMulop(c: char): boolean;
{--------------------------------------------------------------}
procedure Match(x: char);
{--------------------------------------------------------------}
function GetName: string;
{--------------------------------------------------------------}
function GetNumber: string;
end.
Таже самая тестовая программа проверит также и этот сканер. Просто измените раздел "uses" для использования Scanner вместо Scanner1. Теперь у вас должна быть возможность набирать многосимвольные имена и числа. РЕШЕНИЯ, РЕШЕНИЯ Несмотря на относительную простоту
обоих сканеров, много идей вошло в них и много решений было сделано. Я
хотел бы поделиться этими мыслями с вами сейчас чтобы вы могли принимать
свои собственные решения, соответствующие вашему приложению. Сначала заметьте,
что обе версии GetName переводят входные символы в верхний регистр. Очевидно,
здесь было принято проектное решение, и это один из тех случаев, когда
синтаксис языка распределяется по лексическому анализатору. В языке Си
регистр символов имеет значение. Для такого языка мы, очевидно, не сможем
преобразовывать символы в верхний регистр. Дизайн, который я использую,
предполагает язык, подобный Pascal, в котором регистр символов не имеет
значения. Для таких языков проще идти вперед и преобразовывать все идентификаторы
в верхний регистр в лексическом анализаторе, так что мы не должны волноваться
позднее, когда вы сравниваем строки.
{--------------------------------------------------------------}
function GetNumber: longint;
Вы могли бы отложить ее, как я предполагаю, на черный день. СИНТАКСИЧЕСКИЙ АНАЛИЗ К этому моменту мы распределили все
подпрограммы, составляющие наш Cradle, в модули, которые мы можем вытаскивать
когда они необходимы. Очевидно, они будут развиваться дальше когда мы снова
продолжим процесс восстановления, но большая часть их содержимого и несомненно
архитектура, которую они подразумевают, определена. Остается воплотить
синтаксис языка в модуль синтаксического анализа. Мы не будем делать многого
из этого в этой главе, но я хочу сделать немного просто чтобы оставить
вас с хорошим чувством, что мы все еще знаем что делаем. Так что прежде,
чем мы продолжим давай сгенерируем синтаксический анализатор достаточный
только для обработки одиночного показателя в выражении. В процессе мы также
обнаружим, что по необходимости создали также модуль генератора кода.
MOVE #n,D0 Немного погодя, мы повторили этот процесс для переменной, MOVE X(PC),D0 а затем для показателя, который может быть и константой и переменной. В память о прошлом, давайте повторим этот процесс Определите следующий новый модуль: {--------------------------------------------------------------}
{--------------------------------------------------------------}
{--------------------------------------------------------------}
procedure Factor;
end.
Как вы можете видеть, этот модуль вызывает
процедуру LoadConstant, которая фактически выполняет вывод ассемблерного
кода. Модуль также использует новый модуль CodeGen. Этот шаг представляет
последнее главное изменение в нашей архитектуре с более ранних глав: перемещение
машино-зависимого кода в отдельный модуль. Если я дойду до конца, вне CodeGen
не будет ни одной строчки кода, которая указывала бы на то, что мы нацелены
на процессор 68000. И это то место, которое показывает, что моя цель достижима.
{--------------------------------------------------------------}
{--------------------------------------------------------------}
{--------------------------------------------------------------}
{--------------------------------------------------------------}
procedure LoadConstant(n: string);
end.
Скопируйте и откомпилируйте этот модуль и выполните следующую основную программу: {--------------------------------------------------------------}
Вот он, сгенерированный код, такой
как мы и надеялись.
{--------------------------------------------------------------}
{--------------------------------------------------------------}
{--------------------------------------------------------------}
{--------------------------------------------------------------}
procedure LoadConstant(n: string);
{--------------------------------------------------------------}
procedure LoadVariable(Name: string);
end.
Сам модуль Parser не изменяется, но мы имеем более сложную версию процедуры Factor: {--------------------------------------------------------------}
procedure Factor;
Теперь, без изменений основной программы, вы должны обнаружить, что программа обрабатывает и переменный и постоянный показатель. К этому моменту наша архитектура почти завершена; у нас есть модули, выполняющие всю грязную работу и достаточно кода в синтаксическом анализаторе и генераторе кода чтобы продемонстрировать что все работает. Остается расширить модули которые мы определили, в особенности синтаксический анализатор и генератор кода, для поддержки более сложных синтаксических элементов, которые составляют настоящий язык. Так как мы делали это много раз прежде в предыдущих главах, не должно занять у нас много времени вернуться назад к тому месту, где мы были до долгого перерыва. Мы продолжим этот процесс в Главе 16, которая скоро появится. Увидимся. ССЫЛКИ
|