5.5. Компиляция
547
меньше кода будет порождаться, если компилятор сможет вставлять примитивы в виде явного
кода (open coding) — то есть порождать код, который прямо использует эти машинные операции.
Выражение (+ a 1) можно было бы скомпилировать в простую последовательность вроде
43
(assign val (op lookup-variable-value) (const a) (reg env))
(assign val (op +) (reg val) (const 1))
В этом упражнении мы расширим компилятор так, чтобы он поддерживал явное кодирование
отдельных примитивов. При обращениях к этим примитивам будет порождаться специально напи-
санный код, а не о бщий код для вызова процедуры. Для того, чтобы поддержать такую работу,
мы дополним машину специальными регистрами для арг ументов arg1 и arg2. Элементарные
арифметические операции машины буду т принимать свои аргументы в arg1 и arg2. Они могут
помещать результаты в val, arg1 или arg2.
Компилятор должен уметь распознавать вызов явно кодируемого примитива в исходной про-
грамме. Мы дополним распознаватель в процедуре compile, так, чтобы он узнавал имена этих
примитивов в дополнение к зарезервированным словам (особым формам), которые он узна
¨
ет сей-
час
44
. Для каждой о собой формы в компиляторе ес ть свой генератор кода. В этом упражнении мы
построим семью генераторов кода для явно кодируемых примитивов.
а. В отличие от особых форм, явно кодируемые примитивы требуют, чтобы их аргументы вычис-
лялись. Напишите генератор кода spread-arguments, который будут использовать генераторы
явного кода. Spread-arguments должен принимать список операндов и компилировать данные
ему операнды, направляя их в последовательные аргументные регистры. Заметим, что операнд
может содержать вызов явно кодируемого примитива, так что во время вычисления операндов
придется сохранять аргументные регистры.
б. Для каждой из элементарных процедур =, *, - и + напишите по генератору кода, который
принимает комбинацию, содержащую этот оператор вместе с целевым регистром и описателем свя-
зи, и порождает код, который раскидывает аргументы по регистрам, а затем проводит операцию с
данным целевым регистром и указанным типом связи. Достаточно обрабатывать только выражения
с двумя операндами. Заставьте compile передавать управление этим генераторам кода.
в. Опробуйте обновленный компилятор на примере с процедурой factorial. Сравните полу-
ченный результат с результатом, который получается без открытого кодирования.
г. Расширьте свои генераторы кода для + и * так, чтобы они могли обрабатывать выражения с
произвольны м числом операндов. Выражение, в котором операндов больше двух, придется компи-
лировать в последовательность операций, каждая из которых работает с двумя входами.
5.5.6. Лексическая адресация
Одна из наиболе е часто встречающихся в компиляторах оптимизаций связана с по -
иском переменных. В нынешнем виде наш компилятор использует операцию lookup-
variable-value машины-вычислителя. Эта операция ищет переменную, сравнивая ее
со всеми переменными, связанными в данный момент, и проходя кадр за кадром по окру-
жению, имеющемуся во время выполнения. Если кадр глубоко вложен или если имеется
43
Здесь мы одним символом + обозначаем и процедуру исходного языка, и машинную операцию. В общем
случае может не быть однозначного соответствия примитивов исходного языка примитивам машины.
44
Вообще говоря, превращение примитивов в зарезервированные слова — плохая идея, потому что тогда
пользователь не может связать эти имена с другими процедурами. Более того, если мы добавим зарезер-
вированные слова в компилятор, который уже используется, перестанут работать существующие программы,
которые определяют процедуры с такими именами . Идеи, как можно избежать этой проблемы, можно найти в
упражнении 5.44.