14. Типы
ВВЕДЕНИЕ
В последней главе (Часть 13, Процедуры)
я упомянул, что в ней и в следующей главе мы рассмотрим две возможности,
которые помогут отделить игрушечный язык от настоящего, пригодного к использованию.
В ней мы рассмотрели вызовы процедур. Многие из вас терпеливо ждали, начиная
с Августа'89 когда я выдам вторую.
Хорошо, вот она.
В этой главе мы поговорим о том, как
работать с различными типами данных. Как и в последней главе, я не буду
сейчас включать эти возможности непосредственно в компилятор TINY. Вместо
этого я буду использовать тот же самый подход, который так хорошо служил
нам в прошлом: использование только фрагментов синтаксического анализатора
и односимвольных токенов. Как обычно, это позволит на добраться непосредственно
до сути вопроса не продираясь сквозь массу ненужного кода. Так как основные
проблемы при работе с множественными типами данных возникают в арифметических
операциях, на них мы и сконцентрируем свое внимание.
Несколько предупреждений: Во-первых,
есть некоторые типы, которые я не буду охватывать в этой главе. Здесь мы
будем говорить только о простых, встроенных типах. Мы даже не будем работать
с массивами, указателями или строками, я охвачу их в следующих нескольких
главах.
Во-вторых, мы также не будем обсуждать
и типы определяемые пользователем. Это будет значительно позже, по той
простой причине, что я все еще не убедил себя, что эти определяемые пользователем
типы должны быть в языке KISS. В более поздних главах я собираюсь охватить
по крайней мере основные концепции определяемых пользователем типов, записей
и т.д., просто для того, чтобы серия была полной. Но действительно ли они
будут включены как часть KISS - все еще открытый вопрос. Я открыт для комментариев
и предложений по этой теме.
Наконец, я должен предупредить вас:
то, что мы собираемся сделать может добавить массу дополнительных сложностей
и в синтаксический анализатор и в генерируемый код. Поддерживать
различные типы достаточно просто. Сложность возникает когда вы добавляете
правила преобразования между типами. Вообще-то, будет ли ваш компилятор
простым или сложным зависит от способа, выбранного вами для определения
правил преобразования типов. Даже если вы решите запретить любые преобразования
типов (как в Ada, например) проблема все еще остается, и она встроена в
математику. Когда вы умножаете два коротких числа, к примеру, вы можете
получить длинный результат.
Я подошел к этой проблеме очень осторожно,
пытаясь сохранить простоту. Но мы не можем полностью избежать сложности.
Как обычно случается, мы оказываемся перед необходимостью выбирить между
качеством кода и сложностью и, как обычно, я предпочитаю выбрать самый
простой подход.
ЧТО БУДЕТ ДАЛЬШЕ?
Прежде чем мы погрузимся в это занятие,
я думаю вам бы хотелось знать что мы сейчас собираемся делать...
особенно после того, как прошло столько много времени с прошлой главы.
Тем временем я не бездействовал. Я
разбил компилятор на модули. Одна из проблем, с которыми я столкнулся,
в том, что так как мы охватывали новые области и вследствие этого расширяли
возможности компилятора TINY, он становился все больше и больше. Я понял
пару глав назад, что это приводило к затруднениям и именно поэтому я возвратился
к использованию только фрагментов компилятора в последней и этой главах.
Кажется просто глупо заново воспроизводить код для, скажем, обработки булевых
исключающих ИЛИ, когда тема дискуссии - передача параметров.
Очевидным способом получит свой пирог
и съесть его также является разбиение компилятора на раздельно компилируемые
модули и, конечно, модули Turbo Pascal являются для этого идеальным средством.
Это позволит нам скрыть некоторый довольно сложный код (такой как полный
синтаксический анализ арифметических и булевых выражений) в одиночный модуль
и просто вытаскивать его всякий раз когда он необходим. При таком способе
единственным кодом, который я должен буду воспроизводить в этих главах,
будет код который непосредственно касается обсуждаемого вопроса.
Я также игрался с Turbo 5.5 который,
конечно, включает Борландовские объектно-ориентированные расширения Паскаля.
Я не решил, использовать ли эти возможности, по двум причинам. Прежде всего,
многие из вас, кто следовал за этой серией, могут все еще не иметь 5.5
и я конечно не хочу вынуждать кого-либо пойти и купить новый компилятор
только для того, чтобы завершить эту серию. Во-вторых, я не убежден, что
ОО расширения имеют такое большое значение для этого приложения. Мы обсуждали
кое-что из этого на форуме CLM на CompuServe, и пока что мы не нашли
никакой убедительной причины для использования ОО конструкции. Это одна
из тех областей, где я мог бы использовать некоторую обратную связь с читателями.
Кто-нибудь хочет проголосовать за Turbo 5.5 и ООП?
В любом случае после следующих нескольких
глав этой серии я планирую предоставить вам законченный набор модулей а
также законченные функционирующие компиляторы. Планом фактически предусмотрено
три компилятора: один для односимвольной версии TINY (для использования
в наших экспериментах), один для TINY и один для KISS. Я достаточно четко
выделил различия между TINY и KISS:
-
TINY будет поддерживать только два типа данных: символьный
и 16-разрядное целое число. Я могу также попробовать сделать что-нибудь
со строками, так как без них компилятор был бы довольно бесполезным. KISS
будет поддерживать все обычные простые типы, включая массивы и даже числа
с плавающей точкой.
-
TINY будет иметь только две управляющие конструкции IF и
WHILE. KISS будет поддерживать очень богатый набор конструкций включая
одну, которую мы не обсуждали здесь ранее... CASE.
-
KISS будет поддерживать раздельно компилируемые модули.
Одно предостережение: так как я все еще
не знаю достаточно об ассемблере для 80x86, все эти модули компилятора
все еще будут написаны для поддержки кода 68000. Однако в программах, которые
я планирую вам представить, вся генерация кода была тщательно изолирована
в отдельном модуле, так что любой предприимчивый студент смог бы перенастроить
их на любой другой процессор. Эта задача "оставлена как упражнение для
студента". Я сделаю предложение прямо здесь и сейчас: с человеком, который
предоставит нам первый надежный перевод для 80x86, я буду счастлив обсудить
коллективные авторские права и авторские отчисления от предстоящей книги.
Но хватит говорить. Давайте приступим
к изучению типов. Как я сказал ранее, мы будем делать это как и в последней
главе: выполняя эксперименты с использованием односимвольных токенов.
ТАБЛИЦА ИДЕНТИФИКАТОРОВ
Должно быть очевидным, что если мы
собираемся работать с переменными различных типов, нам понадобится какое-то
место для записи их типов. Очевидным средством для этого является таблица
идентификаторов и мы уже использовали ее например для различия между локальными
и глобальными переменными и между переменными и процедурами.
Структура таблицы идентификаторов
для односимвольных токенов особенно проста и мы использовали ее прежде
несколько раз. Для работы с ней, мы возьмем некоторые процедуры, которые
мы использовали раньше.
Сначала, нам необходимо объявить саму
таблицу идентификаторов:
{--------------------------------------------------------------}
{ Variable Declarations }
var Look: char;
{ Lookahead Character }
ST: Array['A'..'Z'] of char;
{ *** ДОБАВЬТЕ ЭТУ СТРОКУ ***}
{--------------------------------------------------------------}
Затем мы должны удостовериться, что
она инициализируется в процедуре Init:
{--------------------------------------------------------------}
{ Initialize }
procedure Init;
var i: char;
begin
for i := 'A' to 'Z' do
ST[i] := '?';
GetChar;
end;
{--------------------------------------------------------------}
Следующая процедура в действительности
нам не нужна, но она будет полезна для отладки. Все, что она делает, это
формирует дамп содержимого таблицы идентификаторов:
{--------------------------------------------------------------}
{ Dump the Symbol Table }
procedure DumpTable;
var i: char;
begin
for i := 'A' to 'Z' do
WriteLn(i, ' ', ST[i]);
end;
{--------------------------------------------------------------}
В действительности не имеет значения,
где вы поместите эту процедуру... я планирую группировать все подпрограммы
таблицы идентификаторов вместе, так что я поместил ее сразу после процедур
сообщений об ошибках.
Если вы осторожный тип (как я), вам
возможно захотелось бы начать с тестовой программы, которая ничего не делает
а просто инициализирует таблицу и затем создает ее дамп. Только для того,
чтобы быть уверенным, что все мы находимся на одной волне, ниже я воспроизвожу
всю программу, дополненную новыми процедурами. Заметьте, что эта версия
включает поддержку пробелов:
{--------------------------------------------------------------}
program Types;
{--------------------------------------------------------------}
{ Constant Declarations }
const TAB = ^I;
CR = ^M;
LF = ^J;
{--------------------------------------------------------------}
{ Variable Declarations }
var Look: char;
{ Lookahead Character }
ST: Array['A'..'Z'] of char;
{--------------------------------------------------------------}
{ Read New Character From Input Stream }
procedure GetChar;
begin
Read(Look);
end;
{--------------------------------------------------------------}
{ Report an Error }
procedure Error(s: string);
begin
WriteLn;
WriteLn(^G, 'Error: ', s, '.');
end;
{--------------------------------------------------------------}
{ Report Error and Halt }
procedure Abort(s: string);
begin
Error(s);
Halt;
end;
{--------------------------------------------------------------}
{ Report What Was Expected }
procedure Expected(s: string);
begin
Abort(s + ' Expected');
end;
{--------------------------------------------------------------}
{ Dump the Symbol Table }
procedure DumpTable;
var i: char;
begin
for i := 'A' to 'Z' do
WriteLn(i,
' ', ST[i]);
end;
{--------------------------------------------------------------}
{ Recognize an Alpha Character }
function IsAlpha(c: char): boolean;
begin
IsAlpha := UpCase(c) in ['A'..'Z'];
end;
{--------------------------------------------------------------}
{ Recognize a Decimal Digit }
function IsDigit(c: char): boolean;
begin
IsDigit := c in ['0'..'9'];
end;
{--------------------------------------------------------------}
{ Recognize an AlphaNumeric Character }
function IsAlNum(c: char): boolean;
begin
IsAlNum := IsAlpha(c) or IsDigit(c);
end;
{--------------------------------------------------------------}
{ Recognize an Addop }
function IsAddop(c: char): boolean;
begin
IsAddop := c in ['+', '-'];
end;
{--------------------------------------------------------------}
{ Recognize a Mulop }
function IsMulop(c: char): boolean;
begin
IsMulop := c in ['*', '/'];
end;
{--------------------------------------------------------------}
{ Recognize a Boolean Orop }
function IsOrop(c: char): boolean;
begin
IsOrop := c in ['|', '~'];
end;
{--------------------------------------------------------------}
{ Recognize a Relop }
function IsRelop(c: char): boolean;
begin
IsRelop := c in ['=', '#', '<', '>'];
end;
{--------------------------------------------------------------}
{ Recognize White Space }
function IsWhite(c: char): boolean;
begin
IsWhite := c in [' ', TAB];
end;
{--------------------------------------------------------------}
{ Skip Over Leading White Space }
procedure SkipWhite;
begin
while IsWhite(Look) do
GetChar;
end;
{--------------------------------------------------------------}
{ Skip Over an End-of-Line }
procedure Fin;
begin
if Look = CR then begin
GetChar;
if Look = LF then
GetChar;
end;
end;
{--------------------------------------------------------------}
{ Match a Specific Input Character }
procedure Match(x: char);
begin
if Look = x then GetChar
else Expected('''' + x + '''');
SkipWhite;
end;
{--------------------------------------------------------------}
{ Get an Identifier }
function GetName: char;
begin
if not IsAlpha(Look) then Expected('Name');
GetName := UpCase(Look);
GetChar;
SkipWhite;
end;
{--------------------------------------------------------------}
{ Get a Number }
function GetNum: char;
begin
if not IsDigit(Look) then Expected('Integer');
GetNum := Look;
GetChar;
SkipWhite;
end;
{--------------------------------------------------------------}
{ Output a String with Tab }
procedure Emit(s: string);
begin
Write(TAB, s);
end;
{--------------------------------------------------------------}
{ Output a String with Tab and CRLF }
procedure EmitLn(s: string);
begin
Emit(s);
WriteLn;
end;
{--------------------------------------------------------------}
{ Initialize }
procedure Init;
var i: char;
begin
for i := 'A' to 'Z' do
ST[i] := '?';
GetChar;
SkipWhite;
end;
{--------------------------------------------------------------}
{ Main Program }
begin
Init;
DumpTable;
end.
{--------------------------------------------------------------}
ОК, запустите эту программу. Вы должны
получить (очень быстро) распечатку всех букв алфавита (потенциальных идентификаторов)
сопровождаемых вопросительным знаком. Не очень захватывающе, но это только
начало.
Конечно, вообще-то мы хотим видеть
типы только тех переменных, которые были определены. Мы можем устранить
другие добавив в DumpTable условие IF. Измените цикл следующим образом:
for i := 'A' to 'Z' do
if ST[i] <> '?' then
WriteLn(i, ' ',
ST[i]);
Теперь запустите программу снова. Что
вы получили?
Хорошо, это даже более скучно чем
раньше! Сейчас вообще ничего не выводится, так как в данный момент ни одно
из имен не было обьявлено. Мы можем немного приправить результат вставив
в основную программу несколько операторов, объявляющих несколько записей.
Попробуйте такие:
ST['A'] := 'a';
ST['P'] := 'b';
ST['X'] := 'c';
На этот раз, когда вы запустите программу,
вы должны получить распечатку, показывающую, что таблица идентификаторов
работает правильно.
ДОБАВЛЕНИЕ ЗАПИСЕЙ
Конечно, заполнение таблицы напрямую
- довольно плохая практика и она не сможет хорошо нам послужить в будущем.
То, что нам нужно, это процедура, добавляющая записи в таблицу. В то же
самое время мы знаем, что нам будет необходимо тестировать таблицу для
проверки, что мы не объявляем повторно переменную, которая уже используется
(что легко может случиться при наличии всего 26 вариантов!). Для поддержки
всего это введите следующие новые процедуры:
{--------------------------------------------------------------}
{ Report Type of a Variable }
function TypeOf(N: char): char;
begin
TypeOf := ST[N];
end;
{--------------------------------------------------------------}
{ Report if a Variable is in the Table }
function InTable(N: char): boolean;
begin
InTable := TypeOf(N) <> '?';
end;
{--------------------------------------------------------------}
{ Check for a Duplicate Variable Name }
procedure CheckDup(N: char);
begin
if InTable(N) then Abort('Duplicate Name
' + N);
end;
{--------------------------------------------------------------}
{ Add Entry to Table }
procedure AddEntry(N, T: char);
begin
CheckDup(N);
ST[N] := T;
end;
{--------------------------------------------------------------}
Теперь измените три строки в основной
программе следующим образом:
AddEntry('A', 'a');
AddEntry('P', 'b');
AddEntry('X', 'c');
и запустите программу снова. Работает? Тогда у нас есть
подпрограммы таблицы идентификаторов, необходимые для поддержки нашей работы
с типами. В следующем разделе мы начнем их использовать на практике.
РАСПРЕДЕЛЕНИЕ ПАМЯТИ
В других программах, подобных этой,
включая сам компилятор TINY, мы уже обращались к вопросу объявления глобальных
переменных и кода, генерируемого для них. Давайте создадим здесь урезанную
версию "компилятора", чья единственная функция - позволить нам объявлять
переменные. Помните, синтаксис для объявления:
<data decl> ::= VAR <identifier>
Снова, мы можем вытащить массу кода
из предыдущих программ. Следующий код - это урезанные версии тех процедур.
Они значительно упрощены, так как я удалил такие тонкости как списки переменных
и инициализаторы. Обратите внимание, что в процедуре Alloc новый вызов
AddEntry будет также заботиться о проверке двойных объявлений:
{--------------------------------------------------------------}
{ Allocate Storage for a Variable }
procedure Alloc(N: char);
begin
AddEntry(N, 'v');
WriteLn(N, ':', TAB, 'DC 0');
end;
{--------------------------------------------------------------}
{ Parse and Translate a Data Declaration }
procedure Decl;
var Name: char;
begin
Match('v');
Alloc(GetName);
end;
{--------------------------------------------------------------}
{ Parse and Translate Global Declarations }
procedure TopDecls;
begin
while Look <> '.' do begin
case Look of
'v': Decl;
else Abort('Unrecognized
Keyword ' + Look);
end;
Fin;
end;
end;
{--------------------------------------------------------------}
Теперь, в основной программе добавьте
вызов TopDecl и запустите программу. Попробуйте распределить несколько
переменных и обратите внимание на полученный сгенерированный код. Для вас
это пройденный этап, поэтому результат должен выглядеть знакомым. Заметьте
из кода для TopDecls что программа завершается точкой.
Пока вы здесь, попробуйте объявить
две переменные с одинаковыми именами и проверьте что синтаксический анализатор
отлавливает ошибку.
ОБЪЯВЛЕНИЕ ТИПОВ
Распределение памяти различных размеров
не сложнее чем изменение процедуры TopDecl для распознавания более чем
одного ключевого слова. Здесь необходимо принять ряд решений, с точки зрения
того, каков должен быть синтаксис и т.п., но сейчас я собираюсь отложить
все эти вопросы и просто объявить не подлежащий утверждению указ что наш
синтаксис будет таким:
<data decl> ::= <typename>
<identifier>
где:
<typename> ::= BYTE | WORD | LONG
(По удивительному совпадению, первые
буквы этих наименований оказались те же самыми что и спецификации длины
ассемблерного кода 68000, так что такой выбор съэкономит нам немного работы.)
Мы можем создать код, который позаботится
об этих объявлениях, внеся всего лишь небольше изменения. Обратите внимание,
что в подпрограммах, показанных ниже, я отделил генерацию код в Alloc от
логической части. Это соответствует нашему желанию изолировать машино-зависимую
часть компилятора.
{--------------------------------------------------------------}
{ Generate Code for Allocation of a Variable }
procedure AllocVar(N, T: char);
begin
WriteLn(N, ':', TAB, 'DC.', T, ' 0');
end;
{--------------------------------------------------------------}
{ Allocate Storage for a Variable }
procedure Alloc(N, T: char);
begin
AddEntry(N, T);
AllocVar(N, T);
end;
{--------------------------------------------------------------}
{ Parse and Translate a Data Declaration }
procedure Decl;
var Typ: char;
begin
Typ := GetName;
Alloc(GetName, Typ);
end;
{--------------------------------------------------------------}
{ Parse and Translate Global Declarations }
procedure TopDecls;
begin
while Look <> '.' do begin
case Look of
'b', 'w',
'l': Decl;
else Abort('Unrecognized
Keyword ' + Look);
end;
Fin;
end;
end;
{--------------------------------------------------------------}
Внесите показанные изменения в эти
процедуры и испытайте программу. Используйте одиночные символы "b", "w"
и "l" как ключевые слова (сейчас они должны быть в нижнем регистре). Вы
увидите, что в каждом случае мы выделяем память соответствующего объема.
Обратите внимание, глядя на дамп таблицы идентификаторов, что размеры также
сохранены для использования позже. Какого использования? Хорошо, это тема
остальной части этой главы.
ПРИСВАИВАНИЯ
Теперь, когда мы можем объявлять переменные
различных размеров, очевидно что мы должны иметь возможность что-то с ними
делать. На первый раз, давайте просто попробуем загружать их в наш рабочий
регистр D0. Имеет смысл использовать ту же самую идею, которую мы использовали
для Alloc, т.е. сделаем процедуру загрузки, которая может загружать переменные
нескольких размеров. Нам также необходимо продолжать изолировать машино-зависимое
содержимое. Процедура загрузки выглядит так:
{---------------------------------------------------------------}
{ Load a Variable to Primary Register }
procedure LoadVar(Name, Typ: char);
begin
Move(Typ, Name + '(PC)', 'D0');
end;
{---------------------------------------------------------------}
По крайней мере для 68000, многие команды
оказываются командами MOVE. Было бы полезно создать отдельный генератор
кода только для этих инструкций и затем вызывать его когда необходимо:
{---------------------------------------------------------------}
{ Generate a Move Instruction }
procedure Move(Size: char; Source, Dest: String);
begin
EmitLn('MOVE.' + Size + ' ' + Source + ','
+ Dest);
end;
{---------------------------------------------------------------}
Обратите внимание, что эти две подпрограммы
- строго генераторы кода; они не имеют проверки ошибок и другой логики.
Чтобы завершить картинку, нам необходим еще один программный уровень, который
предоставляет эти функции.
Прежде всего, мы должны удостовериться,
что типы, с которыми мы работаем - загружаемого типа. Это звучит как работа
для другого распознавателя:
{--------------------------------------------------------------}
{ Recognize a Legal Variable Type }
function IsVarType(c: char): boolean;
begin
IsVarType := c in ['B', 'W', 'L'];
end;
{--------------------------------------------------------------}
Затем, было бы хорошо иметь подпрограмму,
которая извлечет тип переменной из таблицы идентификаторов в т |