ВВЕДЕНИЕ В последней главе (Часть 13, Процедуры)
я упомянул, что в ней и в следующей главе мы рассмотрим две возможности,
которые помогут отделить игрушечный язык от настоящего, пригодного к использованию.
В ней мы рассмотрели вызовы процедур. Многие из вас терпеливо ждали, начиная
с Августа'89 когда я выдам вторую.
Хорошо, вот она.
ЧТО БУДЕТ ДАЛЬШЕ? Прежде чем мы погрузимся в это занятие,
я думаю вам бы хотелось знать что мы сейчас собираемся делать...
особенно после того, как прошло столько много времени с прошлой главы.
Но хватит говорить. Давайте приступим к изучению типов. Как я сказал ранее, мы будем делать это как и в последней главе: выполняя эксперименты с использованием односимвольных токенов. ТАБЛИЦА ИДЕНТИФИКАТОРОВ Должно быть очевидным, что если мы
собираемся работать с переменными различных типов, нам понадобится какое-то
место для записи их типов. Очевидным средством для этого является таблица
идентификаторов и мы уже использовали ее например для различия между локальными
и глобальными переменными и между переменными и процедурами.
{--------------------------------------------------------------}
var Look: char; { Lookahead Character } ST: Array['A'..'Z'] of char;
{ *** ДОБАВЬТЕ ЭТУ СТРОКУ ***}
Затем мы должны удостовериться, что она инициализируется в процедуре Init: {--------------------------------------------------------------}
procedure Init;
Следующая процедура в действительности нам не нужна, но она будет полезна для отладки. Все, что она делает, это формирует дамп содержимого таблицы идентификаторов: {--------------------------------------------------------------}
procedure DumpTable;
В действительности не имеет значения,
где вы поместите эту процедуру... я планирую группировать все подпрограммы
таблицы идентификаторов вместе, так что я поместил ее сразу после процедур
сообщений об ошибках.
{--------------------------------------------------------------}
{--------------------------------------------------------------}
const TAB = ^I;
{--------------------------------------------------------------}
var Look: char; { Lookahead Character } ST: Array['A'..'Z'] of char;
{--------------------------------------------------------------}
procedure GetChar;
{--------------------------------------------------------------}
procedure Error(s: string);
{--------------------------------------------------------------}
procedure Abort(s: string);
{--------------------------------------------------------------}
procedure Expected(s: string);
{--------------------------------------------------------------}
procedure DumpTable;
{--------------------------------------------------------------}
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;
{--------------------------------------------------------------}
function IsOrop(c: char): boolean;
{--------------------------------------------------------------}
function IsRelop(c: char): boolean;
{--------------------------------------------------------------}
function IsWhite(c: char): boolean;
{--------------------------------------------------------------}
procedure SkipWhite;
{--------------------------------------------------------------}
procedure Fin;
{--------------------------------------------------------------}
procedure Match(x: char);
{--------------------------------------------------------------}
function GetName: char;
{--------------------------------------------------------------}
function GetNum: char;
{--------------------------------------------------------------}
procedure Emit(s: string);
{--------------------------------------------------------------}
procedure EmitLn(s: string);
{--------------------------------------------------------------}
procedure Init;
{--------------------------------------------------------------}
begin
ОК, запустите эту программу. Вы должны
получить (очень быстро) распечатку всех букв алфавита (потенциальных идентификаторов)
сопровождаемых вопросительным знаком. Не очень захватывающе, но это только
начало.
for i := 'A' to 'Z' do
Теперь запустите программу снова. Что
вы получили?
ST['A'] := 'a';
На этот раз, когда вы запустите программу, вы должны получить распечатку, показывающую, что таблица идентификаторов работает правильно. ДОБАВЛЕНИЕ ЗАПИСЕЙ Конечно, заполнение таблицы напрямую - довольно плохая практика и она не сможет хорошо нам послужить в будущем. То, что нам нужно, это процедура, добавляющая записи в таблицу. В то же самое время мы знаем, что нам будет необходимо тестировать таблицу для проверки, что мы не объявляем повторно переменную, которая уже используется (что легко может случиться при наличии всего 26 вариантов!). Для поддержки всего это введите следующие новые процедуры: {--------------------------------------------------------------}
function TypeOf(N: char): char;
{--------------------------------------------------------------}
function InTable(N: char): boolean;
{--------------------------------------------------------------}
procedure CheckDup(N: char);
{--------------------------------------------------------------}
procedure AddEntry(N, T: char);
Теперь измените три строки в основной программе следующим образом: AddEntry('A', 'a');
и запустите программу снова. Работает? Тогда у нас есть подпрограммы таблицы идентификаторов, необходимые для поддержки нашей работы с типами. В следующем разделе мы начнем их использовать на практике. РАСПРЕДЕЛЕНИЕ ПАМЯТИ В других программах, подобных этой, включая сам компилятор TINY, мы уже обращались к вопросу объявления глобальных переменных и кода, генерируемого для них. Давайте создадим здесь урезанную версию "компилятора", чья единственная функция - позволить нам объявлять переменные. Помните, синтаксис для объявления: <data decl> ::= VAR <identifier> Снова, мы можем вытащить массу кода из предыдущих программ. Следующий код - это урезанные версии тех процедур. Они значительно упрощены, так как я удалил такие тонкости как списки переменных и инициализаторы. Обратите внимание, что в процедуре Alloc новый вызов AddEntry будет также заботиться о проверке двойных объявлений: {--------------------------------------------------------------}
procedure Alloc(N: char);
{--------------------------------------------------------------}
procedure Decl;
{--------------------------------------------------------------}
procedure TopDecls;
Теперь, в основной программе добавьте
вызов TopDecl и запустите программу. Попробуйте распределить несколько
переменных и обратите внимание на полученный сгенерированный код. Для вас
это пройденный этап, поэтому результат должен выглядеть знакомым. Заметьте
из кода для TopDecls что программа завершается точкой.
ОБЪЯВЛЕНИЕ ТИПОВ Распределение памяти различных размеров не сложнее чем изменение процедуры TopDecl для распознавания более чем одного ключевого слова. Здесь необходимо принять ряд решений, с точки зрения того, каков должен быть синтаксис и т.п., но сейчас я собираюсь отложить все эти вопросы и просто объявить не подлежащий утверждению указ что наш синтаксис будет таким: <data decl> ::= <typename> <identifier> где: <typename> ::= BYTE | WORD | LONG (По удивительному совпадению, первые
буквы этих наименований оказались те же самыми что и спецификации длины
ассемблерного кода 68000, так что такой выбор съэкономит нам немного работы.)
{--------------------------------------------------------------}
procedure AllocVar(N, T: char);
{--------------------------------------------------------------}
procedure Alloc(N, T: char);
{--------------------------------------------------------------}
procedure Decl;
{--------------------------------------------------------------}
procedure TopDecls;
Внесите показанные изменения в эти процедуры и испытайте программу. Используйте одиночные символы "b", "w" и "l" как ключевые слова (сейчас они должны быть в нижнем регистре). Вы увидите, что в каждом случае мы выделяем память соответствующего объема. Обратите внимание, глядя на дамп таблицы идентификаторов, что размеры также сохранены для использования позже. Какого использования? Хорошо, это тема остальной части этой главы. ПРИСВАИВАНИЯ Теперь, когда мы можем объявлять переменные различных размеров, очевидно что мы должны иметь возможность что-то с ними делать. На первый раз, давайте просто попробуем загружать их в наш рабочий регистр D0. Имеет смысл использовать ту же самую идею, которую мы использовали для Alloc, т.е. сделаем процедуру загрузки, которая может загружать переменные нескольких размеров. Нам также необходимо продолжать изолировать машино-зависимое содержимое. Процедура загрузки выглядит так: {---------------------------------------------------------------}
procedure LoadVar(Name, Typ: char);
По крайней мере для 68000, многие команды оказываются командами MOVE. Было бы полезно создать отдельный генератор кода только для этих инструкций и затем вызывать его когда необходимо: {---------------------------------------------------------------}
procedure Move(Size: char; Source, Dest: String);
Обратите внимание, что эти две подпрограммы
- строго генераторы кода; они не имеют проверки ошибок и другой логики.
Чтобы завершить картинку, нам необходим еще один программный уровень, который
предоставляет эти функции.
{--------------------------------------------------------------}
function IsVarType(c: char): boolean;
Затем, было бы хорошо иметь подпрограмму, которая извлечет тип переменной из таблицы идентификаторов в то же время проверяя его на допустимость: {--------------------------------------------------------------}
function VarType(Name: char): char;
Вооруженная этими инструментами, процедура, выполняющая загрузку переменной, становится тривиальной: {--------------------------------------------------------------}
procedure Load(Name: char);
(Примечание для обеспокоившихся: я
знаю, знаю, все это очень неэффективно. В промышленной программы мы, возможно,
предприняли бы шаги чтобы избежать такого глубокого вложения вызовов процедур.
Не волнуйтесь об этом. Это упражнение, помните? Более важно сделать его
правильно и понять его, чем получить неправильный ответ но быстро. Если
вы закончите свой компилятор и обнаружите, что вы несчастны от его быстродействия,
вы вольны вернуться и доработать код для более быстрой работы).
Load('A');
в основную программу. Таким образом, после того, как раздел
объявления завершен, они будут выполнены чтобы генерировать код для загрузки.
Вы можете поиграть с различными комбинациями объявлений чтобы посмотреть
как обрабатываются ошибки.
{---------------------------------------------------------------}
procedure StoreVar(Name, Typ: char);
{--------------------------------------------------------------}
procedure Store(Name: char);
Вы можете проверить их таким же образом,
что и загрузку.
{---------------------------------------------------------------}
procedure Expression;
{--------------------------------------------------------------}
procedure Assignment;
{--------------------------------------------------------------}
procedure Block;
(Стоит заметить, что новые процедуры,
которые позволяют нам манипулировать типами, даже проще и яснее чем те,
что мы видели ранее. Это в основном блягодаря нашим усилиям по изоляции
подпрограмм генерации кода.)
{--------------------------------------------------------------}
begin
(Обратите внимание, что я должен был
расставить несколько обращений к Fin чтобы избежать проблем переносов строк.)
ba
{ byte a } *** НЕ НАБИРАЙТЕ КОММЕНТАРИИ!!! ***
Для каждого объявления вы должны получить
сгенерированный код, распределяющий память. Для каждого присваивания вы
должны получить код который загружает переменную корректного размера и
сохраняет ее, также корректного размера.
MOVE.L C(PC),D0
Этот код корректный. Он приведет к
сохранению младших восьми бит C в A, что является примлемым поведением.
Это почти все, что мы можем ожидать.
MOVE.B A(PC),D0
Это не правильно. Он приведет к сохранению
байтовой переменной A в младших восьми битах D0. Согласно правилам для
процессора 68000 старшие 24 бита останутся неизменными. Это означаем, что
когда мы сохраняем все 32 бита в C, любой мусор, который был в этих старших
разрядах, также будет сохранен. Нехорошо.
ТРУСЛИВЫЙ ВЫХОД Прежде, чем мы заберемся в детали (и
потенциальную сложность) преобразования типов, я хотел бы, чтобы вы видели,
что существует один суперпростой способ решения проблемы: просто переводить
каждую переменную в длинное целое во время загрузки!
{---------------------------------------------------------------}
procedure LoadVar(Name, Typ: char);
(Обратите внимание, что StoreVar не
нуждается в подобном изменении).
CLR.L D0
В этом случае CLR оказывается ненужной,
так как результат помещается в байтовую переменную. Небольшая доработка
помогла бы нам улучшить его. Однако, все это не так уж плохо, и это типичного
рода неэффективность, которую мы видели прежде в нехитрых компиляторах.
{---------------------------------------------------------------}
procedure LoadVar(Name, Typ: char);
В этой версии байт обрабатывается как беззнаковое число (как в Паскале и Си) в то время как слово обрабатывается как знаковое. БОЛЕЕ ПРИЕМЛЕМОЕ РЕШЕНИЕ Как мы видели, перевод каждой переменной
в длинное слово пока она находится в памяти решает проблему, но это едва
ли может быть названо эффективным и, возможно, не было бы приемлемым даже
для тех из нас, кто требует не обращать внимания на эффективность. Это
означает, что все арифметические операции будут выполняться с 32-битной
точностью, что удвоит время выполнения для большинства операций и сделает
его еще больше для умножения и деления. Для этих операций мы должны были
бы вызывать подпрограммы, даже если данные были бы байтом или словом.
Все это слишком походит на уловку, так как уводит нас от всех настоящих
проблем.
{---------------------------------------------------------------}
procedure LoadVar(Name, Typ: char);
Затем, давайте добавим новую процедуру, которая будет выполнять преобразование из одного типа в другой: {---------------------------------------------------------------}
procedure Convert(Source, Dest: char);
Затем, мы должны реализовать логику, требуемую для загрузки и сохранения переменной любого типа. Вот подпрограммы для этого: {---------------------------------------------------------------}
function Load(Name: char): char;
{--------------------------------------------------------------}
procedure Store(Name, T1: char);
Обратите внимание, что Load является
функцией, которая не только выдает код для загрузки, но также возвращает
тип переменной. Таким образом, мы всегда знаем, с каким типом данных мы
работаем. Когда мы выполняем Store, мы передаем ей текущий тип переменной
в D0. Так как Store также знает тип переменной назначения, она может выполнить
преобразование необходимым образом.
{---------------------------------------------------------------}
function Expression: char;
{--------------------------------------------------------------}
procedure Assignment;
Снова, заметьте как невероятно просты
эти две подпрограммы. Мы изолировали всю логику типа в Load и Store и хитрость
с передачей типа делает остальную работу чрезвычайно простой. Конечно,
все это для нашего специального, тривиального случая с Expression. Естественно,
для общего случая это будет более сложно. Но теперь вы смотрите на финальную
версию процедуры Assignment!
ЛИТЕРАЛЬНЫЕ АРГУМЕНТЫ Зоркие читатели могли бы отметить,
однако, что мы еще даже не имеем правильной формы простого показателя,
потому что мы не разрешаем загрузку литеральных констант, только переменных.
Давайте исправим это сейчас.
{--------------------------------------------------------------}
function GetNum: LongInt;
Теперь, когда работаем с литералами,
мы имеем одну небольшую проблему. С переменными мы знаем какого типа они
должны быть потому что они были объявлены с таким типом. Мы не имеем такой
информации о типе для литералов. Когда программист говорит "-1", означает
ли это байт, слово или длинное слово? Мы не имеем никаких сведений. Очевидным
способом было бы использование наибольшего возможного типа, т.е. длинного
слова. Но это плохая идея, потому что когда мы примемся за более сложные
выражения, мы обнаружим, что это заставит каждое выражение включающее литералы,
также переводить в длинное.
{--------------------------------------------------------------}
function LoadNum(N: LongInt): char;
(Я знаю, знаю, база числа не является
в действительности симметричной. Вы можете хранить -128 в одиночном байте
и -32768 в слове. Но это легко исправить и не стоит затраченного времени
или дополнительной сложности возиться с этим сейчас. Стоящая мысль.)
{---------------------------------------------------------------}
procedure LoadConst(N: LongInt; Typ: char);
Теперь мы можем изменить процедуру Expression для использования двух возможных видов показателей: {---------------------------------------------------------------}
function Expression: char;
(Вау, это, уверен, не причинило слишком
большого вреда! Всего несколько дополнительных строк делают всю работу.)
АДДИТИВНЫЕ ВЫРАЖЕНИЯ Если вы следовали за этой серией с
самого начала, я уверен вы знаете, что будет дальше. Мы расширим форму
выражения для поддержки сначала аддитивных выражений, затем мультипликативных,
а затем общих выражений со скобками.
{---------------------------------------------------------------}
function Expression: char;
Обратите внимание, как в этой подпрограмме
каждый вызов процедуры стал вызовом функции и как локальная переменная
Typ модифицируется при каждом проходе.
{---------------------------------------------------------------}
function Unop: char;
Процедура Push - это подпрограмма генерации кода, которая теперь имеет параметр, указывающий тип: {---------------------------------------------------------------}
procedure Push(Size: char);
Теперь давайте взглянем на функции Add и Subtract. В более старых версиях этих подпрограмм мы позволяем им вызывать подпрограммы генерации кода PopAdd и PopSub. Мы продолжим делать это, что делает сами функции чрезвачайно простыми: {---------------------------------------------------------------}
function Add(T1: char): char;
{-------------------------------------------------------------}
function Subtract(T1: char): char;
Но простота обманчива, поскольку мы
переложили всю логику на PopAdd и PopSub, которые больше не являются просто
подпрограммами генерации кода. Они также должны теперь заботиться о необходимых
преобразованиях типов.
{---------------------------------------------------------------}
procedure Pop(Size: char);
Общая идея состоит в том, что все "Pop-Op" подпрограммы могут вызывать ее. Когда это сделано, мы будем иметь оба операнда в регистрах, поэтому мы можем перевести любой нужный нам. Для работы процедуре Convert необходим другой аргумент, имя регистра: {---------------------------------------------------------------}
procedure Convert(Source, Dest: char; Reg: String);
Следующая функция выполняет пребразование, но только если текущий тип T1 меньше по размеру, чем желаемый тип T2. Это функция, возвращающая конечный тип, позволяющий нам знать, что она решила: {---------------------------------------------------------------}
function Promote(T1, T2: char; Reg: string): char;
Наконец, следующая функция приводит два регистра к одному типу: {---------------------------------------------------------------}
function SameType(T1, T2: char): char;
Эти новые подпрограммы дают нам заряд, необходимы нам чтобы разложить PopAdd и PopSub: {---------------------------------------------------------------}
function PopAdd(T1, T2: char): char;
{---------------------------------------------------------------}
function PopSub(T1, T2: char): char;
После всех этих приготовлений, в конечном
результате нет почти ничего кульминационного. Снова, вы можете видеть что
логика совершенно проста. Все что делают эти две подпрограммы - выталкивают
вершину стека в D7, приводят два операнда к одному размеру и затем генерируют
код.
{---------------------------------------------------------------}
procedure GenAdd(Size: char);
{---------------------------------------------------------------}
procedure GenSub(Size: char);
ОК, я соглашусь с вами: я выдал вам
множество подпрограмм с тех пор, как мы в последний раз протестировали
код. Но вы должны признать, что каждая новая подпрограмма довольно проста
и ясна. Если вам (как и мне) не нравится тестировать так много новых подпрограмм
одновременно все в порядке. Вы можете заглушить подпрограммы типа Convert,
Promote и SameType так как они не считывают входной поток. Вы не получите
корректный код, конечно, но программа должна работать. Затем постепенно
расширяйте их.
ПОЧЕМУ ТАК МНОГО ПРОЦЕДУР? К этому моменту вы можете подумать, что я зашел слишком далеко в смысле глубоко вложенных процедур. В этом несомненно есть большие накладные расходы. Но в моем безумии есть смысл. Как в случае с UnOp, я заглядываю вперед на время, когда мы захотим генерировать лучший код. С таким способом организации кода мы можем достичь этого без значительных изменений в программе Например, в случаях, где значение помещенное в стек не должно преобразовываться, все же лучше использовать инструкцию "вытолкнуть и сложить". Если мы решим проверять такие случаи, мы можем включить дополнительные тесты в PopAdd и PopSub не изменяя что-либо еще. МУЛЬТИПЛИКАТИВНЫЕ ВЫРАЖЕНИЯ Процедуры для работы с мультипликативными операторами почти такие же. Фактически, на первом уровне они почти идентичны, так что я просто покажу их здесь без особых фанфар. Первая - наша общая форма для Factor, которая включает подвыражения в скобках: {---------------------------------------------------------------}
function Expression: char; Forward; function Factor: char;
{--------------------------------------------------------------}
Function Multiply(T1: char): char;
{--------------------------------------------------------------}
function Divide(T1: char): char;
{---------------------------------------------------------------}
function Term: char;
Эти подпрограммы соответствуют аддитивным почти полностью. Как и прежде, сложность изолирована в PopMul и PopDiv. Если вам захочется протестировать программу прежде чем мы займемся ими, вы можете написать их пустые версии, аналогичные PopAdd и PopSub. И снова, код не будет корректным в данный момент, но синтаксический анализатор должен обрабатывать выражения произвольной сложности. УМНОЖЕНИЕ Если вы убедились, что сам синтаксический
анализатор работает правильно, мы должны выяснить, что необходимо сделать
для генерации правильного кода. С этого места дела становятся немного труднее
так как правила более сложные.
Эта таблица показывает действия, предпринимаемые
для каждой комбинации типов операндов. Есть три вещи, на которые необходимо
обратить внимание: во-первых, мы предполагаем, что существует библиотечная
подпрограмма MUL32, которая выполняет 32 x 32 умножение, оставляя
32-битное (не 64) произведение.
Если в процессе этого происходит переполнение мы игнорируем его и возвращаем
только младшие 32 бита.
{---------------------------------------------------------------}
procedure GenMult;
{---------------------------------------------------------------}
procedure GenLongMult;
Исследование кода ниже для PopMul должно убедить вас, что условия в таблице выполнены: {---------------------------------------------------------------}
function PopMul(T1, T2: char): char;
Как вы можете видеть, подпрограмма
начинается совсем как PopAdd. Два аргумента приводятся к тому же самому
типу. Два вызова Convert заботятся о случаях, когда оба операнда - байты.
Сами данные переводятся до слова, но подпрограмма помнит тип чтобы назначать
корректный тип результату. В заключение мы вызываем одну из двух подпрограмм
генерации кода и затем назначаем тип результата. Не слишком сложно, действительно.
ДЕЛЕНИЕ Случай с делением совсем не так симметричен.
У меня также есть для вас некоторые плохие новости:
Они вам лгут!!! Если вы не верите в это, попробуйте
разделить любое большое 32-разрядное число (это означает, что оно имеет
ненулевые биты в старших 16 разрядах) на целое число 1. Вы гарантированно
получите исключение переполнения.
Следующий код предоставляет корректную функцию для PopDiv: {---------------------------------------------------------------}
function PopDiv(T1, T2: char): char;
Две подпрограммы генерации кода: {---------------------------------------------------------------}
procedure GenDiv;
{---------------------------------------------------------------}
procedure GenLongDiv;
Обратите внимание, мы предполагаем,
что DIV32 оставляет результат (длинное слово) в D0.
ЗАВЕРШЕНИЕ Наконец-то, в этой главе мы узнали
как работать с переменными (и литералами) различных типов. Как вы можете
видеть, это не было слишком сложно. Фактически, в каком-то отношении большая
часть кода выглядит даже еще проще, чем это было в более ранних программах.
Только операторы умножения и деления требуют небольших размышлений и планирования.
ПРИВОДИТЬ ИЛИ НЕ ПРИВОДИТЬ В случае, если до вас еще не дошло,
уверен дойдет, что TINY и KISS возможно не будут строго типизированными
языками, так как я разрешил автоматическое смешивание и преобразование
почти любых типов. Что поднимает следующий вопрос:
ЗАКЛЮЧЕНИЕ На этом завершается наш урок по преобразованиям
типов. Жаль, что вы должны были так долго ждать его, но, надеюсь, вы чувствуете,
что он того стоил.
|