Разные мысли о метавычислениях, суперкомпиляции, специализации программ. И об их применениях. Желающие приглашаются в соавторы блога.

Показаны сообщения с ярлыком прогонка. Показать все сообщения
Показаны сообщения с ярлыком прогонка. Показать все сообщения

среда, 16 марта 2011 г.

Крупношаговая суперкомпиляция (big-step supercompilation)

По поводу нынешнего послания вспомнился мне один эпизод из моего детства. Это было в те времена, когда по радио часто исполняли зажигательную песню “Русский с китайцем - братья навек!”.

Как-то, то ли в газете, то ли на каком-то плакате я увидел такую картину. Некий китаец, с просветлённым и одухотворённым лицом, совершает прыжок над пропастью. Одна нога китайца ещё опирается на одну сторону пропасти (на которой находится что-то плохое), а другая нога уже занесена над бездной. А на другой стороне пропасти находится что-то очень хорошее и завлекательное.

Меня эта картинка заинтересовала, и я спросил взрослых, что это значит? И мне объяснили, что строить светлое будущее - дело неспешное и утомительное. Поэтому, Председатель Мао, вождь китайского народа, решил ускорить процесс, и придумал для этого теорию “большого скачка”. Поэтому китаец и совершает прыжок над пропастью, на одной стороне которой находится капитализм, а на другой - коммунизм. Тем самым реализуя лозунг, выдвинутый Председателем Мао: “Три года упорного труда - десять тысяч лет счастья”.

Вскоре после этого разговора я вдруг заметил, что песню “Русский с китайцем - братья навек!” по радио передавать перестали. Про “большой скачок” тоже начали как-то скептически говорить. И вообще, постепенно как-то выяснилось, что китайцы - дураки, и над китайскими начальниками даже можно вслух издеваться (и за это никого не накажут).

Применив свою способность к построению логических умозаключений (которая как раз в это время начала у меня появляться), я догадался, что с тем китайцем, который совершал прыжок, приключилась какая-то беда. Но сам Китай и Председатель Мао от этого никуда не делись (раз уж их ругают по радио). И что китайцы не такие уж дураки, как их изображают. Во всяком случае, если они и хотели прыгнуть через пропасть, то не стали делать это все разом, а решили сначала подождать, чем кончиться прыжок у того энтузиаста, что был изображён на плакате (и, кстати, не имел никакого портретного сходства с Председателем Мао).

И вот, мы уже живём в другую эпоху. Смеяться над китайцами мы уже перестали... Не достигнув счастья за три года упорного труда, они почему-то после этого не перестали упорно трудиться. И теперь скорее уже у них есть основания над нами смеяться. “А хорошо смеётся тот, кто смеётся последним...”

Тут уместно спросить: “А какое отношение имеет Китай к суперкомпиляции?” И какое отношение имеет суперкомпиляция к теории “большого скачка” и к теории “малых дел”? По поводу первого могу сказать, что мне известен по крайней мере один аспирант, которого прислали из Китая для того, чтобы он позанимался в России суперкомпиляцией. Может быть, чтобы выяснить, стоит ли этим делом вообще заниматься. А аспирантура - это как раз три года. Так что, “три года упорного труда - …”.

С другой стороны, как выясняется, в области операционной семантики языков программирования как раз существует два подхода: “мелкошаговая семантика” (small-step semantics) и “крупношаговая семантика” (big-step semantics). В некоторых случаях они эквивалентны (в отличие от сферы политики и экономики). А в основе любого суперкомпилятора лежит та или иная версия операционной семантики.

Анатомия суперкомпиляции

Суперкомпиляция - это довольно абстрактная идея. Чтобы дойти от суперкомпиляции как идеи до конкретного суперкомпилятора требуется пройти через следующие этапы.

  • Выбираем язык программирования.
  • Выбираем операционную семантику этого языка.
  • Придумываем язык, на котором описываются конфигурации (множества состояний). Реализуем набор операций над конфигурациями: проверку вложения и (аппроксимации) для объединения.
  • Придумываем прогонку как обобщение операционной семантики.
  • Вводим правила обобщения, разрешающие заменять конфигурации на более общие и правила зацикливания.
  • Получается отношение суперкомпиляции, описывающее отношение между исходными и остаточными программами.
  • Добавляем эвристики, целями которых обычно является
    • Устранение недетерминизма (уменьшение числа порождаемых остаточных программ), вплоть до получения детерминированной суперкомпиляции.
    • Формализация целей суперкомпиляции (какие остаточные программы считаются “хорошими”, а какие - “плохими”).

Вывод:

Облик суперкомпилятора в значительной степени определяется не только входным языком, тем, на каком варианте операционной семантики этого языка основана прогонка.

Семантика: мелкошаговая vs. крупношаговая

Есть два подхода к определению операционной семантики языка: семантика малых шагов (small-step) и семантика большого шага (big-step). Или, по-русски, “мелкошаговая” и “крупношаговая”.

Определяется мелкошаговая семантика следующим образом. Вводится понятие абстрактной машины, которая в каждый момент находится в некотором состоянии. Определяется понятие перехода из одного состояния в другое. При этом процесс вычислений выглядит как последовательность переходов из одного состояния в другое.

Абстрактная машина может быть как детерминированной (и тогда каждое следующее состояние вычисляется из предыдущего с помощью функции), либо - недетерминированной (и тогда допустимые пары из текущего и следующего состояния описываются с помощью отношения).

В случае же крупношаговой семантики, смысл детерминированной программы изображается функцией, “за один шаг” отображающей начальное состояние в конечное. А “операционость” семантики заключается в том, что эта функция описывается в терминах разбиения исходной задачи на конечное число подзадач, с последующим получением решения исходной задачи путём “конструктивной” композиции решений этих подзадач.

Если же требуется изобразить смысл недетерминированной программы, то этот смысл изображается в виде функции, отображающей исходное состояние в множество допустимых конечных состояний.

Пример: CEK-машина

Для сравнения особенностей мелкошаговой и крупношаговой семантики, рассмотрим следующий пример. В статье

Olivier Danvy and Kevin Millikin. 2008. On the equivalence between small-step and big-step abstract machines: a simple application of lightweight fusion. Inf. Process. Lett. 106, 3 (April 2008), 100-109. PDF DOI=10.1016/j.ipl.2007.10.010 http://dx.doi.org/10.1016/j.ipl.2007.10.010

описаны две подхода к определению операционной семантики CEK машины. CEK-машина реализует приведение λ-термов к слабой головной нормальной форме с использованием редукции слева направо в аппликативном порядке. Переменные изображаются индексами де Брюйна (de Bruijn indices). (Кто не верит, что de Bruijn - это “де Брюйн”, может проверить, пошарив по именам авторов книг. Например: http://www.ozon.ru/context/detail/id/2367530/ .)

В статье семантика виртуальных машин запрограммирована на языке Standard ML, мы же перепишем её на языке HLL, являющемся входным языком суперкомпилятора HOSC (и представляющем собой подмножество языка Haskell).

Итак, сначала определяем понятие λ-терма:

data Nat = Z | S Nat;
data Term = Var Nat | Lam Term | App Term Term;

Понадобилось определить понятие натурального числа, поскольку натуральные числа используются в качестве индексов де Брюйна.

Будем считать, что результатом вычисления терма (если это вычисление завершается) является представление его слабой головной нормальной формы в виде замыкания, имеющего вид Clo t e, где t - λ-терм, а e - среда, приписывающая значения свободным переменным терма t. Среда при этом представляет собой список из значений переменных. Заметим, что имена переменных хранить в среде не требуется, поскольку индексы де Брюйна прямо задают позиции для значений переменных в среде.

На языке HLL это описывается так:

data List a = Nil | Cons a (List a);
data Val = Clo Term (List Val);

Извлечение значения переменной с индексом i из среды e может быть выполнено с помощью функции lookup:

lookup = \i env ->
 case env of {
   Cons n env1 ->
     case i of {
       Z -> n;
       S i1 -> lookup i1 env1;
     };
 };

Мелкошаговая семантика CEK-машины

Теперь нужно определить понятие состояния CEK-машины. В принципе, это состояние является λ-термом, но этот терм представлен таким образом, чтобы не требовалось каждый раз искать редекс (подтерм, подлежащий преобразованию) начиная с самого верха. Для этого вводится понятие редукционного контекста (смысл которого будет объяснён ниже):

data RC = RC0
       | RC1 RC Term (List Val)
       | RC2 Val RC;

Мелкошаговая семантика CEK-машины определяется через понятие текущего состояния:

data Conf = Eval Term (List Val) RC
         | Apply RC Val;

data State = Final Val | Inter Conf;

Работа CEK-машины распадается на шаги. Действия, предпринимаемые на каждом шаге, зависят от того, какой вид имеет текущее состояние.

  • Final v . В этом случае состояние считается заключительным, а v - окончательным результатом вычисления.
  • Inter conf . В этом случае состояние считается промежуточным, а confизображает текущую конфигурацию виртуальной машины.

Если состояние является промежуточным, оно содержит в себе конфигурацию машины (не путать с “конфигурациями” в смысле суперкомпиляции), и дальнейшие действия зависят от вида этой конфигурации.

  • Apply c v . Такая конфигурация означает, что значение v нужно вставить внутрь объемлющего контекста c и продолжить вычисление.
  • Eval t e c . Такая конфигурация означает, что терм t следует вычислить в среде e, а потом вставить результат в контекст c.

Как же вычисляется терм t, входящий в конфигурацию Eval t e c ? Способ вычисления t зависит от того, какой вид он имеет.

  • Var i. Нужно достать из окружения e значение переменной, находящееся в e в i-й позиции. И вставить это значение в контекст c. Значение переменной извлекается из контекста с помощью функции lookup (описанной выше).
  • Lam t0 . Нужно сформировать замыкание Clo t0 e и вставить его в контекст c. Помимо терма t0 замыкание содержит и среду, в которой этот терм должен вычисляться.
  • App t0 t1 . Этот случай - самый интересный. Нужно заняться вычислением терма t0, отложив вычисление терма t1 на будущее. Для этого контекст c заменяется на новый контекст RC1 c t1 e, после чего вычисляется t0, и получается результат v, который вставляется в контекст RC1 c t1 e. А операция вставления v в контекст вида RC1 c t1 e реализована так, что начинается вычисление терма t1 в окружении e, а v запоминается в контексте вида RC2 v c1.

Собрав всё воедино, получаем функцию move, реализующую преобразование текущей конфигурации (незаключительного состояния) в следующее состояние:

move = \conf ->
 case conf of {
   Eval t e c ->
     case t of {
       Var i -> Inter (Apply c (lookup i e));
       Lam t0 -> Inter (Apply c (Clo t0 e));
       App t0 t1 -> Inter (Eval t0 e (RC1 c t1 e));
     };
   Apply c v ->
     case c of {
       RC0 -> Final v;
       RC1 c1 t1 e -> Inter (Eval t1 e (RC2 v c1));
       RC2 v1 c1 ->

         case v1 of {

           Clo t2 e2 -> Inter (Eval t2 (Cons v e2) c1); };
     };
 };

Самое интересное место в этом определении - вставление значения v в контекст вида RC2 v1 c1. Этот контекст означает, что наступил момент применить замыкание v1 к аргументу v. Для этого из замыкания Clo t2 e2 извлекается тело функции t2 и среда e2, к среде e2 добавляется v (аргумент функции), после чего t2 вычисляется в этой среде.

Теперь осталось определить функцию-”погонялу” drive, приводящую виртуальную машину в движение. Эта функция смотрит на текущее состояние. Если это состояние - заключительное, то выдаётся окончательный результат работы. Если состояние - незаключительное, выполняется переход в следующее состояние (с помощью функции move) и процесс продолжается:

drive = \state -> case state of {
  Final v -> v;
  Inter conf -> drive (move conf);
};

И вот, наконец, функция smallStepEval, которая инициализирует и запускает мелкошаговую виртуальную машину:

smallStepEval = \t -> drive (Inter (Eval t Nil RC0));

Крупношаговая семантика CEK-машины

Теперь рассмотрим другой вариант семантики CEK-машины: крупношаговую семантику, реализованную в виде функции bigStepEvaluate:

eval = \t e c ->
 case t of {
   Var i -> apply c (lookup i e);
   Lam t1 -> apply c (Clo t1 e);
   App t0 t1 -> eval t0 e (RC1 c t1 e);
 };

apply = \t v ->
 case t of {
   RC0 -> v;
   RC1 c1 t1 e1 -> eval t1 e1 (RC2 v c1);
   RC2 v1 c1 ->

     case v1 of {

       Clo t2 e2 -> eval t2 (Cons v e2) c1; };
 };

bigStepEvaluate = \t -> eval t Nil RC0;

Мы не будет подробно разбирать, как устроено это определение, поскольку самое сложное в нём - смысл контекстов. А он - тот же самый, что и в случае функции smallStepEval. Самое интересное в том, что теперь процесс вычислений не распадается на отдельные “шаги”. В отличие от smallStepEval, отсутствует и понятие “состояния”, как некоего единого и неделимого значения.

Эквивалентность мелкошаговой и крупношаговой семантики CEK-машины

Возникает естественный вопрос: верно ли, что мелкошаговая и крупношаговая семантики CEK-машины эквивалентны? Технически этот вопрос сводится к тому, эквивалентны ли функции smallStepEval и bigStepEval.

В вышеупомянутой работе [Olivier Danvy and Kevin Millikin, 2008] на этот вопрос даётся положительный ответ. Для этого авторам пришлось проявить остроумие и изобретательность. А именно, в статье эквивалентность smallStepEval и bigStepEval доказывается с помощью трансформационного подхода. А именно, авторами была найдена цепочка из нескольких преобразований, последовательное применение которых постепенно превращает smallStepEval в bigStepEval.

Возникает интересный вопрос: может ли доказательство такого рода быть автоматизировано?

Доказательство эквивалентности smallStepEval и bigStepEval с помощью суперкомпиляции

Оказывается, что эквивалентность smallStepEval и bigStepEval может быть доказана автоматически с помощью следующего метода.

Пусть имееется суперкомпилятор SC. Обозначим через SC[e] остаточную программу, выдаваемую SC для входной программы e. Пусть ≅ обозначает отношение операционной эквивалентности программ, а ≡ - отношение синтаксической эквивалентности программ (т.е., что программы либо текстуально совпадают, либо различаются “несущественно”, например, совпадают с точностью до имён связанных переменных).

Пусть ∀e, eSC[e], т.е. SC строго сохраняет эквивалентность программ.

Метод доказательства. Просуперкомпилируем программы/выражения e1 и e2. Пусть получились программы/выражения SC[e1] и SC[e2], которые синтаксически эквивалентны. Тогда можно утверждать, что e1 и e2 эквивалентны. В символическом виде:

SC[e1] ≡ SC[e2] ⇨ e1e2

Обоснование (почти тавтология). Смысл SC[e1] и SC[e2] одинаков, поскольку это - фактически одна и та же программа. А SC сохраняет семантику программ, т.е. смысл e1 и e2, тот же, что и смысл SC[e1] и SC[e2] соответственно. Значит, смысл e1 совпадает со смыслом e2. В символическом виде:

e1SC[e1] ≡ SC[e2] ≅ e2

У читателя уже, наверное, сводит скулы от скуки, при виде тривиальности и очевидности только что описанного метода доказательства. Ну да, если мы, просуперкомпилировав две разные программы, получаем одно и то же, значит - и исходные программы эквивалентны. Однако, до недавнего времени, этот метод почему-то никому не приходил в голову. Первое применение этого метода описано в статье

Alexei Lisitsa and Matt Webster. Supercompilation for equivalence testing in metamorphic computer viruses detection. First International Workshop on Metacomputation in Russia (META 2008). PDF

При этом, правда, использовался суперкомпилятор SCP4, который, вообще говоря, строго сохраняет семантику программ только для тотальных программ (которые никогда не зацикливаются и никогда не попадают в ситуацию аварийного останова). Входным языком SCP4 является Рефал: язык первого порядка работающий с конечными структурами данных.

Позднее, в статье

Ilya Klyuchnikov and Sergei Romanenko. Proving the Equivalence of Higher-Order Terms by Means of Supercompilation. In: Perspectives of Systems Informatics (Proceedings of Seventh International Andrei Ershov Memorial Conference, PSI 2009, Novosibirsk, Russia, June 15-19, 2009). Novosibirsk: A.P. Ershov Institute of Informatics Systems, 2009, pages 150-158. PDF slides PDF DOI

было показано, что этот метод работает и для программ, работающих с бесконечными структурами даных, даже если программы не тотальны (могут зацикливаться и/или аварийно завершаться).

Теперь попробуем применить этот метод к функциям smallStepEval и bigStepEval. Суперкомпилируем определения этих функций с помощью суперкомпилятора HOSC (http://code.google.com/p/hosc/). Оказывается, что и в том, и в другом случае получается одна и та же (с точностью до имён связанных переменных) остаточная программа:

data Nat  = Z  | S Nat;
data List a = Nil  | Cons a (List a);
data Term  = Var Nat | Lam Term | App Term Term;
data Val  = Clo Term (List Val);
data RC  = RC0 | RC1 RC Term (List Val) | RC2 Val RC;
data Conf  = Eval Term (List Val) RC | Apply RC Val;
data State  = Final Val | Inter Conf;

(letrec
 f=(\r47->
   (\s47->
     (\t47->
       case  r47  of {
         App w5 x19 -> (((f w5) s47) (RC1 t47 x19 s47));
         Lam p30 ->
           case  t47  of {
             RC0  -> (Clo p30 s47);
             RC1 y21 w12 x21 ->

               (((f w12) x21) (RC2 (Clo p30 s47) y21));
             RC2 t33 r46 -> case  t33  of {

               Clo y4 w17 ->

                 (((f y4) (Cons (Clo p30 s47) w17)) r46); };
           };
         Var s41 ->
           case  t47  of {
             RC0  ->
               (letrec
                 g=(\x48->
                   (\y48->
                     case  x48  of {

                       Cons w39 z27 ->

                         case  y48  of {

                           Z  -> w39;

                           S p10 -> ((g z27) p10); }; }))
               in
                 ((g s47) s41));
             RC1 w45 z10 p22 ->
               (((f z10) p22)
                 (RC2
                   (letrec
                     h=(\z48->
                       (\u48->
                         case  z48  of {

                           Cons t11 v27 ->

                             case  u48  of {

                               Z  -> t11;

                               S t5 -> ((h v27) t5); }; }))
                   in
                     ((h s47) s41))
                   w45));
             RC2 z12 v1 ->
               case  z12  of {
                 Clo v5 w37 ->
                   (((f v5)
                       (Cons
                         (letrec
                           f1=(\v48->
                             (\w48->
                               case  v48  of {
                                 Cons v26 s14 ->

                                   case  w48  of {

                                     Z  -> v26;

                                     S x16 -> ((f1 s14) x16); };
                               }))
                         in
                           ((f1 s47) s41))
                         w37))
                     v1);
               };
           };
       })))
in
 (((f t) Nil) RC0))

Из этого следует, что и исходные определения функций - эквивалентны. “Вживую” этот пример можно посмотреть здесь: CEK machine.

Интересно, что в этом случае доказательство строится полностью автоматически.

Крупношаговая суперкомпиляция

Обязательно ли суперкомпиляция должна основываться на мелкошаговой семантике?

Как упоминалось выше, при построении конкретного алгоритма суперкомпиляции для некоторого языка, следует начинать с выбора некоторого варианта операционной семантики этого языка. Только после этого может быть построен алгоритм прогонки, поскольку прогонка представляет собой некоторое обобщение обычного процесса исполнения программы. На основе прогонки затем строятся остальные части алгоритма суперкомпиляции.

По историческим причинам, при разработке алгоритмов суперкомпиляции практически всегда в качестве основы выбиралась мелкошаговая операционная семантика. Вероятно, по следующим причинам.

  • Первоначально суперкомпиляция была разработана для языка Рефал, а язык Рефал представляет собой некоторое развитие идеи, алгорифмов Маркова. (Фактически - содержит язык алгорифмов Маркова в качестве подмножества.) А общепринятая семантика алгорифмов Маркова - классический пример мелкошаговой операционной семантики.
  • Первоначально суперкомпиляция разрабатывалась для языков первого порядка с передачей параметров по значению (каковым языком и является язык Рефал). В таких языках, как правило, работают с конечными структурами данных. А для выражения крупношаговой семантики больше подходят языки с передачей параметров по имени (допускающие при этом бесконечные структуры данных).

Однако, как мы видели выше, в качестве операционной семантики языка может быть выбрана крупношаговая семантика! Каковы достоинства такого подхода?

Достоинства крупношаговой суперкомпиляции

В статье

И.Г.Ключников. Суперкомпиляция: идеи и методы // Практика функционального программирования. (http://fprog.ru/)

описан алгоритм суперкомпиляции для простого функционального языка первого порядка с передачей параметров по имени, который основан на крупношаговой операционной семантике. (В данный момент статья ещё не опубликована. Но скоро должна появиться, и тогда я в этом месте вставлю ссылку на её электронную версию.)

2011-04-13. А вот и обещанная ссылка: http://fprog.ru/2011/issue7/.

Достоинства получившегося суперкомпилятора.

  • Необычайно высокая модульность. Суперкомпилятор представляется в виде композиции из нескольких функций.
  • Широко используются бесконечные структуры данных. Например, концептуально, алгоритм прогонки генерирует бесконечное дерево, которое потом “подрезается” и превращается в конечный граф.
  • Есть сильное подозрение, что крупношаговый суперкомпилятор должен легче поддаваться суперкомпиляции (по сравнению с мелкошаговыми) в силу ясности, модульности и функциональности его структуры.
  • В силу модульности и функциональности структуры, крупношаговая суперкомиляция потенциально более удобна для её использования в рамках суперкомпиляции высшего уровня, т.е. для построения систем, составленных из нескольких суперкомпиляторов.

Что интересно попробовать сделать

Если внимательно рассмотреть остаточную программу, которая получилась при доказательстве эквивалентности функций smallStepEval и bigStepEval, можно заметить, что остаточная программа по своей структуре ближе к bigStepEval, чем к smallStepEval. Этот показывает, что суперкомпилятор HOSC имеет тенденцию преобразовывать “мелкошаговые программы” в “крупношаговые программы”. В связи с этим имеет смысл попробовать произвести следующую операцию:

  • Реализуем некоторый суперкомпилятор на основе мелкошаговой семантике.
  • Суперкомпилируем суперкомпилятор с помощью суперкомпилятора HOSC.
  • Изучаем, что получится. Есть основания ожидать, что получится суперкомпилятор, основанный на крупношаговой операционной семантике.

Заключение

Важно назвать вещи своими именами. После того, как осознана возможность двух подходов (small-step supercompilation и big-step supercompilation), становятся понятно, что различия между ними нужно исследовать. У big-step supercompilation просматривается определённый потенциал, который нужно изучить и извлечь из него пользу.

Послесловие к заключению

Илья Ключников, ознакомившись с этим текстом, взял, да и вылил мне на голову ведро холодной воды (к счастью - в фигуральном, а не буквальном смысле). Объяснил мне, что мой пафос - смешон и неуместен.

Если хорошенько подумать, что в природе пока ещё не было ни одного суперкомпилятора, который был бы на 100% основан на мелкошаговой семантике. Все суперкомпиляторы, что называется “сидят на двух стульях” и основывают прогонку на какой-то “адской смеси” мелкошаговой и крупношаговой операционной семантики.

Например, в варианте Сёренсена, если в узле графа находится конфигурация вида C(e1, ..., eN), где C - конструктор, сразу же делается декомпозиция конфигурации, и задача построения дерева сводится к N подзадачам (построению деревьев для e1, ..., eN). А с точки зрения мелкошаговой семантики, в этом месте нужно было бы выполнить шаг редукции внутри одного из аргументов конструктора.

С другой стороны, имеются такие экстремистские варианты суперкомпиляции, при которых граф конфигураций в явном виде вообще не строится, а всё выражается через некоторую суперпозицию монад.

Что же касается статьи Ильи, на которую я указал пальцем как на новое слово в суперкомпиляторостроении, то, действительно, Илья постарался предложить такой вариант суперкомпиляции, который максимально приближается к крупношаговой семантике, но при этом граф конфигураций не исчезает. Какая от этого польза - отдельный (и интересный) вопрос.

Посему, наступая на горло собственном авторскому тщеславию, предлагаю читателям забыть всё, что я тут понаписал! :-)

четверг, 18 июня 2009 г.

Прогонка для функций высших порядков

В заметках

речь шла о том, что метавычисления являются "пародией" на обычные вычисления. А "прогонка" - простейший вариант метавычислений (без обобщений и зацикливаний). В общем случае прогонка порождает бесконечное дерево, которое можно сделать конечным, применяя "обобщение" конфигураций и "зацикливание".

Сейчас мы постараемся подробнее разобраться, как выглядит прогонка в случае "ленивого" языка с функциями высших порядков. Суперкомпилятор HOSC обрабатывает программы именно на языке такого рода: Higher Order Lazy Language (HLL).

В случае языка HLL, программа имеет следующий вид:

d1 ... dN e0 where f1 = e1; ... fN = eN;

и содержит следующие части:

  • d1 ... dN - объявления типов для данных, обрабатываемых программой. (Сейчас эта часть программы нас не очень интересует.)

  • e0 - "целевое" выражение. Считается, что исполнение программы сводится к вычислению e0. Если e0 содержит свободные переменные, то через них в программу передаются исходные данные.

  • f1 = e1; ... fN = eN; - определения глобальных функций. Эти функции могут использоваться внутри целевого выражения e0 .

Вот пример программы:

data Nat = Z | S Nat;

add a b

where

add = \x y -> case x of { Z -> y; S x1 -> S (add x1 y); };

Целевое выражение содержит три свободные переменные a и b, через которые в программу и передаются исходные данные, а исполнение программы сводится к тому, что в целевое выражение add a b подставляются значения переменных a и b, после чего получившееся выражение и вычисляется.

Вот в этом и состоит различие между "обычным" вычислением выражения и "прогонкой". При обычном вычислении обрабатываемое выражение не может содержать свободные переменные, а при прогонке - может.

Заметим, что в случае вычислений первого порядка (как в суперкомпиляторе SPSC) было достаточно сказать, что выражение вообще не содержит переменные (или содержит), а в случае высшего порядка приходится ещё различать свободные и связанные переменные.

Как выполняется такого рода программа, сначала рассмотрим на примере. (И, с точки зрения людей, пишущих на Фортране, такой способ исполнения программы выглядит как чистое извращение. :-) )

Допустим, на вход поступили такие данные:

a = S Z;

b = Z;

Подставляем значения a и b в add a b. Получается

add (S Z) Z

И вот тут-то и начинается самое страшное. В случае языка первого порядка (как в SPSC) мы сразу же, за один шаг, преобразовали бы это выражение в выражение

S (add Z Z)

сделав много разных дел:

  1. Нашли бы определение функции add.
  2. Распознали бы, какое правило из определения add применить. А для этого нужно было бы выполнить сопоставление в образцом.
  3. Применили бы правило.

Но то, что было в случае "первого порядка" одним большим действием, в случае HLL разбивается на несколько более элементарных действий. Разбиение на мелкие действия хорошо с точки зрения компьютера, с точки зрения тех, кто пишет статьи о суперкомпиляторе, и даёт некоторые дополнительные возможности в процессе преобразований. Но есть и "оборотная сторона медали"! При попытке "прокрутить" какие-то примеры вручную, на бумаге, сразу же оказывается, что граф конфигураций жутко раздувается. Точнее, раздувается не сам граф, как таковой, а конфигурации, находящиеся внутри узлов графа. Что мы сейчас и увидим.

Итак, смотрим на выражение

(add (S Z) Z)

На верхнем уровне - вызов функции add. Хорошо бы его применить к аргументам. Чтобы сделать это, ищем определение функции add, и подставляем его вместо имени функции! Таков уж язык высшего порядка. В случае первого порядка, программа и данные строго разделены, но если мы объявили, что функции являются "равноправными значениями" (first-class values), различие между "программой" и "данными" рассеивается как утренний туман (или, говоря ещё более поэтично, как нежные лепестки цветка сакуры под весенним ветром). Если число можно подставить, можно и функцию подставить. Что и делаем. Получается вот такая гадость:

(\x -> (\y -> case x of { Z -> y; S x1 -> S (add x1 y); })) (S Z) Z

Концептуально - ясно и понятно. Но выписывать руками - сущее мучение.

Теперь выражение имеет вид (\v -> e0) e1, и понятно, что нужно сделать дальше: применить функцию к аргументу. Т.е. в e0 заменить все вхождения v на e1. Конкретно, (\x -> ...) применяем к S Z и получаем

(\y -> case S Z of { Z -> y; S x1 -> S (add x1 y); }) Z

Теперь (\y -> ...) применяем к Z. Получается

case S Z of { Z -> Z; S x1 -> S (add x1 Z); }

И вот теперь наступает момент, когда нужно выполнить "сопоставление с образцом". Аргумент case-выражения содержит конструктор S на верхнем уровне. Поэтому, S Z сопоставляется с образцом S x1. При этом, переменная x1 принимает значение Z. После чего, мы извлекаем из case-выражения ветвь

S x1 -> S (add x1 Z);

и подставляем в S (add x1 Z) значение переменной x1, т.е. Z. После чего обрабатываемое выражение принимает вид:

S (add Z Z)

На верхний уровень выражения (из его глубин) выполз конструктор S. Что делать дальше? В случае "строгого" языка нужно было бы продолжать вычисления, поскольку в аргументе конструктора остался вызов функции add. Нужно его вычислять, даже если результат этого вычисления не будет использован.

Но в случае "ленивого" языка, результат вычисления может выдаваться потребителю не весь сразу - а частями. В данном случае, "частичный результат" (в виде конструктора S на верхнем уровне) - налицо. Поэтому, можно считать, что вычисление "частично закончено". Но, предположим, что некий потребитель "съел" верхний конструктор, и захотел использовать значение его аргумента add Z Z. Ну что же, тогда вычисление возобновляется, и получается такая последовательность преобразований:

add Z Z

(\x -> (\y -> case x of { Z -> y; S x1 -> S (add x1 y); })) Z Z

(\y -> case Z of { Z -> y; S x1 -> S (add x1 y); }) Z

case Z of { Z -> Z; S x1 -> S (add x1 Z); }

Z

Т.е., "совсем" окончательный результат - S Z.

А теперь наступил момент, когда нужно перейти от "обычных" вычислений к "метавычислениям". Что будет, если не подавать в программу исходные данные, а оставить в целевом выражении переменные, и попытаться вычислять "прямо так"? Оказывается, что это - не так уж трудно. Начинаем с выражения add a b. И видим, что несколько первых шагов делаются точно так же, как и в случае известных исходных данных:

add a b

(\x -> (\y -> case x of { Z -> y; S x1 -> S (add x1 y); })) a b

(\y -> case a of { Z -> y; S x1 -> S (add x1 y); }) b

case a of { Z -> b; S x1 -> S (add x1 b); }

А вот теперь появляется интересная ситуация: в аргументе case-выражения - переменная a, значение которой неизвестно. Как выбрать нужную ветвь в case-выражении?

Единственный выход - действовать "разбором случаев": рассмотреть отдельно два случая: a = Z и a = S a1, где a1 - новая переменная, которая не встречалась в программе. В этот момент линейная последовательность вычислений превращается в дерево!

Итак, для случая a = Z получается

case Z of { Z -> b; S x1 -> S (add x1 b); }

b

А для случая a = S a1 получается

case S a1 of { Z -> b; S x1 -> S (add x1 b); }

S (add a1 b)

Дальше мы извлекаем из конструктора его аргумент, и начинаем изучать процесс его вычисления отдельно (как и в случае языка первого порядка). И видим, что нам повезло: изучать-то нечего, поскольку add a1 b совпадает (с точностью до имён переменных) с начальной конфигурацией add a b. Значит, можно сделать зацикливание и получить конечный граф конфигураций:

.

Суперкомплятор HOSC именно такой граф и делает: add a b.

Впрочем, этот граф виден, если рассматривать пример через FireFox, ибо для рисования графа используется svg-графика. FireFox рисует эти графы хорошо, а видно ли в других браузерах - не уверен. Кажется, нужно какие-то плагины устанавливать для отрисовки svg-графики...

Итак, пока мы занимались прогонкой, вроде бы никаких особых проблем (по сравнению с "первым порядком") не возникло. Хотя, если описывать прогонку для HLL аккуратно и со всеми подробностями, то это описание получается довольно занудным. Приходится возиться со всякими понятиями, вроде "слабая головная нормальная форма", "наблюдаемое", "редекс", "контекст", и т.п. К счастью, при изучении конкретных примеров суперкомпиляции, всего этого можно и не знать, а руководствоваться здравым смыслом и общими принципами, на которых построены ленивые вычисления.

(Но тех, кому интересно, могу порадовать тем, что в работе сейчас находится документ, в котором будут подробно описаны внутренности суперкомпилятора HOSC, в виде формул и всего прочего, что должно быть в "научных" сочинениях.)

Более интересен другой вопрос! Ну хорошо, с прогонкой "высшего порядка" всё, вроде, получается. Но ведь в суперкомпиляторе, кроме прогонки, нужно делать и кое-что ещё: сравнивать конфигурации, строить обобщения конфигураций, проверять, вкладывается ли одна конфигурация в другую или нет?

И здесь возникают кое-какие тонкости, связанные с тем, что в случае "высшего порядка", переменные в конфигурациях могут быть как свободными, так и связанными (в то время, как в случае "первого порядка", связанные переменные в конфигурациях не появлялись).

Но это - тема для отдельного разговора...

вторник, 12 мая 2009 г.

Ранняя история суперкомпиляции

"Откуда есть пошла" суперкомпиляция? Сейчас мы уже как-то привыкли к тому, что всё новое придумывают иностранцы, что они же это новое изготавливают и продают, а мы потом всё это покупаем за нефть. Однако, в случае с суперкомпиляцией дело обстоит на так: она была изобретена в России. Ну, точнее, в СССР. Причём придумал её не программист, а физик: Валентин Фёдорович Турчин:

Точнее, сначала он в 1966 году придумал "метаалгоритмический язык" Рефал, предназначенный для обработки алгоритмов, записанных на каких-то языках программирования.

  • Турчин В.Ф.. Метаязык для формального описания алгоритмических языков, сб. "Цифровая вычислитель­ная техника и программирование", изд-во "Советское радио", М.-Л., 1966.

  • Турчин В.Ф. Метаалгоритмический язык. — Кибернетика № 4, 1968. С. 116−124. DJVU , PDF

Со второй из статей можно ознакомиться (на выбор - в формате djvu или pdf), а первую из них, я когда-то держал в руках, а где добыть сейчас - не знаю...

Через некоторое время Турчин задумался над такой мыслью: если Рефал, в принципе, годится для обработки программ на любых алгоритмических языках, то, по определению, должен быть пригоден и для обработки программ на нём же самом. (Задним числом эта мысль кажется очевидной.) :-)

Правда, "обработка" программ тоже бывает разная... Например, раскраска ключевых слов в программа - это, можно считать, тоже "обработка" программы. Турчина же заинтересовал особый вид "обработки", вытекающий из идеи "метасистемного перехода". Что понимает Турчин под "метасистемным переходом", можно прочитать в его книге:

Тема эта - интересная, но очень обширная, поэтому мы сейчас в неё не будем углубляться, а рассмотрим только один частный случай, относящийся к исполнению программ.

Рассмотрим некоторый алгоритмический язык.  (Заметим в скобках, что понятие "алгоритмический язык" не совсем совпадает с понятием "язык программирования". Например, язык машин Тьюринга очевидным образом является "алгоритмическим языком", но "языком программирования" его обычно не называют. :-) ) Допустим, у нас есть программа на этом языке, которую нам хочется "выполнить", применив к некоторым "исходным данным". (Впрочем, в некоторых языках, например, в лямбда-исчислении, разницы между "данными" и "программой" не существует.)

Понятно, что "выполнить" программу можно только в том случае, если есть некий субъект, который её "понимает" и умеет выполнять. Этим субъектом может быть "железным" устройством (вроде процессора компьютера), программой или человеком. Для краткости, будем называть этого субъекта "интерпретатором".

Итак, берём интерпретатор, программу и исходные данные и всё это запускаем. Интерпретатор начинает задумчиво "жевать" программу и данные. Всю эту (до боли знакомую) ситуацию назовём "основной системой", находящейся на "основном уровне".

А теперь представим, что над основным уровнем есть ещё "метауровень", на котором находится другой субъект, который с большим интересом наблюдает за тем, что происходит в основной системе. Вот это всё и называется "метасистемой". (Правда, затрудняюсь сейчас сказать, нужно ли считать, что основная система находится внутри метасистемы, являясь её частью, или же метасистема и система разделены? Это, видимо, вопрос определения...)

Ну, а переход от ситуации, когда есть только основная система, к ситуации, когда возникает метасистема, как и следует ожидать, называется "метасистемным переходом".

Имеем ли мы дело с метасистемными переходами в программировании? Не знаю, как там насчёт теоретиков, но программисты-практики сталкиваются с ними каждый день. Ну, например, что представляет из себя процесс отладки? Отладчик исполняет программу + данные по-шагам, а над ними тяжело дышит программист, который тщетно пытается понять, что происходит. Этот программист и находится на "метауровне" и, стало быть, является "метасистемой" (или её частью). Однако же, идея "автоматизации программирования" (жалко, что про этот изящный термин ныне почти забыли), подразумевает, что было бы хорошо, если бы всех программистов можно было повыгонять и заменить из на бездушные машины и/или программы.

Т.е., в идеале, поведение одной программы должна была бы изучать другая программа, а не человек. А над этой программой, естественно, можно было бы поставить другую программу (сидящую на мета-мета-уровне), и т.д. Но, как говорится, "гладко было на бумаге, да забыли про овраге". Светлая и завлекательная мечта об "автоматизации программирования" так мечтой и осталась. Хотя, кое-что сделать всё же удалось...

Итак, Турчин задумался о том, как можно было бы построить метасистему (в виде программы), наблюдающую за Рефал-программой, обрабатывающей какие-то данные. И вскорости он обнаружил, что на метауровне имеются кое-какие возможности изучать поведение программы "в общем виде", т.е. для целых классов входных данных, а не только для какого-то конкретного набора исходных данных (как в случае отладки). Говоря по-простому, обнаружилась возможность перейти от "арифметики" а "алгебре" (в школьном понимании этого слова). Например, то, что 3*(2+1) = 3*2+3*1 - это факт из арифметики. А то, что для любых a, b, и c верно a*(b+c) = a*b+a*c - уже факт из алгебры.

Так появилась система преобразований Рефал-программ, получившая название "прогонка" (driving). Слово "прогонка" появилось из-за того, что "конкретные" данные проходят через процесс вычислений "естественным" путём, а вот в случае "обобщённых" данных, изображающих целые классы "конкретных" данных, дело обстоит хуже: их приходится пропихивать через процесс вычислений буквально пинками, для которых, кстати, в прогонке используются прямо-таки садистские названия: "сужение" и "расщепление". Сразу на ум приходит сцена в духе "Преступления и наказания": стоит на метауровне субъект с топором и то обтёсывает "обобщенные данные", то расщепляет...

Прогонка была описана Турчиным в статьях

  • В.Ф.Турчин, Эквивалентные преобразования рекурсивных функций, описанных на языке РЕФАЛ. В сб.: Труды симпозиума "Теория языков и методы построения систем программирования'', Киев-Алушта: 1972. Стр. 31-42. DJVU scan , PDF scan , DJVU LaTeX , PDF LaTeX

  • Турчин В.Ф. Эквивалентные преобразования программ на РЕФАЛе. Автоматизированная система управления строительством. Труды ЦНИПИАСС, N 6. ЦНИПИАСС. Москва, 1974. Стр. 36-68. DJVU , PDF

Главный недостаток статьи 1972 года в том, что сборник трудов симпозиума был напечатан на серой бумаге бедными серыми буквами. Поэтому, в некоторых местах текст можно прочитать только с помощью сильной лупы. Понятно, что при попытке оцифровать эту статью возникли ну очень большие трудности. Пришлось проявить смекалку и изобретательность. Результат можете оценить сами. Выглядит довольно мерзко (хотя читать уже можно без лупы). Поэтому, пришлось перенабрать статью через LaTeX. Этот вариант выглядит чистенько, но зато в нём не ощущается "аромат эпохи" и возраст текста...

В статьях 1972 и 1974 года описана система преобразований Рефал-программ (прогонка), а также её применение для решения "обратных" задач. А именно, допустим, что на Рефале описана некая функция f, которая для любого входного x выдаёт True или False (если завершается). И вот, Турчин показывает, что с помощью прогонки можно решать "обратную задачу": подбирать для f такие значения x, что f(x) = True.

С точки зрения чистой теории в этом нет ничего удивительного, поскольку известно, что область определения любой рекурсивной функции является рекурсивно-перечислимой. Интерес был в том, что прогонка позволяла искать решения не тупым полным перебором, а вполне разумным способом. Например, в статьях рассматривается вопрос о решении уравнений вида a+x=b, где a и b - натуральные числа, представленные в двоичной системе счисления, а сложение определено как сложение "в столбик" в виде функции на Рефале. И получилось, что поиск x с помощью прогонки делается не перебором, а вычитанием "в столбик". Получалось, что компьютер, получив алгоритм сложения в виде программы автоматически находил разумный алгоритм вычитания.

Должен сказать, что описанное в статьях было тогда же реализовано в виде программ на Рефале (хотя в самих статьях это и не отражено). Турчин написал реализацию прогонки на Рефале, а "хождение на машину" (как это тогда называлось) и отладку поручил (или, скажем более точно, доверил :-) ) мне. К слову, программы тогда набивались на перфокартах , а "машиной" была могучая БЭСМ-6 (которая, правда, несколько уступала американской CDC-6600 ).

И, действительно, всё работало по теории: обратные задачи решались... А решения печатались на широченных лентах бумаги.

Однако же, если внимательно вчитаться в статью 1972 года, то у тех, кто знаком с Прологом, наверняка возникнет ощущение чего-то знакомого... Ну да, "обобщённое отождествление" - это "унификация". Стало быть, Турчин ничего нового не придумал: просто взял Пролог, перекрасил и выдал за своё. :-) Но это не совсем так. Или, точнее, совсем не так. Ведь, как утверждают сведущие люди:

Язык Пролог был изобретен французским математиком-программистом Колмэрауе в 1972 году как язык логического программирования для проведения экспериментов в области “искусственного интеллекта” на ЭВМ.

Т.е. в 1972 году Колмэрауе сидел и придумывал Пролог как раз в то самое время, когда Турчин придумывал прогонку. И, естественно, друг о друге они нечего не знали.

Также есть и различия на содержательном уровне:

  • Пролог основан на "унификации", а унификация симметрична по отношению к своим двум аргументам. А прогонка основана на "обобщённом алгоритме отождествления", который асимметричен, и представляет собой сопоставление выражения, содержащего переменные с образцом. При этом "сужения" (подстановки) над образцом не выполняются, а переменные из образца "принимают значения".

  • Турчин следовал своей методологии "метасистемного перехода". Сначала - основной уровень (т.е. Рефал и обычные вычисления), затем - метауровень (на котором наблюдается и изучается поведение основной системы). Поэтому, прогонка - это надстройка над "обыкновенным" функциональным языком программирования (Рефалом). Программист может сначала написать и отладить программу на традиционном языке и традиционными методами. А после этого - засунуть эту программу в прогонку.

  • Колмэрауе пошёл другим путём. Он объявил, что нужно отказаться от традиционных (неправильных?) языков и методов программирования, и заменить их на новый (правильный?) язык Пролог. Наверное, он при этом надеялся, что программистское сообщество удастся уговорить свернуть с неправильной кривой дорожке на правильную. И многих уговорить удалось! Но не всех... И для этих "не всех" подход Турчина, наверное, выглядит не таким экстремистским, как у Колмэрауе.

Понятно, что прогонка - это только первый шаг на пути построения интересной метасистемы. И Турчин активно занялся дальнейшим продвижением на пути к суперкомпиляции. Но вскоре возникли небольшие затруднения, связанные не с самой научной работой, а, скажем так, с "объемлющей метасистемой" (в виде тогдашних начальников СССР). В результате, Турчина сначала выгнали с работы, а потом и вовсе предложили выбирать: либо поехать далеко-далеко на Восток, либо поехать далеко-далеко на Запад. Вот так Турчин и превратился из советского/российского учёного в американского...

Но не будем отвлекаться от темы, т.е. суперкомпиляции.

 

Итак, Турчин был изгнан с работы. В нынешнее время, когда стало модно получать зарплату "в конвертиках", даже трудно осознать суть проблемы. Нет "официальной работы" - всегда можно найти частную лавочку, в которой - понятно что... Но в эпоху "развитого социализма" частных лавочек не существовало. "Выгнать с работы" означало на самом деле ещё и лишить возможности найти работу в другом месте.

Нет работы - нет зарплаты. А если рассматривать научный аспект, то нет работы - нет возможности что-то опубликовать, поскольку опубликовать научную статью можно было только проделав некоторые обязательные действия по месту работы!

Поэтому, между 1974 и 1979 годами - публикаций нет. Точнее, в 1977 году Турчину удалось-таки (с помощью маленькой военной хитрости) опубликовать аж 4 (!) страницы, посвящённых суперкомпиляции. Вот они, эти страницы, однако:

  • Базисный Рефал и его реализация на вычислительных машинах. М.: ЦНИПИАСС, 1977. - 258 с. Стр. 92-95 DJVU , PDF

Анекдотическая подробность этого дела заключалась в том, что книга, в довершение всего, была издана вообще анонимно(!). Кроме этого, все авторы получили официальные справки, что да, они являются авторами таких-то и таких-то страниц. У меня до сих пор лежит моя справка (в качестве диковинки, передающий "аромат эпохи"):

  • Подтверждение авторства С.А.Романенко... DJVU PDF

(Во избежание недоразумений сразу поясню, что дурь исходила от высшего начальства, а вовсе нет от тех людей, которые выписывали и подписывали эти справки. Они-то как раз делали всё, что могли, чтобы хоть как-то протащить книгу в печать.)

Что касается 4-х страниц, вставленных потихоньку в книгу в процессе её "редакторской правки", так они - просто шедевр научной прозы. В эти 4 страницы Турчину удалось втиснуть:

  • Объяснение сущности суперкомпиляции.

  • Объяснение того, что сейчас известно как "три проекции Футамуры".

"Проекции Футамуры" - вопрос тонкий и имеющий свою интересную историю. Его нужно рассматривать отдельно. Поэтому я изложу его сейчас чисто формально и мимоходом, не рассчитывая, что моё описание будет кому-нибудь понятно (кроме тех, кто уже заранее знает, о чём речь).

Допустим, что у нас есть программа f, исходные данные для которой разбиты на две части: "статическую" часть x и "динамическую" часть y. Обозначим через f(x,y) результат применения программы f к исходным данным (x,y). Тогда "специализатором" называется такая программа s, что

  • s(p,x)(y) = p(x,y)

Допустим, что p - программа, результатом применения которой к исходным данным d является p(d). Тогда  "интерпретатором" называется такая программа i, что

  • i(p,d) = p(d)

Теперь, используя свойства s и i, мы можем построить такую цепочку равенств:

  • p(d) = i(p,d) = s(i,p)(d) = s(s,i)(p)(d) = s(s,s)(i)(p)(d)

из которой легко(!) видно, что

  • s(i,p) - скомпилированная программа.

  • s(s,i) - скомпилированный компилятор для языка, который интерпретирует i.

  • s(s,s) - скомпилированный компилятор компиляторов (преобразующий интерпретаторы в компиляторы).

Впрочем, как потом выяснилось,  Ёсихико Футамура додумался до s(i,p) и s(s,i) ещё в 1971 году:

Yoshihiko Futamura: Partial Evaluation of Computation Process --- An approach to a Compiler-Compiler, Higher-Order and Symbolic Computation, Volume 12, December 1999, pp381-391. (Reproduction of the 1971 paper). PDF

Про s(s,s) он в этой статье не было ничего сказано, и это выглядело загадочно: первые 2 шага сделал - а почему не сделал третий? Не догадался? Но потом обнаружился некий отчёт, в котором s(s,s) было выписано. Так что, загадка только усугубилась. Если знал и понимал, почему не упомянул в статье про s(s,s)?

В общем, как бы то ни было, Футамура действительно самым первым додумался и до s(i,p), и до s(s,i), и до s(s,s). Поэтому, в соответствии с научными традициями и принципами справедливости, эти штуки и называются "проекциями Футамуры".

Однако же, когда Футамура опубликовал свою статью в 1971, на неё никто не обратил внимания. И только когда другие "дозрели" и начали додумываться до того же самого, эта статья была замечена и оценена.

Можно ли считать, что Футамура изобрёл и суперкомпиляцию? Вопрос тонкий. Для практической реализации проекций Футамуры полноценная суперкомпиляция не нужна: вполне достаточно "частичных вычислений". А с точки зрения суперкомпиляции, частичные вычисления - это некоторый экстремальный, вырожденный случай суперкомпиляции. Ну, а для особого, частного случая, можно использовать и особые методы, специально "заточенные" под этот общий случай. Вот и получается, что по решаемой задаче, частичные вычисления это частный случай суперкомпиляции, а по применяемым методам - различаются. Впрочем, это - отдельная интересная тема... А мы вернёмся к суперкомпиляции.

К сожалению, с 1979 года приходится читать уже на английском языке...

Оказавшись в Нью-Йоркском Городском университете, Турчин, первым делом, занялся публикацией того, что ему не удалось втиснуть в 4 страницы, пока он находился в СССР:

  • Turchin, V.F. The Language REFAL, the Theory of Compilation, and Metasystem Analysis. Courant Institute Report #20, New York, 1980. DJVU , PDF

  • Turchin, V. F. 1980. The Use of Metasystem Transition in Theorem Proving and Program Optimization. In Proceedings of the 7th Colloquium on Automata, Languages and Programming (July 14 - 18, 1980). J. W. Bakker and J. v. Leeuwen, Eds. Lecture Notes In Computer Science, vol. 85. Springer-Verlag, London, 645-657.
  • Turchin, V. F., Nirenberg, R. M., and Turchin, D. V. 1982. Experiments with a supercompiler. In Proceedings of the 1982 ACM Symposium on LISP and Functional Programming (Pittsburgh, Pennsylvania, United States, August 15 - 18, 1982). LFP '82. ACM, New York, NY, 47-55. DOI= http://doi.acm.org/10.1145/800068.802134

Все публикации существуют в оцифрованном виде. К сожалению, по правилам игры, установленным правообладателями, кое-что не находится в открытом доступе. Впрочем, некоторые проницательные люди утверждают, что запустив eMule/aMule из сделав поиск по ключевым словам "Turchin" и "supercompilation", кое-что можно найти...)

Ну вот, можно считать, что мы подошли к концу ранней истории суперкомпиляции. Хороший обзор этого периода можно найти у Сёренсена:

  • Morten Heine Sørensen. Turchin's Supercompiler Revisited. Master's thesis, Department of Computer Science, University of Copenhagen, 1994. DIKU-rapport 94/17. PDF

Сёренсена заинтересовал следующий вопрос. У Турчина суперкомпиляция всегда рассматривалась применительно к языку Рефал. Верно ли, что суперкомпиляция применима только к Рефалу. Как и следовало ожидать, оказалось, что можно и без Рефала. Можно и объяснить без Рефала. Что Сёренсен и сделал. Так сказать, реализовал лозунг: "Без царя - а правительство рабочее!"

Постоянные читатели