четверг, 23 июля 2015 г.

ICFPC-2014: Соло для LambdaMan'а на SECD-машине с квартетом императивных Призраков

Лямбдами же чё хочешь можно закодировать:
хоть ДНК для Endo, хоть управление для Hohmann Transfer,
хоть стратегию для робота в лабиринте… дело такое
Народная мудрость

Dreaming of the Crash

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

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

Начнём с организаторов. Nicolas `zenzike` Wu — один из оргов-сопредседателей — имеет среди своих публикаций следующий замечательный труд: Pure and Lazy Lambda Mining: An Experience Report. В соавторах этой работы фигурирует также еще один орг — José Pedro `dreixel` Magalhães. Сама публикация, понятно, посвящена добыванию лямб роботом в ICFPC-2012. Полагаю, все в курсе.

Другой сопредседатель — Duncan `dcoutts` Coutts — довольно-таки известный персонаж в мире haskell.

Об остальных оргах известно меньше, но, например, Martin `mariusz` Lester пишет о себе, что "was part of the team ... that won the ICFP Programming Contest in 2011"...

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

Cornfield Chase

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

В качестве базовых ингредиентов взял ghc-7.8, Emacs и haskell-mode. Добавил туда stylish-haskell, structured-haskell-mode, flycheck-hdevtools и немножко HaRe. Но где-то что-то не рассчитал — а может старые книги врут с составом, — спалил к чёртовой матери макбук и уехал в отпуск лазить по скалам...

Dust

Уже который год орги ICFPC не оставляют Philip Wadler в покое. Вот и в этот раз в официальном твиттере был опубликован такой вот Lambda-Man:
Это кадр из феерической презентации на QCon 2012 в Лондоне.

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

% = \...o.....

Выглядело чертовски похоже на биндинг некоторого лямбда-выражения с пропусками вместо подтермов и переменных. Правда не совсем понятно было, что за "%" слева от "=". К чему 3072? И при чём тут фрукты?

Последние несколько дней перед стартом мы с Лёхой по-полной проникались атмосферой контеста, разгадывали хинты оргов, обсуждали современный c++, прошлогодние SMT-солверы, алгоритмическую разрешимость евклидовой геометрии и философские основания нашего участия в ICFPC.

Day One

В пятницу в четыре часа дня счётчик на странице контеста пересекает 0 снизу вверх и через несколько секунд там появляется Спецификация Проблемы. Сейчас она, как и всякие дополнительные материалы доступна в официальном репозитории.

Первым делом я её отправляю на принтер — незначительная деталь, которая непременно сыграет свою роль в дальнейшем повествовании, — и углубляюсь в чтение. Следующий час проходит за изучением 30-страничного документа и сумбурной перепиской по скайпу.

На первый взгляд задание подозрительно напоминает тот самый Lambda Mining 2012-го года, о котором с ностальгией пишет в своей работе г-н Wu. Снова нужно управлять какой-то хреновиной в двумерном лабиринте, чего-то собирать, от чего-то уворачиваться.

- AP> опять eating pills and evading ghosts?
Дальнейшее чтение показывает, что ностальгируют орги не только по 2012-ому, но и по 80-ым (полностью поддерживаю). Особенно по компьютерным играм (ой, да ладно?).

Что мы имеем? Злые призраки (":=") и гоняются по лабиринту за хорошим LambdaMan'ом ("λ"). Код призраков исполняется на более или менее классическом 8-битном "микроконтроллере" GHC (GHost Cpu) с минималистичным набором инструкций. 256 байт на код, 256 байт на данные, 1024 такта на принятие решения на каждом ходу, ввод/вывод через прерывания.

С LambdaMan'ом ситуация интереснее. Задание содержит подробную многостраничную спецификацию его "микропроцессора" GCC (General Computing Processor), построенного на 4 регистрах: S(tack), E(nvironment), C(control) и D(ump). Микропроцессор имеет частоту 3.072Mhz и исполняет одну инструкцию за такт. Программе доступно 10^6 CONS-ячеек памяти. Одна "минута" машинного времени на инициализацию, одна "секунда" — на ход. К счастью, орги дали не только референсную реализацию GCC, но и симулятор Игровой Механики на js.

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

Лёха первым понимает, что речь идёт о PacMan'е, а я — что о SECD-машине.

Правда с SECD-машиной я в последний раз сталкивался году этак в 2007-ом, когда только начинал изучать FP. А PacMan — это вообще из детства. Т.ч. толку от такого знания было мало.

Тем не менее вырисовывается план действий. Нам предстоит научиться программировать на ассемблере SECD-машины им. тов. Лэндина. Мы даже собираемся что-нибудь засабмитить в лайтнинг. А после этого, предстоит, очевидно, разработка компилятора.

План был хорош, но при попытке его воплощения в жизнь сразу полезли некоторые трудности. Во-первых, выяснилось, что референсная реализация не понимает ассемблер с метками и комментариями. Т.ч. нам требуется Макропроцессор. Во-вторых, Лёха наотрез отказывается параллелить задачи и доверить мне разработку Компилятора ("я тоже хочу повозиться с компилятором") или Стратегии (чем мотивировал — не помню). В итоге решили, что я запилю по-быстрому Макропроцессор, а там видно будет.

Макропроцессор я пилил аж 5 с лишним часов... Вместо того, чтобы сделать всё по-простому на awk, я сразу запилил под это дело целый проект на haskell'е. Идея была в том, чтобы выдавать нормальные сообщения об ошибках (строка, позиция) в исходнике. В конечном итоге это сработало, но поначалу я тупанул и вооружился attoparsec'ом. Только для того, чтобы через 2 часа, уже выписав грамматику со всеми комментариями и метками, выяснить, что из него нельзя достать текущую позицию токена. Пришлось всё срочно переписывать на parsec'е... Ну и была еще пара фейлов помельче.

В общем в 12-ом часу ночи новенький сияющий Макропроцессор был интегрирован в master-ветку репозитория. Жаль, что ошалевший к тому времени от отсутствия комментариев и меток Лёха не был способен оценить мою гордость. Впрочем, Макропроцессор этот выдержал test-of-time и был использован во всех наших решениях, как в течение контеста, так и по его окончании.

Пока я развлекался с Макропроцессором, Лёха "тихо ненавидел html и javascript". Он потихоньку экспериментировал с ассемблером SECD-машины и копался в потрохах референсных симуляторов (скомпилированных ghcjs, ага...)

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

Stay

Суббота начинается с того, что Лёха долго выясняет, как проехать ко мне на работу и где правильно парковать машину, а потом не приезжает. Какой-то у него там форс-мажор. Меня это не слишком расстраивает, потому что план на день совершенно понятен. С лайтнингом мы были, очевидно, в пролёте, поэтому предстоит делать компилятор для SECD-машины. А где SECD-машина, там, естественно, Lisp. Не, ну была одна безумная идея с хаскелем... Впрочем, не будем забегать вперёд.

Можно было даже реализовать подмножество какого-нибудь стандартного лиспа, типа clojure или EmacsLisp (Лёха заморочился-таки с racket'ом). Это позволило бы отлаживать какие-то элементы Стратегии вообще не имея компилятора. Но я от этой идеи отказался, потому что распараллелить работы по созданию Компилятора и Стратегии мне было не с кем, а искусственно завязываться на определённый синтаксис не хотелось.

Message from Home

К 12 на связи появляется Лёха, который доехал-таки к себе на работу. Поначалу он продолжает заниматься ассемблерными разработками. С появлением макропроцессора дела у него идут получше и он всё чаще присылает мне какие-то адские простыни с комментариями в духе "смотри, какой читаемый код!" Вроде такой:
; Мне нужна одна локальная переменная t, поэтому создаю окружение из одной переменной
DUM 1
LDC 0
LDF init
RAP 1
STOP
init:
; Здесь мы получаем замыкание с локальными переменными в родительском стеке
DUM 2 ; x, y
LDC 1 ; x = 1
LDC 2 ; y = 2
LDF getsum
RAP 2
; В этом месте нам вернули в качестве параметра функцию sum z, у которой также захвачены переменные x и y
ST 0 0 ; t := sum 1 2
LDC 10
LD 0 0
AP 1 ; sum 10
RTN
; get_sum x y
getsum:
; В этой функции 2 локальных переменных: x,y
LD 0 0
LD 0 1
CONS
DBUG
; Теперь захватываем родительский контекст и возвращаем наружу адрес функции sum z
LDF sum
RTN
sum:
; Функция sum принимает 1 параметр z, извлекает из родительского стека x и y, возвращает x+y+z
LD 1 0
LD 1 1
LD 0 0
CONS
CONS
DBUG
LD 1 0
LD 1 1
LD 0 0
ADD
ADD
RTN
Совершенно очевидно... эээ... что здесь демонстрируется владение такими концепциями как замыкания (LDF), различные механизмы вызова функций (AP/RAP+DUM), мутабельные переменные (ST) итп.

The Wormhole

Меня это к тому времени не очень впечатляет, так как к половине второго я уже умею компилировать арифметические выражения.
echo "(defun add (x y) (+ x y))" | dist/build/lmc/lmc compile
LD 0 0
LD 0 1
ADD
RTN
Петля положительной обратной связи затягивается, я только успеваю добавлять новые фичи в lmc: определение (defun) и вызов (apply) функций, булевские переменные и операторы... Они, как правило, сразу работают. Но не все.

Mountains

Первым камнем преткновения стал... if. Мало того, что для правильной генерации меток, пришлось протащить через весь "чистый" (ну не переписывать же в самом деле из-за этого всё на монады?) код состояние — счётчик меток, — так еще и пришлось ввести понятие "блока" кода. Дело в том, что специальная форма (if b t f) моделируется, по сути, как
  compile(b)
  SEL t_xxx, f_xxx
  ...
t_xxx:
  compile(t)
  JOIN
f_xxx:
  compile(f)
  JOIN
И если раньше compile возвращал один блок просто как список инструкций (с символьными метками) и такие списки просто конкатенировались в программу, то теперь стало нужно куда-то приткнуть список инструкций, начинающийся с метки t_xxx. Если бы я к тому моменту знал, что в списке инструкций есть jmp (замаскированный под TSEL), то можно было бы влепить этот блок перед compile(b) и перепрыгнуть его. Но я поленился внимательно прочитать про хвостово-рекурсивные расширения SECD-машины и мне пришлось изменить compile так, чтобы он мог возвращать два блока: основной и дополнительный. Дополнительные блоки лепились в конце программы (а точнее — в конце top-level выражения).

TLDR, некоторые баги в этом механизме смогли продержаться еще как минимум сутки :-)

Afraid of Time

Тем не менее, за 15 минут до окончания лайтнинга я "почти умею" считать элементы ряда Фибоначчи. Ну, или, точнее вот так:
(defun main () (apply fib 4))
(defun fib (n) (if (<= n 1) 1 (+ (apply fib (- n 1)) (apply fib (- n 2)))))
Остаток дня занимаюсь добавлением поддержки таплов (tuple/nth), списков (list/car/cdr) и всяких мелочей, вроде отладочной печати. Заодно читаю, наконец, про pascal extensions и закрываю для себя другие белые пятна Спека.

В метро по дороге домой до меня доходит, что на базе TSEL легко организовать jmp, которого так не хватало весь день.

А по приезду домой я еще замутил поддержку let-выражений — давно подозревал, что для этого будет достаточно defun/apply, — и прикрутил магический оператор rap для интерфейса процессор-сопроцессор. Это позволяло исполнять программы не только в симуляторе GCC, но и в симуляторе Игровой Механики, т.е. подступиться к созданию Стратегии.

A Place Among the Stars

К этому моменту основные черты языка Poor LambdaMan Lisp уже утряслись и хочется по ним быстренько пройтись. Должен сказать, что изобретение своего лиспа оказалось довольно увлекательным занятием. По крайней мере более увлекательным, чем последующее программирование на нём...

Во-первых, нужно было понять, каков будет порядок редукции. Я пошёл по пути наименьшего сопротивления и остановился на строгой семантике — аргументы вычисляются слева направо перед вызовом функции. Это хорошо ложилось на SECD-машину. Впрочем, ввиду отсутствия сайд-эффектов — а внедрять "pascal extensions" я не собирался, — это было не слишком важно.

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

А дальше — полная свобода. Например, поддерживать или не поддерживать каррирование и частичное применение функций? Я согласился с предложением Лёхи "забить сразу", т.к. не придумал лучшей реализации, чем такая (на примере функции __eq из компилятора ghclm, см. ниже):

__eq_curry:
  LD 1 0
  LD 0 0
  CEQ
  RTN
__eq:
  LDF __eq_curry
  RTN

Ну, а дальше пошло-поехало. Вместо паттерн-матчинга — распаковка с помощью let и nth/car/cdr. Никаких лямбда-функций. Вообще никаких вложенных функций: только top-level, только хардкор. Соответственно, и замыкания не нужны. О системе типов я вообще молчу, потому что даже соответствие числа формальных и фактических аргументов при вызове функции не проверяется...

Надеюсь, происхождение названия языка теперь понятно. Ладно, вот пример.

Фрагмент

fold (f env) x0 xs
  where f env ...
превращается в элегантное
(car (fold f (tuple x0 env) xs)
...
(defun f (acc y)
  (let ((x   (nth 0 acc))
        (env (nth 1 acc))
  ...
)
т.к. контекст env приходится протаскивать через аккумулятор fold'а. Особенно это доставляет, если нужно добавить еще одно поле в AIState.

Running Out

Если хотите утром сразу включиться в работу, то надо с вечера себе оставить какую-нибудь маленькую приятную задачку.

У меня в воскресенье такой задачкой была реализация fold'а. Конечно, fold можно просто сделать функцией и дело с концом. Но т.к. lmc не поддерживает хвостовую рекурсию, то такая реализация мне не нравится. Вместо этого я решаю сделать fold специальной формой. Она заводит себе на стэке DUMMY-контекст для значения аккумулятора и далее использует pascal extension ST для его обновления и TSEL для реализации цикла. Дёшево и сердито.

I'm Going Home

После появления fold'а, я реализую Первую Тривиальную Стратегию LambdaMan'а, которая заставляет его двигаться строго курсом 0:
(rap)
(defun init (world undoc f)
  (tuple 0 f))

(defun step (state world)
  (tuple state 0))
Практического смысла в этой стратегии, понятное дело, никакого нет. Зато есть очень важный символический смысл. Она есть доказательство того, что теперь можно писать код не только под симулятор GCC, но и в связке с Игровой Механикой.

Вторая Тривиальная Стратегия исполняет заданную программу команд:

(rap)
(defun init (world undoc f)
  (tuple
   (list
    (list 1 1 1 1 1 1 2 2 1 1 0 0 1 1)
    (list)
   f))

(defun step (state world)
  (let
   ((moves (nth 0 state))
    (rmoves (nth 1 state)))
   (tuple
    (list
     (cdr moves)
     (list (car moves) rmoves))
    (car moves))))
Я привожу эту Стратегию целиком, потому что она стала основой для всех будущих построений. Особое внимание следует обратить на rmoves, в котором сохраняется информация, необходимая для возврата в исходную точку.

Coward

Идея отступления лежит в основе моей единственной эвристики по работе с призраками. Заключается она в том, чтобы при виде призрака в клетке, куда собирается наступить LambdaMan, развернуться и трусливо бежать (кроме тех случаев, когда ГГ обдолбался powerpile'ом, конечно).

Параметр rmoves в AIState как раз и является заготовкой под эту логику. Позже в нём будет сохраняться непосредственно список команд, т.ч. для режима паники будет достаточно поменять в нужный момент списки moves и rmoves местами. Et voilà!

Detach

На этом тривиальные конструкции закончились и нужно было строить более содержательную Стратегию. Однако строить было не из чего. Поэтому воскресенье я посвятил разработке Стандартной Библиотеки.

Естественно, начать пришлось со всяких примитивов: length, reverse, splitAt итп. В процессе полезли всякие грабли в компиляторе, которые приходилось тут же исправлять. К вечеру я добрался уже до таких абстракций, как двоичные деревья, которые заменяли мне массивы за неименением последних. Правда деревья были несбалансированными, а на практике так даже и всегда вырожденными в списки. Но я себе честно поставил TODO это исправить, если потребуется...

Наконец, в 9 вечера случился первый прорыв в области Стратегии. Для каждой точки карты я научился вычислять список всех возможных направлений движения из неё. Представлялось это "двумерным массивом" ([вырожденным] бинарным деревом [вырожденных] бинарных деревьев) списков. Теперь, зная пару координат (j,i) можно было получить список всех возможных направлений движения из данной точки карты (maze) с помощью:

(apply ij i j maze)
К 11 я уже умел находить на карте все "интересные" точки: пересечения, тупики и стартовую позицию LambdaMan'а. А к двум часа ночи для каждой пары смежных точек у меня была "программа перехода" между ними. Идея была в том, чтобы принимать решения только в узлах графа, вершинами которого являются "интересные" точки, а рёбрами — "программы перехода". В промежуточных точках, не являющихся интересными, нужно было просто исполнять программу, не забывая поглядывать под ноги, чтобы не наступить на призрака.

S.T.A.Y.

Следует, наверное, пару слов сказать о призраках. Хотя, если често, по причинам, которые станут полностью ясны позднее, мне про призраков сказать особо нечего. Все мои знания почёрпнуты из чтения спека и отчётов других команд.

Через систему прерываний призраки могут задавать своё новое направление движения и получать информацию об окружающей обстановке: содержимое ячеек карты, положение LambdaMan'а, положение второго LambdaMan'а (подразумевался multiplayer?), свой порядковый номер, стартовые и текущие координаты любого из призраков, их текущее состояние и направление движения.

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

На этих фактах, например, основана приведённая в reference materials программа призрака fickle.ghc:

; Keep track of how long we have spent travelling in each direction.
; Try to go in the direction we've travelled in least.

               ; Count of time spent going in direction 0 is in memory address 0, and so on.
mov a,255      ; A is the min value.
mov b,0        ; B is the corresponding direction.
mov c,255      ; C is the candidate direction for the new min.
               ; Start of loop.
inc c          ; Pick new direction.
jgt 7,[c],a    ; Jump if count of direction C is above best so far.
               ; We have a new min.
mov a,[c]      ; Save new min.
mov b,c        ; Save direction.
jlt 3,c,3      ; Jump target. Loop back if we have not tried all 4 directions.

mov a,b        ; Actually set desired direction.
int 0

int 3          ; Get our ghost index in A.
int 6          ; Get out current direction in B.
inc [b]        ; Increment corresponding count.
hlt            ; Stop.
По сути, здесь реализована "условно-рандомная" стратегия-бродилка, которая всё время пытается пойти по тому направлению, по которому призрак ходил реже всего. Понятно, что с такими призраками каши не сваришь.

Наиболее state-of-the-art призрачные решения, встречающиеся в отчётах команд, не только реализуют алгоритмы преследования (и уклонения от) LambdaMan'а разной степени нетривиальности, но и эффективно противодействуют тенденции "кластеризации" призраков в одном коридоре, что важно и для задачи преследования, и для задачи уклонения. Встречаются также попытки охраны powerpiles и "межпризрачного взаимодействия".

Некоторые решения стараются по-максимуму использовать выделенные 1024 такта на ход, намеренно генерят невалидные направления движения или вообще ошибки исполнения программы призраков. Связано это с тем, что недокументированный параметр программы LambdaMan'а, оказывается, представляет собой список закодированных программ призраков. Были даже команды (например THIRTEEN), которые реализовали эмулятор призрачного "микропроцессора" для SECD-машины. Однако, насколько мне известно, никто из этого ничего полезного не выжал.

Where We're Going

Утро понедельника начинается с Лёхиного телефонного звонка. Зевая рассказываю ему о своих успехах и планах. Лёха бодается с вычислением функции Беллмана. Ну-ну...

У меня уже ни с чем сил бодаться нет, но — чёрт возьми! — неужто столько работы пойдёт коту под хвост?

И я иду пилить Стратегию дальше. Для начала нужно хоть как-нибудь научиться обходить граф в отсутствие призраков. По построению графа это гарантировало бы сбор всех pile'ов.

First Step

В качестве простейшей реализации алгоритма обхода выбран следующий. С каждым маршрутом ассоциируется число — количество раз, которое этот маршрут был выбран LambdaMan'ом в качестве текущей программы. При попадании в один из узлов графа LambdaMan выбирает тот маршрут, по которому ходил реже всего. Т.к. граф маршрутов у меня ориентированный, то это приводит к тому, что LambdaMan зачастую проходит некоторый коридор сначала в одну сторону, а потом сразу же в обратную. "Эффективность? Не, не слышали!..". Тем не менее, такой алгоритм набирает 1040 очков на World Classics и удостаивается комментария "WOW!!!" в git'е.

Flying Drone

Таким образом за 3.5 часа до окончания контеста свет в конце тоннеля забрезжил и у меня появилась, наконец, первая содержательная Стратегия. Первый работающий девайс, способный, если ему сильно не мешать, обойти всю карту и собрать все pile'ы и powerpile'ы.

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

Atmospheric Entry

К этому моменту Компилятор и Стандартная Библиотека практически не путаются под ногами. Баги повыпилены, структуры данных — позапилены. Конечно, Poor LambdaMan Lisp по-прежнему доставляет, но тут уже ничего поделать нельзя.

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

No Need to Come Back

Следующим изменением становится добавление счётчика посещений не только маршрутам, но и узлам. Теперь LambdaMan сортирует маршруты сначала по весу узла, а потом — по весу маршрута. Эта простенькая модификация была отлажена за какой-то час и давала 2170 очков на World Classics. Недурно.

Imperfect Lock

Далее нужно научиться что-то делать с призраками. Идея у меня была готова еще в воскресенье и за час до окончания контеста я успеваю её воплотить. Ну, вы знаете уже: режим паники, программа отступления, — вот это вот всё. Это даёт примерно 50%-ные шансы спастись при встрече с призраком на узкой дорожке.

Этого, конечно, совершенно недостаточно для настоящих Ghost Busters, но на World Classics сей нехитрый приём даёт 8700 очков, что близко ко вчерашнему максимуму лидеров на этой карте. Круто...

Кстати, уже после окончания контеста я еще улучшил это решение, заменив сортировку рёбер по числу посещений на сортировку по общему количеству оставшихся ништяков в коридоре. На World Classics эта дало 10200 очков.

No Time for Caution

Почему я вдруг говорю о вчерашнем максимуме?

Тут нас ждёт неожиданная развязка сюжета с распечаткой задания. Дело в том, что весь понедельник у меня не было интернета (пользуясь случаем передаю привет провайдеру). Поэтому получив 8700 очков на World Classics практически за час до окончания контеста, я расслабился и стал неторопясь готовить submission... по распечатанному пятничному рецепту, т.е. по правилам lightning round'а...

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

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

Забавно при этом, что Лёха не успел выложить своё решение до дедлайна, но успел выложить хэш. В итоге орги связались с ним и приняли его решение.

What Happens Now?

Настало время рассказать об еще одной замечательной идее, которая нашла своё воплощение лишь после завершения контеста. Идея состояла в том, чтобы делать не компилятор Lisp'а, а компилятор Haskell'а... Да, ровно так: компилятор подмножества языка хаскелл для SECD-машины.

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

Согласно AOS и GHC Wiki есть как минимум три "точки расширения" в GHC, пригодных для нашей цели. Во-первых, это API, позволяющий использовать GHC как библиотеку. Во-вторых, возможность реализовать свой кодогенератор для компиляции из Cmm. В-третьих, возможность реализовать свой Core-to-Core Pass плагин.

Меня больше всего заинтересовал последний вариант, т.к. из всех представлений хаскельной программы именно GHC Core проще всего транслировать в код SECD-машины. Опять же, плагин для Core-to-Core преобразования элементарно подключается к GHC ключом командной строки.

Так появился на свет проект ghclm. Плагин к GHC, реализующий тождественное Core-to-Core преобразование с небольшим сайд-эффектом в виде .gcc-файла.

Не могу сказать, что познал все тонкости System FС / GHC Core, но кучу базовых тестов по работе с числами, таплами, списками, рекурсией итп плагин честно проходит в связке с официальным GCC-симулятором, запущенным под node.js. Также, в качестве proof-of-concept я замутил рандомную стратегию-бродилку, которая компилируется и исполняется в официальной Игровой Механике. 27 часов на всё про всё, включая тесты. Совсем неплохо.

Конечно, ghclm поддерживает только тот набор типов данных, которые присутствовали в спецификации задачи: числа, 2-таплы, списки. Никаких тебе алгебраических типов данных, массивов, даже строк. Такой себе Poor LambdaMan's Haskell.

Также имеются шероховатости и в самой процедуре трансляции.

Во-первых, как и в компиляторе lmc, не поддерживается хвостовая рекурсия и другие pascal extensions. На практике не такая уж большая проблема, но можно было бы генерить несколько более эффективный код.

Во-вторых, нет поддержки дебажного вывода (DBUG). Вот это действительно неудобно.

В-третьих, есть несколько моментов, связанных со спецификой самого GHC Core. Например, для форсирования thunk'а в этом языке используется конструкция

case thunk of thunk1 {
  __DEFAULT -> ...
}
Моя же разновидность хаскеля предполагала строгую семантику, так-то толку генерить такие case'ы не было. Но до столь тонких оптимизаций руки не дошли.

Еще один пример. В GHC Core все лямбда-выражения имеют только один формальный параметр, а LambdaMan'овская SECD-машина поддерживает вызов функций от нескольких переменных (AP/RAP n). Я не стал заморачиваться, поэтому вызов многоместной функции превращается в цепочку из нескольких последовательных вызовов. А можно было бы сэкономить несколько машинных тактов и CONS-ячеек в Env'е. Зато автоматически поддерживается каррирование функций, чего так не хватало в lmc.

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

С другой стороны, плюсы разработки стратегии LambdaMan'а на почти полноценном хаскеле легко перевешивают любые неудобства. Вплоть до того, что можно использовать часть функций из Prelude и даже других библиотек. Т.ч. если бы я догадался до этого варианта в пятницу, то стоило бы попробовать.

Кстати, команда, которая провернула аналогичную афёру с OCaml'ом — gagallium, — получила judges prize :-)

Do Not Go Gentle Into That Good Night

Финальные результаты можно посмотреть в официальном репозитории. Наши с Лёхой команды называются IKS и SKY (искать ближе к концу списка...). Лёха еще и на 100 с лишним очков обойти меня ухитрился.

Да, к сожалению, из-за небольшого форс-мажора у нас случился split-brain, поэтому команд две, а не одна. Мы не смогли воспользоваться естественным разбиением Проблемы на подзадачи: Компилятор, Стандартная Библиотека, Стратегия, Призраки, — каждый делал всё. На выходе получилось два работающих компилятора, две стандартные библиотеки, две независимые стратегии и ни одного призрака.

Натупили мы напару тоже немало. Взять хотя бы то, что Лёха реализовал подмножество racket'а, но не сообразил, что можно отлаживать Стратегию (или хотя бы Стандартную Библиотеку) на racket'е же. Это как, вообще?! Ха-ха-ха! Я уж молчу про то, что компилятор лиспа в SECD-машину на лиспе гуглится ровно за секунду... Мда.

Отчёты участников в этот раз довольно унылые, честно говоря (или я что-то пропустил?). У _adept_'а, вон, вообще не задался :-) Т.ч. упомяну только отчёт обладателя first prize — Supermassive Black Hom-set и отчёт обладателя judges prize — gagallium.

Зато отжигают организаторы.

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

Ну, тренд понятен... Ждём теперь тетрис для НАМ, sokoban для КАМ, minesweeper для МП итд :-)

The Imitation Game

Ну и пользуясь случаем вангую, что в этом году организатором ICFPC-2015 будет небезызвестная контора Galois.

Да-да, high-assurance cryptography, вот это вот всё. Так что дружно ботаем Cryptol и Co, пока еще есть немного времени.

Анонс в любом случае многообещающий...