Страница:
{–}
{ Process a Procedure Call }
procedure CallProc(Name: char);
var N: integer;
begin
N := ParamList;
Call(Name);
CleanStack(N);
end;
{–}
Здесь я создал еще одну подпрограмму генерации кода:
Следующая проблема имеет отношение к нашему способу адресации относительно указателя стека. Это работает отлично на наших простых примерах, так как с нашей элементарной формой выражений никто больше не засоряет стек. Но рассмотрим другой пример, такой простой как:
PROCEDURE FOO(A, B)
BEGIN
A = A + B
END
Код, сгенерированный нехитрым синтаксическим анализатором, мог бы быть:
FOO: MOVE 6(SP),D0 ; Извлечь A
MOVE D0,-(SP) ; Сохранить его
MOVE 4(SP),D0 ; Извлечь B
ADD (SP)+,D0 ; Добавить A
MOVE D0,6(SP) : Сохранить A
RTS
Это было бы неправильно. Когда мы помещаем первый аргумент в стек, смещения для двух формальных параметров больше не 4 и 6, я 6 и 8. Поэтому вторая выборка вернула бы снова A а не B.
Но это не конец света. Я думаю, вы можете видеть, что все, что мы должны делать – изменять смещение каждый раз, когда мы помещаем в стек и что фактически и делается если ЦПУ не имеет поддержки других методов.
К счастью, все-же, 68000 имеет такую поддержку. Поняв, что этот ЦПУ мог бы использоваться со многими компиляторами языков высокого уровня, Motorola решила добавить прямую поддержку таких вещей.
Проблема, как вы можете видеть в том, что когда процедура выполняется, указатель стека скачет вверх и вниз, и поэтому использование его как ссылки для доступа к формальным параметрам становится неудобным. Решение состоит в том, чтобы вместо него определить и использовать какой-то другой регистр. Этот регистр обычно устанавливается равным подлинному указателю стека и называется указателем кадра.
Команда LINK из набора инструкций 68000 позволяет вам объявить такой указатель кадра и установить его равным указателю стека и все это в одной команде. Фактически, она делает даже больше чем это. Так как этот регистр может использоваться для чего-то еще в вызывающей процедуре, LINK также помещает текущее значение регистра в стек. Вы можете также добавить значение к указателю стека чтобы создать место для локальных переменных.
В дополнение к LINK есть UNLK, которая просто восстанавливает указатель стека и выталкивает старое значение обратно в регистр.
С использованием этих двух команд код для предыдущего примера станет:
FOO: LINK A6,#0
MOVE 10(A6),D0 ; Извлечь A
MOVE D0,-(SP) ; Сохранить его
MOVE 8(A6),D0 ; Извлечь B
ADD (SP)+,D0 ; Добавить A
MOVE D0,10(A6) : Сохранить A
UNLK A6
RTS
Исправить компилятор для генерации этого кода намного проще чем объяснить. Все, что нам нужно сделать – изменить генерацию кода в DoProc. Так как из-за этого код становится немного больше одной строки, я создал новые процедуры, схожие с процедурами Prolog и Epilog, вызываемыми DoMain:
Это все что требуется. Попробуйте и посмотрите как вам это нравится.
К этому моменту мы генерируем некоторый относительно хороший код для процедур и вызовов процедур. С ограничениями, что нет никаких локальных переменных (пока) и не разрешено вложение процедур этот код именно то что нам нужно.
Все еще остается только одна небольшая проблема:
У нас нет способа возвратить результат в вызывающую программу!
Но это, конечно, не ограничение генерируемого нами кода, а ограничение, свойственное протоколу передачи по значению. Обратите внимание, что мы можем использовать формальные параметры любым способом внутри процедуры. Мы можем вычислять для них новое значение, использовать их как счетчики циклов (если бы мы имели циклы!) и т.д. Так что код делает то, что предполагается. Чтобы решить эту последнюю проблему мы должны рассмотреть альтернативный протокол.
Передача по ссылке
Локальные переменные
Заключение
Типы
Введение
Что будет дальше?
Таблица идентификаторов
{ Process a Procedure Call }
procedure CallProc(Name: char);
var N: integer;
begin
N := ParamList;
Call(Name);
CleanStack(N);
end;
{–}
Здесь я создал еще одну подпрограмму генерации кода:
{–}ОК, если вы добавили этот код в ваш компилятор, я думаю вы убедитесь, что стек теперь под контролем.
{ Adjust the Stack Pointer Upwards by N Bytes }
procedure CleanStack(N: integer);
begin
if N > 0 then begin
Emit('ADD #');
WriteLn(N, ',SP');
end;
end;
{–}
Следующая проблема имеет отношение к нашему способу адресации относительно указателя стека. Это работает отлично на наших простых примерах, так как с нашей элементарной формой выражений никто больше не засоряет стек. Но рассмотрим другой пример, такой простой как:
PROCEDURE FOO(A, B)
BEGIN
A = A + B
END
Код, сгенерированный нехитрым синтаксическим анализатором, мог бы быть:
FOO: MOVE 6(SP),D0 ; Извлечь A
MOVE D0,-(SP) ; Сохранить его
MOVE 4(SP),D0 ; Извлечь B
ADD (SP)+,D0 ; Добавить A
MOVE D0,6(SP) : Сохранить A
RTS
Это было бы неправильно. Когда мы помещаем первый аргумент в стек, смещения для двух формальных параметров больше не 4 и 6, я 6 и 8. Поэтому вторая выборка вернула бы снова A а не B.
Но это не конец света. Я думаю, вы можете видеть, что все, что мы должны делать – изменять смещение каждый раз, когда мы помещаем в стек и что фактически и делается если ЦПУ не имеет поддержки других методов.
К счастью, все-же, 68000 имеет такую поддержку. Поняв, что этот ЦПУ мог бы использоваться со многими компиляторами языков высокого уровня, Motorola решила добавить прямую поддержку таких вещей.
Проблема, как вы можете видеть в том, что когда процедура выполняется, указатель стека скачет вверх и вниз, и поэтому использование его как ссылки для доступа к формальным параметрам становится неудобным. Решение состоит в том, чтобы вместо него определить и использовать какой-то другой регистр. Этот регистр обычно устанавливается равным подлинному указателю стека и называется указателем кадра.
Команда LINK из набора инструкций 68000 позволяет вам объявить такой указатель кадра и установить его равным указателю стека и все это в одной команде. Фактически, она делает даже больше чем это. Так как этот регистр может использоваться для чего-то еще в вызывающей процедуре, LINK также помещает текущее значение регистра в стек. Вы можете также добавить значение к указателю стека чтобы создать место для локальных переменных.
В дополнение к LINK есть UNLK, которая просто восстанавливает указатель стека и выталкивает старое значение обратно в регистр.
С использованием этих двух команд код для предыдущего примера станет:
FOO: LINK A6,#0
MOVE 10(A6),D0 ; Извлечь A
MOVE D0,-(SP) ; Сохранить его
MOVE 8(A6),D0 ; Извлечь B
ADD (SP)+,D0 ; Добавить A
MOVE D0,10(A6) : Сохранить A
UNLK A6
RTS
Исправить компилятор для генерации этого кода намного проще чем объяснить. Все, что нам нужно сделать – изменить генерацию кода в DoProc. Так как из-за этого код становится немного больше одной строки, я создал новые процедуры, схожие с процедурами Prolog и Epilog, вызываемыми DoMain:
{–}Процедура DoProc теперь просто вызывает их:
{ Write the Prolog for a Procedure }
procedure ProcProlog(N: char);
begin
PostLabel(N);
EmitLn('LINK A6,#0');
end;
{–}
{ Write the Epilog for a Procedure }
procedure ProcEpilog;
begin
EmitLn('UNLK A6');
EmitLn('RTS');
end;
{–}
{–}В заключение, мы должны изменить ссылки на SP в процедурах LoadParam и StoreParam:
{ Parse and Translate a Procedure Declaration }
procedure DoProc;
var N: char;
begin
Match('p');
N := GetName;
FormalList;
Fin;
if InTable(N) then Duplicate(N);
ST[N] := 'p';
ProcProlog(N);
BeginBlock;
ProcEpilog;
ClearParams;
end;
{–}
{–}(Заметьте, что вычисление Offset изменяется чтобы учесть дополнительное сохранение A6.)
{ Load a Parameter to the Primary Register }
procedure LoadParam(N: integer);
var Offset: integer;
begin
Offset := 8 + 2 * (NumParams – N);
Emit('MOVE ');
WriteLn(Offset, '(A6),D0');
end;
{–}
{ Store a Parameter from the Primary Register }
procedure StoreParam(N: integer);
var Offset: integer;
begin
Offset := 8 + 2 * (NumParams – N);
Emit('MOVE D0,');
WriteLn(Offset, '(A6)');
end;
{–}
Это все что требуется. Попробуйте и посмотрите как вам это нравится.
К этому моменту мы генерируем некоторый относительно хороший код для процедур и вызовов процедур. С ограничениями, что нет никаких локальных переменных (пока) и не разрешено вложение процедур этот код именно то что нам нужно.
Все еще остается только одна небольшая проблема:
У нас нет способа возвратить результат в вызывающую программу!
Но это, конечно, не ограничение генерируемого нами кода, а ограничение, свойственное протоколу передачи по значению. Обратите внимание, что мы можем использовать формальные параметры любым способом внутри процедуры. Мы можем вычислять для них новое значение, использовать их как счетчики циклов (если бы мы имели циклы!) и т.д. Так что код делает то, что предполагается. Чтобы решить эту последнюю проблему мы должны рассмотреть альтернативный протокол.
Передача по ссылке
Это просто теперь, когда мы уже имеем механизм. Мы только должны внести несколько изменений в генерацию кода. Вместо помещения значения в стек, мы должны помещать адрес. Оказывается, 68000 имеет инструкцию PEA которая как раз делает это.
Для этого мы сделаем новую версию тестовой программы. Перед тем, как сделать что-нибудь еще, сделайте копию программы в ее текущем состоянии, потому что позже она понадобится нам снова.
Давайте начнем с рассмотрения кода, который мы хотели бы видеть сгенерированным для нового случая. Используя тот же самый пример что и раньше, мы должны вызов
FOO(X, Y)
оттранслировать в:
PEA X(PC) ; Сохранить адрес X
PEA Y(PC) ; Сохранить адрес Y
BSR FOO ; Вызвать FOO
Это просто вопрос небольших изменений в Param:
На другой стороне, ссылки на формальные параметры должны получить один уровень косвенности:
FOO: LINK A6,#0
MOVE.L 12(A6),A0 ; Извлечь адрес A
MOVE (A0),D0 ; Извлечь A
MOVE D0,-(SP) ; Сохранить
MOVE.L 8(A6),A0 ; Извлечь адрес B
MOVE (A0),D0 ; Извлечь B
ADD (SP)+,D0 ; Добавить A
MOVE.L 12(A6),A0 ; Извлечь адрес A
MOVE D0,(A0) : Сохранить A
UNLK A6
RTS
Все это может быть обработано с изменениями в LoadParam and StoreParam:
ParamList := 4 * N;
Теперь должно работать. Испытайте компилятор и посмотрите, генерирует ли он приемлемый код. Как вы увидите, код вряд ли оптимален, так как мы перезагружаем регистр адреса каждый раз, когда необходим параметр. Но это соответствует нашему принципу KISS – просто генерировать код который работает. Мы только сделаем здесь небольшое замечание, что есть еще один кандидат для оптимизации и пойдем дальше.
Теперь мы научились обрабатывать параметры использую передачу по значению и передачу по ссылке. В реальном мире, конечно, мы хотели бы иметь возможность работать с обоими методами. Однако пока мы не можем этого сделать, потому что у нас еще не было урока по типам.
Если мы можем иметь только один метод, то, конечно, это должен быть старый добрый Фортранов метод передачи по ссылке, так как это единственный способ, которым процедуры могут возвращать значения в вызвавшую программу.
Это, фактически, будет одним из различий между TINY и KISS. В следующей версии TINY мы будем использовать передачу по ссылке для всех параметров. KISS будет поддерживать оба метода.
Для этого мы сделаем новую версию тестовой программы. Перед тем, как сделать что-нибудь еще, сделайте копию программы в ее текущем состоянии, потому что позже она понадобится нам снова.
Давайте начнем с рассмотрения кода, который мы хотели бы видеть сгенерированным для нового случая. Используя тот же самый пример что и раньше, мы должны вызов
FOO(X, Y)
оттранслировать в:
PEA X(PC) ; Сохранить адрес X
PEA Y(PC) ; Сохранить адрес Y
BSR FOO ; Вызвать FOO
Это просто вопрос небольших изменений в Param:
{–}(Обратите внимание, что при передачей по ссылке мы не можем использовать выражения в списке параметров, поэтому Param может просто непосредственно считывать имя).
{ Process an Actual Parameter }
procedure Param;
begin
EmitLn('PEA ' + GetName + '(PC)');
end;
{–}
На другой стороне, ссылки на формальные параметры должны получить один уровень косвенности:
FOO: LINK A6,#0
MOVE.L 12(A6),A0 ; Извлечь адрес A
MOVE (A0),D0 ; Извлечь A
MOVE D0,-(SP) ; Сохранить
MOVE.L 8(A6),A0 ; Извлечь адрес B
MOVE (A0),D0 ; Извлечь B
ADD (SP)+,D0 ; Добавить A
MOVE.L 12(A6),A0 ; Извлечь адрес A
MOVE D0,(A0) : Сохранить A
UNLK A6
RTS
Все это может быть обработано с изменениями в LoadParam and StoreParam:
{–}Для правильного расчета, мы также должны изменить одну строку в ParamList:
{ Load a Parameter to the Primary Register }
procedure LoadParam(N: integer);
var Offset: integer;
begin
Offset := 8 + 4 * (NumParams – N);
Emit('MOVE.L ');
WriteLn(Offset, '(A6),A0');
EmitLn('MOVE (A0),D0');
end;
{–}
{ Store a Parameter from the Primary Register }
procedure StoreParam(N: integer);
var Offset: integer;
begin
Offset := 8 + 4 * (NumParams – N);
Emit('MOVE.L ');
WriteLn(Offset, '(A6),A0');
EmitLn('MOVE D0,(A0)');
end;
{–}
ParamList := 4 * N;
Теперь должно работать. Испытайте компилятор и посмотрите, генерирует ли он приемлемый код. Как вы увидите, код вряд ли оптимален, так как мы перезагружаем регистр адреса каждый раз, когда необходим параметр. Но это соответствует нашему принципу KISS – просто генерировать код который работает. Мы только сделаем здесь небольшое замечание, что есть еще один кандидат для оптимизации и пойдем дальше.
Теперь мы научились обрабатывать параметры использую передачу по значению и передачу по ссылке. В реальном мире, конечно, мы хотели бы иметь возможность работать с обоими методами. Однако пока мы не можем этого сделать, потому что у нас еще не было урока по типам.
Если мы можем иметь только один метод, то, конечно, это должен быть старый добрый Фортранов метод передачи по ссылке, так как это единственный способ, которым процедуры могут возвращать значения в вызвавшую программу.
Это, фактически, будет одним из различий между TINY и KISS. В следующей версии TINY мы будем использовать передачу по ссылке для всех параметров. KISS будет поддерживать оба метода.
Локальные переменные
Пока мы не сказали ничего о локальных переменных и наше определение процедур не разрешает их. Само собой разумеется, что это большой пробел в нашем языке и он должен быть исправлен.
И снова здесь мы стоим перед выбором: статическое или динамическое хранение?
В старых FORTRAN программах локальные переменные использовали статическое хранение подобно глобальным. То есть, каждая локальная переменная получала имя и распределенный адрес как любая другая переменная и к ней обращались по этому имени.
Нам это легко сделать, используя уже имеющийся механизм распределения. Помните, однако, что локальные переменные могут иметь те же самые имена, что и глобальные переменные. Мы так или иначе должны согласиться с этим, назначая уникальные имена для этих переменных.
Характерная особенность статического хранения в том, что данные выживают при вызове процедуры и возврате. Когда процедура вызывается снова, данные все еще будут здесь. Это может быть преимуществом в некоторых приложениях. Во времена FORTRAN мы применяли такой прием как инициализация флажка, чтобы вы могли сказать когда мы входим в процедуру первый раз и могли бы выполнить любую первоначальную инициализацию, которую необходимо выполнить.
Конечно, эта же «особенность» статического хранения делает рекурсию невозможной. Любое новое обращение к процедуре перепишет данные уже находящиеся в локальных переменных.
Альтернативой является динамическое хранение, при котором память распределяется в стеке точно также как и для переданных параметров. Для это мы уже имеем готовый механизм. Фактически, те же самые подпрограммы, которые работают с переданными (по значению) параметрами в стеке, могут так же легко работать и с локальными переменными... генерируемый код тот же самый. Назначение смещения в инструкции 68000 LINK сейчас такое: мы можем использовать его для регулировки указателя стека при выделении места для локальных переменных. Динамическое хранение, конечно, по существу поддерживает рекурсию.
Когда я впервые начал планировать TINY, я должен признаться имел предубеждение в пользу статического хранения. Просто потому, что старые FORTRAN программы были чрезвычайно эффективны... ранние компиляторы FORTRAN производили качественный код, который и сейчас редко сопоставим с современными компиляторами. Даже сегодня данная программа, написанная на FORTRAN вероятно превзойдет ту же самую программу написанную на C или Pascal, иногда с большим отрывом. (Вот так! Что вы скажете на это заявление!)
Я всегда полагал, что причина имела отношение к двум основным различиям между реализациями Фортрана и другими языками: статическое хранение и передача по ссылке. Я знаю, что динамическое хранение поддерживает рекурсию, но мне всегда казалось немного странным желание мириться с более медленным кодом, который в 95% случаев не нуждается в рекурсии, только для того чтобы получить эту возможность когда она понадобится. Идея состоит в том, что со статическим хранением вы можете использовать не косвенную а абсолютную адресацию, которая должна привести к более быстрому коду.
Позднее, однако, некоторые люди указали мне, что в действительности нет никаких падений производительности связанной с динамическим хранением. Для 68000, к примеру, вы в любом случае не должны использовать абсолютную адресацию... большинство операционных систем требуют переместимый код. И команда 68000
MOVE 8(A6),D0
имеет тоже самое время выполнения, что и
MOVE X(PC),D0.
Так что теперь я убежден, что нет никакой важной причины не использовать динамическое хранение.
Так как такое использование локальных переменных так хорошо соответствует схеме передачи параметров по значению, мы будем использовать эту версию транслятора для иллюстрации (я надеюсь вы сохранили копию!).
Основная идея состоит в том, чтобы отслеживать количество локальных параметров. Затем мы используем это число в инструкции LINK для корректировки указателя стека при выделения для них места. Формальные параметры адресуются как положительные смещения от указателя кадра а локальные как отрицательные смещения. С небольшой доработкой те же самые процедуры, которые мы уже создали, могут позаботиться обо всем этом.
Давайте начнем с создания новой переменной Base:
var Base: integer;
Мы будем использовать эту переменную вместо NumParams для вычисления смещения стека. Это подразумевает изменение двух ссылок на NumParams в LoadParam и StoreParam:
Все что мы должны сделать дальше – это установить семантику объявления локальных переменных в синтаксическом анализаторе. Подпрограммы очень похожи на Decl и TopDecls:
Затем мы изменим DoProc для использования этой информации:
Обратите внимание на изменения при вызове ProcProlog. Новый параметр – это число слов (не байт) для распределения памяти. Вот новая версия ProcProlog:
И снова здесь мы стоим перед выбором: статическое или динамическое хранение?
В старых FORTRAN программах локальные переменные использовали статическое хранение подобно глобальным. То есть, каждая локальная переменная получала имя и распределенный адрес как любая другая переменная и к ней обращались по этому имени.
Нам это легко сделать, используя уже имеющийся механизм распределения. Помните, однако, что локальные переменные могут иметь те же самые имена, что и глобальные переменные. Мы так или иначе должны согласиться с этим, назначая уникальные имена для этих переменных.
Характерная особенность статического хранения в том, что данные выживают при вызове процедуры и возврате. Когда процедура вызывается снова, данные все еще будут здесь. Это может быть преимуществом в некоторых приложениях. Во времена FORTRAN мы применяли такой прием как инициализация флажка, чтобы вы могли сказать когда мы входим в процедуру первый раз и могли бы выполнить любую первоначальную инициализацию, которую необходимо выполнить.
Конечно, эта же «особенность» статического хранения делает рекурсию невозможной. Любое новое обращение к процедуре перепишет данные уже находящиеся в локальных переменных.
Альтернативой является динамическое хранение, при котором память распределяется в стеке точно также как и для переданных параметров. Для это мы уже имеем готовый механизм. Фактически, те же самые подпрограммы, которые работают с переданными (по значению) параметрами в стеке, могут так же легко работать и с локальными переменными... генерируемый код тот же самый. Назначение смещения в инструкции 68000 LINK сейчас такое: мы можем использовать его для регулировки указателя стека при выделении места для локальных переменных. Динамическое хранение, конечно, по существу поддерживает рекурсию.
Когда я впервые начал планировать TINY, я должен признаться имел предубеждение в пользу статического хранения. Просто потому, что старые FORTRAN программы были чрезвычайно эффективны... ранние компиляторы FORTRAN производили качественный код, который и сейчас редко сопоставим с современными компиляторами. Даже сегодня данная программа, написанная на FORTRAN вероятно превзойдет ту же самую программу написанную на C или Pascal, иногда с большим отрывом. (Вот так! Что вы скажете на это заявление!)
Я всегда полагал, что причина имела отношение к двум основным различиям между реализациями Фортрана и другими языками: статическое хранение и передача по ссылке. Я знаю, что динамическое хранение поддерживает рекурсию, но мне всегда казалось немного странным желание мириться с более медленным кодом, который в 95% случаев не нуждается в рекурсии, только для того чтобы получить эту возможность когда она понадобится. Идея состоит в том, что со статическим хранением вы можете использовать не косвенную а абсолютную адресацию, которая должна привести к более быстрому коду.
Позднее, однако, некоторые люди указали мне, что в действительности нет никаких падений производительности связанной с динамическим хранением. Для 68000, к примеру, вы в любом случае не должны использовать абсолютную адресацию... большинство операционных систем требуют переместимый код. И команда 68000
MOVE 8(A6),D0
имеет тоже самое время выполнения, что и
MOVE X(PC),D0.
Так что теперь я убежден, что нет никакой важной причины не использовать динамическое хранение.
Так как такое использование локальных переменных так хорошо соответствует схеме передачи параметров по значению, мы будем использовать эту версию транслятора для иллюстрации (я надеюсь вы сохранили копию!).
Основная идея состоит в том, чтобы отслеживать количество локальных параметров. Затем мы используем это число в инструкции LINK для корректировки указателя стека при выделения для них места. Формальные параметры адресуются как положительные смещения от указателя кадра а локальные как отрицательные смещения. С небольшой доработкой те же самые процедуры, которые мы уже создали, могут позаботиться обо всем этом.
Давайте начнем с создания новой переменной Base:
var Base: integer;
Мы будем использовать эту переменную вместо NumParams для вычисления смещения стека. Это подразумевает изменение двух ссылок на NumParams в LoadParam и StoreParam:
{–}Идея состоит в том, что значение Base будет заморожено после того, как мы обработаем формальные параметры и не будет увеличиваться дальше когда новые, локальные, переменные будут вставлены в таблицу идентификаторов. Об этом позаботится код в конце FormalList:
{ Load a Parameter to the Primary Register }
procedure LoadParam(N: integer);
var Offset: integer;
begin
Offset := 8 + 2 * (Base – N);
Emit('MOVE ');
WriteLn(Offset, '(A6),D0');
end;
{–}
{ Store a Parameter from the Primary Register }
procedure StoreParam(N: integer);
var Offset: integer;
begin
Offset := 8 + 2 * (Base – N);
Emit('MOVE D0,');
WriteLn(Offset, '(A6)');
end;
{–}
{–}(Мы добавили четыре слова чтобы учесть адрес возврата и старый указатель кадра, который заканчивается между формальными параметрами и локальными переменными.)
{ Process the Formal Parameter List of a Procedure }
procedure FormalList;
begin
Match('(');
if Look <> ')' then begin
FormalParam;
while Look = ',' do begin
Match(',');
FormalParam;
end;
end;
Match(')');
Fin;
Base := NumParams;
NumParams := NumParams + 4;
end;
{–}
Все что мы должны сделать дальше – это установить семантику объявления локальных переменных в синтаксическом анализаторе. Подпрограммы очень похожи на Decl и TopDecls:
{–}Заметьте, что LocDecls является функцией, возвращающей число локальных переменных в DoProc.
{ Parse and Translate a Local Data Declaration }
procedure LocDecl;
var Name: char;
begin
Match('v');
AddParam(GetName);
Fin;
end;
{–}
{ Parse and Translate Local Declarations }
function LocDecls: integer;
var n: integer;
begin
n := 0;
while Look = 'v' do begin
LocDecl;
inc(n);
end;
LocDecls := n;
end;
{–}
Затем мы изменим DoProc для использования этой информации:
{–}(Я сделал пару изменений, которые не были в действительности необходимы. Кроме небольшой реорганизации я переместил вызов Fin в FormalList а также в LocDecls. Не забудьте поместить его в конец FormalList.)
{ Parse and Translate a Procedure Declaration }
procedure DoProc;
var N: char;
k: integer;
begin
Match('p');
N := GetName;
if InTable(N) then Duplicate(N);
ST[N] := 'p';
FormalList;
k := LocDecls;
ProcProlog(N, k);
BeginBlock;
ProcEpilog;
ClearParams;
end;
{–}
Обратите внимание на изменения при вызове ProcProlog. Новый параметр – это число слов (не байт) для распределения памяти. Вот новая версия ProcProlog:
{–}Сейчас должно работать. Добавьте эти изменения и посмотрите как они работают.
{ Write the Prolog for a Procedure }
procedure ProcProlog(N: char; k: integer);
begin
PostLabel(N);
Emit('LINK A6,#');
WriteLn(-2 * k)
end;
{–}
Заключение
К этому моменту вы знаете как компилировать объявления и вызовы процедур с параметрами, передаваемыми по ссылке и по значению. Вы можете также обрабатывать локальные переменные. Как вы можете видеть, сложность состоит не в предоставлении механизма, а в определении какой механизм использовать. Стоит нам принять эти решения и код для трансляции в действительности не будет таким сложным.
Я не показал вам как работать с комбинацией локальных параметров и передачей параметров по ссылке, но это простое расширение того, что вы уже видели. Это просто немного более хлопотно и все, так как мы должны поддерживать оба механизма вместо одного. Я предпочел оставить это на потом, когда мы научимся работать с различными типами переменных.
Это будет следующая глава, которая появится ближе к Форуму.
Увидимся.
Я не показал вам как работать с комбинацией локальных параметров и передачей параметров по ссылке, но это простое расширение того, что вы уже видели. Это просто немного более хлопотно и все, так как мы должны поддерживать оба механизма вместо одного. Я предпочел оставить это на потом, когда мы научимся работать с различными типами переменных.
Это будет следующая глава, которая появится ближе к Форуму.
Увидимся.
Типы
Введение
В последней главе (Часть 13, Процедуры) я упомянул, что в ней и в следующей главе мы рассмотрим две возможности, которые помогут отделить игрушечный язык от настоящего, пригодного к использованию. В ней мы рассмотрели вызовы процедур. Многие из вас терпеливо ждали, начиная с Августа'89 когда я выдам вторую. Хорошо, вот она.
В этой главе мы поговорим о том, как работать с различными типами данных. Как и в последней главе, я не буду сейчас включать эти возможности непосредственно в компилятор TINY. Вместо этого я буду использовать тот же самый подход, который так хорошо служил нам в прошлом: использование только фрагментов синтаксического анализатора и односимвольных токенов. Как обычно, это позволит на добраться непосредственно до сути вопроса не продираясь сквозь массу ненужного кода. Так как основные проблемы при работе с множественными типами данных возникают в арифметических операциях, на них мы и сконцентрируем свое внимание.
Несколько предупреждений: Во-первых, есть некоторые типы, которые я не буду охватывать в этой главе. Здесь мы будем говорить только о простых, встроенных типах. Мы даже не будем работать с массивами, указателями или строками, я охвачу их в следующих нескольких главах.
Во-вторых, мы также не будем обсуждать и типы определяемые пользователем. Это будет значительно позже, по той простой причине, что я все еще не убедил себя, что эти определяемые пользователем типы должны быть в языке KISS. В более поздних главах я собираюсь охватить по крайней мере основные концепции определяемых пользователем типов, записей и т.д., просто для того, чтобы серия была полной. Но действительно ли они будут включены как часть KISS – все еще открытый вопрос. Я открыт для комментариев и предложений по этой теме.
Наконец, я должен предупредить вас: то, что мы собираемся сделать может добавить массу дополнительных сложностей и в синтаксический анализатор и в генерируемый код. Поддерживать различные типы достаточно просто. Сложность возникает когда вы добавляете правила преобразования между типами. Вообще-то, будет ли ваш компилятор простым или сложным зависит от способа, выбранного вами для определения правил преобразования типов. Даже если вы решите запретить любые преобразования типов (как в Ada, например) проблема все еще остается, и она встроена в математику. Когда вы умножаете два коротких числа, к примеру, вы можете получить длинный результат.
Я подошел к этой проблеме очень осторожно, пытаясь сохранить простоту. Но мы не можем полностью избежать сложности. Как обычно случается, мы оказываемся перед необходимостью выбирить между качеством кода и сложностью и, как обычно, я предпочитаю выбрать самый простой подход.
В этой главе мы поговорим о том, как работать с различными типами данных. Как и в последней главе, я не буду сейчас включать эти возможности непосредственно в компилятор 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, я буду счастлив обсудить коллективные авторские права и авторские отчисления от предстоящей книги.
Но хватит говорить. Давайте приступим к изучению типов. Как я сказал ранее, мы будем делать это как и в последней главе: выполняя эксперименты с использованием односимвольных токенов.
Тем временем я не бездействовал. Я разбил компилятор на модули. Одна из проблем, с которыми я столкнулся, в том, что так как мы охватывали новые области и вследствие этого расширяли возможности компилятора 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, я буду счастлив обсудить коллективные авторские права и авторские отчисления от предстоящей книги.
Но хватит говорить. Давайте приступим к изучению типов. Как я сказал ранее, мы будем делать это как и в последней главе: выполняя эксперименты с использованием односимвольных токенов.
Таблица идентификаторов
Должно быть очевидным, что если мы собираемся работать с переменными различных типов, нам понадобится какое-то место для записи их типов. Очевидным средством для этого является таблица идентификаторов и мы уже использовали ее например для различия между локальными и глобальными переменными и между переменными и процедурами.
Структура таблицы идентификаторов для односимвольных токенов особенно проста и мы использовали ее прежде несколько раз. Для работы с ней, мы возьмем некоторые процедуры, которые мы использовали раньше.
Сначала, нам необходимо объявить саму таблицу идентификаторов:
Если вы осторожный тип (как я), вам возможно захотелось бы начать с тестовой программы, которая ничего не делает а просто инициализирует таблицу и затем создает ее дамп. Только для того, чтобы быть уверенным, что все мы находимся на одной волне, ниже я воспроизвожу всю программу, дополненную новыми процедурами. Заметьте, что эта версия включает поддержку пробелов:
Структура таблицы идентификаторов для односимвольных токенов особенно проста и мы использовали ее прежде несколько раз. Для работы с ней, мы возьмем некоторые процедуры, которые мы использовали раньше.
Сначала, нам необходимо объявить саму таблицу идентификаторов:
{–}Затем мы должны удостовериться, что она инициализируется в процедуре Init:
{ Variable Declarations }
var Look: char; { Lookahead Character }
ST: Array['A'..'Z'] of char; { *** ДОБАВЬТЕ ЭТУ СТРОКУ ***}
{–}
{–}Следующая процедура в действительности нам не нужна, но она будет полезна для отладки. Все, что она делает, это формирует дамп содержимого таблицы идентификаторов:
{ 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);