ВВЕДЕНИЕ Наконец-то мы принимаемся за
хорошую главу!
ПОСЛЕДНЕЕ ОТКЛОНЕНИЕ Эта глава была необычайно трудной для
меня. Причина не имеет никакого отношения непосредственно к теме... я знал,
о чем хотел рассказать уже какое-то время, и фактически я представил большинство
из этого на Software Development '89, в феврале. Больше это имело
отношение к подходу. Позвольте мне объяснить.
ОСНОВЫ Все современные центральные процессоры
предоставляют прямую поддержку вызовов процедур и 68000 не исключение.
Для 68000 вызов - BSR (PC-относительная версия) или JSR, и возвращение
RTS. Все что мы должны сделать это организовать для компилятора выдачу
этих команд в соответствующих местах.
ОСНОВА ДЛЯ ЭКСПЕРИМЕНТОВ Как всегда нам понадобится некоторое
программное обеспечение, которое послужит нам как основание для того, что
мы делаем. Нам не нужна полная версия компилятора TINY но нам нужна достаточная
его часть для того, чтобы некоторые конструкции были представлены. В частности,
нам нужна по крайней мере возможность обрабатывать утверждения некоторых
видов и объявления данных.
<ident> = <ident> Другими словами, единственным допустимым
выражением является одиночное имя переменной. Нет никаких управляющих конструкций...
единственным допустимым утверждением является присваивание.
{--------------------------------------------------------------}
{--------------------------------------------------------------}
const TAB = ^I;
{--------------------------------------------------------------}
var Look: char; { Lookahead Character } var ST: Array['A'..'Z'] of char; {--------------------------------------------------------------}
procedure GetChar;
{--------------------------------------------------------------}
procedure Error(s: string);
{--------------------------------------------------------------}
procedure Abort(s: string);
{--------------------------------------------------------------}
procedure Expected(s: string);
{--------------------------------------------------------------}
procedure Undefined(n: string);
{--------------------------------------------------------------}
procedure Duplicate(n: string);
{--------------------------------------------------------------}
function TypeOf(n: char): char;
{--------------------------------------------------------------}
function InTable(n: char): Boolean;
{--------------------------------------------------------------}
procedure AddEntry(Name, T: char);
{--------------------------------------------------------------}
procedure CheckVar(Name: 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;
{--------------------------------------------------------------}
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 PostLabel(L: string);
{--------------------------------------------------------------}
procedure LoadVar(Name: char);
{--------------------------------------------------------------}
procedure StoreVar(Name: char);
{--------------------------------------------------------------}
procedure Init;
{--------------------------------------------------------------}
procedure Expression;
{--------------------------------------------------------------}
procedure Assignment;
{--------------------------------------------------------------}
procedure DoBlock;
{--------------------------------------------------------------}
procedure BeginBlock;
{--------------------------------------------------------------}
procedure Alloc(N: char);
{--------------------------------------------------------------}
procedure Decl;
{--------------------------------------------------------------}
procedure TopDecls;
{--------------------------------------------------------------}
begin
Обратите внимание, что у нас есть таблица
идентификаторов и есть логика для проверки допустимости имени переменной.
Также стоит обратить внимание на то, что я включил код, который вы
видели ранее для поддержки пробелов и переносов строк. Наконец заметьте,
что основная программа ограничена как обычно операторными скобками BEGIN-END.
va
(для VAR A)
Как обычно, вы должны сделать некоторые преднамеренные ошибки и проверить, что программа правильно их отлавливает. ОБЪЯВЛЕНИЕ ПРОЦЕДУРЫ Если вы удовлетворены, как работает
наша маленькая программа, тогда пришло время поработать с процедурами.
Так как мы еще не говорили о параметрах мы начнем с рассмотрения только
таких процедур которые не имеют списка параметров.
PROGRAM FOO;
BEGIN { MAIN PROGRAM }
MAIN:
Здесь я показал конструкции высокоуровневого
языка слева и желаемый ассемблерный код справа. Прежде всего заметьте,
что здесь несомненно нам не нужно генерировать много кода! Для большей
части процедуры и основной программы наши существующие конструкции позаботятся
о генерируемом коде.
<declaration> ::= <data decl> | <procedure> Это означает, что можно легко изменить TopDecl для работы с процедурами. Как насчет синтаксиса процедуры? Хорошо, вот предлагаемый синтаксис, который по существу такой же как и в Pascal: <procedure> ::= PROCEDURE <ident> <begin-block> Здесь практически не требуется никакой
генерации кода., кроме генерации внутри блока begin. Мы должны только выдать
метку в начале процедуры и RTS в конце.
{--------------------------------------------------------------}
procedure DoProc;
Обратите внимание, что я добавил новую
подпрограмму генерации кода Return, которая просто выдает инструкцию RTS.
Создание этой подпрограммы "оставлено как упражнение студенту".
'p': DoProc; Я должен упомянуть, что эта структура для объявлений и БНФ, которая управляет ей, отличается от стандартного Паскаля. В определении Паскаля от Дженсена и Вирта объявления переменных и, фактически, все виды объявлений, должны следовать в определенном порядке, т.е. метки, константы, типы, переменные, процедуры и основная программа. Чтобы следовать такой схеме, мы должны разделить два объявления и написать в основной программе что-нибудь вроде: DoVars;
Однако, большинство реализаций Паскаля,
включая Turbo, не требуют такого порядка и позволяют вам свободно перемешивать
различные объявления до тех пор пока вы не попробуете обратиться к чему-то
прежде, чем это будет объявлено. Хотя это может быть больше эстетическим
удовлетворением объявлять все глобальные переменные на вершине программы,
конечно не будет никакого вреда от того, чтобы разрешить расставлять их
в любом месте. Фактически, это может быть даже немного полезно, в том смысле,
что это даст нам возможность выполнять небольшое элементарное скрытие
информации. Переменные, к которым нужно обращатся только из основной программы,
к примеру, могут быть объявлены непосредственно перед ней и будут
таким образом недоступны для процедур.
{--------------------------------------------------------------}
procedure DoMain;
begin
Обратите внимание, что DoProc и DoMain
не совсем симметричны. DoProc использует вызов BeginBlock тогда как DoMain
нет. Это из-за того, что начало процедуры определяется по ключевому слову
PROCEDURE (в данном случае сокращенно 'p'), тогда как основная программа
не имеет никакого другого ключевого слова кроме непосредственно BEGIN.
BEGIN { of MAIN } Это всегда казалось мне немного клуджем.
Возникает вопрос: Почему обработка основной программы должна так отличаться
от обработки процедур? Теперь, когда мы осознали, что объявление процедур
это просто... часть глобальных объявлений... не является ли основная программа
также просто еще одним объявлением?
<declaration> ::= <data decl> | <procedure> | <main program> <procedure> ::= PROCEDURE <ident> <begin-block> <main program> ::= PROGRAM <ident> <begin-block> Код также смотрится намного лучше, по крайней мере в том смысле, что DoMain и DoProc выглядят более похоже: {--------------------------------------------------------------}
procedure DoMain;
procedure TopDecls;
{--------------------------------------------------------------}
begin
Так как объявление основной программы
теперь внутри цикла TopDecl, возникают некоторые трудности. Как мы можем
гарантировать, что она - последняя в файле? И выйдем ли мы когда либо из
цикла? Мой ответ на второй вопрос, как вы можете видеть, - в том, чтобы
вернуть нашего старого друга точку. Как только синтаксический анализатор
увидит ее дело сделано.
ВЫЗОВ ПРОЦЕДУРЫ Если вы удовлетворены работой программы,
давайте обратимся ко второй половине уравнения... вызову.
<proc_call> ::= <identifier> с другой стороны БНФ для операции присваивания: <assignment> ::= <identifier> '=' <expression> Кажется у нас проблема. Оба БНФ утверждения
с правой стороны начинаются с токена <identifier>. Как мы предполагаем
узнать, когда мы видим идентификатор, имеем ли мы вызов процедуры или операцию
присваивания? Это похоже на случай, когда наш синтаксический анализатор
перестает быть предсказывающим и действительно это точно такой случай.
Однако, оказывается эту проблему легко решить, так как все, что мы должны
сделать - посмотреть на тип идентификатора записанный в таблице идентификаторов.
Как мы обнаружили раньше, небольшое локальное нарушение правила предсказывающего
синтаксического анализа может быть легко обработано как специальный случай.
{--------------------------------------------------------------}
procedure Assignment(Name: char);
{--------------------------------------------------------------}
procedure AssignOrProc;
{--------------------------------------------------------------}
procedure DoBlock;
Как вы можете видеть, процедура Block сейчас вызывает AssignOrProc вместо Assignment. Назначение этой новой процедуры просто считать идентификатор, определить его тип и затем вызвать процедуру, соответствующую этому типу. Так как имя уже прочитано, мы должны передать его в эти две процедуры и соответственно изменить Assignment. Процедура CallProc - это просто подпрограмма генерации кода: {--------------------------------------------------------------}
procedure CallProc(N: char);
Хорошо, к этому моменту у нас есть
компилятор, который может работать с процедурами. Стоить отметить, что
процедуры могут вызывать процедуры с любой степенью вложенности. Так что,
даже хотя мы и не разрешаем вложенные объявления, нет ничего, чтобы удерживало
нас от вложенных вызовов, точно так, как мы ожидали бы на любом языке.
Мы получили это и это было не слишом сложно, не так ли?
ПЕРЕДАЧА ПАРАМЕТРОВ Снова, все мы знаем основную идею передачи
параметров, но давайте просто для надежности разберем ее заново.
PROCEDURE FOO(X, Y, Z) В объявлении процедуры параметры называются
формальными параметрами и могут упоминаться в теле процедуры по своим именам.
Имена, используемые для формальных параметров в действительности произвольны.
Учитывается только позиция. В примере выше имя 'X' просто означает "первый
параметр" везде, где он используется.
<procedure> ::= PROCEDURE <ident> '(' <param-list> ')' <begin-block> <param_list> ::= <parameter> ( ',' <parameter> )* | null Аналогично, вызов процедуры выглядит так: <proc call> ::= <ident> '(' <param-list> ')' Обратите внимание, что здесь уже есть неявное решение, встроенное в синтаксис. Некоторые языки, такие как Pascal и Ada разрешают списку параметров быть необязательным. Если нет никаких параметров, вы просто полностью отбрасываете скобки. Другие языки, типа C и Modula-2, требуют скобок даже если список пустой. Ясно, что пример, который мы только что привели, соответствует первой точке зрения. Но, сказать правду, я предпочитаю последний. Для одних процедур решение кажется должно быть в пользу "безсписочного" подхода. Оператор Initialize; , стоящий отдельно, может означать только
вызов процедуры. В синтаксических анализаторах, которые мы писали, мы преимущественно
использовали процедуры без параметров и было бы позором каждый раз заставлять
писать пустую пару скобок.
{--------------------------------------------------------------}
procedure FormalList;
В процедуру DoProc необходимо добавить строчку для вызова FormalList: {--------------------------------------------------------------}
procedure DoProc;
Сейчас код для FormalParam всего лишь пустышка, который просто пропускает имена переменных: {--------------------------------------------------------------}
procedure FormalParam;
Для фактического вызова процедуры должен быть аналогичный код для обработки списка фактических параметров: {--------------------------------------------------------------}
procedure Param;
{--------------------------------------------------------------}
procedure ParamList;
{--------------------------------------------------------------}
procedure CallProc(Name: char);
Обратите внимание, что CallProc больше
не является просто простой подпрограммой генерации кода. Она имеет некоторую
структуру. Для обработки я переименовал подпрограмму генерации кода
в просто Call и вызвал ее из CallProc.
СЕМАНТИКА ПАРАМЕТРОВ До этого мы имели дело с синтаксисом
передачи параметров и получили механизм синтаксического анализа для его
обработки. Сейчас мы должны рассмотреть семантику, т.е. действия, которые
должны быть предприняты когда мы столкнемся с параметрами. Это ставит нас
перед вопросом выбора способа передачи параметров.
Старые компиляторы Фортрана передавали все параметры по ссылке. Другими словами, фактически передавался адрес параметра. Это означало, что вызываемая подпрограмма была вольна и считывать и изменять этот параметр, что часто и происходило, как будто это была просто глобальная переменная. Это был фактически самый эффективный способ и он был довольно простым, так как тот же самый механизм использовался во всех случаях с одним исключением, которое я кратко затрону. Хотя имелись и проблемы. Многие люди чувствовали, что этот метод создавал слишком большую связь между вызванной и вызывающей подпрограммой. Фактически, это давало подпрограмме полный доступ ко всем переменным, которые появлялись в списке параметров. Часто нам не хотелось бы фактически изменять параметр а только использовать его как входные данные. К примеру, мы могли бы передавать счетчик элементов в подпрограмму и хотели бы затем использовать этот счетчик в цикле DO. Во избежание изменения значения в вызываемой программе мы должны были сделать локальную копию входного параметра и оперировать только его копией. Некоторые программисты на Фортране фактически сделали практикой копирование всех параметров, исключая те, которые должны были использоваться как возвращаемые значения. Само собой разумеется, все это копирование победило добрую часть эффективности, связанной с этим методом. Существовала, однако, еще более коварная проблема, которая была в действительности не просто ошибкой соглашения "передача по ссылке", а плохой сходимостью нескольких решений реализации. Предположим, у нас есть подпрограмма: SUBROUTINE FOO(X, Y, N) где N - какой-то входной счетчик или флажок. Часто нам бы хотелось иметь возможность передавать литерал или даже выражение вместо переменной, как например: CALL FOO(A, B, J + 1) Третий параметр не является переменной, и поэтому он не имеет никакого адреса. Самые ранние компиляторы Фортрана не позволяли таких вещей, так что мы должны были прибегать к ухищрениям типа: K = J + 1
Здесь снова требовалось копирование
и это бремя ложилось на программистов. Не хорошо.
CALL FOO(A, B, 4) Казалось неэффективным подходить к
проблеме "вычисления" такого целого числа и сохранять его во временной
переменной только для передачи через список параметров. Так как мы в любом
случае передавали адрес, казалось имелся большой смысл в том, чтобы просто
передавать адрес целочисленного литерала, 4 в примере выше.
ПЕРЕДАЧА ПО ЗНАЧЕНИЮ Давайте просто попробуем некоторые нехитрые вещи и посмотрим, куда они нас приведут. Давайте начнем со случая передачи по значению. Рассмотрим вызов процедуры: FOO(X, Y) Почти единственным приемлемым способом передачи данных является передача через стек ЦПУ. Поэтому, код который мы бы хотели видеть сгенерированным мог бы выглядеть так: MOVE X(PC),-(SP)
; Push X
Это конечно не выглядит слишком сложным!
Так что значения параметров имеют адреса с фиксированными смещениями от указателя стека. В этом примере адреса такие:
Теперь рассмотрим, на что могла бы походить вызываемая процедура: PROCEDURE FOO(A, B)
(Помните, что имена формальных параметров
произвольные... учитываются только позиции).
FOO: MOVE 4(SP),D0
Обратите внимание, что для адресации
формальных параметров нам будет необходимо знать, какую позицию они занимают
в списке параметров. Это подразумевает некоторые изменения в содержимом
таблицы идентификаторов. Фактически, в нашем односимвольном случае лучше
всего просто создать новую таблицу идентификаторов для формальных параметров.
var Params: Array['A'..'Z'] of integer; Нам также необходимо отслеживать, сколько параметров имеет данная процедура: var NumParams: integer; И мы должны инициализировать новую таблицу. Теперь, не забудьте, что список формальных параметров будет различным для каждой процедуры, которые мы обрабатываем, так что мы будем должны инициализировать эту таблицу заново для каждой процедуры. Вот инициализатор: {--------------------------------------------------------------}
procedure ClearParams;
Мы поместим обращение к этой процедуре в Init и также в конец DoProc: {--------------------------------------------------------------}
procedure Init;
procedure DoProc;
Обратите внимание, что вызов внутри
DoProc гарантирует, что таблица будет чиста, когда мы в основной программе.
{--------------------------------------------------------------}
function ParamNumber(N: char): integer;
{--------------------------------------------------------------}
function IsParam(N: char): boolean;
{--------------------------------------------------------------}
procedure AddParam(Name: char);
Наконец, нам понадобятся некоторые подпрограммы генерации кода: {--------------------------------------------------------------}
procedure LoadParam(N: integer);
{--------------------------------------------------------------}
procedure StoreParam(N: integer);
{--------------------------------------------------------------}
procedure Push;
(Последнюю подпрограмму мы уже видели
прежде, но ее не было в этой остаточной версии программы.)
{--------------------------------------------------------------}
procedure FormalParam;
Теперь, что делать с формальными параметрами, когда они появляются в теле процедуры? Это требует немного больше работы. Мы должны сначала определить, что это формальный параметр. Чтобы сделать это, я написал модифицированную версию TypeOf: {--------------------------------------------------------------}
function TypeOf(n: char): char;
(Обратите внимание, что так как TypeOf
теперь вызывает IsParam, возможно будет необходимо изменить ее местоположение
в программе.)
{--------------------------------------------------------------}
procedure AssignOrProc;
Наконец, код для обработки операции присваивания и выражения должен быть расширен: {--------------------------------------------------------------}
procedure Expression;
{--------------------------------------------------------------}
procedure Assignment(Name: char);
Как вы можете видеть, эти процедуры
обработают каждое встретившееся имя переменной или как формальный параметр
или как глобальную переменную, в зависимости от того, появляется ли оно
в таблице идентификаторов параметров. Запомните, что мы используем только
остаточную форму Expression. В конечной программе изменения, показанные
здесь, должны быть добавлены в Factor а не Expression.
{--------------------------------------------------------------}
procedure Param;
Так вот. Добавьте эти изменения в вашу программу и испытайте ее. Попробуйте объявить одну или две процедуры, каждая со списком формальных параметров. Затем сделайте какие-нибудь присваивания, используя комбинации глобальных и формальных параметров. Вы можете вызывать одну процедуру из другой, но вы не можете объявлять вложенные процедуры. Вы можете даже передавать формальные параметры из одной процедуры в другую. Если бы мы имели здесь полный синтаксис языка, вы могли бы также читать и выводить формальные параметры или использовать их в сложных выражениях. ЧТО НЕПРАВИЛЬНО? Тут вы могли бы подумать: Уверен, здесь
должно быть что-то большее чем несколько сохранений и восстановлений из
стека. Для передачи параметров здесь должно быть что-то большее чем тут
есть.
{--------------------------------------------------------------}
function ParamList: integer;
Процедура CallProc затем использует его для очистки стека: {--------------------------------------------------------------}
procedure CallProc(Name: char);
Здесь я создал еще одну подпрограмму генерации кода: {--------------------------------------------------------------}
procedure CleanStack(N: integer);
ОК, если вы добавили этот код в ваш
компилятор, я думаю вы убедитесь, что стек теперь под контролем.
PROCEDURE FOO(A, B)
Код, сгенерированный нехитрым синтаксическим анализатором, мог бы быть: FOO: MOVE 6(SP),D0
; Извлечь A
Это было бы неправильно. Когда мы помещаем
первый аргумент в стек, смещения для двух формальных параметров больше
не 4 и 6, я 6 и 8. Поэтому вторая выборка вернула бы снова A а не B.
FOO: LINK A6,#0
Исправить компилятор для генерации этого кода намного проще чем объяснить. Все, что нам нужно сделать - изменить генерацию кода в DoProc. Так как из-за этого код становится немного больше одной строки, я создал новые процедуры, схожие с процедурами Prolog и Epilog, вызываемыми DoMain: {--------------------------------------------------------------}
procedure ProcProlog(N: char);
{--------------------------------------------------------------}
procedure ProcEpilog;
Процедура DoProc теперь просто вызывает их: {--------------------------------------------------------------}
procedure DoProc;
В заключение, мы должны изменить ссылки на SP в процедурах LoadParam и StoreParam: {--------------------------------------------------------------}
procedure LoadParam(N: integer);
{--------------------------------------------------------------}
procedure StoreParam(N: integer);
(Заметьте, что вычисление Offset изменяется
чтобы учесть дополнительное сохранение A6.)
У нас нет способа возвратить результат в вызывающую программу! Но это, конечно, не ограничение генерируемого нами кода, а ограничение, свойственное протоколу передачи по значению. Обратите внимание, что мы можем использовать формальные параметры любым способом внутри процедуры. Мы можем вычислять для них новое значение, использовать их как счетчики циклов (если бы мы имели циклы!) и т.д. Так что код делает то, что предполагается. Чтобы решить эту последнюю проблему мы должны рассмотреть альтернативный протокол. ПЕРЕДАЧА ПО ССЫЛКЕ Это просто теперь, когда мы уже имеем
механизм. Мы только должны внести несколько изменений в генерацию кода.
Вместо помещения значения в стек, мы должны помещать адрес. Оказывается,
68000 имеет инструкцию PEA которая как раз делает это.
FOO(X, Y) оттранслировать в: PEA X(PC)
; Сохранить адрес X
Это просто вопрос небольших изменений в Param: {--------------------------------------------------------------}
procedure Param;
(Обратите внимание, что при передачей
по ссылке мы не можем использовать выражения в списке параметров, поэтому
Param может просто непосредственно считывать имя).
FOO: LINK A6,#0
Все это может быть обработано с изменениями в LoadParam and StoreParam: {--------------------------------------------------------------}
procedure LoadParam(N: integer);
{--------------------------------------------------------------}
procedure StoreParam(N: integer);
Для правильного расчета, мы также должны изменить одну строку в ParamList: ParamList := 4 * N; Теперь должно работать. Испытайте компилятор
и посмотрите, генерирует ли он приемлемый код. Как вы увидите, код вряд
ли оптимален, так как мы перезагружаем регистр адреса каждый раз, когда
необходим параметр. Но это соответствует нашему принципу KISS - просто
генерировать код который работает. Мы только сделаем здесь небольшое замечание,
что есть еще один кандидат для оптимизации и пойдем дальше.
ЛОКАЛЬНЫЕ ПЕРЕМЕННЫЕ Пока мы не сказали ничего о локальных
переменных и наше определение процедур не разрешает их. Само собой разумеется,
что это большой пробел в нашем языке и он должен быть исправлен.
MOVE 8(A6),D0 имеет тоже самое время выполнения, что и MOVE X(PC),D0. Так что теперь я убежден, что нет никакой
важной причины не использовать динамическое хранение.
var Base: integer; Мы будем использовать эту переменную вместо NumParams для вычисления смещения стека. Это подразумевает изменение двух ссылок на NumParams в LoadParam и StoreParam: {--------------------------------------------------------------}
procedure LoadParam(N: integer);
{--------------------------------------------------------------}
procedure StoreParam(N: integer);
Идея состоит в том, что значение Base будет заморожено после того, как мы обработаем формальные параметры и не будет увеличиваться дальше когда новые, локальные, переменные будут вставлены в таблицу идентификаторов. Об этом позаботится код в конце FormalList: {--------------------------------------------------------------}
procedure FormalList;
(Мы добавили четыре слова чтобы учесть
адрес возврата и старый указатель кадра, который заканчивается между формальными
параметрами и локальными переменными.)
{--------------------------------------------------------------}
procedure LocDecl;
{--------------------------------------------------------------}
function LocDecls: integer;
Заметьте, что LocDecls является функцией,
возвращающей число локальных переменных в DoProc.
{--------------------------------------------------------------}
procedure DoProc;
(Я сделал пару изменений, которые не
были в действительности необходимы. Кроме небольшой реорганизации я переместил
вызов Fin в FormalList а также в LocDecls. Не забудьте поместить его в
конец FormalList.)
{--------------------------------------------------------------}
procedure ProcProlog(N: char; k: integer);
Сейчас должно работать. Добавьте эти изменения и посмотрите как они работают. ЗАКЛЮЧЕНИЕ К этому моменту вы знаете как компилировать
объявления и вызовы процедур с параметрами, передаваемыми по ссылке и по
значению. Вы можете также обрабатывать локальные переменные. Как вы можете
видеть, сложность состоит не в предоставлении механизма, а в определении
какой механизм использовать. Стоит нам принять эти решения и код для трансляции
в действительности не будет таким сложным.
|